feat: Enhance dashboard with charts, study report viewer, and pruning tracking
- Add ConvergencePlot component with running best, statistics, gradient fill - Add ParameterImportanceChart with Pearson correlation analysis - Add StudyReportViewer with KaTeX math rendering and full markdown support - Update pruning endpoint to query Optuna database directly - Add /report endpoint for STUDY_REPORT.md files - Fix chart data transformation for single/multi-objective studies - Update Protocol 13 documentation with new components - Update generate-report skill with dashboard integration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
278
atomizer-dashboard/frontend/src/components/StudyReportViewer.tsx
Normal file
278
atomizer-dashboard/frontend/src/components/StudyReportViewer.tsx
Normal file
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* 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-[90vw] max-w-5xl h-[85vh] 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]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user