Skip to main content

:::tip 🎮 Interactive Playground Visualize this concept: Try the System Prompt Design demo on the EngineersOfAI Playground - no code required. :::

Prompt UX Patterns

The Empty Text Box Problem

When Notion launched their AI writing assistant, they faced a problem their engineering team hadn't anticipated: users opened the AI panel, saw a blank text box with the placeholder "Ask AI anything...", and didn't know what to type. Engagement dropped sharply after the first session. Cohort analysis told the story: users who had gotten a genuinely useful AI response in their first session retained at 68%. Users who had typed something vague ("help me write better"), gotten a mediocre response, and concluded the AI wasn't useful retained at 11%. The AI was capable. The interface wasn't communicating that capability.

The same pattern played out at a dozen other companies. A developer productivity tool shipped an AI assistant embedded in their IDE. Usage data showed that 74% of users who tried it typed one of three things: "help", "what can you do", or a question so vague the AI couldn't give a useful answer. The feature had a 40% first-session abandonment rate - users who tried it once, got a bad response, and never came back. When they redesigned the empty state to show four context-specific suggestions based on what file was open in the editor, first-session abandonment dropped to 12%.

The blank text box problem is one of the most underappreciated UX challenges in AI product development. You can build a model that can do extraordinary things, and your users will never discover it because the interface doesn't communicate what it can do or how to ask. Prompt UX is the discipline of designing that communication layer: suggestions, slash commands, mode switching, context transparency, and feedback loops that help users become effective at using the AI.


Why Prompt UX Exists

Traditional software has menus, buttons, and forms that constrain user input to what the system can handle. AI interfaces have a text box that accepts anything - including requests the AI handles poorly, or requests that are so vague the AI cannot give a useful answer.

This openness is both the power and the problem. Users who know how to prompt effectively unlock enormous value. Users who don't - which is most users at most companies - fail to extract meaningful value and churn. Prompt UX bridges this gap by:

  1. Discovery - showing users what the AI can do before they ask
  2. Scaffolding - giving users templates and starting points for effective prompts
  3. Control - letting power users precisely control AI behavior without remembering exact phrasings
  4. Transparency - showing what context the AI is using, so users understand its responses
  5. Feedback - closing the loop so users can refine, regenerate, and improve responses

Pattern 1: Contextual Prompt Suggestions

Show suggested prompts based on current app context, not generic placeholders. "Ask AI anything..." is worthless. "Summarize this contract," "Find the key milestones," or "Draft a response to this email" tell users what the AI can do with what they're currently looking at.

// types/prompt-suggestions.ts
interface PromptSuggestion {
id: string;
label: string; // Short label for the chip/button
fullPrompt: string; // The actual prompt sent to the AI
icon?: string;
category: "analysis" | "writing" | "action" | "search" | "code";
context: string[]; // When to show: "document_open", "text_selected", "empty_chat"
priority: number; // Higher = shown first
}

interface AppContext {
selectedText?: string;
openDocument?: { name: string; type: string; wordCount: number };
lastAction?: string;
conversationLength: number;
userRole?: string;
currentPage?: string;
}

