"use client"; import { useRef, useState, useEffect } from "react"; 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, useSubmitInteraction, } from "@/lib/api/agents"; import type { Interaction, InteractionPayload, InteractionQuestion, InteractionTask, PaperclipComment, } from "@/lib/api/agents"; import { toast } from "sonner"; import { Bot, User, Send, Loader2, MessageSquare, Clock, CheckCircle2, XCircle, HelpCircle, } from "lucide-react"; /* ── Role → color mapping ────────────────────────────────────── */ const ROLE_COLORS: Record = { ceo: "bg-blue-100 text-blue-800 border-blue-200", researcher: "bg-purple-100 text-purple-800 border-purple-200", engineer: "bg-emerald-100 text-emerald-800 border-emerald-200", qa: "bg-amber-100 text-amber-800 border-amber-200", }; const ROLE_DOT: Record = { ceo: "bg-blue-500", researcher: "bg-purple-500", engineer: "bg-emerald-500", qa: "bg-amber-500", }; const ROLE_LABELS: Record = { ceo: "מנהל", researcher: "חוקר", engineer: "מהנדס", qa: "בודק איכות", general: "כללי", }; const ISSUE_STATUS_LABELS: Record = { backlog: "ממתין", todo: "לביצוע", in_progress: "בביצוע", in_review: "בבדיקה", done: "הושלם", cancelled: "בוטל", blocked: "חסום", }; function roleColor(role: string | null) { return ROLE_COLORS[role ?? ""] ?? "bg-gray-100 text-gray-700 border-gray-200"; } function roleDot(role: string | null) { return ROLE_DOT[role ?? ""] ?? "bg-gray-400"; } function roleLabel(role: string | null) { return ROLE_LABELS[role ?? ""] ?? role ?? ""; } function issueStatusLabel(status: string) { return ISSUE_STATUS_LABELS[status] ?? status; } /* ── Time formatting ─────────────────────────────────────────── */ function timeAgo(iso: string | null): string { if (!iso) return ""; const diff = Date.now() - new Date(iso).getTime(); const mins = Math.floor(diff / 60_000); if (mins < 1) return "עכשיו"; if (mins < 60) return `לפני ${mins} דק׳`; const hours = Math.floor(mins / 60); if (hours < 24) return `לפני ${hours} שע׳`; const days = Math.floor(hours / 24); return `לפני ${days} ימים`; } /* ── Issue identifier → find matching identifier ─────────────── */ function issueIdentifier( comment: PaperclipComment, issueMap: Map, ): string { return issueMap.get(comment.issue_id) ?? ""; } /* ── Comment card ────────────────────────────────────────────── */ function CommentCard({ comment, issueMap, }: { comment: PaperclipComment; issueMap: Map; }) { const isAgent = !!comment.author_agent_id; const label = isAgent ? comment.agent_name ?? "סוכן" : "חיים"; const identifier = issueIdentifier(comment, issueMap); return (
{/* Avatar */}
{isAgent ? (
) : (
)}
{/* Content */}
{/* Header */}
{label} {isAgent && comment.agent_role && ( {roleLabel(comment.agent_role)} )} {identifier && ( {identifier} )} {timeAgo(comment.created_at)}
{/* Body */}
); } /* ── 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 && (