import { clsx } from 'clsx';
import { User, Bot, Image, FileText, Download, ExternalLink, CheckCircle, Clock, XCircle, AlertCircle, ShoppingBag } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import { QRCodeSVG } from 'qrcode.react';
import { FileAttachment } from '../stores/chatStore';
import { AssessmentResultCard } from './AssessmentResultCard';
interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
createdAt: string;
attachments?: FileAttachment[];
metadata?: {
toolCalls?: Array<{
name: string;
result: unknown;
}>;
};
}
interface MessageBubbleProps {
message: Message;
isStreaming?: boolean;
}
export function MessageBubble({ message, isStreaming }: MessageBubbleProps) {
const isUser = message.role === 'user';
const hasAttachments = message.attachments && message.attachments.length > 0;
return (
{/* Avatar */}
{isUser ? (
) : (
)}
{/* Message content */}
{/* 文件附件 */}
{hasAttachments && (
{message.attachments!.map((attachment) => (
))}
)}
{/* 文本内容 */}
{message.content && (
{isUser ? (
{message.content}
) : (
{children}
,
h2: ({ children }) => {children}
,
h3: ({ children }) => {children}
,
p: ({ children }) => {children}
,
ul: ({ children }) => ,
ol: ({ children }) => {children}
,
li: ({ children }) => {children},
strong: ({ children }) => {children},
a: ({ href, children }) => (
{children}
),
}}
>
{message.content}
)}
{isStreaming && (
)}
{/* Tool call results (e.g., payment QR code) */}
{message.metadata?.toolCalls?.map((toolCall, index) => (
))}
)}
);
}
function AttachmentPreview({
attachment,
isUser,
}: {
attachment: FileAttachment;
isUser: boolean;
}) {
const isImage = attachment.type === 'image';
const formatFileSize = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
if (isImage && (attachment.downloadUrl || attachment.thumbnailUrl)) {
return (
);
}
return (
{isImage ? (
) : (
)}
{attachment.originalName}
{formatFileSize(attachment.size)}
);
}
function ToolCallResult({
toolCall,
}: {
toolCall: { name: string; result: unknown };
}) {
if (toolCall.name === 'generate_payment') {
const result = toolCall.result as {
success?: boolean;
qrCodeUrl?: string;
paymentUrl?: string;
amount?: number;
currency?: string;
message?: string;
orderId?: string;
expiresAt?: string;
error?: string;
};
if (!result.success) {
return (
{result.error || '支付创建失败'}
);
}
return (
{result.message}
{result.qrCodeUrl ? (
) : result.paymentUrl ? (
前往支付
) : null}
¥{result.amount}
{result.orderId && (
订单号: {result.orderId}
)}
{result.expiresAt && (
有效期至: {new Date(result.expiresAt).toLocaleTimeString('zh-CN')}
)}
);
}
if (toolCall.name === 'check_payment_status') {
const result = toolCall.result as {
success?: boolean;
orderId?: string;
status?: string;
statusLabel?: string;
paidAt?: string;
error?: string;
};
if (!result.success) {
return (
);
}
const statusConfig: Record = {
PAID: { icon: , color: 'bg-green-50 border-green-200' },
COMPLETED: { icon: , color: 'bg-green-50 border-green-200' },
PENDING_PAYMENT: { icon: , color: 'bg-yellow-50 border-yellow-200' },
CREATED: { icon: , color: 'bg-yellow-50 border-yellow-200' },
CANCELLED: { icon: , color: 'bg-red-50 border-red-200' },
REFUNDED: { icon: , color: 'bg-orange-50 border-orange-200' },
};
const config = statusConfig[result.status || ''] || { icon: , color: 'bg-secondary-50 border-secondary-200' };
return (
{config.icon}
{result.statusLabel || result.status}
{result.orderId && (
订单号: {result.orderId}
)}
{result.paidAt && (
支付时间: {new Date(result.paidAt).toLocaleString('zh-CN')}
)}
);
}
if (toolCall.name === 'query_order_history') {
const result = toolCall.result as {
success?: boolean;
totalOrders?: number;
orders?: Array<{
orderId: string;
serviceType: string;
serviceCategory?: string;
amount: number;
currency: string;
status: string;
paidAt?: string;
createdAt: string;
}>;
error?: string;
};
if (!result.success) {
return (
);
}
if (!result.orders || result.orders.length === 0) {
return (
);
}
const statusLabels: Record = {
CREATED: '待支付',
PENDING_PAYMENT: '待支付',
PAID: '已支付',
PROCESSING: '处理中',
COMPLETED: '已完成',
CANCELLED: '已取消',
REFUNDED: '已退款',
};
return (
订单记录 ({result.totalOrders})
{result.orders.map((order) => (
{order.serviceCategory || order.serviceType}
{new Date(order.createdAt).toLocaleDateString('zh-CN')}
¥{order.amount}
{statusLabels[order.status] || order.status}
))}
);
}
if (toolCall.name === 'invoke_assessment_expert') {
// Assessment expert returns a JSON string; parse it
try {
const raw = toolCall.result;
const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
if (data?.assessments && Array.isArray(data.assessments)) {
return ;
}
} catch {
// Not parseable — fall through to null
}
}
if (toolCall.name === 'cancel_order') {
const result = toolCall.result as {
success?: boolean;
orderId?: string;
message?: string;
error?: string;
};
return (
{result.success ? (
) : (
)}
{result.success ? result.message : result.error}
{result.orderId && (
订单号: {result.orderId}
)}
);
}
return null;
}