228 lines
6.9 KiB
TypeScript
228 lines
6.9 KiB
TypeScript
import React, { useState, useCallback } from 'react';
|
|
import {
|
|
Copy,
|
|
Check,
|
|
User,
|
|
Bot,
|
|
Clock,
|
|
ChevronDown,
|
|
ChevronUp,
|
|
} from 'lucide-react';
|
|
import { EnhancedMarkdownRenderer } from './EnhancedMarkdownRenderer';
|
|
import ImageCard from './ImageCard';
|
|
import { GroundingSource } from '../types/ragGrounding';
|
|
|
|
/**
|
|
* 聊天消息接口
|
|
*/
|
|
interface ChatMessage {
|
|
id: string;
|
|
type: 'user' | 'assistant';
|
|
content: string;
|
|
timestamp: Date;
|
|
status?: 'sending' | 'sent' | 'error';
|
|
metadata?: {
|
|
responseTime?: number;
|
|
modelUsed?: string;
|
|
sources?: Array<{
|
|
title: string;
|
|
uri?: string;
|
|
content?: any;
|
|
}>;
|
|
grounding_metadata?: any;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 增强聊天消息组件属性接口
|
|
*/
|
|
interface EnhancedChatMessageV2Props {
|
|
/** 消息数据 */
|
|
message: ChatMessage;
|
|
/** 是否显示来源信息 */
|
|
showSources?: boolean;
|
|
/** 是否启用素材卡片 */
|
|
enableMaterialCards?: boolean;
|
|
/** 是否启用角标引用 */
|
|
enableReferences?: boolean;
|
|
/** 自定义样式类名 */
|
|
className?: string;
|
|
/** 已选中的标签列表 */
|
|
selectedTags?: string[];
|
|
/** 标签点击回调 */
|
|
onTagClick?: (tag: string) => void;
|
|
}
|
|
|
|
/**
|
|
* 增强聊天消息组件 V2
|
|
* 支持角标引用、优化素材展示和Markdown渲染
|
|
*/
|
|
export const EnhancedChatMessageV2: React.FC<EnhancedChatMessageV2Props> = ({
|
|
message,
|
|
showSources = true,
|
|
enableMaterialCards = true,
|
|
enableReferences = true,
|
|
className = '',
|
|
selectedTags = [],
|
|
onTagClick
|
|
}) => {
|
|
const [copied, setCopied] = useState(false);
|
|
const [expandedSources, setExpandedSources] = useState(false);
|
|
|
|
const isUser = message.type === 'user';
|
|
const isAssistant = message.type === 'assistant';
|
|
const groundingMetadata = message.metadata?.grounding_metadata;
|
|
const sources = message.metadata?.sources || [];
|
|
|
|
// 复制消息内容
|
|
const handleCopy = useCallback(async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(message.content);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
} catch (error) {
|
|
console.error('复制失败:', error);
|
|
}
|
|
}, [message.content]);
|
|
|
|
|
|
|
|
// 处理素材卡片点击
|
|
const handleMaterialClick = useCallback((source: GroundingSource) => {
|
|
console.log('素材点击:', source);
|
|
// TODO: 实现素材详情查看逻辑
|
|
}, []);
|
|
|
|
// 格式化时间
|
|
const formatTime = (date: Date) => {
|
|
return date.toLocaleTimeString('zh-CN', {
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className={`flex gap-3 p-4 chat-message-enter ${isUser ? 'flex-row-reverse' : 'flex-row'} ${className}`}>
|
|
{/* 头像 */}
|
|
<div className="flex-shrink-0">
|
|
<div className={`
|
|
w-8 h-8 rounded-full flex items-center justify-center
|
|
${isUser
|
|
? 'bg-blue-500 text-white'
|
|
: 'bg-gradient-to-br from-purple-500 to-pink-500 text-white'
|
|
}
|
|
`}>
|
|
{isUser ? <User className="w-4 h-4" /> : <Bot className="w-4 h-4" />}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 消息内容 */}
|
|
<div className={`flex-1 max-w-full ${isUser ? 'text-right' : 'text-left'}`}>
|
|
{/* 消息气泡 */}
|
|
<div className={`
|
|
inline-block p-4 rounded-2xl chat-message-bubble
|
|
${isUser
|
|
? 'user text-white rounded-br-md'
|
|
: 'assistant rounded-bl-md'
|
|
}
|
|
${message.status === 'sending' ? 'opacity-70' : ''}
|
|
`}>
|
|
{/* 消息内容渲染 */}
|
|
<div className={`${isUser ? 'text-white' : 'text-gray-900'}`}>
|
|
{isAssistant && enableReferences ? (
|
|
<EnhancedMarkdownRenderer
|
|
content={message.content}
|
|
groundingMetadata={groundingMetadata}
|
|
enableReferences={enableReferences}
|
|
enableMarkdown={true}
|
|
/>
|
|
) : (
|
|
<div className="whitespace-pre-wrap leading-relaxed">
|
|
{message.content}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 消息状态 */}
|
|
{message.status === 'sending' && (
|
|
<div className="flex items-center mt-2 text-xs opacity-70">
|
|
<div className="animate-spin rounded-full h-3 w-3 border border-current border-t-transparent mr-2" />
|
|
发送中...
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 消息元信息 */}
|
|
<div className={`flex items-center gap-2 mt-2 text-xs text-gray-500 ${isUser ? 'justify-end' : 'justify-start'}`}>
|
|
<div className="flex items-center gap-1">
|
|
<Clock className="w-3 h-3" />
|
|
<span>{formatTime(message.timestamp)}</span>
|
|
</div>
|
|
|
|
{isAssistant && message.metadata?.responseTime && (
|
|
<span>• 响应时间: {message.metadata.responseTime}ms</span>
|
|
)}
|
|
|
|
{isAssistant && message.metadata?.modelUsed && (
|
|
<span>• {message.metadata.modelUsed}</span>
|
|
)}
|
|
|
|
{/* 复制按钮 */}
|
|
<button
|
|
onClick={handleCopy}
|
|
className="ml-2 p-1 hover:bg-gray-100 rounded transition-colors"
|
|
title="复制消息"
|
|
>
|
|
{copied ? (
|
|
<Check className="w-3 h-3 text-green-500" />
|
|
) : (
|
|
<Copy className="w-3 h-3" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{/* 素材来源展示 */}
|
|
{isAssistant && showSources && sources.length > 0 && enableMaterialCards && (
|
|
<div className="mt-4">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<h4 className="text-sm font-medium text-gray-700">
|
|
相关素材 ({sources.length})
|
|
</h4>
|
|
<button
|
|
onClick={() => setExpandedSources(!expandedSources)}
|
|
className="text-gray-500 hover:text-gray-700 transition-colors"
|
|
>
|
|
{expandedSources ? (
|
|
<ChevronUp className="w-4 h-4" />
|
|
) : (
|
|
<ChevronDown className="w-4 h-4" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{expandedSources && (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
|
|
{sources.map((source, index) => (
|
|
<ImageCard
|
|
key={index}
|
|
source={source as GroundingSource}
|
|
showDetails={true}
|
|
className="cursor-pointer"
|
|
onViewLarge={() => handleMaterialClick(source as GroundingSource)}
|
|
selectedTags={selectedTags}
|
|
onTagClick={onTagClick}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default EnhancedChatMessageV2;
|