// Generate suggestions that are relevant RIGHT NOW
function getContextualSuggestions(
appCtx: AppContext,
maxSuggestions: number = 4,
): PromptSuggestion[] {
const all: PromptSuggestion[] = [];

// Text selected - highest priority context
if (appCtx.selectedText) {
const text = appCtx.selectedText.slice(0, 200);
all.push(
{
id: "explain-selected",
label: "Explain this",
fullPrompt: `Explain this clearly and concisely:\n\n"${text}"`,
icon: "💡",
category: "analysis",
context: ["text_selected"],
priority: 10,
},
{
id: "improve-selected",
label: "Improve writing",
fullPrompt: `Improve the clarity, tone, and impact of this text:\n\n"${text}"`,
icon: "✍️",
category: "writing",
context: ["text_selected"],
priority: 9,
},
{
id: "summarize-selected",
label: "Summarize",
fullPrompt: `Summarize this in 2-3 sentences:\n\n"${text}"`,
icon: "📝",
category: "analysis",
context: ["text_selected"],
priority: 8,
},
{
id: "translate-selected",
label: "Translate to English",
fullPrompt: `Translate this to English:\n\n"${text}"`,
icon: "🌐",
category: "action",
context: ["text_selected"],
priority: 7,
}
);
}

// Document open (no selection)
else if (appCtx.openDocument) {
const { name, type } = appCtx.openDocument;
const isCode = ["js", "ts", "py", "go", "rs"].some(ext => name.endsWith(`.${ext}`));
const isLegal = type === "contract" || name.toLowerCase().includes("agreement");

if (isCode) {
all.push(
{
id: "review-code",
label: "Review this code",
fullPrompt: "Review this code for bugs, performance issues, and best practices violations. Format your feedback as a bulleted list.",
icon: "🔍",
category: "code",
context: ["document_open"],
priority: 10,
},
{
id: "explain-code",
label: "Explain how this works",
fullPrompt: "Explain how this code works, in plain English. Focus on the high-level logic and any non-obvious patterns.",
icon: "💡",
category: "code",
context: ["document_open"],
priority: 9,
},
{
id: "add-tests",
label: "Generate tests",
fullPrompt: "Write comprehensive unit tests for this code. Cover happy paths, edge cases, and error conditions.",
icon: "🧪",
category: "code",
context: ["document_open"],
priority: 8,
}
);
} else if (isLegal) {
all.push(
{
id: "summarize-contract",
label: "Summarize key terms",
fullPrompt: "Summarize the key terms, obligations, and risks in this document. Use bullet points.",
icon: "📋",
category: "analysis",
context: ["document_open"],
priority: 10,
},
{
id: "flag-risks",
label: "Flag potential risks",
fullPrompt: "Identify any unusual clauses, missing standard protections, or potential risks in this document that a lawyer should review.",
icon: "⚠️",
category: "analysis",
context: ["document_open"],
priority: 9,
}
);
} else {
all.push(
{
id: "summarize-doc",
label: "Summarize document",
fullPrompt: "Summarize the key points of this document in bullet points. Include any action items or decisions.",
icon: "📋",
category: "analysis",
context: ["document_open"],
priority: 10,
},
{
id: "extract-actions",
label: "Extract action items",
fullPrompt: "Extract all action items and next steps from this document. Format as a checklist with owners if mentioned.",
icon: "✅",
category: "action",
context: ["document_open"],
priority: 9,
},
{
id: "generate-questions",
label: "Generate questions",
fullPrompt: "Generate 5 thoughtful questions this document raises that deserve follow-up.",
icon: "❓",
category: "analysis",
context: ["document_open"],
priority: 8,
}
);
}
}

// Empty chat - show capability samples
else if (appCtx.conversationLength === 0) {
all.push(
{
id: "draft-email",
label: "Draft an email",
fullPrompt: "Help me write a professional email to ",
icon: "📧",
category: "writing",
context: ["empty_chat"],
priority: 8,
},
{
id: "analyze-data",
label: "Analyze data",
fullPrompt: "I have data I'd like to analyze and understand. Here it is:\n\n",
icon: "📊",
category: "analysis",
context: ["empty_chat"],
priority: 7,
},
{
id: "brainstorm",
label: "Brainstorm ideas",
fullPrompt: "Help me brainstorm ideas for ",
icon: "🧠",
category: "writing",
context: ["empty_chat"],
priority: 6,
}
);
}

return all
.sort((a, b) => b.priority - a.priority)
.slice(0, maxSuggestions);
}


// React component for suggestion chips
interface PromptSuggestionsProps {
suggestions: PromptSuggestion[];
onSelect: (prompt: string, fillInputOnly?: boolean) => void;
layout?: "chips" | "cards" | "list";
}

