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 */}
{/* 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 */}
);
}