{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rag-chatbot",
  "title": "RAG Chatbot",
  "description": "A grounded document support chatbot with a portable sample index, optional pgvector ingestion, and visible tool traces.",
  "dependencies": [
    "@ai-sdk/react",
    "@base-ui/react",
    "@neondatabase/serverless",
    "@phosphor-icons/react",
    "ai",
    "class-variance-authority",
    "drizzle-orm",
    "lucide-react",
    "pdfjs-dist",
    "zod"
  ],
  "registryDependencies": [
    "utils",
    "badge",
    "breadcrumb",
    "button",
    "textarea",
    "https://elements.ai-sdk.dev/api/registry/conversation.json",
    "https://elements.ai-sdk.dev/api/registry/message.json",
    "https://elements.ai-sdk.dev/api/registry/sources.json",
    "https://elements.ai-sdk.dev/api/registry/tool.json"
  ],
  "files": [
    {
      "path": "registry/rag-chatbot/app/demos/rag-chatbot/page.tsx",
      "content": "import { RagChatbotScreen } from \"@/components/rag-chatbot/rag-chatbot-screen\";\n\nexport const dynamic = \"force-dynamic\";\n\nexport default async function RagChatbotPage() {\n  return <RagChatbotScreen />;\n}\n",
      "type": "registry:page",
      "target": "app/demos/rag-chatbot/page.tsx"
    },
    {
      "path": "registry/rag-chatbot/app/api/demos/rag-chatbot/route.ts",
      "content": "import { handleRagChatbotRequest } from \"@/lib/rag-chatbot/runtime\";\n\nexport const runtime = \"nodejs\";\nexport const maxDuration = 30;\n\nexport function POST(request: Request) {\n  return handleRagChatbotRequest(request);\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/rag-chatbot/route.ts"
    },
    {
      "path": "registry/rag-chatbot/app/api/demos/rag-chatbot/index/route.ts",
      "content": "import {\n  getRagChatbotEnv,\n  getRagChatbotIndexSetupIssue,\n} from \"@/lib/rag-chatbot/env\";\nimport { indexRagChatbotSource } from \"@/lib/rag-chatbot/index-source\";\n\nexport const runtime = \"nodejs\";\nexport const maxDuration = 30;\n\nexport async function POST(_request: Request) {\n  const env = getRagChatbotEnv();\n\n  if (env.NODE_ENV === \"production\") {\n    return Response.json(\n      {\n        error:\n          \"Source indexing is disabled in production. Run ingestion through a trusted setup job.\",\n      },\n      { status: 403 }\n    );\n  }\n\n  const setupIssue = getRagChatbotIndexSetupIssue(env);\n\n  if (setupIssue) {\n    return Response.json(\n      {\n        error: setupIssue,\n      },\n      { status: 500 }\n    );\n  }\n\n  try {\n    const result = await indexRagChatbotSource(env);\n\n    return Response.json(result);\n  } catch (error) {\n    return Response.json(\n      {\n        error:\n          error instanceof Error\n            ? error.message\n            : \"Failed to index the RAG source document.\",\n      },\n      { status: 500 }\n    );\n  }\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/rag-chatbot/index/route.ts"
    },
    {
      "path": "registry/rag-chatbot/components/rag-chatbot/rag-chatbot-screen.tsx",
      "content": "import { Badge } from \"@/components/ui/badge\";\nimport {\n  Breadcrumb,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbList,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n} from \"@/components/ui/breadcrumb\";\nimport { ArrowLeft } from \"lucide-react\";\n\nimport { getRagChatbotRuntimeState } from \"@/lib/rag-chatbot/runtime\";\nimport { RagChatbotWorkspace } from \"@/components/rag-chatbot/rag-chatbot-workspace\";\n\nexport async function RagChatbotScreen() {\n  const runtimeState = await getRagChatbotRuntimeState();\n\n  return (\n    <main className=\"min-h-svh bg-background text-foreground\">\n      <div className=\"mx-auto flex w-full max-w-7xl flex-col gap-6 px-4 py-6 md:px-6\">\n        <header className=\"grid gap-4 border border-foreground/10 bg-background px-4 py-5 md:grid-cols-[minmax(0,1fr)_auto] md:items-end\">\n          <div className=\"space-y-2\">\n            <Breadcrumb>\n              <BreadcrumbList className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n                <BreadcrumbItem>\n                  <BreadcrumbLink\n                    aria-label=\"Back to demos\"\n                    className=\"-ml-1 inline-flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground\"\n                    href=\"/\"\n                  >\n                    <ArrowLeft aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n                    <span>Demo</span>\n                  </BreadcrumbLink>\n                </BreadcrumbItem>\n                <BreadcrumbSeparator className=\"text-muted-foreground\">\n                  /\n                </BreadcrumbSeparator>\n                <BreadcrumbItem>\n                  <BreadcrumbPage className=\"font-normal text-muted-foreground\">\n                    RAG Chatbot\n                  </BreadcrumbPage>\n                </BreadcrumbItem>\n              </BreadcrumbList>\n            </Breadcrumb>\n            <h1 className=\"max-w-3xl font-medium text-2xl tracking-tight\">\n              Grounded document support chat over a portable design manual index\n            </h1>\n            <p className=\"max-w-3xl text-muted-foreground text-sm/relaxed\">\n              This slice turns the official AI SDK RAG recipe into a website\n              support chatbot: retrieval-first answers, visible tool state, and\n              source snippets that work out of the box with an optional\n              pgvector upgrade path.\n            </p>\n          </div>\n\n          <div className=\"flex flex-wrap items-center gap-2\">\n            <Badge variant=\"outline\">{runtimeState.statusLabel}</Badge>\n            <Badge variant=\"outline\">{runtimeState.chatModel}</Badge>\n          </div>\n        </header>\n\n        <div className=\"lg:h-svh\">\n          <RagChatbotWorkspace\n            chatModel={runtimeState.chatModel}\n            isChatAvailable={runtimeState.isChatAvailable}\n            nodeVersion={runtimeState.nodeVersion}\n            retrievalLabel={runtimeState.retrievalLabel}\n            sourceDocument={runtimeState.sourceDocument}\n            setupMessage={runtimeState.setupMessage}\n          />\n        </div>\n      </div>\n    </main>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/rag-chatbot/rag-chatbot-screen.tsx"
    },
    {
      "path": "registry/rag-chatbot/components/rag-chatbot/rag-chatbot-workspace.tsx",
      "content": "\"use client\";\n\nimport {\n  ArrowClockwiseIcon,\n  BookOpenIcon,\n  RobotIcon,\n  StopIcon,\n} from \"@phosphor-icons/react\";\nimport {\n  Conversation,\n  ConversationContent,\n  ConversationEmptyState,\n  ConversationScrollButton,\n} from \"@/components/ai-elements/conversation\";\nimport {\n  Message,\n  MessageContent,\n  MessageResponse,\n} from \"@/components/ai-elements/message\";\nimport {\n  PromptInput,\n  PromptInputBody,\n  PromptInputFooter,\n  PromptInputSubmit,\n  PromptInputTextarea,\n} from \"@/components/ai-elements/prompt-input\";\nimport {\n  Source,\n  Sources,\n  SourcesContent,\n  SourcesTrigger,\n} from \"@/components/ai-elements/sources\";\nimport {\n  Tool,\n  ToolContent,\n  ToolHeader,\n  ToolInput,\n  ToolOutput,\n} from \"@/components/ai-elements/tool\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button, buttonVariants } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport Link from \"next/link\";\nimport { useMemo } from \"react\";\n\nimport type { ragChatbotSourceDocument } from \"@/lib/rag-chatbot/source-document\";\n\nimport {\n  getGroundedSourceKey,\n  projectGroundedMessage,\n} from \"./grounded-response\";\nimport { useRagChatbot } from \"./use-rag-chatbot\";\n\nexport interface RagChatbotWorkspaceProps {\n  chatModel: string;\n  isChatAvailable: boolean;\n  nodeVersion: string;\n  retrievalLabel: string;\n  setupMessage: string | null;\n  sourceDocument: typeof ragChatbotSourceDocument;\n}\n\nexport function RagChatbotWorkspace({\n  chatModel,\n  isChatAvailable,\n  nodeVersion,\n  retrievalLabel,\n  sourceDocument,\n  setupMessage,\n}: RagChatbotWorkspaceProps) {\n  const {\n    error,\n    hasMessages,\n    isBusy,\n    messages,\n    regenerate,\n    sendMessage,\n    status,\n    stop,\n  } = useRagChatbot();\n  const samplePrompts = useMemo(\n    () => [\n      \"What does the manual say about the NASA logotype?\",\n      \"How should the seal and the logotype be used differently?\",\n      \"Summarize the core visual system described in this manual.\",\n    ],\n    []\n  );\n\n  return (\n    <div className=\"grid min-h-[70svh] gap-4 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n      <section className=\"flex min-h-[70svh] flex-col border border-foreground/10 bg-background lg:h-full lg:min-h-0\">\n        {isChatAvailable ? null : (\n          <div className=\"border-foreground/10 border-b px-4 py-3 text-muted-foreground text-xs/relaxed\">\n            {setupMessage}\n          </div>\n        )}\n\n        {error ? (\n          <div className=\"border-foreground/10 border-b px-4 py-3 text-destructive text-xs/relaxed\">\n            {error.message}\n          </div>\n        ) : null}\n\n        <Conversation className=\"min-h-0\">\n          <ConversationContent className=\"mx-auto flex w-full max-w-3xl flex-1 gap-6 px-4 py-6\">\n            {hasMessages ? (\n              messages.map((message) => {\n                const projection = projectGroundedMessage(message);\n\n                return (\n                  <Message from={message.role} key={message.id}>\n                    <MessageContent\n                      className={cn(\n                        \"space-y-4\",\n                        message.role === \"assistant\" ? \"max-w-3xl\" : \"max-w-2xl\"\n                      )}\n                    >\n                      {projection.sources.length > 0 ? (\n                        <Sources>\n                          <SourcesTrigger count={projection.sources.length} />\n                          <SourcesContent>\n                            {projection.sources.map((source, index) => (\n                              <Source\n                                href={source.documentUrl}\n                                key={getGroundedSourceKey(\n                                  message.id,\n                                  source,\n                                  index\n                                )}\n                                title={source.citationLabel}\n                              >\n                                <span className=\"font-medium\">\n                                  {source.citationLabel}\n                                </span>\n                                {source.sectionTitle ? (\n                                  <span className=\"text-muted-foreground\">\n                                    {source.sectionTitle}\n                                  </span>\n                                ) : null}\n                              </Source>\n                            ))}\n                          </SourcesContent>\n                        </Sources>\n                      ) : null}\n\n                      {projection.text ? (\n                        <MessageResponse>{projection.text}</MessageResponse>\n                      ) : (\n                        <p className=\"text-muted-foreground text-sm\">\n                          Waiting for visible output.\n                        </p>\n                      )}\n\n                      {projection.toolParts.map((part) => (\n                        <Tool key={part.toolCallId}>\n                          {part.type === \"dynamic-tool\" ? (\n                            <ToolHeader\n                              state={part.state}\n                              title=\"Knowledge Base Retrieval\"\n                              toolName={part.toolName}\n                              type={part.type}\n                            />\n                          ) : (\n                            <ToolHeader\n                              state={part.state}\n                              title=\"Knowledge Base Retrieval\"\n                              type={part.type}\n                            />\n                          )}\n                          <ToolContent>\n                            {part.input ? (\n                              <ToolInput input={part.input} />\n                            ) : null}\n                            <ToolOutput\n                              errorText={part.errorText}\n                              output={part.output}\n                            />\n                          </ToolContent>\n                        </Tool>\n                      ))}\n                    </MessageContent>\n                  </Message>\n                );\n              })\n            ) : (\n              <ConversationEmptyState\n                description=\"Ask about the bundled design manual index. Every grounded answer is expected to flow through retrieval.\"\n                icon={<BookOpenIcon className=\"size-5\" />}\n                title=\"Document support workspace is ready\"\n              />\n            )}\n          </ConversationContent>\n          <ConversationScrollButton />\n        </Conversation>\n\n        <div className=\"border-foreground/10 border-t px-4 py-4\">\n          <div className=\"mx-auto w-full max-w-3xl\">\n            <PromptInput onSubmit={({ text }) => sendMessage({ text })}>\n              <PromptInputBody>\n                <PromptInputTextarea\n                  disabled={!isChatAvailable || isBusy}\n                  placeholder=\"Ask what the manual says, and inspect the retrieval trace underneath the answer.\"\n                />\n              </PromptInputBody>\n              <PromptInputFooter className=\"flex items-center justify-between gap-3 border-foreground/10 border-t px-3 py-3\">\n                <div className=\"flex flex-wrap items-center gap-2\">\n                  <Badge variant=\"outline\">RAG</Badge>\n                  <Badge variant=\"outline\">{retrievalLabel}</Badge>\n                  <Badge variant=\"outline\">{chatModel}</Badge>\n                </div>\n                <div className=\"flex items-center gap-2\">\n                  {isBusy ? (\n                    <Button\n                      onClick={stop}\n                      size=\"sm\"\n                      type=\"button\"\n                      variant=\"outline\"\n                    >\n                      <StopIcon className=\"size-3.5\" />\n                      Stop\n                    </Button>\n                  ) : null}\n                  {hasMessages ? (\n                    <Button\n                      onClick={() => regenerate()}\n                      size=\"sm\"\n                      type=\"button\"\n                      variant=\"outline\"\n                    >\n                      <ArrowClockwiseIcon className=\"size-3.5\" />\n                      Retry\n                    </Button>\n                  ) : null}\n                  <PromptInputSubmit\n                    disabled={!isChatAvailable}\n                    status={status}\n                  />\n                </div>\n              </PromptInputFooter>\n            </PromptInput>\n\n            {hasMessages ? null : (\n              <div className=\"mt-3 flex flex-wrap gap-2\">\n                {samplePrompts.map((prompt) => (\n                  <Button\n                    key={prompt}\n                    onClick={() => sendMessage({ text: prompt })}\n                    size=\"sm\"\n                    type=\"button\"\n                    variant=\"outline\"\n                  >\n                    <RobotIcon className=\"size-3.5\" />\n                    {prompt}\n                  </Button>\n                ))}\n              </div>\n            )}\n          </div>\n        </div>\n      </section>\n\n      <aside className=\"border border-foreground/10 bg-background p-4 lg:min-h-0 lg:overflow-y-auto\">\n        <div className=\"space-y-4\">\n          <div>\n            <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Runtime\n            </p>\n            <p className=\"mt-1 font-medium text-sm\">{nodeVersion}</p>\n          </div>\n          <div>\n            <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Source document\n            </p>\n            <p className=\"mt-1 font-medium text-sm\">{sourceDocument.title}</p>\n            <p className=\"mt-1 text-muted-foreground text-sm\">\n              {sourceDocument.description}\n            </p>\n            <div className=\"mt-2 flex flex-wrap gap-2\">\n              <Link\n                className={buttonVariants({ size: \"sm\", variant: \"outline\" })}\n                href={sourceDocument.documentUrl}\n                target=\"_blank\"\n              >\n                Open PDF\n              </Link>\n              <Link\n                className={buttonVariants({ size: \"sm\", variant: \"outline\" })}\n                href={sourceDocument.sourcePageUrl}\n                target=\"_blank\"\n              >\n                NASA page\n              </Link>\n            </div>\n          </div>\n          <div>\n            <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Contract\n            </p>\n            <p className=\"mt-1 text-sm\">\n              The assistant is expected to retrieve evidence first, answer only\n              from the portable sample index or optional pgvector index, and\n              surface the retrieval trace alongside the response.\n            </p>\n          </div>\n        </div>\n      </aside>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/rag-chatbot/rag-chatbot-workspace.tsx"
    },
    {
      "path": "registry/rag-chatbot/components/rag-chatbot/use-rag-chatbot.ts",
      "content": "\"use client\";\n\nimport { Chat, useChat } from \"@ai-sdk/react\";\nimport { DefaultChatTransport, type UIMessage } from \"ai\";\nimport { useState } from \"react\";\n\nexport function useRagChatbot() {\n  const [chat] = useState(\n    () =>\n      new Chat<UIMessage>({\n        transport: new DefaultChatTransport({\n          api: \"/api/demos/rag-chatbot\",\n        }),\n      })\n  );\n  const controller = useChat({ chat });\n  const hasMessages = controller.messages.length > 0;\n  const isBusy =\n    controller.status === \"submitted\" || controller.status === \"streaming\";\n\n  return {\n    ...controller,\n    hasMessages,\n    isBusy,\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/rag-chatbot/use-rag-chatbot.ts"
    },
    {
      "path": "registry/rag-chatbot/components/rag-chatbot/grounded-response.ts",
      "content": "import { isToolUIPart, type UIMessage } from \"ai\";\nimport type { ToolPart } from \"@/components/ai-elements/tool\";\n\nexport interface GroundedSource {\n  citationLabel: string;\n  content: string;\n  documentUrl: string;\n  pageLabel: string | null;\n  sectionTitle: string | null;\n  similarity: number;\n  title: string;\n}\n\nexport interface GroundedMessageProjection {\n  sources: GroundedSource[];\n  text: string;\n  toolParts: ToolPart[];\n}\n\nfunction isGroundedSource(value: unknown): value is GroundedSource {\n  if (!value || typeof value !== \"object\") {\n    return false;\n  }\n\n  const source = value as Record<string, unknown>;\n\n  return (\n    typeof source.citationLabel === \"string\" &&\n    typeof source.content === \"string\" &&\n    typeof source.documentUrl === \"string\" &&\n    (source.pageLabel === null || typeof source.pageLabel === \"string\") &&\n    (source.sectionTitle === null ||\n      typeof source.sectionTitle === \"string\") &&\n    typeof source.similarity === \"number\" &&\n    typeof source.title === \"string\"\n  );\n}\n\nfunction readSourcesFromToolPart(part: ToolPart) {\n  if (part.state !== \"output-available\" || !part.output) {\n    return [];\n  }\n\n  if (typeof part.output !== \"object\" || !(\"sources\" in part.output)) {\n    return [];\n  }\n\n  const { sources } = part.output as { sources?: unknown[] };\n\n  return Array.isArray(sources)\n    ? sources.filter((source): source is GroundedSource => isGroundedSource(source))\n    : [];\n}\n\nexport function getGroundedSourceKey(\n  messageId: string,\n  source: GroundedSource,\n  index: number\n) {\n  return [\n    messageId,\n    source.citationLabel,\n    source.pageLabel ?? \"no-page\",\n    source.sectionTitle ?? \"no-section\",\n    source.content.slice(0, 96),\n    index,\n  ].join(\"::\");\n}\n\nexport function projectGroundedMessage(\n  message: UIMessage\n): GroundedMessageProjection {\n  const text = message.parts\n    .filter((part) => part.type === \"text\")\n    .map((part) => part.text)\n    .join(\"\\n\");\n  const toolParts = message.parts.filter(isToolUIPart) as ToolPart[];\n  const sources = toolParts.flatMap((part) => readSourcesFromToolPart(part));\n\n  return {\n    sources,\n    text,\n    toolParts,\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/rag-chatbot/grounded-response.ts"
    },
    {
      "path": "registry/rag-chatbot/components/ai-elements/prompt-input.tsx",
      "content": "\"use client\";\n\nimport { CornerDownLeftIcon, LoaderCircleIcon, SquareIcon } from \"lucide-react\";\nimport type {\n  ComponentProps,\n  FormEvent,\n  HTMLAttributes,\n  ReactNode,\n  TextareaHTMLAttributes,\n} from \"react\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\n\ntype PromptStatus = \"error\" | \"ready\" | \"streaming\" | \"submitted\";\n\ninterface PromptInputContextValue {\n  setText: (text: string) => void;\n  text: string;\n}\n\ninterface PromptInputMessage {\n  text: string;\n}\n\ninterface PromptInputProps\n  extends Omit<HTMLAttributes<HTMLFormElement>, \"onSubmit\"> {\n  children: ReactNode;\n  onSubmit: (\n    message: PromptInputMessage,\n    event: FormEvent<HTMLFormElement>\n  ) => void | Promise<void>;\n}\n\ninterface PromptInputSubmitProps\n  extends Omit<ComponentProps<typeof Button>, \"children\" | \"type\"> {\n  onStop?: () => void;\n  status?: PromptStatus;\n}\n\ntype PromptInputClickEvent = Parameters<\n  Exclude<ComponentProps<typeof Button>[\"onClick\"], undefined>\n>[0];\n\nconst PromptInputContext = createContext<PromptInputContextValue | null>(null);\n\nfunction usePromptInputContext() {\n  const context = useContext(PromptInputContext);\n\n  if (!context) {\n    throw new Error(\n      \"PromptInput components must be used inside <PromptInput>.\"\n    );\n  }\n\n  return context;\n}\n\nexport function PromptInput({\n  children,\n  className,\n  onSubmit,\n  ...props\n}: PromptInputProps) {\n  const [text, setText] = useState(\"\");\n\n  const contextValue = useMemo(\n    () => ({\n      setText,\n      text,\n    }),\n    [text]\n  );\n\n  const handleSubmit = useCallback(\n    async (event: FormEvent<HTMLFormElement>) => {\n      event.preventDefault();\n\n      const nextText = text.trim();\n      if (!nextText) {\n        return;\n      }\n\n      const result = onSubmit({ text: nextText }, event);\n\n      try {\n        await result;\n        setText(\"\");\n      } catch {\n        // Keep the text for retry after a failed submit.\n      }\n    },\n    [onSubmit, text]\n  );\n\n  return (\n    <PromptInputContext.Provider value={contextValue}>\n      <form\n        className={cn(\"w-full\", className)}\n        onSubmit={handleSubmit}\n        {...props}\n      >\n        {children}\n      </form>\n    </PromptInputContext.Provider>\n  );\n}\n\nexport function PromptInputBody({\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(\"grid gap-3\", className)} {...props} />;\n}\n\nexport function PromptInputFooter({\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(className)} {...props} />;\n}\n\nexport function PromptInputTextarea({\n  className,\n  disabled,\n  onChange,\n  ...props\n}: TextareaHTMLAttributes<HTMLTextAreaElement>) {\n  const context = usePromptInputContext();\n\n  return (\n    <Textarea\n      className={cn(\"min-h-16 resize-none\", className)}\n      disabled={disabled}\n      onChange={(event) => {\n        context.setText(event.currentTarget.value);\n        onChange?.(event);\n      }}\n      value={context.text}\n      {...props}\n    />\n  );\n}\n\nexport function PromptInputSubmit({\n  className,\n  disabled,\n  onClick,\n  onStop,\n  size = \"icon\",\n  status = \"ready\",\n  variant = \"default\",\n  ...props\n}: PromptInputSubmitProps) {\n  const { text } = usePromptInputContext();\n  const isBusy = status === \"submitted\" || status === \"streaming\";\n  const isDisabled = Boolean(disabled) || (!isBusy && text.trim().length === 0);\n\n  let icon = <CornerDownLeftIcon className=\"size-4\" />;\n\n  if (status === \"submitted\") {\n    icon = <LoaderCircleIcon className=\"size-4 animate-spin\" />;\n  } else if (status === \"streaming\") {\n    icon = <SquareIcon className=\"size-4\" />;\n  }\n\n  return (\n    <Button\n      aria-label={isBusy ? \"Stop\" : \"Submit\"}\n      className={cn(className)}\n      disabled={isDisabled}\n      onClick={(event: PromptInputClickEvent) => {\n        if (isBusy && onStop) {\n          event.preventDefault();\n          onStop();\n          return;\n        }\n\n        onClick?.(event);\n      }}\n      size={size}\n      type={isBusy && onStop ? \"button\" : \"submit\"}\n      variant={variant}\n      {...props}\n    >\n      {icon}\n    </Button>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ai-elements/prompt-input.tsx"
    },
    {
      "path": "registry/rag-chatbot/lib/rag-chatbot/chat.ts",
      "content": "import {\n  convertToModelMessages,\n  stepCountIs,\n  streamText,\n  tool,\n  type UIMessage,\n} from \"ai\";\nimport { z } from \"zod\";\n\nimport {\n  createRagChatbotGateway,\n  getRagChatbotConfig,\n  getRagChatbotEnv,\n  type RagChatbotEnv,\n} from \"./env\";\nimport { findRelevantContent } from \"./retrieval\";\n\nconst systemPrompt = [\n  \"You are the rag-chatbot demo for an independent-site document support agent.\",\n  \"Answer only with information grounded in the indexed document knowledge base.\",\n  \"Use the getInformation tool before answering any question about the document.\",\n  'If the tool returns no relevant evidence, reply exactly with \"Sorry, I don\\'t know.\"',\n  \"When evidence exists, answer concisely and mention the most relevant document section or page when possible.\",\n  \"Do not turn document guidance into legal, trademark, or commercial authorization claims.\",\n].join(\" \");\n\nexport async function streamRagChatbot(\n  messages: UIMessage[],\n  env: RagChatbotEnv = getRagChatbotEnv()\n) {\n  const gateway = createRagChatbotGateway(env);\n  const { chatModel } = getRagChatbotConfig(env);\n\n  const result = streamText({\n    model: gateway(chatModel),\n    system: systemPrompt,\n    messages: await convertToModelMessages(messages),\n    stopWhen: stepCountIs(5),\n    tools: {\n      getInformation: tool({\n        description:\n          \"Retrieve grounded document snippets from the indexed knowledge base to answer the user's question.\",\n        inputSchema: z.object({\n          question: z\n            .string()\n            .min(1)\n            .describe(\"The user's question about the indexed document.\"),\n        }),\n        execute: async ({ question }) => findRelevantContent(question, env),\n      }),\n    },\n  });\n\n  return result.toUIMessageStreamResponse();\n}\n",
      "type": "registry:lib",
      "target": "@lib/rag-chatbot/chat.ts"
    },
    {
      "path": "registry/rag-chatbot/lib/rag-chatbot/database.ts",
      "content": "import { Pool } from \"@neondatabase/serverless\";\nimport { drizzle } from \"drizzle-orm/neon-serverless\";\n\nimport {\n  getRagChatbotDatabaseConfig,\n  getRagChatbotEnv,\n  type RagChatbotEnv,\n} from \"./env\";\nimport {\n  ragChatbotEmbeddings,\n  ragChatbotResources,\n  ragChatbotSchema,\n} from \"./schema\";\n\nfunction createRagChatbotDatabase(connectionString: string) {\n  const client = new Pool({ connectionString });\n  const database = drizzle({\n    client,\n    schema: ragChatbotSchema,\n  });\n\n  return { client, database };\n}\n\ntype RagChatbotDatabase = ReturnType<\n  typeof createRagChatbotDatabase\n>[\"database\"];\n\nexport interface RagChatbotDatabaseModule {\n  database: RagChatbotDatabase;\n  ragChatbotEmbeddings: typeof ragChatbotEmbeddings;\n  ragChatbotResources: typeof ragChatbotResources;\n}\n\nlet databaseModulePromise: Promise<RagChatbotDatabaseModule> | null = null;\n\nexport async function loadRagChatbotDatabase(\n  env: RagChatbotEnv = getRagChatbotEnv()\n): Promise<RagChatbotDatabaseModule> {\n  if (!databaseModulePromise) {\n    const { databaseUrl } = getRagChatbotDatabaseConfig(env);\n    const { database } = createRagChatbotDatabase(databaseUrl);\n\n    databaseModulePromise = Promise.resolve({\n      database,\n      ragChatbotEmbeddings,\n      ragChatbotResources,\n    });\n  }\n\n  return databaseModulePromise;\n}\n",
      "type": "registry:lib",
      "target": "@lib/rag-chatbot/database.ts"
    },
    {
      "path": "registry/rag-chatbot/lib/ai-gateway/contract.ts",
      "content": "import { createGateway } from \"ai\";\n\nexport const DEFAULT_GATEWAY_BASE_URL = \"https://ai-gateway.vercel.sh/v3/ai\";\nexport const MINIMUM_NODE_VERSION = \"22.13.0\";\nconst nodeVersionPattern = /^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:[-+].*)?$/;\n\nexport type AiGatewayEnvRecord = Record<string, string | undefined>;\n\nexport interface ParsedNodeVersion {\n  major: number;\n  minor: number;\n  patch: number;\n}\n\nexport interface AiGatewayContractConfig {\n  apiKey: string;\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewayResolvedEnv {\n  apiKey: string | undefined;\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewaySetupConfig {\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewayContractSetupState<\n  TConfig extends AiGatewaySetupConfig = AiGatewaySetupConfig,\n> {\n  config: TConfig;\n  isReady: boolean;\n  issues: string[];\n  nodeVersion: string;\n}\n\nexport interface AiGatewayContractOptions<\n  TConfig extends AiGatewaySetupConfig = AiGatewaySetupConfig,\n> {\n  buildConfig?: (\n    resolvedEnv: AiGatewayResolvedEnv,\n    env: AiGatewayEnvRecord\n  ) => TConfig;\n  defaultBaseURL?: string;\n  defaultChatModel: string;\n  getAdditionalIssues?: (\n    resolvedEnv: AiGatewayResolvedEnv,\n    env: AiGatewayEnvRecord\n  ) => string[];\n  missingApiKeyError: string;\n  missingApiKeyIssue?: string;\n}\n\nconst genericMissingApiKeyIssue =\n  \"AI_GATEWAY_API_KEY is missing. The demo can render, but chat requests will fail until it is configured.\";\n\nexport function parseNodeVersion(version: string): ParsedNodeVersion {\n  const match = nodeVersionPattern.exec(version);\n  const major = Number(match?.[1]);\n  const minor = Number(match?.[2]);\n  const patch = Number(match?.[3]);\n\n  if (![major, minor, patch].every(Number.isInteger)) {\n    throw new Error(`Unable to parse Node.js version: \"${version}\".`);\n  }\n\n  return { major, minor, patch };\n}\n\nexport function getNodeMajor(version: string): number {\n  return parseNodeVersion(version).major;\n}\n\nfunction compareNodeVersions(\n  left: ParsedNodeVersion,\n  right: ParsedNodeVersion\n): number {\n  if (left.major !== right.major) {\n    return left.major - right.major;\n  }\n\n  if (left.minor !== right.minor) {\n    return left.minor - right.minor;\n  }\n\n  return left.patch - right.patch;\n}\n\nexport function assertSupportedNodeRuntime(version = process.version): number {\n  const parsedVersion = parseNodeVersion(version);\n  const minimumVersion = parseNodeVersion(MINIMUM_NODE_VERSION);\n\n  if (compareNodeVersions(parsedVersion, minimumVersion) < 0) {\n    throw new Error(\n      `Node.js ${version} is unsupported. This demo workspace requires Node.js >=${MINIMUM_NODE_VERSION}.`\n    );\n  }\n\n  return parsedVersion.major;\n}\n\nexport function resolveAiGatewayContractEnv(\n  env: AiGatewayEnvRecord,\n  options: Pick<AiGatewayContractOptions, \"defaultBaseURL\" | \"defaultChatModel\">\n): AiGatewayResolvedEnv {\n  return {\n    apiKey: env.AI_GATEWAY_API_KEY,\n    baseURL:\n      env.AI_GATEWAY_BASE_URL ||\n      options.defaultBaseURL ||\n      DEFAULT_GATEWAY_BASE_URL,\n    chatModel: env.AI_GATEWAY_CHAT_MODEL || options.defaultChatModel,\n  };\n}\n\nexport function readAiGatewayContractConfig(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions\n): AiGatewayContractConfig {\n  assertSupportedNodeRuntime();\n  const resolvedEnv = resolveAiGatewayContractEnv(env, options);\n\n  if (!resolvedEnv.apiKey) {\n    throw new Error(options.missingApiKeyError);\n  }\n\n  return {\n    apiKey: resolvedEnv.apiKey,\n    baseURL: resolvedEnv.baseURL,\n    chatModel: resolvedEnv.chatModel,\n  };\n}\n\nexport function buildAiGatewayContractSetupState<\n  TConfig extends AiGatewaySetupConfig,\n>(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions<TConfig>\n): AiGatewayContractSetupState<TConfig> {\n  const issues: string[] = [];\n  const resolvedEnv = resolveAiGatewayContractEnv(env, options);\n\n  try {\n    assertSupportedNodeRuntime();\n  } catch (error) {\n    issues.push(\n      error instanceof Error ? error.message : \"Unsupported Node.js runtime.\"\n    );\n  }\n\n  if (!resolvedEnv.apiKey) {\n    issues.push(options.missingApiKeyIssue || genericMissingApiKeyIssue);\n  }\n\n  issues.push(...(options.getAdditionalIssues?.(resolvedEnv, env) ?? []));\n\n  return {\n    config:\n      options.buildConfig?.(resolvedEnv, env) ??\n      ({\n        baseURL: resolvedEnv.baseURL,\n        chatModel: resolvedEnv.chatModel,\n      } as TConfig),\n    isReady: issues.length === 0,\n    issues,\n    nodeVersion: process.version,\n  };\n}\n\nexport function createAiGatewayFromContract(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions\n): ReturnType<typeof createGateway> {\n  const { apiKey, baseURL } = readAiGatewayContractConfig(env, options);\n\n  return createGateway({\n    apiKey,\n    baseURL,\n  });\n}\n",
      "type": "registry:lib",
      "target": "@lib/ai-gateway/contract.ts"
    },
    {
      "path": "registry/rag-chatbot/lib/rag-chatbot/env-source.ts",
      "content": "import type { RagChatbotEnv } from \"./env\";\n\nexport function getRagChatbotAppEnv(): RagChatbotEnv {\n  // biome-ignore lint/style/noProcessEnv: Registry source installs into consumer apps without this repo's env wrapper.\n  return process.env;\n}\n",
      "type": "registry:lib",
      "target": "@lib/rag-chatbot/env-source.ts"
    },
    {
      "path": "registry/rag-chatbot/lib/rag-chatbot/env.ts",
      "content": "import { getRagChatbotAppEnv } from \"./env-source\";\nimport {\n  buildAiGatewayContractSetupState,\n  createAiGatewayFromContract,\n  readAiGatewayContractConfig,\n  type AiGatewayContractConfig,\n  type AiGatewayContractSetupState,\n  type AiGatewayEnvRecord,\n  type AiGatewaySetupConfig,\n} from \"@/lib/ai-gateway/contract\";\n\nconst DEFAULT_RAG_CHATBOT_CHAT_MODEL = \"openai/gpt-4.1-mini\";\nconst DEFAULT_EMBEDDING_MODEL = \"openai/text-embedding-3-small\";\n\nexport type RagChatbotEnv = AiGatewayEnvRecord;\n\nexport interface RagChatbotConfig extends AiGatewayContractConfig {\n  databaseUrl: string | undefined;\n  embeddingModel: string;\n}\n\nexport interface RagChatbotSetupConfig extends AiGatewaySetupConfig {\n  embeddingModel: string;\n}\n\nexport type RagChatbotSetupState = AiGatewayContractSetupState<RagChatbotSetupConfig>;\n\nexport type RagChatbotGateway = ReturnType<typeof createAiGatewayFromContract>;\n\nconst ragChatbotContract = {\n  defaultChatModel: DEFAULT_RAG_CHATBOT_CHAT_MODEL,\n  missingApiKeyError:\n    \"Missing AI_GATEWAY_API_KEY. Add it to .env.local before using the RAG chatbot.\",\n  missingApiKeyIssue:\n    \"AI_GATEWAY_API_KEY is missing. The demo can render, but chat requests will fail until it is configured.\",\n} as const;\n\nexport function getRagChatbotEnv(): RagChatbotEnv {\n  return getRagChatbotAppEnv();\n}\n\nfunction resolveRagChatbotEmbeddingModel(env: RagChatbotEnv) {\n  return env.AI_GATEWAY_EMBEDDING_MODEL || DEFAULT_EMBEDDING_MODEL;\n}\n\nexport function getRagChatbotConfig(\n  env: RagChatbotEnv = getRagChatbotEnv()\n): RagChatbotConfig {\n  return {\n    ...readAiGatewayContractConfig(env, ragChatbotContract),\n    databaseUrl: env.DATABASE_URL,\n    embeddingModel: resolveRagChatbotEmbeddingModel(env),\n  };\n}\n\nexport function getRagChatbotDatabaseConfig(\n  env: RagChatbotEnv = getRagChatbotEnv()\n) {\n  const databaseUrl = env.DATABASE_URL;\n\n  if (!databaseUrl) {\n    throw new Error(\n      \"DATABASE_URL is missing. The RAG chatbot requires a writable pgvector database.\"\n    );\n  }\n\n  return { databaseUrl };\n}\n\nexport function getRagChatbotSetupState(\n  env: RagChatbotEnv = getRagChatbotEnv()\n): RagChatbotSetupState {\n  return buildAiGatewayContractSetupState(env, {\n    ...ragChatbotContract,\n    buildConfig: (resolvedEnv, currentEnv) => ({\n      baseURL: resolvedEnv.baseURL,\n      chatModel: resolvedEnv.chatModel,\n      embeddingModel: resolveRagChatbotEmbeddingModel(currentEnv),\n    }),\n  });\n}\n\nexport function getRagChatbotIndexSetupIssue(\n  env: RagChatbotEnv = getRagChatbotEnv()\n): string | null {\n  if (!env.AI_GATEWAY_API_KEY) {\n    return \"AI_GATEWAY_API_KEY is missing. Source indexing requires embedding generation through AI Gateway.\";\n  }\n\n  if (!env.DATABASE_URL) {\n    return \"DATABASE_URL is missing. Source indexing requires a writable pgvector database.\";\n  }\n\n  return null;\n}\n\nexport function createRagChatbotGateway(\n  env: RagChatbotEnv = getRagChatbotEnv()\n): RagChatbotGateway {\n  return createAiGatewayFromContract(env, ragChatbotContract);\n}\n",
      "type": "registry:lib",
      "target": "@lib/rag-chatbot/env.ts"
    },
    {
      "path": "registry/rag-chatbot/lib/rag-chatbot/ingestion.ts",
      "content": "import { getDocument } from \"pdfjs-dist/legacy/build/pdf.mjs\";\n\nexport interface ExtractedPdfPage {\n  pageNumber: number;\n  text: string;\n}\n\nexport interface RagPdfChunk {\n  content: string;\n  pageLabel: string;\n  sectionTitle: string | null;\n}\n\ninterface BuildRagPdfChunksOptions {\n  maxChunkLength?: number;\n}\n\nconst defaultMaxChunkLength = 700;\nfunction normalizeExtractedLine(text: string) {\n  return text.replace(/\\s+/g, \" \").replace(/\\s+([,.;:!?])/g, \"$1\").trim();\n}\n\nfunction normalizePageLines(text: string) {\n  return text\n    .split(\"\\n\")\n    .map((line) => line.trim())\n    .filter((line) => line.length > 0);\n}\n\nfunction deriveSectionTitle(lines: string[]) {\n  const candidate = lines[0];\n\n  if (!candidate || candidate.length > 80) {\n    return null;\n  }\n\n  return candidate;\n}\n\nfunction splitIntoSentences(text: string) {\n  return text\n    .split(/(?<=[.!?])\\s+/)\n    .map((sentence) => sentence.trim())\n    .filter((sentence) => sentence.length > 0);\n}\n\nexport function buildRagPdfChunks(\n  pages: ExtractedPdfPage[],\n  options: BuildRagPdfChunksOptions = {}\n): RagPdfChunk[] {\n  const maxChunkLength = options.maxChunkLength ?? defaultMaxChunkLength;\n\n  return pages.flatMap((page) => {\n    const lines = normalizePageLines(page.text);\n\n    if (lines.length === 0) {\n      return [];\n    }\n\n    const sectionTitle = deriveSectionTitle(lines);\n    const normalizedText = lines.join(\" \");\n    const sentences = splitIntoSentences(normalizedText);\n    const chunks: RagPdfChunk[] = [];\n    let currentChunk = \"\";\n\n    for (const sentence of sentences) {\n      const candidate = currentChunk\n        ? `${currentChunk} ${sentence}`\n        : sentence;\n\n      if (candidate.length <= maxChunkLength || currentChunk.length === 0) {\n        currentChunk = candidate;\n        continue;\n      }\n\n      chunks.push({\n        content: currentChunk,\n        pageLabel: String(page.pageNumber),\n        sectionTitle,\n      });\n      currentChunk = sentence;\n    }\n\n    if (currentChunk.length > 0) {\n      chunks.push({\n        content: currentChunk,\n        pageLabel: String(page.pageNumber),\n        sectionTitle,\n      });\n    }\n\n    return chunks;\n  });\n}\n\nexport async function downloadPdfDocument(\n  documentUrl: string,\n  download = fetch\n): Promise<Uint8Array> {\n  const response = await download(documentUrl);\n\n  if (!response.ok) {\n    throw new Error(\n      `Failed to download the RAG source PDF from ${documentUrl}. Received ${response.status}.`\n    );\n  }\n\n  return new Uint8Array(await response.arrayBuffer());\n}\n\nexport async function extractPdfPages(\n  pdfBytes: Uint8Array\n): Promise<ExtractedPdfPage[]> {\n  const pdf = await getDocument({ data: pdfBytes }).promise;\n  const pages: ExtractedPdfPage[] = [];\n\n  for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber += 1) {\n    const page = await pdf.getPage(pageNumber);\n    const textContent = await page.getTextContent();\n    const lines: string[] = [];\n    let currentLine = \"\";\n\n    for (const item of textContent.items as Array<{\n      hasEOL?: boolean;\n      str: string;\n    }>) {\n      const value = normalizeExtractedLine(item.str);\n\n      if (value.length > 0) {\n        currentLine = currentLine ? `${currentLine} ${value}` : value;\n      }\n\n      if (item.hasEOL) {\n        lines.push(normalizeExtractedLine(currentLine));\n        currentLine = \"\";\n      }\n    }\n\n    if (currentLine.length > 0) {\n      lines.push(normalizeExtractedLine(currentLine));\n    }\n\n    const text = lines.filter((line) => line.length > 0).join(\"\\n\");\n\n    if (text.length > 0) {\n      pages.push({\n        pageNumber,\n        text,\n      });\n    }\n  }\n\n  return pages;\n}\n",
      "type": "registry:lib",
      "target": "@lib/rag-chatbot/ingestion.ts"
    },
    {
      "path": "registry/rag-chatbot/lib/rag-chatbot/index-source.ts",
      "content": "import { createHash } from \"node:crypto\";\nimport { embedMany } from \"ai\";\nimport { count, eq } from \"drizzle-orm\";\n\nimport {\n  type RagChatbotDatabaseModule,\n  loadRagChatbotDatabase,\n} from \"./database\";\nimport {\n  createRagChatbotGateway,\n  getRagChatbotConfig,\n  getRagChatbotEnv,\n  type RagChatbotEnv,\n} from \"./env\";\nimport {\n  buildRagPdfChunks,\n  downloadPdfDocument,\n  type ExtractedPdfPage,\n  extractPdfPages,\n} from \"./ingestion\";\nimport { ragChatbotSourceDocument } from \"./source-document\";\n\ninterface IndexRagChatbotSourceDependencies {\n  downloadDocument: (documentUrl: string) => Promise<Uint8Array>;\n  extractPages: (pdfBytes: Uint8Array) => Promise<ExtractedPdfPage[]>;\n  loadDatabase: () => Promise<RagChatbotDatabaseModule>;\n}\n\nexport interface RagSourceIndexResult {\n  chunkCount: number;\n  sourceSlug: string;\n  status: \"already-indexed\" | \"indexed\";\n}\n\nfunction buildDocumentHash(pdfBytes: Uint8Array) {\n  return createHash(\"sha256\").update(pdfBytes).digest(\"hex\");\n}\n\nfunction buildEmbeddingRows(\n  chunks: ReturnType<typeof buildRagPdfChunks>,\n  embeddings: Awaited<ReturnType<typeof embedMany>>[\"embeddings\"],\n  resourceId: string\n) {\n  return chunks.map((chunk, index) => {\n    const embedding = embeddings[index];\n\n    if (!embedding) {\n      throw new Error(\n        `Missing embedding ${index} while indexing ${ragChatbotSourceDocument.slug}.`\n      );\n    }\n\n    return {\n      chunkIndex: index,\n      content: chunk.content,\n      embedding,\n      pageLabel: chunk.pageLabel,\n      resourceId,\n      sectionTitle: chunk.sectionTitle,\n    };\n  });\n}\n\nexport async function indexRagChatbotSource(\n  env: RagChatbotEnv = getRagChatbotEnv(),\n  dependencies: IndexRagChatbotSourceDependencies = {\n    downloadDocument: downloadPdfDocument,\n    extractPages: extractPdfPages,\n    loadDatabase: loadRagChatbotDatabase,\n  }\n): Promise<RagSourceIndexResult> {\n  const pdfBytes = await dependencies.downloadDocument(\n    ragChatbotSourceDocument.documentUrl\n  );\n  const contentHash = buildDocumentHash(pdfBytes);\n  const { database, ragChatbotEmbeddings, ragChatbotResources } =\n    await dependencies.loadDatabase();\n  const existingResources = await database\n    .select({\n      contentHash: ragChatbotResources.contentHash,\n      id: ragChatbotResources.id,\n    })\n    .from(ragChatbotResources)\n    .where(eq(ragChatbotResources.sourceSlug, ragChatbotSourceDocument.slug))\n    .limit(1);\n  const existingResource = existingResources[0];\n\n  if (existingResource?.contentHash === contentHash) {\n    const chunkCountRows = await database\n      .select({ chunkCount: count() })\n      .from(ragChatbotEmbeddings)\n      .where(eq(ragChatbotEmbeddings.resourceId, existingResource.id));\n\n    return {\n      chunkCount: chunkCountRows[0]?.chunkCount ?? 0,\n      sourceSlug: ragChatbotSourceDocument.slug,\n      status: \"already-indexed\",\n    };\n  }\n\n  const pages = await dependencies.extractPages(pdfBytes);\n  const chunks = buildRagPdfChunks(pages);\n\n  if (chunks.length === 0) {\n    throw new Error(\n      `The RAG source PDF at ${ragChatbotSourceDocument.documentUrl} did not produce any indexable text.`\n    );\n  }\n\n  const gateway = createRagChatbotGateway(env);\n  const { embeddingModel } = getRagChatbotConfig(env);\n  const { embeddings } = await embedMany({\n    model: gateway.embeddingModel(embeddingModel),\n    values: chunks.map((chunk) => chunk.content),\n  });\n  const resourceValues = {\n    contentHash,\n    description: ragChatbotSourceDocument.description,\n    documentUrl: ragChatbotSourceDocument.documentUrl,\n    sourcePageUrl: ragChatbotSourceDocument.sourcePageUrl,\n    sourceSlug: ragChatbotSourceDocument.slug,\n    title: ragChatbotSourceDocument.title,\n    updatedAt: new Date(),\n  };\n\n  if (existingResource) {\n    await database.transaction(async (transaction) => {\n      await transaction\n        .update(ragChatbotResources)\n        .set(resourceValues)\n        .where(eq(ragChatbotResources.id, existingResource.id));\n      await transaction\n        .delete(ragChatbotEmbeddings)\n        .where(eq(ragChatbotEmbeddings.resourceId, existingResource.id));\n      await transaction\n        .insert(ragChatbotEmbeddings)\n        .values(buildEmbeddingRows(chunks, embeddings, existingResource.id));\n    });\n  } else {\n    await database.transaction(async (transaction) => {\n      const [resource] = await transaction\n        .insert(ragChatbotResources)\n        .values(resourceValues)\n        .returning({ id: ragChatbotResources.id });\n\n      if (!resource) {\n        throw new Error(\n          \"Failed to create the source resource row for the RAG chatbot.\"\n        );\n      }\n\n      await transaction\n        .insert(ragChatbotEmbeddings)\n        .values(buildEmbeddingRows(chunks, embeddings, resource.id));\n    });\n  }\n\n  return {\n    chunkCount: chunks.length,\n    sourceSlug: ragChatbotSourceDocument.slug,\n    status: \"indexed\",\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/rag-chatbot/index-source.ts"
    },
    {
      "path": "registry/rag-chatbot/lib/rag-chatbot/knowledge-base-status.ts",
      "content": "import { count, eq } from \"drizzle-orm\";\n\nimport {\n  type RagChatbotDatabaseModule,\n  loadRagChatbotDatabase,\n} from \"./database\";\nimport { getPortableRagIndexResourceCount } from \"./portable-index\";\nimport { ragChatbotSourceDocument } from \"./source-document\";\n\ntype DemoEnv = Record<string, string | undefined>;\ntype RagRetrievalLabel = \"Portable index\" | \"pgvector\";\n\nexport interface RagKnowledgeBaseStatus {\n  indexedResourceCount: number;\n  isReady: boolean;\n  message: string | null;\n  retrievalLabel: RagRetrievalLabel;\n  statusLabel: \"Index required\" | \"Ready\" | \"Setup required\";\n}\n\nexport interface RagKnowledgeBaseStatusDependencies {\n  getIndexedResourceCount?: (env: DemoEnv) => Promise<number>;\n  loadDatabase?: () => Promise<RagChatbotDatabaseModule>;\n}\n\nexport function getRagDatabaseSetupIssue(env: DemoEnv): string | null {\n  if (env.DATABASE_URL) {\n    return null;\n  }\n\n  return null;\n}\n\nexport function getRagIndexRequiredMessage() {\n  return `No preindexed documents are available for the RAG chatbot. Run POST /api/demos/rag-chatbot/index to index ${ragChatbotSourceDocument.title}.`;\n}\n\nexport async function getIndexedResourceCount(\n  env: DemoEnv,\n  dependencies: RagKnowledgeBaseStatusDependencies = {}\n): Promise<number> {\n  if (!env.DATABASE_URL) {\n    return 0;\n  }\n\n  const loadDatabase = dependencies.loadDatabase ?? loadRagChatbotDatabase;\n  const { database, ragChatbotResources } = await loadDatabase();\n  const rows = await database\n    .select({ resourceCount: count() })\n    .from(ragChatbotResources)\n    .where(eq(ragChatbotResources.sourceSlug, ragChatbotSourceDocument.slug));\n\n  return rows[0]?.resourceCount ?? 0;\n}\n\nexport async function getRagKnowledgeBaseStatus(\n  env: DemoEnv,\n  dependencies: RagKnowledgeBaseStatusDependencies = {}\n): Promise<RagKnowledgeBaseStatus> {\n  const databaseIssue = getRagDatabaseSetupIssue(env);\n\n  if (databaseIssue) {\n    return {\n      indexedResourceCount: 0,\n      isReady: false,\n      message: databaseIssue,\n      retrievalLabel: \"pgvector\",\n      statusLabel: \"Setup required\",\n    };\n  }\n\n  if (!env.DATABASE_URL) {\n    return {\n      indexedResourceCount: getPortableRagIndexResourceCount(),\n      isReady: true,\n      message: null,\n      retrievalLabel: \"Portable index\",\n      statusLabel: \"Ready\",\n    };\n  }\n\n  try {\n    const indexedResourceCount = dependencies.getIndexedResourceCount\n      ? await dependencies.getIndexedResourceCount(env)\n      : await getIndexedResourceCount(env, dependencies);\n\n    if (indexedResourceCount === 0) {\n      return {\n        indexedResourceCount,\n        isReady: false,\n        message: getRagIndexRequiredMessage(),\n        retrievalLabel: \"pgvector\",\n        statusLabel: \"Index required\",\n      };\n    }\n\n    return {\n      indexedResourceCount,\n      isReady: true,\n      message: null,\n      retrievalLabel: \"pgvector\",\n      statusLabel: \"Ready\",\n    };\n  } catch (error) {\n    return {\n      indexedResourceCount: 0,\n      isReady: false,\n      message:\n        error instanceof Error\n          ? `Failed to inspect the indexed document state. ${error.message}`\n          : \"Failed to inspect the indexed document state.\",\n      retrievalLabel: \"pgvector\",\n      statusLabel: \"Setup required\",\n    };\n  }\n}\n\nexport async function ensureRagKnowledgeBaseReady(\n  env: DemoEnv,\n  dependencies: RagKnowledgeBaseStatusDependencies = {}\n) {\n  const status = await getRagKnowledgeBaseStatus(env, dependencies);\n\n  if (!status.isReady) {\n    throw new Error(status.message ?? \"The RAG knowledge base is unavailable.\");\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/rag-chatbot/knowledge-base-status.ts"
    },
    {
      "path": "registry/rag-chatbot/lib/rag-chatbot/portable-index.ts",
      "content": "import { ragChatbotSourceDocument } from \"./source-document\";\n\nexport interface PortableRagIndexEntry {\n  content: string;\n  pageLabel: string | null;\n  sectionTitle: string;\n}\n\nconst portableRagIndex: PortableRagIndexEntry[] = [\n  {\n    content:\n      \"The NASA logotype is the primary identifying element in the graphics system. The manual treats it as the agency's routine visual signature and expects it to be used consistently across approved communications.\",\n    pageLabel: null,\n    sectionTitle: \"The NASA Logotype\",\n  },\n  {\n    content:\n      \"The logotype should keep strong contrast and controlled color use. The sample system emphasizes NASA red, black, and white as core identity colors, with the logotype kept clear of busy backgrounds.\",\n    pageLabel: null,\n    sectionTitle: \"The NASA Logotype: Use of Color\",\n  },\n  {\n    content:\n      \"The NASA seal has a narrower role than the logotype. It is reserved for formal, ceremonial, and official contexts, while the logotype is the regular identifier for publications, signage, and day-to-day communications.\",\n    pageLabel: null,\n    sectionTitle: \"Seal and Logotype Usage\",\n  },\n  {\n    content:\n      \"The core visual system combines the NASA logotype, the agency color palette, typography, layout discipline, and clear reproduction rules so that public materials feel consistent across many formats.\",\n    pageLabel: null,\n    sectionTitle: \"Core Visual System\",\n  },\n];\n\nconst wordPattern = /[a-z0-9]+/g;\nconst stopWords = new Set([\n  \"a\",\n  \"about\",\n  \"and\",\n  \"be\",\n  \"does\",\n  \"how\",\n  \"in\",\n  \"is\",\n  \"it\",\n  \"manual\",\n  \"of\",\n  \"say\",\n  \"should\",\n  \"summarize\",\n  \"the\",\n  \"this\",\n  \"to\",\n  \"used\",\n  \"what\",\n]);\n\nfunction tokenize(value: string): string[] {\n  return [...value.toLowerCase().matchAll(wordPattern)]\n    .map((match) => match[0])\n    .filter((token) => !stopWords.has(token));\n}\n\nfunction scorePortableEntry(queryTokens: string[], entry: PortableRagIndexEntry) {\n  const searchableText = `${entry.sectionTitle} ${entry.content}`.toLowerCase();\n  const matchedTokens = queryTokens.filter((token) =>\n    searchableText.includes(token)\n  );\n\n  return matchedTokens.length / Math.max(queryTokens.length, 1);\n}\n\nexport function getPortableRagIndexResourceCount(): number {\n  return 1;\n}\n\nexport function findPortableRagMatches(query: string) {\n  const queryTokens = tokenize(query);\n\n  return portableRagIndex\n    .map((entry) => ({\n      content: entry.content,\n      documentUrl: ragChatbotSourceDocument.documentUrl,\n      pageLabel: entry.pageLabel,\n      sectionTitle: entry.sectionTitle,\n      similarity: scorePortableEntry(queryTokens, entry),\n      title: ragChatbotSourceDocument.title,\n    }))\n    .filter((match) => match.similarity > 0)\n    .sort((left, right) => right.similarity - left.similarity)\n    .slice(0, 4);\n}\n",
      "type": "registry:lib",
      "target": "@lib/rag-chatbot/portable-index.ts"
    },
    {
      "path": "registry/rag-chatbot/lib/rag-chatbot/retrieval.ts",
      "content": "import {\n  and,\n  cosineDistance,\n  desc,\n  eq,\n  gt,\n  sql,\n} from \"drizzle-orm\";\n\nimport { embed } from \"ai\";\n\nimport {\n  type RagChatbotDatabaseModule,\n  loadRagChatbotDatabase,\n} from \"./database\";\nimport {\n  createRagChatbotGateway,\n  getRagChatbotConfig,\n  getRagChatbotEnv,\n  type RagChatbotEnv,\n} from \"./env\";\nimport {\n  ensureRagKnowledgeBaseReady,\n} from \"./knowledge-base-status\";\nimport { findPortableRagMatches } from \"./portable-index\";\nimport { ragChatbotSourceDocument } from \"./source-document\";\n\nconst matchLimit = 4;\nconst minimumSimilarity = 0.55;\n\nexport interface RetrievedRagContent {\n  content: string;\n  documentUrl: string;\n  pageLabel: string | null;\n  sectionTitle: string | null;\n  similarity: number;\n  title: string;\n}\n\nexport interface RagToolSource {\n  citationLabel: string;\n  content: string;\n  documentUrl: string;\n  pageLabel: string | null;\n  sectionTitle: string | null;\n  similarity: number;\n  title: string;\n}\n\nexport interface RagToolResult {\n  answerable: boolean;\n  message: string;\n  sources: RagToolSource[];\n}\n\ninterface FindRelevantContentDependencies {\n  ensureKnowledgeBaseReady?: (env: RagChatbotEnv) => Promise<void>;\n  findMatches?: (input: {\n    queryEmbedding: number[];\n    sourceSlug: string;\n  }) => Promise<RetrievedRagContent[]>;\n  generateEmbedding: (value: string, env: RagChatbotEnv) => Promise<number[]>;\n  loadDatabase?: () => Promise<RagChatbotDatabaseModule>;\n}\n\nfunction createCitationLabel(source: RetrievedRagContent): string {\n  return source.pageLabel\n    ? `${source.title}, p. ${source.pageLabel}`\n    : source.title;\n}\n\nexport function createRagToolResult(\n  matches: RetrievedRagContent[]\n): RagToolResult {\n  if (matches.length === 0) {\n    return {\n      answerable: false,\n      message:\n        \"No relevant indexed document snippets were found for this question.\",\n      sources: [],\n    };\n  }\n\n  return {\n    answerable: true,\n    message: `Found ${matches.length} relevant indexed document ${\n      matches.length === 1 ? \"snippet\" : \"snippets\"\n    }.`,\n    sources: matches.map((match) => ({\n      citationLabel: createCitationLabel(match),\n      content: match.content,\n      documentUrl: match.documentUrl,\n      pageLabel: match.pageLabel,\n      sectionTitle: match.sectionTitle,\n      similarity: match.similarity,\n      title: match.title,\n    })),\n  };\n}\n\nasync function findMatchesForSource(\n  input: {\n    queryEmbedding: number[];\n    sourceSlug: string;\n  },\n  loadDatabase: () => Promise<RagChatbotDatabaseModule>\n): Promise<RetrievedRagContent[]> {\n  const { database, ragChatbotEmbeddings, ragChatbotResources } =\n    await loadDatabase();\n  const similarity = sql<number>`1 - (${cosineDistance(\n    ragChatbotEmbeddings.embedding,\n    input.queryEmbedding\n  )})`;\n\n  return database\n    .select({\n      content: ragChatbotEmbeddings.content,\n      documentUrl: ragChatbotResources.documentUrl,\n      pageLabel: ragChatbotEmbeddings.pageLabel,\n      sectionTitle: ragChatbotEmbeddings.sectionTitle,\n      similarity,\n      title: ragChatbotResources.title,\n    })\n    .from(ragChatbotEmbeddings)\n    .innerJoin(\n      ragChatbotResources,\n      eq(ragChatbotEmbeddings.resourceId, ragChatbotResources.id)\n    )\n    .where(\n      and(\n        eq(ragChatbotResources.sourceSlug, input.sourceSlug),\n        gt(similarity, minimumSimilarity)\n      )\n    )\n    .orderBy((table) => desc(table.similarity))\n    .limit(matchLimit);\n}\n\nexport async function generateRagEmbedding(\n  value: string,\n  env: RagChatbotEnv = getRagChatbotEnv()\n): Promise<number[]> {\n  const gateway = createRagChatbotGateway(env);\n  const normalizedValue = value.replaceAll(\"\\n\", \" \").trim();\n  const { embeddingModel } = getRagChatbotConfig(env);\n  const { embedding } = await embed({\n    model: gateway.embeddingModel(embeddingModel),\n    value: normalizedValue,\n  });\n\n  return embedding;\n}\n\nexport async function findRelevantContent(\n  userQuery: string,\n  env: RagChatbotEnv = getRagChatbotEnv(),\n  dependencies: FindRelevantContentDependencies = {\n    generateEmbedding: generateRagEmbedding,\n  }\n): Promise<RagToolResult> {\n  const normalizedQuery = userQuery.trim();\n\n  if (normalizedQuery.length === 0) {\n    return createRagToolResult([]);\n  }\n\n  const sourceSlug = ragChatbotSourceDocument.slug;\n\n  if (!env.DATABASE_URL) {\n    return createRagToolResult(findPortableRagMatches(normalizedQuery));\n  }\n\n  const loadDatabase = dependencies.loadDatabase ?? loadRagChatbotDatabase;\n  const ensureKnowledgeBaseReady =\n    dependencies.ensureKnowledgeBaseReady ??\n    ((nextEnv: RagChatbotEnv) =>\n      ensureRagKnowledgeBaseReady(nextEnv, {\n        loadDatabase: loadDatabase as never,\n      }));\n  const findMatches =\n    dependencies.findMatches ??\n    ((input: { queryEmbedding: number[]; sourceSlug: string }) =>\n      findMatchesForSource(input, loadDatabase));\n\n  await ensureKnowledgeBaseReady(env);\n\n  const queryEmbedding = await dependencies.generateEmbedding(\n    normalizedQuery,\n    env\n  );\n  const matches = await findMatches({\n    queryEmbedding,\n    sourceSlug,\n  });\n\n  return createRagToolResult(matches);\n}\n",
      "type": "registry:lib",
      "target": "@lib/rag-chatbot/retrieval.ts"
    },
    {
      "path": "registry/rag-chatbot/lib/rag-chatbot/runtime.ts",
      "content": "import { type UIMessage, validateUIMessages } from \"ai\";\n\nimport { streamRagChatbot } from \"./chat\";\nimport {\n  getRagChatbotEnv,\n  getRagChatbotSetupState,\n  type RagChatbotEnv,\n} from \"./env\";\nimport {\n  getRagKnowledgeBaseStatus,\n  type RagKnowledgeBaseStatus,\n} from \"./knowledge-base-status\";\nimport { ragChatbotSourceDocument } from \"./source-document\";\n\ninterface RagChatbotRequestBody {\n  messages?: UIMessage[];\n}\n\nexport interface RagChatbotRuntimeState {\n  chatModel: string;\n  isChatAvailable: boolean;\n  nodeVersion: string;\n  retrievalLabel: string;\n  setupMessage: string | null;\n  sourceDocument: typeof ragChatbotSourceDocument;\n  statusLabel: \"Index required\" | \"Ready\" | \"Setup required\";\n}\n\nexport interface RagChatbotRuntimeDependencies {\n  getKnowledgeBaseStatus: (\n    env: RagChatbotEnv\n  ) => Promise<RagKnowledgeBaseStatus>;\n}\n\ninterface RagChatbotRequestDependencies extends RagChatbotRuntimeDependencies {\n  streamRagChatbot: (\n    messages: UIMessage[],\n    env: RagChatbotEnv\n  ) => Promise<Response>;\n}\n\nconst invalidMessagesError = 'Expected a JSON body with a \"messages\" array.';\nconst invalidUiMessagesError =\n  'Expected each \"messages\" entry to match the UIMessage format.';\nconst malformedJsonError = \"Expected a valid JSON request body.\";\n\nexport async function getRagChatbotRuntimeState(\n  env: RagChatbotEnv = getRagChatbotEnv(),\n  dependencies: RagChatbotRuntimeDependencies = {\n    getKnowledgeBaseStatus: getRagKnowledgeBaseStatus,\n  }\n): Promise<RagChatbotRuntimeState> {\n  const gatewaySetup = getRagChatbotSetupState(env);\n\n  if (gatewaySetup.issues.length > 0) {\n    return {\n      chatModel: gatewaySetup.config.chatModel,\n      isChatAvailable: false,\n      nodeVersion: gatewaySetup.nodeVersion,\n      retrievalLabel: \"Portable index\",\n      sourceDocument: ragChatbotSourceDocument,\n      setupMessage: gatewaySetup.issues.join(\" \"),\n      statusLabel: \"Setup required\",\n    };\n  }\n\n  const knowledgeBaseStatus = await dependencies.getKnowledgeBaseStatus(env);\n\n  return {\n    chatModel: gatewaySetup.config.chatModel,\n    isChatAvailable: knowledgeBaseStatus.isReady,\n    nodeVersion: gatewaySetup.nodeVersion,\n    retrievalLabel: knowledgeBaseStatus.retrievalLabel,\n    sourceDocument: ragChatbotSourceDocument,\n    setupMessage: knowledgeBaseStatus.message,\n    statusLabel: knowledgeBaseStatus.statusLabel,\n  };\n}\n\nasync function readRagChatbotMessages(body: unknown): Promise<UIMessage[]> {\n  const { messages } = (body ?? {}) as RagChatbotRequestBody;\n\n  if (!Array.isArray(messages)) {\n    throw new Error(invalidMessagesError);\n  }\n\n  try {\n    return await validateUIMessages({ messages });\n  } catch {\n    throw new Error(invalidUiMessagesError);\n  }\n}\n\nexport async function handleRagChatbotRequest(\n  request: Request,\n  env: RagChatbotEnv = getRagChatbotEnv(),\n  dependencies: RagChatbotRequestDependencies = {\n    getKnowledgeBaseStatus: getRagKnowledgeBaseStatus,\n    streamRagChatbot,\n  }\n) {\n  const runtimeState = await getRagChatbotRuntimeState(env, dependencies);\n\n  if (!runtimeState.isChatAvailable) {\n    return Response.json(\n      {\n        error: runtimeState.setupMessage,\n      },\n      { status: 500 }\n    );\n  }\n\n  let messages: UIMessage[];\n\n  try {\n    messages = await readRagChatbotMessages(await request.json());\n  } catch (error) {\n    if (error instanceof SyntaxError) {\n      return Response.json(\n        {\n          error: malformedJsonError,\n        },\n        { status: 400 }\n      );\n    }\n\n    if (\n      error instanceof Error &&\n      [invalidMessagesError, invalidUiMessagesError].includes(error.message)\n    ) {\n      return Response.json(\n        {\n          error: error.message,\n        },\n        { status: 400 }\n      );\n    }\n\n    throw error;\n  }\n\n  return dependencies.streamRagChatbot(messages, env);\n}\n",
      "type": "registry:lib",
      "target": "@lib/rag-chatbot/runtime.ts"
    },
    {
      "path": "registry/rag-chatbot/lib/rag-chatbot/schema.ts",
      "content": "import {\n  index,\n  integer,\n  pgTable,\n  text,\n  timestamp,\n  uniqueIndex,\n  uuid,\n  varchar,\n  vector,\n} from \"drizzle-orm/pg-core\";\n\nexport const ragChatbotResources = pgTable(\n  \"rag_chatbot_resources\",\n  {\n    id: uuid(\"id\").defaultRandom().primaryKey(),\n    sourceSlug: varchar(\"source_slug\", { length: 191 }).notNull(),\n    title: text(\"title\").notNull(),\n    sourcePageUrl: text(\"source_page_url\").notNull(),\n    documentUrl: text(\"document_url\").notNull(),\n    description: text(\"description\"),\n    contentHash: varchar(\"content_hash\", { length: 128 }).notNull(),\n    createdAt: timestamp(\"created_at\", { withTimezone: true })\n      .defaultNow()\n      .notNull(),\n    updatedAt: timestamp(\"updated_at\", { withTimezone: true })\n      .defaultNow()\n      .notNull(),\n  },\n  (table) => ({\n    sourceSlugIndex: uniqueIndex(\"rag_chatbot_resources_source_slug_idx\").on(\n      table.sourceSlug\n    ),\n  })\n);\n\nexport const ragChatbotEmbeddings = pgTable(\n  \"rag_chatbot_embeddings\",\n  {\n    id: uuid(\"id\").defaultRandom().primaryKey(),\n    resourceId: uuid(\"resource_id\")\n      .notNull()\n      .references(() => ragChatbotResources.id, { onDelete: \"cascade\" }),\n    chunkIndex: integer(\"chunk_index\").notNull(),\n    content: text(\"content\").notNull(),\n    pageLabel: varchar(\"page_label\", { length: 64 }),\n    sectionTitle: text(\"section_title\"),\n    embedding: vector(\"embedding\", { dimensions: 1536 }).notNull(),\n    createdAt: timestamp(\"created_at\", { withTimezone: true })\n      .defaultNow()\n      .notNull(),\n  },\n  (table) => ({\n    chunkIndex: uniqueIndex(\"rag_chatbot_embeddings_resource_chunk_idx\").on(\n      table.resourceId,\n      table.chunkIndex\n    ),\n    embeddingIndex: index(\"rag_chatbot_embeddings_embedding_idx\").using(\n      \"hnsw\",\n      table.embedding.op(\"vector_cosine_ops\")\n    ),\n  })\n);\n\nexport const ragChatbotSchema = {\n  ragChatbotEmbeddings,\n  ragChatbotResources,\n};\n",
      "type": "registry:lib",
      "target": "@lib/rag-chatbot/schema.ts"
    },
    {
      "path": "registry/rag-chatbot/lib/rag-chatbot/source-document.ts",
      "content": "export const ragChatbotSourceDocument = {\n  slug: \"nasa-graphics-standards-manual\",\n  title: \"NASA Graphics Standards Manual\",\n  documentUrl:\n    \"https://www.nasa.gov/wp-content/uploads/2015/01/nasa_graphics_manual_nhb_1430-2_jan_1976.pdf\",\n  sourcePageUrl:\n    \"https://www.nasa.gov/image-article/nasa-graphics-standards-manual/\",\n  description:\n    \"A public design-guidelines PDF used as the preindexed knowledge source for the document support chatbot demo.\",\n} as const;\n",
      "type": "registry:lib",
      "target": "@lib/rag-chatbot/source-document.ts"
    }
  ],
  "envVars": {
    "AI_GATEWAY_API_KEY": "",
    "AI_GATEWAY_BASE_URL": "https://ai-gateway.vercel.sh/v3/ai",
    "AI_GATEWAY_CHAT_MODEL": "openai/gpt-4.1-mini",
    "AI_GATEWAY_EMBEDDING_MODEL": "openai/text-embedding-3-small"
  },
  "docs": "Install into a Next.js App Router project initialized with shadcn/ui. This item adds the demo page, chat and indexing routes, client workspace components, a portable sample retrieval index, optional pgvector-backed ingestion, and the AI Gateway env vars required for grounded chat.",
  "type": "registry:block"
}
