diff --git a/web-ui/src/components/cases/agent-activity-feed.tsx b/web-ui/src/components/cases/agent-activity-feed.tsx index 3726952..394f2bc 100644 --- a/web-ui/src/components/cases/agent-activity-feed.tsx +++ b/web-ui/src/components/cases/agent-activity-feed.tsx @@ -5,8 +5,18 @@ import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Badge } from "@/components/ui/badge"; import { Markdown } from "@/components/ui/markdown"; -import { useAgentActivity, useSendComment } from "@/lib/api/agents"; -import type { PaperclipComment } from "@/lib/api/agents"; +import { + useAgentActivity, + useSendComment, + useSubmitInteraction, +} from "@/lib/api/agents"; +import type { + Interaction, + InteractionPayload, + InteractionQuestion, + InteractionTask, + PaperclipComment, +} from "@/lib/api/agents"; import { toast } from "sonner"; import { Bot, @@ -15,6 +25,9 @@ import { Loader2, MessageSquare, Clock, + CheckCircle2, + XCircle, + HelpCircle, } from "lucide-react"; /* ── Role → color mapping ────────────────────────────────────── */ @@ -153,6 +166,463 @@ function CommentCard({ ); } +/* ── Interaction card ────────────────────────────────────────── */ + +const RESOLVED_LABELS: Record = { + answered: { text: "נענה", tone: "text-emerald-700 bg-emerald-50 border-emerald-200", Icon: CheckCircle2 }, + accepted: { text: "התקבל", tone: "text-emerald-700 bg-emerald-50 border-emerald-200", Icon: CheckCircle2 }, + rejected: { text: "נדחה", tone: "text-rose-700 bg-rose-50 border-rose-200", Icon: XCircle }, + expired: { text: "פג תוקף", tone: "text-ink-faint bg-gray-50 border-gray-200", Icon: XCircle }, + failed: { text: "כשל", tone: "text-rose-700 bg-rose-50 border-rose-200", Icon: XCircle }, +}; + +function ResolvedBadge({ status }: { status: string }) { + const meta = RESOLVED_LABELS[status]; + if (!meta) return null; + const { text, tone, Icon } = meta; + return ( + + + {text} + + ); +} + +function summaryAnswer(interaction: Interaction): string | null { + const result = interaction.result; + if (!result) return null; + if (typeof result.summaryMarkdown === "string" && result.summaryMarkdown.trim()) { + return result.summaryMarkdown; + } + if (interaction.kind === "ask_user_questions" && Array.isArray(result.answers)) { + const optionLabel = (qid: string, oid: string): string => { + const q = interaction.payload.questions?.find((qq) => qq.id === qid); + return q?.options.find((o) => o.id === oid)?.label ?? oid; + }; + return (result.answers as Array<{ questionId: string; optionIds: string[] }>) + .map((a) => + `**${interaction.payload.questions?.find((q) => q.id === a.questionId)?.prompt ?? a.questionId}** — ${a.optionIds + .map((oid) => optionLabel(a.questionId, oid)) + .join(", ")}`, + ) + .join("\n\n"); + } + if (interaction.kind === "request_confirmation" && typeof result.reason === "string" && result.reason) { + return `נימוק: ${result.reason}`; + } + if (interaction.kind === "suggest_tasks") { + const created = Array.isArray(result.createdTasks) ? result.createdTasks.length : 0; + const skipped = Array.isArray(result.skippedClientKeys) ? result.skippedClientKeys.length : 0; + if (created || skipped) { + const parts: string[] = []; + if (created) parts.push(`נוצרו ${created} משימות`); + if (skipped) parts.push(`דילוג על ${skipped}`); + return parts.join(" · "); + } + } + return null; +} + +function AskUserQuestionsForm({ + interaction, + onSubmit, + pending, +}: { + interaction: Interaction; + onSubmit: (answers: Array<{ questionId: string; optionIds: string[] }>) => void; + pending: boolean; +}) { + const questions: InteractionQuestion[] = interaction.payload.questions ?? []; + const [selections, setSelections] = useState>({}); + + const setSingle = (qid: string, oid: string) => + setSelections((prev) => ({ ...prev, [qid]: [oid] })); + + const toggleMulti = (qid: string, oid: string) => + setSelections((prev) => { + const cur = prev[qid] ?? []; + return { + ...prev, + [qid]: cur.includes(oid) ? cur.filter((x) => x !== oid) : [...cur, oid], + }; + }); + + const missingRequired = questions.some( + (q) => (q.required ?? true) && !(selections[q.id]?.length), + ); + + const handleSend = () => { + const answers = questions + .map((q) => ({ questionId: q.id, optionIds: selections[q.id] ?? [] })) + .filter((a) => a.optionIds.length > 0); + onSubmit(answers); + }; + + return ( +
+ {questions.map((q) => { + const isSingle = (q.selectionMode ?? "single") === "single"; + const chosen = selections[q.id] ?? []; + return ( +
+ + {q.prompt} + {(q.required ?? true) && *} + +
+ {q.options.map((opt) => { + const checked = chosen.includes(opt.id); + return ( + + ); + })} +
+
+ ); + })} +
+ +
+
+ ); +} + +function RequestConfirmationForm({ + interaction, + onAccept, + onReject, + pending, +}: { + interaction: Interaction; + onAccept: () => void; + onReject: (reason: string) => void; + pending: boolean; +}) { + const payload = interaction.payload; + const allowReason = payload.allowDeclineReason !== false; + const requireReason = payload.rejectRequiresReason === true; + const [showReason, setShowReason] = useState(requireReason); + const [reason, setReason] = useState(""); + + const acceptLabel = (payload.acceptLabel as string) || "אישור"; + const rejectLabel = (payload.rejectLabel as string) || "דחייה"; + const reasonLabel = + (payload.rejectReasonLabel as string) || "נימוק (לא חובה)"; + const reasonPlaceholder = + (payload.declineReasonPlaceholder as string) || "סיבת הדחייה..."; + + const handleReject = () => { + if (requireReason && !reason.trim()) { + setShowReason(true); + return; + } + onReject(reason.trim()); + }; + + return ( +
+ {typeof payload.prompt === "string" && ( +
{payload.prompt}
+ )} + {typeof payload.detailsMarkdown === "string" && payload.detailsMarkdown && ( +
+ +
+ )} + {showReason && allowReason && ( +
+ +