function PromptSuggestions({
suggestions,
onSelect,
layout = "chips",
}: PromptSuggestionsProps) {
if (!suggestions.length) return null;

if (layout === "cards") {
return (
<div className="grid grid-cols-2 gap-2 mb-4">
{suggestions.map((s) => (
<button
key={s.id}
onClick={() => onSelect(s.fullPrompt)}
className="text-left p-3 rounded-xl border border-gray-200 hover:border-blue-300 hover:bg-blue-50 transition-colors"
>
<span className="text-lg mb-1 block">{s.icon}</span>
<span className="text-sm font-medium text-gray-800">{s.label}</span>
</button>
))}
</div>
);
}

return (
<div className="flex flex-wrap gap-2 mb-3">
{suggestions.map((s) => (
<button
key={s.id}
onClick={() => onSelect(s.fullPrompt)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-gray-100 hover:bg-blue-100 hover:text-blue-700 text-sm text-gray-700 transition-colors border border-transparent hover:border-blue-200"
>
{s.icon && <span>{s.icon}</span>}
<span>{s.label}</span>
</button>
))}
</div>
);
}

Pattern 2: Slash Commands

Slash commands let power users trigger specific AI modes without memorizing exact phrasings. They are discoverable (autocomplete appears as soon as / is typed), consistent (same command always produces the same behavior), and learnable (users build muscle memory over time).

// slash-commands/registry.ts
interface SlashCommand {
trigger: string; // e.g., "/summarize"
aliases: string[]; // e.g., ["/sum", "/tldr"]
description: string;
icon: string;
systemPromptAddition: string; // Added to system prompt when command is active
placeholder: string; // Updated input placeholder after command
outputFormat?: "bullets" | "prose" | "code" | "table";
}

const SLASH_COMMANDS: SlashCommand[] = [
{
trigger: "/summarize",
aliases: ["/sum", "/tldr"],
description: "Summarize text or documents concisely",
icon: "📝",
systemPromptAddition: "The user wants a concise summary. Use bullet points. Lead with the most important point.",
placeholder: "Paste text to summarize, or describe what to summarize...",
outputFormat: "bullets",
},
{
trigger: "/explain",
aliases: ["/eli5"],
description: "Explain a concept clearly",
icon: "💡",
systemPromptAddition: "Explain clearly using simple language. Use concrete analogies. Avoid jargon. Structure from basic to advanced.",
placeholder: "What would you like explained?",
outputFormat: "prose",
},
{
trigger: "/fix",
aliases: ["/debug", "/repair"],
description: "Fix and improve code",
icon: "🔧",
systemPromptAddition: "Fix the code. Show the corrected version first, then explain what was wrong and why. Highlight the specific changes.",
placeholder: "Paste the code with the issue...",
outputFormat: "code",
},
{
trigger: "/translate",
aliases: ["/tr"],
description: "Translate text to another language",
icon: "🌍",
systemPromptAddition: "Translate the text to the specified language. Maintain tone, formatting, and technical terms appropriately.",
placeholder: "Text to translate. Specify the target language (e.g., 'translate to Spanish')...",
outputFormat: "prose",
},
{
trigger: "/brainstorm",
aliases: ["/ideas"],
description: "Generate diverse ideas",
icon: "🧠",
systemPromptAddition: "Generate 8-10 diverse, creative ideas. Vary the approach: conventional and unconventional. Format as a numbered list with brief rationale for each.",
placeholder: "What would you like to brainstorm about?",
outputFormat: "bullets",
},
{
trigger: "/compare",
aliases: [],
description: "Compare two options",
icon: "⚖️",
systemPromptAddition: "Compare the options in a structured table or side-by-side format. Cover pros, cons, and when to use each.",
placeholder: "What two things would you like to compare?",
outputFormat: "table",
},
{
trigger: "/formal",
aliases: ["/professional"],
description: "Rewrite in formal tone",
icon: "👔",
systemPromptAddition: "Rewrite in a formal, professional tone. Maintain the meaning. Remove casual language, contractions, and colloquialisms.",
placeholder: "Text to make more formal...",
outputFormat: "prose",
},
{
trigger: "/casual",
aliases: ["/friendly"],
description: "Rewrite in conversational tone",
icon: "😊",
systemPromptAddition: "Rewrite in a friendly, conversational tone. Keep it natural and approachable. It's OK to use contractions.",
placeholder: "Text to make more casual...",
outputFormat: "prose",
},
];

const COMMAND_INDEX = new Map<string, SlashCommand>(
SLASH_COMMANDS.flatMap((cmd) => [
[cmd.trigger, cmd],
...cmd.aliases.map((alias) => [alias, cmd] as [string, SlashCommand]),
])
);

export function parseSlashCommand(input: string): {
command: SlashCommand | null;
rest: string;
} {
if (!input.startsWith("/")) return { command: null, rest: input };

const spaceIdx = input.indexOf(" ");
const trigger = spaceIdx === -1 ? input.toLowerCase() : input.slice(0, spaceIdx).toLowerCase();
const rest = spaceIdx === -1 ? "" : input.slice(spaceIdx + 1);

return {
command: COMMAND_INDEX.get(trigger) ?? null,
rest,
};
}

export function getSlashMatches(partial: string): SlashCommand[] {
const query = partial.toLowerCase();
return SLASH_COMMANDS.filter(
(cmd) =>
cmd.trigger.includes(query) ||
cmd.aliases.some((a) => a.includes(query)) ||
cmd.description.toLowerCase().includes(query.replace("/", ""))
);
}


// Slash command autocomplete dropdown
interface SlashCommandMenuProps {
query: string;
onSelect: (command: SlashCommand) => void;
onDismiss: () => void;
}

function SlashCommandMenu({ query, onSelect, onDismiss }: SlashCommandMenuProps) {
const matches = getSlashMatches(query);
const [activeIdx, setActiveIdx] = React.useState(0);

React.useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "ArrowDown") {
setActiveIdx((i) => Math.min(i + 1, matches.length - 1));
e.preventDefault();
} else if (e.key === "ArrowUp") {
setActiveIdx((i) => Math.max(i - 1, 0));
e.preventDefault();
} else if (e.key === "Enter" && matches[activeIdx]) {
onSelect(matches[activeIdx]);
e.preventDefault();
} else if (e.key === "Escape") {
onDismiss();
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [matches, activeIdx, onSelect, onDismiss]);

if (!matches.length) return null;

return (
<div className="absolute bottom-full left-0 right-0 mb-1 bg-white border border-gray-200 rounded-xl shadow-lg overflow-hidden z-50">
<div className="px-3 py-2 text-xs text-gray-400 border-b">
Commands - Tab or Enter to select
</div>
{matches.map((cmd, idx) => (
<button
key={cmd.trigger}
className={`w-full flex items-center gap-3 px-3 py-2.5 text-left hover:bg-gray-50 transition-colors ${
idx === activeIdx ? "bg-blue-50" : ""
}`}
onClick={() => onSelect(cmd)}
>
<span className="text-lg w-8">{cmd.icon}</span>
<div>
<div className="text-sm font-medium text-gray-800">
{cmd.trigger}
{cmd.aliases.length > 0 && (
<span className="text-gray-400 font-normal ml-1">
({cmd.aliases.join(", ")})
</span>
)}
</div>
<div className="text-xs text-gray-500">{cmd.description}</div>
</div>
</button>
))}
</div>
);
}

