"use client"; import { CheckCheckIcon, EyeIcon, MessageCircleMoreIcon } from "lucide-react"; import Image from "next/image"; import { useCallback, useEffect, useLayoutEffect, useRef, } from "react"; 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"; type ConversationDetailDialogProps = { open: boolean; loading: boolean; saving: boolean; item: AdminConversation | null; detail: AdminConversationDetail | null; messages?: AdminMessage[] | null; /** 是否还有更早消息(cursor 分页) */ 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) { switch (status) { case 1: return { label: "AI接待中", variant: "secondary" as const }; case 2: return { label: "待接入", variant: "outline" as const }; case 3: return { label: "处理中", variant: "secondary" as const }; case 4: return { label: "已关闭", variant: "outline" as const }; default: return { label: "未知", variant: "outline" as const }; } } function getServiceModeLabel(mode: number) { switch (mode) { case 1: return "AI 接待"; case 2: return "人工接待"; case 3: return "AI 优先"; default: return "未定义"; } } function getSenderLabel(message: AdminMessage) { switch (message.senderType) { case "agent": return message.senderName || "客服"; case "customer": return message.senderName || "用户"; case "ai": return "AI"; case "system": return "系统"; 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 getMessageLayout(message: AdminMessage) { if (message.senderType === "customer") { return { rowClassName: "justify-start", bubbleClassName: "bg-muted text-foreground border-border", metaClassName: "text-left", }; } if (message.senderType === "system") { return { rowClassName: "justify-center", bubbleClassName: "bg-muted/60 text-muted-foreground border-dashed border-border", metaClassName: "text-center", }; } if (message.senderType === "ai") { return { rowClassName: "justify-end", bubbleClassName: "bg-primary/10 text-foreground border-primary/20", metaClassName: "text-right", }; } return { rowClassName: "justify-end", bubbleClassName: "bg-primary text-primary-foreground border-primary", metaClassName: "text-right", }; } export function ConversationDetailDialog({ open, loading, saving, item, detail, messages: rawMessages, messagesHasMore = false, loadingMoreMessages = false, onLoadMoreMessages, onOpenChange, onOpenAssign, onDispatch, onOpenTransfer, onRead, onOpenClose, }: ConversationDetailDialogProps) { 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) : null; const messageBottomRef = useRef(null); const messagesScrollRootRef = useRef(null); const loadMoreSentinelRef = useRef(null); const pendingScrollAnchorRef = useRef<{ scrollHeight: number; scrollTop: number; } | null>(null); const prevLoadingMoreRef = useRef(false); const { open: openImageLightbox, close: closeImageLightbox } = useImageLightbox(); const getMessagesViewport = useCallback((): HTMLElement | null => { return ( messagesScrollRootRef.current?.querySelector( '[data-slot="scroll-area-viewport"]', ) ?? null ); }, []); useEffect(() => { if (!open) { closeImageLightbox(); return; } if (loading) { return; } const bottom = messageBottomRef.current; if (!bottom) { return; } bottom.scrollIntoView({ block: "end", behavior: "smooth" }); }, [open, loading, closeImageLightbox]); useLayoutEffect(() => { const wasLoading = prevLoadingMoreRef.current; prevLoadingMoreRef.current = loadingMoreMessages; if (wasLoading && !loadingMoreMessages && pendingScrollAnchorRef.current) { const vp = getMessagesViewport(); const anchor = pendingScrollAnchorRef.current; pendingScrollAnchorRef.current = null; if (vp && anchor) { const delta = vp.scrollHeight - anchor.scrollHeight; vp.scrollTop = anchor.scrollTop + delta; } } }, [loadingMoreMessages, messages, getMessagesViewport]); useEffect(() => { if (!open || loading || !messagesHasMore || !onLoadMoreMessages) { return; } const root = getMessagesViewport(); const sentinel = loadMoreSentinelRef.current; if (!root || !sentinel) { return; } const observer = new IntersectionObserver( (entries) => { const hit = entries.some((e) => e.isIntersecting); if (!hit || loadingMoreMessages) { return; } const vp = getMessagesViewport(); if (vp) { pendingScrollAnchorRef.current = { scrollHeight: vp.scrollHeight, scrollTop: vp.scrollTop, }; } void onLoadMoreMessages(); }, { root, rootMargin: "120px 0px 0px 0px", threshold: 0 }, ); observer.observe(sentinel); return () => observer.disconnect(); }, [ open, loading, messagesHasMore, loadingMoreMessages, messages.length, onLoadMoreMessages, getMessagesViewport, ]); return ( {currentConversation?.customerName || "会话详情"}
{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 ? `最后活跃:${formatDateTime(currentConversation.lastMessageAt)}` : "暂无会话信息"}
{!isClosedConversation ? ( ) : null}
} > {loading ? (
正在加载会话详情...
) : currentConversation ? (
{/*

聊天记录

消息数:{messages.length} {messagesHasMore ? " · 向上滑可加载更早" : ""}
*/}
{messagesHasMore ? (
{loadingMoreMessages ? "正在加载更早的消息…" : "继续上滑加载更早消息"}
) : null} {messages.length ? ( messages.map((message) => { const layout = getMessageLayout(message); const isRecalled = Boolean(message.recalledAt) || message.sendStatus === 6; const isHtmlMessage = !isRecalled && (message.messageType === "html" || message.messageType === "attachment"); const isImageMessage = !isRecalled && message.messageType === "image"; return (
{getSenderLabel(message)} · {formatDateTime(message.sentAt)} {isRecalled ? ( <> · 已撤回 ) : null}
{isRecalled ? (
该消息已撤回
) : isHtmlMessage ? ( ) : isImageMessage ? ( ) : (
{getMessageContent(message)}
)}
客服 {message.agentRead ? "已读" : "未读"} / 用户{" "} {message.customerRead ? "已读" : "未读"}
); }) ) : (
暂无消息记录
)}
) : (
暂无可展示的会话详情
)}
); } 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) { if (!src) { return (
{alt || "[图片]"}
); } return ( ); }