Girish Jeswani commited on
Commit
0707d2f
·
1 Parent(s): 8b2581a

add export button

Browse files
phd-advisor-frontend/src/components/ExportButton.js ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Download, FileText, FileType, File, Check, X, Loader2 } from 'lucide-react';
3
+ import '../styles/ExportButton.css';
4
+
5
+ const ExportButton = ({ hasMessages = false }) => {
6
+ const [showDropdown, setShowDropdown] = useState(false);
7
+ const [isExporting, setIsExporting] = useState(false);
8
+ const [exportStatus, setExportStatus] = useState(null);
9
+ const [selectedType, setSelectedType] = useState('chat');
10
+
11
+ const exportTypes = [
12
+ {
13
+ id: 'chat',
14
+ name: 'Full Chat',
15
+ description: 'Complete conversation history',
16
+ endpoint: 'export-chat'
17
+ },
18
+ {
19
+ id: 'summary',
20
+ name: 'Chat Summary',
21
+ description: 'AI-generated conversation summary',
22
+ endpoint: 'chat-summary'
23
+ }
24
+ ];
25
+
26
+ const exportFormats = [
27
+ {
28
+ id: 'txt',
29
+ name: 'Text File',
30
+ description: 'Plain text format (.txt)',
31
+ icon: FileText,
32
+ extension: '.txt'
33
+ },
34
+ {
35
+ id: 'docx',
36
+ name: 'Word Document',
37
+ description: 'Microsoft Word format (.docx)',
38
+ icon: FileType,
39
+ extension: '.docx'
40
+ },
41
+ {
42
+ id: 'pdf',
43
+ name: 'PDF Document',
44
+ description: 'Portable Document Format (.pdf)',
45
+ icon: File,
46
+ extension: '.pdf'
47
+ }
48
+ ];
49
+
50
+ const handleExportClick = () => {
51
+ if (!hasMessages) return;
52
+ setShowDropdown(!showDropdown);
53
+ setExportStatus(null);
54
+ };
55
+
56
+ const handleFormatSelect = async (format) => {
57
+ setIsExporting(true);
58
+ setShowDropdown(false);
59
+ setExportStatus(null);
60
+
61
+ try {
62
+ const selectedTypeData = exportTypes.find(t => t.id === selectedType);
63
+ const endpoint = selectedTypeData.endpoint;
64
+
65
+ const response = await fetch(`http://localhost:8000/${endpoint}?format=${format}`, {
66
+ method: 'GET',
67
+ headers: {
68
+ 'Content-Type': 'application/json',
69
+ },
70
+ });
71
+
72
+ if (!response.ok) {
73
+ const errorData = await response.json().catch(() => ({}));
74
+ throw new Error(errorData.error || `Export failed with status ${response.status}`);
75
+ }
76
+
77
+ // Get the filename from the Content-Disposition header
78
+ const contentDisposition = response.headers.get('Content-Disposition');
79
+ let filename = `${selectedType}_export.${format}`;
80
+
81
+ if (contentDisposition) {
82
+ const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
83
+ if (filenameMatch && filenameMatch[1]) {
84
+ filename = filenameMatch[1].replace(/['"]/g, '');
85
+ }
86
+ }
87
+
88
+ // Create blob and download
89
+ const blob = await response.blob();
90
+ const url = window.URL.createObjectURL(blob);
91
+ const link = document.createElement('a');
92
+ link.href = url;
93
+ link.download = filename;
94
+ document.body.appendChild(link);
95
+ link.click();
96
+ document.body.removeChild(link);
97
+ window.URL.revokeObjectURL(url);
98
+
99
+ setExportStatus('success');
100
+ setTimeout(() => setExportStatus(null), 3000);
101
+
102
+ } catch (error) {
103
+ console.error('Export error:', error);
104
+ setExportStatus('error');
105
+ setTimeout(() => setExportStatus(null), 5000);
106
+ } finally {
107
+ setIsExporting(false);
108
+ }
109
+ };
110
+
111
+ const handleClickOutside = (e) => {
112
+ if (!e.target.closest('.export-button-container')) {
113
+ setShowDropdown(false);
114
+ }
115
+ };
116
+
117
+ React.useEffect(() => {
118
+ if (showDropdown) {
119
+ document.addEventListener('click', handleClickOutside);
120
+ return () => document.removeEventListener('click', handleClickOutside);
121
+ }
122
+ }, [showDropdown]);
123
+
124
+ const getButtonIcon = () => {
125
+ if (isExporting) return <Loader2 size={16} className="spinning" />;
126
+ if (exportStatus === 'success') return <Check size={16} />;
127
+ if (exportStatus === 'error') return <X size={16} />;
128
+ return <Download size={16} />;
129
+ };
130
+
131
+ const getButtonClass = () => {
132
+ let baseClass = 'export-button';
133
+ if (!hasMessages) baseClass += ' disabled';
134
+ if (showDropdown) baseClass += ' active';
135
+ if (exportStatus === 'success') baseClass += ' success';
136
+ if (exportStatus === 'error') baseClass += ' error';
137
+ return baseClass;
138
+ };
139
+
140
+ const getButtonTitle = () => {
141
+ if (!hasMessages) return 'No messages to export';
142
+ if (isExporting) return 'Exporting chat...';
143
+ if (exportStatus === 'success') return 'Export successful!';
144
+ if (exportStatus === 'error') return 'Export failed - click to retry';
145
+ return 'Export chat conversation';
146
+ };
147
+
148
+ return (
149
+ <div className="export-button-container">
150
+ <button
151
+ onClick={handleExportClick}
152
+ className={getButtonClass()}
153
+ disabled={!hasMessages || isExporting}
154
+ title={getButtonTitle()}
155
+ >
156
+ {getButtonIcon()}
157
+ <span className="export-text">Export</span>
158
+ </button>
159
+
160
+ {showDropdown && (
161
+ <div className="export-dropdown">
162
+ <div className="export-dropdown-header">
163
+ <h4>Export Options</h4>
164
+ <p>Choose what to export and in which format</p>
165
+ </div>
166
+
167
+ {/* ADD THIS NEW SECTION - Export Type Selection */}
168
+ <div className="export-type-section">
169
+ <h5>What to export:</h5>
170
+ <div className="export-type-buttons">
171
+ {exportTypes.map((type) => (
172
+ <button
173
+ key={type.id}
174
+ onClick={() => setSelectedType(type.id)}
175
+ className={`export-type-button ${selectedType === type.id ? 'active' : ''}`}
176
+ disabled={isExporting}
177
+ >
178
+ <div className="type-info">
179
+ <div className="type-name">{type.name}</div>
180
+ <div className="type-description">{type.description}</div>
181
+ </div>
182
+ </button>
183
+ ))}
184
+ </div>
185
+ </div>
186
+
187
+ {/* EXISTING FORMAT SECTION - UPDATE THE HEADER */}
188
+ <div className="export-format-section">
189
+ <h5>Format:</h5>
190
+ <div className="export-format-list">
191
+ {exportFormats.map((format) => {
192
+ const Icon = format.icon;
193
+ return (
194
+ <button
195
+ key={format.id}
196
+ onClick={() => handleFormatSelect(format.id)}
197
+ className="export-format-button"
198
+ disabled={isExporting}
199
+ >
200
+ <div className="format-icon">
201
+ <Icon size={20} />
202
+ </div>
203
+ <div className="format-info">
204
+ <div className="format-name">{format.name}</div>
205
+ <div className="format-description">{format.description}</div>
206
+ </div>
207
+ <div className="format-extension">{format.extension}</div>
208
+ </button>
209
+ );
210
+ })}
211
+ </div>
212
+ </div>
213
+
214
+ <div className="export-dropdown-footer">
215
+ <span>
216
+ {selectedType === 'chat'
217
+ ? 'Full conversation history will be included'
218
+ : 'AI will generate a concise summary of your conversation'
219
+ }
220
+ </span>
221
+ </div>
222
+ </div>
223
+ )}
224
+
225
+ {exportStatus && (
226
+ <div className={`export-status ${exportStatus}`}>
227
+ {exportStatus === 'success' && 'Chat exported successfully!'}
228
+ {exportStatus === 'error' && 'Export failed. Please try again.'}
229
+ </div>
230
+ )}
231
+ </div>
232
+ );
233
+ };
234
+
235
+ export default ExportButton;
phd-advisor-frontend/src/styles/ExportButton.css ADDED
@@ -0,0 +1,387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ .export-button-container {
3
+ position: relative;
4
+ display: inline-block;
5
+ }
6
+
7
+ /* Main Export Button */
8
+ .export-button {
9
+ display: flex;
10
+ align-items: center;
11
+ gap: 8px;
12
+ background: var(--bg-secondary);
13
+ border: 1px solid var(--border-secondary);
14
+ border-radius: 8px;
15
+ padding: 8px 16px;
16
+ font-size: 14px;
17
+ font-weight: 500;
18
+ color: var(--text-secondary);
19
+ cursor: pointer;
20
+ transition: all 0.2s ease;
21
+ height: 36px;
22
+ }
23
+
24
+ .export-button:hover:not(.disabled) {
25
+ background: var(--bg-tertiary);
26
+ border-color: var(--border-primary);
27
+ color: var(--text-primary);
28
+ transform: translateY(-1px);
29
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
30
+ }
31
+
32
+ .export-button.active {
33
+ background: var(--accent-primary);
34
+ border-color: var(--accent-primary);
35
+ color: white;
36
+ }
37
+
38
+ .export-button.disabled {
39
+ opacity: 0.5;
40
+ cursor: not-allowed;
41
+ }
42
+
43
+ .export-button.success {
44
+ background: #10B981;
45
+ border-color: #10B981;
46
+ color: white;
47
+ }
48
+
49
+ .export-button.error {
50
+ background: #EF4444;
51
+ border-color: #EF4444;
52
+ color: white;
53
+ }
54
+
55
+ .export-text {
56
+ font-weight: 500;
57
+ }
58
+
59
+ /* Spinning animation for loading state */
60
+ .spinning {
61
+ animation: spin 1s linear infinite;
62
+ }
63
+
64
+ @keyframes spin {
65
+ from { transform: rotate(0deg); }
66
+ to { transform: rotate(360deg); }
67
+ }
68
+
69
+ /* Export Dropdown */
70
+ .export-dropdown {
71
+ position: absolute;
72
+ top: 100%;
73
+ right: 0;
74
+ margin-top: 8px;
75
+ background: var(--bg-primary);
76
+ border: 1px solid var(--border-secondary);
77
+ border-radius: 12px;
78
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
79
+ min-width: 280px;
80
+ z-index: 1000;
81
+ overflow: hidden;
82
+ animation: dropdownSlideIn 0.2s ease-out;
83
+ }
84
+
85
+ @keyframes dropdownSlideIn {
86
+ from {
87
+ opacity: 0;
88
+ transform: translateY(-8px) scale(0.98);
89
+ }
90
+ to {
91
+ opacity: 1;
92
+ transform: translateY(0) scale(1);
93
+ }
94
+ }
95
+
96
+ /* Dark theme shadow */
97
+ [data-theme="dark"] .export-dropdown {
98
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
99
+ }
100
+
101
+ /* Dropdown Header */
102
+ .export-dropdown-header {
103
+ padding: 16px 20px 12px 20px;
104
+ border-bottom: 1px solid var(--border-tertiary);
105
+ }
106
+
107
+ .export-dropdown-header h4 {
108
+ margin: 0 0 4px 0;
109
+ font-size: 16px;
110
+ font-weight: 600;
111
+ color: var(--text-primary);
112
+ }
113
+
114
+ .export-dropdown-header p {
115
+ margin: 0;
116
+ font-size: 13px;
117
+ color: var(--text-secondary);
118
+ line-height: 1.4;
119
+ }
120
+
121
+ /* Format List */
122
+ .export-format-list {
123
+ padding: 0;
124
+ }
125
+
126
+ .export-format-button {
127
+ width: 100%;
128
+ display: flex;
129
+ align-items: center;
130
+ gap: 12px;
131
+ padding: 12px 20px;
132
+ border: none;
133
+ background: none;
134
+ cursor: pointer;
135
+ transition: all 0.2s ease;
136
+ text-align: left;
137
+ }
138
+
139
+ .export-format-button:hover:not(:disabled) {
140
+ background: var(--bg-secondary);
141
+ }
142
+
143
+ .export-format-button:disabled {
144
+ opacity: 0.5;
145
+ cursor: not-allowed;
146
+ }
147
+
148
+ .format-icon {
149
+ width: 40px;
150
+ height: 40px;
151
+ background: var(--bg-secondary);
152
+ border-radius: 8px;
153
+ display: flex;
154
+ align-items: center;
155
+ justify-content: center;
156
+ color: var(--text-secondary);
157
+ flex-shrink: 0;
158
+ }
159
+
160
+ .export-format-button:hover:not(:disabled) .format-icon {
161
+ background: var(--bg-tertiary);
162
+ color: var(--text-primary);
163
+ }
164
+
165
+ .format-info {
166
+ flex: 1;
167
+ min-width: 0;
168
+ }
169
+
170
+ .format-name {
171
+ font-size: 14px;
172
+ font-weight: 500;
173
+ color: var(--text-primary);
174
+ margin-bottom: 2px;
175
+ }
176
+
177
+ .format-description {
178
+ font-size: 12px;
179
+ color: var(--text-secondary);
180
+ line-height: 1.3;
181
+ }
182
+ .export-format-section {
183
+ padding: 16px 20px 8px 20px;
184
+ }
185
+
186
+ .export-format-section h5 {
187
+ margin: 0 0 12px 0;
188
+ font-size: 13px;
189
+ font-weight: 600;
190
+ color: var(--text-secondary);
191
+ text-transform: uppercase;
192
+ letter-spacing: 0.5px;
193
+ }
194
+
195
+
196
+ .format-extension {
197
+ font-size: 12px;
198
+ font-weight: 600;
199
+ color: var(--text-tertiary);
200
+ background: var(--bg-tertiary);
201
+ padding: 4px 8px;
202
+ border-radius: 4px;
203
+ flex-shrink: 0;
204
+ }
205
+
206
+ .export-format-button:hover:not(:disabled) .format-extension {
207
+ background: var(--accent-primary);
208
+ color: white;
209
+ }
210
+
211
+ /* Export Type Selection Section */
212
+ .export-type-section {
213
+ padding: 16px 20px 8px 20px;
214
+ border-bottom: 1px solid var(--border-tertiary);
215
+ }
216
+
217
+ .export-type-section h5 {
218
+ margin: 0 0 12px 0;
219
+ font-size: 13px;
220
+ font-weight: 600;
221
+ color: var(--text-secondary);
222
+ text-transform: uppercase;
223
+ letter-spacing: 0.5px;
224
+ }
225
+
226
+ .export-type-buttons {
227
+ display: flex;
228
+ flex-direction: column;
229
+ gap: 8px;
230
+ }
231
+
232
+ .export-type-button {
233
+ width: 100%;
234
+ display: flex;
235
+ align-items: center;
236
+ padding: 12px 16px;
237
+ border: 2px solid var(--border-secondary);
238
+ border-radius: 8px;
239
+ background: var(--bg-secondary);
240
+ cursor: pointer;
241
+ transition: all 0.2s ease;
242
+ text-align: left;
243
+ color: var(--text-primary); /* ADD THIS */
244
+ }
245
+
246
+ .export-type-button:hover:not(:disabled) {
247
+ border-color: var(--accent-primary);
248
+ background: var(--bg-tertiary);
249
+ color: var(--text-primary); /* ADD THIS */
250
+ }
251
+
252
+ .export-type-button.active {
253
+ border-color: var(--accent-primary);
254
+ background: var(--accent-primary);
255
+ color: white !important; /* MAKE THIS IMPORTANT */
256
+ }
257
+
258
+ .export-type-button:disabled {
259
+ opacity: 0.5;
260
+ cursor: not-allowed;
261
+ }
262
+
263
+ .type-info {
264
+ flex: 1;
265
+ }
266
+
267
+ .type-name {
268
+ font-size: 14px;
269
+ font-weight: 600;
270
+ margin-bottom: 2px;
271
+ color: inherit; /* ADD THIS */
272
+ }
273
+
274
+ .type-description {
275
+ font-size: 12px;
276
+ opacity: 0.8;
277
+ line-height: 1.3;
278
+ color: inherit; /* ADD THIS */
279
+ }
280
+
281
+ /* Remove the separate active state color rules since we use inherit now */
282
+ .export-type-button.active .type-name,
283
+ .export-type-button.active .type-description {
284
+ color: inherit; /* CHANGE THIS TO INHERIT */
285
+ }
286
+
287
+ /* Format Section Updates */
288
+ .export-format-section {
289
+ padding: 16px 20px 8px 20px;
290
+ }
291
+
292
+ .export-format-section h5 {
293
+ margin: 0 0 12px 0;
294
+ font-size: 13px;
295
+ font-weight: 600;
296
+ color: var(--text-secondary);
297
+ text-transform: uppercase;
298
+ letter-spacing: 0.5px;
299
+ }
300
+
301
+ .export-format-list {
302
+ padding: 0;
303
+ }
304
+
305
+ /* Focus states for accessibility */
306
+ .export-type-button:focus-visible {
307
+ outline: 2px solid var(--accent-primary);
308
+ outline-offset: 2px;
309
+ }
310
+
311
+ /* Dropdown Footer */
312
+ .export-dropdown-footer {
313
+ padding: 8px 20px 16px 20px;
314
+ border-top: 1px solid var(--border-tertiary);
315
+ }
316
+
317
+ .export-dropdown-footer span {
318
+ font-size: 11px;
319
+ color: var(--text-tertiary);
320
+ font-style: italic;
321
+ }
322
+
323
+ /* Export Status */
324
+ .export-status {
325
+ position: absolute;
326
+ top: 100%;
327
+ right: 0;
328
+ margin-top: 4px;
329
+ padding: 8px 12px;
330
+ border-radius: 6px;
331
+ font-size: 12px;
332
+ font-weight: 500;
333
+ white-space: nowrap;
334
+ z-index: 1001;
335
+ animation: statusSlideIn 0.2s ease-out;
336
+ }
337
+
338
+ .export-status.success {
339
+ background: #10B981;
340
+ color: white;
341
+ }
342
+
343
+ .export-status.error {
344
+ background: #EF4444;
345
+ color: white;
346
+ }
347
+
348
+ @keyframes statusSlideIn {
349
+ from {
350
+ opacity: 0;
351
+ transform: translateY(-4px);
352
+ }
353
+ to {
354
+ opacity: 1;
355
+ transform: translateY(0);
356
+ }
357
+ }
358
+
359
+ /* Responsive Design */
360
+ @media (max-width: 768px) {
361
+ .export-dropdown {
362
+ right: -20px;
363
+ left: -20px;
364
+ width: auto;
365
+ min-width: unset;
366
+ }
367
+
368
+ .export-format-button {
369
+ padding: 16px 20px;
370
+ }
371
+
372
+ .format-icon {
373
+ width: 36px;
374
+ height: 36px;
375
+ }
376
+ }
377
+
378
+ /* Focus states for accessibility */
379
+ .export-button:focus-visible {
380
+ outline: 2px solid var(--accent-primary);
381
+ outline-offset: 2px;
382
+ }
383
+
384
+ .export-format-button:focus-visible {
385
+ outline: 2px solid var(--accent-primary);
386
+ outline-offset: -2px;
387
+ }