302 lines
9.9 KiB
TypeScript
302 lines
9.9 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import {
|
|
ImagePlus,
|
|
Maximize,
|
|
Eraser,
|
|
Film,
|
|
Captions,
|
|
Volume2,
|
|
Type,
|
|
Wand2,
|
|
FileText,
|
|
TrendingUp,
|
|
Clock,
|
|
CheckCircle,
|
|
} from 'lucide-react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { toast } from 'react-hot-toast';
|
|
import ModuleCard from '@/components/ModuleCard';
|
|
import { useStore } from '@/lib/store';
|
|
import { jobsApi, usersApi } from '@/lib/api';
|
|
|
|
const modules = [
|
|
{
|
|
title: 'Image Generator',
|
|
description: 'Create stunning images with AI using multiple providers',
|
|
icon: ImagePlus,
|
|
href: '/image/generate',
|
|
},
|
|
{
|
|
title: 'Image Upscaler',
|
|
description: 'Enhance image resolution with Topaz Labs AI',
|
|
icon: Maximize,
|
|
href: '/image/upscale',
|
|
},
|
|
{
|
|
title: 'Background Remover',
|
|
description: 'Remove backgrounds instantly with precision',
|
|
icon: Eraser,
|
|
href: '/image/remove-bg',
|
|
},
|
|
{
|
|
title: 'Video Generator',
|
|
description: 'Generate videos with Runway and Google Veo',
|
|
icon: Film,
|
|
href: '/video/generate',
|
|
},
|
|
{
|
|
title: 'Video Upscaler',
|
|
description: 'Upscale videos to higher resolutions',
|
|
icon: Maximize,
|
|
href: '/video/upscale',
|
|
},
|
|
{
|
|
title: 'Subtitle Generator',
|
|
description: 'Auto-generate and translate subtitles',
|
|
icon: Captions,
|
|
href: '/video/subtitles',
|
|
},
|
|
{
|
|
title: 'Text to Speech',
|
|
description: 'Convert text to natural speech with ElevenLabs',
|
|
icon: Volume2,
|
|
href: '/audio/text-to-speech',
|
|
},
|
|
{
|
|
title: 'Voice to Text',
|
|
description: 'Transcribe audio with Whisper AI',
|
|
icon: Type,
|
|
href: '/audio/voice-to-text',
|
|
},
|
|
{
|
|
title: 'Prompt Studio',
|
|
description: 'Enhance your prompts with AI assistance',
|
|
icon: Wand2,
|
|
href: '/text/prompt-studio',
|
|
},
|
|
{
|
|
title: 'Alt Text Generator',
|
|
description: 'Generate accessible alt text for images',
|
|
icon: FileText,
|
|
href: '/text/alt-text',
|
|
},
|
|
];
|
|
|
|
export default function Dashboard() {
|
|
const router = useRouter();
|
|
const { activeJobs } = useStore();
|
|
const [stats, setStats] = useState({
|
|
totalJobs: 0,
|
|
completedToday: 0,
|
|
processingTime: 0,
|
|
});
|
|
const [recentJobs, setRecentJobs] = useState<any[]>([]);
|
|
const [dragTarget, setDragTarget] = useState<{ href: string, valid: boolean } | null>(null);
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
try {
|
|
const jobsResponse = await jobsApi.list({ limit: 5 });
|
|
setRecentJobs(jobsResponse.data.items || []);
|
|
|
|
// Calculate stats from recent jobs
|
|
const completed = jobsResponse.data.items?.filter(
|
|
(j: any) => j.status === 'completed'
|
|
).length || 0;
|
|
setStats({
|
|
totalJobs: jobsResponse.data.total || 0,
|
|
completedToday: completed,
|
|
processingTime: 2.4,
|
|
});
|
|
} catch (err) {
|
|
console.error('Failed to fetch dashboard data:', err);
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
}, []);
|
|
|
|
const validateDrop = (href: string, mimeType: string) => {
|
|
if (href.startsWith('/image/')) return mimeType.startsWith('image/');
|
|
if (href === '/video/upscale') return mimeType.startsWith('video/');
|
|
if (href === '/video/subtitles') return mimeType.startsWith('video/');
|
|
if (href === '/video/generate') return mimeType.startsWith('image/'); // Image to Video
|
|
if (href === '/text/alt-text') return mimeType.startsWith('image/');
|
|
if (href === '/audio/voice-to-text') return mimeType.startsWith('audio/') || mimeType.startsWith('video/');
|
|
return false;
|
|
};
|
|
|
|
const handleDragOver = (e: React.DragEvent, href: string) => {
|
|
e.preventDefault();
|
|
if (e.dataTransfer.types.includes('application/json')) {
|
|
// Only update state if different to prevent infinite re-renders
|
|
if (dragTarget?.href !== href) {
|
|
setDragTarget({ href, valid: true });
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleDrop = (e: React.DragEvent, href: string) => {
|
|
e.preventDefault();
|
|
setDragTarget(null);
|
|
|
|
const raw = e.dataTransfer.getData('application/json');
|
|
if (!raw) return;
|
|
|
|
try {
|
|
const data = JSON.parse(raw);
|
|
if (data.type === 'forge-asset') {
|
|
if (validateDrop(href, data.mime_type || '')) {
|
|
router.push(`${href}?assetId=${data.id}`);
|
|
toast.success('File loaded into tool');
|
|
} else {
|
|
toast.error('Invalid file type for this tool');
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Stats Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div className="bg-forge-dark rounded-xl p-6 border border-gray-800">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-12 h-12 bg-forge-yellow/10 rounded-lg flex items-center justify-center">
|
|
<TrendingUp className="w-6 h-6 text-forge-yellow" />
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 text-sm">Total Jobs</p>
|
|
<p className="text-2xl font-bold text-white">{stats.totalJobs}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-forge-dark rounded-xl p-6 border border-gray-800">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-12 h-12 bg-green-900/30 rounded-lg flex items-center justify-center">
|
|
<CheckCircle className="w-6 h-6 text-green-400" />
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 text-sm">Completed Today</p>
|
|
<p className="text-2xl font-bold text-white">{stats.completedToday}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-forge-dark rounded-xl p-6 border border-gray-800">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-12 h-12 bg-blue-900/30 rounded-lg flex items-center justify-center">
|
|
<Clock className="w-6 h-6 text-blue-400" />
|
|
</div>
|
|
<div>
|
|
<p className="text-gray-500 text-sm">Avg. Processing Time</p>
|
|
<p className="text-2xl font-bold text-white">{stats.processingTime}s</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Active Jobs */}
|
|
{activeJobs.filter((j) => j.status === 'processing').length > 0 && (
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-white mb-4">Active Jobs</h2>
|
|
<div className="space-y-3">
|
|
{activeJobs
|
|
.filter((j) => j.status === 'processing')
|
|
.map((job) => (
|
|
<div
|
|
key={job.id}
|
|
className="bg-forge-dark rounded-xl p-4 border border-gray-800"
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-white font-medium capitalize">
|
|
{job.module.replace('_', ' ')}
|
|
</span>
|
|
<span className="text-gray-500 text-sm">{job.progress}%</span>
|
|
</div>
|
|
<div className="progress-bar">
|
|
<div
|
|
className="progress-bar-fill"
|
|
style={{ width: `${job.progress}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Modules Grid */}
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-white mb-4">AI Tools</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
{modules.map((module) => (
|
|
<div
|
|
key={module.href}
|
|
onDragOver={(e) => handleDragOver(e, module.href)}
|
|
onDragLeave={() => setDragTarget(null)}
|
|
onDrop={(e) => handleDrop(e, module.href)}
|
|
className={`transition-all rounded-xl h-full ${dragTarget?.href === module.href ? 'ring-2 ring-forge-yellow scale-105' : ''}`}
|
|
>
|
|
<ModuleCard {...module} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Jobs */}
|
|
{recentJobs.length > 0 && (
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-white mb-4">Recent Activity</h2>
|
|
<div className="bg-forge-dark rounded-xl border border-gray-800 overflow-hidden">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="border-b border-gray-800">
|
|
<th className="text-left px-6 py-4 text-sm font-medium text-gray-500">
|
|
Module
|
|
</th>
|
|
<th className="text-left px-6 py-4 text-sm font-medium text-gray-500">
|
|
Status
|
|
</th>
|
|
<th className="text-left px-6 py-4 text-sm font-medium text-gray-500">
|
|
Provider
|
|
</th>
|
|
<th className="text-left px-6 py-4 text-sm font-medium text-gray-500">
|
|
Created
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{recentJobs.map((job: any) => (
|
|
<tr key={job.id} className="border-b border-gray-800 last:border-0">
|
|
<td className="px-6 py-4 text-white capitalize">
|
|
{job.module?.replace('_', ' ')}
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<span
|
|
className={`badge badge-${job.status}`}
|
|
>
|
|
{job.status}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 text-gray-400">
|
|
{job.api_provider || '-'}
|
|
</td>
|
|
<td className="px-6 py-4 text-gray-500 text-sm">
|
|
{new Date(job.created_at).toLocaleString()}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|