Design Systems for AI

Designing for Uncertainty

When you design a standard form, you know exactly what can go wrong. A field is empty. An email is invalid. A number is out of range. The error states are finite, predictable, and testable.

When you design for an AI Agent, you have no idea what it might output:

It might hallucinate facts with complete confidence

It might refuse to answer for opaque reasons

It might write a poem instead of JSON

It might loop endlessly attempting and failing

It might produce something 90% correct with a critical error in the remaining 10%

This matters to all of us building AI-powered products because the interface is the trust surface. Users can't see the model, can't understand the inference, can't verify the training data. They experience the UI. If your interface fails to communicate uncertainty, handle failures gracefully, or enable correction, even a capable AI becomes unusable.

The Trust Battery Concept

Think of user trust as a battery. Every successful interaction charges it. Every unexpected failure, hallucination, or confusing response drains it. Your UI design directly controls the charging and draining rates.

Trust Chargers:

Accurate responses that meet expectations

Clear communication of limitations

Easy correction when AI is wrong

Consistent behavior over time

Transparency about what the AI can and cannot do

Trust Drainers:

Confident-sounding hallucinations

Unexplained failures or refusals

Inconsistent behavior for similar inputs

Hidden limitations that surprise users

Inability to correct or guide the AI

Your interface design determines how gracefully you handle both scenarios.

Core Pattern 1: Streaming UI for Transparency

Streaming responses serve multiple purposes beyond perceived speed:

Show Thinking, Not Just Results

function AIResponse({ stream }: { stream: AsyncIterable }) {
const [content, setContent] = useState('');
const [status, setStatus] = useState<'thinking' | 'generating' | 'done'>('thinking');

useEffect(() => {
async function consume() {
setStatus('thinking');

// Show thinking indicator
await delay(500);
setStatus('generating');

for await (const chunk of stream) {
setContent(prev => prev + chunk);
}

setStatus('done');
}
consume();
}, [stream]);

return (

{status === 'thinking' && (

Analyzing your request...

)}

{status === 'generating' && }

{status === 'done' && (

)}

);
}

Why this matters:

"Thinking" phase sets expectations (not instant)

Character-by-character generation shows active work

Users can read while generation happens

Clear transition to "done" state with action options

Progressive Enhancement

Stream additional context after the main response:

function EnhancedResponse({ response, citations }) {
return (

{/ Main content streams first /}

{/ Citations load after main content /}
}>

{/ Related suggestions load last /}
}>

);
}

Users get the answer immediately, with supporting information loading progressively.

Core Pattern 2: User Correction as First-Class Feature

AI will be wrong. Design for easy correction, not as an edge case but as a core workflow.

Inline Editing

function EditableAIOutput({ content, onUpdate }) {
const [isEditing, setIsEditing] = useState(false);
const [editedContent, setEditedContent] = useState(content);

const handleSave = async () => {
await onUpdate(editedContent);
setIsEditing(false);

// Track correction for model improvement
trackCorrection({
original: content,
corrected: editedContent,
type: 'user_edit'
});
};

return (

{isEditing ? (

value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
/>

Save
setIsEditing(false)}>Cancel

) : (

{content}

className="edit-trigger"
onClick={() => setIsEditing(true)}
>
Edit

)}

);
}

Partial Acceptance

Let users accept parts of a response and regenerate others:

function SegmentedResponse({ segments }) {
const [acceptedSegments, setAcceptedSegments] = useState({});

return (

{segments.map((segment, i) => (
key={i}
className={segment ${acceptedSegments[i] ? 'accepted' : ''}}
>

{segment.content}

acceptSegment(i)}>
Accept

regenerateSegment(i)}>
Regenerate

editSegment(i)}>
Edit

))}

);
}

Feedback Loops

Every AI interaction should offer feedback options:

function FeedbackButtons({ responseId }) {
const [feedback, setFeedback] = useState(null);

const submitFeedback = async (type: 'good' | 'bad', details?: string) => {
await api.submitFeedback({ responseId, type, details });
setFeedback(type);
};

if (feedback) {
return Thanks for your feedback!;
}

return (

onClick={() => submitFeedback('good')}
aria-label="This was helpful"
>

onClick={() => openDetailedFeedback('bad')}
aria-label="This wasn't helpful"
>

);
}

Negative feedback should prompt for more detail - what was wrong helps improve the system.

Core Pattern 3: Citation and Source Display

Grounding AI responses in verifiable sources builds trust and enables verification.

Inline Citations

function CitedContent({ content, citations }) {
const citedContent = parseContentWithCitations(content);

return (

{citedContent.map((segment, i) => (
segment.type === 'text' ? (
{segment.content}
) : (
key={i}
citation={citations[segment.citationId]}
/>
)
))}

Sources

{citations.map((citation, i) => (

))}

);
}

function CitationReference({ citation }) {
const [showPreview, setShowPreview] = useState(false);

return (
className="citation-ref"
onMouseEnter={() => setShowPreview(true)}
onMouseLeave={() => setShowPreview(false)}
>
[{citation.id}]
{showPreview && (

{citation.title}

{citation.excerpt}

View source →

)}

);
}

