| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108 |
- "use client";
- import React, { useState, useEffect } from 'react';
- import { Button } from '@/components/ui/button';
- import { ErrorMessage } from '@/components/ui/error-message';
- // 声明全局window属性
- declare global {
- interface Window {
- sseRetryCount?: number;
- }
- }
- // 定义类型
- interface ResearchFormProps {
- className?: string;
- onSubmit?: (data: any) => void;
- }
- // 定义模态框组件的Props接口
- interface PapersListModalProps {
- directionIndex: number;
- directionTitle: string;
- papers: any[];
- onClose: () => void;
- }
- // 论文列表模态框组件 - 只显示论文,不显示研究报告
- function PapersListModal({ directionIndex, directionTitle, papers, onClose }: PapersListModalProps) {
- if (!papers || papers.length === 0) return null;
-
- return (
- <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4 overflow-y-auto">
- <div className="bg-gray-800 rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
- <div className="p-6">
- {/* 标题和关闭按钮 */}
- <div className="flex justify-between items-start mb-6 sticky top-0 bg-gray-800 pb-2 z-10">
- <h3 className="text-xl font-bold text-white">
- 研究方向 {directionIndex + 1}: {directionTitle}
- </h3>
- <button
- onClick={onClose}
- className="bg-gray-700 hover:bg-gray-600 rounded-full p-2 text-white/80 hover:text-white"
- >
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <line x1="18" y1="6" x2="6" y2="18"></line>
- <line x1="6" y1="6" x2="18" y2="18"></line>
- </svg>
- </button>
- </div>
-
- {/* 论文列表部分 */}
- {papers && papers.length > 0 && (
- <div>
- <h4 className="text-lg font-medium text-white mb-3">论文列表 ({papers.length})</h4>
-
- <div className="space-y-4">
- {papers.map((paper: any, index: number) => (
- <div key={index} className="p-4 bg-slate-700/50 rounded-md">
- {/* 论文标题 */}
- <div className="mb-3">
- {paper.link || paper.url ? (
- <a
- href={paper.link || paper.url}
- target="_blank"
- rel="noopener noreferrer"
- className="text-blue-400 hover:text-blue-300 font-medium text-lg hover:underline"
- >
- {paper.title}
- </a>
- ) : (
- <h5 className="text-blue-400 font-medium text-lg">{paper.title}</h5>
- )}
- </div>
-
- {/* 作者信息 */}
- {paper.authors && paper.authors.length > 0 && (
- <div className="mb-3">
- <span className="text-sm text-white/60">作者:</span>
- <p className="text-white/90">{paper.authors.join(', ')}</p>
- </div>
- )}
-
- {/* 年份 */}
- <div className="mb-3">
- <span className="text-sm text-white/60">发表年份:</span>
- <p className="text-white/90">
- {paper.year || (paper.published && paper.published.substring(0, 4)) || "未知"}
- </p>
- </div>
-
- {/* 摘要 */}
- <div className="mb-4">
- <span className="text-sm text-white/60">摘要:</span>
- <div className="text-white/80 text-sm bg-slate-800/70 p-3 rounded-md mt-1">
- {paper.abstract || paper.summary || "此论文暂无摘要"}
- </div>
- </div>
-
- {/* 查看原文按钮 */}
- {(paper.link || paper.url) && (
- <a
- href={paper.link || paper.url}
- target="_blank"
- rel="noopener noreferrer"
- className="inline-block bg-blue-600 hover:bg-blue-700 text-white font-medium py-1.5 px-3 rounded-md transition-colors text-sm"
- >
- 查看原文
- </a>
- )}
- </div>
- ))}
- </div>
- </div>
- )}
- </div>
- </div>
- </div>
- );
- }
- // 定义研究报告模态框Props接口
- interface ReportModalProps {
- directionIndex: number;
- directionTitle: string;
- report: any;
- onClose: () => void;
- }
- // 研究报告模态框组件 - 专门显示研究报告
- function ReportModal({ directionIndex, directionTitle, report, onClose }: ReportModalProps) {
- if (!report) {
- return (
- <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4 overflow-y-auto">
- <div className="bg-gray-800 rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
- <div className="p-6">
- {/* 标题和关闭按钮 */}
- <div className="flex justify-between items-start mb-6 sticky top-0 bg-gray-800 pb-2 z-10">
- <h3 className="text-xl font-bold text-white">
- 研究报告: {directionTitle}
- </h3>
- <button
- onClick={onClose}
- className="bg-gray-700 hover:bg-gray-600 rounded-full p-2 text-white/80 hover:text-white"
- >
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <line x1="18" y1="6" x2="6" y2="18"></line>
- <line x1="6" y1="6" x2="18" y2="18"></line>
- </svg>
- </button>
- </div>
-
- <div className="bg-white text-black p-6 rounded-md">
- <div className="text-center py-8">
- <svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12 mx-auto text-yellow-500 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
- </svg>
- <h3 className="text-xl font-medium text-gray-900 mb-2">无法加载报告</h3>
- <p className="text-gray-600 mb-4">
- 方向 {directionIndex + 1} 的报告数据不可用或连接中断。
- </p>
- <p className="text-gray-500 text-sm mb-6">
- 可能原因: 连接中断、报告生成失败或服务器错误。
- </p>
- <button
- onClick={onClose}
- className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
- >
- 返回
- </button>
- </div>
- </div>
- </div>
- </div>
- </div>
- );
- }
-
- // 状态保存解析后的报告内容
- const [parsedReport, setParsedReport] = useState<string>("");
-
- // 增强错误处理
- useEffect(() => {
- const extractReportContent = () => {
- console.log(`处理方向${directionIndex + 1}的报告数据:`, report);
-
- try {
- if (!report) {
- return `无法加载报告内容。`;
- }
-
- let content = "";
-
- // 处理多种可能的数据格式
- if (typeof report === 'string') {
- // 直接是字符串的情况
- content = report;
- }
- else if (typeof report === 'object') {
- // 1. 有直接内容字段的情况
- if (report.content) {
- content = report.content;
- }
- // 2. 有英文或翻译内容字段的情况
- else if (report.english_content || report.translated_content) {
- content = report.translated_content || report.english_content;
- }
- // 3. 有report子对象的情况
- else if (report.report) {
- if (typeof report.report === 'string') {
- // report字段是字符串
- content = report.report;
- }
- else if (typeof report.report === 'object' && report.report !== null) {
- // report字段是对象,尝试提取其内容
- content = report.report.english_content ||
- report.report.translated_content ||
- report.report.content ||
- JSON.stringify(report.report);
- }
- }
- // 4. 没有找到任何内容字段,尝试寻找最长的字符串字段
- if (!content) {
- let maxLength = 0;
- let bestField = '';
-
- for (const key in report) {
- if (typeof report[key] === 'string' && report[key].length > maxLength) {
- maxLength = report[key].length;
- bestField = key;
- content = report[key];
- }
- }
-
- console.log(`使用最长字符串字段: ${bestField}`);
- }
-
- // 5. 仍然没找到,转为JSON
- if (!content) {
- content = JSON.stringify(report, null, 2);
- }
- }
-
- // 处理特殊字符
- if (content) {
- content = content
- .replace(/\\n/g, '\n')
- .replace(/\\"/g, '"')
- .replace(/\\\\/g, '\\');
- }
-
- return content || `无法解析报告内容。`;
- } catch (error) {
- console.error("解析报告出错:", error);
- return `处理报告时出现错误: ${error}`;
- }
- };
-
- // 解析内容并更新状态
- const content = extractReportContent();
- setParsedReport(content);
- }, [report, directionIndex]);
-
- // 创建HTML格式的报告内容用于下载
- const createHtmlReport = () => {
- // 创建HTML格式
- let htmlContent = `<!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>研究报告: ${directionTitle}</title>
- <style>
- body {
- font-family: 'Arial', sans-serif;
- line-height: 1.6;
- color: #333;
- max-width: 800px;
- margin: 0 auto;
- padding: 20px;
- }
- h1 {
- font-size: 24px;
- margin-top: 30px;
- border-bottom: 1px solid #ddd;
- padding-bottom: 10px;
- }
- h2 {
- font-size: 20px;
- margin-top: 25px;
- }
- p {
- margin: 16px 0;
- }
- .header {
- text-align: center;
- margin-bottom: 40px;
- }
- .content {
- text-align: justify;
- }
- </style>
- </head>
- <body>
- <div class="header">
- <h1>研究报告: ${directionTitle}</h1>
- </div>
- <div class="content">`;
-
- // 转换文本内容为HTML格式
- // 替换标题
- let content = parsedReport
- .replace(/\*\*(\d+)\.\s+([^\*]+)\*\*/g, '<h1>$1. $2</h1>')
- // 替换小节标题
- .replace(/\*\*(\d+\.\d+)\s+([^\*]+)\*\*/g, '<h2>$1 $2</h2>')
- // 替换段落
- .split('\n\n').join('</p><p>');
-
- htmlContent += `<p>${content}</p>`;
- htmlContent += `
- </div>
- </body>
- </html>`;
-
- return htmlContent;
- };
-
- // 创建下载链接
- const createDownloadLink = () => {
- const htmlContent = createHtmlReport();
- const blob = new Blob([htmlContent], { type: 'text/html' });
- const url = URL.createObjectURL(blob);
-
- return {
- url,
- filename: `研究报告_${directionTitle.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_')}.html`,
- cleanup: () => URL.revokeObjectURL(url)
- };
- };
-
- // 下载链接状态
- const [downloadLink, setDownloadLink] = useState<{
- url: string;
- filename: string;
- cleanup: () => void;
- } | null>(null);
-
- // 创建下载链接
- useEffect(() => {
- if (parsedReport) {
- const link = createDownloadLink();
- setDownloadLink(link);
-
- // 清理函数
- return () => {
- if (link && link.cleanup) link.cleanup();
- };
- }
- }, [parsedReport, directionTitle]);
-
- // 直接显示原始内容以便调试
- const showRawContent = () => {
- console.log("显示原始内容");
- alert(JSON.stringify(report, null, 2));
- };
-
- return (
- <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4 overflow-y-auto">
- <div className="bg-gray-800 rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
- <div className="p-6">
- {/* 标题和关闭按钮 */}
- <div className="flex justify-between items-start mb-6 sticky top-0 bg-gray-800 pb-2 z-10">
- <h3 className="text-xl font-bold text-white">
- 研究报告: {directionTitle}
- </h3>
- <button
- onClick={onClose}
- className="bg-gray-700 hover:bg-gray-600 rounded-full p-2 text-white/80 hover:text-white"
- >
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <line x1="18" y1="6" x2="6" y2="18"></line>
- <line x1="6" y1="6" x2="18" y2="18"></line>
- </svg>
- </button>
- </div>
-
- {/* 下载和调试按钮 */}
- <div className="mb-4 flex gap-2">
- {downloadLink && (
- <a
- href={downloadLink.url}
- download={downloadLink.filename}
- className="inline-flex items-center gap-2 bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
- >
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
- <polyline points="7 10 12 15 17 10"></polyline>
- <line x1="12" y1="15" x2="12" y2="3"></line>
- </svg>
- 下载HTML格式报告
- </a>
- )}
-
- <button
- onClick={showRawContent}
- className="inline-flex items-center gap-2 bg-gray-600 hover:bg-gray-700 text-white font-medium py-2 px-4 rounded-md transition-colors text-sm"
- >
- 查看原始数据
- </button>
- </div>
-
- {/* 研究报告内容 */}
- <div className="bg-white text-black p-6 rounded-md prose max-w-none">
- {/* 如果解析出的内容为空,显示待处理信息 */}
- {!parsedReport && (
- <div className="text-center py-8 text-gray-500">
- <svg className="animate-spin h-8 w-8 mx-auto mb-4 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
- <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
- </svg>
- <p>正在处理报告内容...</p>
- </div>
- )}
-
- {/* 显示解析后的内容,使用预格式化文本保留格式 */}
- {parsedReport && (
- <div
- className="whitespace-pre-wrap"
- dangerouslySetInnerHTML={{
- __html: parsedReport
- .replace(/\*\*(\d+)\.\s+([^\*]+)\*\*/g, '<h2 class="text-xl font-bold mt-6 mb-4">$1. $2</h2>')
- .replace(/\*\*(\d+\.\d+)\s+([^\*]+)\*\*/g, '<h3 class="text-lg font-bold mt-5 mb-3">$1 $2</h3>')
- .replace(/\n\n/g, '<br/><br/>')
- }}
- ></div>
- )}
- </div>
- </div>
- </div>
- </div>
- );
- }
- export function ResearchForm({ className = "", onSubmit }: ResearchFormProps) {
- // 基本状态
- const [researchTopic, setResearchTopic] = useState('');
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState<string | null>(null);
- const [processMessage, setProcessMessage] = useState('');
- const [researchCompleted, setResearchCompleted] = useState(false); // 新增:标记研究是否已完成
-
- // 研究数据
- const [keywordsData, setKeywordsData] = useState<any>(null);
- const [directionsData, setDirectionsData] = useState<any>(null);
- const [papersData, setPapersData] = useState<any[]>([]);
- const [reportsData, setReportsData] = useState<any[]>([]);
- const [researchData, setResearchData] = useState<any>(null);
-
- // 模态框状态
- const [showPapersModal, setShowPapersModal] = useState<boolean>(false);
- const [showReportModal, setShowReportModal] = useState<boolean>(false);
- const [selectedDirectionIndex, setSelectedDirectionIndex] = useState<number | null>(null);
-
- // 添加新函数,用于开始新的研究
- const startNewResearch = () => {
- // 重置所有状态
- setResearchTopic('');
- setKeywordsData(null);
- setDirectionsData(null);
- setPapersData([]);
- setReportsData([]);
- setResearchData(null);
- setProcessMessage('');
- setError(null);
- setResearchCompleted(false);
-
- // 重置全局重试计数
- if (window.sseRetryCount) {
- window.sseRetryCount = 0;
- }
- };
-
- // 表单提交
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault();
-
- if (!researchTopic.trim()) {
- setError('请输入研究主题');
- return;
- }
-
- // 如果已经有研究完成,先重置状态
- if (researchCompleted) {
- startNewResearch();
- // 延迟设置新主题并提交,确保状态重置完成
- setTimeout(() => {
- const newTopic = researchTopic; // 保存当前主题
- const newEvent = new Event('submit') as React.FormEvent;
- setResearchTopic(newTopic);
- handleSubmit(newEvent);
- }, 10);
- return;
- }
-
- setLoading(true);
- setError(null);
- setProcessMessage('正在连接服务器...');
-
- // 创建EventSource连接
- try {
- const safeResearchTopic = encodeURIComponent(researchTopic.trim());
- const url = `/api/research/enhanced-process-stream?research_intent=${safeResearchTopic}`;
-
- const eventSource = new EventSource(url);
-
- // 连接事件处理
- eventSource.onopen = () => {
- setProcessMessage('连接成功,开始处理研究内容...');
- };
-
- // 消息事件处理
- eventSource.onmessage = (event) => {
- try {
- const data = JSON.parse(event.data);
- console.log("收到事件数据:", data);
-
- // 更新处理消息
- if (data.message) {
- setProcessMessage(data.message);
- }
-
- // 处理不同类型的事件
- switch(data.status) {
- case 'base_keywords_extracted':
- if (data.data && data.data.keywords) {
- setKeywordsData(data.data);
- console.log("关键词数据:", data.data);
- }
- break;
-
- case 'research_topics_generated':
- if (data.data && data.data.research_topics) {
- setDirectionsData(data.data);
- console.log("研究方向数据:", data.data);
- }
- break;
-
- case 'papers_ready':
- if (data.data) {
- console.log("论文数据:", data.data);
- setPapersData(prev => {
- const newData = [...prev];
- if (data.data.direction_index !== undefined) {
- newData[data.data.direction_index] = data.data;
- }
- return newData;
- });
- }
- break;
-
- case 'report_ready':
- if (data.data) {
- console.log("报告数据:", data.data);
-
- // 使用函数式更新确保报告数据正确合并
- setReportsData((prevReports: any[]) => {
- const newReports = [...prevReports];
- if (data.data.direction_index !== undefined) {
- newReports[data.data.direction_index] = data.data;
- }
- return newReports;
- });
-
- // 如果是最后一个报告已生成,标记研究为完成状态
- if (data.data.direction_index === 4 ||
- (directionsData?.research_topics &&
- data.data.direction_index === directionsData.research_topics.length - 1)) {
-
- console.log("最后一个报告已生成,研究完成");
-
- // 使用setTimeout确保报告数据先被更新完成
- setTimeout(() => {
- setResearchData({
- research_intent: researchTopic,
- keywords: keywordsData?.keywords || [],
- directions: directionsData?.research_topics || [],
- papers_by_direction: papersData,
- reports: reportsData // 直接使用当前状态
- });
-
- setProcessMessage("研究已完成,您可以查看各个方向的报告和论文");
- setResearchCompleted(true);
- eventSource.close();
- setLoading(false);
- }, 100);
- }
- }
- break;
-
- case 'completed':
- setResearchData({
- research_intent: researchTopic,
- keywords: keywordsData?.keywords || [],
- directions: directionsData?.research_topics || [],
- papers_by_direction: papersData,
- reports: reportsData
- });
- setProcessMessage("研究已完成,您可以查看各个方向的报告和论文");
- setResearchCompleted(true);
- eventSource.close();
- setLoading(false);
- break;
-
- case 'error':
- setError(data.message || "处理过程中发生错误");
- eventSource.close();
- setLoading(false);
- break;
- }
- } catch (error) {
- console.error("处理事件数据时出错:", error);
- }
- };
-
- // 错误处理 - 增强版
- eventSource.onerror = (error: Event) => {
- console.error("与研究服务连接失败:", error);
-
- // 重连尝试计数
- if (!window.sseRetryCount) {
- window.sseRetryCount = 0;
- }
-
- // 检查是否已完成大部分研究 (即已经生成了最后一个方向的报告)
- const isLastReportGenerated = reportsData.length > 0 &&
- directionsData?.research_topics &&
- reportsData.length >= directionsData.research_topics.length;
-
- // 如果已经生成了最后一个报告,直接标记为完成
- if (isLastReportGenerated) {
- console.log("所有报告都已生成,标记研究完成");
-
- // 构建最终研究数据
- setResearchData({
- research_intent: researchTopic,
- keywords: keywordsData?.keywords || [],
- directions: directionsData?.research_topics || [],
- papers_by_direction: papersData,
- reports: reportsData
- });
-
- // 显示完成消息
- setProcessMessage("研究已完成,您可以查看各个方向的报告和论文");
- setResearchCompleted(true);
- setLoading(false);
-
- // 确保关闭连接
- try {
- eventSource.close();
- } catch (e) {
- // 忽略关闭错误
- }
- return;
- }
-
- // 增加重连机制 - 但只在未生成足够报告时尝试
- if (window.sseRetryCount < 3) {
- console.log(`尝试重新连接 (${window.sseRetryCount + 1}/3)...`);
- window.sseRetryCount++;
-
- // 关闭当前连接
- try {
- eventSource.close();
- } catch (e) {
- // 忽略关闭错误
- }
-
- // 等待1秒后重连
- setTimeout(() => {
- const newUrl = `/api/research/enhanced-process-stream?research_intent=${safeResearchTopic}`;
- const newEventSource = new EventSource(newUrl);
-
- // 设置新的事件监听器
- // ...复制原来的所有事件监听器...
-
- setProcessMessage(`尝试重新连接 (${window.sseRetryCount}/3)...`);
- }, 1000);
-
- return;
- }
-
- // 检查处理是否已基本完成
- const hasCompletedMostWork = reportsData.some(report => report && (report.report || report.content));
-
- if (hasCompletedMostWork) {
- console.log("尽管连接中断,但大部分报告已生成,尝试恢复...");
-
- // 所有研究方向都有数据了,认为是基本完成
- if (reportsData.length >= directionsData?.research_topics?.length) {
- console.log("所有报告都已生成,手动完成流程");
-
- // 构建最终研究数据
- setResearchData({
- research_intent: researchTopic,
- keywords: keywordsData?.keywords || [],
- directions: directionsData?.research_topics || [],
- papers_by_direction: papersData,
- reports: reportsData
- });
-
- // 显示提示但继续
- setProcessMessage("研究已完成,您可以查看各个方向的报告和论文");
- setResearchCompleted(true);
- setLoading(false);
- } else {
- // 还有部分未完成,给出警告
- setError("与研究服务的连接中断,但部分报告已生成。您可以查看现有结果,或开始新的研究。");
- setResearchCompleted(true);
- setLoading(false);
- }
- } else {
- // 完全失败的情况
- setError("与研究服务的连接中断,可能原因:1) 后端服务未运行 2) 网络连接问题 3) 服务器错误");
- setProcessMessage("您可以开始新的研究,或联系管理员获取帮助。");
- setLoading(false);
- }
-
- // 确保关闭连接
- try {
- eventSource.close();
- } catch (e) {
- // 忽略关闭错误
- }
- };
-
- } catch (error) {
- console.error("创建EventSource失败:", error);
- setError("无法连接到研究服务");
- setLoading(false);
- }
- };
-
- // 取消处理
- const handleCancel = () => {
- setLoading(false);
- };
-
- // 打开论文列表模态框 - 修复论文模态框打开问题
- const openPapersModal = (directionIndex: number) => {
- console.log("打开论文模态框,方向索引:", directionIndex);
- setSelectedDirectionIndex(directionIndex);
- setShowPapersModal(true);
- setShowReportModal(false);
- };
-
- // 关闭论文列表模态框
- const closePapersModal = () => {
- setShowPapersModal(false);
- setSelectedDirectionIndex(null);
- };
-
- // 打开研究报告模态框 - 增加日志以便调试
- const openReportModal = (directionIndex: number) => {
- console.log("打开报告模态框,方向索引:", directionIndex);
- console.log("报告数据:", reportsData[directionIndex]);
- setSelectedDirectionIndex(directionIndex);
- setShowReportModal(true);
- setShowPapersModal(false);
- };
-
- // 关闭研究报告模态框
- const closeReportModal = () => {
- setShowReportModal(false);
- setSelectedDirectionIndex(null);
- };
- return (
- <div className={className}>
- {/* 标题和说明 */}
- <div className="mb-6">
- <h2 className="text-2xl font-bold text-white mb-2 text-center">研究助手</h2>
- </div>
- {/* 研究表单 */}
- <form onSubmit={handleSubmit} className="space-y-4">
- {/* 研究主题输入 */}
- <div className="space-y-2">
- <label htmlFor="research-topic" className="block text-sm font-medium text-white">
- 研究主题
- </label>
- <textarea
- id="research-topic"
- placeholder="输入您想研究的主题,例如:新能源电池技术的最新发展及应用前景"
- className="w-full h-24 px-3 py-2 text-white bg-white/5 border border-white/10 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
- value={researchTopic}
- onChange={(e) => setResearchTopic(e.target.value)}
- disabled={loading}
- />
- </div>
- {/* 错误信息显示 */}
- {error && <ErrorMessage message={error} onDismiss={() => setError(null)} />}
- {/* 提交按钮 - 居中显示 */}
- <div className="flex justify-center mt-4">
- <Button
- type="submit"
- disabled={!researchTopic.trim() || loading}
- className={`${
- loading ? 'bg-blue-600/50' : 'bg-blue-600 hover:bg-blue-700'
- } text-white font-medium py-2 px-4 rounded-md transition-colors`}
- >
- {loading ? "研究进行中..." : researchCompleted ? "开始新研究" : "开始研究"}
- </Button>
- {loading && (
- <button
- type="button"
- onClick={handleCancel}
- className="ml-2 text-white/60 hover:text-white text-sm"
- >
- 取消
- </button>
- )}
- </div>
- </form>
- {/* 处理中状态显示 */}
- {loading && (
- <div className="mt-8 space-y-6">
- {/* 状态消息 */}
- <div className="p-4 bg-blue-900/30 rounded-lg border border-blue-800/50">
- <div className="flex items-center">
- <div className="mr-3">
- <svg className="animate-spin h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
- <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
- </svg>
- </div>
- <div>
- <p className="font-medium text-white">研究进行中...</p>
- <p className="text-sm text-white/80">{processMessage}</p>
- </div>
- </div>
- </div>
- {/* 关键词显示 */}
- {keywordsData && keywordsData.keywords && keywordsData.keywords.length > 0 && (
- <div className="p-4 bg-white/5 rounded-lg">
- <h3 className="text-sm font-medium text-white mb-2">提取到的关键词:</h3>
- <div className="flex flex-wrap gap-2">
- {keywordsData.keywords.map((keyword: string, index: number) => (
- <span
- key={index}
- className="px-3 py-1 bg-blue-500/20 rounded-full text-blue-300"
- >
- {keyword}
- </span>
- ))}
- </div>
- </div>
- )}
- {/* 研究方向显示 */}
- {directionsData && directionsData.research_topics && directionsData.research_topics.length > 0 && (
- <div className="p-4 bg-white/5 rounded-lg">
- <h3 className="text-sm font-medium text-white mb-2">生成的研究方向:</h3>
- <div className="space-y-2">
- {directionsData.research_topics.map((topic: any, index: number) => (
- <div key={index} className="p-2 bg-white/10 rounded-md">
- <p className="font-medium text-white">
- {index + 1}. {topic.english_title || topic.title}
- </p>
- {topic.keywords && topic.keywords.length > 0 && (
- <div className="mt-1 flex flex-wrap gap-1">
- {topic.keywords.map((kw: string, kidx: number) => (
- <span key={kidx} className="px-1.5 py-0.5 text-xs bg-white/10 rounded-full text-white/70">
- {kw}
- </span>
- ))}
- </div>
- )}
- </div>
- ))}
- </div>
- </div>
- )}
-
- {/* 已检索的论文和报告 */}
- {(papersData.length > 0 || reportsData.length > 0) && (
- <div className="p-4 bg-white/5 rounded-lg">
- <h3 className="text-sm font-medium text-white mb-2">研究进展:</h3>
- <div className="space-y-3">
- {directionsData && directionsData.research_topics && directionsData.research_topics.map((topic: any, index: number) => {
- const hasReport = reportsData[index] && (reportsData[index].report || reportsData[index].content);
- const hasPapers = papersData[index] && papersData[index].papers && papersData[index].papers.length > 0;
-
- if (!hasReport && !hasPapers) return null;
-
- return (
- <div key={index} className="p-2 bg-white/10 rounded-md">
- <p className="font-medium text-white mb-1">
- 方向 {index + 1}: {topic.english_title || topic.title}
- </p>
-
- {hasReport && (
- <div className="ml-2 text-sm text-white/80 mb-1 flex items-center">
- <span className="text-green-400 mr-1">✓</span> 研究报告已生成
- <button
- onClick={() => openReportModal(index)}
- className="ml-2 text-xs text-blue-400 hover:text-blue-300"
- >
- 查看报告
- </button>
- </div>
- )}
-
- {hasPapers && (
- <div className="ml-2 text-sm text-white/80 flex items-center">
- <span className="text-green-400 mr-1">✓</span> 已找到 {papersData[index].papers.length} 篇相关论文
- <button
- onClick={() => openPapersModal(index)}
- className="ml-2 text-xs text-blue-400 hover:text-blue-300"
- >
- 查看论文
- </button>
- </div>
- )}
- </div>
- );
- })}
- </div>
- </div>
- )}
- </div>
- )}
- {/* 研究完成后状态显示 */}
- {researchCompleted && !loading && (
- <div className="mt-8 space-y-6">
- {/* 完成状态标志 */}
- <div className="p-4 bg-green-900/30 rounded-lg border border-green-800/50">
- <div className="flex items-center">
- <div className="mr-3">
- <svg className="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
- <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
- </svg>
- </div>
- <div>
- <p className="font-medium text-white">研究完成</p>
- <p className="text-sm text-white/80">{processMessage}</p>
- </div>
- </div>
- </div>
- {/* 研究结果 */}
- <div>
- <h3 className="text-xl font-bold text-white">研究结果</h3>
-
- {/* 关键词部分 */}
- {keywordsData && keywordsData.keywords && keywordsData.keywords.length > 0 && (
- <div className="p-4 bg-white/5 rounded-lg mt-4">
- <h4 className="text-lg font-medium text-white mb-2">关键词</h4>
- <div className="flex flex-wrap gap-2">
- {keywordsData.keywords.map((keyword: string, index: number) => (
- <span
- key={index}
- className="px-3 py-1 bg-blue-500/20 rounded-full text-blue-300"
- >
- {keyword}
- </span>
- ))}
- </div>
- </div>
- )}
-
- {/* 研究报告部分 */}
- {directionsData && directionsData.research_topics && directionsData.research_topics.length > 0 && (
- <div className="space-y-6 mt-4">
- <h4 className="text-lg font-medium text-white">研究报告</h4>
-
- {directionsData.research_topics.map((direction: any, index: number) => {
- // 检查该方向是否有报告数据
- const hasReport = reportsData && reportsData[index];
-
- if (!hasReport) return null; // 没有报告数据则跳过显示
-
- return (
- <div key={index} className="p-4 bg-white/5 rounded-lg">
- <h5 className="text-md font-medium text-white mb-3">
- {index + 1}. {direction.english_title || direction.title}
- </h5>
-
- {/* 方向的关键词 */}
- {direction.keywords && direction.keywords.length > 0 && (
- <div className="mb-4">
- <div className="text-sm text-white/60 mb-1">关键词:</div>
- <div className="flex flex-wrap gap-1">
- {direction.keywords.map((kw: string, kidx: number) => (
- <span key={kidx} className="px-2 py-0.5 text-xs bg-white/10 rounded-full text-white/80">
- {kw}
- </span>
- ))}
- </div>
- </div>
- )}
-
- {/* 研究报告与论文分开显示,分别用按钮打开不同模态框 */}
- <div className="flex flex-wrap gap-4 mb-4">
- {/* 查看研究报告按钮 */}
- <button
- onClick={() => openReportModal(index)}
- className="inline-flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
- >
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
- <polyline points="14 2 14 8 20 8"></polyline>
- <line x1="16" y1="13" x2="8" y2="13"></line>
- <line x1="16" y1="17" x2="8" y2="17"></line>
- <polyline points="10 9 9 9 8 9"></polyline>
- </svg>
- 查看研究报告
- </button>
-
- {/* 查看论文列表按钮 */}
- {papersData[index] && papersData[index].papers && papersData[index].papers.length > 0 && (
- <button
- onClick={() => openPapersModal(index)}
- className="inline-flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
- >
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
- <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
- <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
- </svg>
- 查看相关论文 ({papersData[index].papers.length})
- </button>
- )}
- </div>
-
- {/* 论文列表预览 - 仅显示标题 */}
- {papersData[index] && papersData[index].papers && papersData[index].papers.length > 0 && (
- <div>
- <div className="text-sm font-medium text-white/80 mb-2">
- 论文预览:
- </div>
-
- <div className="space-y-1.5 mb-2">
- {papersData[index].papers.slice(0, 2).map((paper: any, pidx: number) => (
- <a
- key={pidx}
- href={paper.link || paper.url}
- target="_blank"
- rel="noopener noreferrer"
- className="text-blue-400 hover:text-blue-300 hover:underline block"
- >
- {paper.title}
- </a>
- ))}
- {papersData[index].papers.length > 2 && (
- <button
- onClick={() => openPapersModal(index)}
- className="text-xs text-blue-400 hover:text-blue-300 mt-1"
- >
- 查看更多...
- </button>
- )}
- </div>
- </div>
- )}
- </div>
- );
- })}
- </div>
- )}
- </div>
- </div>
- )}
- {/* 论文列表模态框 */}
- {showPapersModal && selectedDirectionIndex !== null && (
- <PapersListModal
- directionIndex={selectedDirectionIndex}
- directionTitle={
- (directionsData?.research_topics?.[selectedDirectionIndex]?.english_title ||
- directionsData?.research_topics?.[selectedDirectionIndex]?.title) ||
- `研究方向 ${selectedDirectionIndex + 1}`
- }
- papers={papersData[selectedDirectionIndex]?.papers || []}
- onClose={closePapersModal}
- />
- )}
- {/* 研究报告模态框 */}
- {showReportModal && selectedDirectionIndex !== null && (
- <ReportModal
- directionIndex={selectedDirectionIndex}
- directionTitle={
- (directionsData?.research_topics?.[selectedDirectionIndex]?.english_title ||
- directionsData?.research_topics?.[selectedDirectionIndex]?.title) ||
- `研究方向 ${selectedDirectionIndex + 1}`
- }
- report={reportsData[selectedDirectionIndex]}
- onClose={closeReportModal}
- />
- )}
- </div>
- );
- }
|