Skip to content

Commit b090f04

Browse files
committed
rendering mermaid diagrams
basic mermaid js implementation add panzoom and change color theme variables
1 parent 0938308 commit b090f04

File tree

5 files changed

+217
-4
lines changed

5 files changed

+217
-4
lines changed

gui/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@continuedev/config-yaml": "file:../packages/config-yaml",
2020
"@headlessui/react": "^2.2.0",
2121
"@heroicons/react": "^2.0.18",
22+
"@panzoom/panzoom": "^4.6.0",
2223
"@reduxjs/toolkit": "^2.3.0",
2324
"@tiptap/core": "^2.3.2",
2425
"@tiptap/extension-document": "^2.3.2",
@@ -43,6 +44,7 @@
4344
"handlebars": "^4.7.8",
4445
"lodash": "^4.17.21",
4546
"lowlight": "^3.3.0",
47+
"mermaid": "^11.6.0",
4648
"minisearch": "^7.0.2",
4749
"mustache": "^4.2.0",
4850
"posthog-js": "^1.130.1",
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import {
2+
ArrowPathRoundedSquareIcon,
3+
MagnifyingGlassMinusIcon,
4+
MagnifyingGlassPlusIcon,
5+
} from "@heroicons/react/24/outline";
6+
import Panzoom from "@panzoom/panzoom";
7+
import mermaid from "mermaid";
8+
import { useEffect, useId, useRef, useState } from "react";
9+
import { useDebouncedEffect } from "../find/useDebounce";
10+
import { ToolTip } from "../gui/Tooltip";
11+
12+
const MINIMUM_ZOOM_STEP = 0.05;
13+
14+
const MERMAID_THEME_COLORS = {
15+
background: "#1e1e1e",
16+
primaryColor: "#4d8bf0",
17+
primaryTextColor: "#ffffff",
18+
primaryBorderColor: "#4d8bf0",
19+
secondaryColor: "#3a6db3",
20+
secondaryTextColor: "#ffffff",
21+
secondaryBorderColor: "#3a6db3",
22+
tertiaryColor: "#59bc89",
23+
tertiaryTextColor: "#ffffff",
24+
tertiaryBorderColor: "#59bc89",
25+
noteBkgColor: "#2d2d2d",
26+
noteTextColor: "#e6e6e6",
27+
noteBorderColor: "#555555",
28+
lineColor: "#8c8c8c",
29+
textColor: "#e6e6e6",
30+
mainBkg: "#252525",
31+
errorBkgColor: "#f44336",
32+
errorTextColor: "#ffffff",
33+
nodeBorder: "#555555",
34+
clusterBkg: "#2a2a2a",
35+
clusterBorder: "#555555",
36+
defaultLinkColor: "#8c8c8c",
37+
titleColor: "#e6e6e6",
38+
edgeLabelBackground: "#252525",
39+
activeTaskBkgColor: "#4caf50",
40+
activeTaskBorderColor: "#388e3c",
41+
doneTaskBkgColor: "#388e3c",
42+
doneTaskBorderColor: "#2e7d32",
43+
critBkgColor: "#f44336",
44+
critBorderColor: "#d32f2f",
45+
taskTextColor: "#e6e6e6",
46+
taskTextOutsideColor: "#e6e6e6",
47+
taskTextLightColor: "#b3b3b3",
48+
sectionBkgColor: "#2a2a2a",
49+
altSectionBkgColor: "#303030",
50+
sectionBkgColor2: "#252525",
51+
excludeBkgColor: "#2d2d2d",
52+
fillType0: "#264f78",
53+
fillType1: "#3a6db3",
54+
fillType2: "#59bc89",
55+
fillType3: "#4d8bf0",
56+
fillType4: "#3a6db3",
57+
fillType5: "#264f78",
58+
fillType6: "#59bc89",
59+
fillType7: "#4d8bf0",
60+
};
61+
62+
mermaid.initialize({
63+
startOnLoad: false,
64+
securityLevel: "loose",
65+
theme: "dark",
66+
themeVariables: {
67+
...MERMAID_THEME_COLORS,
68+
fontSize: "14px",
69+
fontFamily: "var(--vscode-font-family)",
70+
},
71+
});
72+
73+
export default function MermaidDiagram({ code }: { code: string }) {
74+
const mermaidRenderContainerRef = useRef<HTMLDivElement>(null);
75+
const zoomInButtonRef = useRef<SVGSVGElement>(null);
76+
const zoomOutButtonRef = useRef<SVGSVGElement>(null);
77+
const resetZoomButtonRef = useRef<SVGSVGElement>(null);
78+
79+
const zoomInButtonId = useId();
80+
const zoomOutButtonId = useId();
81+
const resetZoomButtonId = useId();
82+
83+
const [isLoading, setIsLoading] = useState(false);
84+
const [error, setError] = useState("");
85+
86+
useEffect(() => {
87+
setIsLoading(true);
88+
}, [code]);
89+
90+
useDebouncedEffect(
91+
() => {
92+
if (mermaidRenderContainerRef.current) {
93+
mermaidRenderContainerRef.current.innerHTML = "";
94+
}
95+
const diagramId = `mermaid-id-${Math.random().toString(36).substring(2)}`;
96+
mermaid
97+
.parse(code)
98+
.then(() => mermaid.render(diagramId, code))
99+
.then(({ svg }) => {
100+
if (mermaidRenderContainerRef.current) {
101+
mermaidRenderContainerRef.current.innerHTML = svg;
102+
setError("");
103+
const panzoom = Panzoom(mermaidRenderContainerRef.current, {
104+
step: MINIMUM_ZOOM_STEP,
105+
});
106+
zoomInButtonRef.current?.addEventListener("click", () =>
107+
panzoom.zoomIn({ step: MINIMUM_ZOOM_STEP * 4 }),
108+
);
109+
zoomOutButtonRef.current?.addEventListener("click", () =>
110+
panzoom.zoomOut({ step: MINIMUM_ZOOM_STEP * 4 }),
111+
);
112+
resetZoomButtonRef.current?.addEventListener("click", () =>
113+
panzoom.reset(),
114+
);
115+
mermaidRenderContainerRef.current.parentElement?.addEventListener(
116+
"wheel",
117+
(event) => {
118+
if (!event.shiftKey) return;
119+
panzoom.zoomWithWheel(event);
120+
},
121+
);
122+
}
123+
})
124+
.catch((err) => {
125+
if (err.message) {
126+
setError(err.message);
127+
} else {
128+
setError(
129+
"Unknown error when parsing or rendering the Mermaid diagram.",
130+
);
131+
}
132+
})
133+
.finally(() => {
134+
setIsLoading(false);
135+
});
136+
},
137+
500,
138+
[code],
139+
);
140+
141+
return (
142+
<>
143+
{isLoading && (
144+
<div className="text-vsc-foreground text-sm">Generating diagram...</div>
145+
)}
146+
{!!error ? (
147+
<div className="text-error whitespace-pre text-sm">{error}</div>
148+
) : (
149+
<div className="relative">
150+
<div className="absolute right-0 z-10 m-2 flex items-center gap-x-1">
151+
<MagnifyingGlassPlusIcon
152+
data-tooltip-id={zoomInButtonId}
153+
ref={zoomInButtonRef}
154+
className="h-4 w-4 cursor-pointer"
155+
/>
156+
<ToolTip id={zoomInButtonId}>Zoom In</ToolTip>
157+
<MagnifyingGlassMinusIcon
158+
data-tooltip-id={zoomOutButtonId}
159+
ref={zoomOutButtonRef}
160+
className="h-4 w-4 cursor-pointer"
161+
/>
162+
<ToolTip id={zoomOutButtonId}>Zoom Out</ToolTip>
163+
<ArrowPathRoundedSquareIcon
164+
data-tooltip-id={resetZoomButtonId}
165+
ref={resetZoomButtonRef}
166+
className="h-4 w-4 cursor-pointer"
167+
/>
168+
<ToolTip id={resetZoomButtonId}>Reset Zoom</ToolTip>
169+
</div>
170+
<div
171+
className="flex min-h-5 justify-center"
172+
ref={mermaidRenderContainerRef}
173+
/>
174+
</div>
175+
)}
176+
</>
177+
);
178+
}

