Seth commited on
Commit
70e3c38
·
1 Parent(s): 6d1e595
backend/app/database.py CHANGED
@@ -293,44 +293,38 @@ def init_db():
293
  conn.commit()
294
 
295
  # Add new columns to assets table if they don't exist (migration)
 
296
  try:
297
  cursor = conn.cursor()
298
- # Check and add extracted_content column
 
299
  cursor.execute("""
300
- DO $$
301
- BEGIN
302
- IF NOT EXISTS (
303
- SELECT 1 FROM information_schema.columns
304
- WHERE table_name='assets' AND column_name='extracted_content'
305
- ) THEN
306
- ALTER TABLE assets ADD COLUMN extracted_content JSONB;
307
- END IF;
308
- END $$;
309
  """)
310
- # Check and add analysis_status column
 
 
 
311
  cursor.execute("""
312
- DO $$
313
- BEGIN
314
- IF NOT EXISTS (
315
- SELECT 1 FROM information_schema.columns
316
- WHERE table_name='assets' AND column_name='analysis_status'
317
- ) THEN
318
- ALTER TABLE assets ADD COLUMN analysis_status VARCHAR DEFAULT 'pending';
319
- END IF;
320
- END $$;
321
  """)
322
- # Check and add analyzed_at column
 
 
 
323
  cursor.execute("""
324
- DO $$
325
- BEGIN
326
- IF NOT EXISTS (
327
- SELECT 1 FROM information_schema.columns
328
- WHERE table_name='assets' AND column_name='analyzed_at'
329
- ) THEN
330
- ALTER TABLE assets ADD COLUMN analyzed_at TIMESTAMP;
331
- END IF;
332
- END $$;
333
  """)
 
 
 
 
334
  conn.commit()
335
  cursor.close()
336
  print("✓ Database migration completed (added new asset columns)")
 
293
  conn.commit()
294
 
295
  # Add new columns to assets table if they don't exist (migration)
296
+ # CockroachDB doesn't support ALTER TABLE in DO blocks, so we check first
297
  try:
298
  cursor = conn.cursor()
299
+
300
+ # Check if columns exist and add them if they don't
301
  cursor.execute("""
302
+ SELECT column_name
303
+ FROM information_schema.columns
304
+ WHERE table_name='assets' AND column_name='extracted_content'
 
 
 
 
 
 
305
  """)
306
+ if not cursor.fetchone():
307
+ cursor.execute("ALTER TABLE assets ADD COLUMN extracted_content JSONB")
308
+ print("✓ Added extracted_content column")
309
+
310
  cursor.execute("""
311
+ SELECT column_name
312
+ FROM information_schema.columns
313
+ WHERE table_name='assets' AND column_name='analysis_status'
 
 
 
 
 
 
314
  """)
315
+ if not cursor.fetchone():
316
+ cursor.execute("ALTER TABLE assets ADD COLUMN analysis_status VARCHAR DEFAULT 'pending'")
317
+ print("✓ Added analysis_status column")
318
+
319
  cursor.execute("""
320
+ SELECT column_name
321
+ FROM information_schema.columns
322
+ WHERE table_name='assets' AND column_name='analyzed_at'
 
 
 
 
 
 
323
  """)
324
+ if not cursor.fetchone():
325
+ cursor.execute("ALTER TABLE assets ADD COLUMN analyzed_at TIMESTAMP")
326
+ print("✓ Added analyzed_at column")
327
+
328
  conn.commit()
329
  cursor.close()
330
  print("✓ Database migration completed (added new asset columns)")
backend/app/main.py CHANGED
@@ -3,10 +3,11 @@ from fastapi.responses import FileResponse, JSONResponse
3
  from fastapi.staticfiles import StaticFiles
4
  from fastapi.middleware.cors import CORSMiddleware
5
  from pathlib import Path
6
- from typing import List, Optional
7
  import os
8
  import uuid
9
  from datetime import datetime
 
10
 
