import React, { useMemo, useEffect, useRef } from 'react'; import { Card, Tag, Badge, Empty, Collapse, Divider, theme } from 'antd'; import { FireOutlined, StarOutlined, ThunderboltOutlined, UserOutlined, } from '@ant-design/icons'; import type { MemoryAnnotation } from './AnnotatedText'; const { Panel } = Collapse; interface MemorySidebarProps { annotations: MemoryAnnotation[]; activeAnnotationId?: string; onAnnotationClick?: (annotation: MemoryAnnotation) => void; scrollToAnnotation?: string; } // 类型配置 const TYPE_CONFIG = { hook: { label: '钩子', icon: , }, foreshadow: { label: '伏笔', icon: , }, plot_point: { label: '情节点', icon: , }, character_event: { label: '角色事件', icon: , }, }; /** * 记忆侧边栏组件 * 展示章节的所有记忆标注 */ const MemorySidebar: React.FC = ({ annotations, activeAnnotationId, onAnnotationClick, scrollToAnnotation, }) => { const { token } = theme.useToken(); const cardRefs = useRef>({}); const typeColors: Record = { hook: token.colorError, foreshadow: token.colorInfo, plot_point: token.colorSuccess, character_event: token.colorWarning, }; // 当需要滚动到特定标注卡片时 useEffect(() => { if (scrollToAnnotation && cardRefs.current[scrollToAnnotation]) { const element = cardRefs.current[scrollToAnnotation]; element?.scrollIntoView({ behavior: 'smooth', block: 'center', }); } }, [scrollToAnnotation]); // 按类型分组 const groupedAnnotations = useMemo(() => { const groups: Record = { hook: [], foreshadow: [], plot_point: [], character_event: [], }; annotations.forEach((annotation) => { if (groups[annotation.type]) { groups[annotation.type].push(annotation); } }); // 每组按重要性排序 Object.keys(groups).forEach((type) => { groups[type].sort((a, b) => b.importance - a.importance); }); return groups; }, [annotations]); // 统计信息 const stats = useMemo(() => { return { total: annotations.length, hooks: groupedAnnotations.hook.length, foreshadows: groupedAnnotations.foreshadow.length, plotPoints: groupedAnnotations.plot_point.length, characterEvents: groupedAnnotations.character_event.length, }; }, [annotations, groupedAnnotations]); // 渲染单个记忆卡片 const renderMemoryCard = (annotation: MemoryAnnotation) => { const config = TYPE_CONFIG[annotation.type]; const color = typeColors[annotation.type]; const isActive = activeAnnotationId === annotation.id; return (
{ cardRefs.current[annotation.id] = el; }} > onAnnotationClick?.(annotation)} style={{ marginBottom: 12, borderLeft: `4px solid ${color}`, backgroundColor: isActive ? `color-mix(in srgb, ${color} 8%, transparent)` : 'transparent', cursor: 'pointer', transition: 'all 0.2s', }} bodyStyle={{ padding: 12 }} >
{config.icon} {annotation.title}
{annotation.content.length > 100 ? `${annotation.content.slice(0, 100)}...` : annotation.content}
{annotation.tags && annotation.tags.length > 0 && (
{annotation.tags.map((tag, index) => ( {tag} ))}
)} {/* 特殊元数据 */} {annotation.metadata.strength && (
强度: {annotation.metadata.strength}/10
)} {annotation.metadata.foreshadowType && ( {annotation.metadata.foreshadowType === 'planted' ? '已埋下' : '已回收'} )}
); }; if (annotations.length === 0) { return (
); } return (
{/* 统计概览 */}
📊 分析概览
钩子
{stats.hooks}
伏笔
{stats.foreshadows}
情节点
{stats.plotPoints}
角色事件
{stats.characterEvents}
{/* 分类展示 */} {Object.entries(groupedAnnotations).map(([type, items]) => { if (items.length === 0) return null; const config = TYPE_CONFIG[type as keyof typeof TYPE_CONFIG]; return ( {config.icon} {config.label} ({items.length}) } > {items.map((annotation) => renderMemoryCard(annotation))} ); })}
); }; export default MemorySidebar;