Pattern 3: AI Mode Switching

Different tasks benefit from different AI personas and behaviors. Mode switching gives users explicit control without requiring them to write different system prompts.

// modes/ai-modes.ts
interface AIMode {
id: string;
name: string;
description: string;
icon: string;
systemPromptSuffix: string;
responseStyle: string;
suggestedPrompts: string[];
color: string;
}

export const AI_MODES: AIMode[] = [
{
id: "assistant",
name: "Assistant",
description: "General-purpose helpful assistant",
icon: "🤖",
color: "blue",
systemPromptSuffix: "Be helpful, concise, and accurate. Format responses clearly.",
responseStyle: "balanced",
suggestedPrompts: [
"Help me think through this problem",
"What are the pros and cons of...",
],
},
{
id: "analyst",
name: "Analyst",
description: "Rigorous data analysis and structured reasoning",
icon: "📊",
color: "indigo",
systemPromptSuffix:
"Analyze rigorously. Show your reasoning step by step. " +
"Highlight key insights, risks, and uncertainties. " +
"Be quantitative where data is available. " +
"State your assumptions explicitly.",
responseStyle: "structured",
suggestedPrompts: [
"What are the key insights from this data?",
"What are the risks I'm not seeing?",
],
},
{
id: "writer",
name: "Writer",
description: "Writing, editing, and content creation",
icon: "✍️",
color: "purple",
systemPromptSuffix:
"Focus on clear, compelling writing. " +
"Improve structure, flow, voice, and impact. " +
"When editing, explain what you changed and why. " +
"Match the tone the user is going for.",
responseStyle: "creative",
suggestedPrompts: [
"Improve the clarity of this paragraph",
"Make this more engaging and compelling",
],
},
{
id: "coder",
name: "Coder",
description: "Code review, debugging, and implementation",
icon: "💻",
color: "green",
systemPromptSuffix:
"Write clean, correct, and maintainable code. " +
"Explain your reasoning and highlight trade-offs. " +
"Show working examples. " +
"Point out potential bugs, edge cases, and security issues. " +
"Follow language-specific idioms and conventions.",
responseStyle: "technical",
suggestedPrompts: [
"Review this code for issues",
"Write a function that...",
],
},
{
id: "teacher",
name: "Teacher",
description: "Learning and deep explanation",
icon: "🎓",
color: "yellow",
systemPromptSuffix:
"Teach clearly and patiently. " +
"Start with fundamentals, build to complexity. " +
"Use concrete examples and analogies. " +
"Check understanding by asking if the user wants to go deeper. " +
"Anticipate and address common misconceptions.",
responseStyle: "educational",
suggestedPrompts: [
"Explain this concept from scratch",
"What's the mental model I should use for this?",
],
},
];

