A streaming AI response can run 10–30 seconds. Without a stop control wired to AbortController, users who trigger an unwanted response — wrong prompt, sensitive topic, runaway context — have no recourse except a full page reload, which destroys conversation history. This directly violates iso-25010:2011 reliability.fault-tolerance: the system must handle user-initiated cancellation gracefully. Beyond UX, holding an open streaming connection for a generation the user abandoned wastes API credits and server resources. On metered plans this creates real cost exposure.
Critical because the user has no escape from an unwanted or runaway generation except destroying the page — a hard blocker for any conversational AI product.
Wire useChat's stop() or an AbortController to a button that renders only while isLoading is true. The control must be in the DOM and visible during streaming — dead code in JSX does not satisfy this check.
const { stop, isLoading } = useChat()
<button
onClick={stop}
aria-label="Stop generating"
className={isLoading ? 'flex items-center gap-1' : 'hidden'}
>
<SquareIcon className="w-4 h-4" /> Stop
</button>
For raw fetch implementations, store an AbortController in a ref and call .abort() from the stop handler in src/components/chat/ChatInput.tsx.
ID: ai-ux-patterns.core-interaction.stop-generation
Severity: critical
What to look for: Count all streaming state variables (isLoading, isStreaming, isPending) across the codebase. For each, enumerate whether a stop, cancel, or abort control is conditionally rendered. Check for AbortController usage in API calls or stop() function calls from AI SDK hooks. Verify the stop control replaces or supplements the submit button while generation is in progress. A stop button that exists in JSX but is never conditionally shown during streaming does not count as pass — do not pass based on dead code.
Pass criteria: A stop or cancel button is visible while the AI is generating a response. The button calls AbortController.abort() or an equivalent cancellation mechanism, ending the stream. At least 1 streaming entry point must have a wired stop control. Report: "X of Y streaming entry points have stop controls."
Fail criteria: No stop button or cancel control found. The user must wait for generation to complete or refresh the page to cancel a long-running or unwanted response.
Skip (N/A) when: Same as regeneration-button — project has no AI generation interface.
Detail on fail: "No stop/cancel button found — streaming state detected (isLoading flag in useChat or similar) but no AbortController or stop() handler wired to a UI control".
Remediation: Long AI responses can take 10-30 seconds. Without a stop button, users who realize the response is wrong have no recourse.
const { stop, isLoading } = useChat()
<button
onClick={stop}
aria-label="Stop generating"
className={isLoading ? 'block' : 'hidden'}
>
<SquareIcon className="w-4 h-4" />
Stop
</button>
With a manual fetch approach:
const abortControllerRef = useRef<AbortController | null>(null)
const startGeneration = () => {
abortControllerRef.current = new AbortController()
fetch('/api/chat', { signal: abortControllerRef.current.signal, ... })
}
const stopGeneration = () => abortControllerRef.current?.abort()