Skip to content

Commit 0a929f9

Browse files
Merge pull request #223 from OpenAssistantGPT/feat/download-pdf-transcript
Feat/download pdf transcript
2 parents b16164c + cc1e30b commit 0a929f9

12 files changed

Lines changed: 8088 additions & 5578 deletions

File tree

.changeset/shaggy-pandas-brake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openassistantgpt/ui': minor
3+
---
4+
5+
Improve the download transcript to be in pdf format

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: Setup pnpm
2424
uses: pnpm/action-setup@v4
2525
with:
26-
version: 8.6.9
26+
version: 9.12.1
2727

2828
- name: Use Node.js ${{ matrix.node-version }}
2929
uses: actions/setup-node@v4

.github/workflows/quality.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: Setup pnpm
1818
uses: pnpm/action-setup@v4
1919
with:
20-
version: 8.6.9
20+
version: 9.12.1
2121

2222
- name: Use Node.js 20
2323
uses: actions/setup-node@v3
@@ -41,7 +41,7 @@ jobs:
4141
- name: Setup pnpm
4242
uses: pnpm/action-setup@v4
4343
with:
44-
version: 8.6.9
44+
version: 9.12.1
4545

4646
- name: Use Node.js 18
4747
uses: actions/setup-node@v3
@@ -65,7 +65,7 @@ jobs:
6565
- name: Setup pnpm
6666
uses: pnpm/action-setup@v4
6767
with:
68-
version: 8.6.9
68+
version: 9.12.1
6969

7070
- name: Use Node.js 20
7171
uses: actions/setup-node@v3

.github/workflows/release-snapshot.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ jobs:
4242
with:
4343
fetch-depth: 0
4444

45-
- name: Setup pnpm 8
45+
- name: Setup pnpm 9
4646
uses: pnpm/action-setup@v4
4747
with:
48-
version: 8.6.9
48+
version: 9.12.1
4949

5050
- name: Setup Node.js 18.x
5151
uses: actions/setup-node@v2

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ jobs:
2323
with:
2424
fetch-depth: 0
2525

26-
- name: Setup pnpm 8
26+
- name: Setup pnpm 9
2727
uses: pnpm/action-setup@v4
2828
with:
29-
version: 8.6.9
29+
version: 9.12.1
3030

