create-interview-cockpit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/index.js +302 -0
- package/package.json +44 -0
- package/template/.env.example +14 -0
- package/template/client/index.html +12 -0
- package/template/client/package-lock.json +6012 -0
- package/template/client/package.json +34 -0
- package/template/client/postcss.config.cjs +6 -0
- package/template/client/src/App.tsx +120 -0
- package/template/client/src/api.ts +132 -0
- package/template/client/src/components/AnnotationDialog.tsx +307 -0
- package/template/client/src/components/ChatMessage.tsx +89 -0
- package/template/client/src/components/ChatView.tsx +763 -0
- package/template/client/src/components/CodeContextPanel.tsx +470 -0
- package/template/client/src/components/FileAttachments.tsx +107 -0
- package/template/client/src/components/FileViewerModal.tsx +470 -0
- package/template/client/src/components/MarkdownRenderer.tsx +333 -0
- package/template/client/src/components/MermaidDiagram.tsx +157 -0
- package/template/client/src/components/Sidebar.tsx +419 -0
- package/template/client/src/components/TextAnnotator.tsx +476 -0
- package/template/client/src/index.css +61 -0
- package/template/client/src/main.tsx +10 -0
- package/template/client/src/store.ts +321 -0
- package/template/client/src/types.ts +65 -0
- package/template/client/src/vite-env.d.ts +1 -0
- package/template/client/tailwind.config.cjs +8 -0
- package/template/client/tsconfig.json +16 -0
- package/template/client/tsconfig.tsbuildinfo +1 -0
- package/template/client/vite.config.ts +12 -0
- package/template/cockpit.json +3 -0
- package/template/data/context-files/.gitkeep +0 -0
- package/template/data/questions/.gitkeep +0 -0
- package/template/data/topics.json +1 -0
- package/template/package.json +14 -0
- package/template/server/package-lock.json +2266 -0
- package/template/server/package.json +31 -0
- package/template/server/src/index.ts +758 -0
- package/template/server/src/storage.ts +303 -0
- package/template/server/tsconfig.json +14 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import ReactMarkdown from "react-markdown";
|
|
2
|
+
import remarkGfm from "remark-gfm";
|
|
3
|
+
import { useMemo, useRef, useEffect } from "react";
|
|
4
|
+
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
5
|
+
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
|
6
|
+
import MermaidDiagram from "./MermaidDiagram";
|
|
7
|
+
import { useStore } from "../store";
|
|
8
|
+
import { Bookmark } from "lucide-react";
|
|
9
|
+
|
|
10
|
+
import type { Components } from "react-markdown";
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
content: string;
|
|
14
|
+
onAnnotationClick?: (annotId: string, rect: DOMRect) => void;
|
|
15
|
+
bookmarkedBlockIndex?: number;
|
|
16
|
+
onBookmarkBlock?: (blockIndex: number) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const markdownComponents: Components = {
|
|
20
|
+
pre({ children }) {
|
|
21
|
+
return <>{children}</>;
|
|
22
|
+
},
|
|
23
|
+
p({ children }) {
|
|
24
|
+
return <p className="my-2 leading-relaxed text-slate-200">{children}</p>;
|
|
25
|
+
},
|
|
26
|
+
code({ className, children, ...props }) {
|
|
27
|
+
const match = /language-(\w+)/.exec(className || "");
|
|
28
|
+
const codeString = String(children).replace(/\n$/, "");
|
|
29
|
+
|
|
30
|
+
if (match) {
|
|
31
|
+
const lang = match[1];
|
|
32
|
+
|
|
33
|
+
if (lang === "mermaid") {
|
|
34
|
+
return <MermaidDiagram chart={codeString} />;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<SyntaxHighlighter
|
|
39
|
+
style={oneDark}
|
|
40
|
+
language={lang}
|
|
41
|
+
PreTag="div"
|
|
42
|
+
customStyle={{
|
|
43
|
+
margin: "0.5rem 0",
|
|
44
|
+
borderRadius: "0.5rem",
|
|
45
|
+
fontSize: "0.8rem",
|
|
46
|
+
background: "#1e293b",
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
{codeString}
|
|
50
|
+
</SyntaxHighlighter>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (codeString.includes("\n")) {
|
|
55
|
+
return (
|
|
56
|
+
<SyntaxHighlighter
|
|
57
|
+
style={oneDark}
|
|
58
|
+
language="text"
|
|
59
|
+
PreTag="div"
|
|
60
|
+
customStyle={{
|
|
61
|
+
margin: "0.5rem 0",
|
|
62
|
+
borderRadius: "0.5rem",
|
|
63
|
+
fontSize: "0.8rem",
|
|
64
|
+
background: "#1e293b",
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{codeString}
|
|
68
|
+
</SyntaxHighlighter>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<code
|
|
74
|
+
className="bg-slate-800 px-1.5 py-0.5 rounded text-cyan-300 text-xs"
|
|
75
|
+
{...props}
|
|
76
|
+
>
|
|
77
|
+
{children}
|
|
78
|
+
</code>
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
table({ children }) {
|
|
82
|
+
return (
|
|
83
|
+
<div className="overflow-x-auto my-3">
|
|
84
|
+
<table className="border-collapse text-xs w-full">{children}</table>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
thead({ children }) {
|
|
89
|
+
return <thead className="bg-slate-800">{children}</thead>;
|
|
90
|
+
},
|
|
91
|
+
th({ children }) {
|
|
92
|
+
return (
|
|
93
|
+
<th className="border border-slate-700 px-3 py-1.5 text-left font-medium text-slate-300">
|
|
94
|
+
{children}
|
|
95
|
+
</th>
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
td({ children }) {
|
|
99
|
+
return (
|
|
100
|
+
<td className="border border-slate-700 px-3 py-1.5 text-slate-400">
|
|
101
|
+
{children}
|
|
102
|
+
</td>
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
blockquote({ children }) {
|
|
106
|
+
return (
|
|
107
|
+
<blockquote className="border-l-2 border-cyan-500/50 pl-3 my-2 text-slate-400 italic">
|
|
108
|
+
{children}
|
|
109
|
+
</blockquote>
|
|
110
|
+
);
|
|
111
|
+
},
|
|
112
|
+
ul({ children }) {
|
|
113
|
+
return (
|
|
114
|
+
<ul className="list-disc list-outside pl-5 space-y-1.5 my-2">
|
|
115
|
+
{children}
|
|
116
|
+
</ul>
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
ol({ children }) {
|
|
120
|
+
return (
|
|
121
|
+
<ol className="list-decimal list-outside pl-5 space-y-1.5 my-2">
|
|
122
|
+
{children}
|
|
123
|
+
</ol>
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
li({ children }) {
|
|
127
|
+
return (
|
|
128
|
+
<li className="pl-1 marker:text-slate-500 [&>p]:inline [&>p]:m-0 [&>ul]:mt-1.5 [&>ol]:mt-1.5">
|
|
129
|
+
{children}
|
|
130
|
+
</li>
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
a({ href, children }) {
|
|
134
|
+
return (
|
|
135
|
+
<a
|
|
136
|
+
href={href}
|
|
137
|
+
className="text-cyan-400 hover:underline"
|
|
138
|
+
target="_blank"
|
|
139
|
+
rel="noopener noreferrer"
|
|
140
|
+
>
|
|
141
|
+
{children}
|
|
142
|
+
</a>
|
|
143
|
+
);
|
|
144
|
+
},
|
|
145
|
+
strong({ children }) {
|
|
146
|
+
return <strong className="font-semibold text-slate-200">{children}</strong>;
|
|
147
|
+
},
|
|
148
|
+
h1({ children }) {
|
|
149
|
+
return (
|
|
150
|
+
<h1 className="text-xl font-bold mt-4 mb-2 text-slate-100">{children}</h1>
|
|
151
|
+
);
|
|
152
|
+
},
|
|
153
|
+
h2({ children }) {
|
|
154
|
+
return (
|
|
155
|
+
<h2 className="text-lg font-semibold mt-3 mb-1.5 text-slate-200">
|
|
156
|
+
{children}
|
|
157
|
+
</h2>
|
|
158
|
+
);
|
|
159
|
+
},
|
|
160
|
+
h3({ children }) {
|
|
161
|
+
return (
|
|
162
|
+
<h3 className="text-base font-medium mt-2 mb-1 text-slate-200">
|
|
163
|
+
{children}
|
|
164
|
+
</h3>
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
hr() {
|
|
168
|
+
return <hr className="border-slate-700 my-4" />;
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export default function MarkdownRenderer({
|
|
173
|
+
content,
|
|
174
|
+
onAnnotationClick,
|
|
175
|
+
bookmarkedBlockIndex,
|
|
176
|
+
onBookmarkBlock,
|
|
177
|
+
}: Props) {
|
|
178
|
+
const openFileViewer = useStore((s) => s.openFileViewer);
|
|
179
|
+
const bookmarkElemRef = useRef<HTMLDivElement | null>(null);
|
|
180
|
+
|
|
181
|
+
// Scroll the bookmarked block into view when the bookmark changes or on mount
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (bookmarkedBlockIndex === undefined) return;
|
|
184
|
+
const id = setTimeout(() => {
|
|
185
|
+
bookmarkElemRef.current?.scrollIntoView({
|
|
186
|
+
behavior: "smooth",
|
|
187
|
+
block: "center",
|
|
188
|
+
});
|
|
189
|
+
}, 150);
|
|
190
|
+
return () => clearTimeout(id);
|
|
191
|
+
}, [bookmarkedBlockIndex]);
|
|
192
|
+
|
|
193
|
+
const components = useMemo<Components>(() => {
|
|
194
|
+
// Wrap a block element with a hover-activated bookmark button.
|
|
195
|
+
// Uses node.position.start.line as a stable block identifier.
|
|
196
|
+
function wrapBlock(
|
|
197
|
+
lineIdx: number | undefined,
|
|
198
|
+
element: React.ReactElement,
|
|
199
|
+
): React.ReactElement {
|
|
200
|
+
if (!onBookmarkBlock || lineIdx === undefined) return element;
|
|
201
|
+
const isThis = bookmarkedBlockIndex === lineIdx;
|
|
202
|
+
return (
|
|
203
|
+
<div
|
|
204
|
+
ref={
|
|
205
|
+
isThis
|
|
206
|
+
? (el) => {
|
|
207
|
+
bookmarkElemRef.current = el;
|
|
208
|
+
}
|
|
209
|
+
: undefined
|
|
210
|
+
}
|
|
211
|
+
data-reading-bookmark={isThis ? "true" : undefined}
|
|
212
|
+
className={`group/bk relative ${
|
|
213
|
+
isThis ? "border-l-2 border-amber-500/50 pl-2 -ml-2" : ""
|
|
214
|
+
}`}
|
|
215
|
+
>
|
|
216
|
+
<button
|
|
217
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
218
|
+
onClick={() => onBookmarkBlock(lineIdx)}
|
|
219
|
+
className={`absolute right-0 top-0 p-0.5 z-10 transition-opacity ${
|
|
220
|
+
isThis
|
|
221
|
+
? "opacity-100 text-amber-400"
|
|
222
|
+
: "opacity-0 group-hover/bk:opacity-100 text-slate-600 hover:text-amber-400"
|
|
223
|
+
}`}
|
|
224
|
+
title={isThis ? "Remove bookmark" : "Bookmark here"}
|
|
225
|
+
>
|
|
226
|
+
<Bookmark className={`w-3 h-3 ${isThis ? "fill-amber-400" : ""}`} />
|
|
227
|
+
</button>
|
|
228
|
+
{element}
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
...markdownComponents,
|
|
235
|
+
p({ children, node }) {
|
|
236
|
+
return wrapBlock(
|
|
237
|
+
(node as any)?.position?.start?.line,
|
|
238
|
+
<p className="my-2 leading-relaxed text-slate-200">{children}</p>,
|
|
239
|
+
);
|
|
240
|
+
},
|
|
241
|
+
h1({ children, node }) {
|
|
242
|
+
return wrapBlock(
|
|
243
|
+
(node as any)?.position?.start?.line,
|
|
244
|
+
<h1 className="text-xl font-bold mt-4 mb-2 text-slate-100">
|
|
245
|
+
{children}
|
|
246
|
+
</h1>,
|
|
247
|
+
);
|
|
248
|
+
},
|
|
249
|
+
h2({ children, node }) {
|
|
250
|
+
return wrapBlock(
|
|
251
|
+
(node as any)?.position?.start?.line,
|
|
252
|
+
<h2 className="text-lg font-semibold mt-3 mb-1.5 text-slate-200">
|
|
253
|
+
{children}
|
|
254
|
+
</h2>,
|
|
255
|
+
);
|
|
256
|
+
},
|
|
257
|
+
h3({ children, node }) {
|
|
258
|
+
return wrapBlock(
|
|
259
|
+
(node as any)?.position?.start?.line,
|
|
260
|
+
<h3 className="text-base font-medium mt-2 mb-1 text-slate-200">
|
|
261
|
+
{children}
|
|
262
|
+
</h3>,
|
|
263
|
+
);
|
|
264
|
+
},
|
|
265
|
+
a({ href, children }) {
|
|
266
|
+
if (href?.startsWith("annot://")) {
|
|
267
|
+
const annotId = href.slice("annot://".length);
|
|
268
|
+
return (
|
|
269
|
+
<span
|
|
270
|
+
onClick={
|
|
271
|
+
onAnnotationClick
|
|
272
|
+
? (e) => {
|
|
273
|
+
const rect = (
|
|
274
|
+
e.currentTarget as HTMLElement
|
|
275
|
+
).getBoundingClientRect();
|
|
276
|
+
onAnnotationClick(annotId, rect);
|
|
277
|
+
}
|
|
278
|
+
: undefined
|
|
279
|
+
}
|
|
280
|
+
className="underline decoration-cyan-400 decoration-dotted underline-offset-2 cursor-pointer text-slate-200 hover:text-cyan-300 transition-colors"
|
|
281
|
+
>
|
|
282
|
+
{children}
|
|
283
|
+
</span>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (href?.startsWith("coderef://")) {
|
|
287
|
+
const filePath = href.slice("coderef://".length);
|
|
288
|
+
return (
|
|
289
|
+
<button
|
|
290
|
+
type="button"
|
|
291
|
+
onClick={() => openFileViewer(filePath)}
|
|
292
|
+
className="inline-flex items-center gap-1 text-amber-400 hover:text-amber-300 underline decoration-amber-500/50 underline-offset-2 transition-colors font-mono text-[0.8em]"
|
|
293
|
+
title={`Open ${filePath}`}
|
|
294
|
+
>
|
|
295
|
+
{children}
|
|
296
|
+
</button>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return (
|
|
300
|
+
<a
|
|
301
|
+
href={href}
|
|
302
|
+
className="text-cyan-400 hover:underline"
|
|
303
|
+
target="_blank"
|
|
304
|
+
rel="noopener noreferrer"
|
|
305
|
+
>
|
|
306
|
+
{children}
|
|
307
|
+
</a>
|
|
308
|
+
);
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}, [
|
|
312
|
+
onAnnotationClick,
|
|
313
|
+
openFileViewer,
|
|
314
|
+
bookmarkedBlockIndex,
|
|
315
|
+
onBookmarkBlock,
|
|
316
|
+
]);
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<ReactMarkdown
|
|
320
|
+
remarkPlugins={[remarkGfm]}
|
|
321
|
+
components={components}
|
|
322
|
+
urlTransform={(url) => {
|
|
323
|
+
// Allow our internal schemes; block javascript: for safety
|
|
324
|
+
if (url.startsWith("annot://")) return url;
|
|
325
|
+
if (url.startsWith("coderef://")) return url;
|
|
326
|
+
if (url.startsWith("javascript:")) return "";
|
|
327
|
+
return url;
|
|
328
|
+
}}
|
|
329
|
+
>
|
|
330
|
+
{content}
|
|
331
|
+
</ReactMarkdown>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { memo, useEffect, useState } from "react";
|
|
2
|
+
import { RefreshCw, Loader2 } from "lucide-react";
|
|
3
|
+
import mermaid from "mermaid";
|
|
4
|
+
|
|
5
|
+
mermaid.initialize({
|
|
6
|
+
startOnLoad: false,
|
|
7
|
+
theme: "dark",
|
|
8
|
+
suppressErrorRendering: true,
|
|
9
|
+
themeVariables: {
|
|
10
|
+
primaryColor: "#0e7490",
|
|
11
|
+
primaryTextColor: "#e2e8f0",
|
|
12
|
+
primaryBorderColor: "#164e63",
|
|
13
|
+
lineColor: "#475569",
|
|
14
|
+
secondaryColor: "#1e293b",
|
|
15
|
+
tertiaryColor: "#0f172a",
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
let counter = 0;
|
|
20
|
+
|
|
21
|
+
// Auto-fix common Mermaid syntax issues the LLM produces
|
|
22
|
+
function sanitizeChart(raw: string): string {
|
|
23
|
+
return (
|
|
24
|
+
raw
|
|
25
|
+
// Quote node labels inside [] that contain unquoted parens/brackets
|
|
26
|
+
// e.g. A[Service A (primary)] → A["Service A (primary)"]
|
|
27
|
+
.replace(
|
|
28
|
+
/\[([^\]"]+\([^)]*\)[^\]]*)\]/g,
|
|
29
|
+
(_match, label) => `["${label}"]`,
|
|
30
|
+
)
|
|
31
|
+
// Quote edge labels inside || that contain parens — ( is parsed as stadium shape
|
|
32
|
+
// e.g. -->|event (publish)| B → -->|"event (publish)"| B
|
|
33
|
+
.replace(/\|([^|"]*[()][^|]*)\|/g, (_match, label) => `|"${label}"|`)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface Props {
|
|
38
|
+
chart: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default memo(function MermaidDiagram({ chart }: Props) {
|
|
42
|
+
const [activeChart, setActiveChart] = useState(chart);
|
|
43
|
+
const [svg, setSvg] = useState<string | null>(null);
|
|
44
|
+
const [error, setError] = useState<string | null>(null);
|
|
45
|
+
const [fixing, setFixing] = useState(false);
|
|
46
|
+
|
|
47
|
+
// Keep activeChart in sync when the prop changes (streaming / message reload)
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
setActiveChart(chart);
|
|
50
|
+
}, [chart]);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
let cancelled = false;
|
|
54
|
+
setSvg(null);
|
|
55
|
+
setError(null);
|
|
56
|
+
|
|
57
|
+
// Debounce: wait for content to stabilise (streaming sends tokens rapidly)
|
|
58
|
+
const timer = setTimeout(async () => {
|
|
59
|
+
if (cancelled) return;
|
|
60
|
+
const id = `mermaid-${++counter}`;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const { svg: rendered } = await mermaid.render(id, activeChart);
|
|
64
|
+
if (!cancelled) setSvg(rendered);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
67
|
+
// Retry with sanitized chart (fix common LLM syntax mistakes)
|
|
68
|
+
const fixed = sanitizeChart(activeChart);
|
|
69
|
+
if (fixed !== activeChart) {
|
|
70
|
+
const retryId = `mermaid-${++counter}`;
|
|
71
|
+
try {
|
|
72
|
+
const { svg: rendered } = await mermaid.render(retryId, fixed);
|
|
73
|
+
if (!cancelled) setSvg(rendered);
|
|
74
|
+
return;
|
|
75
|
+
} catch {
|
|
76
|
+
// fall through to error
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!cancelled) setError(errMsg || "Failed to render diagram");
|
|
80
|
+
}
|
|
81
|
+
}, 400);
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
cancelled = true;
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
// Clean up any mermaid elements that leak into document.body
|
|
87
|
+
document
|
|
88
|
+
.querySelectorAll('[id^="dmermaid-"]')
|
|
89
|
+
.forEach((el) => el.remove());
|
|
90
|
+
};
|
|
91
|
+
}, [activeChart]);
|
|
92
|
+
|
|
93
|
+
const handleFix = async () => {
|
|
94
|
+
setFixing(true);
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch("/api/fix-diagram", {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: { "Content-Type": "application/json" },
|
|
99
|
+
body: JSON.stringify({ chart: activeChart, error }),
|
|
100
|
+
});
|
|
101
|
+
if (!res.ok) throw new Error("Fix request failed");
|
|
102
|
+
const { chart: fixed } = (await res.json()) as { chart: string };
|
|
103
|
+
setActiveChart(fixed);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.error("Fix diagram error:", err);
|
|
106
|
+
} finally {
|
|
107
|
+
setFixing(false);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (error) {
|
|
112
|
+
return (
|
|
113
|
+
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3 my-2">
|
|
114
|
+
<div className="flex items-center justify-between mb-1">
|
|
115
|
+
<p className="text-xs text-red-400">Diagram error:</p>
|
|
116
|
+
<button
|
|
117
|
+
onClick={handleFix}
|
|
118
|
+
disabled={fixing}
|
|
119
|
+
className="flex items-center gap-1 px-2 py-0.5 text-[11px] rounded bg-slate-700 text-slate-300 hover:bg-slate-600 disabled:opacity-50 transition-colors"
|
|
120
|
+
>
|
|
121
|
+
{fixing ? (
|
|
122
|
+
<>
|
|
123
|
+
<Loader2 className="w-3 h-3 animate-spin" />
|
|
124
|
+
Fixing…
|
|
125
|
+
</>
|
|
126
|
+
) : (
|
|
127
|
+
<>
|
|
128
|
+
<RefreshCw className="w-3 h-3" />
|
|
129
|
+
Fix diagram
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
<pre className="text-xs text-slate-400 whitespace-pre-wrap">
|
|
135
|
+
{activeChart}
|
|
136
|
+
</pre>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!svg) {
|
|
142
|
+
return (
|
|
143
|
+
<div className="my-3 flex justify-center bg-slate-800/30 rounded-lg p-4">
|
|
144
|
+
<span className="text-xs text-slate-500 animate-pulse">
|
|
145
|
+
Rendering diagram…
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div
|
|
153
|
+
className="my-3 flex justify-center bg-slate-800/30 rounded-lg p-4 overflow-x-auto mermaid-diagram"
|
|
154
|
+
dangerouslySetInnerHTML={{ __html: svg }}
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
});
|