fix: 优化界面展 及引用关系
This commit is contained in:
253
apps/desktop/src/components/EnhancedChatMessageV2.tsx
Normal file
253
apps/desktop/src/components/EnhancedChatMessageV2.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
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 || [];
|
||||
|
||||
// 复制消息内容
|
||||
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;
|
||||
Reference in New Issue
Block a user