diff --git a/README.md b/README.md
index 7dfdf28..e6c915a 100644
--- a/README.md
+++ b/README.md
@@ -130,6 +130,10 @@ We welcome contributions! See the [Contributing Guidelines](./CONTRIBUTING.md) f
Every contribution brings us one step closer to a future where AI accelerates scientific breakthroughs for all of humanity.
+
+
+
+
🔝Back to top
## 📚 Acknowledgments
diff --git a/package-lock.json b/package-lock.json
index d1883aa..5a83fc2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@evoscientist/webui",
- "version": "0.1.0",
+ "version": "0.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@evoscientist/webui",
- "version": "0.1.0",
+ "version": "0.1.1",
"license": "Apache-2.0",
"dependencies": {
"@langchain/core": "1.1.19",
diff --git a/package.json b/package.json
index dafb97f..fe9b9d3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@evoscientist/webui",
- "version": "0.1.0",
+ "version": "0.1.1",
"description": "Web UI for EvoScientist — a self-evolving AI scientist built on DeepAgents/LangGraph.",
"author": "Xi Zhang ",
"type": "module",
diff --git a/src/app/components/MermaidDiagram.tsx b/src/app/components/MermaidDiagram.tsx
index 60b38d1..a2d9024 100644
--- a/src/app/components/MermaidDiagram.tsx
+++ b/src/app/components/MermaidDiagram.tsx
@@ -8,6 +8,8 @@ import {
DialogDescription,
DialogTitle,
} from "@/components/ui/dialog";
+import { RotateCcw, ZoomIn, ZoomOut } from "lucide-react";
+import { useTheme } from "@/providers/ThemeProvider";
interface MermaidDiagramProps {
code: string;
@@ -20,28 +22,35 @@ interface MermaidDiagramProps {
// call through this promise chain. Also caches the mermaid import + the
// one-time initialize() so they don't run per-diagram.
type MermaidModule = typeof import("mermaid").default;
+type MermaidTheme = "default" | "dark";
let mermaidLoader: Promise | null = null;
let renderChain: Promise = Promise.resolve();
function loadMermaid(): Promise {
if (!mermaidLoader) {
- mermaidLoader = import("mermaid").then((mod) => {
- mod.default.initialize({
- startOnLoad: false,
- theme: "dark",
- // "strict" forbids raw HTML in node labels — safer for AI-generated
- // diagram source rendered alongside user content.
- securityLevel: "strict",
- });
- return mod.default;
- });
+ mermaidLoader = import("mermaid").then((mod) => mod.default);
}
return mermaidLoader;
}
-async function renderMermaid(id: string, code: string): Promise {
+function initializeMermaid(mermaid: MermaidModule, theme: MermaidTheme) {
+ mermaid.initialize({
+ startOnLoad: false,
+ theme,
+ // "strict" forbids raw HTML in node labels — safer for AI-generated
+ // diagram source rendered alongside user content.
+ securityLevel: "strict",
+ });
+}
+
+async function renderMermaid(
+ id: string,
+ code: string,
+ theme: MermaidTheme
+): Promise {
const next = renderChain.then(async () => {
const mermaid = await loadMermaid();
+ initializeMermaid(mermaid, theme);
// parse() with suppressErrors gives a boolean instead of the "red bomb"
// error-SVG that render() produces on bad input.
const parsed = await mermaid.parse(code, { suppressErrors: true });
@@ -60,6 +69,9 @@ export const MermaidDiagram = React.memo(
// useId is React 19's stable-across-renders unique id. Mermaid needs a
// CSS-safe id; useId returns strings like ":r3:" so strip the colons.
const reactId = useId().replace(/:/g, "");
+ const { resolvedTheme } = useTheme();
+ const mermaidTheme: MermaidTheme =
+ resolvedTheme === "dark" ? "dark" : "default";
const [svg, setSvg] = useState(null);
useEffect(() => {
@@ -69,7 +81,8 @@ export const MermaidDiagram = React.memo(
// ends (this effect re-runs because isStreaming is in the deps).
if (isStreaming) return;
let cancelled = false;
- renderMermaid(`mermaid-${reactId}`, code)
+ setSvg(null);
+ renderMermaid(`mermaid-${reactId}`, code, mermaidTheme)
.then((result) => {
if (!cancelled) setSvg(result);
})
@@ -79,7 +92,7 @@ export const MermaidDiagram = React.memo(
return () => {
cancelled = true;
};
- }, [code, reactId, isStreaming]);
+ }, [code, reactId, isStreaming, mermaidTheme]);
// During streaming the source grows token-by-token. Avoid the syntax
// highlighter here — it re-tokenizes the full string on every keystroke
@@ -106,6 +119,16 @@ MermaidDiagram.displayName = "MermaidDiagram";
const MermaidPreview = React.memo<{ svg: string }>(({ svg }) => {
const [open, setOpen] = useState(false);
+ const [zoom, setZoom] = useState(1.25);
+ const zoomPercent = Math.round(zoom * 100);
+
+ useEffect(() => {
+ if (open) setZoom(1.25);
+ }, [open]);
+
+ const zoomOut = () => setZoom((value) => Math.max(0.75, value - 0.25));
+ const zoomIn = () => setZoom((value) => Math.min(2.5, value + 0.25));
+
return (
<>
(({ svg }) => {
open={open}
onOpenChange={setOpen}
>
-
+
Mermaid diagram
Full-size preview of the generated Mermaid diagram.
-
+
+
+
+
+
+ {zoomPercent}%
+
+ = 2.5}
+ aria-label="Zoom in"
+ className="inline-flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-40"
+ >
+
+
+ setZoom(1.25)}
+ aria-label="Reset zoom"
+ className="inline-flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
+ >
+
+
+
+
>