mattrguerra/cortexmainPrivate on GitHub, published here in full

App.tsx

Language TypeScript2.9 KB88 lines

src/App.tsx

  1. import { useEffect, useRef, useState } from 'react'
  2. import { ChatHeader } from './components/ChatHeader'
  3. import { ChatView } from './components/ChatView'
  4. import { Composer, type ComposerHandle } from './components/Composer'
  5. import { SearchPalette } from './components/SearchPalette'
  6. import { SettingsPanel } from './components/SettingsPanel'
  7. import { Sidebar } from './components/Sidebar'
  8. import { BudgetBanner, Toasts } from './components/Toasts'
  9. import { UsagePanel } from './components/UsagePanel'
  10. import { ipc } from './ipc'
  11. import { useTheme } from './lib/theme'
  12. import { useStore } from './state/store'
  13. export function App() {
  14. const [ready, setReady] = useState(false)
  15. const [error, setError] = useState<string | null>(null)
  16. const settings = useStore((s) => s.settings)
  17. const info = useStore((s) => s.info)
  18. const panel = useStore((s) => s.panel)
  19. const composer = useRef<ComposerHandle>(null)
  20. useTheme(settings, !!info?.vibrancy)
  21. useEffect(() => {
  22. ipc()
  23. .then(async (client) => {
  24. await useStore.getState().init(client)
  25. setReady(true)
  26. // Show the window after the first paint. A hidden page gets no
  27. // animation frames, so a timer backs it up (or it would never show).
  28. let shown = false
  29. const show = () => {
  30. if (!shown) {
  31. shown = true
  32. void client.windowReady()
  33. }
  34. }
  35. requestAnimationFrame(show)
  36. setTimeout(show, 100)
  37. })
  38. .catch((e) => {
  39. setError(String(e))
  40. void ipc().then((c) => c.windowReady())
  41. })
  42. }, [])
  43. useEffect(() => {
  44. const onKey = (e: KeyboardEvent) => {
  45. const mod = e.metaKey || e.ctrlKey
  46. const s = useStore.getState()
  47. if (mod && e.key.toLowerCase() === 'k') {
  48. e.preventDefault()
  49. s.setPanel(s.panel === 'search' ? null : 'search')
  50. } else if (mod && e.key.toLowerCase() === 'n') {
  51. e.preventDefault()
  52. s.newChat()
  53. composer.current?.focus()
  54. } else if (mod && e.key === ',') {
  55. e.preventDefault()
  56. s.setPanel('settings')
  57. } else if (e.key === 'Escape' && s.panel) {
  58. s.setPanel(null)
  59. }
  60. }
  61. window.addEventListener('keydown', onKey)
  62. return () => window.removeEventListener('keydown', onKey)
  63. }, [])
  64. if (error) return <div className="fatal">Cortex could not start: {error}</div>
  65. if (!ready) return <div className="app loading" aria-busy />
  66. return (
  67. <div className="app">
  68. <Sidebar />
  69. <main className="main">
  70. <ChatHeader />
  71. <BudgetBanner />
  72. <ChatView onSuggest={(t) => composer.current?.setText(t)} />
  73. <div className="composer-wrap">
  74. <Composer ref={composer} />
  75. </div>
  76. </main>
  77. {panel === 'search' && <SearchPalette />}
  78. {panel === 'settings' && <SettingsPanel />}
  79. {panel === 'usage' && <UsagePanel />}
  80. <Toasts />
  81. </div>
  82. )
  83. }