forked from 77media/video-flow
360 lines
13 KiB
TypeScript
360 lines
13 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 { Textarea } from '@/components/ui/textarea';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from '@/components/ui/dialog';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import {
|
|
Plus,
|
|
Upload,
|
|
Search,
|
|
MoreHorizontal,
|
|
Play,
|
|
Pause,
|
|
Wand2,
|
|
Edit,
|
|
Trash2,
|
|
Volume2,
|
|
User,
|
|
} from 'lucide-react';
|
|
|
|
const mockActors = [
|
|
{
|
|
id: 1,
|
|
name: 'Sarah Chen',
|
|
description: 'Professional corporate presenter with clear articulation',
|
|
avatar: 'https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg?auto=compress&cs=tinysrgb&w=200',
|
|
voice: {
|
|
type: 'generated',
|
|
name: 'Professional Female',
|
|
sample: 'Hello, I\'m Sarah and I\'ll be your guide through this presentation.',
|
|
},
|
|
tags: ['corporate', 'professional', 'female'],
|
|
createdAt: '2024-01-15',
|
|
usageCount: 12,
|
|
},
|
|
{
|
|
id: 2,
|
|
name: 'Dr. Marcus Webb',
|
|
description: 'Expert educator with authoritative voice for technical content',
|
|
avatar: 'https://images.pexels.com/photos/1222271/pexels-photo-1222271.jpeg?auto=compress&cs=tinysrgb&w=200',
|
|
voice: {
|
|
type: 'generated',
|
|
name: 'Expert Male',
|
|
sample: 'Welcome to today\'s lesson on advanced artificial intelligence.',
|
|
},
|
|
tags: ['education', 'expert', 'male'],
|
|
createdAt: '2024-01-12',
|
|
usageCount: 8,
|
|
},
|
|
{
|
|
id: 3,
|
|
name: 'Alex Rivera',
|
|
description: 'Energetic host perfect for engaging, casual content',
|
|
avatar: 'https://images.pexels.com/photos/1239291/pexels-photo-1239291.jpeg?auto=compress&cs=tinysrgb&w=200',
|
|
voice: {
|
|
type: 'uploaded',
|
|
name: 'Custom Voice',
|
|
sample: 'Hey everyone! Ready to dive into something amazing?',
|
|
},
|
|
tags: ['casual', 'energetic', 'neutral'],
|
|
createdAt: '2024-01-10',
|
|
usageCount: 5,
|
|
},
|
|
];
|
|
|
|
const voiceTypes = [
|
|
'Professional Female',
|
|
'Professional Male',
|
|
'Casual Female',
|
|
'Casual Male',
|
|
'Expert Female',
|
|
'Expert Male',
|
|
'Enthusiastic Neutral',
|
|
];
|
|
|
|
export function ActorsLibraryPage() {
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [playingVoice, setPlayingVoice] = useState<number | null>(null);
|
|
const [newActorName, setNewActorName] = useState('');
|
|
const [newActorDescription, setNewActorDescription] = useState('');
|
|
const [newActorPrompt, setNewActorPrompt] = useState('');
|
|
|
|
const filteredActors = mockActors.filter(actor =>
|
|
actor.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
actor.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
actor.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
);
|
|
|
|
const toggleVoicePlayback = (actorId: number) => {
|
|
setPlayingVoice(playingVoice === actorId ? null : actorId);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">Actors Library</h1>
|
|
<p className="text-muted-foreground">
|
|
Create and manage AI actors with custom voices for your videos
|
|
</p>
|
|
</div>
|
|
<Dialog>
|
|
<DialogTrigger asChild>
|
|
<Button>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Create Actor
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>Create New Actor</DialogTitle>
|
|
</DialogHeader>
|
|
<Tabs defaultValue="image" className="space-y-4">
|
|
<TabsList className="grid w-full grid-cols-2">
|
|
<TabsTrigger value="image">Upload Image</TabsTrigger>
|
|
<TabsTrigger value="ai">AI Generated</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="image" className="space-y-4">
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-center w-full">
|
|
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-muted-foreground/25 rounded-lg cursor-pointer hover:bg-muted/50">
|
|
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
|
<Upload className="w-8 h-8 mb-3 text-muted-foreground" />
|
|
<p className="mb-2 text-sm text-muted-foreground">
|
|
<span className="font-semibold">Click to upload</span> or drag and drop
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">PNG, JPG up to 10MB</p>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="ai" className="space-y-4">
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="text-sm font-medium">AI Generation Prompt</label>
|
|
<Textarea
|
|
placeholder="Describe the appearance of your actor (e.g., professional woman in her 30s with short brown hair, wearing business attire)"
|
|
value={newActorPrompt}
|
|
onChange={(e) => setNewActorPrompt(e.target.value)}
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
<Button className="w-full">
|
|
<Wand2 className="mr-2 h-4 w-4" />
|
|
Generate Actor Image
|
|
</Button>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="text-sm font-medium">Actor Name</label>
|
|
<Input
|
|
placeholder="Enter actor name"
|
|
value={newActorName}
|
|
onChange={(e) => setNewActorName(e.target.value)}
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium">Description</label>
|
|
<Textarea
|
|
placeholder="Describe the actor's role and personality"
|
|
value={newActorDescription}
|
|
onChange={(e) => setNewActorDescription(e.target.value)}
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end space-x-2">
|
|
<Button variant="outline">Cancel</Button>
|
|
<Button>Create Actor</Button>
|
|
</div>
|
|
</Tabs>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
|
|
{/* Search */}
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
|
<Input
|
|
placeholder="Search actors by name, description, or tags..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
<Badge variant="secondary">
|
|
{filteredActors.length} actors
|
|
</Badge>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Actors Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{filteredActors.map((actor) => (
|
|
<Card key={actor.id} className="group hover:shadow-lg transition-all duration-200">
|
|
<CardHeader>
|
|
<div className="flex items-center space-x-4">
|
|
<Avatar className="h-16 w-16">
|
|
<AvatarImage src={actor.avatar} alt={actor.name} />
|
|
<AvatarFallback>
|
|
<User className="h-8 w-8" />
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex-1">
|
|
<CardTitle className="text-lg">{actor.name}</CardTitle>
|
|
<p className="text-sm text-muted-foreground line-clamp-2">
|
|
{actor.description}
|
|
</p>
|
|
</div>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem>
|
|
<Edit className="mr-2 h-4 w-4" />
|
|
Edit
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem>
|
|
Duplicate
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem className="text-destructive">
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Tags */}
|
|
<div className="flex flex-wrap gap-1">
|
|
{actor.tags.map((tag) => (
|
|
<Badge key={tag} variant="outline" className="text-xs">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
|
|
{/* Voice Section */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<Volume2 className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-sm font-medium">Voice</span>
|
|
</div>
|
|
<Badge
|
|
variant={actor.voice.type === 'generated' ? 'secondary' : 'outline'}
|
|
className="text-xs"
|
|
>
|
|
{actor.voice.type === 'generated' ? 'AI Generated' : 'Custom'}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="bg-muted p-3 rounded-lg">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-sm font-medium">{actor.voice.name}</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => toggleVoicePlayback(actor.id)}
|
|
>
|
|
{playingVoice === actor.id ? (
|
|
<Pause className="h-4 w-4" />
|
|
) : (
|
|
<Play className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground italic">
|
|
"{actor.voice.sample}"
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex space-x-2">
|
|
<Button variant="outline" size="sm" className="flex-1">
|
|
<Upload className="mr-1 h-3 w-3" />
|
|
Upload Voice
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="flex-1">
|
|
<Wand2 className="mr-1 h-3 w-3" />
|
|
Generate New
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="flex items-center justify-between text-sm text-muted-foreground pt-2 border-t">
|
|
<span>Used in {actor.usageCount} videos</span>
|
|
<span>Created {new Date(actor.createdAt).toLocaleDateString()}</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{filteredActors.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">
|
|
<User className="h-8 w-8 text-muted-foreground" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<h3 className="text-xl font-semibold">No actors found</h3>
|
|
<p className="text-muted-foreground">
|
|
{searchQuery
|
|
? `No actors match "${searchQuery}". Try a different search term.`
|
|
: 'Get started by creating your first AI actor'
|
|
}
|
|
</p>
|
|
</div>
|
|
{!searchQuery && (
|
|
<Dialog>
|
|
<DialogTrigger asChild>
|
|
<Button>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Create Your First Actor
|
|
</Button>
|
|
</DialogTrigger>
|
|
</Dialog>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
} |