feat(frontend): implement clarification questions UI for agent generation

Add interactive form to collect user answers when agent-generator service
returns clarifying questions during agent creation/editing.

Changes:
- Add clarification_needed message type to ChatMessageData
- Create ClarificationQuestionsWidget component for collecting answers
- Update parseToolResponse to detect clarification_needed responses
- Integrate widget into ChatMessage rendering

Fixes issue where users had no way to answer clarifying questions,
causing the chat to keep retrying without necessary context.
This commit is contained in:
Zamil Majdy
2026-01-24 15:45:34 -06:00
parent 653aab44b6
commit eebaf7df14
4 changed files with 210 additions and 0 deletions

View File

@@ -213,6 +213,23 @@ export function parseToolResponse(
timestamp: timestamp || new Date(),
};
}
if (responseType === "clarification_needed") {
return {
type: "clarification_needed",
toolName,
questions:
(parsedResult.questions as Array<{
question: string;
keyword: string;
example?: string;
}>) || [],
message:
(parsedResult.message as string) ||
"I need more information to proceed.",
sessionId: (parsedResult.session_id as string) || "",
timestamp: timestamp || new Date(),
};
}
if (responseType === "need_login") {
return {
type: "login_needed",

View File

@@ -14,6 +14,7 @@ import { AgentCarouselMessage } from "../AgentCarouselMessage/AgentCarouselMessa
import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
import { AuthPromptWidget } from "../AuthPromptWidget/AuthPromptWidget";
import { ChatCredentialsSetup } from "../ChatCredentialsSetup/ChatCredentialsSetup";
import { ClarificationQuestionsWidget } from "../ClarificationQuestionsWidget/ClarificationQuestionsWidget";
import { ExecutionStartedMessage } from "../ExecutionStartedMessage/ExecutionStartedMessage";
import { MarkdownContent } from "../MarkdownContent/MarkdownContent";
import { NoResultsMessage } from "../NoResultsMessage/NoResultsMessage";
@@ -69,6 +70,7 @@ export function ChatMessage({
isToolResponse,
isLoginNeeded,
isCredentialsNeeded,
isClarificationNeeded,
} = useChatMessage(message);
const displayContent = getDisplayContent(message, isUser);
@@ -96,6 +98,22 @@ export function ChatMessage({
}
}
const handleClarificationAnswers = useCallback(
function handleClarificationAnswers(answers: Record<string, string>) {
// Format answers as context for the tool to retry
if (onSendMessage) {
const contextMessage = Object.entries(answers)
.map(([keyword, answer]) => `${keyword}: ${answer}`)
.join("\n");
onSendMessage(
`I have the answers to your questions:\n\n${contextMessage}\n\nPlease proceed with creating the agent.`,
);
}
},
[onSendMessage],
);
const handleCopy = useCallback(
async function handleCopy() {
if (message.type !== "message") return;
@@ -141,6 +159,18 @@ export function ChatMessage({
);
}
// Render clarification needed messages
if (isClarificationNeeded && message.type === "clarification_needed") {
return (
<ClarificationQuestionsWidget
questions={message.questions}
message={message.message}
onSubmitAnswers={handleClarificationAnswers}
className={className}
/>
);
}
// Render login needed messages
if (isLoginNeeded && message.type === "login_needed") {
// If user is already logged in, show success message instead of auth prompt

View File

@@ -91,6 +91,18 @@ export type ChatMessageData =
credentialsSchema?: Record<string, any>;
message: string;
timestamp?: string | Date;
}
| {
type: "clarification_needed";
toolName: string;
questions: Array<{
question: string;
keyword: string;
example?: string;
}>;
message: string;
sessionId: string;
timestamp?: string | Date;
};
export function useChatMessage(message: ChatMessageData) {
@@ -111,5 +123,6 @@ export function useChatMessage(message: ChatMessageData) {
isAgentCarousel: message.type === "agent_carousel",
isExecutionStarted: message.type === "execution_started",
isInputsNeeded: message.type === "inputs_needed",
isClarificationNeeded: message.type === "clarification_needed",
};
}

View File

@@ -0,0 +1,150 @@
"use client";
import { Button } from "@/components/atoms/Button/Button";
import { Card } from "@/components/atoms/Card/Card";
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
import { CheckCircleIcon, QuestionIcon } from "@phosphor-icons/react";
import { useState } from "react";
export interface ClarifyingQuestion {
question: string;
keyword: string;
example?: string;
}
interface Props {
questions: ClarifyingQuestion[];
message: string;
onSubmitAnswers: (answers: Record<string, string>) => void;
onCancel?: () => void;
className?: string;
}
export function ClarificationQuestionsWidget({
questions,
message,
onSubmitAnswers,
onCancel,
className,
}: Props) {
const [answers, setAnswers] = useState<Record<string, string>>({});
function handleAnswerChange(keyword: string, value: string) {
setAnswers((prev) => ({ ...prev, [keyword]: value }));
}
function handleSubmit() {
// Check if all questions are answered
const allAnswered = questions.every((q) => answers[q.keyword]?.trim());
if (!allAnswered) {
return;
}
onSubmitAnswers(answers);
}
const allAnswered = questions.every((q) => answers[q.keyword]?.trim());
return (
<div
className={cn(
"group relative flex w-full justify-start gap-3 px-4 py-3",
className,
)}
>
<div className="flex w-full max-w-3xl gap-3">
<div className="flex-shrink-0">
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-indigo-500">
<QuestionIcon className="h-4 w-4 text-indigo-50" weight="bold" />
</div>
</div>
<div className="flex min-w-0 flex-1 flex-col">
<Card className="space-y-4 p-4">
<div>
<Text variant="h4" className="mb-1 text-slate-900">
I need more information
</Text>
<Text variant="small" className="text-slate-600">
{message}
</Text>
</div>
<div className="space-y-3">
{questions.map((q, index) => {
const isAnswered = !!answers[q.keyword]?.trim();
return (
<div
key={`${q.keyword}-${index}`}
className={cn(
"relative rounded-lg border p-3",
isAnswered
? "border-green-500 bg-green-50/50"
: "border-slate-200 bg-white/50",
)}
>
<div className="mb-2 flex items-start gap-2">
{isAnswered ? (
<CheckCircleIcon
size={16}
className="mt-0.5 text-green-500"
weight="bold"
/>
) : (
<div className="mt-0.5 flex h-4 w-4 items-center justify-center rounded-full border border-slate-300 bg-white text-xs text-slate-500">
{index + 1}
</div>
)}
<div className="flex-1">
<Text
variant="small"
className="mb-2 font-semibold text-slate-900"
>
{q.question}
</Text>
{q.example && (
<Text
variant="small"
className="mb-2 italic text-slate-500"
>
Example: {q.example}
</Text>
)}
<textarea
className="w-full rounded-md border border-slate-200 px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
placeholder="Your answer..."
rows={2}
value={answers[q.keyword] || ""}
onChange={(e) =>
handleAnswerChange(q.keyword, e.target.value)
}
/>
</div>
</div>
</div>
);
})}
</div>
<div className="flex gap-2">
<Button
onClick={handleSubmit}
disabled={!allAnswered}
className="flex-1"
variant="primary"
>
Submit Answers
</Button>
{onCancel && (
<Button onClick={onCancel} variant="outline">
Cancel
</Button>
)}
</div>
</Card>
</div>
</div>
</div>
);
}