{"slug":"partial-response","category":"errors","job":"recover","added":"2026-09-12T14:20Z","title":"Partial Response","description":"A cut-short assistant reply with Continue and Retry actions.","url":"https://ai-patterns.app/patterns/errors/partial-response","markdown":"https://ai-patterns.app/patterns/errors/partial-response.md","json":"https://ai-patterns.app/api/patterns/errors/partial-response","seenIn":[],"spec":"# Partial Response\n\n## Summary\nA message component for responses that ended before completion. Displays whatever text was generated, clearly marks it as incomplete, and offers Continue (resume from the cut-off point) and Retry (regenerate from scratch) actions.\n\n## When to use\n- The stream ended before a natural conclusion — stopped by the user, cut off by an output token limit, or dropped by a transient error — and some content was produced.\n- Any time preserving and resuming the partial output is preferable to discarding it. A partial response with continuation is nearly always more useful than a blank error card.\n- In the message thread at the position of the incomplete assistant turn, not as a modal or a separate panel.\n\n## When not to use\n- Zero content produced — if the response never started, use the Generation Error pattern instead.\n- The content is corrupted or nonsensical (e.g. a mid-token cut-off in a code block) — in this case retry is the better default action; hide Continue.\n- Read-only transcript views where resuming is not an option.\n\n## Reasons\nThree distinct reasons change the copy and available actions:\n\n- **interrupted**: User clicked Stop. Label \"Stopped\". Both Continue and Retry are available.\n- **max-tokens**: Hit the model's output token ceiling. Label \"Cut off\". Both Continue (model continues from the cut-off) and Retry are available.\n- **error**: Transient error mid-stream. Label \"Incomplete\". Only Retry is available — continuing from an error state may reproduce the error.\n\n## Anatomy\n- Content area: the partial text, rendered as-is, with a blinking cursor appended to signal incompleteness.\n- Status bar: a hairline-bordered footer row.\n  - Left: reason label (bold) + separator + one-line hint in muted text.\n  - Right: Retry button (ghost, muted) and Continue button (filled, primary). Continue is the primary action.\n- Blinking cursor: a narrow vertical bar animating opacity 1→0 on a 0.8s loop. Hidden with `aria-hidden`.\n\n## Behavior\n- The cursor blinks continuously at rest to keep the \"mid-stream\" sense alive even after the stream stopped.\n- Tapping Continue puts the button into \"Continuing…\" disabled state immediately; the caller owns the actual resumption request and passes `resuming={true}` back.\n- Resumption semantics (sending the prior context + the partial response as the new prompt) are the caller's responsibility. This component signals intent; it does not issue API calls.\n- Tapping Retry discards the partial content and issues a fresh generation from the original prompt. The caller handles this by removing the partial message and resubmitting.\n- Only one action may be in-flight at a time (Continue disables during `resuming`). Retry does not need a loading state if the caller immediately replaces this component with a new streaming response.\n\n## Content guidelines\n- Reason labels: \"Stopped\", \"Cut off\", \"Incomplete\" — short, factual, no punctuation.\n- Hint text: one sentence, no more. \"Response was stopped before it finished.\" / \"Response reached the output limit.\" / \"Response stopped due to an error.\"\n- Continue label: \"Continue\" with a › chevron — implies the stream will pick up from here, not start over.\n- Retry label: \"Retry\" — unambiguous, separate from Continue.\n- Never expose the token count or technical limit to the user (\"stopped at 4096 tokens\").\n\n## Accessibility\n- The blinking cursor has `aria-hidden` — it is a decorative animation.\n- Continue button `aria-label` changes to \"Resuming response\" when `resuming` is true, giving screen-reader users feedback without a visible spinner.\n- The status bar text is available to assistive technology as static text; no additional live region is needed since the component appears fully rendered.\n- Keyboard: both actions are reachable via Tab; Continue is the last focused element in the component (rightmost in the footer), matching its primary-action status.\n\n## Related patterns\n- **Generation Error** — Use it instead when the request produced no output at all.\n- **Rate Limit** — Use it instead when the person has to wait for a quota to reset before retrying.\n- **Stop Generation Button** — Use it as well, since stopping is what produces the interrupted reason here.\n- **Streaming Text** — Use it before this one, while the answer is still streaming in.\n","code":"\"use client\";\n\nimport * as React from \"react\";\nimport { motion } from \"motion/react\";\nimport { ChevronRight, RotateCcw } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type PartialResponseReason = \"interrupted\" | \"max-tokens\" | \"error\";\n\nexport interface PartialResponseProps {\n  content: string;\n  reason?: PartialResponseReason;\n  onResume?: () => void;\n  onRetry?: () => void;\n  resuming?: boolean;\n  className?: string;\n}\n\nconst reasonConfig: Record<PartialResponseReason, { label: string; hint: string; showResume: boolean }> = {\n  interrupted: {\n    label: \"Stopped\",\n    hint: \"Response was stopped before it finished.\",\n    showResume: true,\n  },\n  \"max-tokens\": {\n    label: \"Cut off\",\n    hint: \"Response reached the output limit.\",\n    showResume: true,\n  },\n  error: {\n    label: \"Incomplete\",\n    hint: \"Response stopped due to an error.\",\n    showResume: false,\n  },\n};\n\nexport function PartialResponse({\n  content,\n  reason = \"interrupted\",\n  onResume,\n  onRetry,\n  resuming = false,\n  className,\n}: PartialResponseProps) {\n  const config = reasonConfig[reason];\n\n  return (\n    <div className={cn(\"flex flex-col gap-0 rounded-2xl border border-border bg-muted/30 overflow-hidden\", className)}>\n      <div className=\"px-4 py-3 text-sm leading-relaxed text-foreground\">\n        {content}\n        <motion.span\n          className=\"ml-0.5 inline-block h-[1em] w-0.5 translate-y-[1px] rounded-sm bg-foreground/40\"\n          animate={{ opacity: [1, 0] }}\n          transition={{ duration: 0.8, repeat: Infinity, ease: \"linear\" }}\n          aria-hidden\n        />\n      </div>\n\n      <div className=\"flex items-center justify-between border-t border-border/60 bg-muted/40 px-4 py-2.5\">\n        <p className=\"text-xs text-muted-foreground\">\n          <span className=\"font-medium text-foreground\">{config.label}</span>\n          {\" · \"}\n          {config.hint}\n        </p>\n\n        <div className=\"flex items-center gap-1.5\">\n          {onRetry && (\n            <button\n              type=\"button\"\n              onClick={onRetry}\n              aria-label=\"Retry from scratch\"\n              className=\"flex items-center gap-1 rounded-full px-2.5 py-1 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground\"\n            >\n              <RotateCcw className=\"size-3\" aria-hidden />\n              Retry\n            </button>\n          )}\n          {config.showResume && onResume && (\n            <button\n              type=\"button\"\n              onClick={onResume}\n              disabled={resuming}\n              aria-label={resuming ? \"Resuming response\" : \"Continue response\"}\n              className=\"flex items-center gap-1 rounded-full bg-foreground px-2.5 py-1 text-xs font-medium text-background transition-opacity hover:opacity-80 disabled:opacity-50\"\n            >\n              {resuming ? (\n                \"Continuing…\"\n              ) : (\n                <>\n                  Continue\n                  <ChevronRight className=\"size-3\" aria-hidden />\n                </>\n              )}\n            </button>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n","dependencies":["lucide-react","motion"],"localImports":["@/lib/utils"]}