forked from 77media/video-flow
226 lines
9.2 KiB
TypeScript
226 lines
9.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from 'react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { ArrowLeft, ArrowRight, Edit, RefreshCw, Users, Play, Plus, X } from 'lucide-react';
|
|
|
|
interface GenerateChaptersStepProps {
|
|
onNext: () => void;
|
|
onPrevious: () => void;
|
|
}
|
|
|
|
const mockChapters = [
|
|
{
|
|
id: 1,
|
|
title: '夜空烟花秀:璀璨瞬间',
|
|
content: '有没有想过,为什么烟花总能让我们突得那么开心?(停顿)当五颜六色的烟花在空中绽放时,人们会聚集在一起,眼睛睁得大大的。(停顿)每一次闪耀,每一次炸呼——都充满了魔力!(停顿)和朋友们一起在星空下看烟花,这许愿就是无与伦比的。',
|
|
duration: '45s',
|
|
selectedActors: [1],
|
|
},
|
|
{
|
|
id: 2,
|
|
title: 'Core Concepts',
|
|
content: 'Let\'s dive into the core concepts of artificial intelligence. We\'ll break down complex topics into digestible pieces that anyone can understand.',
|
|
duration: '1m 20s',
|
|
selectedActors: [2],
|
|
},
|
|
{
|
|
id: 3,
|
|
title: 'Practical Applications',
|
|
content: 'Now we\'ll examine real-world applications of AI technology across various industries and how they\'re transforming our daily lives.',
|
|
duration: '1m 15s',
|
|
selectedActors: [1, 3],
|
|
},
|
|
{
|
|
id: 4,
|
|
title: 'Future Outlook',
|
|
content: 'Finally, let\'s look ahead to the future of AI and what exciting developments we can expect in the coming years.',
|
|
duration: '50s',
|
|
selectedActors: [3],
|
|
},
|
|
];
|
|
|
|
const availableActors = [
|
|
{
|
|
id: 1,
|
|
name: 'Liu Wei 1.10x',
|
|
voice: 'Professional Female',
|
|
avatar: 'https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg?auto=compress&cs=tinysrgb&w=100',
|
|
},
|
|
{
|
|
id: 2,
|
|
name: 'Dr. Marcus Webb',
|
|
voice: 'Expert Male',
|
|
avatar: 'https://images.pexels.com/photos/1222271/pexels-photo-1222271.jpeg?auto=compress&cs=tinysrgb&w=100',
|
|
},
|
|
{
|
|
id: 3,
|
|
name: 'Alex Rivera',
|
|
voice: 'Enthusiastic Neutral',
|
|
avatar: 'https://images.pexels.com/photos/1239291/pexels-photo-1239291.jpeg?auto=compress&cs=tinysrgb&w=100',
|
|
},
|
|
{
|
|
id: 4,
|
|
name: 'Lisa Park',
|
|
voice: 'Friendly Female',
|
|
avatar: 'https://images.pexels.com/photos/1181686/pexels-photo-1181686.jpeg?auto=compress&cs=tinysrgb&w=100',
|
|
},
|
|
];
|
|
|
|
export function GenerateChaptersStep({ onNext, onPrevious }: GenerateChaptersStepProps) {
|
|
const [chapters, setChapters] = useState(mockChapters);
|
|
const [editingChapter, setEditingChapter] = useState<number | null>(null);
|
|
|
|
const handleChapterEdit = (chapterId: number, content: string) => {
|
|
setChapters(chapters.map(ch =>
|
|
ch.id === chapterId ? { ...ch, content } : ch
|
|
));
|
|
setEditingChapter(null);
|
|
};
|
|
|
|
const handleActorToggle = (chapterId: number, actorId: number) => {
|
|
setChapters(chapters.map(ch => {
|
|
if (ch.id === chapterId) {
|
|
const isSelected = ch.selectedActors.includes(actorId);
|
|
const newSelectedActors = isSelected
|
|
? ch.selectedActors.filter(id => id !== actorId)
|
|
: [...ch.selectedActors, actorId];
|
|
return { ...ch, selectedActors: newSelectedActors };
|
|
}
|
|
return ch;
|
|
}));
|
|
};
|
|
|
|
const getSelectedActors = (chapterId: number) => {
|
|
const chapter = chapters.find(ch => ch.id === chapterId);
|
|
if (!chapter) return [];
|
|
return availableActors.filter(actor => chapter.selectedActors.includes(actor.id));
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<Users className="h-5 w-5" />
|
|
<span>Generated Chapters & Actor Assignment</span>
|
|
</div>
|
|
<Badge variant="secondary">
|
|
{chapters.length} Chapters
|
|
</Badge>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="h-[calc(100vh-18rem)] overflow-y-auto hide-scrollbar">
|
|
|
|
<div className="space-y-6">
|
|
{chapters.map((chapter, index) => (
|
|
<Card key={chapter.id} className="border-l-4 border-l-primary/20">
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-3">
|
|
<h3 className="font-semibold text-lg">Chapter {index + 1}: {chapter.title}</h3>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Left side - Chapter Content */}
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<h4 className="text-sm font-medium text-muted-foreground">Chapter Content</h4>
|
|
{editingChapter === chapter.id ? (
|
|
<div className="space-y-3">
|
|
<Textarea
|
|
value={chapter.content}
|
|
onChange={(e) => {
|
|
const updatedContent = e.target.value;
|
|
setChapters(chapters.map(ch =>
|
|
ch.id === chapter.id ? { ...ch, content: updatedContent } : ch
|
|
));
|
|
}}
|
|
onBlur={() => handleChapterEdit(chapter.id, chapter.content)}
|
|
className="min-h-[120px] text-sm"
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div
|
|
className="text-sm leading-relaxed bg-muted/50 p-4 rounded-lg border cursor-pointer hover:bg-muted/70 transition-colors"
|
|
onClick={() => setEditingChapter(chapter.id)}
|
|
>
|
|
{chapter.content}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right side - Actor Selection */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h4 className="text-sm font-medium text-muted-foreground">Select Actors</h4>
|
|
<Badge variant="outline" className="text-xs">
|
|
{getSelectedActors(chapter.id).length} selected
|
|
</Badge>
|
|
</div>
|
|
|
|
{/* Selected Actors Display */}
|
|
{getSelectedActors(chapter.id).length > 0 && (
|
|
<div className="space-y-2">
|
|
<div className="text-xs font-medium text-muted-foreground">Selected:</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{getSelectedActors(chapter.id).map((actor) => (
|
|
<div
|
|
key={actor.id}
|
|
className="flex items-center space-x-2 bg-primary/10 text-primary px-3 py-1 rounded-full text-xs"
|
|
>
|
|
<Avatar className="h-5 w-5">
|
|
<AvatarImage src={actor.avatar} />
|
|
<AvatarFallback className="text-xs">
|
|
{actor.name.split(' ').map(n => n[0]).join('')}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<span>{actor.name}</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-4 w-4 p-0 hover:bg-destructive/20"
|
|
onClick={() => handleActorToggle(chapter.id, actor.id)}
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center">
|
|
<Plus className="h-4 w-4" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex justify-between">
|
|
<Button variant="outline" onClick={onPrevious}>
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
Back to Script
|
|
</Button>
|
|
<Button onClick={onNext}>
|
|
Generate Scenes
|
|
<ArrowRight className="ml-2 h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |