{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"support-copilot","type":"registry:block","title":"Customer Support Copilot","description":"Grounded reply drafts with citations, honest low-confidence answers, inline corrections, an approval gate on refunds, and a rating row on every draft.","author":"Scrim UI (https://scrimui.dev)","categories":["pattern"],"docs":"https://scrimui.dev/patterns/support-copilot","dependencies":[],"registryDependencies":["https://scrimui.dev/r/conversation-sidebar.json","https://scrimui.dev/r/context-picker.json","https://scrimui.dev/r/citation-ui.json","https://scrimui.dev/r/source-list.json","https://scrimui.dev/r/confidence-answer.json","https://scrimui.dev/r/inline-correction.json","https://scrimui.dev/r/approval-request.json","https://scrimui.dev/r/response-rating.json","https://scrimui.dev/r/streaming-message.json","https://scrimui.dev/r/prompt-input.json"],"files":[{"path":"src/showcase/patterns/support-copilot/support-copilot.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { ConversationSidebar, type ConversationGroup } from \"@/components/ui/conversation-sidebar\";\nimport { ContextPicker, type ContextItem } from \"@/components/ui/context-picker\";\nimport { CitationList, type Citation } from \"@/components/ui/citation-ui\";\nimport { SourceList, type RetrievedSource } from \"@/components/ui/source-list\";\nimport { ConfidenceAnswer } from \"@/components/ui/confidence-answer\";\nimport { InlineCorrection } from \"@/components/ui/inline-correction\";\nimport { ApprovalRequest, type ApprovalState } from \"@/components/ui/approval-request\";\nimport { ResponseRating, type Rating } from \"@/components/ui/response-rating\";\nimport { StreamingMessage } from \"@/components/ui/streaming-message\";\nimport { PromptInput } from \"@/components/ui/prompt-input\";\n\n/**\n * A support copilot: the agent talks to customers, the copilot drafts the\n * grounded answers — and knows when to shut up.\n *\n * What this pattern exists to show:\n *\n * 1. **Grounded answers are inspectable.** Citations under the draft, the\n *    retrieved passages behind one disclosure, scores and the floor visible.\n * 2. **Low confidence is said out loud.** The second answer admits it might\n *    be mixing up legacy terms — and offers the exact thing to check.\n * 3. **Corrections feed the copilot, not the void.** The agent fixes the\n *    wrong fact inline; the original stays struck through as training data.\n * 4. **Money moves wait for a human.** The refund is drafted, but sending it\n *    is an approval gate — approve and deny both leave a receipt.\n * 5. **Every draft is rated.** Thumbs and reason chips are how the team\n *    learns which topics the copilot handles badly.\n */\n\n/* ------------------------------------------------------------------ */\n/* Mock data                                                           */\n/* ------------------------------------------------------------------ */\n\nconst SIDEBAR_GROUPS: ConversationGroup[] = [\n  {\n    id: \"today\",\n    label: \"Today\",\n    conversations: [\n      { id: \"t1042\", title: \"#1042 Refund request — Ana R.\", updatedAt: \"2m\", pinned: true },\n      { id: \"t1041\", title: \"#1041 Invoice copy — D. Kim\", updatedAt: \"26m\" },\n      { id: \"t1040\", title: \"#1040 Export fails on Safari\", updatedAt: \"1h\" },\n    ],\n  },\n  {\n    id: \"week\",\n    label: \"Earlier this week\",\n    conversations: [\n      { id: \"t1039\", title: \"#1039 Plan downgrade question\", updatedAt: \"Tue\" },\n      { id: \"t1038\", title: \"#1038 SSO setup help\", updatedAt: \"Mon\" },\n    ],\n  },\n];\n\nconst CONTEXT_SOURCES: ContextItem[] = [\n  { id: \"help\", kind: \"knowledge\", title: \"Help Center\", detail: \"214 articles\", tokens: 0, recent: true },\n  { id: \"policy\", kind: \"file\", title: \"refund-policy-2026.pdf\", detail: \"12 pages\", tokens: 3_800, recent: true },\n  { id: \"order\", kind: \"app\", title: \"Order #8182 — Billing\", detail: \"Stripe dashboard\", tokens: 900 },\n  { id: \"wiki\", kind: \"knowledge\", title: \"Internal support wiki\", detail: \"Notion\", status: \"permission-required\", tokens: 0 },\n  { id: \"macros\", kind: \"app\", title: \"Saved reply macros\", detail: \"38 macros\", status: \"unavailable\" },\n];\n\nconst CITATIONS: Citation[] = [\n  {\n    id: 1,\n    title: \"Refund policy — Help Center\",\n    url: \"https://help.example.com/refunds\",\n    snippet: \"Customers may request a full refund within 30 days of the charge date.\",\n  },\n  {\n    id: 2,\n    title: \"Refund policy §2.4\",\n    url: \"https://help.example.com/refunds#timing\",\n    snippet: \"Refunds are issued to the original payment method within 5–10 business days.\",\n  },\n];\n\nconst RETRIEVED: RetrievedSource[] = [\n  { id: \"s1\", title: \"Refund policy §2.1\", passage: \"Customers may request a full refund within 30 days of the charge date, no questions asked.\", score: 0.86 },\n  { id: \"s2\", title: \"Refund policy §2.4\", passage: \"Refunds are issued to the original payment method within 5–10 business days.\", score: 0.74 },\n  { id: \"s3\", title: \"2023 policy archive\", passage: \"Legacy terms allowed a 14-day window for annual plans purchased before 2025.\", score: 0.41 },\n];\n\nconst ANSWER_CITED =\n  \"Ana is well inside the window: order #8182 was charged 18 days ago and the policy allows a full refund within 30 days [1]. You can approve this one — the money returns to her card in 5–10 business days [2].\";\n\nconst ANSWER_LOW =\n  \"Her plan may still be under legacy terms. The 2023 archive mentions a 14-day window for annual plans purchased before 2025 — if that applies, this refund is 4 days too late.\";\n\nconst HEDGE_LOW = \"Check the plan's purchase date before quoting a window — the 2023 terms said 14 days, and I can't tell from here whether she renewed under the 2026 policy.\";\n\nconst ANSWER_APPROVAL =\n  \"I've drafted the refund for order #8182: $48.20 back to card •• 4242. It clears the 30-day window with room to spare. Approve below and it goes out now.\";\n\nconst GENERIC_ANSWER =\n  \"Based on the ticket history, Ana has been a customer since 2024 with no prior refunds. A short, warm confirmation with the 5–10 day timeline usually closes these well.\";\n\n/* ------------------------------------------------------------------ */\n/* Pattern                                                             */\n/* ------------------------------------------------------------------ */\n\ntype Turn = {\n  id: string;\n  role: \"agent\" | \"copilot\";\n  text: string;\n  kind?: \"cited\" | \"low\" | \"approval\" | \"generic\";\n  streaming?: boolean;\n};\n\nexport function SupportCopilotPattern() {\n  const [turns, setTurns] = React.useState<Turn[]>([]);\n  const [submits, setSubmits] = React.useState(0);\n  const [ratings, setRatings] = React.useState<Record<string, { rating?: Rating; submitted?: boolean }>>({});\n  const [approval, setApproval] = React.useState<ApprovalState>(\"pending\");\n  const [correction, setCorrection] = React.useState<string | undefined>();\n  const [contextSel, setContextSel] = React.useState<string[]>([\"help\", \"policy\"]);\n  const [granted, setGranted] = React.useState<string[]>([]);\n\n  const streaming = turns.some((t) => t.streaming);\n\n  function submit(text: string) {\n    if (streaming) return;\n    const n = submits + 1;\n    setSubmits(n);\n    const kind: Turn[\"kind\"] = n === 1 ? \"cited\" : n === 2 ? \"low\" : n === 3 ? \"approval\" : \"generic\";\n    const answer =\n      kind === \"cited\" ? ANSWER_CITED : kind === \"low\" ? ANSWER_LOW : kind === \"approval\" ? ANSWER_APPROVAL : GENERIC_ANSWER;\n    setTurns((ts) => [\n      ...ts,\n      { id: `a${n}`, role: \"agent\", text },\n      { id: `c${n}`, role: \"copilot\", text: answer, kind, streaming: true },\n    ]);\n  }\n\n  function finishStreaming(id: string) {\n    setTurns((ts) => ts.map((t) => (t.id === id ? { ...t, streaming: false } : t)));\n  }\n\n  function rate(id: string, rating: Rating | undefined) {\n    setRatings((r) => ({ ...r, [id]: { ...r[id], rating, submitted: rating ? r[id]?.submitted : false } }));\n  }\n\n  function submitDetail(id: string) {\n    setRatings((r) => ({ ...r, [id]: { ...r[id], submitted: true } }));\n  }\n\n  const contextItems = CONTEXT_SOURCES.map((item) =>\n    granted.includes(item.id) ? { ...item, status: \"available\" as const } : item,\n  );\n\n  return (\n    <div className=\"flex h-[640px] overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900\">\n      {/* Ticket rail */}\n      <aside className=\"hidden w-56 shrink-0 border-r border-zinc-200 dark:border-zinc-800 md:block\">\n        <ConversationSidebar\n          groups={SIDEBAR_GROUPS}\n          activeId=\"t1042\"\n          newChatLabel=\"New ticket\"\n          searchPlaceholder=\"Search tickets…\"\n          className=\"h-full\"\n        />\n      </aside>\n\n      {/* Copilot thread */}\n      <div className=\"flex min-w-0 flex-1 flex-col\">\n        <div className=\"border-b border-zinc-200 px-4 py-3 dark:border-zinc-800\">\n          <p className=\"text-sm font-semibold text-zinc-900 dark:text-zinc-100\">Ticket #1042 — Refund request</p>\n          <p className=\"truncate text-xs text-zinc-500 dark:text-zinc-400\">\n            Customer: Ana R. · Pro plan since 2024 · Order #8182 · “I was charged but already cancelled”\n          </p>\n        </div>\n\n        <div className=\"flex-1 space-y-4 overflow-y-auto px-4 py-4\">\n          {turns.length === 0 && (\n            <div className=\"rounded-xl border border-dashed border-zinc-300 px-4 py-6 text-center dark:border-zinc-700\">\n              <p className=\"text-sm font-medium text-zinc-700 dark:text-zinc-200\">Paste the customer&#39;s message to get a grounded draft</p>\n              <p className=\"mt-1 text-xs text-zinc-500 dark:text-zinc-400\">\n                Answers cite the Help Center and refund policy — the copilot says when it&#39;s guessing.\n              </p>\n            </div>\n          )}\n\n          {turns.map((turn) =>\n            turn.role === \"agent\" ? (\n              <div key={turn.id} className=\"flex justify-end\">\n                <p className=\"max-w-[85%] rounded-2xl rounded-br-md bg-zinc-900 px-4 py-2.5 text-sm leading-6 text-white dark:bg-zinc-100 dark:text-zinc-900\">\n                  {turn.text}\n                </p>\n              </div>\n            ) : turn.kind === \"low\" && !turn.streaming ? (\n              /* Low-confidence drafts keep their own honest surface. */\n              <div key={turn.id} className=\"space-y-2\">\n                <ConfidenceAnswer confidence=\"low\" text={turn.text} hedge={HEDGE_LOW} />\n                <div className=\"ml-1 space-y-1\">\n                  <p className=\"text-[11px] font-medium uppercase tracking-wide text-zinc-500 dark:text-zinc-400\">\n                    Correct the copilot\n                  </p>\n                  <InlineCorrection\n                    text=\"Legacy annual plans have a 14-day refund window.\"\n                    correction={correction}\n                    correctedBy=\"you\"\n                    onSubmit={(c) => setCorrection(c)}\n                    onRevert={() => setCorrection(undefined)}\n                  />\n                </div>\n                <ResponseRating\n                  rating={ratings[turn.id]?.rating}\n                  submitted={ratings[turn.id]?.submitted}\n                  onRate={(r) => rate(turn.id, r)}\n                  onSubmitDetail={() => submitDetail(turn.id)}\n                  reasons={[\"Wrong policy\", \"Outdated terms\", \"Missed the question\", \"Too cautious\"]}\n                />\n              </div>\n            ) : (\n              <div key={turn.id} className=\"space-y-2\">\n                <StreamingMessage\n                  text={turn.text}\n                  isStreaming={turn.streaming}\n                  showActions={false}\n                  speed={14}\n                  onComplete={() => finishStreaming(turn.id)}\n                />\n                {!turn.streaming && (\n                  <>\n                    {turn.kind === \"cited\" && (\n                      <>\n                        <CitationList citations={CITATIONS} />\n                        <details className=\"group rounded-xl border border-zinc-200 dark:border-zinc-800\">\n                          <summary className=\"cursor-pointer select-none px-3 py-2 text-xs font-medium text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100\">\n                            Inspect retrieved passages\n                          </summary>\n                          <div className=\"border-t border-zinc-200 p-2 dark:border-zinc-800\">\n                            <SourceList sources={RETRIEVED} floor={0.5} />\n                          </div>\n                        </details>\n                      </>\n                    )}\n                    {turn.kind === \"approval\" && (\n                      <ApprovalRequest\n                        title=\"Issue $48.20 refund to card •• 4242\"\n                        requester=\"Copilot\"\n                        description=\"Refund for order #8182 — within the 30-day window\"\n                        detail=\"Sends the refund immediately. This cannot be undone from the copilot.\"\n                        status={approval}\n                        onAllow={() => setApproval(\"approved\")}\n                        onDeny={() => setApproval(\"denied\")}\n                      />\n                    )}\n                    <ResponseRating\n                      rating={ratings[turn.id]?.rating}\n                      submitted={ratings[turn.id]?.submitted}\n                      onRate={(r) => rate(turn.id, r)}\n                      onSubmitDetail={() => submitDetail(turn.id)}\n                      reasons={[\"Wrong policy\", \"Outdated terms\", \"Missed the question\", \"Too cautious\"]}\n                    />\n                  </>\n                )}\n              </div>\n            ),\n          )}\n        </div>\n\n        <div className=\"border-t border-zinc-200 p-3 dark:border-zinc-800\">\n          <ContextPicker\n            className=\"mb-2\"\n            items={contextItems}\n            selectedIds={contextSel}\n            onSelectionChange={setContextSel}\n            onRequestAccess={(item) => setGranted((g) => [...g, item.id])}\n            triggerLabel=\"Add ticket context\"\n          />\n          <PromptInput\n            onSubmit={submit}\n            placeholder=\"Paste the customer's message…\"\n            loading={streaming}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n","type":"registry:block","target":"components/blocks/support-copilot.tsx"}]}