Confidence Indicators

When appropriate, show how confident the AI is:

function ConfidenceIndicator({ confidence }: { confidence: number }) {
const level =
confidence > 0.9 ? 'high' :
confidence > 0.7 ? 'medium' :
confidence > 0.5 ? 'low' : 'very-low';

const labels = {
'high': 'High confidence',
'medium': 'Moderate confidence - verify important details',
'low': 'Low confidence - please verify',
'very-low': 'Very uncertain - treat as starting point only'
};

return (

confidence-indicator ${level}}>

{labels[level]}

);
}

Core Pattern 4: Graceful Failure States

AI fails in unique ways. Design for each failure mode:

Refusal Handling

function RefusalState({ reason, alternatives }) {
return (

I can't help with this request

{reason}

{alternatives.length > 0 && (

Here are some things I can help with instead:

{alternatives.map((alt, i) => (

tryAlternative(alt)}>
{alt.suggestion}

))}

)}

Rephrase your request

);
}

Hallucination Warnings

When the AI generates information that can't be verified:

function UnverifiedContent({ content, canVerify }) {
return (

{!canVerify && (

This response includes information I couldn't verify.
Please double-check important facts.

)}

{content}

Was this information accurate?
reportAccurate()}>Yes
reportInaccurate()}>No, report issue

);
}

Timeout and Error States

function AIError({ error, onRetry }) {
const errorMessages = {
'timeout': {
title: 'This is taking longer than expected',
message: 'The request is still processing. You can wait or try again.',
actions: ['wait', 'retry', 'simplify']
},
'overloaded': {
title: 'High demand right now',
message: 'Please try again in a moment.',
actions: ['retry']
},
'context_too_long': {
title: 'Too much information at once',
message: 'Try breaking your request into smaller parts.',
actions: ['simplify']
},
'unknown': {
title: 'Something went wrong',
message: 'We encountered an unexpected issue.',
actions: ['retry', 'contact_support']
}
};

const { title, message, actions } = errorMessages[error.type] || errorMessages.unknown;

return (

{title}

{message}

{actions.includes('retry') && (
Try again
)}
{actions.includes('simplify') && (
openSimplifyDialog()}>
Help me simplify

)}
{actions.includes('wait') && (
continueWaiting()}>
Keep waiting

)}

);
}

Core Pattern 5: Human-in-the-Loop Workflows

For high-stakes AI actions, build approval workflows:

Preview and Confirm

function AIActionPreview({ action, onConfirm, onCancel }) {
return (

Review before executing

The AI wants to:

{action.description}

This will affect:

{action.affectedItems.map((item, i) => (

{item}

))}

{action.reversible ? (

This can be undone

) : (

This cannot be undone

)}

className="confirm-button"
onClick={onConfirm}
>
Confirm and execute

className="cancel-button"
onClick={onCancel}
>
Cancel

editAction()}>
Modify first

);
}

Staged Execution

For complex operations, show progress and allow intervention:

function StagedExecution({ stages }) {
return (

{stages.map((stage, i) => (

stage ${stage.status}}>

{stage.name}

{stage.status === 'in_progress' && (

pauseExecution()}>
Pause

)}

{stage.status === 'awaiting_approval' && (

{stage.approvalMessage}

approveStage(i)}>
Continue

rejectStage(i)}>
Stop here

)}

{stage.status === 'completed' && stage.result && (

{stage.result.summary}
showDetails(i)}>
View details

)}

))}

);
}

Design Principles for AI UX

1. Set Expectations Early

Before users interact with AI, tell them what to expect:

What the AI can and cannot do

What kinds of inputs work best

When to trust output versus verify

How to get help when AI fails

2. Make the AI's State Visible

Users should always know:

Is the AI working or idle?

How far along is a long operation?

Did something fail or succeed?

What can be done next?

3. Preserve User Agency

The user is in control, not the AI:

Always provide escape hatches

Never force users to accept AI output

Make correction as easy as generation

Allow users to override AI decisions

4. Design for Error Recovery

Every failure mode needs a recovery path:

Clear error messages (not technical jargon)

Suggested next steps

Easy retry mechanisms

Fallback to human assistance

5. Build Trust Gradually

Start with low-stakes, high-visibility operations:

Show AI work before it executes

Require confirmation for impactful actions

Track and display accuracy over time

Let users adjust AI autonomy based on trust

My Take: UX Is the Bottleneck

In my opinion, AI capability is outpacing AI UX. We have models that can do remarkable things, but interfaces that fail to communicate those capabilities, handle failures gracefully, or build user trust.

The teams that win in AI products won't just have the best models - they'll have interfaces that make those models usable, trustworthy, and delightful. Design for uncertainty. Build for correction. Communicate confidence. Show your work.

The AI can be brilliant, but if users don't trust it, don't understand it, and can't correct it - it doesn't matter. UX is the bottleneck.

Design accordingly.