mattrguerra/cortexmainPrivate on GitHub, published here in full

QuickApp.tsx

Language TypeScript4.7 KB135 lines

src/QuickApp.tsx

  1. import { useEffect, useLayoutEffect, useRef, useState } from 'react'
  2. import { Markdown } from './components/Markdown'
  3. import { CostLine } from './components/MessageBubble'
  4. import type { IpcClient } from './ipc/client'
  5. import type { Message } from './ipc/gen/Message'
  6. import type { Settings } from './ipc/gen/Settings'
  7. import type { StreamEvent } from './ipc/gen/StreamEvent'
  8. import { ipc } from './ipc'
  9. import { shortModel } from './lib/format'
  10. import { mod, QUICK_HEIGHT, QUICK_MAX, useTheme } from './lib/theme'
  11. import { useStore } from './state/store'
  12. /** The small always-on-top prompt: one question, one answer, hand off with Cmd+Enter. */
  13. export function QuickApp() {
  14. const [client, setClient] = useState<IpcClient | null>(null)
  15. const [settings, setSettings] = useState<Settings | null>(null)
  16. const [vibrancy, setVibrancy] = useState(false)
  17. const [text, setText] = useState('')
  18. const [answer, setAnswer] = useState('')
  19. const [done, setDone] = useState<Message | null>(null)
  20. const [error, setError] = useState<string | null>(null)
  21. const [conversationId, setConversationId] = useState<string | null>(null)
  22. const [streamId, setStreamId] = useState<string | null>(null)
  23. // Enter before the settings have loaded: remember it and send once they have.
  24. const [queued, setQueued] = useState(false)
  25. const input = useRef<HTMLInputElement>(null)
  26. const body = useRef<HTMLDivElement>(null)
  27. useTheme(settings, vibrancy)
  28. useEffect(() => {
  29. void ipc().then(async (c) => {
  30. useStore.setState({ client: c })
  31. setClient(c)
  32. setSettings(await c.getSettings())
  33. setVibrancy((await c.appInfo()).vibrancy)
  34. c.on('settings-changed', (s) => setSettings(s as Settings))
  35. c.on('quick-shown', () => {
  36. setText('')
  37. setAnswer('')
  38. setDone(null)
  39. setError(null)
  40. setConversationId(null)
  41. void c.resizeQuick(QUICK_HEIGHT)
  42. input.current?.focus()
  43. })
  44. input.current?.focus()
  45. })
  46. }, [])
  47. // Grow the window to fit the answer.
  48. useLayoutEffect(() => {
  49. if (!client) return
  50. const h =
  51. answer || error ? Math.min(QUICK_MAX, QUICK_HEIGHT + (body.current?.scrollHeight ?? 0) + 44) : QUICK_HEIGHT
  52. void client.resizeQuick(h)
  53. }, [answer, error, done, client])
  54. const ask = async () => {
  55. if (!text.trim() || streamId) return
  56. if (!client || !settings) {
  57. setQueued(true)
  58. return
  59. }
  60. setQueued(false)
  61. setAnswer('')
  62. setDone(null)
  63. setError(null)
  64. try {
  65. const conv = await client.createConversation(settings.quick_provider_id, settings.quick_model_id, null, 'quick')
  66. setConversationId(conv.id)
  67. const ack = await client.sendMessage({ conversation_id: conv.id, text, attachment_ids: [] }, (e: StreamEvent) => {
  68. if (e.type === 'delta') setAnswer((a) => a + e.text)
  69. if (e.type === 'done') {
  70. setDone(e.message)
  71. setStreamId(null)
  72. }
  73. if (e.type === 'error') {
  74. setDone(e.message)
  75. setError(e.error)
  76. setStreamId(null)
  77. }
  78. })
  79. setStreamId(ack.stream_id)
  80. } catch (err) {
  81. setError(String(err))
  82. }
  83. }
  84. useEffect(() => {
  85. if (queued && client && settings) void ask()
  86. }, [queued, client, settings])
  87. return (
  88. <div className="quick glass" data-testid="quick">
  89. <div className="quick-row" data-tauri-drag-region>
  90. <span className="quick-mark" aria-hidden />
  91. <input
  92. ref={input}
  93. className="quick-input"
  94. placeholder="Ask quickly"
  95. aria-label="Quick prompt"
  96. value={text}
  97. onChange={(e) => setText(e.target.value)}
  98. onKeyDown={(e) => {
  99. if (e.key === 'Escape') {
  100. if (streamId) void client?.cancelStream(streamId)
  101. else void client?.hideQuick()
  102. } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
  103. e.preventDefault()
  104. if (conversationId) void client?.quickOpenInMain(conversationId)
  105. } else if (e.key === 'Enter') {
  106. e.preventDefault()
  107. void ask()
  108. }
  109. }}
  110. />
  111. <span className="quick-model" title={settings?.quick_model_id}>
  112. {settings ? shortModel(settings.quick_model_id) : ''}
  113. </span>
  114. </div>
  115. {(answer || error) && (
  116. <div className="quick-body" ref={body}>
  117. {answer && <Markdown text={answer} />}
  118. {error && <div className="msg-error">{error}</div>}
  119. <div className="quick-foot">
  120. {done && <CostLine m={done} />}
  121. <span className="spacer" />
  122. {conversationId && <span className="muted small">{mod}Enter opens in Cortex</span>}
  123. </div>
  124. </div>
  125. )}
  126. </div>
  127. )
  128. }