import { Check } from "lucide-react"; import { useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; import type { ClarificationPayload } from "@/roundtable-planning/api/multi-agent"; import { resolveAllowMultiple } from "@/roundtable-planning/lib/clarification"; export type IntentClarificationSelection = { selectedIds: string[]; custom: string; }; /** * Composite selection-map key for one clarification question. When the intent * agent asks several questions in one turn, each needs its own selection state, * so we key by `${msgId}::${clarificationId}` (falling back to the arrival * index when the payload has no id). The map itself lives in `useStep1Intent`. */ export function clarificationKey( msgId: number, clarId: string | undefined, index: number, ): string { return `${msgId}::${clarId ?? `idx-${index}`}`; } type ClarificationOption = { id: string; label: string; description?: string; }; function normalizeOptions(raw: unknown[]): ClarificationOption[] { return raw .map((opt, index) => { if (typeof opt === "string") { const label = opt.trim(); return label ? { id: `option-${index}`, label } : null; } if (opt && typeof opt === "object" && "label" in opt) { const obj = opt as { id?: unknown; label?: unknown; description?: unknown }; const label = String(obj.label ?? "").trim(); if (!label) return null; const id = String(obj.id ?? label).trim() || `option-${index}`; const description = typeof obj.description === "string" && obj.description.trim() ? obj.description.trim() : undefined; return { id, label, description }; } return null; }) .filter((opt): opt is ClarificationOption => opt !== null); } export function formatIntentClarificationAnswer( payload: ClarificationPayload, selection: IntentClarificationSelection, ): string | null { const options = normalizeOptions(Array.isArray(payload.options) ? payload.options : []); const selectedLabels = selection.selectedIds .map((sid) => options.find((option) => option.id === sid)?.label) .filter((label): label is string => Boolean(label && label.trim())); const trimmedCustom = selection.custom.trim(); if (selectedLabels.length > 0 && trimmedCustom) { return `我选择:${selectedLabels.join("、")};补充:${trimmedCustom}`; } if (selectedLabels.length > 0) { return `我选择:${selectedLabels.join("、")}`; } if (trimmedCustom) { return trimmedCustom; } return null; } interface IntentClarificationCardProps { payload: ClarificationPayload; fallbackContent?: string; selection: IntentClarificationSelection; onSelectionChange: (next: IntentClarificationSelection) => void; disabled?: boolean; submitting?: boolean; /** Multi-select: submit current selection. Single-select: also used for custom text. */ onSubmit: (overrideSelection?: IntentClarificationSelection) => void; /** * Defer submission: even single-select option clicks only *record* the * selection (via onSelectionChange) instead of sending immediately, and the * card hides its own submit button. Used when several questions are answered * together and the parent owns one combined submit button. */ deferSubmit?: boolean; } export function IntentClarificationCard({ payload, fallbackContent, selection, onSelectionChange, disabled = false, submitting = false, onSubmit, deferSubmit = false, }: IntentClarificationCardProps) { const options = useMemo( () => normalizeOptions(Array.isArray(payload.options) ? payload.options : []), [payload.options], ); const allowMultiple = resolveAllowMultiple( payload.allowMultiple, payload.clarificationType, ); const canSubmit = useMemo(() => { return selection.selectedIds.length > 0 || selection.custom.trim().length > 0; }, [selection.custom, selection.selectedIds]); // In defer mode the parent owns the (combined) submit button. const showSubmitButton = !deferSubmit && (allowMultiple || payload.allowCustom !== false); function handleOptionClick(optionId: string) { if (disabled || submitting) return; if (!allowMultiple) { if (deferSubmit) { // Single-select but combined submit: just record the choice (replace), // keep any custom text the user already typed. onSelectionChange({ ...selection, selectedIds: [optionId] }); return; } onSubmit({ selectedIds: [optionId], custom: "" }); return; } const next = selection.selectedIds.includes(optionId) ? selection.selectedIds.filter((id) => id !== optionId) : [...selection.selectedIds, optionId]; onSelectionChange({ ...selection, selectedIds: next }); } function updateCustom(value: string) { if (disabled || submitting) return; onSelectionChange({ ...selection, custom: value }); } return (
{payload.context}
) : null}