多智能体协作
This commit is contained in:
1 parent
9a70329db2
commit
525218f9a7
13 files changed
+753
-180
No files matched your search
@@ -0,0 +1,217 @@
|
||||
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;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function IntentClarificationCard({
|
||||
payload,
|
||||
fallbackContent,
|
||||
selection,
|
||||
onSelectionChange,
|
||||
disabled = false,
|
||||
submitting = false,
|
||||
onSubmit,
|
||||
}: 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]);
|
||||
|
||||
const showSubmitButton = allowMultiple || payload.allowCustom !== false;
|
||||
|
||||
function handleOptionClick(optionId: string) {
|
||||
if (disabled || submitting) return;
|
||||
if (!allowMultiple) {
|
||||
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 (
|
||||
<div className="mt-2 rounded-xl border border-red-100 bg-white p-3 shadow-sm">
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wide text-primary">
|
||||
需要你的确认
|
||||
</span>
|
||||
{payload.clarificationType ? (
|
||||
<span className="rounded-full bg-[var(--brand-200)]/60 px-2 py-0.5 text-[9px] font-mono font-bold text-[var(--brand-950)]">
|
||||
{payload.clarificationType}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{payload.context ? (
|
||||
<p className="text-[11px] leading-relaxed text-slate-500 italic">{payload.context}</p>
|
||||
) : null}
|
||||
<div className="text-xs font-medium leading-relaxed text-slate-800">
|
||||
{payload.question || fallbackContent}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{options.length > 0 ? (
|
||||
<div className="mt-3 grid gap-2">
|
||||
<div className="text-[10px] text-slate-500">
|
||||
{allowMultiple ? "可多选(再次点击取消选择)" : "请选择一项(点击即发送)"}
|
||||
</div>
|
||||
{options.map((option) => {
|
||||
const isSelected = selection.selectedIds.includes(option.id);
|
||||
return (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
disabled={disabled || submitting}
|
||||
onClick={() => handleOptionClick(option.id)}
|
||||
className={cn(
|
||||
"flex w-full items-start justify-between gap-2 rounded-xl border px-3 py-2 text-left transition",
|
||||
isSelected && allowMultiple
|
||||
? "border-primary bg-primary text-white shadow-sm"
|
||||
: "border-slate-200 bg-slate-50/80 text-slate-800 hover:border-primary/40 hover:bg-white",
|
||||
!allowMultiple && !disabled && !submitting && "active:scale-[0.99]",
|
||||
(disabled || submitting) && "cursor-not-allowed opacity-60",
|
||||
)}
|
||||
>
|
||||
<span className="flex min-w-0 flex-1 flex-col items-start gap-1">
|
||||
<span className="text-[11px] font-bold">{option.label}</span>
|
||||
{option.description ? (
|
||||
<span
|
||||
className={cn(
|
||||
"text-[10px] leading-snug",
|
||||
isSelected && allowMultiple ? "text-white/80" : "text-slate-500",
|
||||
)}
|
||||
>
|
||||
{option.description}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{isSelected && allowMultiple ? (
|
||||
<Check className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{payload.allowCustom !== false ? (
|
||||
<div className="mt-3 flex flex-col gap-2">
|
||||
<Textarea
|
||||
value={selection.custom}
|
||||
onChange={(event) => updateCustom(event.currentTarget.value)}
|
||||
placeholder={
|
||||
options.length > 0
|
||||
? allowMultiple
|
||||
? "可选:补充说明(与所选项一起发送),或在此填写自定义回答"
|
||||
: "如果以上选项都不合适,可以在这里输入自定义回答"
|
||||
: "如果以上选项都不合适,可以在这里输入你的回答"
|
||||
}
|
||||
disabled={disabled || submitting}
|
||||
className="min-h-16 resize-none border-slate-200 text-xs"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showSubmitButton ? (
|
||||
<div className="mt-3 flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={disabled || submitting || !canSubmit}
|
||||
onClick={() => onSubmit()}
|
||||
className="h-8 rounded-xl px-4 text-xs font-bold"
|
||||
>
|
||||
{submitting ? "发送中…" : "发送回答"}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user