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

View File

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

View File

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