import React from 'react';
import { createRoot } from 'react-dom/client';
// Automatically reload this view if certain other fragments change
const changeFragments = [
'#App [data-type="text/javascript+babel"]'
];
// Start the app
async function render() {
if (!window.cachedAppRoot) {
let element = document.createElement('transient');
element.id = 'app-root';
document.body.appendChild(element);
window.cachedAppRoot = createRoot(element);
}
let content = await Fragment.one('#App .app').require();
window.cachedAppRoot.render(React.createElement(content.App));
};
let reloadTimer = null;
const reload = () => {
clearTimeout(reloadTimer);
reloadTimer = setTimeout(async function reloadReact() {
try {
render();
} catch (ex) {
console.log(ex);
}
}, 1000);
};
changeFragments.forEach(frag => {
let lookedUpFragments = Fragment.find(frag);
lookedUpFragments.forEach((lookedUpFragment) => {
lookedUpFragment.registerOnFragmentChangedHandler(() => {
if (fragmentSelfReference.auto) {
reload();
}
});
});
});
reload();
import React, { createContext, useContext, useRef, useState, useCallback, useEffect } from 'react';
const EditorCtx = createContext(null);
export function useEditor() {
const ctx = useContext(EditorCtx);
if (!ctx) throw new Error('useEditor must be used inside <EditorProvider>');
return ctx;
}
export function EditorProvider({ children }) {
const editorRef = useRef(null);
const [menu, setMenu] = useState(null);
const [actions, setActions] = useState([]); // {id} only — just for ordering
const [promptModal, setPromptModal] = useState(null);
// Stable storage that does NOT trigger re-renders when it changes
const reg = useRef({
counts: new Map(), // id -> number (StrictMode ref counting)
labels: new Map(), // id -> ref<label>
handlers: new Map(), // id -> ref<handler>
shouldShows: new Map(), // id -> ref<shouldShow>
}).current;
const registerAction = useCallback((id, labelRef, handlerRef, shouldShowRef) => {
setActions(prev => {
const count = (reg.counts.get(id) || 0) + 1;
reg.counts.set(id, count);
// StrictMode safety: only insert into list on first real mount
if (prev.some(a => a.id === id)) return prev;
reg.labels.set(id, labelRef);
reg.handlers.set(id, handlerRef);
if (shouldShowRef) reg.shouldShows.set(id, shouldShowRef);
return [...prev, { id }];
});
return () => {
const count = (reg.counts.get(id) || 1) - 1;
if (count <= 0) {
reg.counts.delete(id);
reg.labels.delete(id);
reg.handlers.delete(id);
reg.shouldShows.delete(id);
setActions(prev => prev.filter(a => a.id !== id));
} else {
reg.counts.set(id, count);
}
};
}, [reg]);
const openMenu = useCallback((x, y, context) => setMenu({ x, y, context }), []);
const closeMenu = useCallback(() => setMenu(null), []);
const openPromptModal = useCallback((context, executePrompt) => setPromptModal({ isOpen: true, context, executePrompt }), []);
const closePromptModal = useCallback(() => setPromptModal(null), []);
const getLabel = useCallback((id) => reg.labels.get(id)?.current, [reg]);
const getHandler = useCallback((id) => reg.handlers.get(id)?.current, [reg]);
const getShouldShow = useCallback((id) => reg.shouldShows.get(id)?.current, [reg]);
return (
<EditorCtx.Provider value={{
editorRef, registerAction, openMenu, closeMenu,
menu, actions, getLabel, getHandler, getShouldShow,
promptModal, openPromptModal, closePromptModal,
}}>
{children}
</EditorCtx.Provider>
);
}
export function useContextMenuItem(id, label, handler, shouldShow) {
const { registerAction } = useEditor();
// These refs are reassigned every render so they stay fresh,
// but they NEVER trigger the useEffect below.
const labelRef = useRef(label);
const handlerRef = useRef(handler);
const shouldShowRef = useRef(shouldShow);
labelRef.current = label;
handlerRef.current = handler;
shouldShowRef.current = shouldShow;
useEffect(() => {
return registerAction(id, labelRef, handlerRef, shouldShowRef);
// Only the stable ID (and the stable registerAction) matter now.
// Inline arrow functions in your plugins are safe.
}, [id, registerAction]);
}// ------------------------------------------------------------
// 1. Caret & word selection
// ------------------------------------------------------------
export function getCaretRangeFromPoint(x, y) {
if (document.caretPositionFromPoint) {
const pos = document.caretPositionFromPoint(x, y);
if (!pos) return null;
const r = document.createRange();
r.setStart(pos.offsetNode, pos.offset);
r.setEnd(pos.offsetNode, pos.offset);
return r;
}
if (document.caretRangeFromPoint) {
return document.caretRangeFromPoint(x, y);
}
return null;
}
export function selectWordAt(range) {
if (!range || !range.collapsed) return range;
const node = range.startContainer;
if (node.nodeType !== Node.TEXT_NODE) return range;
const text = node.textContent;
let start = range.startOffset;
while (start > 0 && !/\s/.test(text[start - 1])) start--;
let end = range.startOffset;
while (end < text.length && !/\s/.test(text[end])) end++;
const r = document.createRange();
r.setStart(node, start);
r.setEnd(node, end);
return r;
}
// ------------------------------------------------------------
// 2. DOM helpers
// ------------------------------------------------------------
export function getFlatOffset(targetNode, targetOffset, ancestor) {
let count = 0;
const walker = document.createTreeWalker(ancestor, NodeFilter.SHOW_TEXT);
let n;
while ((n = walker.nextNode())) {
if (n === targetNode) return count + targetOffset;
count += n.textContent.length;
}
return count;
}
export function getParagraphElement(node, root) {
let el = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
while (el && el !== root) {
const tag = el.tagName?.toLowerCase();
if (['p','li','div','h1','h2','h3','h4','h5','h6','blockquote','section','article'].includes(tag)) {
return el;
}
el = el.parentElement;
}
return root;
}
export function findNodeAndOffsetAtChar(paragraphElement, charOffset) {
let count = 0;
const walker = document.createTreeWalker(paragraphElement, NodeFilter.SHOW_TEXT);
let n;
while ((n = walker.nextNode())) {
const len = n.textContent.length;
if (count + len >= charOffset) {
return { node: n, offset: charOffset - count };
}
count += len;
}
// If offset is at the very end
if (n) {
return { node: n, offset: n.textContent.length };
}
return null;
}
// ------------------------------------------------------------
// 3. Mutation helpers
// ------------------------------------------------------------
export function replaceRange(range, text) {
// operate immediately while the range is still valid
range.deleteContents();
const tn = document.createTextNode(text);
range.insertNode(tn);
range.setStartAfter(tn);
range.collapse(true);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
export function wrapRange(range, tagName) {
const el = document.createElement(tagName);
el.appendChild(range.extractContents());
range.insertNode(el);
const sel = window.getSelection();
const r = document.createRange();
r.selectNode(el);
sel.removeAllRanges();
sel.addRange(r);
}
// ------------------------------------------------------------
// 4. Context helpers (simplified)
// ------------------------------------------------------------
function findSentenceBounds(full, caret) {
let s = caret;
while (s > 0 && !'.!?'.includes(full[s - 1])) s--;
let e = caret;
while (e < full.length && !'.!?'.includes(full[e])) e++;
if (e < full.length && '.!?'.includes(full[e])) e++; // include terminator
return { start: s, end: e };
}
export function getContextInfo(range, root) {
const containerElement = getParagraphElement(range.commonAncestorContainer, root);
if (!containerElement) return null;
const full = containerElement.textContent;
const selStart = getFlatOffset(range.startContainer, range.startOffset, containerElement);
const selEnd = getFlatOffset(range.endContainer, range.endOffset, containerElement);
const beforeBounds = findSentenceBounds(full, selStart);
const afterBounds = findSentenceBounds(full, selEnd);
const contextStart = beforeBounds.start;
const contextEnd = afterBounds.end;
return {
text: full.slice(contextStart, contextEnd),
selectionStart: selStart - contextStart,
selectionEnd: selEnd - contextStart,
containerElement,
_docStart: contextStart,
_docEnd: contextEnd,
};
}
export function replaceContextRange(ctxInfo, newText) {
const { containerElement, _docStart, _docEnd } = ctxInfo;
const startInfo = findNodeAndOffsetAtChar(containerElement, _docStart);
const endInfo = findNodeAndOffsetAtChar(containerElement, _docEnd);
if (!startInfo || !endInfo) return;
const r = document.createRange();
r.setStart(startInfo.node, startInfo.offset);
r.setEnd(endInfo.node, endInfo.offset);
// Focus the editable surface first so execCommand targets the right document
let focusTarget = containerElement;
while (focusTarget && focusTarget.contentEditable !== 'true') {
focusTarget = focusTarget.parentElement;
}
if (focusTarget) focusTarget.focus();
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(r);
// Use the browser's native undo stack for this replacement
document.execCommand('insertText', false, newText);
}
import React, { useRef, useEffect, useState } from 'react';
import * as utils from '#App [name="text-utils"]';
/* ============================================================
Toolbar
============================================================ */
function exec(cmd, val = null) {
document.execCommand(cmd, false, val);
}
function ToolbarButton({ cmd, label, variant, active, title }) {
const className = [
'toolbar__btn',
active && 'toolbar__btn--active',
variant && `toolbar__btn--${variant}`,
].filter(Boolean).join(' ');
return (
<button
type="button"
className={className}
onMouseDown={(e) => { e.preventDefault(); exec(cmd); }}
title={title}
>
{label}
</button>
);
}
function ToolbarSeparator() {
return <span className="toolbar__separator" />;
}
function Toolbar({ surfaceRef }) {
const [tick, setTick] = useState(0);
useEffect(() => {
const onSel = () => setTick(t => t + 1);
document.addEventListener('selectionchange', onSel);
return () => document.removeEventListener('selectionchange', onSel);
}, []);
useEffect(() => {
const el = surfaceRef.current;
if (!el) return;
const onUp = () => setTick(t => t + 1);
el.addEventListener('keyup', onUp);
el.addEventListener('mouseup', onUp);
return () => {
el.removeEventListener('keyup', onUp);
el.removeEventListener('mouseup', onUp);
};
}, [surfaceRef]);
const isActive = (cmd) => {
try { return document.queryCommandState(cmd); } catch { return false; }
};
const blockValue = () => {
try { return (document.queryCommandValue('formatBlock') || '').toString().toLowerCase(); }
catch { return ''; }
};
const BlockButton = ({ tag, label, title }) => {
const active = blockValue() === tag;
return (
<button
type="button"
className={[
'toolbar__btn',
active && 'toolbar__btn--active',
].filter(Boolean).join(' ')}
onMouseDown={(e) => { e.preventDefault(); exec('formatBlock', tag); }}
title={title}
>
{label}
</button>
);
};
const Group = ({ children }) => (
<div className="toolbar__group">
{children}
</div>
);
return (
<div className="toolbar">
<Group>
<ToolbarButton cmd="undo" title="Undo" label={<span unapproved="" className="material-icons-outlined">undo</span>} />
<ToolbarButton cmd="redo" title="Redo" label={<span unapproved="" className="material-icons-outlined">redo</span>} />
</Group>
<ToolbarSeparator />
<Group>
<ToolbarButton cmd="bold" title="Bold" label={<span unapproved="" className="material-icons-outlined">format_bold</span>} active={isActive('bold')} />
<ToolbarButton cmd="italic" title="Italic" label={<span unapproved="" className="material-icons-outlined">format_italic</span>} active={isActive('italic')} />
<ToolbarButton cmd="underline" title="Underline" label={<span unapproved="" className="material-icons-outlined">format_underlined</span>} active={isActive('underline')} />
<ToolbarButton cmd="strikeThrough" title="Strikethrough" label={<span unapproved="" className="material-icons-outlined">strikethrough_s</span>} active={isActive('strikeThrough')} />
</Group>
<ToolbarSeparator />
<Group>
<BlockButton tag="h1" title="Heading 1" label={<span unapproved="" className="material-icons-outlined">looks_one</span>} />
<BlockButton tag="h2" title="Heading 2" label={<span unapproved="" className="material-icons-outlined">looks_two</span>} />
<BlockButton tag="p" title="Paragraph" label={<span unapproved="" className="material-icons-outlined">text_fields</span>} />
</Group>
<ToolbarSeparator />
<Group>
<ToolbarButton cmd="justifyLeft" title="Align left" label={<span unapproved="" className="material-icons-outlined">format_align_left</span>} active={isActive('justifyLeft')} />
<ToolbarButton cmd="justifyCenter" title="Align center" label={<span unapproved="" className="material-icons-outlined">format_align_center</span>} active={isActive('justifyCenter')} />
<ToolbarButton cmd="justifyRight" title="Align right" label={<span unapproved="" className="material-icons-outlined">format_align_right</span>} active={isActive('justifyRight')} />
</Group>
<ToolbarSeparator />
<Group>
<ToolbarButton cmd="insertUnorderedList" title="Bulleted list" label={<span unapproved="" className="material-icons-outlined">format_list_bulleted</span>} active={isActive('insertUnorderedList')} />
<ToolbarButton cmd="insertOrderedList" title="Numbered list" label={<span unapproved="" className="material-icons-outlined">format_list_numbered</span>} active={isActive('insertOrderedList')} />
</Group>
</div>
);
}
/* ============================================================
TextEditor
============================================================ */
export function TextEditor({ useEditor, defaultValue }) {
const { editorRef, openMenu, closeMenu } = useEditor();
const surfaceRef = useRef(null);
const initialized = useRef(false);
const saveTimer = useRef(null);
useEffect(() => {
if (surfaceRef.current && !initialized.current) {
const saved = localStorage.getItem('wpm-document');
surfaceRef.current.innerHTML =
saved ??
defaultValue ??
`<h1>Untitled Document</h1>
<p>Start writing here. Use the toolbar above to format your text.</p>`;
initialized.current = true;
surfaceRef.current.focus();
}
}, [defaultValue]);
/* ---- Debounced save to localStorage ---- */
useEffect(() => {
const el = surfaceRef.current;
if (!el) return;
const onInput = () => {
clearTimeout(saveTimer.current);
saveTimer.current = setTimeout(() => {
localStorage.setItem('wpm-document', el.innerHTML);
}, 3000);
};
el.addEventListener('input', onInput);
return () => {
el.removeEventListener('input', onInput);
clearTimeout(saveTimer.current);
};
}, []);
const handleContextMenu = (e) => {
e.preventDefault();
const sel = window.getSelection();
let range = sel.rangeCount ? sel.getRangeAt(0).cloneRange() : null;
if (!range || range.collapsed) {
const caret = utils.getCaretRangeFromPoint(e.clientX, e.clientY);
if (caret) {
range = utils.selectWordAt(caret);
sel.removeAllRanges();
sel.addRange(range);
}
}
if (!range) return;
const root = surfaceRef.current;
const ctxInfo = utils.getContextInfo(range, root);
if (!ctxInfo) return;
const context = {
text: ctxInfo.text,
selectionStart: ctxInfo.selectionStart,
selectionEnd: ctxInfo.selectionEnd,
containerElement: ctxInfo.containerElement,
replace: (newText) => utils.replaceContextRange(ctxInfo, newText),
};
openMenu(e.clientX, e.clientY, context);
};
return (
<div className="editor-page">
<Toolbar surfaceRef={surfaceRef} />
<div className="editor-container">
<div
ref={(node) => { surfaceRef.current = node; editorRef.current = node; }}
className="editor-surface"
contentEditable
suppressContentEditableWarning
onContextMenu={handleContextMenu}
onClick={closeMenu}
/>
</div>
<ContextMenu useEditor={useEditor} />
</div>
);
}
/* ============================================================
ContextMenu
============================================================ */
function ContextMenu({ useEditor }) {
const { menu, closeMenu, actions, getLabel, getHandler, getShouldShow } = useEditor();
useEffect(() => {
if (!menu) return;
const onClick = () => closeMenu();
const onScroll = () => closeMenu();
const onResize = () => closeMenu();
const onKeyDown = (e) => { if (e.key === 'Escape') closeMenu(); };
window.addEventListener('click', onClick);
window.addEventListener('scroll', onScroll, true);
window.addEventListener('resize', onResize);
window.addEventListener('keydown', onKeyDown);
return () => {
window.removeEventListener('click', onClick);
window.removeEventListener('scroll', onScroll, true);
window.removeEventListener('resize', onResize);
window.removeEventListener('keydown', onKeyDown);
};
}, [menu, closeMenu]);
if (!menu) return null;
const { x, y, context } = menu;
const visible = [];
for (const a of actions) {
const shouldShow = getShouldShow(a.id);
if (!shouldShow || shouldShow(context)) {
visible.push(a);
}
}
if (visible.length === 0) return null;
return (
<div
className="context-menu"
style={{
left: Math.min(x + 8, window.innerWidth - 220),
top: Math.min(y + 8, window.innerHeight - visible.length * 36),
}}
onClick={e => e.stopPropagation()}
>
{visible.map(a => (
<div
key={a.id}
className="context-menu__item"
onClick={() => {
const handler = getHandler(a.id);
handler?.(context);
closeMenu();
}}
>
{getLabel(a.id)}
</div>
))}
</div>
);
}
import React, { useCallback } from 'react';
export function SentenceLoggerPlugin({ useContextMenuItem }) {
const handler = useCallback((ctx) => {
console.log('=== TEXT CONTEXT ===');
console.log('Text :', ctx.text);
console.log('Selection :', ctx.text.slice(ctx.selectionStart, ctx.selectionEnd));
console.log('Start :', ctx.selectionStart);
console.log('End :', ctx.selectionEnd);
alert(
`Text: "${ctx.text}"\n\nSelection: "${ctx.text.slice(ctx.selectionStart, ctx.selectionEnd)}"`
);
}, []);
useContextMenuItem('log-sentence', 'Inspect context…', handler);
return null;
}
export function HighlightParagraphPlugin({ useContextMenuItem }) {
const handler = useCallback((ctx) => {
if (!ctx.containerElement) return;
const el = ctx.containerElement;
el.style.backgroundColor = el.style.backgroundColor === 'yellow' ? '' : 'yellow';
}, []);
useContextMenuItem('hl-para', 'Toggle paragraph highlight', handler);
return null;
}
import React from 'react';
import { createRoot } from 'react-dom/client';
import {
EditorProvider,
useEditor,
useContextMenuItem,
} from '#App [name="editor-context"]';
import { TextEditor } from '#App [name="editor"]';
import { PromptModal } from '#App [name="prompt-modal"]';
import { DLLMReimaginePlugin } from '#App [name="plugin-dllm-reimagine"]';
import {
SentenceLoggerPlugin,
HighlightParagraphPlugin,
} from '#App [name="plugins"]';
window.activeModel = {
name: "haic/llada2.1-mini-pruned128",
params: {
temp: {
type: 'range',
min: 0,
max: 2,
step: 0.1,
default: 0.7,
hint: 'Sampling temperature',
},
steps: {
type: 'range',
min: 1,
max: 128,
step: 1,
default: 20,
hint: 'Number of diffusion steps',
},
remasking: {
type: 'enum',
values: [
{ value: 'low_confidence', title: 'Low Confidence', hint: 'Remask low confidence tokens' },
{ value: 'random', title: 'Random', hint: 'Random remasking' },
],
default: 'low_confidence',
hint: 'Remasking strategy',
},
blocksize: {
type: 'range',
min: 1,
max: 128,
step: 1,
default: 8,
hint: 'Block size for parallel decoding',
},
seed: {
type: 'range',
min: 0,
max: 999999,
step: 1,
default: 42,
hint: 'Random seed',
},
},
};
export function App() {
return (
<EditorProvider>
<TextEditor useEditor={useEditor} />
<DLLMReimaginePlugin useEditor={useEditor} useContextMenuItem={useContextMenuItem} />
<SentenceLoggerPlugin useContextMenuItem={useContextMenuItem} />
<HighlightParagraphPlugin useContextMenuItem={useContextMenuItem} />
<PromptModal useEditor={useEditor} />
<div className="prototype-overlay">
Prototype!
<div className="prototype-overlay__sub">Use at own risk</div>
</div>
</EditorProvider>
);
}
import React, { useState, useEffect } from 'react';
/* ------------------------------------------------------------
Diff helpers
------------------------------------------------------------ */
function diffChars(oldStr, newStr) {
const a = Array.from(oldStr);
const b = Array.from(newStr);
const m = a.length, n = b.length;
if (m === 0 && n === 0) return [];
if (m === 0) return [{ start: 0, end: n }];
if (n === 0) return [];
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
for (let i = m - 1; i >= 0; i--) {
for (let j = n - 1; j >= 0; j--) {
if (a[i] === b[j]) dp[i][j] = 1 + dp[i + 1][j + 1];
else dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
const ops = [];
let i = 0, j = 0;
while (i < m || j < n) {
if (i < m && j < n && a[i] === b[j]) {
ops.push({ type: 'same' });
i++; j++;
} else if (j >= n || (i < m && dp[i + 1][j] >= dp[i][j + 1])) {
ops.push({ type: 'del' });
i++;
} else {
ops.push({ type: 'add' });
j++;
}
}
let bIdx = 0;
const ranges = [];
let start = -1;
for (const op of ops) {
if (op.type === 'same') {
if (start !== -1) { ranges.push({ start, end: bIdx }); start = -1; }
bIdx++;
} else if (op.type === 'add') {
if (start === -1) start = bIdx;
bIdx++;
}
}
if (start !== -1) ranges.push({ start, end: bIdx });
return ranges;
}
function escapeHtml(str) {
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
/* ------------------------------------------------------------
Settings builder
------------------------------------------------------------ */
function buildInitialSettings(params) {
const s = {};
for (const [k, v] of Object.entries(params)) {
s[k] = v.default !== undefined
? v.default
: (v.type === 'range' ? (v.min || 0)
: (v.type === 'enum' ? (v.values?.[0]?.value || '')
: (v.type === 'checkbox' ? !!v.default : '')));
}
return s;
}
/* ------------------------------------------------------------
ConfigParam
------------------------------------------------------------ */
function ConfigParam({ name, config, value, onChange }) {
const { type, hint, name: configName } = config || {};
const displayName = configName || name;
if (type === 'range') {
return (
<div className="config-param">
<label className="config-param__label">
{displayName} <span className="config-param__value">{value}</span>
</label>
<input
type="range"
min={config.min}
max={config.max}
step={config.step}
value={value}
title={hint || ''}
onChange={(e) => onChange(parseFloat(e.target.value))}
/>
</div>
);
}
if (type === 'enum') {
return (
<div className="config-param">
<label className="config-param__label">{displayName}</label>
<select value={value} title={hint || ''} onChange={(e) => onChange(e.target.value)}>
{config.values.map((v) => (
<option key={v.value} value={v.value} title={v.hint}>
{v.title || v.value}
</option>
))}
</select>
</div>
);
}
if (type === 'checkbox') {
return (
<div className="config-param">
<label className="config-param__label" style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }} title={hint || ''}>
<input
type="checkbox"
checked={!!value}
onChange={(e) => onChange(e.target.checked)}
/>
<span>{displayName}</span>
</label>
</div>
);
}
if (type === 'text') {
return (
<div className="config-param">
<label className="config-param__label">{displayName}</label>
<textarea value={value} title={hint || ''} onChange={(e) => onChange(e.target.value)} rows={4} />
</div>
);
}
return null;
}
/* ------------------------------------------------------------
PromptModal
------------------------------------------------------------ */
export function PromptModal({ useEditor }) {
const { promptModal, closePromptModal } = useEditor();
const isOpen = promptModal?.isOpen || false;
const modalContext = promptModal?.context || {};
const pluginParams = modalContext.pluginParams || {};
const executePrompt = promptModal?.executePrompt;
const activeModel = window.activeModel || { params: {} };
const modelParams = activeModel.params || {};
/* -- All hooks before any conditional return -- */
const [modelSettings, setModelSettings] = useState(() => buildInitialSettings(modelParams));
const [pluginSettings, setPluginSettings] = useState(() => buildInitialSettings(pluginParams));
const [outputText, setOutputText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// Ensure settings always contain valid defaults so inputs are never uncontrolled
const effectiveModelSettings = React.useMemo(
() => ({ ...buildInitialSettings(modelParams), ...modelSettings }),
[modelParams, modelSettings]
);
const effectivePluginSettings = React.useMemo(
() => ({ ...buildInitialSettings(pluginParams), ...pluginSettings }),
[pluginParams, pluginSettings]
);
// Reset when modal opens
useEffect(() => {
if (!isOpen) return;
setModelSettings(buildInitialSettings(modelParams));
setPluginSettings(buildInitialSettings(pluginParams));
setOutputText('');
setError(null);
}, [isOpen, promptModal]);
// Execute prompt on settings changes
useEffect(() => {
if (!isOpen || !executePrompt) return;
let cancelled = false;
const timer = setTimeout(() => {
setIsLoading(true);
setError(null);
const settings = {
model: modelSettings,
plugin: pluginSettings,
};
Promise.resolve(executePrompt(settings, modalContext))
.then((result) => {
if (cancelled) return;
if (!result) return;
setOutputText(result ?? '');
})
.catch((err) => {
if (cancelled) return;
setError(String(err));
})
.finally(() => {
if (!cancelled) setIsLoading(false);
});
}, 100);
return () => {
cancelled = true;
clearTimeout(timer);
};
}, [modelSettings, pluginSettings, isOpen, executePrompt]);
// Close modal on Escape key
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e) => {
if (e.key === 'Escape') closePromptModal();
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isOpen, closePromptModal]);
/* -- Early return AFTER all hooks -- */
if (!isOpen) return null;
/* -- Render helpers -- */
const { text, selectionStart, selectionEnd, replace } = modalContext;
const buildInputHtml = () => {
if (!text) return '';
const before = escapeHtml(text.slice(0, selectionStart));
const selTxt = escapeHtml(text.slice(selectionStart, selectionEnd));
const after = escapeHtml(text.slice(selectionEnd));
return `<div class="prompt-modal__input-body">${before}<span class="highlight-selection">${selTxt}</span>${after}</div>`;
};
const buildOutputHtml = () => {
if (!outputText) return '';
const ranges = diffChars(text, outputText);
let result = '';
let rangeIdx = 0;
let inDiff = false;
Array.from(outputText).forEach((ch, i) => {
const isChanged = rangeIdx < ranges.length && i >= ranges[rangeIdx].start && i < ranges[rangeIdx].end;
if (isChanged && !inDiff) {
result += '<span class="highlight-diff">';
inDiff = true;
} else if (!isChanged && inDiff) {
result += '</span>';
inDiff = false;
}
result += escapeHtml(ch);
});
if (inDiff) result += '</span>';
return `<div class="prompt-modal__output-body">${result}</div>`;
};
const inputHtml = buildInputHtml();
const outputDisplayHtml = buildOutputHtml();
const handleAccept = () => {
if (isLoading || !outputText) return;
replace(outputText);
closePromptModal();
};
const handleOverlayClick = (e) => {
if (e.target === e.currentTarget) closePromptModal();
};
return (
<div className="prompt-modal-overlay" onClick={handleOverlayClick}>
<div className="prompt-modal">
<button className="prompt-modal__close" onClick={closePromptModal} aria-label="Close">×</button>
<div className="prompt-modal__body">
<div className="prompt-modal__config">
<h3>Configure Effect</h3>
{Object.entries(pluginParams).map(([k, v]) => (
<ConfigParam
key={k}
name={k}
config={v}
value={effectivePluginSettings[k]}
onChange={(val) => setPluginSettings(s => ({ ...s, [k]: val }))}
/>
))}
<h3>Model Parameters</h3>
{Object.entries(modelParams).map(([k, v]) => (
<ConfigParam
key={k}
name={k}
config={v}
value={effectiveModelSettings[k]}
onChange={(val) => setModelSettings(s => ({ ...s, [k]: val }))}
/>
))}
</div>
<div className="prompt-modal__preview">
<div className="prompt-modal__panes">
<div className="prompt-modal__section">
<div className="prompt-modal__section-title">Input</div>
<div className="prompt-modal__doc-piece" dangerouslySetInnerHTML={{ __html: inputHtml }} />
</div>
<div className="prompt-modal__section">
<div className="prompt-modal__section-title">Output {isLoading && <span className="prompt-modal__loading">⏳</span>}</div>
<div className="prompt-modal__doc-piece prompt-modal__doc-piece--output">
{error ? (
<div className="prompt-modal__error">{error}</div>
) : (
<div dangerouslySetInnerHTML={{ __html: outputDisplayHtml || '<span style="color:#999">Loading model, hang in there for 15 seconds…</span>' }} />
)}
</div>
</div>
</div>
<div className="prompt-modal__actions">
<button className="prompt-modal__btn prompt-modal__btn--secondary" onClick={closePromptModal}>Cancel</button>
<button
className="prompt-modal__btn prompt-modal__btn--primary"
onClick={handleAccept}
disabled={isLoading || !outputText}
>
Accept
</button>
</div>
</div>
</div>
</div>
</div>
);
}
import React, { useCallback } from 'react';
/* ============================================================
DLLMReimaginePlugin – LLaDA-powered text reimagining
============================================================ */
const ENDPOINT = 'https://litellm.stream.cavi.au.dk/v1/chat/completions';
function ensureApiKey() {
if (window.litellm_key) return window.litellm_key;
const key = window.prompt('Enter your LiteLLM API key:');
if (key) {
window.litellm_key = key;
return key;
}
throw new Error('No API key provided');
}
async function* streamCompletion(apiKey, body) {
const resp = await fetch(ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(body),
});
if (!resp.ok) {
const text = await resp.text();
throw new Error(`HTTP ${resp.status}: ${text}`);
}
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || !trimmed.startsWith('data: ')) continue;
const jsonStr = trimmed.slice(6);
if (jsonStr === '[DONE]') continue;
try {
const chunk = JSON.parse(jsonStr);
const content = chunk.choices?.[0]?.delta?.content;
if (content != null) yield content;
} catch {
// ignore parse errors for malformed chunks
}
}
}
// trailing buffer
if (buffer.trim()) {
const trimmed = buffer.trim();
if (trimmed.startsWith('data: ')) {
const jsonStr = trimmed.slice(6);
if (jsonStr !== '[DONE]') {
try {
const chunk = JSON.parse(jsonStr);
const content = chunk.choices?.[0]?.delta?.content;
if (content != null) yield content;
} catch {
// ignore
}
}
}
}
}
export function DLLMReimaginePlugin({ useEditor, useContextMenuItem }) {
const { openPromptModal } = useEditor();
const executePrompt = useCallback(async (settings, context) => {
const apiKey = ensureApiKey();
const model = window.activeModel?.name || 'haic/llada2.1-mini-pruned128';
const { text, selectionStart, selectionEnd } = context;
const plugin = settings.plugin || {};
const userPrompt = plugin.prompt || '';
const selectedPart = text.slice(selectionStart, selectionEnd);
const wordCount = selectedPart.split(' ').filter((w) => w.length > 0).length;
const dots = '…'.repeat(Math.max(1, wordCount));
const systemPrompt = 'No thinking, imagine a reply that consist with the user instructions';
const before = text.slice(0, selectionStart);
const after = text.slice(selectionEnd);
const assistantContent = before + dots + after;
const messages = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
{ role: 'assistant', content: assistantContent },
];
const body = {
model,
messages,
stream: true,
extra_body: {
temperature: settings.model?.temp ?? 0.7,
steps: settings.model?.steps ?? 20,
remasking: settings.model?.remasking ?? 'low_confidence',
block_size: settings.model?.blocksize ?? 8,
seed: settings.model?.seed ?? 42,
includeSteps: true,
}
};
let content = '';
for await (const chunk of streamCompletion(apiKey, body)) {
content += chunk;
}
return content;
}, []);
const handler = useCallback(
(ctx) => {
const pluginParams = {
prompt: {
name: 'Prompt',
type: 'text',
default: '',
hint: 'Optional instructions for the reimagining',
}
};
openPromptModal({ ...ctx, pluginParams }, executePrompt);
},
[openPromptModal, executePrompt]
);
useContextMenuItem(
'dllm-reimagine',
'Reimagine…',
handler,
(ctx) => ctx.selectionEnd > ctx.selectionStart
);
return null;
}
body {
background: grey;
padding: 0;
margin: 0;
}
/* iOS-style overlaid scrollbars */
* {
scrollbar-width: thin;
scrollbar-color: rgba(80,80,80,0.35) transparent;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(80, 80, 80, 0.25);
border-radius: 3px;
transition: background 0.4s ease;
}
*:hover::-webkit-scrollbar-thumb {
background: rgba(80, 80, 80, 0.55);
}
.editor-page {
background: #e4e4e4;
min-height: 100vh;
}
.toolbar {
position: sticky;
top: 0;
z-index: 1;
background: #f3f3f3;
border-bottom: 1px solid #ccc;
padding: 8px 16px;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 4px;
font-family: system-ui, sans-serif;
&__group {
display: inline-flex;
align-items: center;
margin-right: 4px;
}
&__separator {
display: inline-block;
width: 1px;
height: 24px;
background: #ccc;
margin: 0 6px;
vertical-align: middle;
}
&__btn {
min-width: 32px;
height: 32px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
font-size: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 4px;
color: #333;
&--active {
background: #d0d0d0;
}
.material-icons-outlined {
font-size: 20px;
}
&--small {
font-size: 13px;
}
&--bold {
font-weight: 700;
}
&--italic {
font-style: italic;
}
&--underline {
text-decoration: underline;
}
}
}
.editor-container {
padding: 2rem 1rem;
}
.editor-surface {
max-width: 210mm;
min-height: 297mm;
margin: 0 auto;
padding: 25mm 20mm;
background: #fff;
box-shadow: 0 0 12px rgba(0, 0, 0, 0.15);
border-radius: 2px;
line-height: 1.6;
font-family: Georgia, "Times New Roman", serif;
font-size: 16px;
color: #222;
outline: none;
}
.context-menu {
position: fixed;
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
padding: 4px 0;
min-width: 200px;
z-index: 10000;
&__item {
padding: 10px 16px;
cursor: pointer;
font-size: 14px;
color: #222;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
&:hover {
background: #f5f5f5;
}
}
}
.prototype-overlay {
position: fixed;
bottom: 32px;
right: -55px;
background: rgba(220, 53, 34, 0.92);
color: #fff;
font-family: system-ui, sans-serif;
font-size: 13px;
font-weight: 700;
text-align: center;
letter-spacing: 1.5px;
text-transform: uppercase;
padding: 10px 70px;
transform: rotate(-45deg);
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
line-height: 1.3;
z-index: 10001;
pointer-events: none;
white-space: nowrap;
&__sub {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.5px;
opacity: 0.95;
text-transform: none;
margin-top: 2px;
}
}
/* ============================================================
PromptModal styles
============================================================ */
.prompt-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 20000;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
overscroll-behavior: contain;
touch-action: none;
}
.prompt-modal {
background: #fff;
border-radius: 8px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.25);
width: 100%;
max-width: Min(100em, 90vw); /* intentional uppercase to pass thru min to css from scss */
max-height: 85vh;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
&__close {
position: absolute;
top: 8px;
right: 12px;
background: transparent;
border: none;
font-size: 22px;
color: #888;
cursor: pointer;
z-index: 2;
line-height: 1;
&:hover {
color: #333;
}
}
&__body {
display: flex;
flex-direction: row;
flex: 1;
overflow: hidden;
}
&__config {
width: 280px;
flex-shrink: 0;
background: #f1f3f4;
color: black;
padding: 20px;
overflow-y: auto;
h3 {
font-family: system-ui, sans-serif;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 1px;
color: #555;
margin: 20px 0 10px;
border-bottom: 1px solid #bbb;
padding-bottom: 4px;
&:first-child {
margin-top: 0;
}
}
}
&__preview {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px;
overflow: hidden;
background: #f8f9fa;
}
&__panes {
flex: 1;
display: flex;
flex-direction: row;
gap: 12px;
overflow: hidden;
}
&__section {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
&__section-title {
font-family: system-ui, sans-serif;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
color: #555;
margin-bottom: 8px;
flex-shrink: 0;
position: relative;
padding-right: 20px;
}
&__doc-piece {
font-family: Georgia, "Times New Roman", serif;
font-size: 15px;
line-height: 1.7;
color: #222;
background: #fff;
padding: 10px;
border-radius: 4px;
border: 1px dashed #ddd;
overflow-wrap: break-word;
flex: 1;
overflow-y: auto;
min-height: 0;
position: relative;
&::before,
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
height: 6px;
background-repeat: repeat-x;
pointer-events: none;
}
&::before {
top: -3px;
background-image: radial-gradient(circle at 50% 0, transparent 3px, #fff 4px);
background-size: 8px 8px;
background-position: top;
}
&::after {
bottom: -3px;
background-image: radial-gradient(circle at 50% 100%, transparent 3px, #fff 4px);
background-size: 8px 8px;
background-position: bottom;
}
}
&__doc-piece--output {
background: #fcfcfc;
border-style: solid;
&::before {
background-image: radial-gradient(circle at 50% 0, transparent 3px, #fcfcfc 4px);
}
&::after {
background-image: radial-gradient(circle at 50% 100%, transparent 3px, #fcfcfc 4px);
}
}
&__input-body,
&__output-body {
white-space: pre-wrap;
}
/* torn-edge pseudo-elements applied to &__doc-piece instead */
&__actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 4px;
}
&__btn {
padding: 8px 18px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
font-family: system-ui, sans-serif;
border: 1px solid transparent;
&--primary {
background: #1a73e8;
color: #fff;
border-color: #1a73e8;
&:hover:not(:disabled) {
background: #1557b0;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
&--secondary {
background: #fff;
color: #333;
border-color: #ccc;
&:hover {
background: #f5f5f5;
}
}
}
&__loading {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
font-weight: 400;
color: #888;
}
&__error {
color: #c5221f;
font-size: 13px;
background: #fce8e8;
padding: 8px 10px;
border-radius: 4px;
border: 1px solid #f5c2c7;
}
}
/* ConfigParam inside light panel */
.config-param {
display: grid;
grid-template-columns: 1fr 1.5fr;
gap: 8px;
align-items: center;
border-radius: 4px;
margin: 0.25em;
&__label {
font-size: 13px;
font-weight: 600;
color: #333;
background: #e8eaed;
padding: 4px 6px;
border-radius: 3px;
height: 100%;
display: flex;
align-items: center;
}
&__value {
font-weight: 700;
color: #1a73e8;
margin-left: 4px;
}
input[type="range"] {
width: 100%;
cursor: pointer;
}
select {
width: 100%;
padding: 4px 6px;
font-size: 13px;
border-radius: 4px;
border: 1px solid #999;
background: #fff;
color: #333;
}
textarea {
width: 100%;
min-height: 60px;
padding: 6px 8px;
font-size: 13px;
border-radius: 4px;
border: 1px solid #999;
background: #fff;
color: #333;
resize: vertical;
font-family: system-ui, sans-serif;
box-sizing: border-box;
}
}
/* Text-highlight helpers */
.highlight-selection {
background: #d4edda;
padding: 0 2px;
border-radius: 2px;
}
.highlight-sentence {
background: #cce5ff;
padding: 0 4px;
border-radius: 3px;
}
.highlight-paragraph {
background: #fffbea;
padding: 4px 6px;
border-radius: 4px;
display: inline;
}
.highlight-diff {
background: #ffeb3b;
padding: 0 1px;
border-radius: 2px;
}