- 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>
279 lines
10 KiB
TypeScript
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>
|
|
);
|
|
}
|