/* * SlashMenu.tsx * Zyquo Cloud Web * * Author: Simon-Pierre Boucher * Mail: contact@spboucher.ai * * Slash-command menu: typing "/" at the start of the input surfaces prompt * templates and quick tools (/summarize, /translate, /rewrite, /explain) and * personas. Selecting a template substitutes {{input}} with the rest of the * draft (native PromptLibraryStore.apply behavior). */ import { useEffect, useMemo, useRef, useState } from 'react' import { PERSONAS } from '../features/personasData' import { PROMPT_TEMPLATES } from '../features/promptLibraryData' import { summarizeConversation } from '../features/conversationTools' import { useStore } from '../state/store' import { IconCommand, IconDoc, IconLightbulb } from './icons' interface SlashItem { id: string section: string title: string subtitle: string apply: () => void } /** Applies a template body: {{input}} ← the user's remaining draft text. */ export function applyTemplate(body: string, input: string): string { if (body.includes('{{input}}')) return body.replace('{{input}}', input) return input === '' ? body : `${body}\n\n${input}` } const QUICK_TOOLS = [ { key: 'summarize', title: '/summarize', body: 'Summarize the following clearly and concisely, preserving the key facts:\n\n{{input}}' }, { key: 'translate', title: '/translate', body: 'Translate the following text to English (or to French if it is already English), keeping the tone natural:\n\n{{input}}' }, { key: 'rewrite', title: '/rewrite', body: 'Rewrite the following for clarity and flow, preserving meaning and voice:\n\n{{input}}' }, { key: 'explain', title: '/explain', body: 'Explain the following simply, step by step, for a smart non-expert:\n\n{{input}}' }, ] export default function SlashMenu({ query, input, conversationID, onApply, onClose, }: { /** Text after the leading slash, up to the first space. */ query: string /** The draft text after the slash command (used as {{input}}). */ input: string conversationID: string onApply: (text: string) => void onClose: () => void }) { const [highlight, setHighlight] = useState(0) const ref = useRef(null) const items: SlashItem[] = useMemo(() => { const q = query.toLowerCase() const match = (...targets: string[]) => q === '' || targets.some((t) => t.toLowerCase().includes(q)) const out: SlashItem[] = [] for (const tool of QUICK_TOOLS) { if (!match(tool.title, tool.key)) continue if (tool.key === 'summarize' && input.trim() === '') { out.push({ id: `tool-${tool.key}`, section: 'Tools', title: tool.title, subtitle: 'Summarize this conversation', apply: () => { onApply('') void summarizeConversation(conversationID) }, }) } else { out.push({ id: `tool-${tool.key}`, section: 'Tools', title: tool.title, subtitle: 'Quick tool', apply: () => onApply(applyTemplate(tool.body, input.trim())), }) } } for (const template of PROMPT_TEMPLATES) { if (!match(template.title, template.category)) continue out.push({ id: `tpl-${template.id}`, section: template.category, title: template.title, subtitle: template.category, apply: () => onApply(applyTemplate(template.body, input.trim())), }) } for (const persona of PERSONAS) { if (!match(persona.name)) continue out.push({ id: `persona-${persona.id}`, section: 'Personas', title: persona.name, subtitle: 'Apply persona to this chat', apply: () => { const state = useStore.getState() state.updateConversation(conversationID, { systemPrompt: persona.systemPrompt, personaID: persona.id, }) state.toast(`Persona: ${persona.name}`, 'success') onApply(input) }, }) } return out.slice(0, 12) }, [query, input, conversationID, onApply]) useEffect(() => setHighlight(0), [query]) // Expose keyboard handling to the parent textarea via a custom event target. useEffect(() => { const onKey = (e: 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.key === 'Tab') { e.preventDefault() items[highlight]?.apply() } else if (e.key === 'Escape') { e.preventDefault() onClose() } } window.addEventListener('keydown', onKey, { capture: true }) return () => window.removeEventListener('keydown', onKey, { capture: true }) }, [items, highlight, onClose]) if (items.length === 0) return null let lastSection = '' return (
{items.map((item, index) => { const header = item.section !== lastSection ? (
{item.section}
) : null lastSection = item.section return (
{header}
) })}
) }