2025-11-04 14:38:59 +08:00
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
|
import { Modal, Progress, Spin, Alert, Tabs, Card, Tag, List, Empty, Statistic, Row, Col, Button } from 'antd';
|
2025-11-05 00:11:27 +08:00
|
|
|
|
import {
|
|
|
|
|
|
ThunderboltOutlined,
|
|
|
|
|
|
BulbOutlined,
|
|
|
|
|
|
FireOutlined,
|
2025-11-04 14:38:59 +08:00
|
|
|
|
HeartOutlined,
|
|
|
|
|
|
TeamOutlined,
|
|
|
|
|
|
TrophyOutlined,
|
|
|
|
|
|
CheckCircleOutlined,
|
|
|
|
|
|
ClockCircleOutlined,
|
|
|
|
|
|
CloseCircleOutlined,
|
|
|
|
|
|
ReloadOutlined
|
|
|
|
|
|
} from '@ant-design/icons';
|
|
|
|
|
|
import type { AnalysisTask, ChapterAnalysisResponse } from '../types';
|
|
|
|
|
|
|
2025-11-05 16:22:14 +08:00
|
|
|
|
// 判断是否为移动设备
|
|
|
|
|
|
const isMobileDevice = () => window.innerWidth < 768;
|
|
|
|
|
|
|
2025-11-04 14:38:59 +08:00
|
|
|
|
interface ChapterAnalysisProps {
|
|
|
|
|
|
chapterId: string;
|
|
|
|
|
|
visible: boolean;
|
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function ChapterAnalysis({ chapterId, visible, onClose }: ChapterAnalysisProps) {
|
|
|
|
|
|
const [task, setTask] = useState<AnalysisTask | null>(null);
|
|
|
|
|
|
const [analysis, setAnalysis] = useState<ChapterAnalysisResponse | null>(null);
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
2025-11-05 16:22:14 +08:00
|
|
|
|
const [isMobile, setIsMobile] = useState(isMobileDevice());
|
2025-11-04 14:38:59 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (visible && chapterId) {
|
|
|
|
|
|
fetchAnalysisStatus();
|
|
|
|
|
|
}
|
2025-11-05 00:11:27 +08:00
|
|
|
|
|
2025-11-05 16:22:14 +08:00
|
|
|
|
// 监听窗口大小变化
|
|
|
|
|
|
const handleResize = () => {
|
|
|
|
|
|
setIsMobile(isMobileDevice());
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
|
|
|
2025-11-05 00:11:27 +08:00
|
|
|
|
// 清理函数:组件卸载或关闭时清除轮询
|
|
|
|
|
|
return () => {
|
2025-11-05 16:22:14 +08:00
|
|
|
|
window.removeEventListener('resize', handleResize);
|
2025-11-05 00:11:27 +08:00
|
|
|
|
// 清除可能存在的轮询
|
|
|
|
|
|
};
|
2025-11-04 14:38:59 +08:00
|
|
|
|
}, [visible, chapterId]);
|
|
|
|
|
|
|
|
|
|
|
|
const fetchAnalysisStatus = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`/api/chapters/${chapterId}/analysis/status`);
|
|
|
|
|
|
|
|
|
|
|
|
if (response.status === 404) {
|
|
|
|
|
|
setTask(null);
|
|
|
|
|
|
setError('该章节还未进行分析');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error('获取分析状态失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const taskData: AnalysisTask = await response.json();
|
2025-11-10 21:16:55 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果状态为 none(无任务),设置 task 为 null,让前端显示"开始分析"按钮
|
|
|
|
|
|
if (taskData.status === 'none' || !taskData.has_task) {
|
|
|
|
|
|
setTask(null);
|
|
|
|
|
|
setError(null); // 清除错误,这不是错误状态
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-04 14:38:59 +08:00
|
|
|
|
setTask(taskData);
|
|
|
|
|
|
|
|
|
|
|
|
if (taskData.status === 'completed') {
|
|
|
|
|
|
await fetchAnalysisResult();
|
|
|
|
|
|
} else if (taskData.status === 'running' || taskData.status === 'pending') {
|
|
|
|
|
|
// 开始轮询
|
|
|
|
|
|
startPolling();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError((err as Error).message);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const fetchAnalysisResult = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/chapters/${chapterId}/analysis`);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error('获取分析结果失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
const data: ChapterAnalysisResponse = await response.json();
|
|
|
|
|
|
setAnalysis(data);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError((err as Error).message);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const startPolling = () => {
|
|
|
|
|
|
const pollInterval = setInterval(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/chapters/${chapterId}/analysis/status`);
|
|
|
|
|
|
if (!response.ok) return;
|
|
|
|
|
|
|
|
|
|
|
|
const taskData: AnalysisTask = await response.json();
|
|
|
|
|
|
setTask(taskData);
|
|
|
|
|
|
|
|
|
|
|
|
if (taskData.status === 'completed') {
|
|
|
|
|
|
clearInterval(pollInterval);
|
|
|
|
|
|
await fetchAnalysisResult();
|
|
|
|
|
|
} else if (taskData.status === 'failed') {
|
|
|
|
|
|
clearInterval(pollInterval);
|
|
|
|
|
|
setError(taskData.error_message || '分析失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('轮询错误:', err);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 2000);
|
|
|
|
|
|
|
|
|
|
|
|
// 5分钟超时
|
|
|
|
|
|
setTimeout(() => clearInterval(pollInterval), 300000);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const triggerAnalysis = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`/api/chapters/${chapterId}/analyze`, {
|
|
|
|
|
|
method: 'POST'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
const errorData = await response.json();
|
|
|
|
|
|
throw new Error(errorData.detail || '触发分析失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-05 00:11:27 +08:00
|
|
|
|
// 触发成功后立即关闭Modal,让父组件的状态管理接管
|
|
|
|
|
|
onClose();
|
2025-11-04 14:38:59 +08:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError((err as Error).message);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-05 00:11:27 +08:00
|
|
|
|
|
2025-11-04 14:38:59 +08:00
|
|
|
|
const renderStatusIcon = () => {
|
|
|
|
|
|
if (!task) return null;
|
|
|
|
|
|
|
|
|
|
|
|
switch (task.status) {
|
|
|
|
|
|
case 'pending':
|
|
|
|
|
|
return <ClockCircleOutlined style={{ color: '#faad14' }} />;
|
|
|
|
|
|
case 'running':
|
|
|
|
|
|
return <Spin />;
|
|
|
|
|
|
case 'completed':
|
|
|
|
|
|
return <CheckCircleOutlined style={{ color: '#52c41a' }} />;
|
|
|
|
|
|
case 'failed':
|
|
|
|
|
|
return <CloseCircleOutlined style={{ color: '#ff4d4f' }} />;
|
|
|
|
|
|
default:
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const renderProgress = () => {
|
|
|
|
|
|
if (!task || task.status === 'completed') return null;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div style={{ padding: '24px' }}>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 16 }}>
|
|
|
|
|
|
{renderStatusIcon()}
|
|
|
|
|
|
<span style={{ marginLeft: 8, fontSize: 16 }}>
|
|
|
|
|
|
{task.status === 'pending' && '等待分析...'}
|
|
|
|
|
|
{task.status === 'running' && 'AI正在分析中...'}
|
|
|
|
|
|
{task.status === 'failed' && '分析失败'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Progress
|
|
|
|
|
|
percent={task.progress}
|
|
|
|
|
|
status={task.status === 'failed' ? 'exception' : 'active'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{task.status === 'failed' && task.error_message && (
|
|
|
|
|
|
<Alert
|
|
|
|
|
|
message="分析失败"
|
|
|
|
|
|
description={task.error_message}
|
|
|
|
|
|
type="error"
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
style={{ marginTop: 16 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const renderAnalysisResult = () => {
|
|
|
|
|
|
if (!analysis) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const { analysis: analysis_data, memories } = analysis;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Tabs
|
|
|
|
|
|
defaultActiveKey="overview"
|
|
|
|
|
|
style={{ height: '100%' }}
|
|
|
|
|
|
items={[
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'overview',
|
|
|
|
|
|
label: '概览',
|
|
|
|
|
|
icon: <TrophyOutlined />,
|
|
|
|
|
|
children: (
|
2025-11-05 16:22:14 +08:00
|
|
|
|
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
|
|
|
|
|
|
<Card title="整体评分" style={{ marginBottom: 16 }} size={isMobile ? 'small' : 'default'}>
|
|
|
|
|
|
<Row gutter={isMobile ? 8 : 16}>
|
|
|
|
|
|
<Col span={isMobile ? 12 : 6}>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
<Statistic
|
|
|
|
|
|
title="整体质量"
|
|
|
|
|
|
value={analysis_data.overall_quality_score || 0}
|
|
|
|
|
|
suffix="/ 10"
|
|
|
|
|
|
valueStyle={{ color: '#3f8600' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Col>
|
2025-11-05 16:22:14 +08:00
|
|
|
|
<Col span={isMobile ? 12 : 6}>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
<Statistic
|
|
|
|
|
|
title="节奏把控"
|
|
|
|
|
|
value={analysis_data.pacing_score || 0}
|
|
|
|
|
|
suffix="/ 10"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Col>
|
2025-11-05 16:22:14 +08:00
|
|
|
|
<Col span={isMobile ? 12 : 6}>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
<Statistic
|
|
|
|
|
|
title="吸引力"
|
|
|
|
|
|
value={analysis_data.engagement_score || 0}
|
|
|
|
|
|
suffix="/ 10"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Col>
|
2025-11-05 16:22:14 +08:00
|
|
|
|
<Col span={isMobile ? 12 : 6}>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
<Statistic
|
|
|
|
|
|
title="连贯性"
|
|
|
|
|
|
value={analysis_data.coherence_score || 0}
|
|
|
|
|
|
suffix="/ 10"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
{analysis_data.analysis_report && (
|
2025-11-05 16:22:14 +08:00
|
|
|
|
<Card title="分析摘要" style={{ marginBottom: 16 }} size={isMobile ? 'small' : 'default'}>
|
|
|
|
|
|
<pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit', fontSize: isMobile ? 13 : 14 }}>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
{analysis_data.analysis_report}
|
|
|
|
|
|
</pre>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{analysis_data.suggestions && analysis_data.suggestions.length > 0 && (
|
2025-11-05 16:22:14 +08:00
|
|
|
|
<Card title={<><BulbOutlined /> 改进建议</>} size={isMobile ? 'small' : 'default'}>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
<List
|
|
|
|
|
|
dataSource={analysis_data.suggestions}
|
|
|
|
|
|
renderItem={(item, index) => (
|
|
|
|
|
|
<List.Item>
|
|
|
|
|
|
<span>{index + 1}. {item}</span>
|
|
|
|
|
|
</List.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'hooks',
|
|
|
|
|
|
label: `钩子 (${analysis_data.hooks?.length || 0})`,
|
|
|
|
|
|
icon: <ThunderboltOutlined />,
|
|
|
|
|
|
children: (
|
2025-11-05 16:22:14 +08:00
|
|
|
|
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
|
|
|
|
|
|
<Card size={isMobile ? 'small' : 'default'}>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
{analysis_data.hooks && analysis_data.hooks.length > 0 ? (
|
|
|
|
|
|
<List
|
|
|
|
|
|
dataSource={analysis_data.hooks}
|
|
|
|
|
|
renderItem={(hook) => (
|
|
|
|
|
|
<List.Item>
|
|
|
|
|
|
<List.Item.Meta
|
|
|
|
|
|
title={
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Tag color="blue">{hook.type}</Tag>
|
|
|
|
|
|
<Tag color="orange">{hook.position}</Tag>
|
|
|
|
|
|
<Tag color="red">强度: {hook.strength}/10</Tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
description={hook.content}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</List.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Empty description="暂无钩子" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'foreshadows',
|
|
|
|
|
|
label: `伏笔 (${analysis_data.foreshadows?.length || 0})`,
|
|
|
|
|
|
icon: <FireOutlined />,
|
|
|
|
|
|
children: (
|
2025-11-05 16:22:14 +08:00
|
|
|
|
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
|
|
|
|
|
|
<Card size={isMobile ? 'small' : 'default'}>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
{analysis_data.foreshadows && analysis_data.foreshadows.length > 0 ? (
|
|
|
|
|
|
<List
|
|
|
|
|
|
dataSource={analysis_data.foreshadows}
|
|
|
|
|
|
renderItem={(foreshadow) => (
|
|
|
|
|
|
<List.Item>
|
|
|
|
|
|
<List.Item.Meta
|
|
|
|
|
|
title={
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Tag color={foreshadow.type === 'planted' ? 'green' : 'purple'}>
|
|
|
|
|
|
{foreshadow.type === 'planted' ? '已埋下' : '已回收'}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
<Tag>强度: {foreshadow.strength}/10</Tag>
|
|
|
|
|
|
<Tag>隐藏度: {foreshadow.subtlety}/10</Tag>
|
|
|
|
|
|
{foreshadow.reference_chapter && (
|
|
|
|
|
|
<Tag color="cyan">呼应第{foreshadow.reference_chapter}章</Tag>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
description={foreshadow.content}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</List.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Empty description="暂无伏笔" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'emotion',
|
|
|
|
|
|
label: '情感曲线',
|
|
|
|
|
|
icon: <HeartOutlined />,
|
|
|
|
|
|
children: (
|
2025-11-05 16:22:14 +08:00
|
|
|
|
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
|
|
|
|
|
|
<Card size={isMobile ? 'small' : 'default'}>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
{analysis_data.emotional_tone ? (
|
|
|
|
|
|
<div>
|
2025-11-05 16:22:14 +08:00
|
|
|
|
<Row gutter={isMobile ? 8 : 16} style={{ marginBottom: isMobile ? 16 : 24 }}>
|
|
|
|
|
|
<Col span={isMobile ? 24 : 12}>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
<Statistic
|
|
|
|
|
|
title="主导情绪"
|
|
|
|
|
|
value={analysis_data.emotional_tone}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Col>
|
2025-11-05 16:22:14 +08:00
|
|
|
|
<Col span={isMobile ? 24 : 12}>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
<Statistic
|
|
|
|
|
|
title="情感强度"
|
|
|
|
|
|
value={(analysis_data.emotional_intensity * 10).toFixed(1)}
|
|
|
|
|
|
suffix="/ 10"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
<Card type="inner" title="剧情阶段" size="small">
|
|
|
|
|
|
<p><strong>阶段:</strong>{analysis_data.plot_stage}</p>
|
|
|
|
|
|
<p><strong>冲突等级:</strong>{analysis_data.conflict_level} / 10</p>
|
|
|
|
|
|
{analysis_data.conflict_types && analysis_data.conflict_types.length > 0 && (
|
|
|
|
|
|
<div style={{ marginTop: 8 }}>
|
|
|
|
|
|
<strong>冲突类型:</strong>
|
|
|
|
|
|
{analysis_data.conflict_types.map((type, idx) => (
|
|
|
|
|
|
<Tag key={idx} color="red" style={{ margin: 4 }}>
|
|
|
|
|
|
{type}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Empty description="暂无情感分析" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'characters',
|
|
|
|
|
|
label: `角色 (${analysis_data.character_states?.length || 0})`,
|
|
|
|
|
|
icon: <TeamOutlined />,
|
|
|
|
|
|
children: (
|
2025-11-05 16:22:14 +08:00
|
|
|
|
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
|
|
|
|
|
|
<Card size={isMobile ? 'small' : 'default'}>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
{analysis_data.character_states && analysis_data.character_states.length > 0 ? (
|
|
|
|
|
|
<List
|
|
|
|
|
|
dataSource={analysis_data.character_states}
|
|
|
|
|
|
renderItem={(char) => (
|
|
|
|
|
|
<List.Item>
|
|
|
|
|
|
<Card
|
|
|
|
|
|
type="inner"
|
|
|
|
|
|
title={char.character_name}
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<p><strong>状态变化:</strong>{char.state_before} → {char.state_after}</p>
|
|
|
|
|
|
<p><strong>心理变化:</strong>{char.psychological_change}</p>
|
|
|
|
|
|
<p><strong>关键事件:</strong>{char.key_event}</p>
|
|
|
|
|
|
{char.relationship_changes && Object.keys(char.relationship_changes).length > 0 && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>关系变化:</strong>
|
|
|
|
|
|
{Object.entries(char.relationship_changes).map(([name, change]) => (
|
|
|
|
|
|
<Tag key={name} color="blue" style={{ margin: 4 }}>
|
|
|
|
|
|
与{name}: {change}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</List.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Empty description="暂无角色分析" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'memories',
|
|
|
|
|
|
label: `记忆 (${memories?.length || 0})`,
|
|
|
|
|
|
icon: <FireOutlined />,
|
|
|
|
|
|
children: (
|
2025-11-05 16:22:14 +08:00
|
|
|
|
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
|
|
|
|
|
|
<Card size={isMobile ? 'small' : 'default'}>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
{memories && memories.length > 0 ? (
|
|
|
|
|
|
<List
|
|
|
|
|
|
dataSource={memories}
|
|
|
|
|
|
renderItem={(memory) => (
|
|
|
|
|
|
<List.Item>
|
|
|
|
|
|
<List.Item.Meta
|
|
|
|
|
|
title={
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Tag color="blue">{memory.type}</Tag>
|
|
|
|
|
|
<Tag color="orange">重要性: {memory.importance.toFixed(1)}</Tag>
|
|
|
|
|
|
{memory.is_foreshadow === 1 && <Tag color="green">已埋下伏笔</Tag>}
|
|
|
|
|
|
{memory.is_foreshadow === 2 && <Tag color="purple">已回收伏笔</Tag>}
|
|
|
|
|
|
<span style={{ marginLeft: 8 }}>{memory.title}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
description={
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p>{memory.content}</p>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
{memory.tags.map((tag, idx) => (
|
|
|
|
|
|
<Tag key={idx} style={{ margin: 2 }}>{tag}</Tag>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</List.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Empty description="暂无记忆片段" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title="章节分析"
|
|
|
|
|
|
open={visible}
|
|
|
|
|
|
onCancel={onClose}
|
2025-11-05 16:22:14 +08:00
|
|
|
|
width={isMobile ? '100%' : '90%'}
|
|
|
|
|
|
centered={!isMobile}
|
2025-11-04 14:38:59 +08:00
|
|
|
|
style={{
|
2025-11-05 16:22:14 +08:00
|
|
|
|
maxWidth: isMobile ? '100%' : '1400px',
|
|
|
|
|
|
paddingBottom: 0,
|
|
|
|
|
|
top: isMobile ? 0 : undefined,
|
|
|
|
|
|
margin: isMobile ? 0 : undefined,
|
|
|
|
|
|
maxHeight: isMobile ? '100vh' : undefined
|
2025-11-04 14:38:59 +08:00
|
|
|
|
}}
|
|
|
|
|
|
styles={{
|
|
|
|
|
|
body: {
|
2025-11-05 16:22:14 +08:00
|
|
|
|
padding: isMobile ? '12px' : '24px',
|
|
|
|
|
|
paddingBottom: 0,
|
|
|
|
|
|
maxHeight: isMobile ? 'calc(100vh - 110px)' : undefined,
|
|
|
|
|
|
overflowY: isMobile ? 'auto' : undefined
|
2025-11-04 14:38:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
footer={[
|
2025-11-05 16:22:14 +08:00
|
|
|
|
<Button key="close" onClick={onClose} size={isMobile ? 'small' : 'middle'}>
|
2025-11-04 14:38:59 +08:00
|
|
|
|
关闭
|
|
|
|
|
|
</Button>,
|
2025-11-05 00:11:27 +08:00
|
|
|
|
!task && !loading && (
|
2025-11-04 14:38:59 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
key="analyze"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
icon={<ReloadOutlined />}
|
|
|
|
|
|
onClick={triggerAnalysis}
|
|
|
|
|
|
loading={loading}
|
2025-11-05 16:22:14 +08:00
|
|
|
|
size={isMobile ? 'small' : 'middle'}
|
2025-11-04 14:38:59 +08:00
|
|
|
|
>
|
|
|
|
|
|
开始分析
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
),
|
2025-11-05 00:11:27 +08:00
|
|
|
|
task && (task.status === 'failed') && (
|
2025-11-04 14:38:59 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
key="reanalyze"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
icon={<ReloadOutlined />}
|
|
|
|
|
|
onClick={triggerAnalysis}
|
|
|
|
|
|
loading={loading}
|
2025-11-05 00:11:27 +08:00
|
|
|
|
danger
|
2025-11-05 16:22:14 +08:00
|
|
|
|
size={isMobile ? 'small' : 'middle'}
|
2025-11-05 00:11:27 +08:00
|
|
|
|
>
|
|
|
|
|
|
重新分析
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
),
|
|
|
|
|
|
task && task.status === 'completed' && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
key="reanalyze"
|
|
|
|
|
|
type="default"
|
|
|
|
|
|
icon={<ReloadOutlined />}
|
|
|
|
|
|
onClick={triggerAnalysis}
|
|
|
|
|
|
loading={loading}
|
2025-11-05 16:22:14 +08:00
|
|
|
|
size={isMobile ? 'small' : 'middle'}
|
2025-11-04 14:38:59 +08:00
|
|
|
|
>
|
|
|
|
|
|
重新分析
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)
|
2025-11-05 00:11:27 +08:00
|
|
|
|
].filter(Boolean)}
|
2025-11-04 14:38:59 +08:00
|
|
|
|
>
|
|
|
|
|
|
{loading && !task && (
|
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '48px' }}>
|
|
|
|
|
|
<Spin size="large" />
|
|
|
|
|
|
<p style={{ marginTop: 16 }}>加载中...</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
|
<Alert
|
|
|
|
|
|
message="错误"
|
|
|
|
|
|
description={error}
|
|
|
|
|
|
type="error"
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{task && task.status !== 'completed' && renderProgress()}
|
|
|
|
|
|
{task && task.status === 'completed' && analysis && renderAnalysisResult()}
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|