11
  from app.schemas import (
12
  IntegrationResponse, AssetResponse, PostResponse, CampaignResponse,
@@ -59,6 +60,9 @@ ai_service = AIService()
59
  asset_analyzer = AssetAnalyzer()
60
  agentic_planner = AgenticPlanner()
61
 
 
 
 
62
  # ---- API Endpoints ----
63
 
64
  @app.get("/api/health")
@@ -174,6 +178,132 @@ async def generate_ai_content(request: AIContentRequest, db: Session = Depends(g
174
 
175
  # ---- Asset Management ----
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  @app.post("/api/assets/upload")
178
  async def upload_asset(
179
  file: UploadFile = File(...),
@@ -301,97 +431,11 @@ async def upload_asset(
301
  else:
302
  raise commit_error
303
 
304
- # Analyze asset using OCR API (agentic step)
305
  asset_id = db_asset.id
306
  if file_type in ["document", "image"]:
307
- # Update status to processing
308
- try:
309
- conn = get_direct_psycopg2_connection()
310
- if conn:
311
- cursor = conn.cursor()
312
- cursor.execute("""
313
- UPDATE assets
314
- SET analysis_status = 'processing'
315
- WHERE id = %s
316
- """, (asset_id,))
317
- conn.commit()
318
- cursor.close()
319
- conn.close()
320
- except Exception as update_error:
321
- print(f"Could not update analysis status: {update_error}")
322
-
323
- # Analyze asset asynchronously (don't block response)
324
- try:
325
- analysis_result = await asset_analyzer.analyze_document(str(file_path))
326
- if analysis_result.get("success") and analysis_result.get("extracted_content"):
327
- # Update asset with extracted content
328
- try:
329
- conn = get_direct_psycopg2_connection()
330
- if conn:
331
- cursor = conn.cursor()
332
- import json
333
- extracted_json = json.dumps(analysis_result["extracted_content"])
334
- cursor.execute("""
335
- UPDATE assets
336
- SET extracted_content = %s::jsonb,
337
- analysis_status = 'completed',
338
- analyzed_at = NOW()
339
- WHERE id = %s
340
- """, (extracted_json, asset_id))
341
- conn.commit()
342
- cursor.close()
343
- conn.close()
344
- print(f"✓ Asset {asset_id} analyzed successfully")
345
- except Exception as update_error:
346
- print(f"Could not save extracted content: {update_error}")
347
- # Try to mark as failed
348
- try:
349
- conn = get_direct_psycopg2_connection()
350
- if conn:
351
- cursor = conn.cursor()
352
- cursor.execute("""
353
- UPDATE assets
354
- SET analysis_status = 'failed'
355
- WHERE id = %s
356
- """, (asset_id,))
357
- conn.commit()
358
- cursor.close()
359
- conn.close()
360
- except:
361
- pass
362
- else:
363
- # Mark as failed if analysis didn't succeed
364
- try:
365
- conn = get_direct_psycopg2_connection()
366
- if conn:
367
- cursor = conn.cursor()
368
- cursor.execute("""
369
- UPDATE assets
370
- SET analysis_status = 'failed'
371
- WHERE id = %s
372
- """, (asset_id,))
373
- conn.commit()
374
- cursor.close()
375
- conn.close()
376
- except:
377
- pass
378
- except Exception as analysis_error:
379
- print(f"Asset analysis error: {analysis_error}")
380
- # Mark as failed
381
- try:
382
- conn = get_direct_psycopg2_connection()
383
- if conn:
384
- cursor = conn.cursor()
385
- cursor.execute("""
386
- UPDATE assets
387
- SET analysis_status = 'failed'
388
- WHERE id = %s
389
- """, (asset_id,))
390
- conn.commit()
391
- cursor.close()
392
- conn.close()
393
- except:
394
- pass
395
 
396
  return {
397
  "id": db_asset.id,
@@ -739,7 +783,7 @@ if FRONTEND_DIST.exists():
739
  if INDEX_FILE.exists():
740
  return FileResponse(str(INDEX_FILE))
741
  return {"detail": "Frontend not found"}
742
-
743
  # SPA fallback: any non-/api route should return React index.html
744
  # This must be last to catch all routes not handled above
745
  @app.get("/{full_path:path}")
 
3
  from fastapi.staticfiles import StaticFiles
4
  from fastapi.middleware.cors import CORSMiddleware
5
  from pathlib import Path
6
+ from typing import List, Optional, Dict
7
  import os
8
  import uuid
9
  from datetime import datetime
10
+ import asyncio
11
 
12
  from app.schemas import (
13
  IntegrationResponse, AssetResponse, PostResponse, CampaignResponse,
 
60
  asset_analyzer = AssetAnalyzer()
61
  agentic_planner = AgenticPlanner()
62
 
63
+ # Upload status tracking (in-memory, could be moved to Redis in production)
64
+ upload_status: Dict[str, Dict] = {}
65
+
66
  # ---- API Endpoints ----
67
 
68
  @app.get("/api/health")
 
178
 
179
  # ---- Asset Management ----
180
 
181
+ @app.get("/api/assets/{asset_id}/status")
182
+ async def get_asset_status(asset_id: int, db: Session = Depends(get_db)):
183
+ """Get the analysis status of an asset"""
184
+ try:
185
+ from app.models import Asset
186
+ conn = get_direct_psycopg2_connection()
187
+ if conn:
188
+ try:
189
+ cursor = conn.cursor()
190
+ cursor.execute("""
191
+ SELECT id, name, analysis_status, analyzed_at
192
+ FROM assets
193
+ WHERE id = %s
194
+ """, (asset_id,))
195
+ row = cursor.fetchone()
196
+ cursor.close()
197
+ conn.close()
198
+ if row:
199
+ return {
200
+ "asset_id": row[0],
201
+ "name": row[1],
202
+ "status": row[2] or "pending",
203
+ "analyzed_at": row[3].isoformat() if row[3] else None
204
+ }
205
+ except Exception as e:
206
+ if conn:
207
+ conn.close()
208
+ raise HTTPException(status_code=500, detail=str(e))
209
+ raise HTTPException(status_code=404, detail="Asset not found")
210
+ except Exception as e:
211
+ raise HTTPException(status_code=500, detail=str(e))
212
+
213
+ async def analyze_asset_background(asset_id: int, file_path: str, file_type: str):
214
+ """Background task to analyze asset"""
215
+ try:
216
+ # Update status to processing
217
+ conn = get_direct_psycopg2_connection()
218
+ if conn:
219
+ try:
220
+ cursor = conn.cursor()
221
+ cursor.execute("""
222
+ UPDATE assets
223
+ SET analysis_status = 'processing'
224
+ WHERE id = %s
225
+ """, (asset_id,))
226
+ conn.commit()
227
+ cursor.close()
228
+ conn.close()
229
+ except Exception as update_error:
230
+ print(f"Could not update analysis status: {update_error}")
231
+ if conn:
232
+ conn.close()
233
+
234
+ # Analyze asset
235
+ analysis_result = await asset_analyzer.analyze_document(str(file_path))
236
+ if analysis_result.get("success") and analysis_result.get("extracted_content"):
237
+ # Update asset with extracted content
238
+ conn = get_direct_psycopg2_connection()
239
+ if conn:
240
+ try:
241
+ cursor = conn.cursor()
242
+ import json
243
+ extracted_json = json.dumps(analysis_result["extracted_content"])
244
+ cursor.execute("""
245
+ UPDATE assets
246
+ SET extracted_content = %s::jsonb,
247
+ analysis_status = 'completed',
248
+ analyzed_at = NOW()
249
+ WHERE id = %s
250
+ """, (extracted_json, asset_id))
251
+ conn.commit()
252
+ cursor.close()
253
+ conn.close()
254
+ print(f"✓ Asset {asset_id} analyzed successfully")
255
+ except Exception as update_error:
256
+ print(f"Could not save extracted content: {update_error}")
257
+ # Try to mark as failed
258
+ try:
259
+ cursor = conn.cursor()
260
+ cursor.execute("""
261
+ UPDATE assets
262
+ SET analysis_status = 'failed'
263
+ WHERE id = %s
264
+ """, (asset_id,))
265
+ conn.commit()
266
+ cursor.close()
267
+ except:
268
+ pass
269
+ if conn:
270
+ conn.close()
271
+ else:
272
+ # Mark as failed if analysis didn't succeed
273
+ conn = get_direct_psycopg2_connection()
274
+ if conn:
275
+ try:
276
+ cursor = conn.cursor()
277
+ cursor.execute("""
278
+ UPDATE assets
279
+ SET analysis_status = 'failed'
280
+ WHERE id = %s
281
+ """, (asset_id,))
282
+ conn.commit()
283
+ cursor.close()
284
+ conn.close()
285
+ except:
286
+ if conn:
287
+ conn.close()
288
+ except Exception as analysis_error:
289
+ print(f"Asset analysis error: {analysis_error}")
290
+ # Mark as failed
291
+ conn = get_direct_psycopg2_connection()
292
+ if conn:
293
+ try:
294
+ cursor = conn.cursor()
295
+ cursor.execute("""
296
+ UPDATE assets
297
+ SET analysis_status = 'failed'
298
+ WHERE id = %s
299
+ """, (asset_id,))
300
+ conn.commit()
301
+ cursor.close()
302
+ conn.close()
303
+ except:
304
+ if conn:
305
+ conn.close()
306
+
307
  @app.post("/api/assets/upload")
308
  async def upload_asset(
309
  file: UploadFile = File(...),
 
431
  else:
432
  raise commit_error
433
 
434
+ # Start background analysis task
435
  asset_id = db_asset.id
436
  if file_type in ["document", "image"]:
437
+ # Start background task (don't await - return immediately)
438
+ asyncio.create_task(analyze_asset_background(asset_id, str(file_path), file_type))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
 
440
  return {
441
  "id": db_asset.id,
 
783
  if INDEX_FILE.exists():
784
  return FileResponse(str(INDEX_FILE))
785
  return {"detail": "Frontend not found"}
786
+
787
  # SPA fallback: any non-/api route should return React index.html
788
  # This must be last to catch all routes not handled above
789
  @app.get("/{full_path:path}")
frontend/src/pages/Repository.jsx CHANGED
@@ -26,6 +26,7 @@ import { Button } from '@/components/ui/button';
26
  import { Input } from '@/components/ui/input';
27
  import { Badge } from '@/components/ui/badge';
28
  import { Label } from '@/components/ui/label';
 
29
  import {
30
  Select,
31
  SelectContent,
@@ -108,6 +109,7 @@ export default function Repository() {
108
  const [isLoadingAssets, setIsLoadingAssets] = useState(false);
109
  const [previewAsset, setPreviewAsset] = useState(null);
110
  const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
 
111
  const fileInputRef = useRef(null);
112
 
113
  const toggleProduct = (productId) => {
@@ -212,6 +214,46 @@ export default function Repository() {
212
  }
213
  };
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  const handleUpload = async () => {
216
  if (selectedFiles.length === 0) {
217
  alert('Please select at least one file to upload');
@@ -223,42 +265,103 @@ export default function Repository() {
223
  }
224
 
225
  setIsUploading(true);
 
 
226
  try {
227
- const uploadPromises = selectedFiles.map(async (file) => {
228
- const formData = new FormData();
229
- formData.append('file', file);
230
- formData.append('product_category', uploadProductCategory);
231
- if (uploadSubCategory && uploadSubCategory !== 'none') {
232
- formData.append('sub_category', uploadSubCategory);
233
- }
 
 
234
 
235
- const response = await fetch('/api/assets/upload', {
236
- method: 'POST',
237
- body: formData,
238
- });
 
 
 
 
 
 
 
239
 
240
- if (!response.ok) {
241
- const errorData = await response.json().catch(() => ({ detail: 'Upload failed' }));
242
- throw new Error(errorData.detail || `Upload failed for ${file.name}`);
243
- }
 
 
244
 
245
- return await response.json();
246
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
- const results = await Promise.all(uploadPromises);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  console.log('Upload results:', results);
250
 
251
- // Reset form
252
- setSelectedFiles([]);
253
- setUploadProductCategory('');
254
- setUploadSubCategory('');
255
- setUploadDialogOpen(false);
256
 
257
- // Show success message
258
- alert(`Successfully uploaded ${selectedFiles.length} file(s)!`);
 
 
 
 
 
 
 
259
 
260
- // Refresh assets list to show newly uploaded files
261
- await fetchAssets();
262
  } catch (error) {
263
  console.error('Upload error:', error);
264
  alert(`Upload failed: ${error.message}`);
@@ -331,14 +434,61 @@ export default function Repository() {
331
  Browse Files
332
  </Button>
333
  {selectedFiles.length > 0 && (
334
- <div className="mt-4 space-y-2">
335
  <p className="text-xs font-medium text-slate-600">Selected files:</p>
336
- {selectedFiles.map((file, index) => (
337
- <div key={index} className="flex items-center justify-between text-xs bg-slate-50 p-2 rounded">
338
- <span className="text-slate-700">{file.name}</span>
339
- <span className="text-slate-500">{(file.size / 1024 / 1024).toFixed(2)} MB</span>
340
- </div>
341
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  </div>
343
  )}
344
  </div>
 
26
  import { Input } from '@/components/ui/input';
27
  import { Badge } from '@/components/ui/badge';
28
  import { Label } from '@/components/ui/label';
29
+ import { Progress } from '@/components/ui/progress';
30
  import {
31
  Select,
32
  SelectContent,
 
109
  const [isLoadingAssets, setIsLoadingAssets] = useState(false);
110
  const [previewAsset, setPreviewAsset] = useState(null);
111
  const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
112
+ const [uploadProgress, setUploadProgress] = useState({});
113
  const fileInputRef = useRef(null);
114
 
115
  const toggleProduct = (productId) => {
 
214
  }
215
  };
216
 
217
+ const pollAssetStatus = async (assetId, fileName) => {
218
+ const maxAttempts = 60; // Poll for up to 60 seconds
219
+ let attempts = 0;
220
+
221
+ while (attempts < maxAttempts) {
222
+ try {
223
+ const response = await fetch(`/api/assets/${assetId}/status`);
224
+ if (response.ok) {
225
+ const status = await response.json();
226
+ setUploadProgress(prev => ({
227
+ ...prev,
228
+ [fileName]: {
229
+ status: status.status,
230
+ message: getStatusMessage(status.status)
231
+ }
232
+ }));
233
+
234
+ if (status.status === 'completed' || status.status === 'failed') {
235
+ break;
236
+ }
237
+ }
238
+ } catch (error) {
239
+ console.error('Error polling status:', error);
240
+ }
241
+
242
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Poll every second
243
+ attempts++;
244
+ }
245
+ };
246
+
247
+ const getStatusMessage = (status) => {
248
+ switch(status) {
249
+ case 'pending': return 'Uploading file...';
250
+ case 'processing': return 'Extracting content with OCR...';
251
+ case 'completed': return 'Content extracted and indexed by AI agent ✓';
252
+ case 'failed': return 'Analysis failed';
253
+ default: return 'Processing...';
254
+ }
255
+ };
256
+
257
  const handleUpload = async () => {
258
  if (selectedFiles.length === 0) {
259
  alert('Please select at least one file to upload');
 
265
  }
266
 
267
  setIsUploading(true);
268
+ setUploadProgress({});
269
+
270
  try {
271
+ // Initialize progress for all files
272
+ const initialProgress = {};
273
+ selectedFiles.forEach(file => {
274
+ initialProgress[file.name] = {
275
+ status: 'pending',
276
+ message: 'Uploading file...'
277
+ };
278
+ });
279
+ setUploadProgress(initialProgress);
280
 
281
+ // Upload files sequentially to show progress
282
+ const results = [];
283
+ for (const file of selectedFiles) {
284
+ try {
285
+ setUploadProgress(prev => ({
286
+ ...prev,
287
+ [file.name]: {
288
+ status: 'pending',
289
+ message: 'Uploading file...'
290
+ }
291
+ }));
292
 
293
+ const formData = new FormData();
294
+ formData.append('file', file);
295
+ formData.append('product_category', uploadProductCategory);
296
+ if (uploadSubCategory && uploadSubCategory !== 'none') {
297
+ formData.append('sub_category', uploadSubCategory);
298
+ }
299
 
300
+ const response = await fetch('/api/assets/upload', {
301
+ method: 'POST',
302
+ body: formData,
303
+ });
304
+
305
+ if (!response.ok) {
306
+ const errorData = await response.json().catch(() => ({ detail: 'Upload failed' }));
307
+ throw new Error(errorData.detail || `Upload failed for ${file.name}`);
308
+ }
309
+
310
+ const result = await response.json();
311
+ results.push(result);
312
+
313
+ // Update progress - file uploaded, now analyzing
314
+ setUploadProgress(prev => ({
315
+ ...prev,
316
+ [file.name]: {
317
+ status: result.analysis_status || 'processing',
318
+ message: result.analysis_status === 'processing'
319
+ ? 'Extracting content with OCR...'
320
+ : 'Upload complete, analyzing...'
321
+ }
322
+ }));
323
 
324
+ // Start polling for analysis status if it's a document/image
325
+ if (result.file_type === 'document' || result.file_type === 'image') {
326
+ pollAssetStatus(result.id, file.name);
327
+ } else {
328
+ setUploadProgress(prev => ({
329
+ ...prev,
330
+ [file.name]: {
331
+ status: 'completed',
332
+ message: 'Upload complete ✓'
333
+ }
334
+ }));
335
+ }
336
+ } catch (error) {
337
+ console.error(`Upload error for ${file.name}:`, error);
338
+ setUploadProgress(prev => ({
339
+ ...prev,
340
+ [file.name]: {
341
+ status: 'failed',
342
+ message: `Upload failed: ${error.message}`
343
+ }
344
+ }));
345
+ }
346
+ }
347
+
348
  console.log('Upload results:', results);
349
 
350
+ // Wait a bit for analysis to complete, then refresh
351
+ setTimeout(async () => {
352
+ await fetchAssets();
353
+ }, 2000);
 
354
 
355
+ // Don't close dialog immediately - let user see the progress
356
+ // Reset form after a delay
357
+ setTimeout(() => {
358
+ setSelectedFiles([]);
359
+ setUploadProductCategory('');
360
+ setUploadSubCategory('');
361
+ setUploadProgress({});
362
+ setUploadDialogOpen(false);
363
+ }, 3000);
364
 
 
 
365
  } catch (error) {
366
  console.error('Upload error:', error);
367
  alert(`Upload failed: ${error.message}`);
 
434
  Browse Files
435
  </Button>
436
  {selectedFiles.length > 0 && (
437
+ <div className="mt-4 space-y-3">
438
  <p className="text-xs font-medium text-slate-600">Selected files:</p>
439
+ {selectedFiles.map((file, index) => {
440
+ const progress = uploadProgress[file.name];
441
+ const getProgressValue = () => {
442
+ if (!progress) return 0;
443
+ switch(progress.status) {
444
+ case 'pending': return 25;
445
+ case 'processing': return 60;
446
+ case 'completed': return 100;
447
+ case 'failed': return 0;
448
+ default: return 0;
449
+ }
450
+ };
451
+ const getProgressColor = () => {
452
+ if (!progress) return 'bg-blue-500';
453
+ switch(progress.status) {
454
+ case 'pending': return 'bg-blue-500';
455
+ case 'processing': return 'bg-yellow-500';
456
+ case 'completed': return 'bg-green-500';
457
+ case 'failed': return 'bg-red-500';
458
+ default: return 'bg-blue-500';
459
+ }
460
+ };
461
+ return (
462
+ <div key={index} className="space-y-2">
463
+ <div className="flex items-center justify-between text-xs bg-slate-50 p-2 rounded">
464
+ <span className="text-slate-700 truncate flex-1 mr-2">{file.name}</span>
465
+ <span className="text-slate-500 whitespace-nowrap">{(file.size / 1024 / 1024).toFixed(2)} MB</span>
466
+ </div>
467
+ {isUploading && (
468
+ <div className="space-y-1">
469
+ <div className="flex items-center justify-between text-xs">
470
+ <span className={`font-medium ${
471
+ progress?.status === 'completed' ? 'text-green-600' :
472
+ progress?.status === 'failed' ? 'text-red-600' :
473
+ progress?.status === 'processing' ? 'text-yellow-600' :
474
+ 'text-blue-600'
475
+ }`}>
476
+ {progress?.message || 'Uploading...'}
477
+ </span>
478
+ {progress?.status === 'completed' && <Check className="w-3 h-3 text-green-600" />}
479
+ {progress?.status === 'failed' && <X className="w-3 h-3 text-red-600" />}
480
+ </div>
481
+ <div className="relative h-1.5 w-full overflow-hidden rounded-full bg-slate-200">
482
+ <div
483
+ className={`h-full transition-all ${getProgressColor()}`}
484
+ style={{ width: `${getProgressValue()}%` }}
485
+ />
486
+ </div>
487
+ </div>
488
+ )}
489
+ </div>
490
+ );
491
+ })}
492
  </div>
493
  )}
494
  </div>