ChatView.tsx
Language TypeScript3.1 KB93 lines
import { useEffect, useLayoutEffect, useRef } from 'react'import { mod } from '../lib/theme'import { useCurrentModel, useStore } from '../state/store'import { MessageBubble } from './MessageBubble'const SUGGESTIONS = ['Explain this error message in plain words','Tighten a paragraph without changing its voice','Draft a small shell script and review it for safety',]export function ChatView({ onSuggest }: { onSuggest: (text: string) => void }) {const view = useStore((s) => s.view)const streaming = useStore((s) => s.streaming)const keys = useStore((s) => s.keys)const setPanel = useStore((s) => s.setPanel)const current = useCurrentModel()const providers = useStore((s) => s.providers)const scroller = useRef<HTMLDivElement>(null)const pinned = useRef(true)// Follow the reply while it streams, unless the reader has scrolled up.useLayoutEffect(() => {const el = scroller.currentif (el && pinned.current) el.scrollTop = el.scrollHeight}, [view?.messages.length, streaming?.text])useEffect(() => {pinned.current = true}, [view?.conversation.id])const provider = providers.find((p) => p.id === current.providerId)const needsKey = provider?.requires_key && keys[provider.id] === falseif (!view || view.messages.length === 0) {return (<div className="chat-scroll" ref={scroller}><div className="empty"><div className="empty-mark" aria-hidden /><h1>What are we working on?</h1>{needsKey ? (<p className="muted">Add your {provider?.name} API key to start.{' '}<button className="link" onClick={() => setPanel('settings')}>Open Settings</button></p>) : (<p className="muted">Every reply shows what it cost. {mod}K searches past chats; {mod}N starts a new one.</p>)}<div className="suggestions">{SUGGESTIONS.map((s) => (<button key={s} className="suggestion glass-tint" onClick={() => onSuggest(s)}>{s}</button>))}</div></div></div>)}const lastAssistant = [...view.messages].reverse().find((m) => m.role === 'assistant')return (<divclassName="chat-scroll"ref={scroller}onScroll={(e) => {const el = e.currentTargetpinned.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80}}><div className="thread" data-testid="thread">{view.conversation.system_prompt && (<details className="system-prompt"><summary>System prompt</summary><div className="selectable">{view.conversation.system_prompt}</div></details>)}{view.messages.map((m) => (<MessageBubblekey={m.id}m={m}live={streaming && streaming.assistantId === m.id ? streaming : null}isLastAssistant={m.id === lastAssistant?.id}/>))}</div></div>)}