interface ModeSwitcherProps {
currentModeId: string;
onModeChange: (mode: AIMode) => void;
compact?: boolean;
}

function ModeSwitcher({ currentModeId, onModeChange, compact = false }: ModeSwitcherProps) {
const currentMode = AI_MODES.find((m) => m.id === currentModeId) ?? AI_MODES[0];

if (compact) {
return (
<div className="relative">
<select
value={currentModeId}
onChange={(e) => {
const mode = AI_MODES.find((m) => m.id === e.target.value);
if (mode) onModeChange(mode);
}}
className="text-sm border border-gray-200 rounded-lg px-2 py-1 bg-white"
>
{AI_MODES.map((m) => (
<option key={m.id} value={m.id}>
{m.icon} {m.name}
</option>
))}
</select>
</div>
);
}

return (
<div className="flex gap-1 p-1 bg-gray-100 rounded-xl">
{AI_MODES.map((mode) => (
<button
key={mode.id}
onClick={() => onModeChange(mode)}
title={mode.description}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${
currentModeId === mode.id
? "bg-white shadow-sm text-gray-900"
: "text-gray-500 hover:text-gray-700"
}`}
>
<span>{mode.icon}</span>
<span>{mode.name}</span>
</button>
))}
</div>
);
}

Pattern 4: Context Transparency

Users should understand what information the AI is using. Transparency builds trust and helps users understand why they got a particular response.

// components/ContextPanel.tsx
interface AIContextState {
mode: {
id: string;
name: string;
description: string;
};
attachedFiles: Array<{
name: string;
tokenCount: number;
type: string;
}>;
ragSources: Array<{
title: string;
similarity: number;
excerpt: string;
}>;
conversationTokens: number;
maxContextTokens: number;
systemPromptTokens: number;
}

function ContextPanel({ context }: { context: AIContextState }) {
const [expanded, setExpanded] = React.useState(false);
const usedTokens = context.conversationTokens + context.systemPromptTokens;
const pct = Math.round((usedTokens / context.maxContextTokens) * 100);
const isNearFull = pct > 75;

return (
<div className={`rounded-lg border text-sm ${isNearFull ? "border-yellow-200 bg-yellow-50" : "border-gray-200 bg-gray-50"}`}>
<button
className="w-full flex items-center justify-between px-3 py-2"
onClick={() => setExpanded(!expanded)}
>
<div className="flex items-center gap-2">
<span className="text-gray-500">🧠</span>
<span className="text-gray-600 font-medium">
Context - {pct}% used
</span>
{isNearFull && (
<span className="text-xs text-yellow-600 bg-yellow-100 px-1.5 py-0.5 rounded">
Almost full
</span>
)}
</div>
<span className="text-gray-400">{expanded ? "▴" : "▾"}</span>
</button>

{expanded && (
<div className="px-3 pb-3 space-y-2 border-t border-gray-200 pt-2">
{/* Mode */}
<div className="flex items-center gap-2 text-gray-600">
<span className="text-gray-400">⚙️</span>
<span>Mode: <strong>{context.mode.name}</strong> - {context.mode.description}</span>
</div>

{/* Attached files */}
{context.attachedFiles.map((file) => (
<div key={file.name} className="flex items-center gap-2 text-gray-600">
<span className="text-gray-400">📎</span>
<span>
{file.name}
<span className="text-gray-400 ml-1">
(~{Math.round(file.tokenCount / 1000)}K tokens)
</span>
</span>
</div>
))}

{/* RAG sources */}
{context.ragSources.length > 0 && (
<div>
<div className="text-gray-400 text-xs mb-1">Retrieved from knowledge base:</div>
{context.ragSources.map((src) => (
<div key={src.title} className="flex items-center gap-2 text-gray-600 ml-4">
<span className="text-gray-400">📚</span>
<span>
{src.title}
<span className="text-gray-400 ml-1">
{Math.round(src.similarity * 100)}% match
</span>
</span>
</div>
))}
</div>
)}

{/* Context window visualization */}
<div className="mt-2">
<div className="flex justify-between text-xs text-gray-400 mb-1">
<span>Context window</span>
<span>{usedTokens.toLocaleString()} / {context.maxContextTokens.toLocaleString()} tokens</span>
</div>
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all ${
pct > 90 ? "bg-red-400" : pct > 75 ? "bg-yellow-400" : "bg-blue-400"
}`}
style={{ width: `${pct}%` }}
/>
</div>
{pct > 75 && (
<p className="text-xs text-yellow-600 mt-1">
Context is nearly full. Start a new conversation to avoid losing earlier context.
</p>
)}
</div>
</div>
)}
</div>
);
}

