ai / templates /main.html
1v1's picture
Upload 3 files
a205f59 verified
<!-- templates/index.html -->
<!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>
/* Additional mobile-friendly styles */
@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;
}
}
/* Improve touch target sizes */
@media (max-width: 640px) {
button {
min-height: 44px;
}
input, textarea {
font-size: 16px; /* Prevents iOS zoom on focus */
}
}
/* Improve text reading on small screens */
@media (max-width: 480px) {
.markdown-content {
font-size: 15px;
line-height: 1.6;
}
}
/* Fix Mermaid diagram overflow issues */
.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([]);
// Toggle section expansion
const toggleSection = (sectionId) => {
setExpandedSections(prev => ({
...prev,
[sectionId]: !prev[sectionId]
}));
};
// Fetch from the API with streaming response
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;
// Process the accumulated buffer
const processedContent = processSSEContent(buffer);
if (processedContent) {
setThinkingOutput(prev => prev + processedContent);
// Clear the processed content from the buffer
buffer = buffer.replace(/data: {"content": "[^"]*"}(?:\r?\n)?/g, '');
}
}
// Process any remaining content in the buffer
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();
}
};
// Fetch research results
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();
// Parse the OpenAI response format
const contentString = data.choices[0].message.content;
// Extract the JSON array from markdown code block format ```json ... ```
const jsonMatch = contentString.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
if (jsonMatch && jsonMatch[1]) {
const parsedData = JSON.parse(jsonMatch[1]);
setResearchSections(parsedData);
// Initialize the first task execution after sections are loaded
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();
}
};
// Add this new function to fetch the report data
const fetchReportData = async () => {
setIsLoading(true);
setResponseContent('');
// 准备API调用的数据
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;
// 处理完整的SSE消息
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; // 同时累积到完整内容中
}
// 实时更新UI显示
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>";
// Process content for PDF-friendly format
let processedContent = content;
// Process mermaid diagrams specially for PDF
processedContent = processedContent.replace(/```mermaid([\s\S]*?)```/g, (match, diagramCode) => {
// For PDF export, we could replace with a note about diagrams
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>';
});
// Process math expressions for PDF
processedContent = processedContent.replace(/\$\$([\s\S]*?)\$\$/g, '<div class="math">$1</div>');
processedContent = processedContent.replace(/\$([^\$]*?)\$/g, '<span class="math">$1</span>');
// Add PDF-specific styling
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 {
// Add loading indicator
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}`);
}
// Create Blob for downloading the PDF
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);
}
};
// Execute tasks sequentially
useEffect(() => {
const executeTask = async (index) => {
if (index < 0 || index >= researchSections.length) {
// All tasks are complete or no tasks to execute
setTaskLoading(false);
return;
}
setTaskLoading(true);
const section = researchSections[index];
// Automatically expand the section being processed
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;
// Process the buffer for SSE format content
const processedContent = processSSEContent(buffer);
if (processedContent) {
setTaskResults(prev => {
const currentContent = prev[section.id || index] || '';
return {
...prev,
[section.id || index]: currentContent + processedContent
};
});
// Remove processed parts from buffer
const regex = /data: {"content": "[^"]*"}(?:\r?\n)?/g;
buffer = buffer.replace(regex, '');
}
}
// Process any remaining content
if (buffer) {
const finalContent = processSSEContent(buffer);
if (finalContent) {
setTaskResults(prev => {
const currentContent = prev[section.id || index] || '';
return {
...prev,
[section.id || index]: currentContent + finalContent
};
});
}
}
// Once done, move to the next task
setCurrentTaskIndex(index + 1);
} catch (error) {
console.error(`Error executing task for section ${section.id || index}:`, error);
setTaskResults(prev => ({
...prev,
[section.id || index]: '获取数据失败,请重试...'
}));
// Even on error, move to the next task
setCurrentTaskIndex(index + 1);
}
};
// Only execute if we have a valid index and are in step 3
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]);
// Countdown timer for thinking
useEffect(() => {
let timer;
if (countdown > 0) {
timer = setTimeout(() => setCountdown(countdown - 1), 1000);
}
return () => clearTimeout(timer);
}, [countdown]);
// Countdown timer for research
useEffect(() => {
let timer;
if (researchCountdown > 0) {
timer = setTimeout(() => setResearchCountdown(researchCountdown - 1), 1000);
}
return () => clearTimeout(timer);
}, [researchCountdown]);
// Move to next step
const goToNextStep = () => {
if (currentStep < 4) {
setCurrentStep(currentStep + 1);
}
};
// Initialize marked and mermaid after component mounts
useEffect(() => {
// Initialize mermaid with improved configuration
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
}
});
}
}, []);
// Function to process mermaid diagrams after markdown is rendered
useEffect(() => {
const processMermaidDiagrams = () => {
// Check if the mermaid library is available
if (window.mermaid) {
// Find all mermaid diagram containers that haven't been processed
const mermaidDiagrams = document.querySelectorAll('.mermaid:not(.processed)');
if (mermaidDiagrams.length > 0) {
// Mark diagrams as processed to avoid reprocessing
mermaidDiagrams.forEach(diagram => {
diagram.classList.add('processed');
});
// Run mermaid to render the diagrams
try {
window.mermaid.init(undefined, mermaidDiagrams);
} catch (e) {
console.error('Error rendering mermaid diagrams:', e);
}
}
}
};
// Process diagrams when task results change
if (Object.keys(taskResults).length > 0) {
// Use a small timeout to ensure the DOM has updated
setTimeout(processMermaidDiagrams, 100);
}
}, [taskResults, expandedSections]);
// Add this useEffect to process mermaid diagrams in the final report
useEffect(() => {
const processFinalReportMermaid = () => {
// Only process if we have response content and we're in step 4
if (responseContent && currentStep === 4 && responseRef.current) {
if (window.mermaid) {
try {
// Find all mermaid diagram containers in the final report
const mermaidDiagrams = responseRef.current.querySelectorAll('.mermaid:not(.processed)');
if (mermaidDiagrams.length > 0) {
// Mark diagrams as processed to avoid reprocessing
mermaidDiagrams.forEach(diagram => {
diagram.classList.add('processed');
});
// Run mermaid to render the diagrams
window.mermaid.init(undefined, mermaidDiagrams);
}
} catch (e) {
console.error('Error rendering mermaid diagrams in final report:', e);
}
}
}
};
// Use a small delay to ensure the DOM has been updated with the content
if (responseContent && currentStep === 4) {
// First processing attempt
processFinalReportMermaid();
// Additional attempts with increasing delays to ensure rendering
setTimeout(processFinalReportMermaid, 100);
setTimeout(processFinalReportMermaid, 500);
setTimeout(processFinalReportMermaid, 1000);
}
}, [responseContent, currentStep]);
useEffect(() => {
// Process MathJax rendering after markdown content is updated
if (window.MathJax && (responseContent || Object.keys(taskResults).length > 0)) {
setTimeout(() => {
if (window.MathJax.typeset) {
window.MathJax.typeset();
}
}, 100);
}
}, [responseContent, taskResults, expandedSections]);
// Advanced markdown renderer with support for tables, diagrams, and code
const renderMarkdown = (text) => {
if (!text) return <div>No content to display</div>;
// Use marked library if available, otherwise fallback to basic rendering
if (window.marked) {
// Configure marked with custom renderer
const renderer = new window.marked.Renderer();
const originalCodeRenderer = renderer.code.bind(renderer);
// Override code block rendering
renderer.code = (code, language) => {
// Special handling for mermaid diagrams - improved detection
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)) { // Specific pattern for your case
// Ensure language is set for better processing
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>`;
}
// For other languages, use the original renderer with better styling
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>`;
};
// Process LaTeX formulas before passing to marked
const processLatex = (text) => {
// Avoid processing already escaped dollar signs
let processed = text.replace(/\\\$/g, 'ESCAPED_DOLLAR');
// Process display math ($$...$$)
processed = processed.replace(/\$\$([\s\S]*?)\$\$/g, '<div class="math-display">$$$$1$$</div>');
// Process inline math ($...$)
processed = processed.replace(/\$([^\$]*?)\$/g, '<span class="math-inline">\\($1\\)</span>');
// Restore escaped dollar signs
processed = processed.replace(/ESCAPED_DOLLAR/g, '\\$');
return processed;
};
text = processLatex(text);
// Rest of the renderer configuration remains the same
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>`;
};
// Use the custom renderer for this specific rendering
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 {
// Basic fallback if marked is not loaded
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 {
// Properly decode escaped characters, especially important for code blocks
const decodedContent = JSON.parse(`"${match[1]}"`);
result += decodedContent;
} catch (e) {
// Fallback if parsing fails
result += match[1];
}
}
return result;
};
const processMarkdownWithMermaid = (content) => {
if (!content) return <div>No content to display</div>;
// First, look for Mermaid blocks and extract them
const blocks = [];
let lastIndex = 0;
// Match for "```mermaid ... ```" style blocks
const mermaidRegex = /```(?:mermaid)?\s*([\s\S]*?)```/g;
let match;
while ((match = mermaidRegex.exec(content)) !== null) {
// Add text before the mermaid block
if (match.index > lastIndex) {
blocks.push({
type: 'text',
content: content.substring(lastIndex, match.index)
});
}
// Check if it's actually mermaid content
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 {
// Not a mermaid block, treat as regular code
blocks.push({
type: 'text',
content: match[0] // Include the complete code block
});
}
lastIndex = match.index + match[0].length;
}
// Add any remaining content
if (lastIndex < content.length) {
blocks.push({
type: 'text',
content: content.substring(lastIndex)
});
}
// Render the blocks
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);
}
}
};
// Multiple attempts with delays to ensure rendering
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} />;
};
// Then in your component where you handle the final report rendering:
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>