
Ai Loading Ux
- 145 installs
- 31 repo stars
- Updated August 2, 2026
- shipshitdev/library
Design loading, streaming, and pending states for AI features—skeletons, progress cues, partial results, and cancel/retry affordances users trust.
About
Defines UX patterns for AI-powered interfaces during slow or uncertain operations: progressive disclosure of streamed responses, skeleton placeholders, step progress for tool chains, clear failure and retry paths, and accessible feedback so users stay oriented while models and agents work.
- Streaming and skeleton patterns for LLM output
- Progress indicators for multi-step agent tasks
- Cancel, retry, and timeout affordances
- Accessible motion and status messaging
Ai Loading Ux by the numbers
- 145 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,022 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/shipshitdev/library --skill ai-loading-uxAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 145 |
|---|---|
| repo stars | ★ 31 |
| Last updated | August 2, 2026 |
| Repository | shipshitdev/library ↗ |
What it does
Design loading, streaming, and pending states for AI features—skeletons, progress cues, partial results, and cancel/retry affordances users trust.
Files
AI Loading UX
Design patterns for showing users what's happening while waiting for AI output.
Decision Framework
First, identify which pattern category applies:
| User is waiting for... | Pattern Category | Key Goal |
|---|---|---|
| AI reasoning/thinking | Reasoning Display | Build trust through transparency |
| Multi-step task completion | Progress Steps | Show advancement toward goal |
| Content generation/streaming | Streaming States | Reduce perceived wait time |
| Background processing | Status Indicators | Confirm work is happening |
Core Principles
1. The Elevator Mirror Effect
Users waiting for AI feel time pass slower. Give them something to watch/read—animated indicators reduce perceived wait time even when actual time is unchanged.
2. Progressive Disclosure
- Show condensed indicator by default ("Thinking...")
- Make details available but not forced
- Let curious users expand; don't burden everyone
3. More Transparency ≠ Better UX
Balance visibility with cognitive load. Users want answers, not reasoning—but they want to trust the answer came from good reasoning.
4. Signal Completion Clearly
Users must know when processing ends. Ambiguous end states frustrate users.
Pattern Quick Reference
Reasoning Display (Chain-of-Thought)
When AI is "thinking" through a problem. See references/reasoning-patterns.md.
Best approach (Claude-style):
- Hidden by default, expandable on demand
- Structured bullets when expanded
- Time counter or progress indicator
- Clear "done" state
Anti-patterns:
- Wall of streaming text (overwhelming)
- Scrolling too fast to read
- No expand option (feels opaque)
- No clear end state
Progress Steps
When AI completes sequential tasks. See references/progress-patterns.md.
Best approach:
- Show current step + total steps
- Mark completed steps visually
- Show what's actively happening
- Allow step-level details on expand
Streaming States
When content generates token-by-token. See references/streaming-patterns.md.
Best approach:
- Typing cursor or text animation
- Smooth token appearance (not jarring)
- Skeleton for expected content shape
- "Stop generating" escape hatch
Status Indicators
When background work happens. See references/status-patterns.md.
Best approach:
- Subtle but visible animation
- Brief description of current action
- Don't block user from other actions
- Notify on completion
Implementation Checklist
When implementing any AI loading state:
1. [ ] Identify pattern category from decision framework above 2. [ ] Choose visibility level: always visible, expandable, or minimal 3. [ ] Add motion: animation reduces perceived wait (but keep it subtle) 4. [ ] Show progress: time elapsed, steps completed, or content streamed 5. [ ] Signal completion: clear visual/state change when done 6. [ ] Provide escape: stop/cancel for long operations 7. [ ] Handle errors: don't leave user in permanent loading state 8. [ ] Test on slow connections: ensure graceful degradation
Product Comparisons (Reference)
| Product | Approach | Strength | Weakness |
|---|---|---|---|
| Claude | Hidden reasoning, expandable, structured bullets | Low cognitive load | Can feel opaque |
| ChatGPT | Brief labels, auto-collapse | Unobtrusive | Less transparent |
| DeepSeek | Full streaming reasoning | Maximum transparency | Overwhelming |
| Gemini | User-scrolled, numbered steps | Clear structure | Unclear completion |
Usage
Read the relevant reference file for your pattern category:
- references/reasoning-patterns.md - Chain-of-thought, thinking indicators
- references/progress-patterns.md - Step sequences, task completion
- references/streaming-patterns.md - Token streaming, content generation
- references/status-patterns.md - Background processing, polling states
{
"name": "ai-loading-ux",
"version": "1.0.0",
"description": "Design AI loading, thinking, and progress indicator UX. Use when explicitly asked to improve AI wait",
"author": {
"name": "Ship Shit Dev",
"email": "hello@shipshit.dev",
"url": "https://shipshit.dev"
},
"license": "MIT",
"skills": "."
}
Progress Step Patterns
For AI interfaces that complete multi-step tasks.
Table of Contents
When to Use
Use progress steps when:
- Task has 3+ discrete steps
- Steps complete sequentially
- Users need to know what's happening at each stage
- Total time exceeds 10 seconds
Examples:
- "Analyzing document → Extracting data → Formatting report"
- "Searching → Filtering → Ranking → Generating response"
- "Uploading → Processing → Saving"
Anatomy of Progress Steps
┌─────────────────────────────────────────────────────┐
│ Processing your request │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━░░░░░░░░ 75% │
├─────────────────────────────────────────────────────┤
│ ✓ Analyzing document 2.3s │
│ ✓ Extracting key information 4.1s │
│ ● Generating summary... 1.2s │
│ ○ Formatting output │
└─────────────────────────────────────────────────────┘Components:
1. Header - Overall task description 2. Progress bar - Visual completion indicator 3. Step list - Individual steps with status 4. Timing - Per-step or total elapsed time
Step States
| State | Visual | Behavior |
|---|---|---|
| Pending | ○ (empty circle) | Grayed out, waiting |
| Active | ● (filled, pulsing) | Highlighted, animated |
| Complete | ✓ (checkmark) | Green, shows duration |
| Error | ✗ (x mark) | Red, shows error message |
| Skipped | — (dash) | Gray, strikethrough |
Layout Patterns
Vertical List (Default)
✓ Step 1: Completed
● Step 2: In progress...
○ Step 3: Pending
○ Step 4: PendingBest for: 3-7 steps, detailed descriptions
Horizontal Stepper
[1]───[2]───[3]───[4]
✓ ● ○ ○Best for: 3-5 steps, compact UI, wizards
Compact Badge
Step 2 of 4: Processing...Best for: Minimal UI, mobile, inline display
Timeline
2:00 PM ─● Started analysis
2:01 PM ─✓ Extracted 234 records
2:02 PM ─● Generating report...
○ FinalizingBest for: Audit trails, long-running tasks
Implementation Patterns
Pattern A: Linear Steps with Substeps
// Pseudocode
ProgressSteps:
steps: Step[]
currentStep: number
Step:
label: string
status: pending | active | complete | error
substeps?: string[]
duration?: number
render:
<Container>
<ProgressBar percent={currentStep / steps.length * 100} />
<StepList>
{steps.map((step, i) =>
<StepRow status={step.status}>
<StatusIcon status={step.status} />
<Label>{step.label}</Label>
{step.duration && <Duration>{step.duration}s</Duration>}
{step.status === "active" && step.substeps && (
<Substeps>
{step.substeps.map(sub => <SubstepRow>{sub}</SubstepRow>)}
</Substeps>
)}
</StepRow>
)}
</StepList>
</Container>Pattern B: Streaming Steps
// Steps appear as they're discovered (not predetermined)
StreamingProgress:
steps: Step[] = []
onNewStep(step):
steps.push(step)
onStepComplete(stepId):
steps.find(s => s.id === stepId).status = "complete"
render:
<Container>
{steps.map(step => <StepRow {...step} />)}
{isProcessing && <PendingIndicator />}
</Container>Pattern C: Action Plan Preview
// Show planned steps before execution (builds trust)
ActionPlan:
plannedSteps: string[]
executedSteps: Step[]
render:
<Container>
<Header>I'll do the following:</Header>
{plannedSteps.map((step, i) => {
const executed = executedSteps[i]
return (
<StepRow
status={executed?.status || "pending"}
label={step}
actual={executed?.label} // May differ from planned
/>
)
})}
</Container>Edge Cases
Unknown Step Count
When you don't know how many steps upfront:
Processing... (Step 3)
✓ Analyzed structure
✓ Extracted entities
● Resolving references...- Show completed count, not percentage
- Add "..." to indicate more may come
Step Failure with Recovery
✓ Step 1: Complete
✗ Step 2: Failed - Retrying (attempt 2/3)
○ Step 3: Waiting- Show retry status
- Don't reset the whole progress
- Offer skip option for non-critical steps
Parallel Steps
● Processing (3 tasks running)
├─ ● Analyzing images (12/50)
├─ ● Extracting text (34/50)
└─ ● Classifying content (8/50)- Show each parallel task's progress
- Complete parent when all children complete
Long-Running Steps
● Generating report... (2m 34s)
Last update: Processing section 4 of 12
[Show Details] [Cancel]- Provide sub-progress or last action
- Always offer cancel for long steps
- Consider timeout warnings
Accessibility
- Use
aria-live="polite"for step announcements - Progress bar needs
role="progressbar"witharia-valuenow - Ensure color isn't only indicator (icons matter)
- Support keyboard navigation between steps
Reasoning Display Patterns
For AI interfaces that show chain-of-thought or "thinking" processes.
Table of Contents
- When to Use
- Anatomy of a Thinking Indicator
- Visibility Levels
- State Machine
- Implementation Patterns
- Accessibility
When to Use
Use reasoning display when:
- AI is solving complex problems (math, logic, planning)
- Users benefit from understanding how the answer was reached
- Trust-building is important (financial, medical, legal domains)
- The process takes >3 seconds
Skip reasoning display when:
- Simple Q&A or lookups
- Speed is more important than transparency
- Content speaks for itself (creative writing, summaries)
Anatomy of a Thinking Indicator
┌─────────────────────────────────────────┐
│ 🧠 Thinking... 12s │ ← Header: icon + label + timer
├─────────────────────────────────────────┤
│ ▸ Analyzing the problem │ ← Expandable reasoning steps
│ ▸ Considering edge cases │
│ • Checking mathematical constraints │ ← Current step (animated)
└─────────────────────────────────────────┘Components:
1. Icon - Animated to show activity (brain, sparkles, dots) 2. Label - Changes based on phase ("Thinking...", "Analyzing...", "Finalizing...") 3. Timer - Elapsed time (reduces anxiety about stuck states) 4. Reasoning list - Structured bullets, progressive reveal 5. Expand/collapse - User control over detail level
Visibility Levels
Level 1: Minimal (ChatGPT-style)
Thinking...- Just a label + animation
- Collapses when done
- Best for: Fast responses, simple queries
Level 2: Summary (Claude-style)
▼ Thinking (8s)
• Analyzing request
• Searching knowledge
• Formulating response- Collapsed by default, expandable
- Shows high-level steps
- Best for: Balance of transparency and simplicity
Level 3: Verbose (DeepSeek-style)
Thinking...
First, I need to understand what the user is asking.
They want to know about X, which involves Y and Z.
Let me consider the implications...
[continues streaming]- Full reasoning visible
- Streams in real-time
- Best for: Power users, debugging, research contexts
State Machine
┌─────────┐ start ┌──────────┐ complete ┌──────────┐
│ Idle │ ─────────→ │ Thinking │ ─────────→ │ Done │
└─────────┘ └──────────┘ └──────────┘
│ │
│ error │ dismiss
▼ ▼
┌──────────┐ ┌──────────┐
│ Error │ │ Hidden │
└──────────┘ └──────────┘State behaviors:
- Idle: No indicator visible
- Thinking: Animated indicator, timer counting, steps appearing
- Done: Animation stops, checkmark or fade, then transition to answer
- Error: Red state, error message, retry option
- Hidden: Collapsed but accessible via "Show thinking"
Implementation Patterns
Pattern A: Collapsible Accordion
// Pseudocode - adapt to your framework
ThinkingIndicator:
state: collapsed | expanded
steps: string[]
currentStep: number
elapsedTime: number
render:
if collapsed:
<Row onClick={expand}>
<AnimatedIcon />
<Label>"Thinking..."</Label>
<Timer>{elapsedTime}s</Timer>
<ChevronDown />
</Row>
if expanded:
<Column>
<Header onClick={collapse}>
<AnimatedIcon />
<Label>"Thinking..."</Label>
<Timer>{elapsedTime}s</Timer>
<ChevronUp />
</Header>
<StepList>
{steps.map((step, i) =>
<Step
complete={i < currentStep}
active={i === currentStep}
>
{step}
</Step>
)}
</StepList>
</Column>Pattern B: Inline Badge
// For chat interfaces where thinking appears inline
<Message>
<ThinkingBadge status={thinking ? "active" : "complete"}>
{thinking ? "Thinking..." : `Thought for ${time}s`}
</ThinkingBadge>
{!thinking && <Content>{response}</Content>}
</Message>Pattern C: Side Panel
// For complex reasoning that shouldn't interrupt main flow
<Layout>
<MainContent>
{response}
</MainContent>
<SidePanel visible={showReasoning}>
<ReasoningTrace steps={steps} />
</SidePanel>
</Layout>Accessibility
- Screen readers: Announce state changes ("Now thinking", "Response ready")
- Reduced motion: Replace animations with static indicators
- Keyboard: Expand/collapse with Enter/Space
- Color: Don't rely on color alone for state (use icons too)
Common Mistakes
1. No end state - Users don't know thinking finished 2. Too verbose - Reasoning overwhelms the answer 3. No escape - Can't stop or skip long thinking 4. Frozen UI - Blocking interaction during thinking 5. Lost on error - No recovery path when thinking fails
Status Indicator Patterns
For AI interfaces with background processing or polling states.
Table of Contents
- When to Use
- Indicator Types
- Placement Strategies
- Implementation Patterns
- Notification Patterns
- Edge Cases
When to Use
Use status indicators when:
- Work happens in the background
- User can continue other tasks while waiting
- Process duration is unpredictable
- User needs awareness without blocking interaction
Examples:
- "Syncing your data..."
- "Indexing files in background"
- "Waiting for API response"
- "Training model (this may take a while)"
Indicator Types
Spinner / Loader
⟳ Processing...- Simple, universally understood
- No progress information
- Best for: Unknown duration, quick tasks
Pulsing Dot
● Syncing- Subtle, non-intrusive
- Shows activity without demanding attention
- Best for: Ambient background status
Progress Ring / Bar
◐ 45% complete
━━━━━━━━░░░░░░░░- Shows measurable progress
- Best for: Known duration/size, file uploads
Status Badge
[Processing] Document.pdf- Inline with content
- Best for: Per-item status in lists
Toast / Banner
┌──────────────────────────────────┐
│ ● Background sync in progress... │
└──────────────────────────────────┘- Temporary, dismissible
- Best for: Temporary states, notifications
Placement Strategies
Global Status Bar
┌────────────────────────────────────────┐
│ App Header ● Sync │ ← Top-right badge
├────────────────────────────────────────┤
│ │
│ Main Content │
│ │
└────────────────────────────────────────┘- Always visible
- Doesn't interrupt flow
- Click to expand details
Contextual Inline
┌─ Files ────────────────────────────────┐
│ 📄 Report.pdf ✓ Ready │
│ 📄 Analysis.xlsx ⟳ Processing │ ← Per-item status
│ 📄 Summary.doc ✓ Ready │
└────────────────────────────────────────┘- Status next to affected item
- Clear relationship
- Good for lists/tables
Floating Indicator
┌───────────────────┐
│ ● 3 tasks running │
│ View details → │
└───────────────────┘
Main Content- Doesn't take layout space
- Can be minimized
- Good for non-blocking background tasks
Full-Screen Overlay (Use Sparingly)
┌────────────────────────────────────────┐
│ │
│ ⟳ Preparing workspace... │
│ │
│ This may take a minute │
│ │
└────────────────────────────────────────┘- Blocks interaction
- Only for critical initialization
- Must have timeout/escape
Implementation Patterns
Pattern A: Background Task Manager
// Pseudocode
TaskManager:
tasks: Task[] = []
Task:
id: string
label: string
status: pending | running | complete | error
progress?: number
addTask(task):
tasks.push(task)
updateTask(id, updates):
tasks.find(t => t.id === id).merge(updates)
render:
<FloatingPanel>
<Header>
{runningCount} task(s) running
<CollapseButton />
</Header>
{expanded && (
<TaskList>
{tasks.map(task => (
<TaskRow status={task.status}>
<Label>{task.label}</Label>
{task.progress && <ProgressBar value={task.progress} />}
{task.status === "error" && <RetryButton />}
</TaskRow>
))}
</TaskList>
)}
</FloatingPanel>Pattern B: Polling Status
// For async operations that require polling
PollingStatus:
status: idle | polling | success | error
lastCheck: timestamp
startPolling(interval = 2000):
status = "polling"
poll()
poll():
result = await checkStatus()
lastCheck = now()
if result.complete:
status = "success"
onComplete(result)
else if result.error:
status = "error"
else:
setTimeout(poll, interval)
render:
switch status:
case "polling":
<Badge>
<Spinner />
Checking... (last: {timeAgo(lastCheck)})
</Badge>
case "success":
<Badge success>Complete</Badge>
case "error":
<Badge error>
Failed <RetryButton onClick={startPolling} />
</Badge>Pattern C: Optimistic UI with Background Sync
// Show immediate feedback, sync in background
OptimisticAction:
localState: any
syncStatus: synced | syncing | error
performAction(action):
// Update UI immediately
localState = applyAction(localState, action)
syncStatus = "syncing"
// Sync in background
try:
await syncToServer(action)
syncStatus = "synced"
catch:
syncStatus = "error"
// Optionally revert localState
render:
<Container>
<Content data={localState} />
<SyncIndicator status={syncStatus} />
</Container>Notification Patterns
Completion Notifications
// When background task completes
onTaskComplete(task):
if document.hidden:
// User not looking - use system notification
showSystemNotification(`${task.label} complete`)
else if !taskPanelVisible:
// User in app but not watching - use toast
showToast(`${task.label} complete`, { action: "View" })
else:
// User watching - just update status
updateTaskStatus(task.id, "complete")Error Notifications
// Errors need more attention than success
onTaskError(task, error):
// Always show prominent error
showToast({
type: "error",
title: `${task.label} failed`,
message: error.message,
actions: [
{ label: "Retry", onClick: () => retryTask(task) },
{ label: "Dismiss", onClick: () => dismissTask(task) }
],
persistent: true // Don't auto-dismiss errors
})Edge Cases
Multiple Concurrent Tasks
<StatusBar>
{tasks.length === 1 ? (
<SingleTaskView task={tasks[0]} />
) : (
<MultiTaskSummary count={tasks.length} onClick={showDetails} />
)}
</StatusBar>Stale Status
// Detect stuck states
if (task.status === "running" && now() - task.lastUpdate > STALE_THRESHOLD):
<TaskRow>
<Label>{task.label}</Label>
<Warning>
No updates for {timeAgo(task.lastUpdate)}
<Button onClick={checkStatus}>Check</Button>
<Button onClick={cancel}>Cancel</Button>
</Warning>
</TaskRow>Reconnection
// After connection loss
<ConnectionStatus>
{isReconnecting && (
<Banner>
<Spinner /> Reconnecting...
</Banner>
)}
{justReconnected && (
<Banner success>
✓ Reconnected. Syncing {pendingChanges} changes...
</Banner>
)}
</ConnectionStatus>Long-Running Background Tasks
// Tasks that take minutes/hours
<LongTaskStatus task={task}>
<Title>{task.label}</Title>
<Progress value={task.progress} />
<Estimate>~{task.estimatedRemaining} remaining</Estimate>
<Options>
<Checkbox checked={notifyOnComplete}>
Notify me when complete
</Checkbox>
<Button onClick={runInBackground}>
Continue in background
</Button>
</Options>
</LongTaskStatus>Accessibility
- Use
aria-live="polite"for status updates aria-busy="true"on elements being processed- Don't use only color to indicate status
- Ensure spinners have accessible labels
- Provide text alternatives for all indicators
- Allow users to pause/hide animations
Streaming Content Patterns
For AI interfaces that display content as it generates token-by-token.
Table of Contents
- When to Use
- Streaming Behaviors
- Visual Indicators
- Implementation Patterns
- Performance Considerations
- Edge Cases
When to Use
Use streaming display when:
- Content generates incrementally (LLM token streaming)
- Showing progress reduces perceived wait time
- Users can start reading before generation completes
- Response length is unpredictable
Skip streaming when:
- Response is very short (<50 tokens)
- Content needs validation before display
- Structured output must be complete to render (JSON, tables)
Streaming Behaviors
Text Streaming
The quick brown fox jum|
The quick brown fox jumps over|
The quick brown fox jumps over the lazy dog.|- Append tokens as received
- Cursor indicates active generation
Block Streaming
┌──────────────────────────┐
│ Generating... │
│ ████████░░░░░░░░ 50% │
└──────────────────────────┘
↓
┌──────────────────────────┐
│ Here is your summary: │
│ │
│ The document discusses...│
└──────────────────────────┘- Show placeholder until enough content
- Reveal in chunks for smoother reading
Skeleton → Content
┌──────────────────────────┐ ┌──────────────────────────┐
│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │ Introduction │
│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ → │ This report covers... │
│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │ Key findings include: │
└──────────────────────────┘ └──────────────────────────┘- Pre-render expected shape
- Replace skeleton with real content
- Reduces layout shift
Visual Indicators
Cursor Styles
█ Block cursor (classic terminal)
| Line cursor (text editor)
● Dot cursor (minimal)
▋ Half-block cursor (modern)Animation States
- Typing: Cursor visible, blinking or solid
- Paused: Cursor visible, no blink, "paused" indicator
- Complete: Cursor hidden, content stable
- Error: Red indicator, error message appended
Container Indicators
┌─ Generating ─────────────────┐
│ Content streams here... │
│ ... │ ← Activity dots
└──────────────────────────────┘Implementation Patterns
Pattern A: Simple Token Append
// Pseudocode
StreamingText:
content: string = ""
isGenerating: boolean = false
onToken(token):
content += token
render:
<Container>
<Text>{content}</Text>
{isGenerating && <Cursor />}
</Container>Pattern B: Smooth Token Rendering
// Batch tokens to reduce render frequency
StreamingText:
displayContent: string = ""
buffer: string = ""
onToken(token):
buffer += token
// Flush buffer on interval (e.g., 50ms) or buffer size
flushBuffer():
displayContent += buffer
buffer = ""
render:
<Container>
<Text>{displayContent}</Text>
{isGenerating && <Cursor />}
</Container>Pattern C: Markdown Streaming
// Handle markdown as it streams
MarkdownStream:
rawContent: string = ""
onToken(token):
rawContent += token
render:
// Parse complete markdown blocks, hold incomplete ones
const { complete, partial } = parseMarkdown(rawContent)
<Container>
<Markdown>{complete}</Markdown>
<RawText>{partial}</RawText>
{isGenerating && <Cursor />}
</Container>Pattern D: Code Block Streaming
// Special handling for code blocks
CodeStream:
content: string = ""
inCodeBlock: boolean = false
onToken(token):
content += token
inCodeBlock = detectCodeBlock(content)
render:
if inCodeBlock:
<CodeEditor
value={content}
language={detectLanguage(content)}
streaming={true}
/>
else:
<Text>{content}</Text>Performance Considerations
Token Batching
Don't render on every single token—batch updates:
// Bad: renders 100 times for 100 tokens
tokens.forEach(token => setContent(c => c + token))
// Good: batches into ~20 renders
const BATCH_INTERVAL = 50 // ms
let batch = ""
tokens.forEach(token => batch += token)
setInterval(() => {
if (batch) {
setContent(c => c + batch)
batch = ""
}
}, BATCH_INTERVAL)Virtual Scrolling
For very long outputs:
// Only render visible portion
<VirtualList
items={contentLines}
renderItem={(line) => <Line>{line}</Line>}
autoScroll={isGenerating}
/>Debounced Parsing
For rich content (markdown, code highlighting):
// Don't re-parse on every token
const parsedContent = useDebouncedMemo(
() => parseMarkdown(rawContent),
[rawContent],
100 // ms
)Edge Cases
Stop Generation
<Container>
<StreamingContent content={content} />
{isGenerating && (
<StopButton onClick={stop}>
■ Stop generating
</StopButton>
)}
</Container>- Always provide escape hatch
- Clearly indicate what will happen (content preserved)
Connection Loss
<Container>
<Text>{content}</Text>
{connectionLost && (
<Warning>
Connection lost.
<Button onClick={resume}>Resume</Button>
<Button onClick={retry}>Retry from start</Button>
</Warning>
)}
</Container>- Preserve partial content
- Offer resume if possible
Rate Limiting
<Container>
<Text>{content}</Text>
{rateLimited && (
<Notice>
Slowing down... will continue in {countdown}s
</Notice>
)}
</Container>- Don't lose content on rate limit
- Show clear status and countdown
Empty Response
if (isComplete && !content) {
<EmptyState>
No response generated. This might mean:
• The query was too vague
• Content was filtered
<Button onClick={retry}>Try again</Button>
</EmptyState>
}Accessibility
- Announce streaming start: "Generating response"
- Announce completion: "Response complete"
- Don't auto-scroll aggressively (disorienting for screen readers)
- Provide "skip to end" option for long streams
- Ensure stop button is keyboard accessible