Pattern 5: Response Actions and Regeneration

Always give users control over AI output. The regeneration dropdown is one of the highest-value, lowest-effort UX improvements you can make.

# backend/response_actions.py
import anthropic
from typing import Optional

client = anthropic.Anthropic()

REGENERATION_INSTRUCTIONS = {
"retry": None,
"shorter": "Be more concise. Cut to the essential points only.",
"longer": "Provide more detail, depth, and explanation.",
"simpler": "Use simpler language. Avoid jargon. Explain like I'm new to this.",
"formal": "Use formal, professional language.",
"casual": "Use casual, conversational language.",
"examples": "Add more concrete examples to illustrate the points.",
"bullets": "Format the response as a bulleted list.",
"step_by_step": "Break this down into clear numbered steps.",
"table": "Format this as a comparison table where appropriate.",
}


def regenerate_response(
original_messages: list[dict],
original_response: str,
instruction_key: str,
system: str = "You are a helpful assistant.",
) -> str:
"""
Regenerate a response with a style modification.

Strategy: Append the original response and modification instruction
to the conversation. Claude sees its previous attempt and improves on it.

This produces better results than simply re-running the original request,
because the model can see what it did before and explicitly improve.
"""
instruction = REGENERATION_INSTRUCTIONS.get(instruction_key)

if instruction is None and instruction_key == "retry":
# Pure retry - same request, different random seed
messages = original_messages
else:
# Informed regeneration - model sees its previous attempt
messages = original_messages + [
{"role": "assistant", "content": original_response},
{
"role": "user",
"content": f"Please revise your response. Instruction: {instruction}",
},
]

response = client.messages.create(
model="claude-opus-4-6",
max_tokens=2048,
system=system,
messages=messages,
)
return response.content[0].text
// components/ResponseActions.tsx
interface ResponseActionsProps {
messageId: string;
content: string;
conversationMessages: Message[];
onRegenerate: (instruction: string) => void;
onCopy: () => void;
onFeedback: (positive: boolean, reason?: string) => void;
}

const REGENERATE_OPTIONS = [
{ key: "retry", label: "Try again", icon: "🔄" },
{ key: "shorter", label: "Make shorter", icon: "✂️" },
{ key: "longer", label: "More detail", icon: "📖" },
{ key: "simpler", label: "Simpler language", icon: "🔤" },
{ key: "formal", label: "More formal", icon: "👔" },
{ key: "casual", label: "More casual", icon: "😊" },
{ key: "examples", label: "Add examples", icon: "📌" },
{ key: "bullets", label: "Use bullet points", icon: "•" },
{ key: "step_by_step", label: "Step by step", icon: "1️⃣" },
];

