Files
Atomizer/atomizer-dashboard/frontend/src/components/StudyReportViewer.tsx
Antoine f8b90156b3 feat: Improve dashboard performance and Claude terminal context
- Add trial limiting (300 max) and reduce polling to 15s for large studies
- Make dashboard layout wider with col-span adjustments
- Claude terminal now runs from Atomizer root for CLAUDE.md/skills access
- Add study context display in terminal on connect
- Add KaTeX math rendering styles for study reports
- Add surrogate tuner module for hyperparameter optimization
- Fix backend proxy to port 8001

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 17:36:00 -05:00

279 lines
10 KiB
TypeScript

/**
* Study Report Viewer
* Displays the STUDY_REPORT.md file with proper markdown rendering
* Includes math equation support via KaTeX and syntax highlighting
*/
import { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import 'katex/dist/katex.min.css';
import { FileText, X, ExternalLink, RefreshCw } from 'lucide-react';
interface StudyReportViewerProps {
studyId: string;
studyPath?: string;
}
export function StudyReportViewer({ studyId, studyPath }: StudyReportViewerProps) {
const [isOpen, setIsOpen] = useState(false);
const [markdown, setMarkdown] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchReport = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/optimization/studies/${studyId}/report`);
if (!response.ok) {
if (response.status === 404) {
setError('No STUDY_REPORT.md found for this study');
} else {
throw new Error(`HTTP ${response.status}`);
}
return;
}
const data = await response.json();
setMarkdown(data.content);
} catch (err) {
setError(`Failed to load report: ${err}`);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (isOpen && !markdown) {
fetchReport();
}
}, [isOpen, studyId]);
if (!isOpen) {
return (
<button
onClick={() => setIsOpen(true)}
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors text-sm font-medium"
>
<FileText size={16} />
Study Report
</button>
);
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70">
<div className="bg-dark-800 rounded-xl shadow-2xl w-[95vw] max-w-7xl h-[90vh] flex flex-col border border-dark-600">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-dark-600">
<div className="flex items-center gap-3">
<FileText className="text-primary-400" size={24} />
<div>
<h2 className="text-lg font-semibold text-dark-100">Study Report</h2>
<p className="text-sm text-dark-400">{studyId}/STUDY_REPORT.md</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={fetchReport}
className="p-2 hover:bg-dark-600 rounded-lg transition-colors"
title="Refresh"
>
<RefreshCw size={18} className={`text-dark-300 ${loading ? 'animate-spin' : ''}`} />
</button>
{studyPath && (
<a
href={`file:///${studyPath.replace(/\\/g, '/')}/STUDY_REPORT.md`}
target="_blank"
rel="noopener noreferrer"
className="p-2 hover:bg-dark-600 rounded-lg transition-colors"
title="Open in editor"
>
<ExternalLink size={18} className="text-dark-300" />
</a>
)}
<button
onClick={() => setIsOpen(false)}
className="p-2 hover:bg-dark-600 rounded-lg transition-colors"
>
<X size={20} className="text-dark-300" />
</button>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-auto p-8 bg-dark-700">
{loading && (
<div className="flex items-center justify-center h-full">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-400"></div>
</div>
)}
{error && (
<div className="flex flex-col items-center justify-center h-full text-dark-400">
<FileText size={48} className="mb-4 opacity-50" />
<p>{error}</p>
<button
onClick={fetchReport}
className="mt-4 px-4 py-2 text-sm bg-dark-600 hover:bg-dark-500 text-dark-200 rounded-lg transition-colors"
>
Try Again
</button>
</div>
)}
{markdown && !loading && (
<article className="markdown-body">
<ReactMarkdown
remarkPlugins={[remarkGfm, [remarkMath, { singleDollarTextMath: false }]]}
rehypePlugins={[[rehypeKatex, { strict: false, trust: true, output: 'html' }]]}
components={{
// Custom heading styles
h1: ({children}) => (
<h1 className="text-3xl font-bold text-dark-50 mb-6 pb-3 border-b border-dark-500">
{children}
</h1>
),
h2: ({children}) => (
<h2 className="text-2xl font-semibold text-dark-100 mt-8 mb-4 pb-2 border-b border-dark-600">
{children}
</h2>
),
h3: ({children}) => (
<h3 className="text-xl font-semibold text-dark-100 mt-6 mb-3">
{children}
</h3>
),
h4: ({children}) => (
<h4 className="text-lg font-medium text-dark-200 mt-4 mb-2">
{children}
</h4>
),
// Paragraph styling
p: ({children}) => (
<p className="text-dark-200 leading-relaxed mb-4">
{children}
</p>
),
// List styling
ul: ({children}) => (
<ul className="list-disc list-inside text-dark-200 mb-4 space-y-1 ml-2">
{children}
</ul>
),
ol: ({children}) => (
<ol className="list-decimal list-inside text-dark-200 mb-4 space-y-1 ml-2">
{children}
</ol>
),
li: ({children}) => (
<li className="text-dark-200 leading-relaxed">
{children}
</li>
),
// Strong/bold text
strong: ({children}) => (
<strong className="font-semibold text-dark-100">
{children}
</strong>
),
// Emphasis/italic
em: ({children}) => (
<em className="italic text-dark-200">
{children}
</em>
),
// Links
a: ({href, children}) => (
<a
href={href}
className="text-primary-400 hover:text-primary-300 underline decoration-primary-400/30 hover:decoration-primary-300"
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
),
// Inline code
code: ({className, children, ...props}) => {
const isInline = !className;
if (isInline) {
return (
<code className="bg-dark-800 text-primary-300 px-1.5 py-0.5 rounded text-sm font-mono">
{children}
</code>
);
}
// Code block
return (
<code className={`${className} text-sm`} {...props}>
{children}
</code>
);
},
// Code blocks with pre wrapper
pre: ({children}) => (
<pre className="bg-dark-900 text-dark-100 p-4 rounded-lg overflow-x-auto mb-4 text-sm font-mono border border-dark-600">
{children}
</pre>
),
// Blockquotes
blockquote: ({children}) => (
<blockquote className="border-l-4 border-primary-500 pl-4 py-1 my-4 bg-dark-800/50 rounded-r italic text-dark-300">
{children}
</blockquote>
),
// Tables
table: ({children}) => (
<div className="overflow-x-auto mb-6">
<table className="min-w-full border-collapse border border-dark-500 text-sm">
{children}
</table>
</div>
),
thead: ({children}) => (
<thead className="bg-dark-600">
{children}
</thead>
),
th: ({children}) => (
<th className="border border-dark-500 px-4 py-2 text-left font-semibold text-dark-100">
{children}
</th>
),
td: ({children}) => (
<td className="border border-dark-500 px-4 py-2 text-dark-200">
{children}
</td>
),
tr: ({children}) => (
<tr className="hover:bg-dark-600/50 transition-colors">
{children}
</tr>
),
// Horizontal rule
hr: () => (
<hr className="border-dark-500 my-8" />
),
// Images
img: ({src, alt}) => (
<img
src={src}
alt={alt || ''}
className="max-w-full h-auto rounded-lg border border-dark-500 my-4"
/>
),
}}
>
{markdown}
</ReactMarkdown>
</article>
)}
</div>
</div>
</div>
);
}