"use client"; import { CheckCheckIcon, EyeIcon, MessageCircleMoreIcon } from "lucide-react"; import Image from "next/image"; import { useEffect } from "react"; import { ConversationMessageBubble } from "@/components/chat/conversation-message-bubble"; import { ConversationMessageRow, } from "@/components/chat/conversation-message-row"; import { ConversationMessageScroller, ConversationMessageScrollerItem, } from "@/components/chat/conversation-message-scroller"; import { ImMessageHTML } from "@/components/im-message-html"; import { useImageLightbox } from "@/components/image-lightbox"; import { ProjectDialog } from "@/components/project-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { type AdminConversation, type AdminConversationDetail, type AdminMessage, } from "@/lib/api/admin"; import { parseMessageAssetPayload, renderIMMessageHTML, } from "@/lib/im-message"; import { formatDateTime } from "@/lib/utils"; import { useI18n } from "@/i18n/provider"; type ConversationDetailDialogProps = { open: boolean; loading: boolean; saving: boolean; readOnly?: boolean; item: AdminConversation | null; detail: AdminConversationDetail | null; messages?: AdminMessage[] | null; /** Whether older messages are available through cursor pagination. */ messagesHasMore?: boolean; loadingMoreMessages?: boolean; onLoadMoreMessages?: () => void | Promise; onOpenChange: (open: boolean) => void; onOpenAssign: () => void; onDispatch: () => Promise; onOpenTransfer: () => void; onRead: () => Promise; onOpenClose: () => void; }; function getStatusMeta( status: number, t: (key: string, values?: Record) => string ) { switch (status) { case 1: return { label: t("conversation.filterAiServing"), variant: "secondary" as const }; case 2: return { label: t("conversation.filterPending"), variant: "outline" as const }; case 3: return { label: t("conversation.filterActive"), variant: "secondary" as const }; case 4: return { label: t("conversation.filterClosed"), variant: "outline" as const }; default: return { label: t("conversationMonitor.unknown"), variant: "outline" as const }; } } function getServiceModeLabel( mode: number, t: (key: string, values?: Record) => string ) { switch (mode) { case 1: return t("conversationMonitor.serviceAi"); case 2: return t("conversationMonitor.serviceHuman"); case 3: return t("conversationMonitor.serviceAiFirst"); default: return t("conversationMonitor.serviceUndefined"); } } function getSenderLabel( message: AdminMessage, t: (key: string, values?: Record) => string ) { switch (message.senderType) { case "agent": return message.senderName || t("conversationMonitor.senderAgent"); case "customer": return message.senderName || t("conversationMonitor.senderCustomer"); case "ai": return "AI"; case "system": return t("conversationMonitor.senderSystem"); default: return message.senderType; } } function getMessageContent(message: AdminMessage) { return message.content || message.payload || "-"; } function getImageMessageUrl(message: AdminMessage) { return parseMessageAssetPayload(message.payload)?.url || ""; } function getParticipantIdentity( participant: NonNullable[number], ) { return participant.participantId || participant.externalParticipantId || "-"; } function getMessageAlign(message: AdminMessage): "start" | "end" { return message.senderType === "customer" || message.senderType === "system" ? "start" : "end"; } function getMessageVariant(message: AdminMessage) { switch (message.senderType) { case "agent": return "agent" as const; case "ai": return "ai" as const; case "system": return "system" as const; default: return "customer" as const; } } function getMessageHtmlClassName(message: AdminMessage, isRecalled: boolean) { if (isRecalled) { return message.senderType === "agent" || message.senderType === "ai" ? "text-emerald-800 [&_p]:text-emerald-800" : "text-muted-foreground [&_p]:text-muted-foreground"; } if (message.senderType === "agent") { return "[&_p]:text-white [&_a]:text-white [&_a]:underline [&_img]:rounded-md"; } return "[&_a]:text-foreground [&_a]:underline [&_img]:rounded-md"; } function getRecalledBubbleClassName(message: AdminMessage) { return message.senderType === "agent" || message.senderType === "ai" ? "border-dashed border-emerald-200 bg-emerald-50 text-emerald-800" : "border-dashed border-border/70 bg-muted/40 text-muted-foreground"; } export function ConversationDetailDialog({ open, loading, saving, readOnly = false, item, detail, messages: rawMessages, messagesHasMore = false, loadingMoreMessages = false, onLoadMoreMessages, onOpenChange, onOpenAssign, onDispatch, onOpenTransfer, onRead, onOpenClose, }: ConversationDetailDialogProps) { const t = useI18n(); const messages = Array.isArray(rawMessages) ? rawMessages : []; const currentConversation = detail ?? item; const isClosedConversation = currentConversation?.status === 4; const isPendingConversation = currentConversation?.status === 2; const statusMeta = currentConversation ? getStatusMeta(currentConversation.status, t) : null; const { open: openImageLightbox, close: closeImageLightbox } = useImageLightbox(); useEffect(() => { if (!open) { closeImageLightbox(); } }, [open, closeImageLightbox]); return ( {currentConversation?.customerName || t("conversationMonitor.detailFallbackTitle")}
{statusMeta ? ( {statusMeta.label} ) : null}
} size="xl" // allowFullscreen defaultFullscreen bodyScrollable={false} bodyClassName="flex min-h-0 flex-1 flex-col overflow-hidden p-0" contentClassName="h-[calc(100vh-40px)] max-h-[calc(100vh-40px)]" footer={
{currentConversation ? t("conversationMonitor.lastActivePrefix", { time: formatDateTime(currentConversation.lastMessageAt), }) : t("conversationMonitor.noConversationInfo")}
{readOnly ? ( ) : (
{!isClosedConversation ? ( ) : null}
)}
} > {loading ? (
{t("conversationMonitor.loadingDetail")}
) : currentConversation ? (
{loadingMoreMessages ? t("conversationMonitor.loadingOlder") : t("conversationMonitor.loadOlderHint")}
) : null } > {messages.length ? ( messages.map((message) => ( )) ) : (
{t("conversationMonitor.emptyMessages")}
)} ) : (
{t("conversationMonitor.emptyDetail")}
)}
); } type ConversationMonitorMessageProps = { message: AdminMessage; onImagePreview: (src: string, alt?: string) => void; }; function ConversationMonitorMessage({ message, onImagePreview, }: ConversationMonitorMessageProps) { const t = useI18n(); const isRecalled = Boolean(message.recalledAt) || message.sendStatus === 6; const isHtmlMessage = !isRecalled && (message.messageType === "html" || message.messageType === "attachment"); const isImageMessage = !isRecalled && message.messageType === "image"; const isSystem = message.senderType === "system"; const align = getMessageAlign(message); const variant = isRecalled ? "recalled" : getMessageVariant(message); const htmlClassName = getMessageHtmlClassName(message, isRecalled); return ( {getSenderLabel(message, t)} · {formatDateTime(message.sentAt)} {isRecalled ? ( <> · {t("conversationMonitor.messageRecalled")} ) : null} } footer={t("conversationMonitor.readStatus", { agent: message.agentRead ? t("conversationMonitor.read") : t("conversationMonitor.unread"), customer: message.customerRead ? t("conversationMonitor.read") : t("conversationMonitor.unread"), })} > {isRecalled ? (
{t("conversationMonitor.messageRecalledBody")}
) : isHtmlMessage ? ( ) : isImageMessage ? ( ) : (
{getMessageContent(message)}
)}
); } type InfoItemProps = { label: string; value: string; fullWidth?: boolean; }; function InfoItem({ label, value, fullWidth = false }: InfoItemProps) { return (

{label}

{value || "-"}

); } type MessageImageProps = { src: string; alt: string; onPreview: (src: string, alt?: string) => void; }; function MessageImage({ src, alt, onPreview }: MessageImageProps) { const t = useI18n(); if (!src) { return (
{alt || t("conversationMonitor.imageFallback")}
); } return ( ); }