{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-screen",
  "title": "Chat Screen",
  "description": "Thread list, message log and reply bar, wired to Stream Response.",
  "registryDependencies": [
    "https://spotcheckui.com/r/spotcheck-base.json",
    "https://spotcheckui.com/r/app-frame.json",
    "https://spotcheckui.com/r/stream-response.json",
    "https://spotcheckui.com/r/use-stream.json"
  ],
  "files": [
    {
      "path": "registry/spotcheck/components/spotcheck/chat-screen.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { AppFrame } from \"@/registry/spotcheck/components/spotcheck/app-frame\"\nimport { SavedResponse, StreamResponse } from \"@/registry/spotcheck/components/spotcheck/stream-response\"\nimport { useStream, type StreamSource } from \"@/registry/spotcheck/hooks/use-stream\"\n\nexport type ChatMessage = { role: \"user\" | \"assistant\"; text: string; sources?: StreamSource[] }\nexport type ChatThread = { id: string; title: string; when: string; messages: ChatMessage[] }\n\ntype ChatScreenProps = {\n  /** The endpoint that streams replies. It receives { messages } as JSON. */\n  api: string\n  threads: ChatThread[]\n  className?: string\n}\n\nexport function ChatScreen({ api, threads, className }: ChatScreenProps) {\n  const [all, setAll] = React.useState(threads)\n  const [activeId, setActiveId] = React.useState(threads[0]?.id)\n  const [draft, setDraft] = React.useState(\"\")\n  const stream = useStream(api)\n  const log = React.useRef<HTMLDivElement>(null)\n  const active = all.find((thread) => thread.id === activeId)\n  const streaming = stream.status === \"streaming\"\n\n  const append = (id: string | undefined, messages: ChatMessage[]) =>\n    setAll((current) =>\n      current.map((thread) => (thread.id === id ? { ...thread, messages: [...thread.messages, ...messages] } : thread)),\n    )\n\n  // The live reply, as a message that can be saved to the thread it arrived in.\n  const liveReply = (): ChatMessage[] =>\n    stream.text ? [{ role: \"assistant\", text: stream.text, sources: stream.sources }] : []\n\n  const send = (event: React.FormEvent) => {\n    event.preventDefault()\n    const text = draft.trim()\n    if (!text || streaming || !active) return\n    const added: ChatMessage[] = [...liveReply(), { role: \"user\", text }]\n    append(active.id, added)\n    setDraft(\"\")\n    void stream.start({ messages: [...active.messages, ...added] })\n  }\n\n  const choose = (id: string) => {\n    if (id === activeId) return\n    append(activeId, liveReply())\n    stream.reset()\n    setActiveId(id)\n  }\n\n  React.useEffect(() => {\n    log.current?.scrollTo({ top: log.current.scrollHeight })\n  }, [active?.messages.length, stream.text])\n\n  return (\n    <AppFrame title=\"assistant\" className={className}>\n      <div className=\"grid h-[440px] grid-rows-[auto_minmax(0,1fr)] md:grid-cols-[200px_minmax(0,1fr)] md:grid-rows-1\">\n        <nav\n          aria-label=\"Conversations\"\n          className=\"border-ink flex [scrollbar-width:none] overflow-x-auto border-b-(length:--rule) text-[13px] md:flex-col md:overflow-x-hidden md:overflow-y-auto md:border-r-(length:--rule) md:border-b-0\"\n        >\n          {all.map((thread) => (\n            <button\n              key={thread.id}\n              type=\"button\"\n              aria-current={thread.id === activeId ? \"true\" : undefined}\n              onClick={() => choose(thread.id)}\n              className={cn(\n                \"spotcheck-focus flex-none cursor-pointer px-3 pt-2 pb-1 text-left -outline-offset-2 md:truncate\",\n                thread.id === activeId && \"bg-accent-ink text-on-accent\",\n              )}\n            >\n              <span className=\"block max-w-40 truncate md:max-w-none\">{thread.title}</span>\n              <small className=\"block font-(family-name:--f-mono) text-[10px]\">{thread.when}</small>\n            </button>\n          ))}\n        </nav>\n        <div className=\"flex min-h-0 min-w-0 flex-col\">\n          <div ref={log} role=\"log\" aria-label=\"Conversation\" className=\"flex grow flex-col gap-3.5 overflow-auto p-3.5\">\n            {active?.messages.map((message, index) =>\n              message.role === \"user\" ? (\n                <div key={index} className=\"bg-ink text-paper max-w-[78%] self-end px-3 py-2 text-sm\">\n                  {message.text}\n                </div>\n              ) : (\n                <div key={index} className=\"max-w-[88%] text-sm\">\n                  <div className=\"spotcheck-label mb-0.5\">Assistant</div>\n                  <SavedResponse text={message.text} sources={message.sources} />\n                </div>\n              ),\n            )}\n            {stream.status !== \"idle\" && (\n              <div className=\"max-w-[88%] text-sm\">\n                <div className=\"spotcheck-label mb-0.5\">Assistant</div>\n                <StreamResponse\n                  text={stream.text}\n                  sources={stream.sources}\n                  status={stream.status}\n                  error={stream.error}\n                  onStop={stream.stop}\n                />\n              </div>\n            )}\n          </div>\n          <form onSubmit={send} className=\"border-ink flex border-t-(length:--rule)\">\n            <input\n              value={draft}\n              aria-label=\"Reply\"\n              placeholder=\"Reply\"\n              onChange={(event) => setDraft(event.target.value)}\n              className=\"placeholder:text-mute min-w-0 flex-1 bg-transparent px-3 py-2.5 text-sm outline-none\"\n            />\n            <button\n              type=\"submit\"\n              disabled={streaming}\n              className=\"spotcheck-focus border-ink bg-accent-ink text-on-accent cursor-pointer border-l-(length:--rule) px-3.5 font-(family-name:--f-mono) text-xs uppercase disabled:cursor-not-allowed disabled:opacity-45\"\n            >\n              Send\n            </button>\n          </form>\n        </div>\n      </div>\n    </AppFrame>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "screens"
  ],
  "type": "registry:component"
}