|
|
|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<title>Research Interface</title> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> |
|
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script> |
|
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script> |
|
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown.min.css"> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css"> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.5/marked.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.6.1/mermaid.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css"> |
|
<script src="https://cdn.tailwindcss.com?plugins=typography"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.0/es5/tex-mml-chtml.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script> |
|
<style> |
|
|
|
@media (max-width: 640px) { |
|
pre { |
|
max-width: 100%; |
|
overflow-x: auto; |
|
} |
|
.mermaid { |
|
max-width: 100%; |
|
overflow-x: auto; |
|
} |
|
table { |
|
display: block; |
|
width: 100%; |
|
overflow-x: auto; |
|
} |
|
} |
|
|
|
|
|
@media (max-width: 640px) { |
|
button { |
|
min-height: 44px; |
|
} |
|
|
|
input, textarea { |
|
font-size: 16px; |
|
} |
|
} |
|
|
|
|
|
@media (max-width: 480px) { |
|
.markdown-content { |
|
font-size: 15px; |
|
line-height: 1.6; |
|
} |
|
} |
|
|
|
|
|
.mermaid-container { |
|
width: 100%; |
|
overflow-x: auto; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div id="root"></div> |
|
|
|
<script type="text/babel"> |
|
const { useState, useEffect, useRef } = React; |
|
|
|
let finalReport = ''; |
|
|
|
const ResearchInterface = () => { |
|
const [isLoading, setIsLoading] = useState(false); |
|
const [researchLoading, setResearchLoading] = useState(false); |
|
const [countdown, setCountdown] = useState(0); |
|
const [researchCountdown, setResearchCountdown] = useState(0); |
|
const [currentStep, setCurrentStep] = useState(1); |
|
const [responseContent, setResponseContent] = useState(''); |
|
const [expandedSections, setExpandedSections] = useState({}); |
|
const [researchTopic, setResearchTopic] = useState(''); |
|
const [thinkingOutput, setThinkingOutput] = useState(''); |
|
const [researchSections, setResearchSections] = useState([]); |
|
const [userThoughts, setUserThoughts] = useState(''); |
|
const responseRef = useRef(null); |
|
const [taskResults, setTaskResults] = useState({}); |
|
const [currentTaskIndex, setCurrentTaskIndex] = useState(-1); |
|
const [taskLoading, setTaskLoading] = useState(false); |
|
const stepRefs = useRef([]); |
|
|
|
|
|
const toggleSection = (sectionId) => { |
|
setExpandedSections(prev => ({ |
|
...prev, |
|
[sectionId]: !prev[sectionId] |
|
})); |
|
}; |
|
|
|
|
|
|
|
const fetchThinkingData = async () => { |
|
setIsLoading(true); |
|
setCountdown(3); |
|
setThinkingOutput(''); |
|
|
|
try { |
|
const response = await fetch('/api/think', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify({ topic: researchTopic }), |
|
}); |
|
|
|
const reader = response.body.getReader(); |
|
const decoder = new TextDecoder(); |
|
let buffer = ''; |
|
|
|
while (true) { |
|
const { done, value } = await reader.read(); |
|
if (done) break; |
|
|
|
const chunk = decoder.decode(value); |
|
buffer += chunk; |
|
|
|
|
|
const processedContent = processSSEContent(buffer); |
|
|
|
if (processedContent) { |
|
setThinkingOutput(prev => prev + processedContent); |
|
|
|
buffer = buffer.replace(/data: {"content": "[^"]*"}(?:\r?\n)?/g, ''); |
|
} |
|
} |
|
|
|
|
|
if (buffer) { |
|
const finalContent = processSSEContent(buffer); |
|
if (finalContent) { |
|
setThinkingOutput(prev => prev + finalContent); |
|
} |
|
} |
|
} catch (error) { |
|
console.error('Error with stream:', error); |
|
setThinkingOutput('请求发生错误,请重试...'); |
|
} finally { |
|
setIsLoading(false); |
|
goToNextStep(); |
|
} |
|
}; |
|
|
|
|
|
const fetchResearchData = async () => { |
|
setResearchLoading(true); |
|
setResearchCountdown(3); |
|
const prompt = [ |
|
`Initial Query: `+researchTopic, |
|
`Follow-up Questions: `+thinkingOutput, |
|
`Follow-up Feedback: `+userThoughts |
|
].join("\n\n"); |
|
console.log(prompt); |
|
try { |
|
const response = await fetch('/api/research', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify({ query: prompt }), |
|
}); |
|
|
|
const data = await response.json(); |
|
|
|
|
|
const contentString = data.choices[0].message.content; |
|
|
|
|
|
const jsonMatch = contentString.match(/```(?:json)?\s*([\s\S]*?)\s*```/); |
|
|
|
if (jsonMatch && jsonMatch[1]) { |
|
const parsedData = JSON.parse(jsonMatch[1]); |
|
setResearchSections(parsedData); |
|
|
|
setCurrentTaskIndex(0); |
|
} else { |
|
throw new Error('Invalid response format from API'); |
|
} |
|
|
|
} catch (error) { |
|
console.error('Error fetching research data:', error); |
|
setResearchSections([{ |
|
id: 'error', |
|
query: '获取数据错误', |
|
researchGoal: '无法获取研究数据,请重试...' |
|
}]); |
|
} finally { |
|
setResearchLoading(false); |
|
goToNextStep(); |
|
} |
|
}; |
|
|
|
|
|
const fetchReportData = async () => { |
|
setIsLoading(true); |
|
setResponseContent(''); |
|
|
|
|
|
const learnings = researchSections.map(section => { |
|
return taskResults[section.id || researchSections.indexOf(section)] || ''; |
|
}); |
|
|
|
try { |
|
const response = await fetch("/api/report", { |
|
method: "POST", |
|
headers: { |
|
"Content-Type": "application/json", |
|
}, |
|
body: JSON.stringify({ |
|
query: researchTopic, |
|
learnings: learnings |
|
}), |
|
}); |
|
|
|
const reader = response.body.getReader(); |
|
const decoder = new TextDecoder(); |
|
let buffer = ''; |
|
|
|
while (true) { |
|
const { done, value } = await reader.read(); |
|
if (done) break; |
|
|
|
const chunk = decoder.decode(value); |
|
buffer += chunk; |
|
|
|
|
|
const messages = buffer.split(/\n\n/); |
|
buffer = messages.pop() || ''; |
|
|
|
let newContent = ''; |
|
|
|
for (const message of messages) { |
|
const dataLines = message.split('\n') |
|
.filter(line => line.startsWith('data: ')) |
|
.map(line => { |
|
try { |
|
return JSON.parse(line.substring(6)).content || ''; |
|
} catch (e) { |
|
console.error('Error parsing JSON:', e); |
|
return ''; |
|
} |
|
}) |
|
.filter(content => content); |
|
|
|
|
|
const messageContent = dataLines.join(''); |
|
newContent += messageContent; |
|
finalReport += messageContent; |
|
} |
|
|
|
|
|
if (newContent) { |
|
setResponseContent(prev => prev + newContent); |
|
} |
|
} |
|
|
|
|
|
if (buffer) { |
|
const remainingContent = buffer.split('\n') |
|
.filter(line => line.startsWith('data: ')) |
|
.map(line => { |
|
try { |
|
return JSON.parse(line.substring(6)).content || ''; |
|
} catch (e) { |
|
return ''; |
|
} |
|
}) |
|
.filter(content => content) |
|
.join(''); |
|
|
|
if (remainingContent) { |
|
setResponseContent(prev => prev + remainingContent); |
|
finalReport += remainingContent; |
|
} |
|
} |
|
|
|
|
|
setTimeout(() => { |
|
|
|
setResponseContent(finalReport); |
|
console.log(finalReport); |
|
}, 300); |
|
|
|
} catch (error) { |
|
console.error('Error generating report:', error); |
|
setResponseContent('生成报告时发生错误,请重试...'); |
|
} finally { |
|
setIsLoading(false); |
|
} |
|
}; |
|
|
|
|
|
|
|
const convertToPDFFormat = (content) => { |
|
if (!content) return "<p>No content available</p>"; |
|
|
|
let processedContent = content; |
|
|
|
|
|
processedContent = processedContent.replace(/```mermaid([\s\S]*?)```/g, (match, diagramCode) => { |
|
|
|
return '<div class="pdf-note" style="padding: 10px; background-color: #f5f5f5; border-left: 4px solid #4a5568; margin: 10px 0;">' + |
|
'<p><strong>Note:</strong> This document contains a diagram that may not render in PDF format. ' + |
|
'Please refer to the original online report to view all diagrams.</p></div>'; |
|
}); |
|
|
|
|
|
processedContent = processedContent.replace(/\$\$([\s\S]*?)\$\$/g, '<div class="math">$1</div>'); |
|
processedContent = processedContent.replace(/\$([^\$]*?)\$/g, '<span class="math">$1</span>'); |
|
|
|
|
|
const styledContent = `<style> |
|
body { |
|
font-family: 'Arial', sans-serif; |
|
font-size: 12pt; |
|
line-height: 1.5; |
|
color: #333; |
|
margin: 1cm; |
|
} |
|
h1, h2, h3, h4, h5, h6 { |
|
margin-top: 1.5em; |
|
margin-bottom: 0.5em; |
|
color: #1a365d; |
|
} |
|
h1 { font-size: 20pt; } |
|
h2 { font-size: 16pt; } |
|
h3 { font-size: 14pt; } |
|
p { margin: 0.5em 0; } |
|
ul, ol { margin: 0.5em 0; padding-left: 2em; } |
|
code { |
|
font-family: 'Courier New', monospace; |
|
background-color: #f5f5f5; |
|
padding: 2px 4px; |
|
border-radius: 3px; |
|
} |
|
pre { |
|
background-color: #f5f5f5; |
|
padding: 10px; |
|
border-radius: 5px; |
|
white-space: pre-wrap; |
|
font-family: 'Courier New', monospace; |
|
margin: 1em 0; |
|
} |
|
table { |
|
border-collapse: collapse; |
|
width: 100%; |
|
margin: 1em 0; |
|
} |
|
th, td { |
|
border: 1px solid #ddd; |
|
padding: 8px; |
|
text-align: left; |
|
} |
|
th { |
|
background-color: #f2f2f2; |
|
} |
|
blockquote { |
|
border-left: 4px solid #e2e8f0; |
|
margin: 1em 0; |
|
padding-left: 1em; |
|
color: #4a5568; |
|
} |
|
.pdf-note { |
|
padding: 10px; |
|
background-color: #f5f5f5; |
|
border-left: 4px solid #4a5568; |
|
margin: 1em 0; |
|
} |
|
img { |
|
max-width: 100%; |
|
height: auto; |
|
} |
|
.page-break { |
|
page-break-after: always; |
|
} |
|
.report-title { |
|
font-size: 24pt; |
|
text-align: center; |
|
margin-bottom: 2em; |
|
color: #1a365d; |
|
} |
|
.report-date { |
|
text-align: right; |
|
margin-bottom: 2em; |
|
font-style: italic; |
|
} |
|
.report-summary { |
|
margin: 2em 0; |
|
padding: 1em; |
|
background-color: #f8f9fa; |
|
border-radius: 5px; |
|
}</style><div class="report-title">${researchTopic}</div><div class="report-date">Generated on: ${new Date().toLocaleDateString()}</div><div class="report-content"> |
|
${window.marked ? window.marked.parse(processedContent) : processedContent.replace(/\n/g, "<br>")} |
|
</div> |
|
`; |
|
|
|
return styledContent; |
|
}; |
|
|
|
const handleDownloadPDF = async () => { |
|
if (!responseContent.trim()) { |
|
alert("报告内容为空,无法导出"); |
|
return; |
|
} |
|
try { |
|
|
|
setIsLoading(true); |
|
const filename = researchTopic ? `${researchTopic}.pdf` : "report.pdf"; |
|
const response = await fetch("/api/pdf", { |
|
method: "POST", |
|
headers: { |
|
"Content-Type": "application/json", |
|
}, |
|
body: JSON.stringify({ |
|
text: finalReport, |
|
filename |
|
}), |
|
|
|
}); |
|
|
|
if (!response.ok) { |
|
const errorText = await response.text(); |
|
throw new Error(`生成 PDF 失败: ${errorText}`); |
|
} |
|
|
|
|
|
const blob = await response.blob(); |
|
const link = document.createElement("a"); |
|
link.href = window.URL.createObjectURL(blob); |
|
link.download = filename; |
|
document.body.appendChild(link); |
|
link.click(); |
|
document.body.removeChild(link); |
|
} catch (error) { |
|
console.error("PDF 生成失败:", error); |
|
alert("导出失败,请稍后重试"); |
|
} finally { |
|
setIsLoading(false); |
|
} |
|
}; |
|
|
|
|
|
|
|
useEffect(() => { |
|
const executeTask = async (index) => { |
|
if (index < 0 || index >= researchSections.length) { |
|
|
|
setTaskLoading(false); |
|
return; |
|
} |
|
|
|
setTaskLoading(true); |
|
const section = researchSections[index]; |
|
|
|
|
|
setExpandedSections(prev => ({ |
|
...prev, |
|
[section.id || index]: true |
|
})); |
|
|
|
try { |
|
const response = await fetch('/api/taskExec', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify({ |
|
query: section.query, |
|
researchGoal: section.researchGoal |
|
}), |
|
}); |
|
|
|
const reader = response.body.getReader(); |
|
const decoder = new TextDecoder(); |
|
let buffer = ''; |
|
|
|
while (true) { |
|
const { done, value } = await reader.read(); |
|
if (done) break; |
|
|
|
const chunk = decoder.decode(value); |
|
buffer += chunk; |
|
|
|
|
|
const processedContent = processSSEContent(buffer); |
|
|
|
if (processedContent) { |
|
setTaskResults(prev => { |
|
const currentContent = prev[section.id || index] || ''; |
|
return { |
|
...prev, |
|
[section.id || index]: currentContent + processedContent |
|
}; |
|
}); |
|
|
|
|
|
const regex = /data: {"content": "[^"]*"}(?:\r?\n)?/g; |
|
buffer = buffer.replace(regex, ''); |
|
} |
|
} |
|
|
|
|
|
if (buffer) { |
|
const finalContent = processSSEContent(buffer); |
|
if (finalContent) { |
|
setTaskResults(prev => { |
|
const currentContent = prev[section.id || index] || ''; |
|
return { |
|
...prev, |
|
[section.id || index]: currentContent + finalContent |
|
}; |
|
}); |
|
} |
|
} |
|
|
|
|
|
setCurrentTaskIndex(index + 1); |
|
} catch (error) { |
|
console.error(`Error executing task for section ${section.id || index}:`, error); |
|
setTaskResults(prev => ({ |
|
...prev, |
|
[section.id || index]: '获取数据失败,请重试...' |
|
})); |
|
|
|
|
|
setCurrentTaskIndex(index + 1); |
|
} |
|
}; |
|
|
|
|
|
if (currentTaskIndex >= 0 && currentStep === 3) { |
|
executeTask(currentTaskIndex); |
|
} |
|
}, [currentTaskIndex, researchSections, currentStep]); |
|
|
|
useEffect(() => { |
|
if (stepRefs.current[currentStep]) { |
|
stepRefs.current[currentStep].scrollIntoView({ |
|
behavior: "smooth", |
|
block: "start" |
|
}); |
|
} |
|
}, [currentStep]); |
|
|
|
useEffect(() => { |
|
if ((currentStep === 3 || currentStep === 4) && responseRef.current) { |
|
responseRef.current.scrollTop = responseRef.current.scrollHeight; |
|
} |
|
}, [taskResults,responseContent]); |
|
|
|
|
|
useEffect(() => { |
|
let timer; |
|
if (countdown > 0) { |
|
timer = setTimeout(() => setCountdown(countdown - 1), 1000); |
|
} |
|
|
|
return () => clearTimeout(timer); |
|
}, [countdown]); |
|
|
|
|
|
useEffect(() => { |
|
let timer; |
|
if (researchCountdown > 0) { |
|
timer = setTimeout(() => setResearchCountdown(researchCountdown - 1), 1000); |
|
} |
|
|
|
return () => clearTimeout(timer); |
|
}, [researchCountdown]); |
|
|
|
|
|
const goToNextStep = () => { |
|
if (currentStep < 4) { |
|
setCurrentStep(currentStep + 1); |
|
} |
|
}; |
|
|
|
|
|
useEffect(() => { |
|
|
|
if (window.mermaid) { |
|
window.mermaid.initialize({ |
|
startOnLoad: true, |
|
theme: 'neutral', |
|
securityLevel: 'loose', |
|
fontSize: 16, |
|
fontFamily: 'sans-serif', |
|
flowchart: { |
|
htmlLabels: true, |
|
curve: 'basis' |
|
}, |
|
er: { |
|
useMaxWidth: true |
|
}, |
|
sequence: { |
|
useMaxWidth: true, |
|
diagramMarginX: 50, |
|
diagramMarginY: 10, |
|
actorMargin: 50, |
|
width: 150, |
|
height: 65 |
|
}, |
|
pie: { |
|
useWidth: true |
|
} |
|
}); |
|
} |
|
}, []); |
|
|
|
|
|
useEffect(() => { |
|
const processMermaidDiagrams = () => { |
|
|
|
if (window.mermaid) { |
|
|
|
const mermaidDiagrams = document.querySelectorAll('.mermaid:not(.processed)'); |
|
|
|
if (mermaidDiagrams.length > 0) { |
|
|
|
mermaidDiagrams.forEach(diagram => { |
|
diagram.classList.add('processed'); |
|
}); |
|
|
|
|
|
try { |
|
window.mermaid.init(undefined, mermaidDiagrams); |
|
} catch (e) { |
|
console.error('Error rendering mermaid diagrams:', e); |
|
} |
|
} |
|
} |
|
}; |
|
|
|
|
|
if (Object.keys(taskResults).length > 0) { |
|
|
|
setTimeout(processMermaidDiagrams, 100); |
|
} |
|
}, [taskResults, expandedSections]); |
|
|
|
|
|
useEffect(() => { |
|
const processFinalReportMermaid = () => { |
|
|
|
if (responseContent && currentStep === 4 && responseRef.current) { |
|
if (window.mermaid) { |
|
try { |
|
|
|
const mermaidDiagrams = responseRef.current.querySelectorAll('.mermaid:not(.processed)'); |
|
|
|
if (mermaidDiagrams.length > 0) { |
|
|
|
mermaidDiagrams.forEach(diagram => { |
|
diagram.classList.add('processed'); |
|
}); |
|
|
|
|
|
window.mermaid.init(undefined, mermaidDiagrams); |
|
} |
|
} catch (e) { |
|
console.error('Error rendering mermaid diagrams in final report:', e); |
|
} |
|
} |
|
} |
|
}; |
|
|
|
|
|
if (responseContent && currentStep === 4) { |
|
|
|
processFinalReportMermaid(); |
|
|
|
|
|
setTimeout(processFinalReportMermaid, 100); |
|
setTimeout(processFinalReportMermaid, 500); |
|
setTimeout(processFinalReportMermaid, 1000); |
|
} |
|
}, [responseContent, currentStep]); |
|
|
|
useEffect(() => { |
|
|
|
if (window.MathJax && (responseContent || Object.keys(taskResults).length > 0)) { |
|
setTimeout(() => { |
|
if (window.MathJax.typeset) { |
|
window.MathJax.typeset(); |
|
} |
|
}, 100); |
|
} |
|
}, [responseContent, taskResults, expandedSections]); |
|
|
|
|
|
const renderMarkdown = (text) => { |
|
if (!text) return <div>No content to display</div>; |
|
|
|
|
|
if (window.marked) { |
|
|
|
const renderer = new window.marked.Renderer(); |
|
const originalCodeRenderer = renderer.code.bind(renderer); |
|
|
|
|
|
renderer.code = (code, language) => { |
|
|
|
if (language === 'mermaid' || |
|
code.trim().startsWith('graph ') || |
|
code.trim().startsWith('pie ') || |
|
code.trim().startsWith('flowchart ') || |
|
code.trim().startsWith('sequenceDiagram ') || |
|
code.trim().startsWith('gantt ') || |
|
code.trim().startsWith('classDiagram ') || |
|
code.trim().startsWith('stateDiagram') || |
|
code.trim().startsWith('erDiagram') || |
|
code.trim().startsWith('journey') || |
|
code.trim().startsWith('gitGraph') || |
|
/graph\s+TD\s*\n/.test(code)) { |
|
|
|
|
|
if (language !== 'mermaid') { |
|
language = 'mermaid'; |
|
} |
|
const id = 'mermaid-diagram-' + Math.random().toString(36).substring(2, 15); |
|
return `<div class="mermaid-container" style="margin:1rem 0;text-align:center;"><pre class="mermaid" id="${id}">${code}</pre></div>`; |
|
} |
|
|
|
return `<pre style="margin:0.75rem 0;padding:1rem;overflow-x:auto;background-color:#f8f9fa;border-radius:0.25rem;"><code class="language-${language || ''}">${originalCodeRenderer(code, language)}</code></pre>`; |
|
}; |
|
|
|
|
|
const processLatex = (text) => { |
|
|
|
let processed = text.replace(/\\\$/g, 'ESCAPED_DOLLAR'); |
|
|
|
|
|
processed = processed.replace(/\$\$([\s\S]*?)\$\$/g, '<div class="math-display">$$$$1$$</div>'); |
|
|
|
|
|
processed = processed.replace(/\$([^\$]*?)\$/g, '<span class="math-inline">\\($1\\)</span>'); |
|
|
|
|
|
processed = processed.replace(/ESCAPED_DOLLAR/g, '\\$'); |
|
|
|
return processed; |
|
}; |
|
|
|
text = processLatex(text); |
|
|
|
|
|
renderer.paragraph = (text) => { |
|
return `<p style="margin-top:0.5rem;margin-bottom:0.5rem;">${text}</p>`; |
|
}; |
|
|
|
renderer.heading = (text, level) => { |
|
return `<h${level} style="margin-top:1.25rem;margin-bottom:0.75rem;font-weight:600;">${text}</h${level}>`; |
|
}; |
|
|
|
renderer.list = (body, ordered) => { |
|
const type = ordered ? 'ol' : 'ul'; |
|
return `<${type} style="margin-top:0.5rem;margin-bottom:0.5rem;padding-left:1.5rem;">${body}</${type}>`; |
|
}; |
|
|
|
renderer.listitem = (text) => { |
|
return `<li style="margin-bottom:0.25rem;">${text}</li>`; |
|
}; |
|
|
|
renderer.blockquote = (quote) => { |
|
return `<blockquote style="margin:0.75rem 0;padding-left:1rem;border-left:4px solid #e2e8f0;color:#4a5568;">${quote}</blockquote>`; |
|
}; |
|
|
|
|
|
const customMarked = window.marked.setOptions({ |
|
renderer: renderer, |
|
highlight: function(code, lang) { |
|
if (window.hljs && lang) { |
|
try { |
|
return window.hljs.highlight(code, { language: lang }).value; |
|
} catch (e) { |
|
return code; |
|
} |
|
} |
|
return code; |
|
}, |
|
pedantic: false, |
|
gfm: true, |
|
breaks: true, |
|
sanitize: false, |
|
smartypants: true, |
|
xhtml: false |
|
}); |
|
|
|
return ( |
|
<div |
|
className="markdown-content" |
|
style={{fontSize: '16px', lineHeight: '1.5'}} |
|
dangerouslySetInnerHTML={{ __html: customMarked.parse(text) }} |
|
/> |
|
); |
|
} else { |
|
|
|
return ( |
|
<div |
|
style={{fontSize: '16px', lineHeight: '1.5'}} |
|
dangerouslySetInnerHTML={{ __html: text.replace(/\n/g, '<br/>') }} |
|
/> |
|
); |
|
} |
|
}; |
|
|
|
const processSSEContent = (text) => { |
|
let result = ''; |
|
const regex = /data: {"content": "([^"]*)"}(?:\r?\n)?/g; |
|
let match; |
|
|
|
while ((match = regex.exec(text)) !== null) { |
|
try { |
|
|
|
const decodedContent = JSON.parse(`"${match[1]}"`); |
|
result += decodedContent; |
|
} catch (e) { |
|
|
|
result += match[1]; |
|
} |
|
} |
|
|
|
return result; |
|
}; |
|
|
|
const processMarkdownWithMermaid = (content) => { |
|
if (!content) return <div>No content to display</div>; |
|
|
|
|
|
const blocks = []; |
|
let lastIndex = 0; |
|
|
|
|
|
const mermaidRegex = /```(?:mermaid)?\s*([\s\S]*?)```/g; |
|
let match; |
|
|
|
while ((match = mermaidRegex.exec(content)) !== null) { |
|
|
|
if (match.index > lastIndex) { |
|
blocks.push({ |
|
type: 'text', |
|
content: content.substring(lastIndex, match.index) |
|
}); |
|
} |
|
|
|
|
|
const mermaidCode = match[1].trim(); |
|
const isMermaid = |
|
mermaidCode.startsWith('graph ') || |
|
mermaidCode.startsWith('pie ') || |
|
mermaidCode.startsWith('flowchart ') || |
|
mermaidCode.startsWith('sequenceDiagram') || |
|
mermaidCode.startsWith('gantt') || |
|
mermaidCode.startsWith('classDiagram') || |
|
mermaidCode.startsWith('stateDiagram') || |
|
mermaidCode.startsWith('erDiagram') || |
|
mermaidCode.startsWith('journey') || |
|
mermaidCode.startsWith('gitGraph'); |
|
|
|
if (isMermaid) { |
|
blocks.push({ |
|
type: 'mermaid', |
|
content: mermaidCode |
|
}); |
|
} else { |
|
|
|
blocks.push({ |
|
type: 'text', |
|
content: match[0] |
|
}); |
|
} |
|
|
|
lastIndex = match.index + match[0].length; |
|
} |
|
|
|
|
|
if (lastIndex < content.length) { |
|
blocks.push({ |
|
type: 'text', |
|
content: content.substring(lastIndex) |
|
}); |
|
} |
|
|
|
|
|
return blocks.map((block, index) => { |
|
if (block.type === 'mermaid') { |
|
return renderMermaidDiagram(block.content, `final-mermaid-${index}`); |
|
} else { |
|
return renderMarkdown(block.content); |
|
} |
|
}); |
|
}; |
|
|
|
const renderMermaidDiagram = (code, customId) => { |
|
const id = customId || 'mermaid-direct-' + Math.random().toString(36).substring(2, 15); |
|
|
|
const MermaidDiagram = () => { |
|
useEffect(() => { |
|
const renderDiagram = () => { |
|
if (window.mermaid) { |
|
try { |
|
const element = document.getElementById(id); |
|
if (element && !element.classList.contains('processed')) { |
|
element.classList.add('processed'); |
|
window.mermaid.init(undefined, element); |
|
} |
|
} catch (e) { |
|
console.error('Error rendering mermaid diagram:', e, code); |
|
} |
|
} |
|
}; |
|
|
|
|
|
renderDiagram(); |
|
const timers = [ |
|
setTimeout(renderDiagram, 100), |
|
setTimeout(renderDiagram, 500), |
|
setTimeout(renderDiagram, 1000) |
|
]; |
|
|
|
return () => timers.forEach(timer => clearTimeout(timer)); |
|
}, []); |
|
|
|
return ( |
|
<div className="mermaid-container my-4 text-center border border-blue-100 p-4 rounded"> |
|
<pre className="mermaid" id={id}>{code}</pre> |
|
</div> |
|
); |
|
}; |
|
|
|
return <MermaidDiagram key={id} />; |
|
}; |
|
|
|
|
|
const processReportContent = (content) => { |
|
return processMarkdownWithMermaid(content); |
|
}; |
|
|
|
|
|
|
|
return ( |
|
<div className="flex flex-col h-screen bg-blue-50 text-blue-900"> |
|
<header className="bg-blue-800 text-white p-4 shadow-md"> |
|
<h1 className="text-xl font-bold">深度研究</h1> |
|
</header> |
|
|
|
<div className="flex-grow overflow-auto p-4"> |
|
{/* Step 1: Research Direction */} |
|
<div ref={(el) => stepRefs.current[1] = el} className={`mb-6 p-4 bg-white rounded-lg shadow-md transition-all duration-300 ${currentStep === 1 ? 'opacity-100' : 'opacity-50'}`}> |
|
<h2 className="text-lg font-bold border-b border-blue-200 pb-2 mb-4">1. 研究方向</h2> |
|
|
|
<div className="mb-4"> |
|
<label className="block text-sm font-medium mb-1">研究主题</label> |
|
<input |
|
type="text" |
|
className="w-full p-2 border border-blue-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" |
|
disabled={currentStep !== 1 || isLoading} |
|
value={researchTopic} |
|
onChange={(e) => setResearchTopic(e.target.value)} |
|
/> |
|
</div> |
|
|
|
<button |
|
className={`w-full py-2 rounded text-white font-medium transition-all duration-300 ${ |
|
currentStep === 1 && !isLoading && researchTopic.trim() |
|
? 'bg-blue-600 hover:bg-blue-700 active:bg-blue-800' |
|
: 'bg-gray-400 cursor-not-allowed' |
|
}`} |
|
onClick={() => { |
|
if (currentStep === 1 && !isLoading && researchTopic.trim()) { |
|
fetchThinkingData(); |
|
} |
|
}} |
|
disabled={currentStep !== 1 || !researchTopic.trim() || isLoading} |
|
> |
|
{isLoading ? `开始思考中 (${countdown > 0 ? countdown : '...'})` : '开始思考'} |
|
</button> |
|
</div> |
|
|
|
{/* Step 2: Research Method */} |
|
<div ref={(el) => stepRefs.current[2] = el} className={`mb-6 p-4 bg-white rounded-lg shadow-md transition-all duration-300 ${currentStep === 2 ? 'opacity-100' : 'opacity-50'}`}> |
|
<h2 className="text-lg font-bold border-b border-blue-200 pb-2 mb-4">2. 提出您的想法</h2> |
|
|
|
<div className="mb-4 text-sm"> |
|
{thinkingOutput ? ( |
|
<div className="p-3 bg-blue-50 rounded shadow-inner"> |
|
{renderMarkdown(thinkingOutput)} |
|
</div> |
|
) : ( |
|
<p className="text-gray-500 italic">AI 思考的结果将在这里显示...</p> |
|
)} |
|
</div> |
|
|
|
<div className="mb-4"> |
|
<textarea |
|
className="w-full p-2 border border-blue-300 rounded h-24 focus:outline-none focus:ring-2 focus:ring-blue-500" |
|
placeholder="输入回答,读取您的想法..." |
|
disabled={currentStep !== 2 || researchLoading} |
|
value={userThoughts} |
|
onChange={(e) => setUserThoughts(e.target.value)} |
|
/> |
|
</div> |
|
|
|
<button |
|
className={`w-full py-2 rounded text-white font-medium transition-all duration-300 ${ |
|
currentStep === 2 && !researchLoading && userThoughts.trim() |
|
? 'bg-blue-600 hover:bg-blue-700 active:bg-blue-800' |
|
: 'bg-gray-400 cursor-not-allowed' |
|
}`} |
|
onClick={() => { |
|
if (currentStep === 2 && !researchLoading && userThoughts.trim()) { |
|
fetchResearchData(); |
|
} |
|
}} |
|
disabled={currentStep !== 2 || researchLoading || !userThoughts.trim()} |
|
> |
|
{researchLoading ? `开始研究中 (${researchCountdown > 0 ? researchCountdown : '...'})` : '开始研究'} |
|
</button> |
|
</div> |
|
|
|
{/* Step 3: Information Sources */} |
|
<div ref={(el) => stepRefs.current[3] = el} className={`mb-6 p-4 bg-white rounded-lg shadow-md transition-all duration-300 ${currentStep === 3 ? 'opacity-100' : 'opacity-50'}`}> |
|
<h2 className="text-lg font-bold border-b border-blue-200 pb-2 mb-4">3. 信息搜集</h2> |
|
|
|
<div className="mb-4"> |
|
{researchSections.length > 0 ? ( |
|
researchSections.map((section, index) => ( |
|
<div |
|
key={section.id || index} |
|
className="mb-2 border border-blue-200 rounded overflow-hidden" |
|
> |
|
<div |
|
className="flex items-center justify-between p-3 bg-blue-100 cursor-pointer" |
|
onClick={() => toggleSection(section.id || index)} |
|
> |
|
<div className="flex items-center"> |
|
<div className="w-6 h-6 rounded-full flex items-center justify-center bg-blue-500 text-white mr-2"> |
|
{expandedSections[section.id || index] ? '▼' : '►'} |
|
</div> |
|
<span>{section.query}</span> |
|
</div> |
|
<div className="flex items-center"> |
|
{/* Status indicator */} |
|
{currentTaskIndex > index ? ( |
|
<span className="px-2 py-1 bg-green-100 text-green-700 text-xs rounded-full flex items-center"> |
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path> |
|
</svg> |
|
完成 |
|
</span> |
|
) : currentTaskIndex === index && taskLoading ? ( |
|
<span className="px-2 py-1 bg-yellow-100 text-yellow-700 text-xs rounded-full flex items-center"> |
|
<svg className="w-4 h-4 mr-1 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path> |
|
</svg> |
|
处理中 |
|
</span> |
|
) : currentTaskIndex < index ? ( |
|
<span className="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full flex items-center"> |
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path> |
|
</svg> |
|
等待中 |
|
</span> |
|
) : null} |
|
</div> |
|
</div> |
|
|
|
{expandedSections[section.id || index] && ( |
|
<div className="p-3 bg-white"> |
|
<p className="text-sm text-gray-700 mb-2">{section.researchGoal}</p> |
|
{taskResults[section.id || index] ? ( |
|
<div className="mt-4 p-3 bg-blue-50 rounded"> |
|
{renderMarkdown(taskResults[section.id || index])} |
|
</div> |
|
) : ( |
|
currentTaskIndex === index && taskLoading ? ( |
|
<div className="mt-4 p-3 bg-blue-50 rounded text-gray-600"> |
|
<div className="flex items-center"> |
|
<svg className="w-5 h-5 mr-2 animate-spin text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path> |
|
</svg> |
|
<p>正在获取研究结果,请稍候...</p> |
|
</div> |
|
</div> |
|
) : ( |
|
<div className="mt-4 p-3 bg-blue-50 rounded text-gray-600"> |
|
<div className="flex items-center"> |
|
<svg className="w-5 h-5 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path> |
|
</svg> |
|
<p>尚未获取研究结果</p> |
|
</div> |
|
</div> |
|
) |
|
)} |
|
</div> |
|
)} |
|
</div> |
|
)) |
|
) : ( |
|
<p className="text-gray-500 italic">研究结果将在这里显示...</p> |
|
)} |
|
</div> |
|
|
|
<div className="flex space-x-2"> |
|
<button |
|
className={`flex-1 py-2 rounded text-white font-medium transition-all duration-300 ${ |
|
currentStep === 3 && !isLoading && !(currentTaskIndex < researchSections.length && currentTaskIndex >= 0) |
|
? 'bg-green-600 hover:bg-green-700 active:bg-green-800' |
|
: 'bg-gray-400 cursor-not-allowed' |
|
}`} |
|
onClick={() => { |
|
if (currentStep === 3 && !isLoading && !(currentTaskIndex < researchSections.length && currentTaskIndex >= 0)) { |
|
fetchReportData(); |
|
goToNextStep(); |
|
} |
|
}} |
|
disabled={currentStep !== 3 || isLoading || (currentTaskIndex < researchSections.length && currentTaskIndex >= 0)} |
|
> |
|
生成报告 |
|
</button> |
|
</div> |
|
</div> |
|
|
|
{/* Step 4: Final Report */} |
|
<div ref={(el) => stepRefs.current[4] = el} className={`p-4 bg-white rounded-lg shadow-md transition-all duration-300 ${currentStep === 4 ? 'opacity-100' : 'opacity-50'}`}> |
|
<h2 className="text-lg font-bold border-b border-blue-200 pb-2 mb-4">4. 最终报告</h2> |
|
|
|
<div |
|
ref={responseRef} |
|
className="p-4 bg-blue-50 rounded min-h-64 mb-4 overflow-auto" |
|
> |
|
{responseContent ? processReportContent(responseContent) : '等待数据加载...'} |
|
</div> |
|
|
|
<div className="flex space-x-2"> |
|
<button |
|
className={`flex-1 py-2 rounded text-white font-medium transition-all duration-300 ${ |
|
currentStep === 4 && !isLoading |
|
? 'bg-green-600 hover:bg-green-700 active:bg-green-800' |
|
: 'bg-gray-400 cursor-not-allowed' |
|
}`} |
|
onClick={() => { |
|
handleDownloadPDF() |
|
}} |
|
> |
|
导出报告 |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="mt-6 py-4 bg-gray-100"> |
|
<div class="container mx-auto max-w-5xl px-4 text-center text-gray-600"> |
|
<p>© 2025 Deep Research. Create By 飙猪狂</p> |
|
</div> |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
ReactDOM.render(<ResearchInterface />, document.getElementById('root')); |
|
</script> |
|
</body> |
|
</html> |