forked from 77media/video-flow
331 lines
12 KiB
TypeScript
331 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from '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 { Progress } from '@/components/ui/progress';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import {
|
|
Search,
|
|
Filter,
|
|
MoreHorizontal,
|
|
Play,
|
|
Download,
|
|
Trash2,
|
|
RefreshCw,
|
|
Clock,
|
|
CheckCircle,
|
|
XCircle,
|
|
Eye,
|
|
} from 'lucide-react';
|
|
|
|
const mockTasks = [
|
|
{
|
|
id: 1,
|
|
title: 'Tech Product Demo',
|
|
status: 'completed',
|
|
progress: 100,
|
|
duration: '2:45',
|
|
createdAt: '2024-01-15T10:30:00Z',
|
|
completedAt: '2024-01-15T11:15:00Z',
|
|
chapters: 4,
|
|
actors: ['Sarah Chen', 'Dr. Marcus Webb'],
|
|
thumbnail: 'https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=300',
|
|
},
|
|
{
|
|
id: 2,
|
|
title: 'Marketing Campaign Video',
|
|
status: 'processing',
|
|
progress: 65,
|
|
duration: '1:30',
|
|
createdAt: '2024-01-14T15:20:00Z',
|
|
completedAt: null,
|
|
chapters: 3,
|
|
actors: ['Alex Rivera'],
|
|
thumbnail: 'https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=300',
|
|
},
|
|
{
|
|
id: 3,
|
|
title: 'Educational Content Series',
|
|
status: 'completed',
|
|
progress: 100,
|
|
duration: '5:20',
|
|
createdAt: '2024-01-12T09:00:00Z',
|
|
completedAt: '2024-01-12T10:30:00Z',
|
|
chapters: 6,
|
|
actors: ['Dr. Marcus Webb', 'Sarah Chen'],
|
|
thumbnail: 'https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=300',
|
|
},
|
|
{
|
|
id: 4,
|
|
title: 'Company Introduction',
|
|
status: 'failed',
|
|
progress: 0,
|
|
duration: '0:00',
|
|
createdAt: '2024-01-10T14:45:00Z',
|
|
completedAt: null,
|
|
chapters: 2,
|
|
actors: ['Sarah Chen'],
|
|
thumbnail: null,
|
|
},
|
|
{
|
|
id: 5,
|
|
title: 'Quarterly Report Presentation',
|
|
status: 'processing',
|
|
progress: 25,
|
|
duration: '3:15',
|
|
createdAt: '2024-01-09T11:00:00Z',
|
|
completedAt: null,
|
|
chapters: 5,
|
|
actors: ['Dr. Marcus Webb', 'Alex Rivera'],
|
|
thumbnail: 'https://images.pexels.com/photos/3184291/pexels-photo-3184291.jpeg?auto=compress&cs=tinysrgb&w=300',
|
|
},
|
|
];
|
|
|
|
const statusConfig = {
|
|
completed: {
|
|
icon: CheckCircle,
|
|
color: 'text-green-600',
|
|
bgColor: 'bg-green-100 dark:bg-green-900/20',
|
|
label: 'Completed',
|
|
},
|
|
processing: {
|
|
icon: RefreshCw,
|
|
color: 'text-blue-600',
|
|
bgColor: 'bg-blue-100 dark:bg-blue-900/20',
|
|
label: 'Processing',
|
|
},
|
|
failed: {
|
|
icon: XCircle,
|
|
color: 'text-red-600',
|
|
bgColor: 'bg-red-100 dark:bg-red-900/20',
|
|
label: 'Failed',
|
|
},
|
|
};
|
|
|
|
export function HistoryPage() {
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [statusFilter, setStatusFilter] = useState<string>('all');
|
|
|
|
const filteredTasks = mockTasks.filter(task => {
|
|
const matchesSearch = task.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
task.actors.some(actor => actor.toLowerCase().includes(searchQuery.toLowerCase()));
|
|
const matchesStatus = statusFilter === 'all' || task.status === statusFilter;
|
|
return matchesSearch && matchesStatus;
|
|
});
|
|
|
|
const formatDate = (dateString: string) => {
|
|
return new Date(dateString).toLocaleString();
|
|
};
|
|
|
|
const getTimeSince = (dateString: string) => {
|
|
const now = new Date();
|
|
const date = new Date(dateString);
|
|
const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
|
|
|
|
if (diffInHours < 1) return 'Less than an hour ago';
|
|
if (diffInHours < 24) return `${diffInHours} hours ago`;
|
|
const diffInDays = Math.floor(diffInHours / 24);
|
|
return `${diffInDays} days ago`;
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">Task History</h1>
|
|
<p className="text-muted-foreground">
|
|
Track the progress and manage your video generation tasks
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
|
<Input
|
|
placeholder="Search tasks..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-10 w-64"
|
|
/>
|
|
</div>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline">
|
|
<Filter className="mr-2 h-4 w-4" />
|
|
Status: {statusFilter === 'all' ? 'All' : statusConfig[statusFilter as keyof typeof statusConfig]?.label}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuItem onClick={() => setStatusFilter('all')}>
|
|
All Status
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setStatusFilter('completed')}>
|
|
Completed
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setStatusFilter('processing')}>
|
|
Processing
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setStatusFilter('failed')}>
|
|
Failed
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
<Badge variant="secondary">
|
|
{filteredTasks.length} tasks
|
|
</Badge>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Tasks List */}
|
|
<div className="space-y-4">
|
|
{filteredTasks.map((task) => {
|
|
const StatusIcon = statusConfig[task.status as keyof typeof statusConfig].icon;
|
|
const statusProps = statusConfig[task.status as keyof typeof statusConfig];
|
|
|
|
return (
|
|
<Card key={task.id} className="hover:shadow-md transition-shadow">
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center space-x-4">
|
|
{/* Thumbnail */}
|
|
<div className="relative w-24 h-16 bg-muted rounded-lg overflow-hidden flex-shrink-0">
|
|
{task.thumbnail ? (
|
|
<img
|
|
src={task.thumbnail}
|
|
alt={task.title}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center">
|
|
<span className="text-2xl">🎬</span>
|
|
</div>
|
|
)}
|
|
{task.status === 'processing' && (
|
|
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
|
|
<RefreshCw className="h-4 w-4 text-white animate-spin" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Task Info */}
|
|
<div className="flex-1 space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="font-semibold text-lg">{task.title}</h3>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
{task.status === 'completed' && (
|
|
<>
|
|
<DropdownMenuItem>
|
|
<Play className="mr-2 h-4 w-4" />
|
|
Preview
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem>
|
|
<Download className="mr-2 h-4 w-4" />
|
|
Download
|
|
</DropdownMenuItem>
|
|
</>
|
|
)}
|
|
{task.status === 'failed' && (
|
|
<DropdownMenuItem>
|
|
<RefreshCw className="mr-2 h-4 w-4" />
|
|
Retry
|
|
</DropdownMenuItem>
|
|
)}
|
|
<DropdownMenuItem>
|
|
<Eye className="mr-2 h-4 w-4" />
|
|
View Details
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem className="text-destructive">
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-4 text-sm">
|
|
<Badge
|
|
className={`${statusProps.bgColor} ${statusProps.color} border-none`}
|
|
>
|
|
<StatusIcon className="mr-1 h-3 w-3" />
|
|
{statusProps.label}
|
|
</Badge>
|
|
<div className="flex items-center text-muted-foreground">
|
|
<Clock className="mr-1 h-3 w-3" />
|
|
{task.duration}
|
|
</div>
|
|
<span className="text-muted-foreground">
|
|
{task.chapters} chapters
|
|
</span>
|
|
<span className="text-muted-foreground">
|
|
{task.actors.join(', ')}
|
|
</span>
|
|
</div>
|
|
|
|
{task.status === 'processing' && (
|
|
<div className="space-y-1">
|
|
<div className="flex justify-between text-xs text-muted-foreground">
|
|
<span>Processing...</span>
|
|
<span>{task.progress}%</span>
|
|
</div>
|
|
<Progress value={task.progress} className="h-1" />
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
<span>Started {getTimeSince(task.createdAt)}</span>
|
|
{task.completedAt && (
|
|
<span>Completed {formatDate(task.completedAt)}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{filteredTasks.length === 0 && (
|
|
<Card className="text-center py-12">
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
<div className="mx-auto w-16 h-16 rounded-full bg-muted flex items-center justify-center">
|
|
<Clock className="h-8 w-8 text-muted-foreground" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<h3 className="text-xl font-semibold">No tasks found</h3>
|
|
<p className="text-muted-foreground">
|
|
{searchQuery || statusFilter !== 'all'
|
|
? 'No tasks match your current filters. Try adjusting your search.'
|
|
: 'You haven\'t created any videos yet. Start by creating your first AI video!'
|
|
}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
} |