// Caret landing — the command panel (dark-glass HUD) + the animated keyboard-flow engine. // Default "Quick" tab actions shown in the panel list. const QUICK_ACTIONS = [ { alias: 'ss', name: 'Summarize', icon: 'align-left', desc: 'Condense the selection' }, { alias: 'fg', name: 'Fix grammar', icon: 'spell-check', desc: 'Spelling & grammar' }, { alias: 'ak', name: 'Ask anything', icon: 'message-circle-question', desc: 'Your typed prompt' }, { alias: 'ex', name: 'Explain this', icon: 'lightbulb', desc: 'Plain-language answer' }, { alias: 'lu', name: 'Lookup word', icon: 'book-open', desc: 'Dictionary lookup' }, ]; // Presentational panel. Driven entirely by props. function CaretPanel({ query = '', showCursor = false, mode = 'list', activeAlias = null, result = '', streaming = false, tab = 'Quick', actions = QUICK_ACTIONS, resultLabel = 'Summarize · result' }) { return (
{query ? {query} : Search actions, or type an alias…} {showCursor && }
{mode === 'list' && (
{tab}
{actions.map((a) => { const active = activeAlias === a.alias; return (
{a.name} {a.desc} {a.alias}
); })}
)} {mode === 'result' && (
{streaming ? : } {resultLabel}
{result}{streaming && }
)}
{mode === 'list' ? ( Run Navigate esc Dismiss ) : ( Insert Replace Copy )}
); } // ——— generic looping timeline engine ——— // events: [{ t, patch }] applied cumulatively over `initial`; resets each loop. function useTimeline(events, initial, loopMs, enabled, staticState) { const [state, setState] = useState(enabled ? initial : (staticState || initial)); useEffect(() => { if (!enabled) { setState(staticState || initial); return; } let timers = []; function run() { setState(initial); events.forEach((ev) => { timers.push(setTimeout(() => setState((s) => ({ ...s, ...(typeof ev.patch === 'function' ? ev.patch(s) : ev.patch) })), ev.t)); }); timers.push(setTimeout(run, loopMs)); } run(); return () => timers.forEach(clearTimeout); }, [enabled]); return state; } // Build streaming word-reveal events for a result string. function streamEvents(words, startT, stepMs, key = 'result') { return words.map((_, i) => ({ t: startT + i * stepMs, patch: { [key]: words.slice(0, i + 1).join(' ') }, })); } Object.assign(window, { CaretPanel, QUICK_ACTIONS, useTimeline, streamEvents });