gui/src/components/StyledMarkdownPreview/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ToolTip } from "../gui/Tooltip";
2121
import FilenameLink from "./FilenameLink";
2222
import "./katex.css";
2323
import "./markdown.css";
24+
import MermaidBlock from "./MermaidBlock";
2425
import { rehypeHighlightPlugin } from "./rehypeHighlightPlugin";
2526
import { StepContainerPreToolbar } from "./StepContainerPreToolbar";
2627
import SymbolLink from "./SymbolLink";
@@ -331,6 +332,10 @@ const StyledMarkdownPreview = memo(function StyledMarkdownPreview(
331332
}
332333
}
333334
}
335+
if (codeProps.className?.includes("language-mermaid")) {
336+
const codeText = String(codeProps.children || "");
337+
return <MermaidBlock code={codeText} />;
338+
}
334339
return <code {...codeProps}>{codeProps.children}</code>;
335340
},
336341
},

gui/src/components/find/FindWidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
SearchMatch,
1919
searchWithinContainer,
2020
} from "./findWidgetSearch";
21-
import useDebounceValue from "./useDebounce";
21+
import { useDebounceValue } from "./useDebounce";
2222
import { useElementSize } from "./useElementSize";
2323

2424
interface HighlightOverlayProps {

gui/src/components/find/useDebounce.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { useEffect, useState } from "react";
1+
import { useEffect, useRef, useState } from "react";
22

3-
function useDebounceValue<T>(value: T, delay: number = 500): T {
3+
export function useDebounceValue<T>(value: T, delay: number = 500): T {
44
const [debouncedValue, setDebouncedValue] = useState<T>(value);
55

66
useEffect(() => {
@@ -16,4 +16,32 @@ function useDebounceValue<T>(value: T, delay: number = 500): T {
1616
return debouncedValue;
1717
}
1818

19-
export default useDebounceValue;
19+
export function useDebouncedEffect(
20+
effect: () => void,
21+
delay: number,
22+
deps: unknown[],
23+
) {
24+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
25+
const callBackRef = useRef(() => {});
26+
27+
useEffect(() => {
28+
// do not want the timeout to reset on effect dep change
29+
callBackRef.current = effect;
30+
}, [effect]);
31+
32+
useEffect(() => {
33+
if (timeoutRef.current) {
34+
clearTimeout(timeoutRef.current);
35+
}
36+
37+
timeoutRef.current = setTimeout(() => {
38+
callBackRef.current();
39+
}, delay);
40+
41+
return () => {
42+
if (timeoutRef.current) {
43+
clearTimeout(timeoutRef.current);
44+
}
45+
};
46+
}, [delay, ...deps]);
47+
}

0 commit comments

Comments
 (0)