forge/frontend/app/page.tsx

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>
);
}