function ResponseActions({
messageId,
content,
onRegenerate,
onCopy,
onFeedback,
}: ResponseActionsProps) {
const [copied, setCopied] = React.useState(false);
const [showMenu, setShowMenu] = React.useState(false);
const [feedback, setFeedback] = React.useState<"up" | "down" | null>(null);

const handleCopy = async () => {
await navigator.clipboard.writeText(content);
setCopied(true);
onCopy();
setTimeout(() => setCopied(false), 2000);
};

const handleFeedback = (positive: boolean) => {
setFeedback(positive ? "up" : "down");
onFeedback(positive);
};

return (
<div className="flex items-center gap-1 mt-2 opacity-0 group-hover:opacity-100 transition-opacity">
{/* Copy */}
<button
onClick={handleCopy}
title="Copy response"
className="p-1.5 rounded hover:bg-gray-100 text-gray-400 hover:text-gray-600"
>
{copied ? "✓" : "📋"}
</button>

{/* Thumbs up */}
<button
onClick={() => handleFeedback(true)}
title="Good response"
className={`p-1.5 rounded hover:bg-gray-100 transition-colors ${
feedback === "up" ? "text-green-600" : "text-gray-400 hover:text-gray-600"
}`}
>
👍
</button>

{/* Thumbs down */}
<button
onClick={() => handleFeedback(false)}
title="Poor response"
className={`p-1.5 rounded hover:bg-gray-100 transition-colors ${
feedback === "down" ? "text-red-600" : "text-gray-400 hover:text-gray-600"
}`}
>
👎
</button>

{/* Regenerate dropdown */}
<div className="relative">
<button
onClick={() => setShowMenu(!showMenu)}
className="flex items-center gap-1 px-2 py-1.5 rounded hover:bg-gray-100 text-gray-400 hover:text-gray-600 text-sm"
>
🔄 <span>Regenerate</span> <span></span>
</button>

{showMenu && (
<div className="absolute left-0 bottom-full mb-1 bg-white border border-gray-200 rounded-xl shadow-lg py-1 z-50 min-w-48">
{REGENERATE_OPTIONS.map((opt) => (
<button
key={opt.key}
onClick={() => {
onRegenerate(opt.key);
setShowMenu(false);
}}
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 text-left"
>
<span>{opt.icon}</span>
<span>{opt.label}</span>
</button>
))}
</div>
)}
</div>
</div>
);
}

Pattern 6: Smart Prompt Expansion

For short, ambiguous inputs, offer to expand the prompt before sending. This helps users articulate what they actually need.

# backend/prompt_expansion.py
import anthropic
import re

client = anthropic.Anthropic()

VAGUE_PATTERNS = [
r"^(help|help me|assist)\s*$",
r"^(what can you do|what are you)\s*\??$",
r"^(tell me about|explain|analyze)\s+\w{1,3}\s*$", # "explain AI", "analyze this"
]


def is_prompt_too_vague(user_input: str) -> bool:
"""Detect prompts that are too short or vague to answer well."""
stripped = user_input.strip()

# Too short
if len(stripped.split()) < 5:
return True

# Matches known vague patterns
if any(re.match(p, stripped.lower()) for p in VAGUE_PATTERNS):
return True

# No specific noun/topic
if re.match(r"^(write|create|make|build|generate)\s+(something|it|this)\s*$", stripped.lower()):
return True

return False


def generate_prompt_expansions(
vague_prompt: str,
app_context: str = "",
count: int = 3,
) -> list[str]:
"""
Generate more specific, actionable versions of a vague prompt.
Show to user so they can choose the interpretation that matches their intent.
"""
response = client.messages.create(
model="claude-haiku-4-5-20251001", # Fast + cheap for this utility task
max_tokens=250,
messages=[{
"role": "user",
"content": (
f"The user typed this vague request: '{vague_prompt}'\n"
f"App context: {app_context}\n\n"
f"Generate {count} specific, actionable versions of this request. "
f"Each should be a complete, self-contained prompt that would get a great response. "
f"Make them meaningfully different from each other. "
f"Output one per line, no numbering or bullets."
),
}],
)

lines = [
line.strip()
for line in response.content[0].text.strip().split("\n")
if line.strip() and not line.startswith(("-", "•", "*", "1", "2", "3"))
]

return lines[:count]

Production Engineering Notes

Instrument every suggestion with click tracking. You need to know which suggestions are clicked vs ignored. If "Summarize document" gets clicked 80% of the time and "Generate questions" never gets clicked, promote the popular ones and remove the dead weight. This data also reveals what users actually want from your AI vs. what you assumed they wanted.

Slash commands need keyboard-first design. Power users hate the mouse. The slash command flow should be: type / → autocomplete appears → arrow keys to navigate → Enter to select → cursor positioned in input ready to type the payload. No mouse required.

Mode switching should be sticky but not irreversible. Save the user's last-used mode in localStorage. Switch automatically between sessions. But always show the current mode prominently and make it trivially easy to switch - a confused user who can't figure out why "the AI is being weird" is probably in the wrong mode.

Context window indicators prevent "AI amnesia" frustration. One of the most common complaints about AI chat: "Why did it forget what I told it?" The answer is context window overflow, but users don't know that. A visible context usage indicator that turns yellow at 75% and red at 90% prevents this confusion. Add a "Start fresh conversation" button that appears at 80%.

