Skip to content

Commit 86bfb1d

Browse files
authored
Merge pull request #79 from Yugansh5013/feature/export-chat
Add export chat menu with multiple download formats
2 parents c4e4217 + 12c7673 commit 86bfb1d

File tree

9 files changed

+736
-45
lines changed

9 files changed

+736
-45
lines changed

frontend/package-lock.json

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

frontend/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
"preview": "vite preview"
1414
},
1515
"dependencies": {
16+
"docx": "^9.5.1",
17+
"jspdf": "^3.0.4",
18+
"lucide-react": "^0.562.0",
1619
"react": "^19.1.0",
1720
"react-dom": "^19.1.0",
1821
"react-use-websocket": "^4.13.0",
@@ -25,7 +28,7 @@
2528
"@testing-library/jest-dom": "^6.6.3",
2629
"@testing-library/react": "^16.3.0",
2730
"@types/jest": "^30.0.0",
28-
"@types/react": "^19.1.2",
31+
"@types/react": "^19.2.7",
2932
"@types/react-dom": "^19.1.2",
3033
"@vitejs/plugin-react": "^4.4.1",
3134
"eslint": "^9.25.0",
@@ -36,9 +39,9 @@
3639
"jest-axe": "^10.0.0",
3740
"jest-environment-jsdom": "^30.0.2",
3841
"jest-fetch-mock": "^3.0.3",
42+
"prettier": "^3.6.2",
3943
"ts-jest": "^29.4.0",
4044
"ts-node": "^10.9.2",
41-
"prettier": "^3.6.2",
4245
"typescript": "~5.8.3",
4346
"typescript-eslint": "^8.30.1",
4447
"vite": "^6.3.5"

frontend/src/components/Chatbot.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ export const Chatbot = () => {
324324
currentSessionId={currentSessionId}
325325
openSideBar={openSideBar}
326326
clearMessages={openConfirmDeleteChatPopup}
327+
messages={getSessionMessages(currentSessionId)}
327328
/>
328329
{currentSessionId !== null ? (
329330
<>

frontend/src/components/Header.tsx

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
import { getChatbotText } from "../data/chatbotTexts";
22
import { chatbotStyles } from "../styles/styles";
3+
import {
4+
exportAsTxt,
5+
exportAsMd,
6+
exportAsDocx,
7+
exportAsPdf,
8+
} from "../utils/exportchat";
9+
import { type Message } from "../model/Message";
10+
import { useEffect, useRef, useState } from "react";
11+
import {
12+
Upload,
13+
Trash2,
14+
FileText,
15+
FileCode,
16+
FileSpreadsheet,
17+
File,
18+
} from "lucide-react";
319

420
/**
521
* Props for the Header component.
@@ -8,6 +24,7 @@ export interface HeaderProps {
824
currentSessionId: string | null;
925
clearMessages: (chatSessionId: string) => void;
1026
openSideBar: () => void;
27+
messages: Message[];
1128
}
1229

1330
/**
@@ -19,7 +36,29 @@ export const Header = ({
1936
currentSessionId,
2037
clearMessages,
2138
openSideBar,
39+
messages,
2240
}: HeaderProps) => {
41+
const [showExportMenu, setShowExportMenu] = useState(false);
42+
const exportMenuRef = useRef<HTMLDivElement | null>(null);
43+
44+
useEffect(() => {
45+
const handleClickOutside = (event: MouseEvent) => {
46+
if (
47+
showExportMenu &&
48+
exportMenuRef.current &&
49+
!exportMenuRef.current.contains(event.target as Node)
50+
) {
51+
setShowExportMenu(false);
52+
}
53+
};
54+
55+
document.addEventListener("mousedown", handleClickOutside);
56+
57+
return () => {
58+
document.removeEventListener("mousedown", handleClickOutside);
59+
};
60+
}, [showExportMenu]);
61+
2362
return (
2463
<div style={chatbotStyles.chatbotHeader}>
2564
<button
@@ -30,12 +69,74 @@ export const Header = ({
3069
{getChatbotText("sidebarLabel")}
3170
</button>
3271
{currentSessionId !== null && (
33-
<button
34-
onClick={() => clearMessages(currentSessionId)}
35-
style={chatbotStyles.clearButton}
36-
>
37-
{getChatbotText("clearChat")}
38-
</button>
72+
<div ref={exportMenuRef} style={chatbotStyles.headerActions}>
73+
<div style={{ position: "relative", display: "inline-block" }}>
74+
{/* Export button */}
75+
<button
76+
onClick={() => setShowExportMenu((prev) => !prev)}
77+
style={chatbotStyles.exportButton}
78+
title="Export chat"
79+
aria-label="Export chat"
80+
>
81+
<Upload size={16} />
82+
</button>
83+
84+
{/* Export menu */}
85+
{showExportMenu && (
86+
<div style={chatbotStyles.exportMenu}>
87+
<button
88+
style={chatbotStyles.exportMenuItem}
89+
onClick={() => {
90+
exportAsTxt(messages);
91+
setShowExportMenu(false);
92+
}}
93+
>
94+
<FileText size={20} />
95+
<span>.txt</span>
96+
</button>
97+
<button
98+
style={chatbotStyles.exportMenuItem}
99+
onClick={() => {
100+
exportAsMd(messages);
101+
setShowExportMenu(false);
102+
}}
103+
>
104+
<FileCode size={20} />
105+
<span>.md</span>
106+
</button>
107+
<button
108+
style={chatbotStyles.exportMenuItem}
109+
onClick={() => {
110+
exportAsDocx(messages);
111+
setShowExportMenu(false);
112+
}}
113+
>
114+
<FileSpreadsheet size={20} />
115+
<span>.docx</span>
116+
</button>
117+
<button
118+
style={chatbotStyles.exportMenuItem}
119+
onClick={() => {
120+
exportAsPdf(messages);
121+
setShowExportMenu(false);
122+
}}
123+
>
124+
<File size={20} />
125+
<span>.pdf</span>
126+
</button>
127+
</div>
128+
)}
129+
</div>
130+
131+
<button
132+
onClick={() => clearMessages(currentSessionId)}
133+
style={chatbotStyles.clearButton}
134+
title="Clear chat"
135+
aria-label="Clear chat"
136+
>
137+
<Trash2 size={16} />
138+
</button>
139+
</div>
39140
)}
40141
</div>
41142
);

frontend/src/data/chatbotTexts.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"placeholder": "Type your message...",
66
"noInputWarningMessage": "Please enter a message before sending.",
77
"generatingMessage": "Generating...",
8-
"clearChat": "Delete chat",
8+
"exportChat": "Export chat",
9+
"clearChat": "Clear chat",
910
"popupTitle": "Delete this chat?",
1011
"popupMessage": "This action cannot be undone. Are you sure you want to delete this conversation?",
1112
"popupDeleteButton": "Delete",

0 commit comments

Comments
 (0)