spb/zyquo-cloud-web Public MIT
Zyquo Cloud Web — every cloud model, one beautiful chat, entirely in your browser.
TypeScript 81.9%
CSS 8.9%
JavaScript 7.5%
Shell 1.1%
HTML 0.6%
1/*2 * App.tsx3 * Zyquo Cloud Web4 *5 * Author: Simon-Pierre Boucher6 * Mail: contact@spboucher.ai7 *8 * The app shell: sidebar + chat area (drawer on phones), root empty state,9 * first-run notice, settings modal, toasts, and global keyboard shortcuts10 * (⌘N new chat, ⌘K model menu/palette, ⌘F search, ⌘, settings, ⌘. stop,11 * ⌘⇧E export).12 */1314import { useEffect } from 'react'15import ChatView from './components/ChatView'16import CommandPalette from './components/CommandPalette'17import CompareView from './components/CompareView'18import FirstRun from './components/FirstRun'19import ModelMenu from './components/ModelMenu'20import SettingsModal from './components/SettingsModal'21import Sidebar from './components/Sidebar'22import SuggestionGrid from './components/SuggestionGrid'23import Toasts from './components/Toasts'24import StreamTestView from './features/StreamTestView'25import { exportConversationMarkdown } from './features/exporters'26import { useStore } from './state/store'27import './design/components.css'2829export default function App() {30 const loaded = useStore((s) => s.loaded)31 const loadAll = useStore((s) => s.loadAll)32 const conversations = useStore((s) => s.conversations)33 const selectedID = useStore((s) => s.selectedID)34 const settings = useStore((s) => s.settings)35 const settingsOpen = useStore((s) => s.settingsOpen)36 const sidebarOpen = useStore((s) => s.sidebarOpen)37 const setSidebarOpen = useStore((s) => s.setSidebarOpen)38 const modelMenuOpen = useStore((s) => s.modelMenuOpen)39 const setModelMenuOpen = useStore((s) => s.setModelMenuOpen)40 const newConversation = useStore((s) => s.newConversation)41 const openSettings = useStore((s) => s.openSettings)42 const stop = useStore((s) => s.stop)43 const send = useStore((s) => s.send)44 const updateSettings = useStore((s) => s.updateSettings)4546 const selected = conversations.find((c) => c.id === selectedID) ?? null4748 useEffect(() => {49 void loadAll()50 }, [loadAll])5152 // Global shortcuts53 useEffect(() => {54 const onKeyDown = (e: KeyboardEvent) => {55 const mod = e.metaKey || e.ctrlKey56 if (!mod) return57 const state = useStore.getState()58 switch (e.key) {59 case 'n':60 e.preventDefault()61 state.newConversation()62 break63 case 'k':64 e.preventDefault()65 state.setPaletteOpen(!state.paletteOpen)66 break67 case 'f': {68 e.preventDefault()69 const input = document.querySelector<HTMLInputElement>('[data-search-input]')70 input?.focus()71 break72 }73 case ',':74 e.preventDefault()75 state.openSettings()76 break77 case '.':78 e.preventDefault()79 if (state.selectedID) state.stop(state.selectedID)80 break81 case 'E':82 if (e.shiftKey) {83 e.preventDefault()84 const conversation = state.conversations.find((c) => c.id === state.selectedID)85 if (conversation) exportConversationMarkdown(conversation)86 }87 break88 }89 }90 window.addEventListener('keydown', onKeyDown)91 return () => window.removeEventListener('keydown', onKeyDown)92 }, [newConversation, openSettings, stop])9394 if (window.location.hash === '#stream-test') return <StreamTestView />9596 if (!loaded) {97 return (98 <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>99 <span style={{ color: 'var(--z-text-tertiary)' }}>Loading…</span>100 </div>101 )102 }103104 return (105 <div className={`shell${settings.focusMode ? ' focus-mode' : ''}`}>106 <Sidebar />107 {sidebarOpen && <div className="drawer-scrim" onClick={() => setSidebarOpen(false)} />}108 {selected ? (109 <ChatView conversation={selected} />110 ) : (111 <div className="chat-area">112 <SuggestionGrid113 onPick={(prompt) => {114 const conversation = newConversation()115 // Native behavior: self-contained suggestions send immediately;116 // ones expecting pasted input (trailing blank line) seed the draft.117 if (prompt.endsWith('\n\n')) {118 useStore.getState().setDraftSeed(prompt)119 } else {120 void send(conversation.id, prompt, [])121 }122 }}123 />124 {modelMenuOpen && (125 <ModelMenu126 onPick={(model) => {127 updateSettings({ defaultModelID: model.id, defaultProvider: model.provider })128 newConversation({ model })129 }}130 onClose={() => setModelMenuOpen(false)}131 />132 )}133 </div>134 )}135 {settingsOpen && <SettingsModal />}136 <PaletteHost />137 <CompareHost />138 {!settings.firstRunAcknowledged && <FirstRun />}139 <Toasts />140 </div>141 )142}143144function PaletteHost() {145 const paletteOpen = useStore((s) => s.paletteOpen)146 const setPaletteOpen = useStore((s) => s.setPaletteOpen)147 if (!paletteOpen) return null148 return <CommandPalette onClose={() => setPaletteOpen(false)} />149}150151function CompareHost() {152 const compareOpen = useStore((s) => s.compareOpen)153 const setCompareOpen = useStore((s) => s.setCompareOpen)154 if (!compareOpen) return null155 return <CompareView onClose={() => setCompareOpen(false)} />156}157