{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-chat",
  "title": "Ai Chat",
  "description": "A complete chat surface — conversation, streamed messages, reasoning, tools, sources, suggestions and composer — composed from the AI primitives with mocked local streaming.",
  "dependencies": [
    "@untitledui/icons@^0.0.22"
  ],
  "registryDependencies": [
    "@nx-ui/ai-conversation",
    "@nx-ui/ai-loader",
    "@nx-ui/ai-message",
    "@nx-ui/ai-prompt-input",
    "@nx-ui/ai-reasoning",
    "@nx-ui/ai-sources",
    "@nx-ui/ai-tool",
    "@nx-ui/ai-types",
    "@nx-ui/button",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/ai-chat.tsx",
      "content": "\"use client\"\n\n/**\n * ai-chat — a composed, opinionated chat surface assembled from the nx-ui AI\n * Elements primitives. It wires a sticky-to-bottom conversation, streamed\n * assistant messages (reasoning trace, tool call, cited sources), suggestion\n * chips and a full composer into one block, driven entirely by mocked local\n * streaming — no backend, no AI SDK runtime.\n *\n * It is registered as a block so a consumer can vendor the whole chat screen in\n * one `shadcn add @nx-ui/ai-chat`, then swap the mock stream for their own\n * `useChat`/transport. Every part it uses is an `@nx-ui/ai-*` registry item.\n */\n\nimport * as React from \"react\"\nimport { Copy01, RefreshCw01, ThumbsUp } from \"@untitledui/icons\"\n\nimport {\n  Conversation,\n  ConversationContent,\n  ConversationEmptyState,\n  ConversationScrollButton,\n} from \"@/components/ai/conversation\"\nimport {\n  Message,\n  MessageAction,\n  MessageActions,\n  MessageContent,\n  MessageResponse,\n  MessageToolbar,\n} from \"@/components/ai/message\"\nimport {\n  Reasoning,\n  ReasoningContent,\n  ReasoningTrigger,\n} from \"@/components/ai/reasoning\"\nimport {\n  Tool,\n  ToolContent,\n  ToolHeader,\n  ToolInput,\n  ToolOutput,\n} from \"@/components/ai/tool\"\nimport { Source, Sources, SourcesContent, SourcesTrigger } from \"@/components/ai/sources\"\nimport { Loader } from \"@/components/ai/loader\"\nimport {\n  PromptInput,\n  PromptInputActionMenu,\n  PromptInputActionMenuContent,\n  PromptInputActionMenuItem,\n  PromptInputActionMenuTrigger,\n  PromptInputBody,\n  type PromptInputMessage,\n  PromptInputSelect,\n  PromptInputSelectContent,\n  PromptInputSelectItem,\n  PromptInputSelectTrigger,\n  PromptInputSelectValue,\n  PromptInputSubmit,\n  PromptInputTextarea,\n  PromptInputTools,\n} from \"@/components/ai/prompt-input\"\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\nimport type { ChatStatus, ToolUIPart } from \"@/components/ai/types\"\n\n/** A single turn in the mock transcript. */\ntype ChatTurn = {\n  id: string\n  role: \"user\" | \"assistant\"\n  /** The reasoning trace shown above an assistant answer. */\n  reasoning?: string\n  /** A tool invocation rendered before the answer. */\n  tool?: {\n    part: ToolUIPart\n    title: string\n  }\n  /** The assistant markdown answer, revealed token-by-token while streaming. */\n  content: string\n  /** Cited sources shown beneath the answer. */\n  sources?: { href: string; title: string }[]\n}\n\nconst SUGGESTIONS = [\n  \"How do I compost timber offcuts?\",\n  \"Summarise this week's intake\",\n  \"What's the C:N ratio of digestate?\",\n]\n\n/** A canned, deterministic assistant reply used for every prompt in the demo. */\nfunction buildReply(prompt: string): ChatTurn {\n  return {\n    id: `assistant-${Date.now()}`,\n    role: \"assistant\",\n    reasoning:\n      \"The user is asking about waste-stream processing. I should check the site records for the relevant feedstock, then summarise the recommended handling in plain terms.\",\n    tool: {\n      title: \"search_site_records\",\n      part: {\n        type: \"tool-search_site_records\",\n        toolCallId: \"call_1\",\n        state: \"output-available\",\n        input: { query: prompt, limit: 3 },\n        output: {\n          matches: 2,\n          sites: [\"Hartfield Composting Site\", \"Cranbourne Organics\"],\n        },\n      },\n    },\n    content: `Based on the site records, here's the short version:\\n\\n- **Timber offcuts** shred well and add carbon to a compost mix.\\n- Keep the carbon-to-nitrogen ratio near **30:1** by balancing with green waste.\\n- Turn the windrow weekly and watch the core temperature.\\n\\nWould you like the full handling procedure for a specific site?`,\n    sources: [\n      { href: \"https://example.com/composting-guide\", title: \"Composting field guide\" },\n      { href: \"https://example.com/cn-ratios\", title: \"C:N ratios for common feedstocks\" },\n    ],\n  }\n}\n\nconst INITIAL: ChatTurn[] = [\n  {\n    id: \"user-0\",\n    role: \"user\",\n    content: \"What's the best way to handle timber offcuts on site?\",\n  },\n  buildReply(\"timber offcuts\"),\n]\n\n/**\n * AiChat — the composed chat block. Accepts an optional starting transcript and\n * renders at whatever height its container gives it (defaults to a tall panel).\n */\nexport function AiChat({\n  className,\n  initialTurns = INITIAL,\n}: {\n  className?: string\n  initialTurns?: ChatTurn[]\n}) {\n  const [turns, setTurns] = React.useState<ChatTurn[]>(initialTurns)\n  const [status, setStatus] = React.useState<ChatStatus>(\"ready\")\n  const [model, setModel] = React.useState(\"nexus-large\")\n  // Id of the assistant turn currently being revealed, and how many characters.\n  const [streamingId, setStreamingId] = React.useState<string | null>(null)\n  const [revealed, setRevealed] = React.useState(0)\n\n  const send = React.useCallback(\n    (text: string) => {\n      if (!text.trim() || status !== \"ready\") return\n\n      const userTurn: ChatTurn = {\n        id: `user-${Date.now()}`,\n        role: \"user\",\n        content: text.trim(),\n      }\n      const reply = buildReply(text.trim())\n\n      setTurns((prev) => [...prev, userTurn])\n      setStatus(\"submitted\")\n\n      // Mock the request→stream lifecycle with timers.\n      window.setTimeout(() => {\n        setTurns((prev) => [...prev, reply])\n        setStatus(\"streaming\")\n        setStreamingId(reply.id)\n        setRevealed(0)\n      }, 500)\n    },\n    [status]\n  )\n\n  // Reveal the streaming turn's content a few characters at a time.\n  React.useEffect(() => {\n    if (status !== \"streaming\" || !streamingId) return\n    const turn = turns.find((t) => t.id === streamingId)\n    if (!turn) return\n\n    if (revealed >= turn.content.length) {\n      const done = window.setTimeout(() => {\n        setStatus(\"ready\")\n        setStreamingId(null)\n      }, 300)\n      return () => window.clearTimeout(done)\n    }\n\n    const tick = window.setTimeout(() => {\n      setRevealed((n) => Math.min(n + 4, turn.content.length))\n    }, 16)\n    return () => window.clearTimeout(tick)\n  }, [status, streamingId, revealed, turns])\n\n  const handleSubmit = (message: PromptInputMessage) => {\n    send(message.text)\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex h-[36rem] w-full flex-col overflow-hidden rounded-xl border border-border bg-background\",\n        className\n      )}\n    >\n      <Conversation className=\"flex-1\">\n        <ConversationContent>\n          {turns.length === 0 ? (\n            <ConversationEmptyState\n              title=\"Ask about your sites\"\n              description=\"Start a conversation to see streamed answers with reasoning, tools and sources.\"\n            />\n          ) : (\n            turns.map((turn) => {\n              const isStreaming = turn.id === streamingId\n              const body = isStreaming\n                ? turn.content.slice(0, revealed)\n                : turn.content\n\n              if (turn.role === \"user\") {\n                return (\n                  <Message from=\"user\" key={turn.id}>\n                    <MessageContent>{turn.content}</MessageContent>\n                  </Message>\n                )\n              }\n\n              return (\n                <Message from=\"assistant\" key={turn.id}>\n                  <MessageContent>\n                    {turn.reasoning && (\n                      <Reasoning isStreaming={isStreaming} defaultOpen={false}>\n                        <ReasoningTrigger />\n                        <ReasoningContent>{turn.reasoning}</ReasoningContent>\n                      </Reasoning>\n                    )}\n\n                    {turn.tool && (\n                      <Tool>\n                        <ToolHeader\n                          title={turn.tool.title}\n                          type={turn.tool.part.type}\n                          state={turn.tool.part.state}\n                        />\n                        <ToolContent>\n                          <ToolInput input={turn.tool.part.input} />\n                          <ToolOutput\n                            output={turn.tool.part.output}\n                            errorText={turn.tool.part.errorText}\n                          />\n                        </ToolContent>\n                      </Tool>\n                    )}\n\n                    {body ? (\n                      <MessageResponse>{body}</MessageResponse>\n                    ) : (\n                      <Loader />\n                    )}\n\n                    {turn.sources && !isStreaming && (\n                      <Sources>\n                        <SourcesTrigger count={turn.sources.length} />\n                        <SourcesContent>\n                          {turn.sources.map((source) => (\n                            <Source\n                              href={source.href}\n                              key={source.href}\n                              title={source.title}\n                            />\n                          ))}\n                        </SourcesContent>\n                      </Sources>\n                    )}\n                  </MessageContent>\n\n                  {!isStreaming && (\n                    <MessageToolbar>\n                      <MessageActions>\n                        <MessageAction tooltip=\"Copy\">\n                          <Copy01 className=\"size-4\" />\n                        </MessageAction>\n                        <MessageAction tooltip=\"Regenerate\">\n                          <RefreshCw01 className=\"size-4\" />\n                        </MessageAction>\n                        <MessageAction tooltip=\"Good response\">\n                          <ThumbsUp className=\"size-4\" />\n                        </MessageAction>\n                      </MessageActions>\n                    </MessageToolbar>\n                  )}\n                </Message>\n              )\n            })\n          )}\n        </ConversationContent>\n        <ConversationScrollButton />\n      </Conversation>\n\n      <div className=\"border-t border-border p-4\">\n        <div className=\"mb-3 flex flex-wrap gap-2\">\n          {SUGGESTIONS.map((suggestion) => (\n            <Button\n              key={suggestion}\n              variant=\"outline\"\n              size=\"sm\"\n              className=\"rounded-full\"\n              onClick={() => send(suggestion)}\n              disabled={status !== \"ready\"}\n            >\n              {suggestion}\n            </Button>\n          ))}\n        </div>\n\n        <PromptInput onSubmit={handleSubmit}>\n          <PromptInputBody>\n            <PromptInputTextarea placeholder=\"Ask about a waste stream, site or intake…\" />\n          </PromptInputBody>\n          <PromptInputTools>\n            <PromptInputActionMenu>\n              <PromptInputActionMenuTrigger />\n              <PromptInputActionMenuContent>\n                <PromptInputActionMenuItem>\n                  Add context\n                </PromptInputActionMenuItem>\n                <PromptInputActionMenuItem>\n                  Attach a file\n                </PromptInputActionMenuItem>\n              </PromptInputActionMenuContent>\n            </PromptInputActionMenu>\n            <PromptInputSelect\n              value={model}\n              onValueChange={(value) => setModel(value as string)}\n            >\n              <PromptInputSelectTrigger>\n                <PromptInputSelectValue />\n              </PromptInputSelectTrigger>\n              <PromptInputSelectContent>\n                <PromptInputSelectItem value=\"nexus-large\">\n                  Nexus Large\n                </PromptInputSelectItem>\n                <PromptInputSelectItem value=\"nexus-fast\">\n                  Nexus Fast\n                </PromptInputSelectItem>\n              </PromptInputSelectContent>\n            </PromptInputSelect>\n            <PromptInputSubmit className=\"ml-auto\" status={status} />\n          </PromptInputTools>\n        </PromptInput>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:block"
}