Girish Jeswani commited on
Commit
219cba3
·
1 Parent(s): 6bbf91b

add file upload feature

Browse files
phd-advisor-frontend/src/components/FileUpload.js ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef } from 'react';
2
+ import { Upload, FileText, File, X, CheckCircle, AlertCircle } from 'lucide-react';
3
+ import { useTheme } from '../contexts/ThemeContext';
4
+ import '../styles/FileUpload.css'
5
+
6
+ const FileUpload = ({ onFileUploaded, isUploading, onUploadStart }) => {
7
+ const [dragActive, setDragActive] = useState(false);
8
+ const [uploadStatus, setUploadStatus] = useState(null); // 'success', 'error', null
9
+ const [uploadMessage, setUploadMessage] = useState('');
10
+ const [selectedFile, setSelectedFile] = useState(null);
11
+ const fileInputRef = useRef(null);
12
+ const { isDark } = useTheme();
13
+
14
+ const supportedTypes = {
15
+ 'application/pdf': { ext: 'PDF', icon: FileText, color: '#EF4444' },
16
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': { ext: 'DOCX', icon: File, color: '#3B82F6' },
17
+ 'text/plain': { ext: 'TXT', icon: FileText, color: '#10B981' }
18
+ };
19
+
20
+ const validateFile = (file) => {
21
+ if (!supportedTypes[file.type]) {
22
+ return { valid: false, error: 'Only PDF, DOCX, and TXT files are supported.' };
23
+ }
24
+
25
+ if (file.size > 10 * 1024 * 1024) { // 10MB limit
26
+ return { valid: false, error: 'File size must be less than 10MB.' };
27
+ }
28
+
29
+ return { valid: true };
30
+ };
31
+
32
+ const uploadFile = async (file) => {
33
+ const validation = validateFile(file);
34
+ if (!validation.valid) {
35
+ setUploadStatus('error');
36
+ setUploadMessage(validation.error);
37
+ return;
38
+ }
39
+
40
+ setSelectedFile(file);
41
+ onUploadStart && onUploadStart();
42
+
43
+ const formData = new FormData();
44
+ formData.append('file', file);
45
+
46
+ try {
47
+ const response = await fetch('http://localhost:8000/upload-document', {
48
+ method: 'POST',
49
+ body: formData,
50
+ });
51
+
52
+ if (response.ok) {
53
+ const data = await response.json();
54
+ setUploadStatus('success');
55
+ setUploadMessage(`${file.name} uploaded successfully and added to context.`);
56
+ onFileUploaded && onFileUploaded(file, data);
57
+
58
+ // Auto-clear success message after 5 seconds
59
+ setTimeout(() => {
60
+ setUploadStatus(null);
61
+ setSelectedFile(null);
62
+ setUploadMessage('');
63
+ }, 5000);
64
+ } else {
65
+ const errorData = await response.json();
66
+ throw new Error(errorData.detail || 'Upload failed');
67
+ }
68
+ } catch (error) {
69
+ setUploadStatus('error');
70
+ setUploadMessage(`Upload failed: ${error.message}`);
71
+ console.error('Upload error:', error);
72
+ }
73
+ };
74
+
75
+ const handleDrag = (e) => {
76
+ e.preventDefault();
77
+ e.stopPropagation();
78
+ if (e.type === 'dragenter' || e.type === 'dragover') {
79
+ setDragActive(true);
80
+ } else if (e.type === 'dragleave') {
81
+ setDragActive(false);
82
+ }
83
+ };
84
+
85
+ const handleDrop = (e) => {
86
+ e.preventDefault();
87
+ e.stopPropagation();
88
+ setDragActive(false);
89
+
90
+ if (isUploading) return;
91
+
92
+ const files = Array.from(e.dataTransfer.files);
93
+ if (files.length > 0) {
94
+ uploadFile(files[0]);
95
+ }
96
+ };
97
+
98
+ const handleFileSelect = (e) => {
99
+ const files = Array.from(e.target.files);
100
+ if (files.length > 0) {
101
+ uploadFile(files[0]);
102
+ }
103
+ };
104
+
105
+ const openFileDialog = () => {
106
+ if (!isUploading) {
107
+ fileInputRef.current?.click();
108
+ }
109
+ };
110
+
111
+ const clearStatus = () => {
112
+ setUploadStatus(null);
113
+ setSelectedFile(null);
114
+ setUploadMessage('');
115
+ };
116
+
117
+ const getFileIcon = (file) => {
118
+ const fileInfo = supportedTypes[file.type];
119
+ if (fileInfo) {
120
+ const Icon = fileInfo.icon;
121
+ return <Icon size={16} style={{ color: fileInfo.color }} />;
122
+ }
123
+ return <File size={16} />;
124
+ };
125
+
126
+ return (
127
+ <div className="file-upload-container">
128
+ {/* Upload Status Banner */}
129
+ {uploadStatus && (
130
+ <div className={`upload-status ${uploadStatus}`}>
131
+ <div className="status-content">
132
+ {uploadStatus === 'success' ? (
133
+ <CheckCircle size={16} className="status-icon success" />
134
+ ) : (
135
+ <AlertCircle size={16} className="status-icon error" />
136
+ )}
137
+ <span className="status-message">{uploadMessage}</span>
138
+ <button onClick={clearStatus} className="status-close">
139
+ <X size={14} />
140
+ </button>
141
+ </div>
142
+ </div>
143
+ )}
144
+
145
+ {/* Upload Area */}
146
+ <div
147
+ className={`file-upload-area ${dragActive ? 'drag-active' : ''} ${isUploading ? 'uploading' : ''}`}
148
+ onDragEnter={handleDrag}
149
+ onDragLeave={handleDrag}
150
+ onDragOver={handleDrag}
151
+ onDrop={handleDrop}
152
+ onClick={openFileDialog}
153
+ >
154
+ <input
155
+ ref={fileInputRef}
156
+ type="file"
157
+ onChange={handleFileSelect}
158
+ accept=".pdf,.docx,.txt"
159
+ style={{ display: 'none' }}
160
+ disabled={isUploading}
161
+ />
162
+
163
+ <div className="upload-content">
164
+ {isUploading ? (
165
+ <>
166
+ <div className="upload-spinner">
167
+ <div className="spinner"></div>
168
+ </div>
169
+ <p className="upload-text">Uploading {selectedFile?.name}...</p>
170
+ </>
171
+ ) : (
172
+ <>
173
+ <Upload size={24} className="upload-icon" />
174
+ <p className="upload-text">
175
+ <span className="upload-primary">Click to upload</span> or drag and drop
176
+ </p>
177
+ <p className="upload-secondary">PDF, DOCX, or TXT files only</p>
178
+ </>
179
+ )}
180
+ </div>
181
+ </div>
182
+
183
+ {/* Supported File Types */}
184
+ <div className="supported-types">
185
+ {Object.entries(supportedTypes).map(([mimeType, info]) => {
186
+ const Icon = info.icon;
187
+ return (
188
+ <div key={mimeType} className="file-type-chip">
189
+ <Icon size={12} style={{ color: info.color }} />
190
+ <span>{info.ext}</span>
191
+ </div>
192
+ );
193
+ })}
194
+ </div>
195
+ </div>
196
+ );
197
+ };
198
+
199
+ export default FileUpload;
phd-advisor-frontend/src/styles/FileUpload.css ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* File Upload Container */
2
+ .file-upload-container {
3
+ width: 100%;
4
+ margin-bottom: 12px;
5
+ }
6
+
7
+ /* Upload Status Banner */
8
+ .upload-status {
9
+ padding: 8px 12px;
10
+ border-radius: 8px;
11
+ margin-bottom: 8px;
12
+ border: 1px solid;
13
+ animation: slideDown 0.3s ease-out;
14
+ }
15
+
16
+ .upload-status.success {
17
+ background-color: #DCFCE7;
18
+ border-color: #22C55E;
19
+ color: #15803D;
20
+ }
21
+
22
+ [data-theme="dark"] .upload-status.success {
23
+ background-color: #14532D;
24
+ border-color: #22C55E;
25
+ color: #22C55E;
26
+ }
27
+
28
+ .upload-status.error {
29
+ background-color: #FEF2F2;
30
+ border-color: #EF4444;
31
+ color: #DC2626;
32
+ }
33
+
34
+ [data-theme="dark"] .upload-status.error {
35
+ background-color: #7F1D1D;
36
+ border-color: #EF4444;
37
+ color: #FCA5A5;
38
+ }
39
+
40
+ .status-content {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 8px;
44
+ }
45
+
46
+ .status-icon {
47
+ flex-shrink: 0;
48
+ }
49
+
50
+ .status-message {
51
+ flex: 1;
52
+ font-size: 14px;
53
+ font-weight: 500;
54
+ }
55
+
56
+ .status-close {
57
+ background: none;
58
+ border: none;
59
+ padding: 2px;
60
+ cursor: pointer;
61
+ border-radius: 4px;
62
+ color: inherit;
63
+ opacity: 0.7;
64
+ transition: opacity 0.2s ease;
65
+ }
66
+
67
+ .status-close:hover {
68
+ opacity: 1;
69
+ }
70
+
71
+ /* Upload Area */
72
+ .file-upload-area {
73
+ border: 2px dashed #D1D5DB;
74
+ border-radius: 12px;
75
+ padding: 24px;
76
+ text-align: center;
77
+ cursor: pointer;
78
+ transition: all 0.3s ease;
79
+ background-color: #F9FAFB;
80
+ position: relative;
81
+ overflow: hidden;
82
+ }
83
+
84
+ [data-theme="dark"] .file-upload-area {
85
+ border-color: #374151;
86
+ background-color: #1F2937;
87
+ }
88
+
89
+ .file-upload-area:hover:not(.uploading) {
90
+ border-color: #3B82F6;
91
+ background-color: #EFF6FF;
92
+ transform: translateY(-1px);
93
+ }
94
+
95
+ [data-theme="dark"] .file-upload-area:hover:not(.uploading) {
96
+ border-color: #60A5FA;
97
+ background-color: #1E3A8A;
98
+ }
99
+
100
+ .file-upload-area.drag-active {
101
+ border-color: #3B82F6;
102
+ background-color: #EFF6FF;
103
+ transform: scale(1.02);
104
+ }
105
+
106
+ [data-theme="dark"] .file-upload-area.drag-active {
107
+ border-color: #60A5FA;
108
+ background-color: #1E3A8A;
109
+ }
110
+
111
+ .file-upload-area.uploading {
112
+ cursor: not-allowed;
113
+ opacity: 0.7;
114
+ }
115
+
116
+ .upload-content {
117
+ display: flex;
118
+ flex-direction: column;
119
+ align-items: center;
120
+ gap: 8px;
121
+ }
122
+
123
+ .upload-icon {
124
+ color: #6B7280;
125
+ margin-bottom: 4px;
126
+ }
127
+
128
+ [data-theme="dark"] .upload-icon {
129
+ color: #9CA3AF;
130
+ }
131
+
132
+ .file-upload-area:hover:not(.uploading) .upload-icon {
133
+ color: #3B82F6;
134
+ }
135
+
136
+ [data-theme="dark"] .file-upload-area:hover:not(.uploading) .upload-icon {
137
+ color: #60A5FA;
138
+ }
139
+
140
+ .upload-text {
141
+ margin: 0;
142
+ font-size: 14px;
143
+ color: #374151;
144
+ }
145
+
146
+ [data-theme="dark"] .upload-text {
147
+ color: #D1D5DB;
148
+ }
149
+
150
+ .upload-primary {
151
+ font-weight: 600;
152
+ color: #3B82F6;
153
+ }
154
+
155
+ [data-theme="dark"] .upload-primary {
156
+ color: #60A5FA;
157
+ }
158
+
159
+ .upload-secondary {
160
+ margin: 0;
161
+ font-size: 12px;
162
+ color: #6B7280;
163
+ margin-top: 4px;
164
+ }
165
+
166
+ [data-theme="dark"] .upload-secondary {
167
+ color: #9CA3AF;
168
+ }
169
+
170
+ /* Upload Spinner */
171
+ .upload-spinner {
172
+ display: flex;
173
+ justify-content: center;
174
+ align-items: center;
175
+ }
176
+
177
+ .spinner {
178
+ width: 24px;
179
+ height: 24px;
180
+ border: 2px solid #E5E7EB;
181
+ border-top: 2px solid #3B82F6;
182
+ border-radius: 50%;
183
+ animation: spin 1s linear infinite;
184
+ }
185
+
186
+ [data-theme="dark"] .spinner {
187
+ border-color: #374151;
188
+ border-top-color: #60A5FA;
189
+ }
190
+
191
+ /* Supported File Types */
192
+ .supported-types {
193
+ display: flex;
194
+ justify-content: center;
195
+ gap: 8px;
196
+ margin-top: 8px;
197
+ flex-wrap: wrap;
198
+ }
199
+
200
+ .file-type-chip {
201
+ display: flex;
202
+ align-items: center;
203
+ gap: 4px;
204
+ padding: 4px 8px;
205
+ background-color: #F3F4F6;
206
+ border-radius: 16px;
207
+ font-size: 11px;
208
+ font-weight: 500;
209
+ color: #6B7280;
210
+ border: 1px solid #E5E7EB;
211
+ }
212
+
213
+ [data-theme="dark"] .file-type-chip {
214
+ background-color: #374151;
215
+ border-color: #4B5563;
216
+ color: #9CA3AF;
217
+ }
218
+
219
+ /* Enhanced Chat Input with Upload */
220
+ .enhanced-chat-input {
221
+ display: flex;
222
+ flex-direction: column;
223
+ gap: 8px;
224
+ width: 100%;
225
+ }
226
+
227
+ .input-with-upload {
228
+ display: flex;
229
+ align-items: flex-end;
230
+ gap: 8px;
231
+ width: 100%;
232
+ }
233
+
234
+ .upload-toggle-btn {
235
+ background: #F3F4F6;
236
+ border: 1px solid #D1D5DB;
237
+ border-radius: 8px;
238
+ padding: 8px;
239
+ cursor: pointer;
240
+ transition: all 0.2s ease;
241
+ display: flex;
242
+ align-items: center;
243
+ justify-content: center;
244
+ min-width: 40px;
245
+ height: 40px;
246
+ }
247
+
248
+ [data-theme="dark"] .upload-toggle-btn {
249
+ background-color: #374151;
250
+ border-color: #4B5563;
251
+ }
252
+
253
+ .upload-toggle-btn:hover {
254
+ background-color: #E5E7EB;
255
+ border-color: #3B82F6;
256
+ }
257
+
258
+ [data-theme="dark"] .upload-toggle-btn:hover {
259
+ background-color: #4B5563;
260
+ border-color: #60A5FA;
261
+ }
262
+
263
+ .upload-toggle-btn.active {
264
+ background-color: #3B82F6;
265
+ border-color: #3B82F6;
266
+ color: white;
267
+ }
268
+
269
+ .upload-toggle-btn svg {
270
+ color: #6B7280;
271
+ }
272
+
273
+ [data-theme="dark"] .upload-toggle-btn svg {
274
+ color: #9CA3AF;
275
+ }
276
+
277
+ .upload-toggle-btn:hover svg,
278
+ .upload-toggle-btn.active svg {
279
+ color: currentColor;
280
+ }
281
+
282
+ /* Animations */
283
+ @keyframes slideDown {
284
+ from {
285
+ opacity: 0;
286
+ transform: translateY(-10px);
287
+ }
288
+ to {
289
+ opacity: 1;
290
+ transform: translateY(0);
291
+ }
292
+ }
293
+
294
+ @keyframes spin {
295
+ 0% { transform: rotate(0deg); }
296
+ 100% { transform: rotate(360deg); }
297
+ }
298
+
299
+ /* Compact upload area for chat input */
300
+ .file-upload-container.compact .file-upload-area {
301
+ padding: 16px;
302
+ font-size: 13px;
303
+ }
304
+
305
+ .file-upload-container.compact .upload-content {
306
+ gap: 6px;
307
+ }
308
+
309
+ .file-upload-container.compact .upload-icon {
310
+ width: 20px;
311
+ height: 20px;
312
+ }
313
+
314
+ .file-upload-container.compact .supported-types {
315
+ margin-top: 6px;
316
+ }
317
+
318
+ /* Responsive adjustments */
319
+ @media (max-width: 640px) {
320
+ .file-upload-area {
321
+ padding: 16px;
322
+ }
323
+
324
+ .supported-types {
325
+ gap: 6px;
326
+ }
327
+
328
+ .file-type-chip {
329
+ font-size: 10px;
330
+ padding: 3px 6px;
331
+ }
332
+ }