diff --git "a/frontend/src/pages/Repository.jsx" "b/frontend/src/pages/Repository.jsx"
--- "a/frontend/src/pages/Repository.jsx"
+++ "b/frontend/src/pages/Repository.jsx"
@@ -1,1169 +1,1257 @@
-import React, { useState, useRef, useEffect } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
-import {
- Upload,
- FolderOpen,
- Image,
- FileText,
- Video,
- Search,
- Filter,
- Grid3X3,
- List,
- MoreVertical,
- Download,
- Trash2,
- Eye,
- ChevronDown,
- ChevronRight,
- Plus,
- X,
- Check,
- File,
- Copy
-} from 'lucide-react';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Badge } from '@/components/ui/badge';
-import { Label } from '@/components/ui/label';
-import { Progress } from '@/components/ui/progress';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from '@/components/ui/dropdown-menu';
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@/components/ui/dialog';
-import { Checkbox } from '@/components/ui/checkbox';
-
-const products = [
- {
- id: 'ocr',
- name: 'Intelligent Document Parsing (OCR)',
- shortName: 'OCR',
- color: 'blue',
- subCategories: []
- },
- {
- id: 'p2p',
- name: 'Purchase To Pay (P2P)',
- shortName: 'P2P',
- color: 'emerald',
- subCategories: [
- 'Budget Approval Workflow',
- 'Purchase Request Workflow',
- 'Accounts Payable Workflow'
- ]
- },
- {
- id: 'o2c',
- name: 'Order to Cash (O2C)',
- shortName: 'O2C',
- color: 'violet',
- subCategories: [
- 'Quotation Workflow',
- 'Sales Order Workflow',
- 'PickSlip Delivery Workflow',
- 'Accounts Receivable Workflow'
- ]
- }
-];
-
-const mockAssets = [
- { id: '1', name: 'OCR_Demo_Screenshot.png', type: 'image', product: 'ocr', subCategory: null, size: '2.4 MB', date: '2024-12-20' },
- { id: '2', name: 'P2P_Workflow_Diagram.pdf', type: 'document', product: 'p2p', subCategory: 'Budget Approval Workflow', size: '1.8 MB', date: '2024-12-19' },
- { id: '3', name: 'Invoice_Processing_Video.mp4', type: 'video', product: 'ocr', subCategory: null, size: '45.2 MB', date: '2024-12-18' },
- { id: '4', name: 'Sales_Order_Infographic.png', type: 'image', product: 'o2c', subCategory: 'Sales Order Workflow', size: '3.1 MB', date: '2024-12-17' },
- { id: '5', name: 'AP_Automation_Brochure.pdf', type: 'document', product: 'p2p', subCategory: 'Accounts Payable Workflow', size: '5.6 MB', date: '2024-12-16' },
- { id: '6', name: 'O2C_Product_Banner.png', type: 'image', product: 'o2c', subCategory: 'Quotation Workflow', size: '1.2 MB', date: '2024-12-15' },
- { id: '7', name: 'Document_Parsing_Demo.png', type: 'image', product: 'ocr', subCategory: null, size: '890 KB', date: '2024-12-14' },
- { id: '8', name: 'PR_Workflow_Guide.pdf', type: 'document', product: 'p2p', subCategory: 'Purchase Request Workflow', size: '2.3 MB', date: '2024-12-13' },
-];
-
-export default function Repository() {
- const [viewMode, setViewMode] = useState('grid');
- const [searchQuery, setSearchQuery] = useState('');
- const [selectedProduct, setSelectedProduct] = useState('all');
- const [expandedProducts, setExpandedProducts] = useState(['ocr', 'p2p', 'o2c']);
- const [selectedAssets, setSelectedAssets] = useState([]);
- const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
- const [dragOver, setDragOver] = useState(false);
- const [selectedFiles, setSelectedFiles] = useState([]);
- const [uploadProductCategory, setUploadProductCategory] = useState('');
- const [uploadSubCategory, setUploadSubCategory] = useState('');
- const [isUploading, setIsUploading] = useState(false);
- const [assets, setAssets] = useState(mockAssets);
- const [isLoadingAssets, setIsLoadingAssets] = useState(false);
- const [previewAsset, setPreviewAsset] = useState(null);
- const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
- const [uploadProgress, setUploadProgress] = useState({});
- const [pdfPages, setPdfPages] = useState(null);
- const [isLoadingPdf, setIsLoadingPdf] = useState(false);
- const [isDeleting, setIsDeleting] = useState(false);
- const fileInputRef = useRef(null);
-
- const toggleProduct = (productId) => {
- setExpandedProducts(prev =>
- prev.includes(productId)
- ? prev.filter(id => id !== productId)
- : [...prev, productId]
- );
- };
-
- const filteredAssets = assets.filter(asset => {
- const matchesSearch = asset.name.toLowerCase().includes(searchQuery.toLowerCase());
- const matchesProduct = selectedProduct === 'all' || asset.product === selectedProduct;
- return matchesSearch && matchesProduct;
- });
-
- const getAssetsByProduct = (productId) => {
- return assets.filter(asset => asset.product === productId);
- };
-
- const getTypeIcon = (type) => {
- switch(type) {
- case 'image': return ;
- case 'video': return ;
- case 'document': return ;
- default: return ;
- }
- };
-
- const getProductColor = (productId) => {
- const product = products.find(p => p.id === productId);
- return product?.color || 'slate';
- };
-
- // Fetch assets from API
- const fetchAssets = async () => {
- setIsLoadingAssets(true);
- try {
- const response = await fetch('/api/assets');
- if (response.ok) {
- const data = await response.json();
- // Convert API response to match mockAssets format
- // IMPORTANT: Keep IDs as strings to preserve precision for large CockroachDB IDs
- const formattedAssets = data.map(asset => ({
- id: String(asset.id), // Explicitly convert to string to preserve precision
- name: asset.name,
- type: asset.file_type,
- product: asset.product_category,
- subCategory: asset.sub_category,
- size: formatFileSize(asset.size),
- date: asset.created_at ? new Date(asset.created_at).toISOString().split('T')[0] : new Date().toISOString().split('T')[0]
- }));
- setAssets(formattedAssets);
- } else {
- console.error('Failed to fetch assets');
- // Keep mockAssets on error
- }
- } catch (error) {
- console.error('Error fetching assets:', error);
- // Keep mockAssets on error
- } finally {
- setIsLoadingAssets(false);
- }
- };
-
- // Format file size
- const formatFileSize = (bytes) => {
- if (!bytes) return '0 B';
- const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
- };
-
- // Handle delete asset
- const handleDeleteAsset = async (assetId) => {
- setIsDeleting(true);
- try {
- // Ensure assetId is a string to preserve precision
- const assetIdStr = String(assetId);
- const response = await fetch(`/api/assets/${assetIdStr}`, {
- method: 'DELETE',
- });
-
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({ detail: 'Delete failed' }));
- throw new Error(errorData.detail || 'Delete failed');
- }
-
- const result = await response.json();
- console.log('Delete result:', result);
-
- // Refresh assets list
- await fetchAssets();
-
- // Close preview if deleted asset was being previewed
- if (previewAsset && previewAsset.id === assetId) {
- setPreviewDialogOpen(false);
- setPreviewAsset(null);
- setPdfPages(null);
- }
-
- alert('Asset deleted successfully');
- } catch (error) {
- console.error('Delete error:', error);
- alert(`Delete failed: ${error.message}`);
- } finally {
- setIsDeleting(false);
- }
- };
-
- // Load PDF pages when previewing a PDF
- const loadPdfPages = async (assetId) => {
- setIsLoadingPdf(true);
- setPdfPages(null);
- try {
- // Ensure assetId is a string to preserve precision
- const assetIdStr = String(assetId);
- const response = await fetch(`/api/assets/${assetIdStr}/pdf-pages`);
- if (response.ok) {
- const data = await response.json();
- setPdfPages(data);
- } else {
- const errorData = await response.json().catch(() => ({ detail: 'Failed to load PDF' }));
- console.error('Failed to load PDF pages:', errorData);
- // Store error message for display
- setPdfPages({ error: errorData.detail || 'Failed to load PDF preview' });
- }
- } catch (error) {
- console.error('Error loading PDF pages:', error);
- setPdfPages({ error: error.message || 'Failed to load PDF preview' });
- } finally {
- setIsLoadingPdf(false);
- }
- };
-
- // Fetch assets on component mount and when upload dialog closes
- useEffect(() => {
- fetchAssets();
- }, []);
-
- // Refresh assets after successful upload
- useEffect(() => {
- if (!uploadDialogOpen && !isUploading) {
- fetchAssets();
- }
- }, [uploadDialogOpen, isUploading]);
-
- const handleFileSelect = (files) => {
- const fileArray = Array.from(files);
- setSelectedFiles(fileArray);
- };
-
- const handleDragDrop = (e) => {
- e.preventDefault();
- setDragOver(false);
- if (e.dataTransfer.files) {
- handleFileSelect(e.dataTransfer.files);
- }
- };
-
- const handleFileInputChange = (e) => {
- if (e.target.files) {
- handleFileSelect(e.target.files);
- }
- };
-
- const pollAssetStatus = async (assetId, fileName) => {
- const maxAttempts = 60; // Poll for up to 60 seconds
- let attempts = 0;
- let consecutive404s = 0;
- const max404s = 5; // Allow up to 5 consecutive 404s (asset might not be visible yet)
-
- // Small delay before first poll to allow database commit
- await new Promise(resolve => setTimeout(resolve, 500));
-
- while (attempts < maxAttempts) {
- try {
- const response = await fetch(`/api/assets/${assetId}/status`);
- if (response.ok) {
- const status = await response.json();
- consecutive404s = 0; // Reset 404 counter on success
- setUploadProgress(prev => ({
- ...prev,
- [fileName]: {
- status: status.status,
- message: getStatusMessage(status.status),
- extractedContent: status.extracted_content || null // Store extracted JSON
- }
- }));
-
- if (status.status === 'completed' || status.status === 'failed') {
- break;
- }
- } else if (response.status === 404) {
- consecutive404s++;
- // If we get too many 404s, stop polling (asset might not exist)
- if (consecutive404s >= max404s) {
- console.warn(`Asset ${assetId} not found after ${max404s} attempts`);
- setUploadProgress(prev => ({
- ...prev,
- [fileName]: {
- status: 'pending',
- message: 'Asset status unavailable'
- }
- }));
- break;
- }
- // For first few 404s, keep trying (might be replication lag)
- } else {
- // Other errors - log but keep trying
- console.error(`Status check failed: ${response.status}`);
- }
- } catch (error) {
- console.error('Error polling status:', error);
- // On network errors, keep trying
- }
-
- await new Promise(resolve => setTimeout(resolve, 1000)); // Poll every second
- attempts++;
- }
- };
-
- const getStatusMessage = (status) => {
- switch(status) {
- case 'pending': return 'Uploading file...';
- case 'processing': return 'Extracting content with OCR...';
- case 'completed': return 'Content extracted and indexed by AI agent ✓';
- case 'failed': return 'Analysis failed';
- default: return 'Processing...';
- }
- };
-
- const handleUpload = async () => {
- if (selectedFiles.length === 0) {
- alert('Please select at least one file to upload');
- return;
- }
- if (!uploadProductCategory) {
- alert('Please select a product category');
- return;
- }
-
- setIsUploading(true);
- setUploadProgress({});
-
- try {
- // Initialize progress for all files
- const initialProgress = {};
- selectedFiles.forEach(file => {
- initialProgress[file.name] = {
- status: 'pending',
- message: 'Uploading file...'
- };
- });
- setUploadProgress(initialProgress);
-
- // Upload files sequentially to show progress
- const results = [];
- for (const file of selectedFiles) {
- try {
- setUploadProgress(prev => ({
- ...prev,
- [file.name]: {
- status: 'pending',
- message: 'Uploading file...'
- }
- }));
-
- const formData = new FormData();
- formData.append('file', file);
- formData.append('product_category', uploadProductCategory);
- if (uploadSubCategory && uploadSubCategory !== 'none') {
- formData.append('sub_category', uploadSubCategory);
- }
-
- const response = await fetch('/api/assets/upload', {
- method: 'POST',
- body: formData,
- });
-
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({ detail: 'Upload failed' }));
- throw new Error(errorData.detail || `Upload failed for ${file.name}`);
- }
-
- const result = await response.json();
- results.push(result);
-
- // Ensure ID is kept as string to preserve precision
- const assetId = String(result.id);
-
- // Update progress - file uploaded, now analyzing
- setUploadProgress(prev => ({
- ...prev,
- [file.name]: {
- status: result.analysis_status || 'processing',
- message: result.analysis_status === 'processing'
- ? 'Extracting content with OCR...'
- : 'Upload complete, analyzing...'
- }
- }));
-
- // Start polling for analysis status if it's a document/image
- if (result.file_type === 'document' || result.file_type === 'image') {
- pollAssetStatus(assetId, file.name);
- } else {
- setUploadProgress(prev => ({
- ...prev,
- [file.name]: {
- status: 'completed',
- message: 'Upload complete ✓'
- }
- }));
- }
- } catch (error) {
- console.error(`Upload error for ${file.name}:`, error);
- setUploadProgress(prev => ({
- ...prev,
- [file.name]: {
- status: 'failed',
- message: `Upload failed: ${error.message}`
- }
- }));
- }
- }
-
- console.log('Upload results:', results);
-
- // Wait a bit for analysis to complete, then refresh
- setTimeout(async () => {
- await fetchAssets();
- }, 2000);
-
- // Don't close dialog immediately - let user see the progress
- // Reset form after a delay
- setTimeout(() => {
- setSelectedFiles([]);
- setUploadProductCategory('');
- setUploadSubCategory('');
- setUploadProgress({});
- setUploadDialogOpen(false);
- }, 3000);
-
- } catch (error) {
- console.error('Upload error:', error);
- alert(`Upload failed: ${error.message}`);
- } finally {
- setIsUploading(false);
- }
- };
-
- return (
-
-
- {/* Header */}
-
-
-
-
- Asset Repository
-
-
- Manage your marketing materials, screenshots, and documents
-
-
-
-
-
-
-
- {/* Sidebar - Product Categories */}
-
-
-
-
- Product Categories
-
-
-
-
-
-
- {products.map(product => (
-
-
-
-
- {expandedProducts.includes(product.id) && product.subCategories.length > 0 && (
-
-
- {product.subCategories.map((sub, idx) => (
-
- ))}
-
-
- )}
-
-
- ))}
-
-
-
-
-
- {/* Main Content */}
-
- {/* Search and Filters */}
-
-
-
-
-
- setSearchQuery(e.target.value)}
- className="pl-10 border-slate-200"
- />
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Assets Grid/List */}
- {viewMode === 'grid' ? (
-
- {filteredAssets.map((asset, index) => (
-
-
-
- {asset.type === 'image' ? (
-
- ) : asset.type === 'video' ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {asset.name}
-
-
- {asset.size} • {asset.date}
-
-
-
-
-
-
-
- {
- setPreviewAsset(asset);
- setPreviewDialogOpen(true);
- // Load PDF pages if it's a PDF
- if (asset.type === 'document' && asset.name.toLowerCase().endsWith('.pdf')) {
- await loadPdfPages(String(asset.id));
- } else {
- setPdfPages(null);
- }
- }}>
- Preview
-
- {
- window.open(`/api/assets/${String(asset.id)}/download`, '_blank');
- }}>
- Download
-
- {
- if (confirm(`Are you sure you want to delete "${asset.name}"?`)) {
- await handleDeleteAsset(String(asset.id));
- }
- }}
- >
- Delete
-
-
-
-
-
-
- {products.find(p => p.id === asset.product)?.shortName}
-
- {asset.subCategory && (
-
- {asset.subCategory}
-
- )}
-
-
-
-
- ))}
-
- ) : (
-
-
-
- {filteredAssets.map((asset, index) => (
-
-
-
- {getTypeIcon(asset.type)}
-
-
-
- {asset.name}
-
-
- {asset.subCategory || products.find(p => p.id === asset.product)?.name}
-
-
-
- {asset.size}
-
-
- {asset.date}
-
-
- {products.find(p => p.id === asset.product)?.shortName}
-
-
-
-
-
-
- {
- setPreviewAsset(asset);
- setPreviewDialogOpen(true);
- // Load PDF pages if it's a PDF
- if (asset.type === 'document' && asset.name.toLowerCase().endsWith('.pdf')) {
- await loadPdfPages(String(asset.id));
- } else {
- setPdfPages(null);
- }
- }}>
- Preview
-
- {
- window.open(`/api/assets/${String(asset.id)}/download`, '_blank');
- }}>
- Download
-
- {
- if (confirm(`Are you sure you want to delete "${asset.name}"?`)) {
- await handleDeleteAsset(String(asset.id));
- }
- }}
- >
- Delete
-
-
-
-
- ))}
-
-
-
- )}
-
-
-
-
- {/* Preview Dialog */}
-
-
- );
-}
-
+ import React, { useState, useRef, useEffect } from 'react';
+ import { motion, AnimatePresence } from 'framer-motion';
+ import {
+ Upload,
+ FolderOpen,
+ Image,
+ FileText,
+ Video,
+ Search,
+ Filter,
+ Grid3X3,
+ List,
+ MoreVertical,
+ Download,
+ Trash2,
+ Eye,
+ ChevronDown,
+ ChevronRight,
+ Plus,
+ X,
+ Check,
+ File,
+ Copy
+ } from 'lucide-react';
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+ import { Button } from '@/components/ui/button';
+ import { Input } from '@/components/ui/input';
+ import { Badge } from '@/components/ui/badge';
+ import { Label } from '@/components/ui/label';
+ import { Progress } from '@/components/ui/progress';
+ import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+ } from '@/components/ui/select';
+ import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+ } from '@/components/ui/dropdown-menu';
+ import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+ } from '@/components/ui/dialog';
+ import { Checkbox } from '@/components/ui/checkbox';
+
+ const products = [
+ {
+ id: 'ocr',
+ name: 'Intelligent Document Parsing (OCR)',
+ shortName: 'OCR',
+ color: 'blue',
+ subCategories: []
+ },
+ {
+ id: 'p2p',
+ name: 'Purchase To Pay (P2P)',
+ shortName: 'P2P',
+ color: 'emerald',
+ subCategories: [
+ 'Budget Approval Workflow',
+ 'Purchase Request Workflow',
+ 'Accounts Payable Workflow'
+ ]
+ },
+ {
+ id: 'o2c',
+ name: 'Order to Cash (O2C)',
+ shortName: 'O2C',
+ color: 'violet',
+ subCategories: [
+ 'Quotation Workflow',
+ 'Sales Order Workflow',
+ 'PickSlip Delivery Workflow',
+ 'Accounts Receivable Workflow'
+ ]
+ }
+ ];
+
+ const mockAssets = [
+ { id: '1', name: 'OCR_Demo_Screenshot.png', type: 'image', product: 'ocr', subCategory: null, size: '2.4 MB', date: '2024-12-20' },
+ { id: '2', name: 'P2P_Workflow_Diagram.pdf', type: 'document', product: 'p2p', subCategory: 'Budget Approval Workflow', size: '1.8 MB', date: '2024-12-19' },
+ { id: '3', name: 'Invoice_Processing_Video.mp4', type: 'video', product: 'ocr', subCategory: null, size: '45.2 MB', date: '2024-12-18' },
+ { id: '4', name: 'Sales_Order_Infographic.png', type: 'image', product: 'o2c', subCategory: 'Sales Order Workflow', size: '3.1 MB', date: '2024-12-17' },
+ { id: '5', name: 'AP_Automation_Brochure.pdf', type: 'document', product: 'p2p', subCategory: 'Accounts Payable Workflow', size: '5.6 MB', date: '2024-12-16' },
+ { id: '6', name: 'O2C_Product_Banner.png', type: 'image', product: 'o2c', subCategory: 'Quotation Workflow', size: '1.2 MB', date: '2024-12-15' },
+ { id: '7', name: 'Document_Parsing_Demo.png', type: 'image', product: 'ocr', subCategory: null, size: '890 KB', date: '2024-12-14' },
+ { id: '8', name: 'PR_Workflow_Guide.pdf', type: 'document', product: 'p2p', subCategory: 'Purchase Request Workflow', size: '2.3 MB', date: '2024-12-13' },
+ ];
+
+ export default function Repository() {
+ const [viewMode, setViewMode] = useState('grid');
+ const [searchQuery, setSearchQuery] = useState('');
+ const [selectedProduct, setSelectedProduct] = useState('all');
+ const [expandedProducts, setExpandedProducts] = useState(['ocr', 'p2p', 'o2c']);
+ const [selectedAssets, setSelectedAssets] = useState([]);
+ const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
+ const [dragOver, setDragOver] = useState(false);
+ const [selectedFiles, setSelectedFiles] = useState([]);
+ const [uploadProductCategory, setUploadProductCategory] = useState('');
+ const [uploadSubCategory, setUploadSubCategory] = useState('');
+ const [isUploading, setIsUploading] = useState(false);
+ const [assets, setAssets] = useState(mockAssets);
+ const [isLoadingAssets, setIsLoadingAssets] = useState(false);
+ const [previewAsset, setPreviewAsset] = useState(null);
+ const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
+ const [uploadProgress, setUploadProgress] = useState({});
+ const [pdfPages, setPdfPages] = useState(null);
+ const [isLoadingPdf, setIsLoadingPdf] = useState(false);
+ const [isDeleting, setIsDeleting] = useState(false);
+ const fileInputRef = useRef(null);
+
+ const toggleProduct = (productId) => {
+ setExpandedProducts(prev =>
+ prev.includes(productId)
+ ? prev.filter(id => id !== productId)
+ : [...prev, productId]
+ );
+ };
+
+ const filteredAssets = assets.filter(asset => {
+ const matchesSearch = asset.name.toLowerCase().includes(searchQuery.toLowerCase());
+ const matchesProduct = selectedProduct === 'all' || asset.product === selectedProduct;
+ return matchesSearch && matchesProduct;
+ });
+
+ const getAssetsByProduct = (productId) => {
+ return assets.filter(asset => asset.product === productId);
+ };
+
+ const getTypeIcon = (type) => {
+ switch(type) {
+ case 'image': return ;
+ case 'video': return ;
+ case 'document': return ;
+ default: return ;
+ }
+ };
+
+ const getProductColor = (productId) => {
+ const product = products.find(p => p.id === productId);
+ return product?.color || 'slate';
+ };
+
+ // Fetch assets from API
+ const fetchAssets = async () => {
+ setIsLoadingAssets(true);
+ try {
+ const response = await fetch('/api/assets');
+ if (response.ok) {
+ const data = await response.json();
+ console.log('Fetched assets from API:', data); // Debug log
+
+ // Convert API response to match mockAssets format
+ // IMPORTANT: Keep IDs as strings to preserve precision for large CockroachDB IDs
+ const formattedAssets = data.map(asset => {
+ // Handle date formatting more robustly
+ let dateStr = new Date().toISOString().split('T')[0]; // Default to today
+ if (asset.created_at) {
+ try {
+ // Handle both string and datetime object
+ let date;
+ if (asset.created_at instanceof Date) {
+ date = asset.created_at;
+ } else if (typeof asset.created_at === 'string') {
+ date = new Date(asset.created_at);
+ } else {
+ // Handle datetime object from backend
+ date = new Date(asset.created_at);
+ }
+
+ if (!isNaN(date.getTime())) {
+ dateStr = date.toISOString().split('T')[0];
+ } else {
+ console.warn('Invalid date:', asset.created_at);
+ }
+ } catch (e) {
+ console.warn('Date parsing error:', e, asset.created_at);
+ }
+ }
+
+ // CRITICAL: Keep ID as string - never convert to number (precision loss for large IDs)
+ // If asset.id is already a number, convert it carefully to preserve precision
+ let assetIdStr;
+ if (typeof asset.id === 'string') {
+ assetIdStr = asset.id;
+ } else if (typeof asset.id === 'number') {
+ // For very large numbers, use BigInt to preserve precision, then convert to string
+ assetIdStr = BigInt(asset.id).toString();
+ } else {
+ assetIdStr = String(asset.id);
+ }
+
+ return {
+ id: assetIdStr, // Always keep as string to preserve precision for large CockroachDB IDs
+ name: asset.name || 'Unknown',
+ type: asset.file_type || 'unknown',
+ product: asset.product_category || 'ocr',
+ subCategory: asset.sub_category || null,
+ size: formatFileSize(asset.size || 0),
+ date: dateStr
+ };
+ });
+
+ console.log('Formatted assets:', formattedAssets); // Debug log
+ setAssets(formattedAssets);
+ } else {
+ const errorText = await response.text();
+ console.error('Failed to fetch assets:', response.status, errorText);
+ // Don't reset to mockAssets - keep what we have
+ }
+ } catch (error) {
+ console.error('Error fetching assets:', error);
+ // Don't reset to mockAssets - keep what we have
+ } finally {
+ setIsLoadingAssets(false);
+ }
+ };
+
+ // Format file size
+ const formatFileSize = (bytes) => {
+ if (!bytes) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
+ };
+
+ // Handle delete asset
+ const handleDeleteAsset = async (assetId) => {
+ setIsDeleting(true);
+ try {
+ // Ensure assetId is a string to preserve precision
+ const assetIdStr = String(assetId);
+ const response = await fetch(`/api/assets/${assetIdStr}`, {
+ method: 'DELETE',
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({ detail: 'Delete failed' }));
+ throw new Error(errorData.detail || 'Delete failed');
+ }
+
+ const result = await response.json();
+ console.log('Delete result:', result);
+
+ // Refresh assets list
+ await fetchAssets();
+
+ // Close preview if deleted asset was being previewed
+ if (previewAsset && previewAsset.id === assetId) {
+ setPreviewDialogOpen(false);
+ setPreviewAsset(null);
+ setPdfPages(null);
+ }
+
+ alert('Asset deleted successfully');
+ } catch (error) {
+ console.error('Delete error:', error);
+ alert(`Delete failed: ${error.message}`);
+ } finally {
+ setIsDeleting(false);
+ }
+ };
+
+ // Load PDF pages when previewing a PDF
+ const loadPdfPages = async (assetId) => {
+ setIsLoadingPdf(true);
+ setPdfPages(null);
+ try {
+ // CRITICAL: Ensure assetId is a string to preserve precision for large IDs
+ // Never convert to number - JavaScript loses precision for numbers > Number.MAX_SAFE_INTEGER
+ let assetIdStr;
+ if (typeof assetId === 'string') {
+ assetIdStr = assetId;
+ } else if (typeof assetId === 'number') {
+ // Use BigInt to preserve precision, then convert to string
+ assetIdStr = BigInt(assetId).toString();
+ } else {
+ assetIdStr = String(assetId);
+ }
+ console.log(`Loading PDF for asset ID: ${assetIdStr} (original type: ${typeof assetId})`);
+ const response = await fetch(`/api/assets/${assetIdStr}/pdf-pages`);
+
+ if (response.ok) {
+ const data = await response.json();
+ setPdfPages(data);
+ } else {
+ const errorData = await response.json().catch(() => ({
+ detail: response.status === 503
+ ? 'PDF preview requires poppler-utils to be installed on the server'
+ : 'Failed to load PDF preview'
+ }));
+ console.error('Failed to load PDF pages:', errorData);
+ setPdfPages({ error: errorData.detail || 'Failed to load PDF preview' });
+ }
+ } catch (error) {
+ console.error('Error loading PDF pages:', error);
+ setPdfPages({
+ error: error.message || 'Failed to load PDF preview. Make sure poppler-utils is installed.'
+ });
+ } finally {
+ setIsLoadingPdf(false);
+ }
+ };
+
+ // Fetch assets on component mount and when upload dialog closes
+ useEffect(() => {
+ fetchAssets();
+ }, []);
+
+ // Refresh assets after successful upload
+ useEffect(() => {
+ if (!uploadDialogOpen && !isUploading) {
+ console.log('useEffect: Refreshing assets (dialog closed, not uploading)');
+ fetchAssets();
+ }
+ }, [uploadDialogOpen, isUploading]);
+
+ // Auto-load PDF pages when preview opens for PDF files
+ useEffect(() => {
+ if (previewAsset && previewDialogOpen) {
+ // Load PDF pages if it's a PDF
+ if (previewAsset.type === 'document' && previewAsset.name.toLowerCase().endsWith('.pdf')) {
+ console.log('Auto-loading PDF pages for:', previewAsset.id);
+ loadPdfPages(String(previewAsset.id));
+ } else {
+ setPdfPages(null);
+ }
+ }
+ }, [previewAsset, previewDialogOpen]);
+
+ const handleFileSelect = (files) => {
+ const fileArray = Array.from(files);
+ setSelectedFiles(fileArray);
+ };
+
+ const handleDragDrop = (e) => {
+ e.preventDefault();
+ setDragOver(false);
+ if (e.dataTransfer.files) {
+ handleFileSelect(e.dataTransfer.files);
+ }
+ };
+
+ const handleFileInputChange = (e) => {
+ if (e.target.files) {
+ handleFileSelect(e.target.files);
+ }
+ };
+
+ const pollAssetStatus = async (assetId, fileName) => {
+ const maxAttempts = 60; // Poll for up to 60 seconds
+ let attempts = 0;
+ let consecutive404s = 0;
+ const max404s = 5; // Allow up to 5 consecutive 404s (asset might not be visible yet)
+
+ // Small delay before first poll to allow database commit
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ while (attempts < maxAttempts) {
+ try {
+ const response = await fetch(`/api/assets/${assetId}/status`);
+ if (response.ok) {
+ const status = await response.json();
+ consecutive404s = 0; // Reset 404 counter on success
+ setUploadProgress(prev => ({
+ ...prev,
+ [fileName]: {
+ status: status.status,
+ message: getStatusMessage(status.status),
+ extractedContent: status.extracted_content || null // Store extracted JSON
+ }
+ }));
+
+ if (status.status === 'completed' || status.status === 'failed') {
+ break;
+ }
+ } else if (response.status === 404) {
+ consecutive404s++;
+ // If we get too many 404s, stop polling (asset might not exist)
+ if (consecutive404s >= max404s) {
+ console.warn(`Asset ${assetId} not found after ${max404s} attempts`);
+ setUploadProgress(prev => ({
+ ...prev,
+ [fileName]: {
+ status: 'pending',
+ message: 'Asset status unavailable'
+ }
+ }));
+ break;
+ }
+ // For first few 404s, keep trying (might be replication lag)
+ } else {
+ // Other errors - log but keep trying
+ console.error(`Status check failed: ${response.status}`);
+ }
+ } catch (error) {
+ console.error('Error polling status:', error);
+ // On network errors, keep trying
+ }
+
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Poll every second
+ attempts++;
+ }
+ };
+
+ const getStatusMessage = (status) => {
+ switch(status) {
+ case 'pending': return 'Uploading file...';
+ case 'processing': return 'Extracting content with OCR...';
+ case 'completed': return 'Content extracted and indexed by AI agent ✓';
+ case 'failed': return 'Analysis failed';
+ default: return 'Processing...';
+ }
+ };
+
+ const handleUpload = async () => {
+ if (selectedFiles.length === 0) {
+ alert('Please select at least one file to upload');
+ return;
+ }
+ if (!uploadProductCategory) {
+ alert('Please select a product category');
+ return;
+ }
+
+ setIsUploading(true);
+ setUploadProgress({});
+
+ try {
+ // Initialize progress for all files
+ const initialProgress = {};
+ selectedFiles.forEach(file => {
+ initialProgress[file.name] = {
+ status: 'pending',
+ message: 'Uploading file...'
+ };
+ });
+ setUploadProgress(initialProgress);
+
+ // Upload files sequentially to show progress
+ const results = [];
+ for (const file of selectedFiles) {
+ try {
+ setUploadProgress(prev => ({
+ ...prev,
+ [file.name]: {
+ status: 'pending',
+ message: 'Uploading file...'
+ }
+ }));
+
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('product_category', uploadProductCategory);
+ if (uploadSubCategory && uploadSubCategory !== 'none') {
+ formData.append('sub_category', uploadSubCategory);
+ }
+
+ const response = await fetch('/api/assets/upload', {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({ detail: 'Upload failed' }));
+ throw new Error(errorData.detail || `Upload failed for ${file.name}`);
+ }
+
+ const result = await response.json();
+ results.push(result);
+
+ // Ensure ID is kept as string to preserve precision
+ const assetId = String(result.id);
+
+ // Update progress - file uploaded, now analyzing
+ setUploadProgress(prev => ({
+ ...prev,
+ [file.name]: {
+ status: result.analysis_status || 'processing',
+ message: result.analysis_status === 'processing'
+ ? 'Extracting content with OCR...'
+ : 'Upload complete, analyzing...'
+ }
+ }));
+
+ // Start polling for analysis status if it's a document/image
+ if (result.file_type === 'document' || result.file_type === 'image') {
+ pollAssetStatus(assetId, file.name);
+ } else {
+ setUploadProgress(prev => ({
+ ...prev,
+ [file.name]: {
+ status: 'completed',
+ message: 'Upload complete ✓'
+ }
+ }));
+ }
+ } catch (error) {
+ console.error(`Upload error for ${file.name}:`, error);
+ setUploadProgress(prev => ({
+ ...prev,
+ [file.name]: {
+ status: 'failed',
+ message: `Upload failed: ${error.message}`
+ }
+ }));
+ }
+ }
+
+ console.log('Upload results:', results);
+
+ // Refresh assets immediately after upload
+ console.log('Refreshing assets after upload...');
+ await fetchAssets();
+
+ // Wait a bit more for database to commit, then refresh again
+ setTimeout(async () => {
+ console.log('Refreshing assets again after delay...');
+ await fetchAssets();
+ }, 1500);
+
+ // Don't close dialog immediately - let user see the progress
+ // Reset form after a delay
+ setTimeout(() => {
+ setSelectedFiles([]);
+ setUploadProductCategory('');
+ setUploadSubCategory('');
+ setUploadProgress({});
+ setUploadDialogOpen(false);
+ // Refresh again after dialog closes to ensure latest data
+ console.log('Refreshing assets after dialog closes...');
+ fetchAssets();
+ }, 3000);
+
+ } catch (error) {
+ console.error('Upload error:', error);
+ alert(`Upload failed: ${error.message}`);
+ } finally {
+ setIsUploading(false);
+ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+ Asset Repository
+
+
+ Manage your marketing materials, screenshots, and documents
+
+
+
+
+
+
+
+ {/* Sidebar - Product Categories */}
+
+
+
+
+ Product Categories
+
+
+
+
+
+
+ {products.map(product => (
+
+
+
+
+ {expandedProducts.includes(product.id) && product.subCategories.length > 0 && (
+
+
+ {product.subCategories.map((sub, idx) => (
+
+ ))}
+
+
+ )}
+
+
+ ))}
+
+
+
+
+
+ {/* Main Content */}
+
+ {/* Search and Filters */}
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-10 border-slate-200"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Assets Grid/List */}
+ {viewMode === 'grid' ? (
+
+ {filteredAssets.map((asset, index) => (
+
+
+
+ {asset.type === 'image' ? (
+
+ ) : asset.type === 'video' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {asset.name}
+
+
+ {asset.size} • {asset.date}
+
+
+
+
+
+
+
+ {
+ setPreviewAsset(asset);
+ setPreviewDialogOpen(true);
+ // Load PDF pages if it's a PDF
+ if (asset.type === 'document' && asset.name.toLowerCase().endsWith('.pdf')) {
+ await loadPdfPages(String(asset.id));
+ } else {
+ setPdfPages(null);
+ }
+ }}>
+ Preview
+
+ {
+ window.open(`/api/assets/${String(asset.id)}/download`, '_blank');
+ }}>
+ Download
+
+ {
+ if (confirm(`Are you sure you want to delete "${asset.name}"?`)) {
+ await handleDeleteAsset(String(asset.id));
+ }
+ }}
+ >
+ Delete
+
+
+
+
+
+
+ {products.find(p => p.id === asset.product)?.shortName}
+
+ {asset.subCategory && (
+
+ {asset.subCategory}
+
+ )}
+
+
+
+
+ ))}
+
+ ) : (
+
+
+
+ {filteredAssets.map((asset, index) => (
+
+
+
+ {getTypeIcon(asset.type)}
+
+
+
+ {asset.name}
+
+
+ {asset.subCategory || products.find(p => p.id === asset.product)?.name}
+
+
+
+ {asset.size}
+
+
+ {asset.date}
+
+
+ {products.find(p => p.id === asset.product)?.shortName}
+
+
+
+
+
+
+ {
+ setPreviewAsset(asset);
+ setPreviewDialogOpen(true);
+ // Load PDF pages if it's a PDF
+ if (asset.type === 'document' && asset.name.toLowerCase().endsWith('.pdf')) {
+ await loadPdfPages(String(asset.id));
+ } else {
+ setPdfPages(null);
+ }
+ }}>
+ Preview
+
+ {
+ window.open(`/api/assets/${String(asset.id)}/download`, '_blank');
+ }}>
+ Download
+
+ {
+ if (confirm(`Are you sure you want to delete "${asset.name}"?`)) {
+ await handleDeleteAsset(String(asset.id));
+ }
+ }}
+ >
+ Delete
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+
+ {/* Preview Dialog */}
+
+
+ );
+ }
+