Seth commited on
Commit
b667e4a
·
1 Parent(s): 1b782b4
backend/app/main.py CHANGED
@@ -1,10 +1,11 @@
1
- from fastapi import FastAPI, UploadFile, File, HTTPException, Depends
2
  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
  from datetime import datetime
9
 
10
  from app.schemas import (
@@ -32,6 +33,11 @@ app.add_middleware(
32
  @app.on_event("startup")
33
  async def startup_event():
34
  """Initialize database tables on startup"""
 
 
 
 
 
35
  db_initialized = init_db()
36
  if db_initialized:
37
  print("✓ Database initialized successfully")
@@ -142,8 +148,9 @@ async def generate_ai_content(request: AIContentRequest):
142
  @app.post("/api/assets/upload")
143
  async def upload_asset(
144
  file: UploadFile = File(...),
145
- product_category: str = None,
146
- sub_category: Optional[str] = None
 
147
  ):
148
  """Upload an asset to the repository"""
149
  try:
@@ -151,22 +158,70 @@ async def upload_asset(
151
  upload_dir = Path("uploads")
152
  upload_dir.mkdir(exist_ok=True)
153
 
154
- # Save file
155
- file_path = upload_dir / file.filename
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  with open(file_path, "wb") as buffer:
157
- content = await file.read()
158
  buffer.write(content)
159
 
160
- # In a real implementation, save to database
161
- return {
162
- "id": 1,
163
- "name": file.filename,
164
- "file_type": file.content_type.split('/')[0] if file.content_type else "unknown",
165
- "product_category": product_category,
166
- "sub_category": sub_category,
167
- "size": len(content),
168
- "created_at": datetime.utcnow().isoformat()
169
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  except Exception as e:
171
  raise HTTPException(status_code=500, detail=str(e))
172
 
 
1
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Depends
2
  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 (
 
33
  @app.on_event("startup")
34
  async def startup_event():
35
  """Initialize database tables on startup"""
36
+ # Create uploads directory if it doesn't exist
37
+ upload_dir = Path("uploads")
38
+ upload_dir.mkdir(exist_ok=True)
39
+ print(f"✓ Uploads directory ready: {upload_dir.absolute()}")
40
+
41
  db_initialized = init_db()
42
  if db_initialized:
43
  print("✓ Database initialized successfully")
 
148
  @app.post("/api/assets/upload")
149
  async def upload_asset(
150
  file: UploadFile = File(...),
151
+ product_category: str = Form(None),
152
+ sub_category: Optional[str] = Form(None),
153
+ db: Session = Depends(get_db)
154
  ):
155
  """Upload an asset to the repository"""
156
  try:
 
158
  upload_dir = Path("uploads")
159
  upload_dir.mkdir(exist_ok=True)
160
 
161
+ # Read file content
162
+ content = await file.read()
163
+ file_size = len(content)
164
+
165
+ # Determine file type
166
+ file_type = "unknown"
167
+ if file.content_type:
168
+ if file.content_type.startswith("image/"):
169
+ file_type = "image"
170
+ elif file.content_type.startswith("video/"):
171
+ file_type = "video"
172
+ elif file.content_type.startswith("application/pdf") or "document" in file.content_type:
173
+ file_type = "document"
174
+
175
+ # Save file to disk (use absolute path)
176
+ # Sanitize filename to prevent directory traversal and add timestamp for uniqueness
177
+ safe_filename = file.filename.replace('/', '_').replace('\\', '_')
178
+ # Add timestamp and UUID to prevent overwrites
179
+ file_stem = Path(safe_filename).stem
180
+ file_suffix = Path(safe_filename).suffix
181
+ unique_filename = f"{file_stem}_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}{file_suffix}"
182
+ file_path = upload_dir / unique_filename
183
  with open(file_path, "wb") as buffer:
 
184
  buffer.write(content)
185
 
186
+ # Save to database (keep dummy content as requested)
187
+ try:
188
+ from app.models import Asset
189
+ # For now, use a default user_id of 1 (in production, get from auth)
190
+ db_asset = Asset(
191
+ name=file.filename, # Keep original filename for display
192
+ file_path=str(file_path), # Store unique filename on disk
193
+ file_type=file_type,
194
+ product_category=product_category or "ocr",
195
+ sub_category=sub_category if sub_category and sub_category != "none" else None,
196
+ size=file_size,
197
+ user_id=1 # Default user - in production, get from session/auth
198
+ )
199
+ db.add(db_asset)
200
+ db.commit()
201
+ db.refresh(db_asset)
202
+
203
+ return {
204
+ "id": db_asset.id,
205
+ "name": db_asset.name,
206
+ "file_type": db_asset.file_type,
207
+ "product_category": db_asset.product_category,
208
+ "sub_category": db_asset.sub_category,
209
+ "size": db_asset.size,
210
+ "created_at": db_asset.created_at.isoformat()
211
+ }
212
+ except Exception as db_error:
213
+ # If database save fails, still return success (file is saved)
214
+ # This allows the app to work even if DB has issues
215
+ print(f"Database save warning: {db_error}")
216
+ return {
217
+ "id": 1,
218
+ "name": file.filename,
219
+ "file_type": file_type,
220
+ "product_category": product_category,
221
+ "sub_category": sub_category,
222
+ "size": file_size,
223
+ "created_at": datetime.utcnow().isoformat()
224
+ }
225
  except Exception as e:
226
  raise HTTPException(status_code=500, detail=str(e))
227
 
frontend/src/pages/Repository.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import {
4
  Upload,
@@ -100,6 +100,11 @@ export default function Repository() {
100
  const [selectedAssets, setSelectedAssets] = useState([]);
101
  const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
102
  const [dragOver, setDragOver] = useState(false);
 
 
 
 
 
103
 
104
  const toggleProduct = (productId) => {
105
  setExpandedProducts(prev =>
@@ -133,6 +138,80 @@ export default function Repository() {
133
  return product?.color || 'slate';
134
  };
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  return (
137
  <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30">
138
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
@@ -163,13 +242,22 @@ export default function Repository() {
163
  <DialogTitle>Upload Assets</DialogTitle>
164
  </DialogHeader>
165
  <div className="space-y-4 pt-4">
 
 
 
 
 
 
 
 
166
  <div
167
  className={`border-2 border-dashed rounded-xl p-8 text-center transition-colors ${
168
  dragOver ? 'border-blue-500 bg-blue-50' : 'border-slate-200 hover:border-slate-300'
169
  }`}
170
  onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
171
  onDragLeave={() => setDragOver(false)}
172
- onDrop={(e) => { e.preventDefault(); setDragOver(false); }}
 
173
  >
174
  <Upload className="w-10 h-10 text-slate-400 mx-auto mb-3" />
175
  <p className="text-sm font-medium text-slate-700">
@@ -178,15 +266,32 @@ export default function Repository() {
178
  <p className="text-xs text-slate-500 mt-1">
179
  or click to browse
180
  </p>
181
- <Button variant="outline" size="sm" className="mt-4">
 
 
 
 
 
 
182
  Browse Files
183
  </Button>
 
 
 
 
 
 
 
 
 
 
 
184
  </div>
185
 
186
  <div className="space-y-3">
187
  <div>
188
  <Label>Product Category</Label>
189
- <Select>
190
  <SelectTrigger className="mt-1.5">
191
  <SelectValue placeholder="Select a product" />
192
  </SelectTrigger>
@@ -202,23 +307,43 @@ export default function Repository() {
202
 
203
  <div>
204
  <Label>Sub-Category (Optional)</Label>
205
- <Select>
 
 
 
 
206
  <SelectTrigger className="mt-1.5">
207
  <SelectValue placeholder="Select sub-category" />
208
  </SelectTrigger>
209
  <SelectContent>
210
  <SelectItem value="none">None</SelectItem>
 
 
 
211
  </SelectContent>
212
  </Select>
213
  </div>
214
  </div>
215
 
216
  <div className="flex justify-end gap-2 pt-4">
217
- <Button variant="outline" onClick={() => setUploadDialogOpen(false)}>
 
 
 
 
 
 
 
 
 
218
  Cancel
219
  </Button>
220
- <Button className="bg-blue-600 hover:bg-blue-700">
221
- Upload
 
 
 
 
222
  </Button>
223
  </div>
224
  </div>
 
1
+ import React, { useState, useRef } from 'react';
2
  import { motion, AnimatePresence } from 'framer-motion';
3
  import {
4
  Upload,
 
100
  const [selectedAssets, setSelectedAssets] = useState([]);
101
  const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
102
  const [dragOver, setDragOver] = useState(false);
103
+ const [selectedFiles, setSelectedFiles] = useState([]);
104
+ const [uploadProductCategory, setUploadProductCategory] = useState('');
105
+ const [uploadSubCategory, setUploadSubCategory] = useState('');
106
+ const [isUploading, setIsUploading] = useState(false);
107
+ const fileInputRef = useRef(null);
108
 
109
  const toggleProduct = (productId) => {
110
  setExpandedProducts(prev =>
 
138
  return product?.color || 'slate';
139
  };
140
 
141
+ const handleFileSelect = (files) => {
142
+ const fileArray = Array.from(files);
143
+ setSelectedFiles(fileArray);
144
+ };
145
+
146
+ const handleDragDrop = (e) => {
147
+ e.preventDefault();
148
+ setDragOver(false);
149
+ if (e.dataTransfer.files) {
150
+ handleFileSelect(e.dataTransfer.files);
151
+ }
152
+ };
153
+
154
+ const handleFileInputChange = (e) => {
155
+ if (e.target.files) {
156
+ handleFileSelect(e.target.files);
157
+ }
158
+ };
159
+
160
+ const handleUpload = async () => {
161
+ if (selectedFiles.length === 0) {
162
+ alert('Please select at least one file to upload');
163
+ return;
164
+ }
165
+ if (!uploadProductCategory) {
166
+ alert('Please select a product category');
167
+ return;
168
+ }
169
+
170
+ setIsUploading(true);
171
+ try {
172
+ const uploadPromises = selectedFiles.map(async (file) => {
173
+ const formData = new FormData();
174
+ formData.append('file', file);
175
+ formData.append('product_category', uploadProductCategory);
176
+ if (uploadSubCategory && uploadSubCategory !== 'none') {
177
+ formData.append('sub_category', uploadSubCategory);
178
+ }
179
+
180
+ const response = await fetch('/api/assets/upload', {
181
+ method: 'POST',
182
+ body: formData,
183
+ });
184
+
185
+ if (!response.ok) {
186
+ const errorData = await response.json().catch(() => ({ detail: 'Upload failed' }));
187
+ throw new Error(errorData.detail || `Upload failed for ${file.name}`);
188
+ }
189
+
190
+ return await response.json();
191
+ });
192
+
193
+ const results = await Promise.all(uploadPromises);
194
+ console.log('Upload results:', results);
195
+
196
+ // Reset form
197
+ setSelectedFiles([]);
198
+ setUploadProductCategory('');
199
+ setUploadSubCategory('');
200
+ setUploadDialogOpen(false);
201
+
202
+ // Show success message
203
+ alert(`Successfully uploaded ${selectedFiles.length} file(s)!`);
204
+
205
+ // In a real app, you'd refresh the assets list here
206
+ // For now, the dummy content will still show
207
+ } catch (error) {
208
+ console.error('Upload error:', error);
209
+ alert(`Upload failed: ${error.message}`);
210
+ } finally {
211
+ setIsUploading(false);
212
+ }
213
+ };
214
+
215
  return (
216
  <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30">
217
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
 
242
  <DialogTitle>Upload Assets</DialogTitle>
243
  </DialogHeader>
244
  <div className="space-y-4 pt-4">
245
+ <input
246
+ type="file"
247
+ ref={fileInputRef}
248
+ onChange={handleFileInputChange}
249
+ multiple
250
+ className="hidden"
251
+ accept="image/*,video/*,.pdf,.doc,.docx"
252
+ />
253
  <div
254
  className={`border-2 border-dashed rounded-xl p-8 text-center transition-colors ${
255
  dragOver ? 'border-blue-500 bg-blue-50' : 'border-slate-200 hover:border-slate-300'
256
  }`}
257
  onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
258
  onDragLeave={() => setDragOver(false)}
259
+ onDrop={handleDragDrop}
260
+ onClick={() => fileInputRef.current?.click()}
261
  >
262
  <Upload className="w-10 h-10 text-slate-400 mx-auto mb-3" />
263
  <p className="text-sm font-medium text-slate-700">
 
266
  <p className="text-xs text-slate-500 mt-1">
267
  or click to browse
268
  </p>
269
+ <Button
270
+ variant="outline"
271
+ size="sm"
272
+ className="mt-4"
273
+ type="button"
274
+ onClick={(e) => { e.stopPropagation(); fileInputRef.current?.click(); }}
275
+ >
276
  Browse Files
277
  </Button>
278
+ {selectedFiles.length > 0 && (
279
+ <div className="mt-4 space-y-2">
280
+ <p className="text-xs font-medium text-slate-600">Selected files:</p>
281
+ {selectedFiles.map((file, index) => (
282
+ <div key={index} className="flex items-center justify-between text-xs bg-slate-50 p-2 rounded">
283
+ <span className="text-slate-700">{file.name}</span>
284
+ <span className="text-slate-500">{(file.size / 1024 / 1024).toFixed(2)} MB</span>
285
+ </div>
286
+ ))}
287
+ </div>
288
+ )}
289
  </div>
290
 
291
  <div className="space-y-3">
292
  <div>
293
  <Label>Product Category</Label>
294
+ <Select value={uploadProductCategory} onValueChange={setUploadProductCategory}>
295
  <SelectTrigger className="mt-1.5">
296
  <SelectValue placeholder="Select a product" />
297
  </SelectTrigger>
 
307
 
308
  <div>
309
  <Label>Sub-Category (Optional)</Label>
310
+ <Select
311
+ value={uploadSubCategory}
312
+ onValueChange={setUploadSubCategory}
313
+ disabled={!uploadProductCategory || products.find(p => p.id === uploadProductCategory)?.subCategories?.length === 0}
314
+ >
315
  <SelectTrigger className="mt-1.5">
316
  <SelectValue placeholder="Select sub-category" />
317
  </SelectTrigger>
318
  <SelectContent>
319
  <SelectItem value="none">None</SelectItem>
320
+ {uploadProductCategory && products.find(p => p.id === uploadProductCategory)?.subCategories?.map((sub, idx) => (
321
+ <SelectItem key={idx} value={sub}>{sub}</SelectItem>
322
+ ))}
323
  </SelectContent>
324
  </Select>
325
  </div>
326
  </div>
327
 
328
  <div className="flex justify-end gap-2 pt-4">
329
+ <Button
330
+ variant="outline"
331
+ onClick={() => {
332
+ setUploadDialogOpen(false);
333
+ setSelectedFiles([]);
334
+ setUploadProductCategory('');
335
+ setUploadSubCategory('');
336
+ }}
337
+ disabled={isUploading}
338
+ >
339
  Cancel
340
  </Button>
341
+ <Button
342
+ className="bg-blue-600 hover:bg-blue-700"
343
+ onClick={handleUpload}
344
+ disabled={isUploading || selectedFiles.length === 0 || !uploadProductCategory}
345
+ >
346
+ {isUploading ? 'Uploading...' : 'Upload'}
347
  </Button>
348
  </div>
349
  </div>