263 lines
16 KiB
TypeScript
263 lines
16 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { assetsApi } from '@/lib/api';
|
|
import { Clock, FileImage, FileVideo, FileAudio, FileText, ChevronRight, GripVertical, ChevronLeft, ChevronLast, ChevronFirst } from 'lucide-react';
|
|
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
|
|
|
|
const RECENT_LIMIT = 40;
|
|
|
|
export default function RecentAssets() {
|
|
const router = useRouter();
|
|
const [assets, setAssets] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
const [selectedAssetId, setSelectedAssetId] = useState<string | null>(null);
|
|
|
|
const fetchRecentAssets = async () => {
|
|
try {
|
|
const response = await assetsApi.list({ limit: RECENT_LIMIT, sort: 'created_at', order: 'desc' });
|
|
setAssets(response.data);
|
|
} catch (err) {
|
|
console.error("Failed to fetch recent assets", err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchRecentAssets();
|
|
const interval = setInterval(fetchRecentAssets, 30000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
const renderPreview = (asset: any) => {
|
|
if (asset.mime_type.startsWith('image/')) {
|
|
return (
|
|
<div className="w-16 h-16 rounded overflow-hidden bg-black/20">
|
|
<img src={`/api/v1/assets/${asset.id}/download`} alt="preview" className="w-full h-full object-cover" />
|
|
</div>
|
|
);
|
|
}
|
|
if (asset.mime_type.startsWith('video/')) {
|
|
return (
|
|
<div className="w-16 h-16 rounded overflow-hidden relative bg-black">
|
|
<video src={`/api/v1/assets/${asset.id}/download`} className="w-full h-full object-cover" />
|
|
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
|
<FileVideo className="w-6 h-6 text-white" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
if (asset.mime_type.startsWith('audio/')) {
|
|
return <div className="w-16 h-16 flex items-center justify-center bg-green-500/20 rounded text-green-400"><FileAudio className="w-8 h-8" /></div>;
|
|
}
|
|
|
|
return <div className="w-16 h-16 flex items-center justify-center bg-gray-500/20 rounded text-gray-400"><FileText className="w-8 h-8" /></div>;
|
|
};
|
|
|
|
const handleClick = (asset: any) => {
|
|
if (asset.mime_type.startsWith('image/')) {
|
|
router.push(`/image/upscale?assetId=${asset.id}`);
|
|
} else if (asset.mime_type.startsWith('video/')) {
|
|
router.push(`/video/upscale?assetId=${asset.id}`);
|
|
} else if (asset.mime_type.startsWith('audio/')) {
|
|
router.push(`/audio/voice-to-text?assetId=${asset.id}`);
|
|
} else {
|
|
router.push(`/files?assetId=${asset.id}`);
|
|
}
|
|
};
|
|
|
|
const handleAction = (e: React.MouseEvent, asset: any, action: string) => {
|
|
e.stopPropagation();
|
|
setSelectedAssetId(null);
|
|
|
|
switch (action) {
|
|
case 'upscale_image':
|
|
router.push(`/image/upscale?assetId=${asset.id}`);
|
|
break;
|
|
case 'img_to_video':
|
|
router.push(`/video/generate?assetId=${asset.id}`);
|
|
break;
|
|
case 'remove_bg':
|
|
router.push(`/image/remove-bg?assetId=${asset.id}`);
|
|
break;
|
|
case 'upscale_video':
|
|
router.push(`/video/upscale?assetId=${asset.id}`);
|
|
break;
|
|
case 'subtitles':
|
|
router.push(`/video/subtitles?assetId=${asset.id}`);
|
|
break;
|
|
case 'transcribe':
|
|
router.push(`/audio/voice-to-text?assetId=${asset.id}`);
|
|
break;
|
|
case 'alt_text':
|
|
router.push(`/text/alt-text?assetId=${asset.id}`);
|
|
break;
|
|
default:
|
|
router.push(`/files?assetId=${asset.id}`);
|
|
}
|
|
};
|
|
|
|
const toggleMenu = (e: React.MouseEvent, assetId: string) => {
|
|
e.stopPropagation();
|
|
if (selectedAssetId === assetId) {
|
|
setSelectedAssetId(null);
|
|
} else {
|
|
setSelectedAssetId(assetId);
|
|
}
|
|
};
|
|
|
|
const handleDragEnd = (result: any) => {
|
|
if (!result.destination) return;
|
|
const items = Array.from(assets);
|
|
const [reorderedItem] = items.splice(result.source.index, 1);
|
|
items.splice(result.destination.index, 0, reorderedItem);
|
|
setAssets(items);
|
|
};
|
|
|
|
if (loading && assets.length === 0) {
|
|
return (
|
|
<div className={`bg-forge-dark border-l border-gray-800 flex flex-col transition-all duration-300 ${isCollapsed ? 'w-16 items-center py-4' : 'w-80 p-4'}`}>
|
|
<div className="animate-pulse bg-gray-800/50 w-8 h-8 rounded" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`bg-forge-dark border-l border-gray-800 flex flex-col flex-shrink-0 transition-all duration-300 ${isCollapsed ? 'w-14' : 'w-80'}`}>
|
|
<div className={`p-4 border-b border-gray-800 flex items-center ${isCollapsed ? 'justify-center' : 'justify-between'}`}>
|
|
{!isCollapsed && (
|
|
<h3 className="text-sm font-semibold text-white flex items-center gap-2">
|
|
<Clock className="w-4 h-4 text-forge-yellow" />
|
|
Recent
|
|
</h3>
|
|
)}
|
|
<button
|
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
className="text-gray-400 hover:text-white transition-colors"
|
|
>
|
|
{isCollapsed ? <ChevronFirst className="w-5 h-5" /> : <ChevronLast className="w-5 h-5" />}
|
|
</button>
|
|
</div>
|
|
|
|
{!isCollapsed && (
|
|
<div className="flex-1 overflow-y-auto p-4">
|
|
<DragDropContext onDragEnd={handleDragEnd}>
|
|
<Droppable droppableId="recent-assets">
|
|
{(provided: any) => (
|
|
<div
|
|
{...provided.droppableProps}
|
|
ref={provided.innerRef}
|
|
className="space-y-3"
|
|
>
|
|
{assets.map((asset, index) => (
|
|
<Draggable key={asset.id} draggableId={asset.id} index={index}>
|
|
{(provided: any, snapshot: any) => (
|
|
<div
|
|
ref={provided.innerRef}
|
|
{...provided.draggableProps}
|
|
{...provided.dragHandleProps}
|
|
draggable={true}
|
|
onDragStart={(e) => {
|
|
// Standard HTML5 drag start
|
|
// We pass a JSON string with asset details
|
|
const dragData = JSON.stringify({
|
|
id: asset.id,
|
|
url: `/api/v1/assets/${asset.id}/download`,
|
|
filename: asset.original_filename,
|
|
mime_type: asset.mime_type,
|
|
type: 'forge-asset'
|
|
});
|
|
e.dataTransfer.setData('application/json', dragData);
|
|
e.dataTransfer.setData('text/plain', dragData); // Fallback
|
|
|
|
// If dragging an image, try to set the drag image
|
|
if (asset.mime_type.startsWith('image/')) {
|
|
// Optional: Set custom drag image if needed, but browser default is usually okay for img tags
|
|
// But here we are dragging a div.
|
|
// e.dataTransfer.setDragImage(e.currentTarget.querySelector('img'), 0, 0);
|
|
}
|
|
}}
|
|
className={`group relative bg-forge-gray/30 hover:bg-forge-gray rounded-xl p-3 border border-transparent hover:border-gray-700 transition-all cursor-pointer ${snapshot.isDragging ? 'shadow-xl ring-2 ring-forge-yellow border-forge-yellow z-50' : ''
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className="text-gray-500 group-hover:text-gray-300">
|
|
<GripVertical className="w-4 h-4" />
|
|
</div>
|
|
<div className="flex-shrink-0">
|
|
{renderPreview(asset)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-[11px] text-gray-200 break-words font-medium leading-tight">{asset.original_filename}</p>
|
|
<p className="text-xs text-gray-500 truncate">
|
|
{(asset.file_size_bytes / 1024).toFixed(1)} KB
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={(e) => toggleMenu(e, asset.id)}
|
|
className="p-1 hover:bg-gray-700 rounded transition-colors self-center"
|
|
>
|
|
<ChevronRight className={`w-5 h-5 text-gray-400 transition-transform ${selectedAssetId === asset.id ? 'rotate-90 text-forge-yellow' : ''}`} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Context Menu */}
|
|
{selectedAssetId === asset.id && (
|
|
<div className="absolute top-full right-0 mt-1 w-56 bg-forge-dark border border-gray-700 rounded-lg shadow-xl z-50 py-1">
|
|
{asset.mime_type.startsWith('image/') && (
|
|
<>
|
|
<button onClick={(e) => handleAction(e, asset, 'upscale_image')} className="w-full text-left px-4 py-2 text-sm text-gray-300 hover:bg-forge-gray hover:text-white transition-colors">Upscale Image</button>
|
|
<button onClick={(e) => handleAction(e, asset, 'img_to_video')} className="w-full text-left px-4 py-2 text-sm text-gray-300 hover:bg-forge-gray hover:text-white transition-colors">Generate Video</button>
|
|
<button onClick={(e) => handleAction(e, asset, 'remove_bg')} className="w-full text-left px-4 py-2 text-sm text-gray-300 hover:bg-forge-gray hover:text-white transition-colors">Remove Background</button>
|
|
<button onClick={(e) => handleAction(e, asset, 'alt_text')} className="w-full text-left px-4 py-2 text-sm text-gray-300 hover:bg-forge-gray hover:text-white transition-colors">Generate Alt Text</button>
|
|
</>
|
|
)}
|
|
{asset.mime_type.startsWith('video/') && (
|
|
<>
|
|
<button onClick={(e) => handleAction(e, asset, 'upscale_video')} className="w-full text-left px-4 py-2 text-sm text-gray-300 hover:bg-forge-gray hover:text-white transition-colors">Upscale Video</button>
|
|
<button onClick={(e) => handleAction(e, asset, 'subtitles')} className="w-full text-left px-4 py-2 text-sm text-gray-300 hover:bg-forge-gray hover:text-white transition-colors">Generate Subtitles</button>
|
|
</>
|
|
)}
|
|
{asset.mime_type.startsWith('audio/') && (
|
|
<button onClick={(e) => handleAction(e, asset, 'transcribe')} className="w-full text-left px-4 py-2 text-sm text-gray-300 hover:bg-forge-gray hover:text-white transition-colors">Transcribe Audio</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</Draggable>
|
|
))}
|
|
{provided.placeholder}
|
|
{assets.length === 0 && (
|
|
<p className="text-center text-gray-500 text-sm py-4">
|
|
No recent activity
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</Droppable>
|
|
</DragDropContext>
|
|
</div>
|
|
)}
|
|
|
|
{isCollapsed && (
|
|
<div className="flex-1 flex flex-col items-center pt-4 gap-4">
|
|
{assets.slice(0, 5).map(asset => (
|
|
<div key={asset.id} className="w-10 h-10 rounded-lg overflow-hidden opacity-50 hover:opacity-100 transition-opacity cursor-pointer" title={asset.original_filename} onClick={() => setIsCollapsed(false)}>
|
|
{asset.mime_type.startsWith('image/') ? (
|
|
<img src={`/api/v1/assets/${asset.id}/download`} className="w-full h-full object-cover" />
|
|
) : (
|
|
<div className="w-full h-full bg-gray-800 flex items-center justify-center">
|
|
<Clock className="w-4 h-4 text-gray-500" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|