// 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 });