mattrguerra/cortexmainPrivate on GitHub, published here in full

ChatView.tsx

Language TypeScript3.1 KB93 lines

src/components/ChatView.tsx

  1. import { useEffect, useLayoutEffect, useRef } from 'react'
  2. import { mod } from '../lib/theme'
  3. import { useCurrentModel, useStore } from '../state/store'
  4. import { MessageBubble } from './MessageBubble'
  5. const SUGGESTIONS = [
  6. 'Explain this error message in plain words',
  7. 'Tighten a paragraph without changing its voice',
  8. 'Draft a small shell script and review it for safety',
  9. ]
  10. export function ChatView({ onSuggest }: { onSuggest: (text: string) => void }) {
  11. const view = useStore((s) => s.view)
  12. const streaming = useStore((s) => s.streaming)
  13. const keys = useStore((s) => s.keys)
  14. const setPanel = useStore((s) => s.setPanel)
  15. const current = useCurrentModel()
  16. const providers = useStore((s) => s.providers)
  17. const scroller = useRef<HTMLDivElement>(null)
  18. const pinned = useRef(true)
  19. // Follow the reply while it streams, unless the reader has scrolled up.
  20. useLayoutEffect(() => {
  21. const el = scroller.current
  22. if (el && pinned.current) el.scrollTop = el.scrollHeight
  23. }, [view?.messages.length, streaming?.text])
  24. useEffect(() => {
  25. pinned.current = true
  26. }, [view?.conversation.id])
  27. const provider = providers.find((p) => p.id === current.providerId)
  28. const needsKey = provider?.requires_key && keys[provider.id] === false
  29. if (!view || view.messages.length === 0) {
  30. return (
  31. <div className="chat-scroll" ref={scroller}>
  32. <div className="empty">
  33. <div className="empty-mark" aria-hidden />
  34. <h1>What are we working on?</h1>
  35. {needsKey ? (
  36. <p className="muted">
  37. Add your {provider?.name} API key to start.{' '}
  38. <button className="link" onClick={() => setPanel('settings')}>
  39. Open Settings
  40. </button>
  41. </p>
  42. ) : (
  43. <p className="muted">
  44. Every reply shows what it cost. {mod}K searches past chats; {mod}N starts a new one.
  45. </p>
  46. )}
  47. <div className="suggestions">
  48. {SUGGESTIONS.map((s) => (
  49. <button key={s} className="suggestion glass-tint" onClick={() => onSuggest(s)}>
  50. {s}
  51. </button>
  52. ))}
  53. </div>
  54. </div>
  55. </div>
  56. )
  57. }
  58. const lastAssistant = [...view.messages].reverse().find((m) => m.role === 'assistant')
  59. return (
  60. <div
  61. className="chat-scroll"
  62. ref={scroller}
  63. onScroll={(e) => {
  64. const el = e.currentTarget
  65. pinned.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80
  66. }}
  67. >
  68. <div className="thread" data-testid="thread">
  69. {view.conversation.system_prompt && (
  70. <details className="system-prompt">
  71. <summary>System prompt</summary>
  72. <div className="selectable">{view.conversation.system_prompt}</div>
  73. </details>
  74. )}
  75. {view.messages.map((m) => (
  76. <MessageBubble
  77. key={m.id}
  78. m={m}
  79. live={streaming && streaming.assistantId === m.id ? streaming : null}
  80. isLastAssistant={m.id === lastAssistant?.id}
  81. />
  82. ))}
  83. </div>
  84. </div>
  85. )
  86. }