mixvideo-v2/apps/desktop/src/components/EnhancedChatMessageV2.tsx

267 lines
8.5 KiB
TypeScript

import React, { useState, useCallback } from 'react';
import {
Copy,
Check,
User,
Bot,
Clock,
ChevronDown,
ChevronUp,
ExternalLink,
Grid3X3,
List
} from 'lucide-react';
import { EnhancedMarkdownRenderer } from './EnhancedMarkdownRenderer';
import { ChatMaterialCard } from './ChatMaterialCard';
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;
}
/**
* 增强聊天消息组件 V2
* 支持角标引用、优化素材展示和Markdown渲染
*/
export const EnhancedChatMessageV2: React.FC<EnhancedChatMessageV2Props> = ({
message,
showSources = true,
enableMaterialCards = true,
enableReferences = true,
className = ''
}) => {
const [copied, setCopied] = useState(false);
const [expandedSources, setExpandedSources] = useState(false);
const [materialViewMode, setMaterialViewMode] = useState<'grid' | 'list'>('grid');
const [selectedReference, setSelectedReference] = useState<number | null>(null);
const isUser = message.type === 'user';
const isAssistant = message.type === 'assistant';
const groundingMetadata = message.metadata?.grounding_metadata;
const sources = message.metadata?.sources || [];
// 调试信息
console.log('🔍 EnhancedChatMessageV2 Debug:', {
messageId: message.id,
messageType: message.type,
contentLength: message.content.length,
hasMetadata: !!message.metadata,
hasGroundingMetadata: !!groundingMetadata,
groundingSupportsCount: groundingMetadata?.grounding_supports?.length || 0,
sourcesCount: sources.length,
enableReferences,
groundingMetadata
});
// 复制消息内容
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 handleReferenceClick = useCallback((sources: GroundingSource[], index: number) => {
setSelectedReference(selectedReference === index ? null : index);
console.log('引用点击:', { index, sources });
}, [selectedReference]);
// 处理素材卡片点击
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-3xl ${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}
onReferenceClick={handleReferenceClick}
/>
) : (
<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>
{expandedSources && (
<div className="flex items-center gap-1">
<button
onClick={() => setMaterialViewMode('grid')}
className={`p-1 rounded ${materialViewMode === 'grid' ? 'bg-gray-200' : 'hover:bg-gray-100'}`}
title="网格视图"
>
<Grid3X3 className="w-4 h-4" />
</button>
<button
onClick={() => setMaterialViewMode('list')}
className={`p-1 rounded ${materialViewMode === 'list' ? 'bg-gray-200' : 'hover:bg-gray-100'}`}
title="列表视图"
>
<List className="w-4 h-4" />
</button>
</div>
)}
</div>
{expandedSources && (
<div className={`
${materialViewMode === 'grid'
? 'material-grid compact'
: 'space-y-2'
}
`}>
{sources.map((source, index) => (
<ChatMaterialCard
key={index}
source={source as GroundingSource}
size={materialViewMode === 'grid' ? 'small' : 'medium'}
showDetails={materialViewMode === 'list'}
onClick={handleMaterialClick}
/>
))}
</div>
)}
</div>
)}
</div>
</div>
);
};
export default EnhancedChatMessageV2;