{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "object-generation",
  "title": "Object Generation",
  "description": "A structured-output review workspace that streams a generated object directly inside the assistant message.",
  "dependencies": [
    "@ai-sdk/react",
    "@base-ui/react",
    "@phosphor-icons/react",
    "ai",
    "class-variance-authority",
    "lucide-react",
    "zod"
  ],
  "registryDependencies": [
    "utils",
    "badge",
    "breadcrumb",
    "button",
    "progress",
    "textarea",
    "https://elements.ai-sdk.dev/api/registry/attachments.json",
    "https://elements.ai-sdk.dev/api/registry/conversation.json",
    "https://elements.ai-sdk.dev/api/registry/message.json"
  ],
  "files": [
    {
      "path": "registry/object-generation/app/demos/object-generation/page.tsx",
      "content": "import { ObjectGenerationScreen } from \"@/components/object-generation/object-generation-screen\";\n\nexport default function ObjectGenerationPage() {\n  return <ObjectGenerationScreen />;\n}\n",
      "type": "registry:page",
      "target": "app/demos/object-generation/page.tsx"
    },
    {
      "path": "registry/object-generation/app/api/demos/object-generation/route.ts",
      "content": "import { handleObjectGenerationRequest } from \"@/lib/object-generation/runtime\";\n\nexport const maxDuration = 30;\n\nexport async function POST(request: Request) {\n  return handleObjectGenerationRequest(request);\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/object-generation/route.ts"
    },
    {
      "path": "registry/object-generation/components/object-generation/object-generation-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\";\nimport { ObjectGenerationWorkspace } from \"@/components/object-generation/object-generation-workspace\";\nimport { getObjectGenerationRuntimeState } from \"@/lib/object-generation/runtime\";\n\nexport function ObjectGenerationScreen() {\n  const runtimeState = getObjectGenerationRuntimeState();\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                    Object Generation\n                  </BreadcrumbPage>\n                </BreadcrumbItem>\n              </BreadcrumbList>\n            </Breadcrumb>\n            <h1 className=\"max-w-3xl font-medium text-2xl tracking-tight\">\n              Generate a structured object directly inside the assistant message\n            </h1>\n            <p className=\"max-w-3xl text-muted-foreground text-sm/relaxed\">\n              This slice keeps AI SDK structured output front and center:\n              multimodal inputs go in, and the generated object progressively\n              renders inside the assistant card in the thread.\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          <ObjectGenerationWorkspace\n            acceptedMediaTypes={runtimeState.acceptedMediaTypes}\n            chatModel={runtimeState.chatModel}\n            isReviewAvailable={runtimeState.isReviewAvailable}\n            nodeVersion={runtimeState.nodeVersion}\n            setupMessage={runtimeState.setupMessage}\n          />\n        </div>\n      </div>\n    </main>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/object-generation/object-generation-screen.tsx"
    },
    {
      "path": "registry/object-generation/components/object-generation/object-generation-workspace.tsx",
      "content": "\"use client\";\n\nimport {\n  ArrowClockwiseIcon,\n  PaperclipIcon,\n  ShieldCheckIcon,\n  StopIcon,\n  XIcon,\n} from \"@phosphor-icons/react\";\nimport type { FileUIPart } from \"ai\";\nimport {\n  Attachment,\n  AttachmentInfo,\n  AttachmentPreview,\n  AttachmentRemove,\n  Attachments,\n} from \"@/components/ai-elements/attachments\";\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 { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { useMemo, useRef } from \"react\";\n\nimport { ObjectGenerationResultCard } from \"./object-generation-result-card\";\nimport {\n  type DisplayReviewThreadEntry,\n} from \"./object-generation-session\";\nimport { useObjectGenerationSession } from \"./use-object-generation-session\";\n\nexport interface ObjectGenerationWorkspaceProps {\n  acceptedMediaTypes: string[];\n  chatModel: string;\n  isReviewAvailable: boolean;\n  nodeVersion: string;\n  setupMessage: string | null;\n}\n\nfunction getUserAttachmentParts(attachments: DisplayReviewThreadEntry[\"attachments\"]) {\n  return attachments.map(\n    (attachment): FileUIPart => ({\n      filename: attachment.filename,\n      mediaType: attachment.mediaType,\n      type: \"file\",\n      url: attachment.previewUrl,\n    })\n  );\n}\n\nexport function ObjectGenerationWorkspace({\n  acceptedMediaTypes,\n  chatModel,\n  isReviewAvailable,\n  nodeVersion,\n  setupMessage,\n}: ObjectGenerationWorkspaceProps) {\n  const fileInputRef = useRef<HTMLInputElement>(null);\n  const {\n    composerError,\n    entries,\n    hasMessages,\n    inputResetKey,\n    isLoading,\n    pendingAttachments,\n    streamErrorMessage,\n    appendFiles,\n    removePendingAttachment,\n    retryReview,\n    stopReview,\n    submitReview,\n  } = useObjectGenerationSession();\n\n  const samplePrompts = useMemo(\n    () => [\n      \"Generate a launch-risk object for this pricing page draft and flag unsupported claims.\",\n      \"Read this policy PDF and screenshot, then generate a publishability object with evidence.\",\n      \"Turn the attached assets into a structured moderation object with findings and next action.\",\n    ],\n    []\n  );\n\n  return (\n    <div className=\"grid min-h-[70svh] grid-cols-[minmax(0,1fr)] gap-4 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n      <section className=\"flex min-h-[70svh] min-w-0 flex-col border border-foreground/10 bg-background lg:h-full lg:min-h-0\">\n        {isReviewAvailable ? 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        {composerError ? (\n          <div className=\"border-foreground/10 border-b px-4 py-3 text-destructive text-xs/relaxed\">\n            {composerError}\n          </div>\n        ) : null}\n\n        {streamErrorMessage ? (\n          <div className=\"border-foreground/10 border-b px-4 py-3 text-destructive text-xs/relaxed\">\n            {streamErrorMessage}\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              entries.map((entry) => {\n                const attachmentParts = getUserAttachmentParts(entry.attachments);\n\n                return (\n                  <div className=\"space-y-4\" key={entry.id}>\n                    <Message from=\"user\">\n                      <MessageContent className=\"space-y-4 max-w-2xl\">\n                        {entry.prompt ? (\n                          <MessageResponse>{entry.prompt}</MessageResponse>\n                        ) : (\n                          <p className=\"text-muted-foreground text-sm\">\n                            Attachment-only object generation request.\n                          </p>\n                        )}\n\n                        {attachmentParts.length > 0 ? (\n                          <Attachments variant=\"list\">\n                            {attachmentParts.map((part, index) => (\n                              <Attachment\n                                data={{\n                                  ...part,\n                                  id: `${entry.id}-${index}-${part.filename ?? \"attachment\"}`,\n                                }}\n                                key={`${entry.id}-${index}-${part.filename ?? \"attachment\"}`}\n                              >\n                                <AttachmentPreview />\n                                <AttachmentInfo showMediaType />\n                              </Attachment>\n                            ))}\n                          </Attachments>\n                        ) : null}\n                      </MessageContent>\n                    </Message>\n\n                    <Message from=\"assistant\">\n                      <MessageContent className=\"space-y-3 max-w-3xl\">\n                        <ObjectGenerationResultCard\n                          errorMessage={entry.errorMessage}\n                          result={entry.liveResult}\n                          status={entry.liveStatus}\n                        />\n\n                        {entry.liveStatus !== \"streaming\" ? (\n                          <div className=\"flex justify-end\">\n                            <Button\n                              onClick={() => retryReview(entry)}\n                              size=\"sm\"\n                              type=\"button\"\n                              variant=\"outline\"\n                            >\n                              <ArrowClockwiseIcon className=\"size-3.5\" />\n                              Replay generation\n                            </Button>\n                          </div>\n                        ) : null}\n                      </MessageContent>\n                    </Message>\n                  </div>\n                );\n              })\n            ) : (\n              <ConversationEmptyState\n                description=\"Send text, screenshots, or PDFs. The assistant message streams a structured object directly into the thread.\"\n                icon={<ShieldCheckIcon className=\"size-5\" />}\n                title=\"Object generation 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 }) => submitReview(text)}>\n              <PromptInputBody>\n                {pendingAttachments.length > 0 ? (\n                  <Attachments className=\"mb-3\" variant=\"list\">\n                    {pendingAttachments.map((attachment) => (\n                      <Attachment\n                        data={{\n                          filename: attachment.file.name,\n                          id: attachment.id,\n                          mediaType:\n                            attachment.file.type || \"application/octet-stream\",\n                          type: \"file\",\n                          url: attachment.previewUrl,\n                        }}\n                        key={attachment.id}\n                      >\n                        <AttachmentPreview />\n                        <AttachmentInfo showMediaType />\n                        <AttachmentRemove\n                          onClick={() => removePendingAttachment(attachment.id)}\n                        />\n                      </Attachment>\n                    ))}\n                  </Attachments>\n                ) : null}\n                <PromptInputTextarea\n                  disabled={!isReviewAvailable || isLoading}\n                  placeholder=\"Describe the object you want, attach images or PDFs, and watch the structured object stream into the assistant message.\"\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                  <input\n                    accept=\"image/*,application/pdf\"\n                    className=\"hidden\"\n                    key={inputResetKey}\n                    multiple\n                    onChange={(event) => appendFiles(event.target.files)}\n                    ref={fileInputRef}\n                    type=\"file\"\n                  />\n                  <Button\n                    onClick={() => fileInputRef.current?.click()}\n                    size=\"sm\"\n                    type=\"button\"\n                    variant=\"outline\"\n                  >\n                    <PaperclipIcon className=\"size-3.5\" />\n                    Attach\n                  </Button>\n                  <Badge variant=\"outline\">Structured output</Badge>\n                  <Badge variant=\"outline\">useObject</Badge>\n                  <Badge variant=\"outline\">{chatModel}</Badge>\n                </div>\n                <div className=\"flex items-center gap-2\">\n                  {isLoading ? (\n                    <Button\n                      onClick={stopReview}\n                      size=\"sm\"\n                      type=\"button\"\n                      variant=\"outline\"\n                    >\n                      <StopIcon className=\"size-3.5\" />\n                      Stop\n                    </Button>\n                  ) : null}\n                  <PromptInputSubmit disabled={!isReviewAvailable || isLoading} />\n                </div>\n              </PromptInputFooter>\n            </PromptInput>\n\n            {!hasMessages ? (\n              <div className=\"mt-3 flex flex-wrap gap-2\">\n                {samplePrompts.map((prompt) => (\n                  <Button\n                    className={cn(\n                      \"h-auto min-h-7 min-w-0 max-w-full shrink justify-start whitespace-normal py-1.5 text-left leading-5 [overflow-wrap:anywhere]\"\n                    )}\n                    disabled={!isReviewAvailable || isLoading}\n                    key={prompt}\n                    onClick={() => submitReview(prompt)}\n                    size=\"sm\"\n                    type=\"button\"\n                    variant=\"outline\"\n                  >\n                    {prompt}\n                  </Button>\n                ))}\n              </div>\n            ) : null}\n          </div>\n        </div>\n      </section>\n\n      <aside className=\"grid min-w-0 content-start gap-4 border border-foreground/10 bg-background px-4 py-4 lg:min-h-0 lg:overflow-y-auto\">\n        <div className=\"space-y-2\">\n          <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n            Runtime\n          </p>\n          <div className=\"flex flex-wrap gap-2\">\n            <Badge variant=\"outline\">\n              {isReviewAvailable ? \"Ready\" : \"Setup required\"}\n            </Badge>\n            <Badge variant=\"outline\">{chatModel}</Badge>\n            <Badge variant=\"outline\">Node {nodeVersion.replace(/^v/, \"\")}</Badge>\n          </div>\n        </div>\n\n        <div className=\"space-y-2\">\n          <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n            Input contract\n          </p>\n          <p className=\"text-muted-foreground text-sm/relaxed\">\n            One request can combine freeform text with image and PDF\n            attachments. The backend streams a single structured object.\n          </p>\n          <div className=\"flex flex-wrap gap-2\">\n            {acceptedMediaTypes.map((mediaType) => (\n              <Badge key={mediaType} variant=\"outline\">\n                {mediaType}\n              </Badge>\n            ))}\n          </div>\n        </div>\n\n        <div className=\"space-y-2\">\n          <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n            Output shape\n          </p>\n          <p className=\"text-muted-foreground text-sm/relaxed\">\n            The assistant card is driven by a streamed object with decision,\n            risk score, findings, evidence, recommended action, and open\n            questions.\n          </p>\n        </div>\n      </aside>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/object-generation/object-generation-workspace.tsx"
    },
    {
      "path": "registry/object-generation/components/object-generation/object-generation-result-card.tsx",
      "content": "\"use client\";\n\nimport type { DeepPartial } from \"ai\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Progress } from \"@/components/ui/progress\";\nimport { cn } from \"@/lib/utils\";\n\nimport type { ObjectGenerationResult } from \"@/lib/object-generation/schema\";\n\ntype ReviewCardStatus = \"streaming\" | \"ready\" | \"error\" | \"stopped\";\n\ninterface ObjectGenerationResultCardProps {\n  errorMessage?: string | null;\n  result: DeepPartial<ObjectGenerationResult> | undefined;\n  status: ReviewCardStatus;\n}\n\nfunction compactList<T>(items: ReadonlyArray<T | undefined> | undefined): T[] {\n  return Array.isArray(items)\n    ? items.filter((item): item is T => item !== undefined)\n    : [];\n}\n\nfunction toTitleCase(value: string) {\n  return value\n    .split(\"_\")\n    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n    .join(\" \");\n}\n\nfunction getDecisionVariant(\n  decision: ObjectGenerationResult[\"decision\"] | undefined\n): \"default\" | \"secondary\" | \"destructive\" | \"outline\" {\n  switch (decision) {\n    case \"approved\":\n      return \"default\";\n    case \"needs_review\":\n      return \"secondary\";\n    case \"blocked\":\n      return \"destructive\";\n    default:\n      return \"outline\";\n  }\n}\n\nfunction getStatusCopy(status: ReviewCardStatus) {\n  switch (status) {\n    case \"streaming\":\n      return \"Streaming object\";\n    case \"ready\":\n      return \"Object ready\";\n    case \"stopped\":\n      return \"Generation stopped\";\n    case \"error\":\n      return \"Generation failed\";\n  }\n}\n\nexport function ObjectGenerationResultCard({\n  errorMessage,\n  result,\n  status,\n}: ObjectGenerationResultCardProps) {\n  const categories = compactList(result?.categories);\n  const evidence = compactList(result?.evidence);\n  const findings = compactList(result?.findings);\n  const openQuestions = compactList(result?.openQuestions);\n  const riskScore =\n    typeof result?.riskScore === \"number\"\n      ? Math.max(0, Math.min(100, result.riskScore))\n      : null;\n\n  return (\n    <div className=\"space-y-4 border border-foreground/10 bg-background px-4 py-4\">\n      <div className=\"flex flex-wrap items-center gap-2\">\n        <Badge variant=\"outline\">{getStatusCopy(status)}</Badge>\n        {result?.decision ? (\n          <Badge variant={getDecisionVariant(result.decision)}>\n            {toTitleCase(result.decision)}\n          </Badge>\n        ) : null}\n        {riskScore !== null ? (\n          <Badge variant=\"outline\">Risk {riskScore}/100</Badge>\n        ) : null}\n      </div>\n\n      {riskScore !== null ? (\n        <div className=\"space-y-2\">\n          <div className=\"flex items-center gap-2 text-xs\">\n            <span>Risk score</span>\n            <span className=\"ml-auto text-muted-foreground tabular-nums\">\n              {riskScore}\n            </span>\n          </div>\n          <Progress value={riskScore} />\n        </div>\n      ) : null}\n\n      {result?.summary ? (\n        <section className=\"space-y-1\">\n          <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n            Summary\n          </p>\n          <p className=\"text-sm/relaxed\">{result.summary}</p>\n        </section>\n      ) : null}\n\n      {categories.length > 0 ? (\n        <section className=\"space-y-2\">\n          <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n            Categories\n          </p>\n          <div className=\"flex flex-wrap gap-2\">\n            {categories.map((category, index) => (\n              <Badge\n                className=\"h-auto max-w-full whitespace-normal py-1 text-left\"\n                key={`${category.label ?? \"category\"}-${index}`}\n                variant=\"outline\"\n              >\n                <span className=\"font-medium\">{category.label ?? \"Category\"}</span>\n                {category.severity ? ` · ${toTitleCase(category.severity)}` : \"\"}\n              </Badge>\n            ))}\n          </div>\n        </section>\n      ) : null}\n\n      {findings.length > 0 ? (\n        <section className=\"space-y-2\">\n          <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n            Findings\n          </p>\n          <div className=\"space-y-2\">\n            {findings.map((finding, index) => (\n              <div\n                className=\"border border-foreground/10 px-3 py-3\"\n                key={`${finding.title ?? \"finding\"}-${index}`}\n              >\n                <div className=\"flex flex-wrap items-center gap-2\">\n                  <p className=\"font-medium text-sm\">{finding.title ?? \"Finding\"}</p>\n                  {finding.severity ? (\n                    <Badge variant=\"outline\">{toTitleCase(finding.severity)}</Badge>\n                  ) : null}\n                  {finding.policyLabel ? (\n                    <Badge variant=\"outline\">{finding.policyLabel}</Badge>\n                  ) : null}\n                </div>\n                {finding.details ? (\n                  <p className=\"mt-2 text-muted-foreground text-sm/relaxed\">\n                    {finding.details}\n                  </p>\n                ) : null}\n              </div>\n            ))}\n          </div>\n        </section>\n      ) : null}\n\n      {evidence.length > 0 ? (\n        <section className=\"space-y-2\">\n          <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n            Evidence\n          </p>\n          <div className=\"space-y-2\">\n            {evidence.map((item, index) => (\n              <div\n                className=\"border border-foreground/10 px-3 py-3\"\n                key={`${item.quote ?? \"evidence\"}-${index}`}\n              >\n                <div className=\"flex flex-wrap items-center gap-2\">\n                  <Badge variant=\"outline\">\n                    {item.sourceType ? toTitleCase(item.sourceType) : \"Source\"}\n                  </Badge>\n                  {item.filename ? (\n                    <span className=\"text-muted-foreground text-xs\">\n                      {item.filename}\n                    </span>\n                  ) : null}\n                </div>\n                {item.quote ? (\n                  <p className=\"mt-2 text-sm/relaxed\">\"{item.quote}\"</p>\n                ) : null}\n                {item.rationale ? (\n                  <p className=\"mt-2 text-muted-foreground text-xs/relaxed\">\n                    {item.rationale}\n                  </p>\n                ) : null}\n              </div>\n            ))}\n          </div>\n        </section>\n      ) : null}\n\n      {result?.recommendedAction ? (\n        <section className=\"space-y-1\">\n          <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n            Recommended action\n          </p>\n          <p className=\"text-sm/relaxed\">{result.recommendedAction}</p>\n        </section>\n      ) : null}\n\n      {openQuestions.length > 0 ? (\n        <section className=\"space-y-2\">\n          <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.18em]\">\n            Open questions\n          </p>\n          <ul className=\"space-y-2 text-sm/relaxed\">\n            {openQuestions.map((question, index) => (\n              <li\n                className=\"border border-foreground/10 px-3 py-2\"\n                key={`${question}-${index}`}\n              >\n                {question}\n              </li>\n            ))}\n          </ul>\n        </section>\n      ) : null}\n\n      {!result?.summary &&\n      categories.length === 0 &&\n      findings.length === 0 &&\n      evidence.length === 0 &&\n      !result?.recommendedAction &&\n      openQuestions.length === 0 ? (\n        <p\n          className={cn(\n            \"text-sm/relaxed\",\n            status === \"error\" ? \"text-destructive\" : \"text-muted-foreground\"\n          )}\n        >\n          {errorMessage ??\n            \"Waiting for the structured object to accumulate fields.\"}\n        </p>\n      ) : null}\n\n      {status === \"error\" && errorMessage ? (\n        <p className=\"text-destructive text-xs/relaxed\">{errorMessage}</p>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/object-generation/object-generation-result-card.tsx"
    },
    {
      "path": "registry/object-generation/components/object-generation/object-generation-session.ts",
      "content": "import type { DeepPartial } from \"ai\";\n\nimport type {\n  ObjectGenerationAttachment,\n  ObjectGenerationResult,\n} from \"@/lib/object-generation/schema\";\nimport type {\n  PendingReviewAttachment,\n  SubmittedReviewAttachment,\n} from \"./convert-files-to-object-generation-inputs\";\n\nexport type ReviewEntryStatus = \"streaming\" | \"ready\" | \"error\" | \"stopped\";\n\nexport interface ReviewThreadEntry {\n  attachments: SubmittedReviewAttachment[];\n  errorMessage: string | null;\n  id: string;\n  prompt: string;\n  requestAttachments: ObjectGenerationAttachment[];\n  result: DeepPartial<ObjectGenerationResult> | undefined;\n  status: ReviewEntryStatus;\n}\n\nexport interface DisplayReviewThreadEntry extends ReviewThreadEntry {\n  isActive: boolean;\n  liveResult: DeepPartial<ObjectGenerationResult> | undefined;\n  liveStatus: ReviewEntryStatus;\n}\n\ninterface CreateReviewThreadEntryInput {\n  id: string;\n  pendingAttachments: PendingReviewAttachment[];\n  prompt: string;\n  requestAttachments: ObjectGenerationAttachment[];\n}\n\ninterface FinalizeReviewThreadEntryInput {\n  entryId: string;\n  errorMessage?: string | null;\n  result: DeepPartial<ObjectGenerationResult> | undefined;\n  status: ReviewEntryStatus;\n}\n\nexport function createReviewThreadEntry({\n  id,\n  pendingAttachments,\n  prompt,\n  requestAttachments,\n}: CreateReviewThreadEntryInput): ReviewThreadEntry {\n  return {\n    attachments: toSubmittedReviewAttachments(pendingAttachments),\n    errorMessage: null,\n    id,\n    prompt,\n    requestAttachments,\n    result: undefined,\n    status: \"streaming\",\n  };\n}\n\nexport function mergePendingReviewAttachments(\n  current: PendingReviewAttachment[],\n  nextAttachments: PendingReviewAttachment[]\n) {\n  const byId = new Map(current.map((attachment) => [attachment.id, attachment]));\n\n  for (const attachment of nextAttachments) {\n    byId.set(attachment.id, attachment);\n  }\n\n  return Array.from(byId.values());\n}\n\nexport function removePendingReviewAttachment(\n  current: PendingReviewAttachment[],\n  attachmentId: string\n) {\n  const removedAttachment =\n    current.find((attachment) => attachment.id === attachmentId) ?? null;\n\n  return {\n    nextAttachments: current.filter((attachment) => attachment.id !== attachmentId),\n    removedAttachment,\n  };\n}\n\nexport function collectReviewPreviewUrls(\n  pendingAttachments: PendingReviewAttachment[],\n  entries: ReviewThreadEntry[]\n) {\n  const previewUrls = new Set<string>();\n\n  for (const attachment of pendingAttachments) {\n    previewUrls.add(attachment.previewUrl);\n  }\n\n  for (const entry of entries) {\n    for (const attachment of entry.attachments) {\n      previewUrls.add(attachment.previewUrl);\n    }\n  }\n\n  return previewUrls;\n}\n\nexport function finalizeReviewThreadEntry(\n  entries: ReviewThreadEntry[],\n  {\n    entryId,\n    errorMessage = null,\n    result,\n    status,\n  }: FinalizeReviewThreadEntryInput\n) {\n  return entries.map((entry) =>\n    entry.id === entryId\n      ? {\n          ...entry,\n          errorMessage,\n          result,\n          status,\n        }\n      : entry\n  );\n}\n\nexport function failReviewThreadEntry(\n  entries: ReviewThreadEntry[],\n  entryId: string,\n  errorMessage: string,\n  result: DeepPartial<ObjectGenerationResult> | undefined\n) {\n  return finalizeReviewThreadEntry(entries, {\n    entryId,\n    errorMessage,\n    result,\n    status: \"error\",\n  });\n}\n\nexport function stopReviewThreadEntry(\n  entries: ReviewThreadEntry[],\n  entryId: string,\n  result: DeepPartial<ObjectGenerationResult> | undefined\n) {\n  return finalizeReviewThreadEntry(entries, {\n    entryId,\n    result,\n    status: \"stopped\",\n  });\n}\n\nexport function restartReviewThreadEntry(\n  entries: ReviewThreadEntry[],\n  entryId: string\n) : ReviewThreadEntry[] {\n  return entries.map((entry): ReviewThreadEntry =>\n    entry.id === entryId\n        ? {\n            ...entry,\n            errorMessage: null,\n            status: \"streaming\" as const,\n          }\n      : entry\n  );\n}\n\nexport function toDisplayReviewThreadEntries(\n  entries: ReviewThreadEntry[],\n  activeEntryId: string | null,\n  isLoading: boolean,\n  liveObject: DeepPartial<ObjectGenerationResult> | undefined\n): DisplayReviewThreadEntry[] {\n  return entries.map((entry) => {\n    const isActive = activeEntryId === entry.id;\n\n    return {\n      ...entry,\n      isActive,\n      liveResult: isActive ? liveObject ?? entry.result : entry.result,\n      liveStatus: isActive && isLoading ? \"streaming\" : entry.status,\n    };\n  });\n}\n\nexport function toSubmittedReviewAttachments(\n  attachments: PendingReviewAttachment[]\n): SubmittedReviewAttachment[] {\n  return attachments.map((attachment) => ({\n    filename: attachment.file.name,\n    id: attachment.id,\n    mediaType: attachment.file.type || \"application/octet-stream\",\n    previewUrl: attachment.previewUrl,\n  }));\n}\n",
      "type": "registry:component",
      "target": "@components/object-generation/object-generation-session.ts"
    },
    {
      "path": "registry/object-generation/components/object-generation/use-object-generation-session.ts",
      "content": "\"use client\";\n\nimport { experimental_useObject as useObject } from \"@ai-sdk/react\";\nimport type { DeepPartial } from \"ai\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\n\nimport type {\n  ObjectGenerationAttachment,\n  ObjectGenerationResult,\n} from \"@/lib/object-generation/schema\";\nimport { objectGenerationResultSchema } from \"@/lib/object-generation/schema\";\nimport {\n  buildPendingReviewAttachment,\n  convertFilesToReviewAttachments,\n  type PendingReviewAttachment,\n} from \"./convert-files-to-object-generation-inputs\";\nimport {\n  collectReviewPreviewUrls,\n  createReviewThreadEntry,\n  failReviewThreadEntry,\n  mergePendingReviewAttachments,\n  restartReviewThreadEntry,\n  stopReviewThreadEntry,\n  toDisplayReviewThreadEntries,\n  type DisplayReviewThreadEntry,\n  type ReviewThreadEntry,\n} from \"./object-generation-session\";\n\nexport interface ObjectGenerationSessionController {\n  composerError: string | null;\n  entries: DisplayReviewThreadEntry[];\n  hasMessages: boolean;\n  inputResetKey: number;\n  isLoading: boolean;\n  pendingAttachments: PendingReviewAttachment[];\n  streamErrorMessage: string | null;\n  appendFiles: (fileList: FileList | null) => void;\n  removePendingAttachment: (attachmentId: string) => void;\n  retryReview: (entry: ReviewThreadEntry) => void;\n  stopReview: () => void;\n  submitReview: (text: string) => Promise<void>;\n}\n\nexport function useObjectGenerationSession(): ObjectGenerationSessionController {\n  const [pendingAttachments, setPendingAttachments] = useState<\n    PendingReviewAttachment[]\n  >([]);\n  const [composerError, setComposerError] = useState<string | null>(null);\n  const [entries, setEntries] = useState<ReviewThreadEntry[]>([]);\n  const [activeEntryId, setActiveEntryId] = useState<string | null>(null);\n  const [inputResetKey, setInputResetKey] = useState(0);\n  const pendingAttachmentsRef = useRef<PendingReviewAttachment[]>([]);\n  const entriesRef = useRef<ReviewThreadEntry[]>([]);\n  const latestObjectRef = useRef<DeepPartial<ObjectGenerationResult> | undefined>(\n    undefined\n  );\n  const activeEntryIdRef = useRef<string | null>(null);\n\n  const { clear, error, isLoading, object, stop, submit } = useObject<\n    typeof objectGenerationResultSchema,\n    ObjectGenerationResult,\n    {\n      attachments: ObjectGenerationAttachment[];\n      prompt: string;\n    }\n  >({\n    api: \"/api/demos/object-generation\",\n    onError(streamError) {\n      const entryId = activeEntryIdRef.current;\n\n      if (!entryId) {\n        return;\n      }\n\n      setEntries((current) =>\n        failReviewThreadEntry(\n          current,\n          entryId,\n          streamError.message,\n          latestObjectRef.current\n        )\n      );\n      setActiveEntryId(null);\n    },\n    async onFinish({ error: finishError, object: finalObject }) {\n      const entryId = activeEntryIdRef.current;\n\n      if (!entryId) {\n        return;\n      }\n\n      const currentEntry = entriesRef.current.find((entry) => entry.id === entryId);\n      const result = finalObject ?? latestObjectRef.current ?? currentEntry?.result;\n\n      setEntries((current) =>\n        current.map((entry) =>\n          entry.id === entryId\n            ? {\n                ...entry,\n                errorMessage: finishError?.message ?? null,\n                result,\n                status: finishError ? \"error\" : \"ready\",\n              }\n            : entry\n        )\n      );\n      setActiveEntryId(null);\n    },\n    schema: objectGenerationResultSchema,\n  });\n\n  useEffect(() => {\n    pendingAttachmentsRef.current = pendingAttachments;\n  }, [pendingAttachments]);\n\n  useEffect(() => {\n    entriesRef.current = entries;\n  }, [entries]);\n\n  useEffect(() => {\n    activeEntryIdRef.current = activeEntryId;\n  }, [activeEntryId]);\n\n  useEffect(() => {\n    latestObjectRef.current = object;\n  }, [object]);\n\n  useEffect(() => {\n    return () => {\n      for (const url of collectReviewPreviewUrls(\n        pendingAttachmentsRef.current,\n        entriesRef.current\n      )) {\n        URL.revokeObjectURL(url);\n      }\n    };\n  }, []);\n\n  function removePendingAttachment(attachmentId: string) {\n    setPendingAttachments((current) => {\n      const target =\n        current.find((attachment) => attachment.id === attachmentId) ?? null;\n\n      if (target) {\n        URL.revokeObjectURL(target.previewUrl);\n      }\n\n      return current.filter((attachment) => attachment.id !== attachmentId);\n    });\n  }\n\n  function appendFiles(fileList: FileList | null) {\n    if (!fileList) {\n      return;\n    }\n\n    const nextAttachments = Array.from(fileList).map(buildPendingReviewAttachment);\n    setPendingAttachments((current) =>\n      mergePendingReviewAttachments(current, nextAttachments)\n    );\n  }\n\n  async function submitReview(text: string) {\n    const prompt = text.trim();\n\n    if (!prompt && pendingAttachments.length === 0) {\n      return;\n    }\n\n    try {\n      setComposerError(null);\n      const requestAttachments = await convertFilesToReviewAttachments(\n        pendingAttachments.map((attachment) => attachment.file)\n      );\n      const entryId = crypto.randomUUID();\n\n      setEntries((current) => [\n        ...current,\n        createReviewThreadEntry({\n          id: entryId,\n          pendingAttachments,\n          prompt,\n          requestAttachments,\n        }),\n      ]);\n      setActiveEntryId(entryId);\n      latestObjectRef.current = undefined;\n      submit({\n        attachments: requestAttachments,\n        prompt,\n      });\n      setPendingAttachments([]);\n      setInputResetKey((current) => current + 1);\n    } catch (attachmentError) {\n      setComposerError(\n        attachmentError instanceof Error\n          ? attachmentError.message\n          : \"Failed to prepare review attachments.\"\n      );\n    }\n  }\n\n  function stopReview() {\n    const entryId = activeEntryIdRef.current;\n\n    stop();\n\n    if (!entryId) {\n      return;\n    }\n\n    setEntries((current) =>\n      stopReviewThreadEntry(current, entryId, latestObjectRef.current)\n    );\n    setActiveEntryId(null);\n  }\n\n  function retryReview(entry: ReviewThreadEntry) {\n    if (isLoading) {\n      return;\n    }\n\n    clear();\n    setEntries((current) => restartReviewThreadEntry(current, entry.id));\n    setActiveEntryId(entry.id);\n    latestObjectRef.current = undefined;\n    submit({\n      attachments: entry.requestAttachments,\n      prompt: entry.prompt,\n    });\n  }\n\n  return {\n    composerError,\n    entries: useMemo(\n      () => toDisplayReviewThreadEntries(entries, activeEntryId, isLoading, object),\n      [entries, activeEntryId, isLoading, object]\n    ),\n    hasMessages: entries.length > 0,\n    inputResetKey,\n    isLoading,\n    pendingAttachments,\n    streamErrorMessage: error?.message ?? null,\n    appendFiles,\n    removePendingAttachment,\n    retryReview,\n    stopReview,\n    submitReview,\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/object-generation/use-object-generation-session.ts"
    },
    {
      "path": "registry/object-generation/components/object-generation/convert-files-to-object-generation-inputs.ts",
      "content": "import type { ObjectGenerationAttachment } from \"@/lib/object-generation/schema\";\n\nexport interface PendingReviewAttachment {\n  file: File;\n  id: string;\n  previewUrl: string;\n}\n\nexport interface SubmittedReviewAttachment {\n  filename: string;\n  id: string;\n  mediaType: string;\n  previewUrl: string;\n}\n\nexport function buildPendingReviewAttachment(\n  file: File\n): PendingReviewAttachment {\n  return {\n    file,\n    id: `${file.name}-${file.size}-${file.lastModified}`,\n    previewUrl: URL.createObjectURL(file),\n  };\n}\n\nexport async function convertFilesToReviewAttachments(\n  files: File[]\n): Promise<ObjectGenerationAttachment[]> {\n  return Promise.all(\n    files.map(\n      (file) =>\n        new Promise<ObjectGenerationAttachment>((resolve, reject) => {\n          const reader = new FileReader();\n\n          reader.onload = () => {\n            if (typeof reader.result !== \"string\") {\n              reject(new Error(`Failed to read ${file.name} as a data URL.`));\n              return;\n            }\n\n            resolve({\n              filename: file.name,\n              mediaType: file.type || \"application/octet-stream\",\n              url: reader.result,\n            });\n          };\n\n          reader.onerror = () => {\n            reject(new Error(`Failed to read ${file.name} as a data URL.`));\n          };\n\n          reader.readAsDataURL(file);\n        })\n    )\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/object-generation/convert-files-to-object-generation-inputs.ts"
    },
    {
      "path": "registry/object-generation/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/object-generation/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/object-generation/lib/object-generation/env-source.ts",
      "content": "export function getObjectGenerationAppEnv() {\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/object-generation/env-source.ts"
    },
    {
      "path": "registry/object-generation/lib/object-generation/env.ts",
      "content": "import { getObjectGenerationAppEnv } 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\nexport const DEFAULT_CHAT_MODEL = \"openai/gpt-4.1-mini\";\n\nexport type ObjectGenerationEnv = AiGatewayEnvRecord;\n\nexport type ObjectGenerationConfig = AiGatewayContractConfig;\n\nexport type ObjectGenerationSetupState = AiGatewayContractSetupState<AiGatewaySetupConfig>;\n\nexport type ObjectGenerationGateway = ReturnType<typeof createAiGatewayFromContract>;\n\nconst objectGenerationContract = {\n  defaultChatModel: DEFAULT_CHAT_MODEL,\n  missingApiKeyError: \"Missing AI_GATEWAY_API_KEY. Add it to .env.local before using object generation.\",\n  missingApiKeyIssue: \"AI_GATEWAY_API_KEY is missing. The demo can render, but object generation requests will fail until it is configured.\",\n} as const;\n\nexport function getObjectGenerationEnv(): ObjectGenerationEnv {\n  return getObjectGenerationAppEnv();\n}\n\nexport function getObjectGenerationConfig(\n  env: ObjectGenerationEnv = getObjectGenerationEnv()\n): ObjectGenerationConfig {\n  return readAiGatewayContractConfig(env, objectGenerationContract);\n}\n\nexport function getObjectGenerationSetupState(\n  env: ObjectGenerationEnv = getObjectGenerationEnv()\n): ObjectGenerationSetupState {\n  return buildAiGatewayContractSetupState(env, objectGenerationContract);\n}\n\nexport function createObjectGenerationGateway(\n  env: ObjectGenerationEnv = getObjectGenerationEnv()\n): ObjectGenerationGateway {\n  return createAiGatewayFromContract(env, objectGenerationContract);\n}\n",
      "type": "registry:lib",
      "target": "@lib/object-generation/env.ts"
    },
    {
      "path": "registry/object-generation/lib/object-generation/runtime.ts",
      "content": "import { Output, streamText } from \"ai\";\nimport {\n  createObjectGenerationGateway,\n  getObjectGenerationConfig,\n  getObjectGenerationEnv,\n  getObjectGenerationSetupState,\n  type ObjectGenerationEnv,\n} from \"@/lib/object-generation/env\";\n\nimport {\n  type ObjectGenerationRequest,\n  objectGenerationAcceptedMediaTypes,\n  objectGenerationRequestSchema,\n  objectGenerationResultSchema,\n} from \"@/lib/object-generation/schema\";\n\nconst invalidBodyError =\n  'Expected a JSON body with a \"prompt\" string and an \"attachments\" array.';\nconst malformedJsonError = \"Expected a valid JSON request body.\";\nconst unsupportedMediaTypeError =\n  \"Only image attachments and PDF attachments are supported.\";\nconst missingReviewInputError =\n  \"Provide text guidance, at least one attachment, or both.\";\n\nconst reviewSystemPrompt = [\n  \"You are the Object Generation structured-output demo in a production-ready agent demos monorepo.\",\n  \"Generate the requested object from the provided text, images, and PDFs.\",\n  \"Evaluate policy, trust, and brand-safety issues before filling the object fields.\",\n  \"Return only the structured object requested by the output schema.\",\n  \"Use approved when the content is safe to publish as-is, needs_review when it needs human review or minor changes, and blocked when it should not be published.\",\n  \"Ground every finding in provided evidence and keep summaries concise.\",\n].join(\" \");\n\nexport interface ObjectGenerationRuntimeState {\n  acceptedMediaTypes: string[];\n  chatModel: string;\n  isReviewAvailable: boolean;\n  nodeVersion: string;\n  setupMessage: string | null;\n  statusLabel: \"Ready\" | \"Setup required\";\n}\n\ninterface ObjectGenerationRequestDependencies {\n  createObjectGenerationStream: (\n    input: ObjectGenerationRequest,\n    env: ObjectGenerationEnv\n  ) => ObjectGenerationStreamResult | Promise<ObjectGenerationStreamResult>;\n}\n\nexport interface ObjectGenerationStreamResult {\n  textStream: AsyncIterable<string>;\n}\n\nfunction isAcceptedMediaType(mediaType: string) {\n  return mediaType.startsWith(\"image/\") || mediaType === \"application/pdf\";\n}\n\nfunction readObjectGenerationInput(body: unknown): ObjectGenerationRequest {\n  const parsed = objectGenerationRequestSchema.safeParse(body);\n\n  if (!parsed.success) {\n    throw new Error(invalidBodyError);\n  }\n\n  const input = {\n    attachments: parsed.data.attachments,\n    prompt: parsed.data.prompt.trim(),\n  };\n\n  if (!input.prompt && input.attachments.length === 0) {\n    throw new Error(missingReviewInputError);\n  }\n\n  for (const attachment of input.attachments) {\n    if (!isAcceptedMediaType(attachment.mediaType)) {\n      throw new Error(unsupportedMediaTypeError);\n    }\n  }\n\n  return input;\n}\n\nfunction buildReviewMessages(input: ObjectGenerationRequest) {\n  return [\n    {\n      content: [\n        {\n          text: [\n            \"Generate the structured object for the provided content submission.\",\n            \"Focus on safety, policy, trust, unsupported claims, and editorial risk.\",\n            input.prompt\n              ? `Requester guidance: ${input.prompt}`\n              : \"Requester guidance: Use the provided attachments only.\",\n          ].join(\"\\n\"),\n          type: \"text\" as const,\n        },\n        ...input.attachments.map((attachment) => ({\n          data: attachment.url,\n          filename: attachment.filename,\n          mediaType: attachment.mediaType,\n          type: \"file\" as const,\n        })),\n      ],\n      role: \"user\" as const,\n    },\n  ];\n}\n\nexport function getObjectGenerationRuntimeState(\n  env: ObjectGenerationEnv = getObjectGenerationEnv()\n): ObjectGenerationRuntimeState {\n  const setup = getObjectGenerationSetupState(env);\n\n  return {\n    acceptedMediaTypes: [...objectGenerationAcceptedMediaTypes],\n    chatModel: setup.config.chatModel,\n    isReviewAvailable: setup.isReady,\n    nodeVersion: setup.nodeVersion,\n    setupMessage: setup.issues.length > 0 ? setup.issues.join(\" \") : null,\n    statusLabel: setup.isReady ? \"Ready\" : \"Setup required\",\n  };\n}\n\nexport function createObjectGenerationStream(\n  input: ObjectGenerationRequest,\n  env: ObjectGenerationEnv\n): Promise<ObjectGenerationStreamResult> {\n  const gateway = createObjectGenerationGateway(env);\n  const { chatModel } = getObjectGenerationConfig(env);\n\n  const result = streamText({\n    model: gateway(chatModel),\n    messages: buildReviewMessages(input),\n    output: Output.object({\n      description:\n        \"A structured object with grounded findings, evidence, and next action.\",\n      name: \"ObjectGenerationResult\",\n      schema: objectGenerationResultSchema,\n    }),\n    system: reviewSystemPrompt,\n  });\n\n  return Promise.resolve({\n    textStream: result.textStream,\n  });\n}\n\nfunction createObjectGenerationResponse(stream: ObjectGenerationStreamResult) {\n  const encoder = new TextEncoder();\n\n  const body = new ReadableStream<Uint8Array>({\n    async start(controller) {\n      try {\n        for await (const chunk of stream.textStream) {\n          controller.enqueue(encoder.encode(chunk));\n        }\n\n        controller.close();\n      } catch (error) {\n        controller.error(error);\n      }\n    },\n  });\n\n  return new Response(body, {\n    headers: {\n      \"content-type\": \"text/plain; charset=utf-8\",\n    },\n  });\n}\n\nexport async function handleObjectGenerationRequest(\n  request: Request,\n  env: ObjectGenerationEnv = getObjectGenerationEnv(),\n  dependencies: ObjectGenerationRequestDependencies = {\n    createObjectGenerationStream,\n  }\n) {\n  const runtimeState = getObjectGenerationRuntimeState(env);\n\n  if (!runtimeState.isReviewAvailable) {\n    return Response.json(\n      {\n        error: runtimeState.setupMessage,\n      },\n      { status: 500 }\n    );\n  }\n\n  let input: ObjectGenerationRequest;\n\n  try {\n    input = readObjectGenerationInput(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      [\n        invalidBodyError,\n        missingReviewInputError,\n        unsupportedMediaTypeError,\n      ].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  const stream = await dependencies.createObjectGenerationStream(input, env);\n\n  return createObjectGenerationResponse(stream);\n}\n",
      "type": "registry:lib",
      "target": "@lib/object-generation/runtime.ts"
    },
    {
      "path": "registry/object-generation/lib/object-generation/schema.ts",
      "content": "import { z } from \"zod\";\n\nexport const objectGenerationDecisionSchema = z.enum([\n  \"approved\",\n  \"needs_review\",\n  \"blocked\",\n]);\n\nexport const objectGenerationSeveritySchema = z.enum([\"low\", \"medium\", \"high\"]);\n\nexport const objectGenerationAcceptedMediaTypes = [\n  \"application/pdf\",\n  \"image/*\",\n] as const;\n\nexport const objectGenerationAttachmentSchema = z.object({\n  filename: z.string().min(1),\n  mediaType: z.string().min(1),\n  url: z.string().min(1),\n});\n\nexport const objectGenerationRequestSchema = z.object({\n  attachments: z.array(objectGenerationAttachmentSchema).default([]),\n  prompt: z.string().default(\"\"),\n});\n\nexport const objectGenerationResultSchema = z.object({\n  decision: objectGenerationDecisionSchema,\n  summary: z.string(),\n  riskScore: z.number().min(0).max(100),\n  categories: z.array(\n    z.object({\n      label: z.string(),\n      rationale: z.string(),\n      severity: objectGenerationSeveritySchema,\n    })\n  ),\n  findings: z.array(\n    z.object({\n      details: z.string(),\n      policyLabel: z.string(),\n      severity: objectGenerationSeveritySchema,\n      title: z.string(),\n    })\n  ),\n  evidence: z.array(\n    z.object({\n      filename: z.string(),\n      rationale: z.string(),\n      sourceType: z.enum([\"text\", \"image\", \"pdf\"]),\n      quote: z.string(),\n    })\n  ),\n  recommendedAction: z.string(),\n  openQuestions: z.array(z.string()),\n});\n\nexport type ObjectGenerationAttachment = z.infer<\n  typeof objectGenerationAttachmentSchema\n>;\nexport type ObjectGenerationRequest = z.infer<typeof objectGenerationRequestSchema>;\nexport type ObjectGenerationResult = z.infer<typeof objectGenerationResultSchema>;\n",
      "type": "registry:lib",
      "target": "@lib/object-generation/schema.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"
  },
  "docs": "Install into a Next.js App Router project initialized with shadcn/ui. This item adds the object-generation demo page, streaming review routes, client workspace, and AI Gateway env vars required for structured output over text, image, and PDF inputs.",
  "type": "registry:block"
}
