import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Card, Spin, Alert, Button, Space, Switch, Drawer, message, Progress } from 'antd'; import { ArrowLeftOutlined, EyeOutlined, EyeInvisibleOutlined, MenuOutlined, ReloadOutlined, LeftOutlined, RightOutlined, } from '@ant-design/icons'; import api from '../services/api'; import AnnotatedText, { type MemoryAnnotation } from '../components/AnnotatedText'; import MemorySidebar from '../components/MemorySidebar'; interface ChapterData { id: string; chapter_number: number; title: string; content: string; word_count: number; } interface AnnotationsData { chapter_id: string; chapter_number: number; title: string; word_count: number; annotations: MemoryAnnotation[]; has_analysis: boolean; summary: { total_annotations: number; hooks: number; foreshadows: number; plot_points: number; character_events: number; }; } interface NavigationData { current: { id: string; chapter_number: number; title: string; }; previous: { id: string; chapter_number: number; title: string; } | null; next: { id: string; chapter_number: number; title: string; } | null; } /** * 章节阅读器页面 * 展示带有记忆标注的章节内容 */ const ChapterReader: React.FC = () => { const { chapterId } = useParams<{ chapterId: string }>(); const navigate = useNavigate(); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [chapter, setChapter] = useState(null); const [annotationsData, setAnnotationsData] = useState(null); const [showAnnotations, setShowAnnotations] = useState(true); const [activeAnnotationId, setActiveAnnotationId] = useState(); const [sidebarVisible, setSidebarVisible] = useState(false); const [analyzing, setAnalyzing] = useState(false); const [analysisProgress, setAnalysisProgress] = useState(0); const [navigation, setNavigation] = useState(null); useEffect(() => { if (chapterId) { loadChapterData(); } }, [chapterId]); const loadChapterData = async () => { try { setLoading(true); setError(null); // 并行加载章节内容、标注数据和导航信息 // 注意:api拦截器已经解析了response.data,所以直接返回数据对象 const [chapterData, annotationsData, navigationData] = await Promise.all([ api.get(`/chapters/${chapterId}`).catch(err => { console.error('加载章节失败:', err); throw err; }), api.get(`/chapters/${chapterId}/annotations`).catch(err => { console.warn('加载标注失败:', err); return null; }), // 如果没有分析数据也不报错 api.get(`/chapters/${chapterId}/navigation`).catch(err => { console.warn('加载导航信息失败:', err); return null; }), ]); console.log('章节数据:', chapterData); console.log('标注数据:', annotationsData); console.log('导航数据:', navigationData); // 验证数据 if (!chapterData || !chapterData.content) { throw new Error('章节数据无效:缺少内容'); } setChapter(chapterData); setNavigation(navigationData); // 验证标注数据 if (annotationsData) { const validAnnotations = annotationsData.annotations.filter( (a: MemoryAnnotation) => a.position >= 0 && a.position < chapterData.content.length ); const invalidCount = annotationsData.annotations.length - validAnnotations.length; if (invalidCount > 0) { console.warn(`${invalidCount}个标注位置无效,将仅显示${validAnnotations.length}个有效标注`); } setAnnotationsData(annotationsData); } else { setAnnotationsData(null); } } catch (err: any) { console.error('加载章节数据失败:', err); setError(err.response?.data?.detail || err.message || '加载失败'); } finally { setLoading(false); } }; const handleAnnotationClick = (annotation: MemoryAnnotation) => { setActiveAnnotationId(annotation.id); // 移动端显示侧边栏 if (window.innerWidth < 768) { setSidebarVisible(true); } }; const handleBackClick = () => { navigate(-1); }; const handlePreviousChapter = () => { if (navigation?.previous) { navigate(`/chapters/${navigation.previous.id}/reader`); } }; const handleNextChapter = () => { if (navigation?.next) { navigate(`/chapters/${navigation.next.id}/reader`); } }; const handleReanalyze = async () => { if (!chapterId) return; try { setAnalyzing(true); setAnalysisProgress(0); message.loading({ content: '开始分析章节...', key: 'analyze', duration: 0 }); // 触发分析 await api.post(`/chapters/${chapterId}/analyze`); // 轮询分析状态 const pollInterval = setInterval(async () => { try { const statusRes = await api.get(`/chapters/${chapterId}/analysis/status`); const { status, progress, error_message } = statusRes.data; setAnalysisProgress(progress || 0); if (status === 'completed') { clearInterval(pollInterval); setAnalyzing(false); message.success({ content: '分析完成!', key: 'analyze' }); // 重新加载标注数据 const annotationsRes = await api.get(`/chapters/${chapterId}/annotations`); setAnnotationsData(annotationsRes.data); } else if (status === 'failed') { clearInterval(pollInterval); setAnalyzing(false); message.error({ content: `分析失败:${error_message || '未知错误'}`, key: 'analyze' }); } } catch (err) { console.error('轮询分析状态失败:', err); } }, 2000); // 每2秒轮询一次 // 30秒超时 setTimeout(() => { clearInterval(pollInterval); if (analyzing) { setAnalyzing(false); message.warning({ content: '分析超时,请稍后刷新查看结果', key: 'analyze' }); } }, 30000); } catch (err: any) { setAnalyzing(false); message.error({ content: err.response?.data?.detail || '触发分析失败', key: 'analyze' }); } }; if (loading) { return (
); } if (error || !chapter) { return (
); } const hasAnnotations = annotationsData && annotationsData.annotations.length > 0; return (
{/* 顶部工具栏 */}
第{chapter.chapter_number}章: {chapter.title} {hasAnnotations && ( <> } unCheckedChildren={} /> 显示标注 )}
{analyzing && (
正在分析章节...
)} {!analyzing && hasAnnotations && annotationsData && (
共有 {annotationsData.summary.total_annotations} 个标注: {annotationsData.summary.hooks > 0 && ` 🎣${annotationsData.summary.hooks}个钩子`} {annotationsData.summary.foreshadows > 0 && ` 🌟${annotationsData.summary.foreshadows}个伏笔`} {annotationsData.summary.plot_points > 0 && ` 💎${annotationsData.summary.plot_points}个情节点`} {annotationsData.summary.character_events > 0 && ` 👤${annotationsData.summary.character_events}个角色事件`}
)}
{/* 主内容区域 */}
{/* 左侧:章节内容 */}
{!hasAnnotations && ( )} {showAnnotations && hasAnnotations && annotationsData ? ( ) : (
{chapter.content}
)} {/* 底部翻页按钮 */}
{/* 右侧:记忆侧边栏(桌面端) */} {hasAnnotations && annotationsData && window.innerWidth >= 768 && (
)}
{/* 移动端抽屉 */} {hasAnnotations && annotationsData && ( setSidebarVisible(false)} open={sidebarVisible} width="80%" > { handleAnnotationClick(annotation); setSidebarVisible(false); }} /> )}
); }; export default ChapterReader;