"use client"; import { AlertTriangleIcon, Building2Icon, Link2Icon, MailIcon, PencilIcon, PhoneIcon, TimerIcon, UserRoundIcon, WorkflowIcon, } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { type CustomerFormSavePayload } from "@/components/customer-form"; import { CustomerFormDialog } from "@/components/customer-form-dialog"; import { CustomerLinkOrCreateDialog } from "@/components/customer-link-or-create-dialog"; import { JsonTreeViewer } from "@/components/json-tree-viewer"; import { ProjectDialog } from "@/components/project-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Field, FieldContent, FieldLabel, } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import type { AgentConversation } from "@/lib/api/agent"; import { fetchAIWorkflowRun, fetchAIWorkflowRuns, type AIWorkflowNodeRun, type AIWorkflowRun, type TagTree, fetchTagsAll, } from "@/lib/api/admin"; import { updateCompany, type AdminCompany } from "@/lib/api/company"; import { fetchTickets, type TicketItem } from "@/lib/api/ticket"; import { fetchCustomer, saveCustomerProfile, type AdminCustomer, } from "@/lib/api/customer"; import { fetchCustomerContacts, type AdminCustomerContact, } from "@/lib/api/customer-contact"; import { ContactType, Gender, } from "@/lib/generated/enums"; import { useAgentConversationsStore } from "@/lib/stores/agent-conversations"; import { cn, formatDateTime } from "@/lib/utils"; import { useI18n } from "@/i18n/provider"; import { ConversationTagBadges, ConversationTagPicker, } from "./conversation-tag-picker"; import { TicketStatusBadge } from "../../tickets/_components/ticket-status-badge"; function contactTypeLabel( contactType: ContactType | string, t: (key: string, values?: Record) => string ) { switch (contactType) { case ContactType.Mobile: return t("conversation.contactMobile"); case ContactType.Email: return t("conversation.contactEmail"); case ContactType.Other: return t("conversation.contactOther"); default: return String(contactType); } } function ContactTypeIcon({ contactType }: { contactType: ContactType | string }) { const cls = "size-3.5 shrink-0 text-muted-foreground"; switch (contactType) { case ContactType.Mobile: return ; case ContactType.Email: return ; default: return ; } } function DetailRow({ label, value, valueClassName, }: { label: string; value: string; valueClassName?: string; }) { const empty = !value.trim(); return (
{label} {empty ? "—" : value}
); } function SectionHeading({ children, action, }: { children: React.ReactNode; action?: React.ReactNode; }) { return (

{children}

{action}
); } function UnlinkedCustomerEmpty({ conversation }: { conversation: AgentConversation }) { const t = useI18n(); const [linkDialogOpen, setLinkDialogOpen] = useState(false); const loadConversations = useAgentConversationsStore((s) => s.loadConversations); return (

{t("conversation.unlinkedCustomerTitle")}

{t("conversation.unlinkedCustomerDescription")}

void loadConversations()} />
); } function MissingCustomerEmpty({ conversation }: { conversation: AgentConversation }) { const t = useI18n(); const [linkDialogOpen, setLinkDialogOpen] = useState(false); const loadConversations = useAgentConversationsStore((s) => s.loadConversations); return (

{t("conversation.missingCustomerTitle")}

{t("conversation.missingCustomerDescription")}

{t("conversation.conversationOwner")}
void loadConversations()} />
); } type ConversationInfoPanelProps = { conversation: AgentConversation | null; className?: string; variant?: "default" | "embedded"; }; export function ConversationInfoPanel({ conversation, className, variant = "default", }: ConversationInfoPanelProps) { const t = useI18n(); const embedded = variant === "embedded"; return (

{t("conversation.conversationInfo")}

{!conversation ? (

{embedded ? t("conversation.selectConversationForInfo") : t("conversation.selectSidebarConversationForInfo")}

) : (
)}
); } function ConversationTagSection({ conversation, }: { conversation: AgentConversation; }) { const t = useI18n(); const setConversationTags = useAgentConversationsStore( (state) => state.setConversationTags, ); const [availableTags, setAvailableTags] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { let cancelled = false; async function loadTags() { setLoading(true); try { const data = await fetchTagsAll(); if (!cancelled) { setAvailableTags(Array.isArray(data) ? data : []); } } catch (error) { if (!cancelled) { toast.error(error instanceof Error ? error.message : t("conversation.loadTagsFailed")); } } finally { if (!cancelled) { setLoading(false); } } } void loadTags(); return () => { cancelled = true; }; }, [t]); return (
{ setConversationTags(conversation.id, tags); }} /> } > {t("conversation.conversationTags")} {!conversation.tags || conversation.tags.length === 0 ? (

{t("conversation.noConversationTags")}

) : null}
); } function WorkflowRunsSection({ conversation }: { conversation: AgentConversation }) { const [runs, setRuns] = useState([]); const [loading, setLoading] = useState(false); const [detailLoading, setDetailLoading] = useState(false); const [activeRun, setActiveRun] = useState(null); const [detailOpen, setDetailOpen] = useState(false); useEffect(() => { let cancelled = false; async function loadRuns() { setLoading(true); try { const data = await fetchAIWorkflowRuns({ conversationId: conversation.id, page: 1, limit: 5, }); if (!cancelled) { setRuns(Array.isArray(data.results) ? data.results : []); } } catch (error) { if (!cancelled) { toast.error(error instanceof Error ? error.message : "加载 AI 执行记录失败"); } } finally { if (!cancelled) { setLoading(false); } } } void loadRuns(); return () => { cancelled = true; }; }, [conversation.id]); async function openDetail(runId: number) { setDetailOpen(true); setDetailLoading(true); try { const data = await fetchAIWorkflowRun(runId); setActiveRun(data); } catch (error) { toast.error(error instanceof Error ? error.message : "加载 AI 执行详情失败"); setDetailOpen(false); } finally { setDetailLoading(false); } } return (
AI 执行记录 {loading ? (

加载执行记录中

) : runs.length > 0 ? (
{runs.map((run) => ( ))}
) : (

暂无 AI 执行记录

)} { setDetailOpen(open); if (!open) { setActiveRun(null); } }} />
); } function WorkflowRunDetailDialog({ open, loading, run, onOpenChange, }: { open: boolean; loading: boolean; run: AIWorkflowRun | null; onOpenChange: (open: boolean) => void; }) { return ( AI 执行详情 } description={run ? `Run #${run.id}` : "Workflow 执行链路"} size="xl" allowFullscreen footer={ } > {loading ? (
加载执行详情中
) : run ? (
{run.errorMessage ? (
{run.errorMessage}
) : null}
{(run.nodes ?? []).map((node) => ( ))} {!run.nodes || run.nodes.length === 0 ? (

暂无节点记录

) : null}
) : (
未找到执行记录
)}
); } function WorkflowNodeRunBlock({ node }: { node: AIWorkflowNodeRun }) { const inputValue = safeParseJSON(node.inputPreview); const outputValue = safeParseJSON(node.outputPreview); return (
{node.nodeId || `Node #${node.id}`}
{node.nodeType || "unknown"}
{node.durationMs} ms
{node.errorMessage ? (
{node.errorMessage}
) : null}
); } function PreviewBlock({ title, raw, value, }: { title: string; raw: string; value: unknown; }) { return (
{title}
{value !== null ? ( ) : raw.trim() ? (
          {raw}
        
) : (
)}
); } function WorkflowRunStatusBadge({ statusName }: { statusName: string }) { const normalized = statusName.trim(); const variant = normalized === "failed" ? "destructive" : normalized === "interrupted" ? "outline" : "secondary"; return ( {normalized || "unknown"} ); } function safeParseJSON(raw: string): unknown | null { const trimmed = raw.trim(); if (!trimmed) { return null; } try { return JSON.parse(trimmed); } catch { return null; } } function CustomerBody({ conversation }: { conversation: AgentConversation }) { const customerId = conversation.customerId ?? 0; if (customerId <= 0) { return (
); } return ; } type CustomerLinkedBodyProps = { conversation: AgentConversation; customerId: number; }; function CustomerLinkedBody({ conversation, customerId }: CustomerLinkedBodyProps) { const t = useI18n(); const [loading, setLoading] = useState(true); const [customer, setCustomer] = useState(null); const [contacts, setContacts] = useState([]); const [customerEditOpen, setCustomerEditOpen] = useState(false); const [customerEditSaving, setCustomerEditSaving] = useState(false); const [companyEditOpen, setCompanyEditOpen] = useState(false); const load = useCallback(async () => { setLoading(true); try { const c = await fetchCustomer(customerId); setCustomer(c); if (!c) { setContacts([]); return; } const list = await fetchCustomerContacts(customerId); setContacts(Array.isArray(list) ? list : []); } catch (e) { const msg = e instanceof Error ? e.message : t("conversation.loadCustomerFailed"); toast.error(msg); setCustomer(null); setContacts([]); } finally { setLoading(false); } }, [customerId, t]); useEffect(() => { void load(); }, [load]); const isProfileEmpty = customer && !customer.name.trim() && !customer.primaryMobile.trim() && !customer.primaryEmail.trim() && customer.companyId === 0 && !customer.remark.trim(); if (loading && !customer) { return (

{t("conversation.loadingCustomer")}

); } if (!customer) { return (
); } const displayName = customer.name.trim() || t("conversation.unnamedCustomer"); const company = customer.company ?? null; const genderLabel = customer.gender === Gender.Male ? t("conversation.genderMale") : customer.gender === Gender.Female ? t("conversation.genderFemale") : null; return (
{isProfileEmpty ? (
{t("conversation.customerProfileEmpty")}
) : null}

{displayName} {genderLabel ? ( {" "} · {genderLabel} ) : null}

{contacts.length === 0 ? (

{t("conversation.noContacts")}

) : (
    {contacts.map((row) => { const tags: string[] = []; if (row.isPrimary) { tags.push(t("conversation.primaryContactBadge")); } if (row.isVerified) { tags.push(t("conversation.verifiedContactBadge")); } return (
  • {row.contactValue} {contactTypeLabel(row.contactType, t)} {tags.length > 0 ? ( {tags.join(" · ")} ) : null}

    {row.remark ? (

    {row.remark}

    ) : null}
  • ); })}
)}
{customer.companyId > 0 ? (
{company ? (

{company.name}

{company.code ? (

{company.code}

) : null}
) : (

{t("conversation.companyUnavailable")}

)}
) : null} { if (customerEditSaving) { return; } setCustomerEditSaving(true); try { await saveCustomerProfile({ ...payload, id: customer.id }); toast.success(t("conversation.saved")); void load(); setCustomerEditOpen(false); } catch (e) { toast.error(e instanceof Error ? e.message : t("conversation.saveFailed")); } finally { setCustomerEditSaving(false); } }} /> {company ? ( { void load(); }} /> ) : null}
); } function RelatedTicketsSection({ conversation }: { conversation: AgentConversation }) { const t = useI18n(); const [tickets, setTickets] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { let cancelled = false; async function loadTickets() { setLoading(true); try { const data = await fetchTickets({ conversationId: conversation.id, page: 1, limit: 5, }); if (!cancelled) { setTickets(Array.isArray(data.results) ? data.results : []); } } catch (error) { if (!cancelled) { toast.error(error instanceof Error ? error.message : t("conversation.loadTicketsFailed")); } } finally { if (!cancelled) { setLoading(false); } } } void loadTickets(); return () => { cancelled = true; }; }, [conversation.id, t]); return (
{t("conversation.relatedTickets")} {loading ? (

{t("conversation.loadingTickets")}

) : tickets.length > 0 ? (
{tickets.map((ticket) => (
{ticket.title}
{ticket.ticketNo}
{ticket.updatedAt ? formatDateTime(ticket.updatedAt) : "—"}
))}
) : (

{t("conversation.noRelatedTickets")}

)}
); } type CompanyEditDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; company: AdminCompany; onSaved: () => void; }; function CompanyEditDialog({ open, onOpenChange, company, onSaved, }: CompanyEditDialogProps) { const t = useI18n(); const [name, setName] = useState(""); const [code, setCode] = useState(""); const [remark, setRemark] = useState(""); const [saving, setSaving] = useState(false); useEffect(() => { if (!open) { return; } setName(company.name); setCode(company.code); setRemark(company.remark); }, [open, company]); const handleSubmit = async () => { const trimmedName = name.trim(); if (!trimmedName) { toast.error(t("conversation.companyNameRequired")); return; } setSaving(true); try { await updateCompany({ id: company.id, name: trimmedName, code: code.trim(), remark: remark.trim(), }); toast.success(t("conversation.saved")); onSaved(); onOpenChange(false); } catch (e) { toast.error(e instanceof Error ? e.message : t("conversation.saveFailed")); } finally { setSaving(false); } }; return ( {t("conversation.editCompany")}
{t("conversation.companyName")} setName(e.target.value)} /> {t("conversation.companyCode")} setCode(e.target.value)} /> {t("conversation.remark")}