3131
- name: Setup Node.js 20.x
3232
uses: actions/setup-node@v2

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"keywords": [
5252
"ai"
5353
],
54-
"packageManager": "pnpm@8.6.9",
54+
"packageManager": "pnpm@9.12.1",
5555
"prettier": {
5656
"tabWidth": 2,
5757
"useTabs": false,

packages/ui/components/chat-header.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@ export function ChatHeader({
125125
style={{ color: chatbot.chatHeaderTextColor }}
126126
className="h-4 w-4"
127127
/>
128-
<span className="sr-only">Download Transcript</span>
128+
<span className="sr-only">Download as PDF</span>
129129
</Button>
130130
</TooltipTrigger>
131-
<TooltipContent>Download Transcript</TooltipContent>
131+
<TooltipContent>Download as PDF</TooltipContent>
132132
</Tooltip>
133133
{withExitX && (
134134
<Tooltip>
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import React, { ReactNode } from 'react';
2+
import {
3+
Document,
4+
Page,
5+
Text,
6+
View,
7+
StyleSheet,
8+
pdf,
9+
} from '@react-pdf/renderer';
10+
import { Message } from '@openassistantgpt/react';
11+
import { ChatbotConfig } from '@/src/chatbot';
12+
13+
const styles = StyleSheet.create({
14+
page: {
15+
flexDirection: 'column',
16+
backgroundColor: '#ffffff',
17+
padding: 40,
18+
fontFamily: 'Helvetica',
19+
},
20+
header: {
21+
marginBottom: 30,
22+
paddingBottom: 20,
23+
borderBottom: '2 solid #e5e7eb',
24+
},
25+
title: {
26+
fontSize: 24,
27+
fontWeight: 'bold',
28+
color: '#1f2937',
29+
marginBottom: 5,
30+
fontFamily: 'Helvetica-Bold',
31+
},
32+
subtitle: {
33+
fontSize: 12,
34+
color: '#6b7280',
35+
marginBottom: 10,
36+
fontFamily: 'Helvetica',
37+
},
38+
chatContainer: {
39+
flexDirection: 'column',
40+
gap: 16,
41+
},
42+
messageContainer: {
43+
flexDirection: 'column',
44+
marginBottom: 16,
45+
break: false, // Prevent breaking this container across pages
46+
},
47+
userMessage: {
48+
alignSelf: 'flex-end',
49+
maxWidth: '75%',
50+
wrap: false, // Keep the entire message together
51+
alignItems: 'flex-end', // Align all content to the right within the container
52+
},
53+
assistantMessage: {
54+
alignSelf: 'flex-start',
55+
maxWidth: '85%',
56+
wrap: false, // Keep the entire message together
57+
alignItems: 'flex-start', // Align all content to the left within the container
58+
},
59+
messageHeader: {
60+
flexDirection: 'row',
61+
alignItems: 'center',
62+
marginBottom: 6,
63+
},
64+
userBubble: {
65+
backgroundColor: '#3b82f6',
66+
color: '#ffffff',
67+
padding: 12,
68+
borderRadius: 18,
69+
borderBottomRightRadius: 4,
70+
break: false, // Prevent breaking the bubble across pages
71+
},
72+
assistantBubble: {
73+
backgroundColor: '#f3f4f6',
74+
color: '#1f2937',
75+
padding: 12,
76+
borderRadius: 18,
77+
borderBottomLeftRadius: 4,
78+
break: false, // Prevent breaking the bubble across pages
79+
},
80+
messageText: {
81+
fontSize: 11,
82+
lineHeight: 1.5,
83+
fontFamily: 'Helvetica',
84+
},
85+
roleLabel: {
86+
fontSize: 9,
87+
fontWeight: 'bold',
88+
color: '#6b7280',
89+
marginBottom: 4,
90+
textTransform: 'uppercase',
91+
letterSpacing: 0.5,
92+
fontFamily: 'Helvetica-Bold',
93+
},
94+
timestamp: {
95+
fontSize: 8,
96+
color: '#9ca3af',
97+
marginTop: 4,
98+
textAlign: 'right',
99+
fontFamily: 'Helvetica',
100+
},
101+
footer: {
102+
position: 'absolute',
103+
bottom: 30,
104+
left: 40,
105+
right: 40,
106+
textAlign: 'center',
107+
color: '#9ca3af',
108+
fontSize: 8,
109+
borderTop: '1 solid #e5e7eb',
110+
paddingTop: 10,
111+
fontFamily: 'Helvetica',
112+
},
113+
pageNumber: {
114+
position: 'absolute',
115+
fontSize: 8,
116+
bottom: 20,
117+
left: 0,
118+
right: 0,
119+
textAlign: 'center',
120+
color: '#9ca3af',
121+
fontFamily: 'Helvetica',
122+
},
123+
});
124+
125+
interface ChatPDFDocumentProps {
126+
messages: Message[];
127+
chatbot: ChatbotConfig;
128+
timestamp: string;
129+
}
130+
131+
const ChatPDFDocument: React.FC<ChatPDFDocumentProps> = ({
132+
messages,
133+
chatbot,
134+
timestamp,
135+
}) => {
136+
const formatMessageContent = (content: string) => {
137+
// Remove markdown formatting for PDF
138+
return content
139+
.replace(/\*\*(.*?)\*\*/g, '$1') // Remove bold
140+
.replace(/\*(.*?)\*/g, '$1') // Remove italic
141+
.replace(/```[\s\S]*?```/g, '[Code Block]') // Replace code blocks
142+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove links but keep text
143+
.replace(/#{1,6}\s/g, '') // Remove headers
144+
.replace(/\【.*?/g, '') // Remove any special annotations
145+
.trim(); // Clean up whitespace
146+
};
147+
148+
return (
149+
<Document>
150+
<Page size="A4" style={styles.page}>
151+
{/* Header */}
152+
<View style={styles.header}>
153+
<Text style={styles.title}>Chat Conversation</Text>
154+
<Text style={styles.subtitle}>
155+
{chatbot.chatTitle || chatbot.name}
156+
</Text>
157+
<Text style={styles.subtitle}>Exported on {timestamp}</Text>
158+
</View>
159+
160+
{/* Chat Content */}
161+
<View style={styles.chatContainer}>
162+
{/* Welcome Message */}
163+
<View
164+
style={[styles.messageContainer, styles.assistantMessage]}
165+
wrap={false}
166+
>
167+
<Text style={styles.roleLabel}>Assistant</Text>
168+
<View style={styles.assistantBubble}>
169+
<Text style={styles.messageText}>
170+
{formatMessageContent(chatbot.welcomeMessage)}
171+
</Text>
172+
</View>
173+
</View>
174+
175+
{/* Chat Messages */}
176+
{messages.map((message, index) => (
177+
<View
178+
key={`message-${index}`}
179+
style={[
180+
styles.messageContainer,
181+
message.role === 'user'
182+
? styles.userMessage
183+
: styles.assistantMessage,
184+
]}
185+
wrap={false} // Prevent individual messages from breaking across pages
186+
>
187+
<Text style={styles.roleLabel}>
188+
{message.role === 'user' ? 'You' : 'Assistant'}
189+
</Text>
190+
<View
191+
style={
192+
message.role === 'user'
193+
? styles.userBubble
194+
: styles.assistantBubble
195+
}
196+
>
197+
<Text style={styles.messageText}>
198+
{formatMessageContent(message.content)}
199+
</Text>
200+
</View>
201+
</View>
202+
))}
203+
</View>
204+
205+
{/* Footer */}
206+
<View style={styles.footer}>
207+
<Text>
208+
Generated by {chatbot.name || 'OpenAssistantGPT'} -{' '}
209+
{chatbot.footerTextName || 'OpenAssistantGPT'}
210+
</Text>
211+
</View>
212+
213+
{/* Page Number */}
214+
<Text
215+
style={styles.pageNumber}
216+
render={({ pageNumber, totalPages }) =>
217+
`Page ${pageNumber} of ${totalPages}`
218+
}
219+
fixed
220+
/>
221+
</Page>
222+
</Document>
223+
);
224+
};
225+
226+
export const generateChatPDF = async (
227+
messages: Message[],
228+
chatbot: ChatbotConfig,
229+
): Promise<Blob> => {
230+
const timestamp = new Date().toLocaleString();
231+
232+
const doc = (
233+
<ChatPDFDocument
234+
messages={messages}
235+
chatbot={chatbot}
236+
timestamp={timestamp}
237+
/>
238+
) as React.ReactElement;
239+
240+
const pdfBlob = await pdf(doc).toBlob();
241+
return pdfBlob;
242+
};
243+
244+
export default ChatPDFDocument;

packages/ui/components/chat.tsx

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { ChatbotConfig } from '@/src/chatbot';
2424
import { ChatHeader } from '@/components/chat-header';
2525
import { PreviewAttachment } from './preview-attachement';
2626
import { Attachment } from '@/types/attachements';
27+
import { generateChatPDF } from './chat-pdf-export';
2728

2829
interface ChatbotProps {
2930
chatbot: ChatbotConfig;
@@ -153,21 +154,28 @@ export function OpenAssistantGPTChat({
153154
window.parent.postMessage('closeChat', '*');
154155
}
155156

156-
function downloadTranscript() {
157-
const transcript =
158-
`assistant: ${chatbot.welcomeMessage}\n\n` +
159-
messages
160-
.map((msg: Message) => `${msg.role}: ${msg.content}`)
161-
.join('\n\n');
162-
const blob = new Blob([transcript], { type: 'text/plain' });
163-
const url = window.URL.createObjectURL(blob);
164-
const a = document.createElement('a');
165-
a.style.display = 'none';
166-
a.href = url;
167-
a.download = 'chat_transcript.txt';
168-
document.body.appendChild(a);
169-
a.click();
170-
window.URL.revokeObjectURL(url);
157+
async function downloadTranscript() {
158+
try {
159+
const pdfBlob = await generateChatPDF(messages, chatbot);
160+
const url = window.URL.createObjectURL(pdfBlob);
161+
const a = document.createElement('a');
162+
a.style.display = 'none';
163+
a.href = url;
164+
a.download = `chat_conversation_${
165+
new Date().toISOString().split('T')[0]
166+
}.pdf`;
167+
document.body.appendChild(a);
168+
a.click();
169+
window.URL.revokeObjectURL(url);
170+
document.body.removeChild(a);
171+
} catch (error) {
172+
console.error('Error generating PDF:', error);
173+
toast({
174+
title: 'Error',
175+
description: 'Failed to generate PDF. Please try again.',
176+
variant: 'destructive',
177+
});
178+
}
171179
}
172180

173181
// files

packages/ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@radix-ui/react-label": "^2.1.0",
3636
"@radix-ui/react-toast": "^1.2.1",
3737
"@radix-ui/react-tooltip": "^1.1.2",
38+
"@react-pdf/renderer": "^4.3.0",
3839
"@tailwindcss/typography": "^0.5.15",
3940
"autoprefixer": "^10.4.19",
4041
"better-react-mathjax": "^2.0.3",

0 commit comments

Comments
 (0)