fix: 优化markdown解析器

This commit is contained in:
imeepos
2025-07-22 17:15:35 +08:00
parent eb9ec73889
commit 8f910c033e
3 changed files with 172 additions and 243 deletions

View File

@@ -7,12 +7,9 @@ import {
Clock, Clock,
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
ExternalLink,
Grid3X3,
List
} from 'lucide-react'; } from 'lucide-react';
import { EnhancedMarkdownRenderer } from './EnhancedMarkdownRenderer'; import { EnhancedMarkdownRenderer } from './EnhancedMarkdownRenderer';
import { ChatMaterialCard } from './ChatMaterialCard'; import ImageCard from './ImageCard';
import { GroundingSource } from '../types/ragGrounding'; import { GroundingSource } from '../types/ragGrounding';
/** /**
@@ -65,8 +62,6 @@ export const EnhancedChatMessageV2: React.FC<EnhancedChatMessageV2Props> = ({
}) => { }) => {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [expandedSources, setExpandedSources] = 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 isUser = message.type === 'user';
const isAssistant = message.type === 'assistant'; const isAssistant = message.type === 'assistant';
@@ -84,11 +79,7 @@ export const EnhancedChatMessageV2: React.FC<EnhancedChatMessageV2Props> = ({
} }
}, [message.content]); }, [message.content]);
// 处理引用点击
const handleReferenceClick = useCallback((sources: GroundingSource[], index: number) => {
setSelectedReference(selectedReference === index ? null : index);
console.log('引用点击:', { index, sources });
}, [selectedReference]);
// 处理素材卡片点击 // 处理素材卡片点击
const handleMaterialClick = useCallback((source: GroundingSource) => { const handleMaterialClick = useCallback((source: GroundingSource) => {
@@ -138,7 +129,6 @@ export const EnhancedChatMessageV2: React.FC<EnhancedChatMessageV2Props> = ({
groundingMetadata={groundingMetadata} groundingMetadata={groundingMetadata}
enableReferences={enableReferences} enableReferences={enableReferences}
enableMarkdown={true} enableMarkdown={true}
onReferenceClick={handleReferenceClick}
/> />
) : ( ) : (
<div className="whitespace-pre-wrap leading-relaxed"> <div className="whitespace-pre-wrap leading-relaxed">
@@ -204,41 +194,17 @@ export const EnhancedChatMessageV2: React.FC<EnhancedChatMessageV2Props> = ({
)} )}
</button> </button>
</div> </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> </div>
{expandedSources && ( {expandedSources && (
<div className={` <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
${materialViewMode === 'grid'
? 'material-grid compact'
: 'space-y-2'
}
`}>
{sources.map((source, index) => ( {sources.map((source, index) => (
<ChatMaterialCard <ImageCard
key={index} key={index}
source={source as GroundingSource} source={source as GroundingSource}
size={materialViewMode === 'grid' ? 'small' : 'medium'} showDetails={true}
showDetails={materialViewMode === 'list'} className="cursor-pointer"
onClick={handleMaterialClick} onViewLarge={() => handleMaterialClick(source as GroundingSource)}
/> />
))} ))}
</div> </div>

View File

@@ -37,7 +37,6 @@ interface EnhancedMarkdownRendererProps {
export const EnhancedMarkdownRenderer: React.FC<EnhancedMarkdownRendererProps> = ({ export const EnhancedMarkdownRenderer: React.FC<EnhancedMarkdownRendererProps> = ({
content, content,
groundingMetadata, groundingMetadata,
enableReferences = true,
enableMarkdown = true, enableMarkdown = true,
className = '', className = '',
showStatistics = false, showStatistics = false,
@@ -47,8 +46,7 @@ export const EnhancedMarkdownRenderer: React.FC<EnhancedMarkdownRendererProps> =
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [validation, setValidation] = useState<ValidationResult | null>(null); const [validation, setValidation] = useState<ValidationResult | null>(null);
const [selectedGrounding, setSelectedGrounding] = useState<any>(null); const [expandedGrounding, setExpandedGrounding] = useState<any>(null);
const [showGroundingModal, setShowGroundingModal] = useState(false);
const [previewSource, setPreviewSource] = useState<any>(null); const [previewSource, setPreviewSource] = useState<any>(null);
// 解析Markdown内容 // 解析Markdown内容
@@ -98,17 +96,16 @@ export const EnhancedMarkdownRenderer: React.FC<EnhancedMarkdownRendererProps> =
} }
}, [parseContent, enableRealTimeParsing]); }, [parseContent, enableRealTimeParsing]);
// 显示grounding详情 // 切换grounding详情展开状态
const showGroundingDetails = useCallback((groundingAnalysis: any) => { const toggleGroundingDetails = useCallback((groundingAnalysis: any) => {
setSelectedGrounding(groundingAnalysis); if (expandedGrounding && expandedGrounding === groundingAnalysis) {
setShowGroundingModal(true); // 如果当前已展开相同的内容,则收起
}, []); setExpandedGrounding(null);
} else {
// 关闭grounding详情 // 展开新的内容
const closeGroundingDetails = useCallback(() => { setExpandedGrounding(groundingAnalysis);
setShowGroundingModal(false); }
setSelectedGrounding(null); }, [expandedGrounding]);
}, []);
// 查看大图 // 查看大图
const handleViewLarge = useCallback((source: any) => { const handleViewLarge = useCallback((source: any) => {
@@ -216,22 +213,36 @@ export const EnhancedMarkdownRenderer: React.FC<EnhancedMarkdownRendererProps> =
}, [groundingMetadata, parseResult]); }, [groundingMetadata, parseResult]);
// 渲染单个Markdown节点 // 渲染单个Markdown节点
const renderNode = useCallback((node: MarkdownNode, depth: number = 0, index: number = 0): React.ReactNode => { const renderNode = useCallback((node: MarkdownNode, depth: number = 0, index: number = 0, showGroundingIndicator: boolean = true): React.ReactNode => {
const key = `${node.range?.start?.line || 0}-${node.range?.start?.column || 0}-${node.range?.start?.offset || 0}-${depth}-${index}`; const key = `${node.range?.start?.line || 0}-${node.range?.start?.column || 0}-${node.range?.start?.offset || 0}-${depth}-${index}`;
// 分析当前节点的引用关联 // 分析当前节点的引用关联
const groundingAnalysis = analyzeNodeGrounding(node); const groundingAnalysis = analyzeNodeGrounding(node);
// 创建引用指示器组件 // 创建引用指示器组件
const GroundingIndicator = groundingAnalysis ? ( const isExpanded = expandedGrounding === groundingAnalysis;
const GroundingIndicator = (groundingAnalysis && showGroundingIndicator) ? (
<span <span
className="inline-flex items-center ml-1 px-1 py-0.5 text-xs bg-blue-100 text-blue-700 rounded cursor-help" className={`inline-flex items-center ml-1 px-2 py-1 text-xs rounded cursor-pointer transition-all duration-200 ${
title={`引用了 ${groundingAnalysis.groundingInfo.sourceCount} 个来源`} isExpanded
? 'bg-blue-500 text-white shadow-md'
: 'bg-blue-100 text-blue-700 hover:bg-blue-200'
}`}
title={`${isExpanded ? '收起' : '查看'} ${groundingAnalysis.groundingInfo.sourceCount} 个相关素材`}
onClick={() => { onClick={() => {
console.log('📚 点击查看引用详情:', groundingAnalysis); console.log('📚 点击查看引用详情:', groundingAnalysis);
// 显示详细的引用信息,包括图片 // 切换详细的引用信息展示
showGroundingDetails(groundingAnalysis); toggleGroundingDetails(groundingAnalysis);
}} }}
> >
{groundingAnalysis.groundingInfo.sourceCount} <span className="mr-1">🖼</span>
<span>{groundingAnalysis.groundingInfo.sourceCount}</span>
<svg
className={`w-3 h-3 ml-1 transition-transform duration-200 ${isExpanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</span> </span>
) : null; ) : null;
@@ -332,7 +343,7 @@ export const EnhancedMarkdownRenderer: React.FC<EnhancedMarkdownRendererProps> =
case MarkdownNodeType.Strong: case MarkdownNodeType.Strong:
return ( return (
<strong key={key}> <strong key={key}>
{node.children.map((child, childIndex) => renderNode(child, depth + 1, childIndex))} {node.children.map((child, childIndex) => renderNode(child, depth + 1, childIndex, false))}
</strong> </strong>
); );
@@ -424,80 +435,48 @@ export const EnhancedMarkdownRenderer: React.FC<EnhancedMarkdownRendererProps> =
</div> </div>
</div> </div>
)} )}
{/* Grounding详情内联展开 */}
{/* 显示引用来源信息(如果有的话) */} {expandedGrounding && (
{enableReferences && groundingMetadata?.sources && groundingMetadata.sources.length > 0 && ( <div className="mt-6 border-t border-gray-200 pt-4 animate-in slide-in-from-top-2 duration-300">
<div className="mt-4 pt-3 border-t border-gray-200"> <div className="flex items-center justify-between mb-4">
<div className="text-xs text-gray-500 mb-2"> <h3 className="text-base font-semibold text-gray-900 flex items-center">
{groundingMetadata.sources.length} <span className="mr-2">🖼</span>
</div> ({expandedGrounding.groundingInfo?.sourceCount || 0})
<div className="flex flex-wrap gap-1"> </h3>
{groundingMetadata.sources.slice(0, 5).map((source, index) => (
<span
key={index}
className="inline-flex items-center px-2 py-1 text-xs bg-blue-50 text-blue-700 rounded-full cursor-pointer hover:bg-blue-100"
title={source.title || `来源 ${index + 1}`}
onClick={() => showGroundingDetails({
groundingInfo: {
sourceCount: 1,
sources: [source]
}
})}
>
{index + 1}
</span>
))}
{groundingMetadata.sources.length > 5 && (
<span className="text-xs text-gray-400">
+{groundingMetadata.sources.length - 5}
</span>
)}
</div>
</div>
)}
{/* Grounding详情模态框 */}
{showGroundingModal && selectedGrounding && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white rounded-lg shadow-xl max-w-4xl max-h-[90vh] overflow-hidden">
{/* 模态框头部 */}
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900"></h3>
<button <button
onClick={closeGroundingDetails} onClick={() => setExpandedGrounding(null)}
className="text-gray-400 hover:text-gray-600 transition-colors" className="text-gray-400 hover:text-gray-600 transition-colors p-2 rounded-full hover:bg-gray-100"
title="收起"
> >
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
</svg> </svg>
</button> </button>
</div> </div>
{/* 模态框内容 */} <div className="bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl p-6 shadow-inner">
<div className="p-4 overflow-y-auto max-h-[calc(90vh-8rem)]"> {expandedGrounding.groundingInfo?.sources && expandedGrounding.groundingInfo.sources.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{selectedGrounding.groundingInfo?.sources?.map((source: any, index: number) => ( {expandedGrounding.groundingInfo.sources.map((source: any, index: number) => (
<div key={index} className="relative"> <div key={index} className="relative transform hover:scale-105 transition-transform duration-200">
<ImageCard <ImageCard
source={source} source={source}
showDetails={true} showDetails={true}
onViewLarge={handleViewLarge} onViewLarge={handleViewLarge}
className="shadow-lg border border-gray-200 hover:shadow-xl transition-shadow" className="shadow-lg border border-gray-200 hover:shadow-xl transition-all duration-300 bg-white"
/> />
</div> </div>
))} ))}
</div> </div>
) : (
{/* 如果没有图片,显示提示信息 */} <div className="text-center py-12 text-gray-500">
{(!selectedGrounding.groundingInfo?.sources || selectedGrounding.groundingInfo.sources.length === 0) && ( <div className="text-6xl mb-4 opacity-50">🖼</div>
<div className="text-center py-8 text-gray-500"> <p className="text-base font-medium mb-2"></p>
<div className="text-4xl mb-2">🖼</div> <p className="text-sm text-gray-400"></p>
<p></p>
</div> </div>
)} )}
</div> </div>
</div> </div>
</div>
)} )}
{/* 图片预览模态框 */} {/* 图片预览模态框 */}

