research-form.tsx 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108
  1. "use client";
  2. import React, { useState, useEffect } from 'react';
  3. import { Button } from '@/components/ui/button';
  4. import { ErrorMessage } from '@/components/ui/error-message';
  5. // 声明全局window属性
  6. declare global {
  7. interface Window {
  8. sseRetryCount?: number;
  9. }
  10. }
  11. // 定义类型
  12. interface ResearchFormProps {
  13. className?: string;
  14. onSubmit?: (data: any) => void;
  15. }
  16. // 定义模态框组件的Props接口
  17. interface PapersListModalProps {
  18. directionIndex: number;
  19. directionTitle: string;
  20. papers: any[];
  21. onClose: () => void;
  22. }
  23. // 论文列表模态框组件 - 只显示论文,不显示研究报告
  24. function PapersListModal({ directionIndex, directionTitle, papers, onClose }: PapersListModalProps) {
  25. if (!papers || papers.length === 0) return null;
  26. return (
  27. <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4 overflow-y-auto">
  28. <div className="bg-gray-800 rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
  29. <div className="p-6">
  30. {/* 标题和关闭按钮 */}
  31. <div className="flex justify-between items-start mb-6 sticky top-0 bg-gray-800 pb-2 z-10">
  32. <h3 className="text-xl font-bold text-white">
  33. 研究方向 {directionIndex + 1}: {directionTitle}
  34. </h3>
  35. <button
  36. onClick={onClose}
  37. className="bg-gray-700 hover:bg-gray-600 rounded-full p-2 text-white/80 hover:text-white"
  38. >
  39. <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">
  40. <line x1="18" y1="6" x2="6" y2="18"></line>
  41. <line x1="6" y1="6" x2="18" y2="18"></line>
  42. </svg>
  43. </button>
  44. </div>
  45. {/* 论文列表部分 */}
  46. {papers && papers.length > 0 && (
  47. <div>
  48. <h4 className="text-lg font-medium text-white mb-3">论文列表 ({papers.length})</h4>
  49. <div className="space-y-4">
  50. {papers.map((paper: any, index: number) => (
  51. <div key={index} className="p-4 bg-slate-700/50 rounded-md">
  52. {/* 论文标题 */}
  53. <div className="mb-3">
  54. {paper.link || paper.url ? (
  55. <a
  56. href={paper.link || paper.url}
  57. target="_blank"
  58. rel="noopener noreferrer"
  59. className="text-blue-400 hover:text-blue-300 font-medium text-lg hover:underline"
  60. >
  61. {paper.title}
  62. </a>
  63. ) : (
  64. <h5 className="text-blue-400 font-medium text-lg">{paper.title}</h5>
  65. )}
  66. </div>
  67. {/* 作者信息 */}
  68. {paper.authors && paper.authors.length > 0 && (
  69. <div className="mb-3">
  70. <span className="text-sm text-white/60">作者:</span>
  71. <p className="text-white/90">{paper.authors.join(', ')}</p>
  72. </div>
  73. )}
  74. {/* 年份 */}
  75. <div className="mb-3">
  76. <span className="text-sm text-white/60">发表年份:</span>
  77. <p className="text-white/90">
  78. {paper.year || (paper.published && paper.published.substring(0, 4)) || "未知"}
  79. </p>
  80. </div>
  81. {/* 摘要 */}
  82. <div className="mb-4">
  83. <span className="text-sm text-white/60">摘要:</span>
  84. <div className="text-white/80 text-sm bg-slate-800/70 p-3 rounded-md mt-1">
  85. {paper.abstract || paper.summary || "此论文暂无摘要"}
  86. </div>
  87. </div>
  88. {/* 查看原文按钮 */}
  89. {(paper.link || paper.url) && (
  90. <a
  91. href={paper.link || paper.url}
  92. target="_blank"
  93. rel="noopener noreferrer"
  94. 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"
  95. >
  96. 查看原文
  97. </a>
  98. )}
  99. </div>
  100. ))}
  101. </div>
  102. </div>
  103. )}
  104. </div>
  105. </div>
  106. </div>
  107. );
  108. }
  109. // 定义研究报告模态框Props接口
  110. interface ReportModalProps {
  111. directionIndex: number;
  112. directionTitle: string;
  113. report: any;
  114. onClose: () => void;
  115. }
  116. // 研究报告模态框组件 - 专门显示研究报告
  117. function ReportModal({ directionIndex, directionTitle, report, onClose }: ReportModalProps) {
  118. if (!report) {
  119. return (
  120. <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4 overflow-y-auto">
  121. <div className="bg-gray-800 rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
  122. <div className="p-6">
  123. {/* 标题和关闭按钮 */}
  124. <div className="flex justify-between items-start mb-6 sticky top-0 bg-gray-800 pb-2 z-10">
  125. <h3 className="text-xl font-bold text-white">
  126. 研究报告: {directionTitle}
  127. </h3>
  128. <button
  129. onClick={onClose}
  130. className="bg-gray-700 hover:bg-gray-600 rounded-full p-2 text-white/80 hover:text-white"
  131. >
  132. <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">
  133. <line x1="18" y1="6" x2="6" y2="18"></line>
  134. <line x1="6" y1="6" x2="18" y2="18"></line>
  135. </svg>
  136. </button>
  137. </div>
  138. <div className="bg-white text-black p-6 rounded-md">
  139. <div className="text-center py-8">
  140. <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">
  141. <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" />
  142. </svg>
  143. <h3 className="text-xl font-medium text-gray-900 mb-2">无法加载报告</h3>
  144. <p className="text-gray-600 mb-4">
  145. 方向 {directionIndex + 1} 的报告数据不可用或连接中断。
  146. </p>
  147. <p className="text-gray-500 text-sm mb-6">
  148. 可能原因: 连接中断、报告生成失败或服务器错误。
  149. </p>
  150. <button
  151. onClick={onClose}
  152. className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
  153. >
  154. 返回
  155. </button>
  156. </div>
  157. </div>
  158. </div>
  159. </div>
  160. </div>
  161. );
  162. }
  163. // 状态保存解析后的报告内容
  164. const [parsedReport, setParsedReport] = useState<string>("");
  165. // 增强错误处理
  166. useEffect(() => {
  167. const extractReportContent = () => {
  168. console.log(`处理方向${directionIndex + 1}的报告数据:`, report);
  169. try {
  170. if (!report) {
  171. return `无法加载报告内容。`;
  172. }
  173. let content = "";
  174. // 处理多种可能的数据格式
  175. if (typeof report === 'string') {
  176. // 直接是字符串的情况
  177. content = report;
  178. }
  179. else if (typeof report === 'object') {
  180. // 1. 有直接内容字段的情况
  181. if (report.content) {
  182. content = report.content;
  183. }
  184. // 2. 有英文或翻译内容字段的情况
  185. else if (report.english_content || report.translated_content) {
  186. content = report.translated_content || report.english_content;
  187. }
  188. // 3. 有report子对象的情况
  189. else if (report.report) {
  190. if (typeof report.report === 'string') {
  191. // report字段是字符串
  192. content = report.report;
  193. }
  194. else if (typeof report.report === 'object' && report.report !== null) {
  195. // report字段是对象,尝试提取其内容
  196. content = report.report.english_content ||
  197. report.report.translated_content ||
  198. report.report.content ||
  199. JSON.stringify(report.report);
  200. }
  201. }
  202. // 4. 没有找到任何内容字段,尝试寻找最长的字符串字段
  203. if (!content) {
  204. let maxLength = 0;
  205. let bestField = '';
  206. for (const key in report) {
  207. if (typeof report[key] === 'string' && report[key].length > maxLength) {
  208. maxLength = report[key].length;
  209. bestField = key;
  210. content = report[key];
  211. }
  212. }
  213. console.log(`使用最长字符串字段: ${bestField}`);
  214. }
  215. // 5. 仍然没找到,转为JSON
  216. if (!content) {
  217. content = JSON.stringify(report, null, 2);
  218. }
  219. }
  220. // 处理特殊字符
  221. if (content) {
  222. content = content
  223. .replace(/\\n/g, '\n')
  224. .replace(/\\"/g, '"')
  225. .replace(/\\\\/g, '\\');
  226. }
  227. return content || `无法解析报告内容。`;
  228. } catch (error) {
  229. console.error("解析报告出错:", error);
  230. return `处理报告时出现错误: ${error}`;
  231. }
  232. };
  233. // 解析内容并更新状态
  234. const content = extractReportContent();
  235. setParsedReport(content);
  236. }, [report, directionIndex]);
  237. // 创建HTML格式的报告内容用于下载
  238. const createHtmlReport = () => {
  239. // 创建HTML格式
  240. let htmlContent = `<!DOCTYPE html>
  241. <html lang="en">
  242. <head>
  243. <meta charset="UTF-8">
  244. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  245. <title>研究报告: ${directionTitle}</title>
  246. <style>
  247. body {
  248. font-family: 'Arial', sans-serif;
  249. line-height: 1.6;
  250. color: #333;
  251. max-width: 800px;
  252. margin: 0 auto;
  253. padding: 20px;
  254. }
  255. h1 {
  256. font-size: 24px;
  257. margin-top: 30px;
  258. border-bottom: 1px solid #ddd;
  259. padding-bottom: 10px;
  260. }
  261. h2 {
  262. font-size: 20px;
  263. margin-top: 25px;
  264. }
  265. p {
  266. margin: 16px 0;
  267. }
  268. .header {
  269. text-align: center;
  270. margin-bottom: 40px;
  271. }
  272. .content {
  273. text-align: justify;
  274. }
  275. </style>
  276. </head>
  277. <body>
  278. <div class="header">
  279. <h1>研究报告: ${directionTitle}</h1>
  280. </div>
  281. <div class="content">`;
  282. // 转换文本内容为HTML格式
  283. // 替换标题
  284. let content = parsedReport
  285. .replace(/\*\*(\d+)\.\s+([^\*]+)\*\*/g, '<h1>$1. $2</h1>')
  286. // 替换小节标题
  287. .replace(/\*\*(\d+\.\d+)\s+([^\*]+)\*\*/g, '<h2>$1 $2</h2>')
  288. // 替换段落
  289. .split('\n\n').join('</p><p>');
  290. htmlContent += `<p>${content}</p>`;
  291. htmlContent += `
  292. </div>
  293. </body>
  294. </html>`;
  295. return htmlContent;
  296. };
  297. // 创建下载链接
  298. const createDownloadLink = () => {
  299. const htmlContent = createHtmlReport();
  300. const blob = new Blob([htmlContent], { type: 'text/html' });
  301. const url = URL.createObjectURL(blob);
  302. return {
  303. url,
  304. filename: `研究报告_${directionTitle.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_')}.html`,
  305. cleanup: () => URL.revokeObjectURL(url)
  306. };
  307. };
  308. // 下载链接状态
  309. const [downloadLink, setDownloadLink] = useState<{
  310. url: string;
  311. filename: string;
  312. cleanup: () => void;
  313. } | null>(null);
  314. // 创建下载链接
  315. useEffect(() => {
  316. if (parsedReport) {
  317. const link = createDownloadLink();
  318. setDownloadLink(link);
  319. // 清理函数
  320. return () => {
  321. if (link && link.cleanup) link.cleanup();
  322. };
  323. }
  324. }, [parsedReport, directionTitle]);
  325. // 直接显示原始内容以便调试
  326. const showRawContent = () => {
  327. console.log("显示原始内容");
  328. alert(JSON.stringify(report, null, 2));
  329. };
  330. return (
  331. <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4 overflow-y-auto">
  332. <div className="bg-gray-800 rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
  333. <div className="p-6">
  334. {/* 标题和关闭按钮 */}
  335. <div className="flex justify-between items-start mb-6 sticky top-0 bg-gray-800 pb-2 z-10">
  336. <h3 className="text-xl font-bold text-white">
  337. 研究报告: {directionTitle}
  338. </h3>
  339. <button
  340. onClick={onClose}
  341. className="bg-gray-700 hover:bg-gray-600 rounded-full p-2 text-white/80 hover:text-white"
  342. >
  343. <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">
  344. <line x1="18" y1="6" x2="6" y2="18"></line>
  345. <line x1="6" y1="6" x2="18" y2="18"></line>
  346. </svg>
  347. </button>
  348. </div>
  349. {/* 下载和调试按钮 */}
  350. <div className="mb-4 flex gap-2">
  351. {downloadLink && (
  352. <a
  353. href={downloadLink.url}
  354. download={downloadLink.filename}
  355. 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"
  356. >
  357. <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">
  358. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
  359. <polyline points="7 10 12 15 17 10"></polyline>
  360. <line x1="12" y1="15" x2="12" y2="3"></line>
  361. </svg>
  362. 下载HTML格式报告
  363. </a>
  364. )}
  365. <button
  366. onClick={showRawContent}
  367. 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"
  368. >
  369. 查看原始数据
  370. </button>
  371. </div>
  372. {/* 研究报告内容 */}
  373. <div className="bg-white text-black p-6 rounded-md prose max-w-none">
  374. {/* 如果解析出的内容为空,显示待处理信息 */}
  375. {!parsedReport && (
  376. <div className="text-center py-8 text-gray-500">
  377. <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">
  378. <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
  379. <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>
  380. </svg>
  381. <p>正在处理报告内容...</p>
  382. </div>
  383. )}
  384. {/* 显示解析后的内容,使用预格式化文本保留格式 */}
  385. {parsedReport && (
  386. <div
  387. className="whitespace-pre-wrap"
  388. dangerouslySetInnerHTML={{
  389. __html: parsedReport
  390. .replace(/\*\*(\d+)\.\s+([^\*]+)\*\*/g, '<h2 class="text-xl font-bold mt-6 mb-4">$1. $2</h2>')
  391. .replace(/\*\*(\d+\.\d+)\s+([^\*]+)\*\*/g, '<h3 class="text-lg font-bold mt-5 mb-3">$1 $2</h3>')
  392. .replace(/\n\n/g, '<br/><br/>')
  393. }}
  394. ></div>
  395. )}
  396. </div>
  397. </div>
  398. </div>
  399. </div>
  400. );
  401. }
  402. export function ResearchForm({ className = "", onSubmit }: ResearchFormProps) {
  403. // 基本状态
  404. const [researchTopic, setResearchTopic] = useState('');
  405. const [loading, setLoading] = useState(false);
  406. const [error, setError] = useState<string | null>(null);
  407. const [processMessage, setProcessMessage] = useState('');
  408. const [researchCompleted, setResearchCompleted] = useState(false); // 新增:标记研究是否已完成
  409. // 研究数据
  410. const [keywordsData, setKeywordsData] = useState<any>(null);
  411. const [directionsData, setDirectionsData] = useState<any>(null);
  412. const [papersData, setPapersData] = useState<any[]>([]);
  413. const [reportsData, setReportsData] = useState<any[]>([]);
  414. const [researchData, setResearchData] = useState<any>(null);
  415. // 模态框状态
  416. const [showPapersModal, setShowPapersModal] = useState<boolean>(false);
  417. const [showReportModal, setShowReportModal] = useState<boolean>(false);
  418. const [selectedDirectionIndex, setSelectedDirectionIndex] = useState<number | null>(null);
  419. // 添加新函数,用于开始新的研究
  420. const startNewResearch = () => {
  421. // 重置所有状态
  422. setResearchTopic('');
  423. setKeywordsData(null);
  424. setDirectionsData(null);
  425. setPapersData([]);
  426. setReportsData([]);
  427. setResearchData(null);
  428. setProcessMessage('');
  429. setError(null);
  430. setResearchCompleted(false);
  431. // 重置全局重试计数
  432. if (window.sseRetryCount) {
  433. window.sseRetryCount = 0;
  434. }
  435. };
  436. // 表单提交
  437. const handleSubmit = (e: React.FormEvent) => {
  438. e.preventDefault();
  439. if (!researchTopic.trim()) {
  440. setError('请输入研究主题');
  441. return;
  442. }
  443. // 如果已经有研究完成,先重置状态
  444. if (researchCompleted) {
  445. startNewResearch();
  446. // 延迟设置新主题并提交,确保状态重置完成
  447. setTimeout(() => {
  448. const newTopic = researchTopic; // 保存当前主题
  449. const newEvent = new Event('submit') as React.FormEvent;
  450. setResearchTopic(newTopic);
  451. handleSubmit(newEvent);
  452. }, 10);
  453. return;
  454. }
  455. setLoading(true);
  456. setError(null);
  457. setProcessMessage('正在连接服务器...');
  458. // 创建EventSource连接
  459. try {
  460. const safeResearchTopic = encodeURIComponent(researchTopic.trim());
  461. const url = `/api/research/enhanced-process-stream?research_intent=${safeResearchTopic}`;
  462. const eventSource = new EventSource(url);
  463. // 连接事件处理
  464. eventSource.onopen = () => {
  465. setProcessMessage('连接成功,开始处理研究内容...');
  466. };
  467. // 消息事件处理
  468. eventSource.onmessage = (event) => {
  469. try {
  470. const data = JSON.parse(event.data);
  471. console.log("收到事件数据:", data);
  472. // 更新处理消息
  473. if (data.message) {
  474. setProcessMessage(data.message);
  475. }
  476. // 处理不同类型的事件
  477. switch(data.status) {
  478. case 'base_keywords_extracted':
  479. if (data.data && data.data.keywords) {
  480. setKeywordsData(data.data);
  481. console.log("关键词数据:", data.data);
  482. }
  483. break;
  484. case 'research_topics_generated':
  485. if (data.data && data.data.research_topics) {
  486. setDirectionsData(data.data);
  487. console.log("研究方向数据:", data.data);
  488. }
  489. break;
  490. case 'papers_ready':
  491. if (data.data) {
  492. console.log("论文数据:", data.data);
  493. setPapersData(prev => {
  494. const newData = [...prev];
  495. if (data.data.direction_index !== undefined) {
  496. newData[data.data.direction_index] = data.data;
  497. }
  498. return newData;
  499. });
  500. }
  501. break;
  502. case 'report_ready':
  503. if (data.data) {
  504. console.log("报告数据:", data.data);
  505. // 使用函数式更新确保报告数据正确合并
  506. setReportsData((prevReports: any[]) => {
  507. const newReports = [...prevReports];
  508. if (data.data.direction_index !== undefined) {
  509. newReports[data.data.direction_index] = data.data;
  510. }
  511. return newReports;
  512. });
  513. // 如果是最后一个报告已生成,标记研究为完成状态
  514. if (data.data.direction_index === 4 ||
  515. (directionsData?.research_topics &&
  516. data.data.direction_index === directionsData.research_topics.length - 1)) {
  517. console.log("最后一个报告已生成,研究完成");
  518. // 使用setTimeout确保报告数据先被更新完成
  519. setTimeout(() => {
  520. setResearchData({
  521. research_intent: researchTopic,
  522. keywords: keywordsData?.keywords || [],
  523. directions: directionsData?.research_topics || [],
  524. papers_by_direction: papersData,
  525. reports: reportsData // 直接使用当前状态
  526. });
  527. setProcessMessage("研究已完成,您可以查看各个方向的报告和论文");
  528. setResearchCompleted(true);
  529. eventSource.close();
  530. setLoading(false);
  531. }, 100);
  532. }
  533. }
  534. break;
  535. case 'completed':
  536. setResearchData({
  537. research_intent: researchTopic,
  538. keywords: keywordsData?.keywords || [],
  539. directions: directionsData?.research_topics || [],
  540. papers_by_direction: papersData,
  541. reports: reportsData
  542. });
  543. setProcessMessage("研究已完成,您可以查看各个方向的报告和论文");
  544. setResearchCompleted(true);
  545. eventSource.close();
  546. setLoading(false);
  547. break;
  548. case 'error':
  549. setError(data.message || "处理过程中发生错误");
  550. eventSource.close();
  551. setLoading(false);
  552. break;
  553. }
  554. } catch (error) {
  555. console.error("处理事件数据时出错:", error);
  556. }
  557. };
  558. // 错误处理 - 增强版
  559. eventSource.onerror = (error: Event) => {
  560. console.error("与研究服务连接失败:", error);
  561. // 重连尝试计数
  562. if (!window.sseRetryCount) {
  563. window.sseRetryCount = 0;
  564. }
  565. // 检查是否已完成大部分研究 (即已经生成了最后一个方向的报告)
  566. const isLastReportGenerated = reportsData.length > 0 &&
  567. directionsData?.research_topics &&
  568. reportsData.length >= directionsData.research_topics.length;
  569. // 如果已经生成了最后一个报告,直接标记为完成
  570. if (isLastReportGenerated) {
  571. console.log("所有报告都已生成,标记研究完成");
  572. // 构建最终研究数据
  573. setResearchData({
  574. research_intent: researchTopic,
  575. keywords: keywordsData?.keywords || [],
  576. directions: directionsData?.research_topics || [],
  577. papers_by_direction: papersData,
  578. reports: reportsData
  579. });
  580. // 显示完成消息
  581. setProcessMessage("研究已完成,您可以查看各个方向的报告和论文");
  582. setResearchCompleted(true);
  583. setLoading(false);
  584. // 确保关闭连接
  585. try {
  586. eventSource.close();
  587. } catch (e) {
  588. // 忽略关闭错误
  589. }
  590. return;
  591. }
  592. // 增加重连机制 - 但只在未生成足够报告时尝试
  593. if (window.sseRetryCount < 3) {
  594. console.log(`尝试重新连接 (${window.sseRetryCount + 1}/3)...`);
  595. window.sseRetryCount++;
  596. // 关闭当前连接
  597. try {
  598. eventSource.close();
  599. } catch (e) {
  600. // 忽略关闭错误
  601. }
  602. // 等待1秒后重连
  603. setTimeout(() => {
  604. const newUrl = `/api/research/enhanced-process-stream?research_intent=${safeResearchTopic}`;
  605. const newEventSource = new EventSource(newUrl);
  606. // 设置新的事件监听器
  607. // ...复制原来的所有事件监听器...
  608. setProcessMessage(`尝试重新连接 (${window.sseRetryCount}/3)...`);
  609. }, 1000);
  610. return;
  611. }
  612. // 检查处理是否已基本完成
  613. const hasCompletedMostWork = reportsData.some(report => report && (report.report || report.content));
  614. if (hasCompletedMostWork) {
  615. console.log("尽管连接中断,但大部分报告已生成,尝试恢复...");
  616. // 所有研究方向都有数据了,认为是基本完成
  617. if (reportsData.length >= directionsData?.research_topics?.length) {
  618. console.log("所有报告都已生成,手动完成流程");
  619. // 构建最终研究数据
  620. setResearchData({
  621. research_intent: researchTopic,
  622. keywords: keywordsData?.keywords || [],
  623. directions: directionsData?.research_topics || [],
  624. papers_by_direction: papersData,
  625. reports: reportsData
  626. });
  627. // 显示提示但继续
  628. setProcessMessage("研究已完成,您可以查看各个方向的报告和论文");
  629. setResearchCompleted(true);
  630. setLoading(false);
  631. } else {
  632. // 还有部分未完成,给出警告
  633. setError("与研究服务的连接中断,但部分报告已生成。您可以查看现有结果,或开始新的研究。");
  634. setResearchCompleted(true);
  635. setLoading(false);
  636. }
  637. } else {
  638. // 完全失败的情况
  639. setError("与研究服务的连接中断,可能原因:1) 后端服务未运行 2) 网络连接问题 3) 服务器错误");
  640. setProcessMessage("您可以开始新的研究,或联系管理员获取帮助。");
  641. setLoading(false);
  642. }
  643. // 确保关闭连接
  644. try {
  645. eventSource.close();
  646. } catch (e) {
  647. // 忽略关闭错误
  648. }
  649. };
  650. } catch (error) {
  651. console.error("创建EventSource失败:", error);
  652. setError("无法连接到研究服务");
  653. setLoading(false);
  654. }
  655. };
  656. // 取消处理
  657. const handleCancel = () => {
  658. setLoading(false);
  659. };
  660. // 打开论文列表模态框 - 修复论文模态框打开问题
  661. const openPapersModal = (directionIndex: number) => {
  662. console.log("打开论文模态框,方向索引:", directionIndex);
  663. setSelectedDirectionIndex(directionIndex);
  664. setShowPapersModal(true);
  665. setShowReportModal(false);
  666. };
  667. // 关闭论文列表模态框
  668. const closePapersModal = () => {
  669. setShowPapersModal(false);
  670. setSelectedDirectionIndex(null);
  671. };
  672. // 打开研究报告模态框 - 增加日志以便调试
  673. const openReportModal = (directionIndex: number) => {
  674. console.log("打开报告模态框,方向索引:", directionIndex);
  675. console.log("报告数据:", reportsData[directionIndex]);
  676. setSelectedDirectionIndex(directionIndex);
  677. setShowReportModal(true);
  678. setShowPapersModal(false);
  679. };
  680. // 关闭研究报告模态框
  681. const closeReportModal = () => {
  682. setShowReportModal(false);
  683. setSelectedDirectionIndex(null);
  684. };
  685. return (
  686. <div className={className}>
  687. {/* 标题和说明 */}
  688. <div className="mb-6">
  689. <h2 className="text-2xl font-bold text-white mb-2 text-center">研究助手</h2>
  690. </div>
  691. {/* 研究表单 */}
  692. <form onSubmit={handleSubmit} className="space-y-4">
  693. {/* 研究主题输入 */}
  694. <div className="space-y-2">
  695. <label htmlFor="research-topic" className="block text-sm font-medium text-white">
  696. 研究主题
  697. </label>
  698. <textarea
  699. id="research-topic"
  700. placeholder="输入您想研究的主题,例如:新能源电池技术的最新发展及应用前景"
  701. 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"
  702. value={researchTopic}
  703. onChange={(e) => setResearchTopic(e.target.value)}
  704. disabled={loading}
  705. />
  706. </div>
  707. {/* 错误信息显示 */}
  708. {error && <ErrorMessage message={error} onDismiss={() => setError(null)} />}
  709. {/* 提交按钮 - 居中显示 */}
  710. <div className="flex justify-center mt-4">
  711. <Button
  712. type="submit"
  713. disabled={!researchTopic.trim() || loading}
  714. className={`${
  715. loading ? 'bg-blue-600/50' : 'bg-blue-600 hover:bg-blue-700'
  716. } text-white font-medium py-2 px-4 rounded-md transition-colors`}
  717. >
  718. {loading ? "研究进行中..." : researchCompleted ? "开始新研究" : "开始研究"}
  719. </Button>
  720. {loading && (
  721. <button
  722. type="button"
  723. onClick={handleCancel}
  724. className="ml-2 text-white/60 hover:text-white text-sm"
  725. >
  726. 取消
  727. </button>
  728. )}
  729. </div>
  730. </form>
  731. {/* 处理中状态显示 */}
  732. {loading && (
  733. <div className="mt-8 space-y-6">
  734. {/* 状态消息 */}
  735. <div className="p-4 bg-blue-900/30 rounded-lg border border-blue-800/50">
  736. <div className="flex items-center">
  737. <div className="mr-3">
  738. <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">
  739. <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
  740. <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>
  741. </svg>
  742. </div>
  743. <div>
  744. <p className="font-medium text-white">研究进行中...</p>
  745. <p className="text-sm text-white/80">{processMessage}</p>
  746. </div>
  747. </div>
  748. </div>
  749. {/* 关键词显示 */}
  750. {keywordsData && keywordsData.keywords && keywordsData.keywords.length > 0 && (
  751. <div className="p-4 bg-white/5 rounded-lg">
  752. <h3 className="text-sm font-medium text-white mb-2">提取到的关键词:</h3>
  753. <div className="flex flex-wrap gap-2">
  754. {keywordsData.keywords.map((keyword: string, index: number) => (
  755. <span
  756. key={index}
  757. className="px-3 py-1 bg-blue-500/20 rounded-full text-blue-300"
  758. >
  759. {keyword}
  760. </span>
  761. ))}
  762. </div>
  763. </div>
  764. )}
  765. {/* 研究方向显示 */}
  766. {directionsData && directionsData.research_topics && directionsData.research_topics.length > 0 && (
  767. <div className="p-4 bg-white/5 rounded-lg">
  768. <h3 className="text-sm font-medium text-white mb-2">生成的研究方向:</h3>
  769. <div className="space-y-2">
  770. {directionsData.research_topics.map((topic: any, index: number) => (
  771. <div key={index} className="p-2 bg-white/10 rounded-md">
  772. <p className="font-medium text-white">
  773. {index + 1}. {topic.english_title || topic.title}
  774. </p>
  775. {topic.keywords && topic.keywords.length > 0 && (
  776. <div className="mt-1 flex flex-wrap gap-1">
  777. {topic.keywords.map((kw: string, kidx: number) => (
  778. <span key={kidx} className="px-1.5 py-0.5 text-xs bg-white/10 rounded-full text-white/70">
  779. {kw}
  780. </span>
  781. ))}
  782. </div>
  783. )}
  784. </div>
  785. ))}
  786. </div>
  787. </div>
  788. )}
  789. {/* 已检索的论文和报告 */}
  790. {(papersData.length > 0 || reportsData.length > 0) && (
  791. <div className="p-4 bg-white/5 rounded-lg">
  792. <h3 className="text-sm font-medium text-white mb-2">研究进展:</h3>
  793. <div className="space-y-3">
  794. {directionsData && directionsData.research_topics && directionsData.research_topics.map((topic: any, index: number) => {
  795. const hasReport = reportsData[index] && (reportsData[index].report || reportsData[index].content);
  796. const hasPapers = papersData[index] && papersData[index].papers && papersData[index].papers.length > 0;
  797. if (!hasReport && !hasPapers) return null;
  798. return (
  799. <div key={index} className="p-2 bg-white/10 rounded-md">
  800. <p className="font-medium text-white mb-1">
  801. 方向 {index + 1}: {topic.english_title || topic.title}
  802. </p>
  803. {hasReport && (
  804. <div className="ml-2 text-sm text-white/80 mb-1 flex items-center">
  805. <span className="text-green-400 mr-1">✓</span> 研究报告已生成
  806. <button
  807. onClick={() => openReportModal(index)}
  808. className="ml-2 text-xs text-blue-400 hover:text-blue-300"
  809. >
  810. 查看报告
  811. </button>
  812. </div>
  813. )}
  814. {hasPapers && (
  815. <div className="ml-2 text-sm text-white/80 flex items-center">
  816. <span className="text-green-400 mr-1">✓</span> 已找到 {papersData[index].papers.length} 篇相关论文
  817. <button
  818. onClick={() => openPapersModal(index)}
  819. className="ml-2 text-xs text-blue-400 hover:text-blue-300"
  820. >
  821. 查看论文
  822. </button>
  823. </div>
  824. )}
  825. </div>
  826. );
  827. })}
  828. </div>
  829. </div>
  830. )}
  831. </div>
  832. )}
  833. {/* 研究完成后状态显示 */}
  834. {researchCompleted && !loading && (
  835. <div className="mt-8 space-y-6">
  836. {/* 完成状态标志 */}
  837. <div className="p-4 bg-green-900/30 rounded-lg border border-green-800/50">
  838. <div className="flex items-center">
  839. <div className="mr-3">
  840. <svg className="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
  841. <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" />
  842. </svg>
  843. </div>
  844. <div>
  845. <p className="font-medium text-white">研究完成</p>
  846. <p className="text-sm text-white/80">{processMessage}</p>
  847. </div>
  848. </div>
  849. </div>
  850. {/* 研究结果 */}
  851. <div>
  852. <h3 className="text-xl font-bold text-white">研究结果</h3>
  853. {/* 关键词部分 */}
  854. {keywordsData && keywordsData.keywords && keywordsData.keywords.length > 0 && (
  855. <div className="p-4 bg-white/5 rounded-lg mt-4">
  856. <h4 className="text-lg font-medium text-white mb-2">关键词</h4>
  857. <div className="flex flex-wrap gap-2">
  858. {keywordsData.keywords.map((keyword: string, index: number) => (
  859. <span
  860. key={index}
  861. className="px-3 py-1 bg-blue-500/20 rounded-full text-blue-300"
  862. >
  863. {keyword}
  864. </span>
  865. ))}
  866. </div>
  867. </div>
  868. )}
  869. {/* 研究报告部分 */}
  870. {directionsData && directionsData.research_topics && directionsData.research_topics.length > 0 && (
  871. <div className="space-y-6 mt-4">
  872. <h4 className="text-lg font-medium text-white">研究报告</h4>
  873. {directionsData.research_topics.map((direction: any, index: number) => {
  874. // 检查该方向是否有报告数据
  875. const hasReport = reportsData && reportsData[index];
  876. if (!hasReport) return null; // 没有报告数据则跳过显示
  877. return (
  878. <div key={index} className="p-4 bg-white/5 rounded-lg">
  879. <h5 className="text-md font-medium text-white mb-3">
  880. {index + 1}. {direction.english_title || direction.title}
  881. </h5>
  882. {/* 方向的关键词 */}
  883. {direction.keywords && direction.keywords.length > 0 && (
  884. <div className="mb-4">
  885. <div className="text-sm text-white/60 mb-1">关键词:</div>
  886. <div className="flex flex-wrap gap-1">
  887. {direction.keywords.map((kw: string, kidx: number) => (
  888. <span key={kidx} className="px-2 py-0.5 text-xs bg-white/10 rounded-full text-white/80">
  889. {kw}
  890. </span>
  891. ))}
  892. </div>
  893. </div>
  894. )}
  895. {/* 研究报告与论文分开显示,分别用按钮打开不同模态框 */}
  896. <div className="flex flex-wrap gap-4 mb-4">
  897. {/* 查看研究报告按钮 */}
  898. <button
  899. onClick={() => openReportModal(index)}
  900. 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"
  901. >
  902. <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">
  903. <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
  904. <polyline points="14 2 14 8 20 8"></polyline>
  905. <line x1="16" y1="13" x2="8" y2="13"></line>
  906. <line x1="16" y1="17" x2="8" y2="17"></line>
  907. <polyline points="10 9 9 9 8 9"></polyline>
  908. </svg>
  909. 查看研究报告
  910. </button>
  911. {/* 查看论文列表按钮 */}
  912. {papersData[index] && papersData[index].papers && papersData[index].papers.length > 0 && (
  913. <button
  914. onClick={() => openPapersModal(index)}
  915. 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"
  916. >
  917. <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">
  918. <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
  919. <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>
  920. </svg>
  921. 查看相关论文 ({papersData[index].papers.length})
  922. </button>
  923. )}
  924. </div>
  925. {/* 论文列表预览 - 仅显示标题 */}
  926. {papersData[index] && papersData[index].papers && papersData[index].papers.length > 0 && (
  927. <div>
  928. <div className="text-sm font-medium text-white/80 mb-2">
  929. 论文预览:
  930. </div>
  931. <div className="space-y-1.5 mb-2">
  932. {papersData[index].papers.slice(0, 2).map((paper: any, pidx: number) => (
  933. <a
  934. key={pidx}
  935. href={paper.link || paper.url}
  936. target="_blank"
  937. rel="noopener noreferrer"
  938. className="text-blue-400 hover:text-blue-300 hover:underline block"
  939. >
  940. {paper.title}
  941. </a>
  942. ))}
  943. {papersData[index].papers.length > 2 && (
  944. <button
  945. onClick={() => openPapersModal(index)}
  946. className="text-xs text-blue-400 hover:text-blue-300 mt-1"
  947. >
  948. 查看更多...
  949. </button>
  950. )}
  951. </div>
  952. </div>
  953. )}
  954. </div>
  955. );
  956. })}
  957. </div>
  958. )}
  959. </div>
  960. </div>
  961. )}
  962. {/* 论文列表模态框 */}
  963. {showPapersModal && selectedDirectionIndex !== null && (
  964. <PapersListModal
  965. directionIndex={selectedDirectionIndex}
  966. directionTitle={
  967. (directionsData?.research_topics?.[selectedDirectionIndex]?.english_title ||
  968. directionsData?.research_topics?.[selectedDirectionIndex]?.title) ||
  969. `研究方向 ${selectedDirectionIndex + 1}`
  970. }
  971. papers={papersData[selectedDirectionIndex]?.papers || []}
  972. onClose={closePapersModal}
  973. />
  974. )}
  975. {/* 研究报告模态框 */}
  976. {showReportModal && selectedDirectionIndex !== null && (
  977. <ReportModal
  978. directionIndex={selectedDirectionIndex}
  979. directionTitle={
  980. (directionsData?.research_topics?.[selectedDirectionIndex]?.english_title ||
  981. directionsData?.research_topics?.[selectedDirectionIndex]?.title) ||
  982. `研究方向 ${selectedDirectionIndex + 1}`
  983. }
  984. report={reportsData[selectedDirectionIndex]}
  985. onClose={closeReportModal}
  986. />
  987. )}
  988. </div>
  989. );
  990. }