{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"moderation-flag","type":"registry:ui","title":"Moderation Flag","description":"What to show when the content filter fires — a stopped stream that keeps its partial text, or a blocked prompt, with a false-positive path.","author":"Scrim UI (https://scrimui.dev)","categories":["safety"],"docs":"https://scrimui.dev/components/moderation-flag","dependencies":[],"registryDependencies":[],"files":[{"path":"src/showcase/moderation-flag/moderation-flag.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * The classifier fired. Now be honest about it.\n *\n * Moderation interrupts at two very different moments, and one component\n * covers both because the reader's question is the same — \"what just\n * happened, and what can I do about it?\"\n *\n * - `output`: the response was streaming and got cut. **Keep the partial\n *   text on screen.** Deleting already-streamed words the user has read is\n *   gaslighting — they saw them, and pretending otherwise destroys trust in\n *   every other message. The partial stays, dimmed and faded where it broke,\n *   with the notice attached to the cut.\n * - `input`: the prompt never reached the model. Nothing was generated, so\n *   there is nothing to preserve — only the block and the way forward.\n *\n * **Always offer the false-positive path.** Classifiers are wrong often\n * enough that \"Report a mistake\" is not a courtesy, it is the mechanism by\n * which the flagging gets better — and the only thing that keeps a legitimate\n * user (the fiction writer, the security researcher, the nurse) from reading\n * the flag as an accusation.\n *\n * **One recovery action, not two.** Retry regenerates; appeal disputes. Both\n * at once and the user cannot tell which one their situation calls for, so\n * the appeal lives as a quiet text link and the primary action is the way\n * forward.\n */\n\nexport type ModerationFlagProps = {\n  /** Where the flag fired: on the prompt, or mid-response. */\n  stage: \"input\" | \"output\";\n  /** The partial response, kept visible where the stream was cut. Output stage only. */\n  stoppedText?: string;\n  /** What happened, in one plain sentence. */\n  message: string;\n  /** Primary recovery — regenerate, or edit and resend the prompt. */\n  onRetry?: () => void;\n  /** The false-positive path. Rendered as a quiet link, always secondary. */\n  onAppeal?: () => void;\n  className?: string;\n};\n\n/* ------------------------------------------------------------------ */\n/* Icons                                                               */\n/* ------------------------------------------------------------------ */\n\nfunction FlagIcon() {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"16\" height=\"16\">\n      <path d=\"M4 22V4a1 1 0 0 1 .4-.8A6 6 0 0 1 8 2c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10a1 1 0 0 1-.4.8A6 6 0 0 1 16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528\" />\n    </svg>\n  );\n}\n\nfunction RetryIcon() {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"13\" height=\"13\">\n      <path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\" />\n      <path d=\"M3 3v5h5\" />\n    </svg>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* ModerationFlag                                                      */\n/* ------------------------------------------------------------------ */\n\nexport function ModerationFlag({\n  stage,\n  stoppedText,\n  message,\n  onRetry,\n  onAppeal,\n  className = \"\",\n}: ModerationFlagProps) {\n  const isOutput = stage === \"output\";\n\n  return (\n    <div className={className}>\n      {/* The cut partial: still readable, visibly unfinished. The mask fades\n          the tail so the eye lands on the notice as the end of the turn. */}\n      {isOutput && stoppedText && (\n        <p\n          className=\"mb-2 text-sm leading-6 text-zinc-500 dark:text-zinc-400 [mask-image:linear-gradient(to_bottom,black_35%,transparent)]\"\n          aria-hidden=\"true\"\n        >\n          {stoppedText}\n        </p>\n      )}\n\n      <div className=\"flex items-start gap-3 rounded-2xl rounded-tl-md border border-amber-200 bg-amber-50/60 px-4 py-3 dark:border-amber-900/50 dark:bg-amber-950/30\">\n        <span className=\"mt-0.5 shrink-0 text-amber-600 dark:text-amber-400\">\n          <FlagIcon />\n        </span>\n        <div className=\"min-w-0 flex-1\">\n          <div className=\"text-sm font-medium text-zinc-900 dark:text-zinc-100\">\n            {isOutput ? \"Response stopped\" : \"Message not sent\"}\n          </div>\n          {/* zinc-600 on both caption and link: they sit on the tinted amber\n              surface, where zinc-500 measures under AA at 12-13px. */}\n          <p className=\"mt-0.5 text-[13px] leading-5 text-zinc-600 dark:text-zinc-400\">{message}</p>\n\n          {(onRetry || onAppeal) && (\n            <div className=\"mt-2 flex items-center gap-3\">\n              {onRetry && (\n                <button\n                  type=\"button\"\n                  onClick={onRetry}\n                  className=\"inline-flex h-7 items-center gap-1.5 rounded-lg border border-zinc-200 bg-white px-2.5 text-xs font-medium text-zinc-700 transition-colors hover:bg-zinc-100 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800\"\n                >\n                  <RetryIcon />\n                  {isOutput ? \"Regenerate\" : \"Edit and resend\"}\n                </button>\n              )}\n              {onAppeal && (\n                <button\n                  type=\"button\"\n                  onClick={onAppeal}\n                  className=\"text-xs font-medium text-zinc-600 underline underline-offset-2 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-200\"\n                >\n                  Report a mistake\n                </button>\n              )}\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n","type":"registry:ui","target":"components/ui/moderation-flag.tsx"}]}