SPB Git

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%
5.2 KB · 157 lines tsx
Raw Blame History
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