{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"response-versions","type":"registry:ui","title":"Response Versions","description":"The 1/3 pager after a regenerate — previous and next versions, branch markers, continue-from-here, and honest generating, failed and stopped states.","author":"Scrim UI (https://scrimui.dev)","categories":["messages"],"docs":"https://scrimui.dev/components/response-versions","dependencies":[],"registryDependencies":[],"files":[{"path":"src/showcase/response-versions/response-versions.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * The version navigator that appears after a regenerate.\n *\n * Message Actions can fire \"regenerate\", but what happens next is usually\n * left broken: the old answer is overwritten, or the new one shoves the\n * reader somewhere they did not ask to go. This component owns the state\n * model for what comes after:\n *\n * **Versions are identified by id, never by array position.** The caller\n * passes a stable `id` per version; the pager's \"2 / 3\" is display only.\n * Retries and branches append — nothing is ever overwritten in place.\n *\n * **A new version must not steal the reader's place.** The navigator\n * auto-advances to an arriving version only while the reader is already\n * sitting on the newest one. Someone paging back through v1 while v4\n * streams in stays on v1; the pager badge tells them something new landed.\n *\n * **A failed version keeps its partial content.** The half-written answer\n * is often still useful, and it is always evidence. It stays on screen\n * under the failure notice instead of vanishing.\n *\n * **Branching is marked, not diagrammed.** When a version was created off\n * an older one (an edited prompt, a continue-from-here), a chip names the\n * parent. A full branch tree is a pattern-level concern.\n */\n\nexport type ResponseVersionStatus = \"ready\" | \"generating\" | \"failed\" | \"stopped\";\n\nexport type ResponseVersion = {\n  id: string;\n  content: React.ReactNode;\n  status?: ResponseVersionStatus;\n  /** Id of the version this one was branched off, if any. */\n  branchedFrom?: string;\n};\n\nexport type ResponseVersionsProps = {\n  versions: ResponseVersion[];\n  /** Seeds the navigator. After that, position is internal state. */\n  defaultCurrentId?: string;\n  onVersionChange?: (id: string) => void;\n  /** Ask the caller for another version. Appending it is the caller's job. */\n  onRegenerate?: () => void;\n  /** Continue the conversation from a version that is not the latest. */\n  onContinueFrom?: (id: string) => void;\n  /** Open a comparison of the current version against another. */\n  onCompare?: (currentId: string) => void;\n  className?: string;\n};\n\n/* ------------------------------------------------------------------ */\n/* Icons                                                               */\n/* ------------------------------------------------------------------ */\n\nfunction ChevronLeftIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"13\" height=\"13\" {...props}>\n      <path d=\"m15 18-6-6 6-6\" />\n    </svg>\n  );\n}\n\nfunction ChevronRightIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"13\" height=\"13\" {...props}>\n      <path d=\"m9 18 6-6-6-6\" />\n    </svg>\n  );\n}\n\nfunction BranchIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"11\" height=\"11\" {...props}>\n      <path d=\"M6 3v12\" />\n      <circle cx=\"18\" cy=\"6\" r=\"3\" />\n      <circle cx=\"6\" cy=\"18\" r=\"3\" />\n      <path d=\"M18 9a9 9 0 0 1-9 9\" />\n    </svg>\n  );\n}\n\nfunction RegenerateIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"13\" height=\"13\" {...props}>\n      <path d=\"M3 12a9 9 0 0 1 15-6.7L21 8\" />\n      <path d=\"M21 3v5h-5\" />\n      <path d=\"M21 12a9 9 0 0 1-15 6.7L3 16\" />\n      <path d=\"M3 21v-5h5\" />\n    </svg>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* ResponseVersions                                                    */\n/* ------------------------------------------------------------------ */\n\nexport function ResponseVersions({\n  versions,\n  defaultCurrentId,\n  onVersionChange,\n  onRegenerate,\n  onContinueFrom,\n  onCompare,\n  className = \"\",\n}: ResponseVersionsProps) {\n  const lastId = versions.length > 0 ? versions[versions.length - 1].id : undefined;\n  const [currentId, setCurrentId] = React.useState(defaultCurrentId ?? lastId);\n\n  /* Auto-follow the newest version, but only while the reader is already\n     there. Render-phase adjustment: when the versions array gains a new\n     tail, advance iff the previous tail was the one on screen. A reader\n     paging through older versions is never yanked forward. */\n  const [followed, setFollowed] = React.useState(lastId);\n  if (lastId !== followed) {\n    setFollowed(lastId);\n    if (currentId === followed && lastId) setCurrentId(lastId);\n  }\n\n  if (versions.length === 0) return null;\n  const current = versions.find((v) => v.id === currentId) ?? versions[versions.length - 1];\n  const index = versions.indexOf(current);\n  const status = current.status ?? \"ready\";\n  const branchIndex = current.branchedFrom\n    ? versions.findIndex((v) => v.id === current.branchedFrom)\n    : -1;\n\n  function go(id: string) {\n    setCurrentId(id);\n    onVersionChange?.(id);\n  }\n\n  const navBtn =\n    \"rounded-md p-1 text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-800 disabled:opacity-30 disabled:hover:bg-transparent dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200\";\n\n  return (\n    <div className={className}>\n      {status === \"generating\" && (\n        <p className=\"mb-1.5 flex items-center gap-1.5 text-[11px] font-medium text-zinc-500 dark:text-zinc-400\">\n          <span className=\"h-1.5 w-1.5 animate-pulse rounded-full bg-blue-500\" />\n          Generating a new version…\n        </p>\n      )}\n\n      <div>{current.content}</div>\n\n      {status === \"failed\" && (\n        <div className=\"mt-2 flex items-center justify-between gap-3 rounded-lg border border-red-200 bg-red-50 px-3 py-2 dark:border-red-900/50 dark:bg-red-950/40\">\n          <span className=\"text-xs text-red-700 dark:text-red-300\">\n            This version failed to finish — the partial answer is kept above.\n          </span>\n          {onRegenerate && (\n            <button\n              type=\"button\"\n              onClick={onRegenerate}\n              className=\"shrink-0 rounded-md bg-red-600 px-2.5 py-1 text-xs font-medium text-white transition-opacity hover:opacity-90\"\n            >\n              Retry\n            </button>\n          )}\n        </div>\n      )}\n      {status === \"stopped\" && (\n        <p className=\"mt-2 text-xs text-zinc-500 dark:text-zinc-400\">\n          Stopped — this version is incomplete.\n        </p>\n      )}\n\n      {(versions.length > 1 || onRegenerate) && (\n        <div role=\"group\" aria-label=\"Response versions\" className=\"mt-2 flex items-center gap-1\">\n          {versions.length > 1 && (\n            <>\n              <button\n                type=\"button\"\n                aria-label={`Previous version (${index} of ${versions.length})`}\n                disabled={index === 0}\n                onClick={() => go(versions[index - 1].id)}\n                className={navBtn}\n              >\n                <ChevronLeftIcon />\n              </button>\n              <span\n                aria-live=\"polite\"\n                className=\"min-w-9 text-center text-xs tabular-nums text-zinc-500 dark:text-zinc-400\"\n              >\n                {index + 1} / {versions.length}\n              </span>\n              <button\n                type=\"button\"\n                aria-label={`Next version (${index + 2} of ${versions.length})`}\n                disabled={index === versions.length - 1}\n                onClick={() => go(versions[index + 1].id)}\n                className={navBtn}\n              >\n                <ChevronRightIcon />\n              </button>\n              {branchIndex >= 0 && (\n                <span className=\"ml-1 flex items-center gap-1 rounded-full bg-zinc-100 px-2 py-0.5 text-[11px] text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400\">\n                  <BranchIcon />\n                  from v{branchIndex + 1}\n                </span>\n              )}\n            </>\n          )}\n\n          <span className=\"flex-1\" />\n\n          {onCompare && versions.filter((v) => (v.status ?? \"ready\") === \"ready\").length > 1 && (\n            <button\n              type=\"button\"\n              onClick={() => onCompare(current.id)}\n              className=\"rounded-md px-2 py-1 text-xs font-medium text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200\"\n            >\n              Compare\n            </button>\n          )}\n          {index < versions.length - 1 && onContinueFrom && status === \"ready\" && (\n            <button\n              type=\"button\"\n              onClick={() => onContinueFrom(current.id)}\n              className=\"rounded-md px-2 py-1 text-xs font-medium text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200\"\n            >\n              Continue from here\n            </button>\n          )}\n          {onRegenerate && status !== \"failed\" && (\n            <button\n              type=\"button\"\n              aria-label=\"Regenerate response\"\n              onClick={onRegenerate}\n              className={navBtn}\n            >\n              <RegenerateIcon />\n            </button>\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n","type":"registry:ui","target":"components/ui/response-versions.tsx"}]}