video-flow-b/components/workflow/input-script-step.tsx
2025-06-23 20:47:46 +08:00

416 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { ArrowRight, Sparkles, Users, FileText, Play, Pause, RefreshCw, Palette, Volume2 } from 'lucide-react';
import { Progress } from '@/components/ui/progress';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { motion } from 'framer-motion';
interface InputScriptStepProps {
onNext: () => void;
}
interface Character {
id: string;
name: string;
description: string;
personality: string;
appearance: string;
voice: string;
avatar: string;
fullBodyImage: string;
audioSample: string;
styles: string[];
currentStyle: number;
}
const aiModels = [
{ id: 'gpt-4', name: 'GPT-4 Turbo', description: 'Most advanced model with superior creativity' },
{ id: 'gpt-3.5', name: 'GPT-3.5 Turbo', description: 'Fast and efficient for most tasks' },
{ id: 'claude-3', name: 'Claude 3 Opus', description: 'Excellent for narrative and storytelling' },
];
const loadingSteps = [
{ text: "分析脚本内容...", progress: 20 },
{ text: "提取角色信息...", progress: 40 },
{ text: "生成角色形象...", progress: 60 },
{ text: "匹配音色特征...", progress: 80 },
{ text: "完成角色创建...", progress: 100 },
];
const mockCharacters: Character[] = [
{
id: "1",
name: "凤青楗",
description: "重生的凤凰,拥有强大的意志力,决心改变自己的命运",
personality: "坚强、勇敢、充满希望",
appearance: "优雅的凤凰形象,金色羽毛,炯炯有神的眼睛",
voice: "温暖而坚定的女声",
avatar: "https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=300",
fullBodyImage: "https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=400",
audioSample: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav",
styles: [
"https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=300",
"https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=300",
"https://images.pexels.com/photos/1222271/pexels-photo-1222271.jpeg?auto=compress&cs=tinysrgb&w=300"
],
currentStyle: 0
},
{
id: "2",
name: "星光使者",
description: "掌控星辰力量的神秘角色,与凤青楗一同战斗",
personality: "智慧、冷静、神秘",
appearance: "星光环绕的身影,深邃的蓝色长袍",
voice: "低沉磁性的男声",
avatar: "https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=300",
fullBodyImage: "https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=400",
audioSample: "https://www.soundjay.com/misc/sounds/bell-ringing-05.wav",
styles: [
"https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=300",
"https://images.pexels.com/photos/1222271/pexels-photo-1222271.jpeg?auto=compress&cs=tinysrgb&w=300",
"https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=300"
],
currentStyle: 0
}
];
// 新的Loading组件
const CharacterLoading = ({ step }: { step: string }) => {
return (
<div className="flex flex-col items-center justify-center min-h-[300px] bg-gradient-to-b from-gray-900 to-black text-white relative overflow-hidden rounded-xl">
{/* 旋转粒子环 */}
<motion.div
className="absolute w-40 h-40 border-2 border-cyan-400 rounded-full opacity-30"
animate={{ rotate: 360 }}
transition={{ repeat: Infinity, duration: 3, ease: 'linear' }}
/>
{/* 中心波动光圈 */}
<motion.div
className="absolute w-20 h-20 bg-cyan-500/10 rounded-full blur-xl"
animate={{
scale: [1, 1.2, 1],
opacity: [0.3, 0.6, 0.3]
}}
transition={{ repeat: Infinity, duration: 2 }}
/>
{/* 扫光线条 */}
<motion.div
className="absolute bottom-0 w-full h-1 bg-gradient-to-r from-transparent via-cyan-400 to-transparent blur"
animate={{ y: [-30, 300] }}
transition={{ repeat: Infinity, duration: 2, ease: 'easeInOut' }}
/>
{/* 核心文本 */}
<motion.div
className="relative z-10 mt-12 text-lg font-semibold text-cyan-300"
animate={{ opacity: [1, 0.4, 1] }}
transition={{ repeat: Infinity, duration: 1.5 }}
>
{step}
</motion.div>
</div>
);
};
// 角色卡片组件
const CharacterCard = ({
character,
onStyleChange,
onPlayAudio,
isPlaying
}: {
character: Character;
onStyleChange: (id: string, styleIndex: number) => void;
onPlayAudio: (id: string) => void;
isPlaying: string | null;
}) => (
<Card className="bg-gradient-to-br from-gray-800 to-gray-900 border-gray-600 overflow-hidden group hover:shadow-2xl transition-all duration-300 hover:scale-105">
<CardContent className="p-0">
{/* 角色头像区域 */}
<div className="relative">
<div className="aspect-[3/4] overflow-hidden bg-gradient-to-b from-blue-500/20 to-purple-500/20">
<img
src={character.fullBodyImage}
alt={character.name}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
/>
{/* 渐变遮罩 */}
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
{/* 角色名字 */}
<div className="absolute bottom-4 left-4 right-4">
<h3 className="text-white text-xl font-bold mb-2">{character.name}</h3>
<Badge variant="secondary" className="bg-blue-600/80 text-white">
{character.voice}
</Badge>
</div>
{/* 音频播放按钮 */}
<Button
size="sm"
variant="ghost"
className="absolute top-4 right-4 bg-black/50 hover:bg-black/70 text-white"
onClick={() => onPlayAudio(character.id)}
>
{isPlaying === character.id ? (
<Pause className="h-4 w-4" />
) : (
<Play className="h-4 w-4" />
)}
<Volume2 className="h-4 w-4 ml-1" />
</Button>
</div>
{/* 详细信息 */}
<div className="p-4 space-y-3">
<div>
<Label className="text-sm font-medium text-gray-300"></Label>
<p className="text-sm text-gray-400 mt-1">{character.description}</p>
</div>
<div>
<Label className="text-sm font-medium text-gray-300"></Label>
<p className="text-sm text-gray-400 mt-1">{character.personality}</p>
</div>
<div>
<Label className="text-sm font-medium text-gray-300"></Label>
<p className="text-sm text-gray-400 mt-1">{character.appearance}</p>
</div>
{/* 样式切换 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium text-gray-300"></Label>
<Button
size="sm"
variant="ghost"
className="text-blue-400 hover:text-blue-300"
onClick={() => onStyleChange(character.id, (character.currentStyle + 1) % character.styles.length)}
>
<Palette className="h-4 w-4 mr-1" />
</Button>
</div>
<div className="flex space-x-2">
{character.styles.map((style, index) => (
<button
key={index}
className={`w-12 h-12 rounded-lg overflow-hidden border-2 transition-all ${
character.currentStyle === index
? 'border-blue-500 shadow-lg'
: 'border-gray-600 hover:border-gray-500'
}`}
onClick={() => onStyleChange(character.id, index)}
>
<img src={style} alt={`Style ${index + 1}`} className="w-full h-full object-cover" />
</button>
))}
</div>
</div>
</div>
</div>
</CardContent>
</Card>
);
// 角色生成结果组件
const CharacterGenerationResult = ({
characters,
onStyleChange,
onPlayAudio,
isPlaying,
onContinue
}: {
characters: Character[];
onStyleChange: (id: string, styleIndex: number) => void;
onPlayAudio: (id: string) => void;
isPlaying: string | null;
onContinue: () => void;
}) => (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-blue-900 to-purple-900 p-6">
<div className="max-w-7xl mx-auto space-y-8">
{/* 标题区域 */}
<div className="text-center space-y-4">
<div className="inline-flex items-center space-x-2 bg-gradient-to-r from-blue-600 to-purple-600 text-white px-6 py-3 rounded-full">
<Users className="h-5 w-5" />
<span className="font-medium"></span>
<Sparkles className="h-5 w-5" />
</div>
<h1 className="text-3xl font-bold text-white"></h1>
<p className="text-gray-300 max-w-2xl mx-auto">
AI已经根据您的脚本生成了{characters.length}
</p>
</div>
{/* 角色网格 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{characters.map((character) => (
<div key={character.id} className="transform transition-all duration-500 hover:-translate-y-2">
<CharacterCard
character={character}
onStyleChange={onStyleChange}
onPlayAudio={onPlayAudio}
isPlaying={isPlaying}
/>
</div>
))}
</div>
{/* 操作按钮 */}
<div className="flex justify-center space-x-4 pt-8">
<Button
variant="outline"
size="lg"
className="border-gray-600 text-gray-300 hover:bg-gray-800"
>
<RefreshCw className="mr-2 h-5 w-5" />
</Button>
<Button
size="lg"
onClick={onContinue}
className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
>
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
</div>
</div>
</div>
);
export function InputScriptStep({ onNext }: InputScriptStepProps) {
const [script, setScript] = useState('');
const [chapters, setChapters] = useState('4');
const [shots, setShots] = useState('8');
const [showActorsPanel, setShowActorsPanel] = useState(false);
const [isGenerating, setIsGenerating] = useState(true);
const [showCharacters, setShowCharacters] = useState(false);
const [loadingStep, setLoadingStep] = useState(0);
const [characters, setCharacters] = useState<Character[]>(mockCharacters);
const [playingAudio, setPlayingAudio] = useState<string | null>(null);
// 模拟生成过程
useEffect(() => {
if (isGenerating) {
const timer = setInterval(() => {
setLoadingStep((prev) => {
if (prev >= loadingSteps.length - 1) {
clearInterval(timer);
setTimeout(() => {
setIsGenerating(false);
setShowCharacters(true);
}, 500);
return prev;
}
return prev + 1;
});
}, 1500);
return () => clearInterval(timer);
}
}, [isGenerating]);
const handleSubmit = () => {
if (script.trim() && chapters) {
setIsGenerating(true);
setLoadingStep(0);
}
};
const handleStyleChange = (characterId: string, styleIndex: number) => {
setCharacters(prev =>
prev.map(char =>
char.id === characterId
? { ...char, currentStyle: styleIndex, fullBodyImage: char.styles[styleIndex] }
: char
)
);
};
const handlePlayAudio = (characterId: string) => {
if (playingAudio === characterId) {
setPlayingAudio(null);
} else {
setPlayingAudio(characterId);
// 模拟音频播放3秒后自动停止
setTimeout(() => setPlayingAudio(null), 3000);
}
};
const handleContinue = () => {
setShowCharacters(false);
onNext();
};
// 显示角色生成结果
if (showCharacters) {
return (
<CharacterGenerationResult
characters={characters}
onStyleChange={handleStyleChange}
onPlayAudio={handlePlayAudio}
isPlaying={playingAudio}
onContinue={handleContinue}
/>
);
}
// 原始的脚本输入界面
return (
<div className="space-y-6">
<Card>
<CardContent className="space-y-6">
{/* Script Input */}
<div className="space-y-3">
<Label htmlFor="script" className="text-base font-medium">
Your Script
</Label>
<Textarea
id="script"
placeholder="Paste your script here... The AI will analyze it and break it into chapters with suggested actors and scenes."
value={script}
onChange={(e) => setScript(e.target.value)}
className="min-h-[200px] resize-none"
/>
<div className="flex justify-between text-sm text-muted-foreground">
<span>{script.length} characters</span>
</div>
</div>
</CardContent>
</Card>
{/* Loading Animation - 显示在输入框下方 */}
{isGenerating && (
<CharacterLoading step={loadingSteps[loadingStep].text} />
)}
{/* Action Buttons */}
<div className="flex justify-end">
<Button
onClick={handleSubmit}
disabled={!script.trim() || isGenerating}
size="lg"
>
<Sparkles className="mr-2 h-4 w-4" />
Generate
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
);
}