View File

@@ -2,13 +2,11 @@ import React, { useState, useCallback } from 'react';
import { import {
Download, Download,
ExternalLink, ExternalLink,
Eye,
X, X,
Calendar, Calendar,
MapPin, MapPin,
User, User,
Shirt, Shirt,
Image as ImageIcon,
AlertCircle AlertCircle
} from 'lucide-react'; } from 'lucide-react';
import { GroundingSource } from '../types/ragGrounding'; import { GroundingSource } from '../types/ragGrounding';
@@ -92,13 +90,6 @@ export const ImageCard: React.FC<ImageCardProps> = ({
} }
}, [onDownload, source, isDownloading]); }, [onDownload, source, isDownloading]);
// 处理查看大图
const handleViewLarge = useCallback(() => {
if (onViewLarge) {
onViewLarge(source);
}
}, [onViewLarge, source]);
// 构建位置样式 // 构建位置样式
const positionStyle = position ? { const positionStyle = position ? {
position: 'absolute' as const, position: 'absolute' as const,
@@ -111,30 +102,30 @@ export const ImageCard: React.FC<ImageCardProps> = ({
return ( return (
<div <div
className={`bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden max-w-sm ${className}`} className={`bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden max-w-sm group ${className}`}
style={positionStyle} style={positionStyle}
> >
{/* 卡片头部 */} {/* 图片展示区域 */}
<div className="flex items-center justify-between p-3 border-b border-gray-100"> <div className="relative bg-gray-50">
<div className="flex items-center space-x-2"> {imageUri && !imageError ? (
<ImageIcon className="w-4 h-4 text-pink-500" /> <div className="relative">
<h3 className="text-sm font-medium text-gray-900 truncate">{title}</h3> <img
</div> src={imageUri}
<div className="flex items-center space-x-1"> alt={description || title}
{onViewLarge && ( className={`w-full h-48 object-cover transition-all duration-300 ${
<button imageLoaded ? 'opacity-100' : 'opacity-0'
onClick={handleViewLarge} } group-hover:scale-105`}
className="p-1 text-gray-400 hover:text-gray-600 transition-colors" onLoad={handleImageLoad}
title="查看大图" onError={handleImageError}
> />
<Eye className="w-4 h-4" />
</button> {/* 悬浮按钮组 */}
)} <div className="absolute top-2 right-2 flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
{onDownload && ( {onDownload && (
<button <button
onClick={handleDownload} onClick={handleDownload}
disabled={isDownloading} disabled={isDownloading}
className="p-1 text-gray-400 hover:text-pink-500 transition-colors disabled:opacity-50" className="p-2 bg-black/50 hover:bg-black/70 text-white rounded-full backdrop-blur-sm transition-all duration-200 hover:scale-110 disabled:opacity-50 disabled:cursor-not-allowed"
title="下载到本地" title="下载到本地"
> >
<Download className={`w-4 h-4 ${isDownloading ? 'animate-pulse' : ''}`} /> <Download className={`w-4 h-4 ${isDownloading ? 'animate-pulse' : ''}`} />
@@ -145,7 +136,7 @@ export const ImageCard: React.FC<ImageCardProps> = ({
href={imageUri} href={imageUri}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="p-1 text-gray-400 hover:text-blue-500 transition-colors" className="p-2 bg-black/50 hover:bg-black/70 text-white rounded-full backdrop-blur-sm transition-all duration-200 hover:scale-110"
title="在新窗口打开" title="在新窗口打开"
> >
<ExternalLink className="w-4 h-4" /> <ExternalLink className="w-4 h-4" />
@@ -154,36 +145,23 @@ export const ImageCard: React.FC<ImageCardProps> = ({
{onClose && ( {onClose && (
<button <button
onClick={onClose} onClick={onClose}
className="p-1 text-gray-400 hover:text-gray-600 transition-colors" className="p-2 bg-black/50 hover:bg-black/70 text-white rounded-full backdrop-blur-sm transition-all duration-200 hover:scale-110"
title="关闭" title="关闭"
> >
<X className="w-4 h-4" /> <X className="w-4 h-4" />
</button> </button>
)} )}
</div> </div>
</div>
{/* 图片展示区域 */} {/* 加载指示器 */}
<div className="relative bg-gray-50">
{imageUri && !imageError ? (
<div className="relative">
<img
src={imageUri}
alt={description || title}
className={`w-full h-48 object-cover transition-opacity duration-300 ${
imageLoaded ? 'opacity-100' : 'opacity-0'
}`}
onLoad={handleImageLoad}
onError={handleImageError}
/>
{!imageLoaded && ( {!imageLoaded && (
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center bg-gray-100">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-pink-500"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-pink-500"></div>
</div> </div>
)} )}
</div> </div>
) : ( ) : (
<div className="h-48 flex items-center justify-center text-gray-400"> <div className="h-48 flex items-center justify-center text-gray-400 bg-gray-100">
<div className="text-center"> <div className="text-center">
<AlertCircle className="w-8 h-8 mx-auto mb-2" /> <AlertCircle className="w-8 h-8 mx-auto mb-2" />
<p className="text-sm"></p> <p className="text-sm"></p>
@@ -194,27 +172,29 @@ export const ImageCard: React.FC<ImageCardProps> = ({
{/* 详细信息区域 */} {/* 详细信息区域 */}
{showDetails && ( {showDetails && (
<div className="p-3 space-y-3"> <div className="p-4 space-y-3">
{/* 描述 */} {/* 描述 */}
{description && ( {description && (
<p className="text-sm text-gray-600 line-clamp-2">{description}</p> <p className="text-sm text-gray-700 line-clamp-2 leading-relaxed">{description}</p>
)} )}
{/* 标签和信息 */}
<div className="space-y-2">
{/* 环境标签 */} {/* 环境标签 */}
{environmentTags.length > 0 && ( {environmentTags.length > 0 && (
<div className="flex items-center space-x-1"> <div className="flex items-start space-x-2">
<MapPin className="w-3 h-3 text-gray-400" /> <MapPin className="w-3 h-3 text-gray-400 mt-0.5 flex-shrink-0" />
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1 min-w-0">
{environmentTags.slice(0, 3).map((tag: string, index: number) => ( {environmentTags.slice(0, 2).map((tag: string, index: number) => (
<span <span
key={index} key={index}
className="px-2 py-0.5 bg-blue-50 text-blue-600 text-xs rounded-full" className="px-2 py-1 bg-blue-50 text-blue-700 text-xs rounded-md font-medium"
> >
{tag} {tag}
</span> </span>
))} ))}
{environmentTags.length > 3 && ( {environmentTags.length > 2 && (
<span className="text-xs text-gray-400">+{environmentTags.length - 3}</span> <span className="text-xs text-gray-500 py-1">+{environmentTags.length - 2}</span>
)} )}
</div> </div>
</div> </div>
@@ -222,29 +202,31 @@ export const ImageCard: React.FC<ImageCardProps> = ({
{/* 服装类别 */} {/* 服装类别 */}
{categories.length > 0 && ( {categories.length > 0 && (
<div className="flex items-center space-x-1"> <div className="flex items-start space-x-2">
<Shirt className="w-3 h-3 text-gray-400" /> <Shirt className="w-3 h-3 text-gray-400 mt-0.5 flex-shrink-0" />
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1 min-w-0">
{categories.slice(0, 4).map((category: string, index: number) => ( {categories.slice(0, 2).map((category: string, index: number) => (
<span <span
key={index} key={index}
className="px-2 py-0.5 bg-pink-50 text-pink-600 text-xs rounded-full" className="px-2 py-1 bg-pink-50 text-pink-700 text-xs rounded-md font-medium"
> >
{category} {category}
</span> </span>
))} ))}
{categories.length > 4 && ( {categories.length > 2 && (
<span className="text-xs text-gray-400">+{categories.length - 4}</span> <span className="text-xs text-gray-500 py-1">+{categories.length - 2}</span>
)} )}
</div> </div>
</div> </div>
)} )}
{/* 底部信息行 */}
<div className="flex items-center justify-between pt-1 border-t border-gray-100">
{/* 模特信息 */} {/* 模特信息 */}
{models.length > 0 && ( {models.length > 0 && (
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<User className="w-3 h-3 text-gray-400" /> <User className="w-3 h-3 text-gray-400" />
<span className="text-xs text-gray-500"> <span className="text-xs text-gray-600 font-medium">
{models.length} {models.length}
</span> </span>
</div> </div>
@@ -260,6 +242,8 @@ export const ImageCard: React.FC<ImageCardProps> = ({
</div> </div>
)} )}
</div> </div>
</div>
</div>
)} )}
</div> </div>
); );