/* * CommandPalette.tsx * Zyquo Cloud Web * * Author: Simon-Pierre Boucher * Mail: contact@spboucher.ai * * ⌘K command palette — everything in one place: actions (new chat, switch * model, compare, settings, focus mode, summarize), jump to conversation, * apply persona, insert template, switch model. Keyboard-driven. */ import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { CATALOG } from '../features/catalogHelpers' import { PERSONAS } from '../features/personasData' import { PROMPT_TEMPLATES } from '../features/promptLibraryData' import { summarizeConversation } from '../features/conversationTools' import { PROVIDER_META } from '../providers/registry' import { uid, useStore } from '../state/store' import { contextBadge, type AIModel } from '../types' import { IconColumns, IconCommand, IconDoc, IconGear, IconLightbulb, IconPlus, IconSearch, ProviderGlyph, } from './icons' interface PaletteItem { id: string section: string icon: ReactNode title: string subtitle?: string run: () => void } export default function CommandPalette({ onClose }: { onClose: () => void }) { const [query, setQuery] = useState('') const [highlight, setHighlight] = useState(0) const inputRef = useRef(null) const conversations = useStore((s) => s.conversations) const selectedID = useStore((s) => s.selectedID) useEffect(() => inputRef.current?.focus(), []) const items: PaletteItem[] = useMemo(() => { const state = useStore.getState() const q = query.toLowerCase().trim() const match = (...targets: (string | undefined)[]) => q === '' || targets.some((t) => t?.toLowerCase().includes(q)) const out: PaletteItem[] = [] // Actions const actions: PaletteItem[] = [ { id: 'act-new', section: 'Actions', icon: , title: 'New chat', subtitle: '⌘N', run: () => state.newConversation(), }, { id: 'act-compare', section: 'Actions', icon: , title: 'Compare models…', run: () => state.setCompareOpen(true), }, { id: 'act-model', section: 'Actions', icon: , title: 'Switch model…', run: () => state.setModelMenuOpen(true), }, { id: 'act-settings', section: 'Actions', icon: , title: 'Open Settings', subtitle: '⌘,', run: () => state.openSettings(), }, { id: 'act-focus', section: 'Actions', icon: , title: state.settings.focusMode ? 'Exit focus mode' : 'Focus mode', run: () => state.updateSettings({ focusMode: !state.settings.focusMode }), }, ...(selectedID ? [ { id: 'act-summarize', section: 'Actions', icon: , title: 'Summarize this conversation', run: () => void summarizeConversation(selectedID), }, { id: 'act-duplicate', section: 'Actions', icon: , title: 'Duplicate this conversation', run: () => { const conversation = state.conversations.find((c) => c.id === selectedID) if (!conversation) return state.restoreConversation({ ...conversation, id: uid(), title: `${conversation.title} (copy)`, messages: conversation.messages.map((m) => ({ ...m, id: uid() })), createdAt: Date.now(), updatedAt: Date.now(), pinned: false, }) }, }, ] : []), ] out.push(...actions.filter((a) => match(a.title))) // Conversations out.push( ...conversations .filter((c) => match(c.title)) .slice(0, 6) .map((c) => ({ id: `conv-${c.id}`, section: 'Conversations', icon: , title: c.title, subtitle: c.modelID, run: () => state.select(c.id), })) ) // Models const models = (q === '' ? CATALOG.filter((m) => m.isRecommended) : CATALOG).filter((m) => match(m.displayName, m.id, PROVIDER_META[m.provider].displayName) ) out.push( ...models.slice(0, 8).map((m: AIModel) => ({ id: `model-${m.provider}-${m.id}`, section: 'Models', icon: , title: m.displayName, subtitle: `${PROVIDER_META[m.provider].displayName} · ${contextBadge(m)}`, run: () => { if (state.selectedID) { state.updateConversation(state.selectedID, { modelID: m.id, provider: m.provider }) } else { state.newConversation({ model: m }) } }, })) ) // Personas out.push( ...PERSONAS.filter((p) => match(p.name, p.systemPrompt)) .slice(0, 6) .map((p) => ({ id: `persona-${p.id}`, section: 'Personas', icon: , title: p.name, subtitle: p.systemPrompt.slice(0, 70), run: () => { if (state.selectedID) { state.updateConversation(state.selectedID, { systemPrompt: p.systemPrompt, personaID: p.id, }) } else { state.newConversation({ systemPrompt: p.systemPrompt, personaID: p.id }) } state.toast(`Persona: ${p.name}`, 'success') }, })) ) // Templates out.push( ...PROMPT_TEMPLATES.filter((t) => match(t.title, t.category)) .slice(0, 10) .map((t) => ({ id: `tpl-${t.id}`, section: 'Prompt Library', icon: , title: t.title, subtitle: t.category, run: () => { if (!state.selectedID) state.newConversation() state.setDraftSeed(t.body.replace('{{input}}', '')) }, })) ) return out }, [query, conversations, selectedID]) useEffect(() => setHighlight(0), [query]) const runItem = (item: PaletteItem) => { onClose() item.run() } const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'ArrowDown') { e.preventDefault() setHighlight((h) => Math.min(h + 1, items.length - 1)) } else if (e.key === 'ArrowUp') { e.preventDefault() setHighlight((h) => Math.max(h - 1, 0)) } else if (e.key === 'Enter') { e.preventDefault() const item = items[highlight] if (item) runItem(item) } else if (e.key === 'Escape') { onClose() } } let lastSection = '' return (
e.target === e.currentTarget && onClose()}>
setQuery(e.target.value)} />
{items.length === 0 &&
Nothing matches.
} {items.map((item, index) => { const header = item.section !== lastSection ? (
{item.section}
) : null lastSection = item.section return (
{header}
) })}
↑↓ navigate run esc close
) } function IconCopyIconFallback() { return }