/* * App.tsx * Zyquo Cloud Web * * Author: Simon-Pierre Boucher * Mail: contact@spboucher.ai * * The app shell: sidebar + chat area (drawer on phones), root empty state, * first-run notice, settings modal, toasts, and global keyboard shortcuts * (⌘N new chat, ⌘K model menu/palette, ⌘F search, ⌘, settings, ⌘. stop, * ⌘⇧E export). */ import { useEffect } from 'react' import ChatView from './components/ChatView' import CommandPalette from './components/CommandPalette' import CompareView from './components/CompareView' import FirstRun from './components/FirstRun' import ModelMenu from './components/ModelMenu' import SettingsModal from './components/SettingsModal' import Sidebar from './components/Sidebar' import SuggestionGrid from './components/SuggestionGrid' import Toasts from './components/Toasts' import StreamTestView from './features/StreamTestView' import { exportConversationMarkdown } from './features/exporters' import { useStore } from './state/store' import './design/components.css' export default function App() { const loaded = useStore((s) => s.loaded) const loadAll = useStore((s) => s.loadAll) const conversations = useStore((s) => s.conversations) const selectedID = useStore((s) => s.selectedID) const settings = useStore((s) => s.settings) const settingsOpen = useStore((s) => s.settingsOpen) const sidebarOpen = useStore((s) => s.sidebarOpen) const setSidebarOpen = useStore((s) => s.setSidebarOpen) const modelMenuOpen = useStore((s) => s.modelMenuOpen) const setModelMenuOpen = useStore((s) => s.setModelMenuOpen) const newConversation = useStore((s) => s.newConversation) const openSettings = useStore((s) => s.openSettings) const stop = useStore((s) => s.stop) const send = useStore((s) => s.send) const updateSettings = useStore((s) => s.updateSettings) const selected = conversations.find((c) => c.id === selectedID) ?? null useEffect(() => { void loadAll() }, [loadAll]) // Global shortcuts useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { const mod = e.metaKey || e.ctrlKey if (!mod) return const state = useStore.getState() switch (e.key) { case 'n': e.preventDefault() state.newConversation() break case 'k': e.preventDefault() state.setPaletteOpen(!state.paletteOpen) break case 'f': { e.preventDefault() const input = document.querySelector('[data-search-input]') input?.focus() break } case ',': e.preventDefault() state.openSettings() break case '.': e.preventDefault() if (state.selectedID) state.stop(state.selectedID) break case 'E': if (e.shiftKey) { e.preventDefault() const conversation = state.conversations.find((c) => c.id === state.selectedID) if (conversation) exportConversationMarkdown(conversation) } break } } window.addEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown) }, [newConversation, openSettings, stop]) if (window.location.hash === '#stream-test') return if (!loaded) { return (
Loading…
) } return (
{sidebarOpen &&
setSidebarOpen(false)} />} {selected ? ( ) : (
{ const conversation = newConversation() // Native behavior: self-contained suggestions send immediately; // ones expecting pasted input (trailing blank line) seed the draft. if (prompt.endsWith('\n\n')) { useStore.getState().setDraftSeed(prompt) } else { void send(conversation.id, prompt, []) } }} /> {modelMenuOpen && ( { updateSettings({ defaultModelID: model.id, defaultProvider: model.provider }) newConversation({ model }) }} onClose={() => setModelMenuOpen(false)} /> )}
)} {settingsOpen && } {!settings.firstRunAcknowledged && }
) } function PaletteHost() { const paletteOpen = useStore((s) => s.paletteOpen) const setPaletteOpen = useStore((s) => s.setPaletteOpen) if (!paletteOpen) return null return setPaletteOpen(false)} /> } function CompareHost() { const compareOpen = useStore((s) => s.compareOpen) const setCompareOpen = useStore((s) => s.setCompareOpen) if (!compareOpen) return null return setCompareOpen(false)} /> }