feat: add 5 dashboard features — dark mode, trade history, backtests, model insights, alerts
- Dark mode: class-based theme toggle with localStorage persistence and flash prevention - Trade History (/trades): paginated table, stats cards, equity curve chart with DB API endpoints - Backtest Viewer (/backtests): log parser for 35 backtest results, sidebar + detail + comparison tabs - Model Insights: dashboard card + dialog showing feature importance, regime distribution, training history - Alert/Signal Log (/alerts): signal stats, filterable table with execution tracking - API: 8 new endpoints with psycopg2 DB connection pool - Dark mode sweep across books page, about dialog, and all dashboard components - Architecture docs rewritten with Mermaid diagrams (23 docs) - README and FEATURES.md rewritten bilingual (Indonesian + English) - main_live.py: write model_metrics.json on startup and retrain Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
b2dc2dacd7
commit
e8355b3f62
@@ -0,0 +1,170 @@
|
||||
"use client";
|
||||
|
||||
import { Children, isValidElement, useMemo } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import type { Components } from "react-markdown";
|
||||
import { MermaidDiagram } from "./mermaid-diagram";
|
||||
|
||||
interface MarkdownRendererProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
function getTextContent(node: React.ReactNode): string {
|
||||
if (typeof node === "string") return node;
|
||||
if (typeof node === "number") return String(node);
|
||||
if (!node) return "";
|
||||
if (Array.isArray(node)) return node.map(getTextContent).join("");
|
||||
if (isValidElement(node) && (node.props as Record<string, unknown>)?.children)
|
||||
return getTextContent((node.props as Record<string, unknown>).children as React.ReactNode);
|
||||
return "";
|
||||
}
|
||||
|
||||
const components: Components = {
|
||||
// ── Headings ──
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-[2rem] font-bold text-slate-800 mt-10 mb-5 pb-3 border-b-2 border-blue-200/60 first:mt-0">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-[1.6rem] font-semibold text-slate-800 mt-9 mb-4 pb-2 border-b border-slate-200/80">
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="text-[1.35rem] font-semibold text-slate-700 mt-6 mb-3 pl-3 border-l-3 border-blue-400/40">
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
h4: ({ children }) => (
|
||||
<h4 className="text-[1.15rem] font-semibold text-slate-700 mt-5 mb-2">
|
||||
{children}
|
||||
</h4>
|
||||
),
|
||||
|
||||
// ── Paragraphs & text ──
|
||||
p: ({ children }) => (
|
||||
<p className="text-[1.05rem] leading-[1.8] text-slate-600 mb-4">
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
strong: ({ children }) => (
|
||||
<strong className="font-semibold text-slate-800">{children}</strong>
|
||||
),
|
||||
em: ({ children }) => (
|
||||
<em className="italic text-slate-500">{children}</em>
|
||||
),
|
||||
|
||||
// ── Lists ──
|
||||
ul: ({ children }) => (
|
||||
<ul className="list-disc list-outside ml-5 mb-4 space-y-1.5 text-[1.05rem] text-slate-600">
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="list-decimal list-outside ml-5 mb-4 space-y-1.5 text-[1.05rem] text-slate-600">
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
li: ({ children }) => <li className="leading-[1.7] pl-1">{children}</li>,
|
||||
|
||||
// ── Links ──
|
||||
a: ({ href, children }) => (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800 underline underline-offset-2 decoration-blue-300 hover:decoration-blue-500 transition-colors"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
|
||||
// ── Blockquotes ──
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-[3px] border-amber-400/60 pl-4 py-2 my-4 bg-amber-50/40 rounded-r-lg">
|
||||
<div className="text-slate-600 [&>p]:text-slate-600 [&>p]:mb-1">{children}</div>
|
||||
</blockquote>
|
||||
),
|
||||
|
||||
// ── Code blocks — with Mermaid detection ──
|
||||
pre: ({ children }) => {
|
||||
const child = Children.only(children);
|
||||
const childProps = isValidElement(child) ? (child.props as Record<string, unknown>) : null;
|
||||
if (childProps?.className === "language-mermaid") {
|
||||
const chart = getTextContent(childProps.children as React.ReactNode);
|
||||
return <MermaidDiagram chart={chart} />;
|
||||
}
|
||||
return (
|
||||
<pre className="my-4 p-4 rounded-xl bg-slate-50/80 border border-slate-200/80 overflow-x-auto text-[0.95rem] leading-relaxed">
|
||||
{children}
|
||||
</pre>
|
||||
);
|
||||
},
|
||||
code: ({ className, children }) => {
|
||||
const isBlock = className?.startsWith("language-");
|
||||
if (isBlock) {
|
||||
return (
|
||||
<code className="block font-mono text-slate-700">{children}</code>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<code className="px-1.5 py-0.5 rounded-md bg-blue-50/70 text-blue-700 font-mono text-[0.95rem] border border-blue-100/50">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
|
||||
// ── Tables — Excel-style ──
|
||||
table: ({ children }) => (
|
||||
<div className="my-5 overflow-x-auto rounded-xl border border-slate-200 shadow-sm">
|
||||
<table className="w-full text-[1rem] border-collapse">{children}</table>
|
||||
</div>
|
||||
),
|
||||
thead: ({ children }) => (
|
||||
<thead className="bg-gradient-to-b from-slate-100 to-slate-50 border-b-2 border-slate-200">
|
||||
{children}
|
||||
</thead>
|
||||
),
|
||||
th: ({ children }) => (
|
||||
<th className="px-4 py-2.5 text-left font-semibold text-slate-700 text-[0.95rem] uppercase tracking-wide border-r border-slate-200/60 last:border-r-0">
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
tbody: ({ children }) => <tbody className="divide-y divide-slate-100">{children}</tbody>,
|
||||
tr: ({ children }) => (
|
||||
<tr className="hover:bg-blue-50/30 transition-colors even:bg-slate-50/40">
|
||||
{children}
|
||||
</tr>
|
||||
),
|
||||
td: ({ children }) => (
|
||||
<td className="px-4 py-2.5 text-slate-600 border-r border-slate-100/80 last:border-r-0 align-top">
|
||||
{children}
|
||||
</td>
|
||||
),
|
||||
|
||||
// ── Horizontal rule ──
|
||||
hr: () => (
|
||||
<hr className="my-8 border-0 h-px bg-gradient-to-r from-transparent via-slate-300 to-transparent" />
|
||||
),
|
||||
|
||||
// ── Images (placeholder) ──
|
||||
img: ({ alt }) => (
|
||||
<span className="block my-4 text-center text-slate-400 text-sm italic">
|
||||
[{alt || "image"}]
|
||||
</span>
|
||||
),
|
||||
};
|
||||
|
||||
export function MarkdownRenderer({ content }: MarkdownRendererProps) {
|
||||
const processedContent = useMemo(() => content, [content]);
|
||||
|
||||
return (
|
||||
<div className="max-w-none prose-slate">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
|
||||
{processedContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user