Skip to content

Commit 9123368

Browse files
Merge pull request #239 from OpenAssistantGPT/feat/spellcheck
feat: implement PDF download for chat transcripts
2 parents 845cbb4 + f2fd87f commit 9123368

File tree

5 files changed

+664
-23
lines changed

5 files changed

+664
-23
lines changed

.changeset/silent-coats-sleep.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openassistantgpt/ui': patch
3+
---
4+
5+
Dowload pdf instead of txt file for transcript

packages/ui/components/chat.tsx

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Icons } from '@/components/icons';
44
import { Label } from '@/components/ui/label';
55
import { Textarea } from '@/components/ui/textarea';
66
import { cn } from '@/lib/utils';
7+
import { generatePDFTranscript } from '@/lib/pdf-utils';
78

89
import { useAssistant, Message } from '@openassistantgpt/react';
910
import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';
@@ -153,21 +154,17 @@ 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+
await generatePDFTranscript(messages, chatbot);
160+
} catch (error) {
161+
console.error('Error generating PDF:', error);
162+
toast({
163+
title: 'Error',
164+
description: 'Failed to generate PDF transcript. Please try again.',
165+
variant: 'destructive',
166+
});
167+
}
171168
}
172169

173170
// files

packages/ui/lib/pdf-utils.tsx

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import React from 'react';
2+
// @ts-ignore - Type conflicts with React 19 RC
3+
import {
4+
Document,
5+
Page,
6+
Text,
7+
View,
8+
StyleSheet,
9+
pdf,
10+
} from '@react-pdf/renderer';
11+
import { Message } from '@openassistantgpt/react';
12+
import { ChatbotConfig } from '../src/chatbot';
13+
14+
// Define styles for the PDF with bubble layout
15+
const styles = StyleSheet.create({
16+
page: {
17+
flexDirection: 'column',
18+
backgroundColor: '#ffffff',
19+
padding: 20,
20+
fontSize: 12,
21+
fontFamily: 'Helvetica',
22+
},
23+
header: {
24+
fontSize: 20,
25+
fontWeight: 'bold',
26+
textAlign: 'center',
27+
marginBottom: 15,
28+
color: '#2563eb',
29+
},
30+
subtitle: {
31+
fontSize: 10,
32+
textAlign: 'center',
33+
color: '#6b7280',
34+
marginBottom: 20,
35+
},
36+
divider: {
37+
borderBottomWidth: 1,
38+
borderBottomColor: '#e5e7eb',
39+
marginBottom: 20,
40+
},
41+
messageRow: {
42+
flexDirection: 'row',
43+
marginBottom: 15,
44+
width: '100%',
45+
},
46+
// Assistant messages - left aligned
47+
assistantRow: {
48+
justifyContent: 'flex-start',
49+
},
50+
assistantBubble: {
51+
backgroundColor: '#f3f4f6',
52+
borderRadius: 15,
53+
borderTopLeftRadius: 5,
54+
padding: 12,
55+
maxWidth: '70%',
56+
borderWidth: 1,
57+
borderColor: '#e5e7eb',
58+
},
59+
assistantLabel: {
60+
fontSize: 10,
61+
fontWeight: 'bold',
62+
color: '#2563eb',
63+
marginBottom: 4,
64+
},
65+
assistantText: {
66+
fontSize: 11,
67+
lineHeight: 1.4,
68+
color: '#374151',
69+
},
70+
// User messages - right aligned
71+
userRow: {
72+
justifyContent: 'flex-end',
73+
},
74+
userBubble: {
75+
backgroundColor: '#f3f4f6',
76+
borderColor: '#e5e7eb',
77+
borderWidth: 1,
78+
borderRadius: 15,
79+
borderTopRightRadius: 5,
80+
padding: 12,
81+
maxWidth: '70%',
82+
},
83+
userLabel: {
84+
fontSize: 10,
85+
fontWeight: 'bold',
86+
color: '#2563eb',
87+
marginBottom: 4,
88+
},
89+
userText: {
90+
fontSize: 11,
91+
lineHeight: 1.4,
92+
color: '#374151',
93+
},
94+
footer: {
95+
position: 'absolute',
96+
bottom: 20,
97+
left: 0,
98+
right: 0,
99+
textAlign: 'center',
100+
fontSize: 8,
101+
color: '#9ca3af',
102+
},
103+
});
104+
105+
// Clean message content from markdown and special characters
106+
function cleanMessageContent(content: string): string {
107+
return content
108+
.replace(/\*\*(.*?)\*\*/g, '$1') // Remove bold markdown
109+
.replace(/\*(.*?)\*/g, '$1') // Remove italic markdown
110+
.replace(/`(.*?)`/g, '$1') // Remove inline code markdown
111+
.replace(/```[\s\S]*?```/g, '[Code Block]') // Replace code blocks
112+
.replace(/#{1,6}\s*(.*)/g, '$1') // Remove headers
113+
.replace(/\[(.*?)\]\((.*?)\)/g, '$1 ($2)') // Convert links
114+
.replace(/\【.*?/g, '') // Remove citation markers
115+
.replace(/loading/g, 'Generating response...')
116+
.trim();
117+
}
118+
119+
// Function to generate and download PDF
120+
export async function generatePDFTranscript(
121+
messages: Message[],
122+
chatbot: ChatbotConfig,
123+
): Promise<void> {
124+
try {
125+
// @ts-ignore - Working around React 19 RC type conflicts
126+
const PDFDocument = React.createElement(
127+
Document as any,
128+
{},
129+
// @ts-ignore
130+
React.createElement(
131+
Page as any,
132+
{ size: 'A4', style: styles.page },
133+
// Header
134+
// @ts-ignore
135+
React.createElement(
136+
Text as any,
137+
{ style: styles.header },
138+
'Chat Transcript',
139+
),
140+
// @ts-ignore
141+
React.createElement(
142+
Text as any,
143+
{ style: styles.subtitle },
144+
`Generated: ${new Date().toLocaleString()}`,
145+
),
146+
// @ts-ignore
147+
React.createElement(View as any, { style: styles.divider }),
148+
149+
// Welcome Message - styled like assistant bubble
150+
...(chatbot.welcomeMessage
151+
? [
152+
// @ts-ignore
153+
React.createElement(
154+
View as any,
155+
{
156+
style: [styles.messageRow, styles.assistantRow],
157+
key: 'welcome',
158+
},
159+
// @ts-ignore
160+
React.createElement(
161+
View as any,
162+
{ style: styles.assistantBubble },
163+
// @ts-ignore
164+
React.createElement(
165+
Text as any,
166+
{ style: styles.assistantLabel },
167+
'Assistant:',
168+
),
169+
// @ts-ignore
170+
React.createElement(
171+
Text as any,
172+
{ style: styles.assistantText },
173+
cleanMessageContent(chatbot.welcomeMessage),
174+
),
175+
),
176+
),
177+
]
178+
: []),
179+
180+
// Messages with bubble layout
181+
...messages.map((message, index) => {
182+
const isUser = message.role === 'user';
183+
// @ts-ignore
184+
return React.createElement(
185+
View as any,
186+
{
187+
style: [
188+
styles.messageRow,
189+
isUser ? styles.userRow : styles.assistantRow,
190+
],
191+
key: index.toString(),
192+
},
193+
// @ts-ignore
194+
React.createElement(
195+
View as any,
196+
{
197+
style: isUser ? styles.userBubble : styles.assistantBubble,
198+
},
199+
// @ts-ignore
200+
React.createElement(
201+
Text as any,
202+
{
203+
style: isUser ? styles.userLabel : styles.assistantLabel,
204+
},
205+
isUser ? 'You:' : 'Assistant:',
206+
),
207+
// @ts-ignore
208+
React.createElement(
209+
Text as any,
210+
{
211+
style: isUser ? styles.userText : styles.assistantText,
212+
},
213+
cleanMessageContent(message.content),
214+
),
215+
),
216+
);
217+
}),
218+
219+
// Footer
220+
// @ts-ignore
221+
React.createElement(Text as any, {
222+
style: styles.footer,
223+
render: ({ pageNumber, totalPages }: any) =>
224+
`Page ${pageNumber} of ${totalPages}`,
225+
fixed: true,
226+
}),
227+
),
228+
);
229+
230+
// Generate PDF blob
231+
// @ts-ignore - Final type assertion for React 19 RC compatibility
232+
const blob = await pdf(PDFDocument as any).toBlob();
233+
234+
// Create download link
235+
const url = URL.createObjectURL(blob);
236+
const link = document.createElement('a');
237+
link.href = url;
238+
link.download = `chat-transcript-${
239+
new Date().toISOString().split('T')[0]
240+
}.pdf`;
241+
242+
// Trigger download
243+
document.body.appendChild(link);
244+
link.click();
245+
246+
// Cleanup
247+
document.body.removeChild(link);
248+
URL.revokeObjectURL(url);
249+
} catch (error) {
250+
console.error('Error generating PDF:', error);
251+
throw new Error('Failed to generate PDF transcript');
252+
}
253+
}

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": "^3.1.14",
3839
"@tailwindcss/typography": "^0.5.15",
3940
"autoprefixer": "^10.4.19",
4041
"better-react-mathjax": "^2.0.3",

0 commit comments

Comments
 (0)