Skip to content

Commit 36a9577

Browse files
authored
feat: chatbot display files modal (#1196)
1 parent dfc8f0e commit 36a9577

File tree

16 files changed

+1763
-78
lines changed

16 files changed

+1763
-78
lines changed

.hintrc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": [
3+
"development"
4+
],
5+
"hints": {
6+
"axe/forms": "off"
7+
}
8+
}

package-lock.json

Lines changed: 1334 additions & 58 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"js-yaml": "^4.1.0",
5252
"jszip": "^3.10.1",
5353
"lodash": "^4.17.21",
54+
"mermaid": "^11.6.0",
5455
"motion": "^12.4.2",
5556
"pako": "^2.1.0",
5657
"psl": "^1.15.0",

src/components/molecules/copyButton.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ export const CopyButton = ({
1818
tabIndex = 0,
1919
text,
2020
title,
21+
buttonText,
2122
}: {
23+
buttonText?: string;
2224
className?: string;
2325
size?: Extract<SystemSizes, "xs" | "sm" | "md">;
2426
successMessage?: string;
@@ -39,7 +41,7 @@ export const CopyButton = ({
3941
}, 300);
4042

4143
const copyButtonStyle = cn(
42-
"flex size-12 items-center justify-center bg-transparent hover:bg-gray-900",
44+
"flex size-12 items-center justify-center bg-transparent hover:bg-gray-900 ",
4345
{
4446
"size-12": size === "md",
4547
"size-8": size === "sm",
@@ -66,7 +68,7 @@ export const CopyButton = ({
6668
title={t("copyButtonText", { text: title })}
6769
type="button"
6870
>
69-
<CopyIcon className={copyButtonIconStyle} />
71+
<CopyIcon className={copyButtonIconStyle} /> <div className="ml-2 text-white">{buttonText}</div>
7072
</Button>
7173
);
7274
};

src/components/molecules/modal.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const Modal = ({
2626
children,
2727
className,
2828
hideCloseButton,
29+
closeButtonClass,
2930
name,
3031
focusTabIndexOnLoad,
3132
wrapperClass,
@@ -41,7 +42,7 @@ export const Modal = ({
4142
const wrapperClassName = cn("fixed left-0 top-0 z-50 flex size-full items-center justify-center", wrapperClass);
4243
const modalClasses = cn("w-500 rounded-2xl border border-gray-950 bg-white p-3.5 text-gray-1250", className);
4344
const bgClass = cn("absolute left-0 top-0 -z-10 size-full bg-black/70");
44-
45+
const closeButtonClasseName = cn("group ml-auto h-default-icon w-default-icon bg-gray-250 p-0", closeButtonClass);
4546
useEffect(() => {
4647
if (isOpen && modalRef.current) {
4748
const buttons = modalRef.current.querySelectorAll("button");
@@ -115,10 +116,7 @@ export const Modal = ({
115116
variants={modalVariants}
116117
>
117118
{hideCloseButton ? null : (
118-
<IconButton
119-
className="group ml-auto h-default-icon w-default-icon bg-gray-250 p-0"
120-
onClick={() => onClose(name)}
121-
>
119+
<IconButton className={closeButtonClasseName} onClick={() => onClose(name)}>
122120
<Close className="size-3 fill-black transition group-hover:fill-white" />
123121
</IconButton>
124122
)}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/* eslint-disable no-console */
2+
"use client";
3+
4+
import React, { useEffect, useRef } from "react";
5+
6+
import mermaid from "mermaid";
7+
import { useTranslation } from "react-i18next";
8+
9+
import { ModalName } from "@enums/components";
10+
import { DiagramViewerModalProps } from "@interfaces/components";
11+
12+
import { useModalStore } from "@store";
13+
14+
import { Button } from "@components/atoms";
15+
import { Modal } from "@components/molecules";
16+
17+
// Mermaid Initialization (from your snippet)
18+
try {
19+
mermaid.initialize({
20+
startOnLoad: false,
21+
securityLevel: "loose",
22+
theme: "base",
23+
logLevel: "error",
24+
flowchart: {
25+
useMaxWidth: true,
26+
htmlLabels: true,
27+
},
28+
themeVariables: {
29+
background: "#1b1b1b",
30+
// Primary colors
31+
primaryColor: "#7FAE3C", // green-600 from your theme
32+
primaryTextColor: "white", // gray-1150
33+
primaryBorderColor: "#7FAE3C", // green-600
34+
35+
// Secondary colors
36+
secondaryColor: "#ededed", // gray-300
37+
secondaryTextColor: "#2d2d2d", // gray-1100
38+
secondaryBorderColor: "#bec3d1", // gray-600
39+
40+
// Tertiary colors
41+
tertiaryColor: "#f3f3f6", // gray-200
42+
tertiaryTextColor: "#515151", // gray-1000
43+
tertiaryBorderColor: "#cdcdcd", // gray-550
44+
45+
// Line colors
46+
lineColor: "#626262", // gray-850
47+
48+
// Note colors
49+
noteBkgColor: "#E8FFCA", // green-200
50+
noteTextColor: "#2d2d2d", // gray-1100
51+
noteBorderColor: "#7FAE3C", // green-600
52+
53+
// Actor colors
54+
actorBkg: "#7FAE3C", // green-600
55+
actorTextColor: "#ffffff", // white
56+
actorBorder: "#626262", // gray-850
57+
58+
// Other elements
59+
activationBorderColor: "#BCF870", // green-800
60+
activationBkgColor: "#E8FFCA", // green-200
61+
},
62+
});
63+
} catch (e) {
64+
console.warn("Mermaid already initialized or error during initialization:", e);
65+
}
66+
67+
// Props type for the MermaidDiagram component
68+
type MermaidDiagramComponentProps = {
69+
code: string;
70+
};
71+
72+
// MermaidDiagram Component (from your snippet)
73+
const MermaidDiagram = ({ code }: MermaidDiagramComponentProps) => {
74+
const mermaidRef = useRef<HTMLDivElement>(null);
75+
76+
useEffect(() => {
77+
if (code) {
78+
const currentRef = mermaidRef.current;
79+
if (!currentRef) return;
80+
81+
currentRef.innerHTML = "";
82+
83+
let cleanCode = code.trim();
84+
85+
if (cleanCode.startsWith("```sequenceDiagram")) {
86+
cleanCode = cleanCode.replace(/```sequenceDiagram\s*/, "").replace(/```\s*$/, "");
87+
} else if (!cleanCode.startsWith("sequenceDiagram")) {
88+
cleanCode = "sequenceDiagram\n" + cleanCode;
89+
}
90+
91+
if (cleanCode.startsWith("sequenceDiagram") && !cleanCode.startsWith("sequenceDiagram\n")) {
92+
cleanCode = cleanCode.replace("sequenceDiagram", "sequenceDiagram\n");
93+
}
94+
95+
cleanCode = cleanCode.replace(/\n\s{4}/g, "\n");
96+
97+
try {
98+
const uniqueId = `mermaid-${Math.random().toString(36).substring(2, 11)}`;
99+
100+
mermaid
101+
.render(uniqueId, cleanCode)
102+
.then((result) => {
103+
if (currentRef) {
104+
while (currentRef.firstChild) {
105+
currentRef.removeChild(currentRef.firstChild);
106+
}
107+
currentRef.innerHTML = result.svg;
108+
109+
const svg = currentRef.querySelector("svg");
110+
if (svg) {
111+
svg.style.backgroundColor = "#161616";
112+
svg.style.padding = "0.5rem";
113+
}
114+
}
115+
return result;
116+
})
117+
.catch((error) => {
118+
console.error("Error rendering Mermaid diagram:", error);
119+
if (currentRef) {
120+
currentRef.innerHTML = `<pre>Error rendering diagram:\n${String(error)}\n\nCode:\n${cleanCode}</pre>`;
121+
}
122+
});
123+
} catch (error) {
124+
console.error("Synchronous error during Mermaid diagram rendering:", error);
125+
if (currentRef) {
126+
currentRef.innerHTML = `<pre>Error rendering diagram:\n${String(error)}\n\nCode:\n${cleanCode}</pre>`;
127+
}
128+
}
129+
} else if (mermaidRef.current) {
130+
mermaidRef.current.innerHTML = "";
131+
}
132+
}, [code]);
133+
134+
if (!code) return null;
135+
136+
return (
137+
<div className="h-full overflow-auto rounded-xl bg-gray-1250 text-sm shadow-md">
138+
<div className="flex h-full items-center justify-center" ref={mermaidRef}>
139+
{/* Mermaid will render the diagram here */}
140+
</div>
141+
</div>
142+
);
143+
};
144+
145+
// Main DiagramViewerModal component
146+
export const DiagramViewerModal = () => {
147+
const { t } = useTranslation("modals", { keyPrefix: "diagramViewer" });
148+
const { closeModal, data } = useModalStore();
149+
const diagramData = data as DiagramViewerModalProps | undefined;
150+
151+
if (!diagramData || !diagramData.content) {
152+
return null;
153+
}
154+
155+
return (
156+
<Modal className="h-5/6 w-4/5 max-w-6xl bg-gray-1250" name={ModalName.diagramViewer}>
157+
<div className="flex h-full flex-col">
158+
<div className="flex-1 overflow-hidden">
159+
<MermaidDiagram code={diagramData.content} />
160+
</div>
161+
162+
<div className="flex items-center justify-end gap-3 border-t border-gray-950 bg-gray-1250 px-4 py-3">
163+
<Button
164+
ariaLabel={t("proceedButton")}
165+
className="h-12 bg-gray-1100 px-4 py-3 font-semibold"
166+
onClick={() => closeModal(ModalName.fileViewer)}
167+
variant="filled"
168+
>
169+
{t("proceedButton")}
170+
</Button>
171+
</div>
172+
</div>
173+
</Modal>
174+
);
175+
};
176+
177+
// Note: The original export default MermaidDiagram; is removed as this file now exports DiagramViewerModal.
178+
// The MermaidDiagram component is now a local component within this modal file.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import React from "react";
2+
3+
import { Editor } from "@monaco-editor/react";
4+
import { useTranslation } from "react-i18next";
5+
6+
import { ModalName } from "@enums/components";
7+
import { FileViewerModalProps } from "@interfaces/components";
8+
import { monacoLanguages } from "@src/constants";
9+
10+
import { useModalStore } from "@store";
11+
12+
import { Button, Loader } from "@components/atoms";
13+
import { CopyButton, Modal } from "@components/molecules";
14+
15+
export const FileViewerModal = () => {
16+
const { t } = useTranslation("modals", { keyPrefix: "fileViewer" });
17+
const { closeModal } = useModalStore();
18+
const fileData = useModalStore((state) => state.data as FileViewerModalProps);
19+
20+
if (!fileData || typeof (fileData as FileViewerModalProps).filename !== "string") {
21+
return null;
22+
}
23+
24+
const { filename, content, language } = fileData;
25+
26+
const fileExtension = `.${filename.split(".").pop() || ""}`;
27+
const editorLanguage =
28+
language ||
29+
(fileExtension in monacoLanguages
30+
? monacoLanguages[fileExtension as keyof typeof monacoLanguages]
31+
: "plaintext");
32+
33+
return (
34+
<Modal
35+
className="h-5/6 w-4/5 max-w-5xl overflow-hidden bg-gray-1250"
36+
closeButtonClass="p-2"
37+
name={ModalName.fileViewer}
38+
>
39+
<div className="flex h-full flex-col p-4">
40+
<h3 className="mb-2 text-lg text-white">{filename}</h3>
41+
42+
<div className="flex-1 overflow-hidden rounded">
43+
<Editor
44+
className="size-full"
45+
defaultLanguage={editorLanguage}
46+
defaultValue={content}
47+
loading={<Loader size="lg" />}
48+
options={{
49+
readOnly: true,
50+
minimap: { enabled: true },
51+
scrollBeyondLastLine: false,
52+
automaticLayout: true,
53+
fontFamily: "monospace, sans-serif",
54+
fontSize: 14,
55+
renderLineHighlight: "none",
56+
wordWrap: "on",
57+
}}
58+
theme="vs-dark"
59+
/>
60+
</div>
61+
62+
<div className="flex items-center justify-end gap-3 border-t border-gray-950 bg-gray-1250 px-4 py-3">
63+
<CopyButton
64+
buttonText={t("copyButton")}
65+
className="w-24 bg-gray-1100 px-4 py-3 font-semibold"
66+
size="md"
67+
text={content}
68+
/>
69+
<Button
70+
ariaLabel={t("proceedButton")}
71+
className="h-12 bg-gray-1100 px-4 py-3 font-semibold"
72+
onClick={() => closeModal(ModalName.fileViewer)}
73+
variant="filled"
74+
>
75+
{t("proceedButton")}
76+
</Button>
77+
</div>
78+
</div>
79+
</Modal>
80+
);
81+
};

src/components/organisms/modals/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ export { DeleteActiveDeploymentProjectModal } from "@components/organisms/modals
66
export { DeleteDrainingDeploymentProjectModal } from "@components/organisms/modals/deleteDrainingDeploymentProjectModal";
77
export { RateLimitModal } from "@components/organisms/modals/rateLimitModal";
88
export { QuotaLimitModal } from "@components/organisms/modals/quotaLimitModal";
9+
export { FileViewerModal } from "@components/organisms/modals/fileViewerModal";
10+
export { DiagramViewerModal } from "@components/organisms/modals/diagramViewerModal";

0 commit comments

Comments
 (0)