:::tip Track Which Suggestions Convert Prompt suggestions are features, not cosmetics. Measure their impact: what % of sessions use a suggestion vs typing from scratch? Do sessions that start with a suggestion have higher satisfaction rates (thumbs up) and longer session lengths? Use this data to promote high-converting suggestions and remove low-converting ones. :::

:::warning Don't Auto-Fill Input with Long Prompts Never automatically populate the full text of a suggestion into the input box without user action. If users see a 60-word prompt they didn't write, they feel robbed of agency. Better approaches: (A) Fill the input and place cursor at the end so users can complete it. (B) Run the suggestion immediately and let users edit the response. (C) Show a preview of the prompt before running it. Choice A is most common for templates, choice B for actions. :::


Interview Q&A

Q1: Why is the empty text box a serious UX problem for AI interfaces?

The blank input box requires users to independently know what the AI can do and how to ask effectively. Most users don't have this knowledge, especially early in the product lifecycle. The result is vague queries ("help me"), mediocre AI responses, and a conclusion that "the AI is useless" - even when the AI is capable. The research is consistent: first-session satisfaction with AI products is the strongest predictor of long-term retention, and first-session satisfaction is heavily determined by whether the user got a good response on their first query. Contextual prompt suggestions directly address this by showing users the high-value use cases for their specific situation, reducing the "blank canvas anxiety" that kills first-session success.

Q2: How do you design slash commands for an AI product?

Five design principles: (1) Commands should map to distinct AI behaviors, not just keyword shortcuts - /summarize changes the AI's system prompt to produce a summary format, not just prepend "summarize: ". (2) Show a discovery menu the instant / is typed - never require memorization. (3) Keep the command set to 10-15 - more than that overwhelms users. (4) Each command should update the input placeholder to set expectations about what to type next. (5) Track usage analytics - commands that nobody uses should be removed; commands with high click rates deserve prominent placement. Also support keyboard navigation (arrow keys, Enter) for power users who hate the mouse.

Q3: How do you implement context transparency in an AI interface?

Show a "context panel" that reveals: what mode/persona the AI is in, what files are attached to the conversation, what RAG sources were retrieved for recent queries, and how full the context window is. Don't show the raw system prompt - it's too technical and can reveal business logic. Show human-readable descriptions: "Document assistant mode," "Attached: contract.pdf (12K tokens)." The token usage bar is critical - it's the only signal users have that the AI might start "forgetting" things. This also helps users debug "why did the AI say that" - they can see when the context window is saturated with stale conversation history.

Q4: How do you implement response regeneration that actually improves on the original?

The naive approach - simply re-running the original request - produces different random output but doesn't specifically improve on what went wrong. The better approach: append the original response to the conversation, then send a new user message with the specific modification instruction ("be more concise," "add examples," etc.). The model sees its previous attempt and the explicit instruction, and produces a targeted improvement rather than random variation. This produces dramatically better results for modifications like "shorter," "more formal," or "add step-by-step format." Track which regeneration options are most-used - this tells you what the primary prompt is consistently getting wrong.

Q5: How do you decide which prompt suggestions to show and when?

Context-first: suggestions should be derived from what the user is currently doing, not generic. If a document is open, suggest document actions. If code is selected, suggest code actions. If the chat is empty, show capability-sampling prompts. Priority ranking: base suggestions on historical click-through rates for that context type, the user's current mode, and what the most recent conversation turn was about. Limit to 3-4 suggestions - more than that creates choice paralysis. A/B test the suggestions themselves: a 30% CTR difference between two suggestions for the same context is significant and worth acting on. Remove suggestions with CTR under 5%; promote suggestions with CTR over 40%.

Q6: What metrics tell you if your Prompt UX is working?

Three layers: (1) Discovery: what % of sessions use at least one suggestion, slash command, or mode switch? Target: 40%+ (means users understand the AI's capabilities). (2) Quality lift: do sessions using suggestions have higher thumbs-up rates than sessions where users typed from scratch? Target: 15%+ improvement (means suggestions are guiding users to good prompts). (3) Retention: do users who used a suggestion in session 1 have higher week-2 retention? Target: significant (means good first sessions drive long-term adoption). These three together confirm that Prompt UX is not just decorative - it is actually improving outcomes.

© 2026 EngineersOfAI. All rights reserved.