video-flow-b/components/pages/create-to-video.tsx
2025-06-26 20:12:55 +08:00

1031 lines
53 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, useRef } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { ArrowLeft, ChevronDown, ChevronUp, Video, ListOrdered, Play, Loader2, Pause, MoreHorizontal, Edit2, Check, X, RefreshCw } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, SheetClose } from "@/components/ui/sheet";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import './style/create-to-video.css';
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { Skeleton } from "@/components/ui/skeleton";
// 添加自定义滚动条样式
const scrollbarStyles = `
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 2px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
`;
interface SceneVideo {
id: number;
video_url: string;
script: any;
}
export function CreateToVideo() {
const router = useRouter();
const [isUploading, setIsUploading] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [videoUrl, setVideoUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [loadingText, setLoadingText] = useState('Generating...');
const [isPaused, setIsPaused] = useState(false);
const [showMoreSettings, setShowMoreSettings] = useState(false);
const [generateObj, setGenerateObj] = useState<any>({
scripts: null,
frame_urls: null,
video_info: null,
scene_videos: null,
cut_video_url: null,
audio_video_url: null,
final_video_url: null
});
const [showAllFrames, setShowAllFrames] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const [showScrollNav, setShowScrollNav] = useState(false);
const [selectedVideoIndex, setSelectedVideoIndex] = useState<number | null>(null);
const videosContainerRef = useRef<HTMLDivElement>(null);
const scriptsContainerRef = useRef<HTMLDivElement>(null);
const [activeTab, setActiveTab] = useState('clone');
const [editingField, setEditingField] = useState<{
type: 'shot' | 'frame' | 'atmosphere' | null;
value: string;
}>({ type: null, value: '' });
const [alternativeVideos, setAlternativeVideos] = useState<{ [key: number]: string[] }>({
0: [
'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4',
],
1: [
'https://cdn.qikongjian.com/videos/1750384661_d8e30b79-828e-48cd-9025-ab62a996717c_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750320040_4b47996e-7c70-490e-8433-80c7df990fdd_text_to_video_0.mp4',
]
});
const [currentVideoIndex, setCurrentVideoIndex] = useState<{ [key: number]: number }>({});
const [volume, setVolume] = useState<{ [key: number]: number }>({});
const [transition, setTransition] = useState<{ [key: number]: string }>({});
const [buttonPosition, setButtonPosition] = useState({ x: window.innerWidth - 100, y: window.innerHeight / 2 });
const [isDragging, setIsDragging] = useState(false);
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
const buttonRef = useRef<HTMLDivElement>(null);
// 计算每行可以显示的图片数量基于图片高度100px和容器宽度
const imagesPerRow = Math.floor(1080 / (100 * 16/9 + 8)); // 假设图片宽高比16:9间距8px
// 计算三行可以显示的最大图片数量
const maxVisibleImages = imagesPerRow * 3;
// 生成分镜视频后默认选中第一个
useEffect(() => {
if (generateObj.scene_videos?.length > 0 && selectedVideoIndex === null) {
setSelectedVideoIndex(0);
}
}, [generateObj.scene_videos]);
// 处理暂停/继续
const handlePauseResume = () => {
setIsPaused(!isPaused);
// TODO: 实现具体的暂停/继续逻辑
};
const handleUploadVideo = () => {
console.log('upload video');
// 打开文件选择器
const input = document.createElement('input');
input.type = 'file';
input.accept = 'video/*';
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
setVideoUrl(URL.createObjectURL(file));
}
}
input.click();
}
const generateSences = async () => {
try {
generateObj.scene_videos = [];
const videoUrls = [
'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750384661_d8e30b79-828e-48cd-9025-ab62a996717c_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750320040_4b47996e-7c70-490e-8433-80c7df990fdd_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750303377_8c3c4ca6-c4ea-4376-8583-de3afa5681d8_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4'
];
setGenerateObj({...generateObj, scene_videos: []});
// 使用 Promise.all 和 Array.map 来处理异步操作
const promises = generateObj.scripts.map((element: any, index: number) => {
return new Promise<void>((resolveVideo) => {
setTimeout(() => {
generateObj.scene_videos.push({
id: index,
video_url: videoUrls[index],
script: element
});
setGenerateObj({...generateObj});
setLoadingText(`生成第 ${index + 1} 个分镜视频...`);
resolveVideo();
}, index * 3000); // 每个视频间隔3秒
});
});
// 等待所有视频生成完成
await Promise.all(promises);
} catch (error) {
console.error('生成分镜视频失败:', error);
throw error;
}
}
const handleCreateVideo = async () => {
try {
// 清空所有数据
setGenerateObj({
scripts: null,
frame_urls: null,
video_info: null,
scene_videos: null,
cut_video_url: null,
audio_video_url: null,
final_video_url: null
});
console.log('create video');
setIsLoading(true);
setIsExpanded(true);
// 提取帧
await new Promise(resolve => setTimeout(resolve, 3000));
setLoadingText('提取帧...');
// 生成帧
await new Promise(resolve => setTimeout(resolve, 3000));
generateObj.frame_urls = [
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000000.jpg/1750507510_tmphfb431oc_000000.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000001.jpg/1750507511_tmphfb431oc_000001.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000002.jpg/1750507511_tmphfb431oc_000002.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000003.jpg/1750507511_tmphfb431oc_000003.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000004.jpg/1750507512_tmphfb431oc_000004.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000005.jpg/1750507512_tmphfb431oc_000005.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000006.jpg/1750507513_tmphfb431oc_000006.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000007.jpg/1750507514_tmphfb431oc_000007.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000008.jpg/1750507515_tmphfb431oc_000008.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000009.jpg/1750507515_tmphfb431oc_000009.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000010.jpg/1750507516_tmphfb431oc_000010.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000011.jpg/1750507516_tmphfb431oc_000011.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000012.jpg/1750507516_tmphfb431oc_000012.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000013.jpg/1750507517_tmphfb431oc_000013.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000014.jpg/1750507517_tmphfb431oc_000014.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000015.jpg/1750507517_tmphfb431oc_000015.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000016.jpg/1750507517_tmphfb431oc_000016.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000017.jpg/1750507517_tmphfb431oc_000017.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000018.jpg/1750507518_tmphfb431oc_000018.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000019.jpg/1750507520_tmphfb431oc_000019.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000020.jpg/1750507521_tmphfb431oc_000020.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000021.jpg/1750507523_tmphfb431oc_000021.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000022.jpg/1750507523_tmphfb431oc_000022.jpg",
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000023.jpg/1750507524_tmphfb431oc_000023.jpg",
];
setGenerateObj({...generateObj});
setLoadingText('分析视频...');
// 生成视频信息
await new Promise(resolve => setTimeout(resolve, 6000));
generateObj.video_info = {
roles: [
{
name: '雪 (YUKI)',
core_identity: '一位接近二十岁或二十出头的年轻女性,东亚裔,拥有深色长发和刘海,五官柔和且富有表现力。',
avatar: 'https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000000.jpg/1750507510_tmphfb431oc_000000.jpg',
},{
name: '春 (HARU)',
core_identity: '一位接近二十岁或二十出头的年轻男性,东亚裔,拥有深色、发型整洁的中长发和深思的气质。',
avatar: 'https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000000.jpg/1750507510_tmphfb431oc_000000.jpg',
}
],
sence: '叙事在两个不同的时间段展开。现在时空设定在一个安静的乡下小镇,冬季时被大雪覆盖。记忆则设定在春夏两季,一个日本高中的校园内外,天气温暖而晴朗。',
style: '电影感照片般逼真带有柔和、梦幻般的质感。其美学风格让人联想到日本的浪漫剧情片。现在时空的场景使用冷色、蓝色调的调色板而记忆序列则沐浴在温暖的黄金时刻光晕中。大量使用浅景深、微妙的镜头光晕以及平滑、富有情感的节奏。8K分辨率。'
};
setGenerateObj({...generateObj});
setLoadingText('提取分镜脚本...');
// 生成分镜脚本
await new Promise(resolve => setTimeout(resolve, 6000));
generateObj.scripts = [
{
shot: '面部特写,拉远至广角镜头。',
frame: '序列以雪YUKI面部的特写开场她闭着眼睛躺在一片纯净的雪地里。柔和的雪花轻轻飘落在她的黑发和苍白的皮肤上。摄像机缓慢拉远形成一幅令人惊叹的广角画面揭示出在黄昏时分她是在一片广阔、寂静、白雪覆盖的景观中的一个渺小、孤独的身影。',
atmosphere: '忧郁、宁静、寂静且寒冷。'
}, {
shot: '切至室内中景,随后是一个透过窗户的主观视角镜头。',
frame: '我们切到雪YUKI在一个舒适、光线温暖的卧室里醒来。她穿着一件舒适的毛衣从床上下来走向一扇窗户。她的呼吸在冰冷的玻璃上凝成雾气。她的主观视角镜头揭示了外面的雪景远处有一座红色的房子以及一封信被放入邮箱的记忆从而触发了一段闪回。',
atmosphere: '怀旧、温暖、内省。'
}, {
shot: '跟踪镜头,随后是一系列切出镜头和特写。',
frame: '记忆开始。一个跟踪镜头跟随着一群学生包括雪YUKI他们在飘落的樱花花瓣构成的华盖下走路上学。场景切到一个阳光普照的教室。雪YUKI穿着校服坐在她的课桌前害羞地瞥了一眼坐在前几排的春HARU。他仿佛感觉到她的凝视巧妙地转过头来。',
atmosphere: '充满青春气息、怀旧、温暖,带有一种萌芽的、未言明的浪漫感。'
}, {
shot: '静态中景,切到另一个中景,营造出共享空间的感觉。',
frame: '记忆转移到学校图书馆充满了金色的光束。一个中景镜头显示春HARU靠在一个书架上全神贯注地读一本书。然后摄像机切到雪YUKI她坐在附近的一张桌子旁专注于画架上的一幅小画当她感觉到他的存在时嘴唇上泛起一丝淡淡的、秘密的微笑。',
atmosphere: '平和、亲密、书卷气、温暖。'
}, {
shot: '雪YUKI的中景过渡到通过相机镜头的特写主观视角。',
frame: '在一个阳光明媚的运动日YUKI站在运动场边缘拿着一台老式相机。她举起相机镜头推进到她透过取景器看的眼睛的特写。我们切到她的主观视角除了站在运动场上、表情专注而坚定的春HARU之外整个世界都是失焦的。',
atmosphere: '充满活力、专注,一种投入的观察和遥远的钦佩感。'
},
];
setGenerateObj({...generateObj}); // 使用展开运算符创建新对象,确保触发更新
setLoadingText('生成分镜视频...');
// 生成分镜视频
await new Promise(resolve => setTimeout(resolve, 2000));
await generateSences();
setLoadingText('分镜剪辑...');
// 生成剪辑后的视频
await new Promise(resolve => setTimeout(resolve, 6000));
generateObj.cut_video_url = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4';
setGenerateObj({...generateObj});
setLoadingText('口型同步...');
// 口型同步后生成视频
await new Promise(resolve => setTimeout(resolve, 6000));
generateObj.audio_video_url = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4';
setGenerateObj({...generateObj});
setLoadingText('一致化处理...');
// 最终完成
await new Promise(resolve => setTimeout(resolve, 6000));
generateObj.final_video_url = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4';
setGenerateObj({...generateObj});
setLoadingText('完成');
setIsLoading(false);
} catch (error) {
console.error('视频生成过程出错:', error);
setLoadingText('生成失败,请重试');
setIsLoading(false);
}
}
// 处理视频选中
const handleVideoSelect = (index: number) => {
setSelectedVideoIndex(index);
// 滚动脚本到对应位置
const scriptElement = document.getElementById(`script-${index}`);
scriptElement?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
};
// 处理脚本选中
const handleScriptSelect = (index: number) => {
setSelectedVideoIndex(index);
// 滚动视频到对应位置
const videoElement = document.getElementById(`video-${index}`);
videoElement?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
};
// 处理脚本编辑
const handleEditScript = (type: 'shot' | 'frame' | 'atmosphere', value: string) => {
setEditingField({ type, value });
};
// 处理脚本点击
const handleScriptClick = (type: 'shot' | 'frame' | 'atmosphere', value: string) => {
if (editingField.type !== type) {
handleEditScript(type, value);
}
};
// 自动保存脚本编辑
useEffect(() => {
if (editingField.type && editingField.value && selectedVideoIndex !== null) {
const timeoutId = setTimeout(() => {
const updatedScripts = [...generateObj.scripts];
const fieldType = editingField.type as keyof typeof updatedScripts[number];
updatedScripts[selectedVideoIndex] = {
...updatedScripts[selectedVideoIndex],
[fieldType]: editingField.value
};
setGenerateObj({
...generateObj,
scripts: updatedScripts
});
}, 500); // 500ms 防抖
return () => clearTimeout(timeoutId);
}
}, [editingField.value, editingField.type, selectedVideoIndex, generateObj]);
// 刷新分镜脚本
const handleRefreshScript = () => {
if (selectedVideoIndex === null) return;
// TODO: 实现刷新脚本的逻辑
console.log('Refresh script for scene', selectedVideoIndex + 1);
};
// 处理删除分镜视频
const handleDeleteScene = (index: number) => {
if (generateObj.scene_videos) {
const newSceneVideos = generateObj.scene_videos.filter((_: SceneVideo, i: number) => i !== index);
setGenerateObj({
...generateObj,
scene_videos: newSceneVideos
});
setSelectedVideoIndex(null);
}
};
// 处理重新生成分镜视频
const handleRegenerateScene = async (index: number) => {
// TODO: 实现重新生成逻辑
console.log('Regenerate scene', index);
};
// 处理切换其他生成的视频
const handleSwitchVideo = (sceneIndex: number, videoIndex: number) => {
if (alternativeVideos[sceneIndex]?.[videoIndex]) {
const newSceneVideos = [...generateObj.scene_videos];
newSceneVideos[sceneIndex] = {
...newSceneVideos[sceneIndex],
video_url: alternativeVideos[sceneIndex][videoIndex]
};
setGenerateObj({
...generateObj,
scene_videos: newSceneVideos
});
setCurrentVideoIndex({
...currentVideoIndex,
[sceneIndex]: videoIndex
});
}
};
// 处理音量调节
const handleVolumeChange = (index: number, value: number) => {
setVolume({
...volume,
[index]: value
});
// TODO: 实现音量调节逻辑
};
// 处理转场设置
const handleTransitionChange = (index: number, value: string) => {
setTransition({
...transition,
[index]: value
});
// TODO: 实现转场效果逻辑
};
// 处理拖动开始
const handleDragStart = (e: React.MouseEvent) => {
setIsDragging(true);
setDragStart({
x: e.clientX - buttonPosition.x,
y: e.clientY - buttonPosition.y
});
};
// 处理拖动
const handleDrag = (e: MouseEvent) => {
if (isDragging && buttonRef.current) {
const newX = Math.min(
Math.max(0, e.clientX - dragStart.x),
window.innerWidth - buttonRef.current.offsetWidth
);
const newY = Math.min(
Math.max(0, e.clientY - dragStart.y),
window.innerHeight - buttonRef.current.offsetHeight
);
setButtonPosition({ x: newX, y: newY });
}
};
// 处理拖动结束
const handleDragEnd = () => {
setIsDragging(false);
};
// 添加拖动事件监听
useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', handleDrag);
window.addEventListener('mouseup', handleDragEnd);
}
return () => {
window.removeEventListener('mousemove', handleDrag);
window.removeEventListener('mouseup', handleDragEnd);
};
}, [isDragging, dragStart]);
return (
<div
ref={containerRef}
className="container mx-auto overflow-hidden custom-scrollbar"
style={isExpanded ? {height: 'calc(100vh - 12rem)'} : {height: 'calc(100vh - 20rem)'}}
>
{/* 展示创作详细过程 */}
{generateObj && (
<div className='video-creation-process-container'>
<ResizablePanelGroup
direction="vertical"
className="min-h-[800px] rounded-lg border bg-white/5 backdrop-blur-sm"
>
{/* 第一行 - 占2/3高度 */}
<ResizablePanel defaultSize={66}>
<ResizablePanelGroup direction="horizontal">
{/* 第一列 - 角色档案卡 */}
<ResizablePanel defaultSize={20}>
<div className="h-full flex flex-col">
<div className="p-4 border-b border-white/10">
<h3 className="text-base font-medium"></h3>
</div>
<div className="flex-1 overflow-y-auto p-4">
{generateObj.video_info ? (
<div className="space-y-4">
{generateObj.video_info.roles.map((role: any, index: number) => (
<div
key={index}
className='flex items-start gap-3 p-3 rounded-lg bg-white/[0.03] hover:bg-white/[0.05] transition-colors duration-200 group'
>
<div className='flex-shrink-0'>
<div className='w-[48px] h-[48px] rounded-full overflow-hidden border-2 border-white/10 group-hover:border-white/20 transition-colors duration-200'>
<img
src={role.avatar}
alt={role.name}
className='w-full h-full object-cover'
/>
</div>
</div>
<div className='flex-1 min-w-0'>
<div className='text-base font-medium mb-1 text-white/90'>{role.name}</div>
<div className='text-sm text-white/60 line-clamp-2'>{role.core_identity}</div>
</div>
</div>
))}
</div>
) : (
<div className="space-y-4">
{[1, 2].map((i) => (
<div key={i} className="p-3 rounded-lg bg-white/[0.03]">
<div className="flex items-start gap-3">
<Skeleton className="w-12 h-12 rounded-full" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-full" />
<Skeleton className="h-3 w-3/4" />
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</ResizablePanel>
<ResizableHandle withHandle />
{/* 第二列 - 视频预览和加载状态 */}
<ResizablePanel defaultSize={60}>
<div className="h-full flex flex-col">
<div className="p-4 border-b border-white/10">
<h3 className="text-base font-medium"></h3>
</div>
<div className="flex-1 overflow-y-auto">
<div className="h-full flex items-center justify-center bg-white/[0.03] rounded-lg">
{isLoading ? (
<div className='relative'>
<div className='w-[40px] h-[40px] rounded-full bg-white/[0.05] flex items-center justify-center animate-bounce'>
<div className='w-[20px] h-[20px] rounded-full bg-white/[0.05] flex items-center justify-center animate-pulse'>
<div className='w-[10px] h-[10px] rounded-full bg-white/[0.05] flex items-center justify-center animate-ping'>
</div>
</div>
</div>
<div className='absolute top-[50px] left-1/2 -translate-x-1/2 whitespace-nowrap'>
<span className='text-white/70 text-sm animate-pulse inline-block'>{loadingText}</span>
<span className='inline-block ml-1 animate-bounce'>
<span className='inline-block animate-bounce delay-100'>.</span>
<span className='inline-block animate-bounce delay-200'>.</span>
<span className='inline-block animate-bounce delay-300'>.</span>
</span>
</div>
</div>
) : generateObj.final_video_url ? (
<video
src={generateObj.final_video_url}
className="w-full h-full object-contain"
controls
/>
) : (
<div className="text-white/40">...</div>
)}
</div>
</div>
</div>
</ResizablePanel>
<ResizableHandle withHandle />
{/* 第三列 - 概要信息 */}
<ResizablePanel defaultSize={20}>
<div className="h-full flex flex-col">
<div className="p-4 border-b border-white/10">
<h3 className="text-base font-medium"></h3>
</div>
<div className="flex-1 overflow-y-auto p-4">
{generateObj.video_info ? (
<div className="space-y-6">
<div>
<span className='inline-block text-sm font-medium text-blue-400/90 mb-1'></span>
<p className='text-sm text-white/80 leading-relaxed'>{generateObj.video_info.sence}</p>
</div>
<div>
<span className='inline-block text-sm font-medium text-blue-400/90 mb-1'></span>
<p className='text-sm text-white/80 leading-relaxed'>{generateObj.video_info.style}</p>
</div>
</div>
) : (
<div className="space-y-6">
<div>
<Skeleton className="h-4 w-16 mb-2" />
<Skeleton className="h-20 w-full" />
</div>
<div>
<Skeleton className="h-4 w-16 mb-2" />
<Skeleton className="h-20 w-full" />
</div>
</div>
)}
</div>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle withHandle />
{/* 第二行 - 占1/3高度 */}
<ResizablePanel defaultSize={34}>
<ResizablePanelGroup direction="horizontal">
{/* 第一列 - 分镜脚本 */}
<ResizablePanel defaultSize={20}>
<div className="h-full flex flex-col">
<div className="p-4 border-b border-white/10 flex items-center justify-between">
<div className="flex items-center gap-2">
<h3 className="text-base font-medium"></h3>
{selectedVideoIndex !== null && (
<span className="px-2 py-1 rounded-full bg-blue-400/10 text-xs text-blue-400">
Scene {selectedVideoIndex + 1}
</span>
)}
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 hover:bg-white/5"
onClick={handleRefreshScript}
>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
<div className="flex-1 overflow-y-auto p-4">
{generateObj.scripts && selectedVideoIndex !== null ? (
<div className="space-y-6">
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className='text-sm font-medium text-blue-400/90'></span>
</div>
{editingField.type === 'shot' ? (
<Textarea
value={editingField.value}
onChange={(e) => setEditingField({ ...editingField, value: e.target.value })}
className="min-h-[80px] resize-none bg-transparent border-white/10 focus:border-white/20 focus-visible:ring-0 focus-visible:ring-offset-0 transition-colors"
placeholder="输入镜头描述..."
autoFocus
onBlur={() => setEditingField({ type: null, value: '' })}
/>
) : (
<div
className='text-sm text-white/80 leading-relaxed p-2 rounded hover:bg-white/5 cursor-text transition-colors'
onClick={() => handleScriptClick('shot', generateObj.scripts[selectedVideoIndex].shot)}
>
{generateObj.scripts[selectedVideoIndex].shot}
</div>
)}
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className='text-sm font-medium text-blue-400/90'></span>
</div>
{editingField.type === 'frame' ? (
<Textarea
value={editingField.value}
onChange={(e) => setEditingField({ ...editingField, value: e.target.value })}
className="min-h-[120px] resize-none bg-transparent border-white/10 focus:border-white/20 focus-visible:ring-0 focus-visible:ring-offset-0 transition-colors"
placeholder="输入场景描述..."
autoFocus
onBlur={() => setEditingField({ type: null, value: '' })}
/>
) : (
<div
className='text-sm text-white/80 leading-relaxed p-2 rounded hover:bg-white/5 cursor-text transition-colors'
onClick={() => handleScriptClick('frame', generateObj.scripts[selectedVideoIndex].frame)}
>
{generateObj.scripts[selectedVideoIndex].frame}
</div>
)}
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className='text-sm font-medium text-blue-400/90'></span>
</div>
{editingField.type === 'atmosphere' ? (
<Input
value={editingField.value}
onChange={(e) => setEditingField({ ...editingField, value: e.target.value })}
className="bg-transparent border-white/10 focus:border-white/20 focus-visible:ring-0 focus-visible:ring-offset-0 transition-colors"
placeholder="输入氛围描述..."
autoFocus
onBlur={() => setEditingField({ type: null, value: '' })}
/>
) : (
<div
className='text-sm text-white/80 leading-relaxed p-2 rounded hover:bg-white/5 cursor-text transition-colors'
onClick={() => handleScriptClick('atmosphere', generateObj.scripts[selectedVideoIndex].atmosphere)}
>
{generateObj.scripts[selectedVideoIndex].atmosphere}
</div>
)}
</div>
</div>
) : (
<div className="space-y-6">
<div>
<Skeleton className="h-4 w-16 mb-2" />
<Skeleton className="h-20 w-full" />
</div>
<div>
<Skeleton className="h-4 w-16 mb-2" />
<Skeleton className="h-20 w-full" />
</div>
</div>
)}
</div>
</div>
</ResizablePanel>
<ResizableHandle withHandle />
{/* 第二列 - 分镜视频 */}
<ResizablePanel defaultSize={60}>
<div className="h-full flex flex-col">
<div className="p-4 border-b border-white/10">
<h3 className="text-base font-medium"></h3>
</div>
<div className="flex-1 overflow-y-auto p-4">
<div
ref={videosContainerRef}
className='flex gap-4'
>
{generateObj.scripts ? (
generateObj.scripts.map((script: any, index: number) => {
const video = generateObj.scene_videos?.find((v: any) => v.id === index);
const isSelected = selectedVideoIndex === index;
return (
<div
key={index}
id={`video-${index}`}
className={`flex-shrink-0 w-[320px] aspect-video rounded-lg overflow-hidden relative cursor-pointer transition-all duration-300
${isSelected ? 'ring-2 ring-blue-400 ring-offset-2 ring-offset-[#191B1E]' : 'hover:ring-2 hover:ring-white/20'}`}
onClick={() => handleVideoSelect(index)}
>
{video ? (
<>
<video
src={video.video_url}
className="w-full h-full object-cover"
controls={isSelected}
/>
{!isSelected && (
<div className="absolute inset-0 bg-black/40 flex items-center justify-center">
<Play className="w-8 h-8 text-white/90" />
</div>
)}
</>
) : (
<div className="w-full h-full bg-white/5 flex items-center justify-center">
<Loader2 className="w-8 h-8 text-white/40 animate-spin" />
</div>
)}
<div className="absolute top-2 right-2 px-2 py-1 rounded-full bg-black/60 text-xs text-white/80">
Scene {index + 1}
</div>
</div>
);
})
) : (
<div className="flex-1 flex items-center justify-center">
<div className="grid grid-cols-3 gap-4">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="w-[320px] aspect-video rounded-lg" />
))}
</div>
</div>
)}
</div>
</div>
</div>
</ResizablePanel>
<ResizableHandle withHandle />
{/* 第三列 - 属性设置 */}
<ResizablePanel defaultSize={20}>
<div className="h-full flex flex-col">
<div className="p-4 border-b border-white/10 flex items-center justify-between">
<div className="flex items-center gap-2">
<h3 className="text-base font-medium"></h3>
{selectedVideoIndex !== null && (
<span className="px-2 py-1 rounded-full bg-blue-400/10 text-xs text-blue-400">
Scene {selectedVideoIndex + 1}
</span>
)}
</div>
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-white/5">
<MoreHorizontal className="h-4 w-4" />
</Button>
</SheetTrigger>
<SheetContent className="w-[400px] sm:w-[540px] bg-gradient-to-b from-gray-900/95 to-black/95 border-gray-800">
<SheetHeader>
<SheetTitle className="text-xl font-semibold text-white"></SheetTitle>
<div className="text-sm text-white/60">
{selectedVideoIndex !== null ? `Scene ${selectedVideoIndex + 1} 的高级设置选项` : '全局高级设置选项'}
</div>
</SheetHeader>
<div className="mt-6 space-y-6">
<div className="space-y-4">
<h4 className="text-sm font-medium text-white/90"></h4>
<div className="grid grid-cols-2 gap-4">
<div className="p-4 rounded-lg bg-white/5 hover:bg-white/10 transition-colors cursor-pointer">
<div className="text-sm font-medium mb-1"></div>
<div className="text-xs text-white/60"></div>
</div>
<div className="p-4 rounded-lg bg-white/5 hover:bg-white/10 transition-colors cursor-pointer">
<div className="text-sm font-medium mb-1"></div>
<div className="text-xs text-white/60"></div>
</div>
</div>
</div>
<div className="space-y-4">
<h4 className="text-sm font-medium text-white/90"></h4>
<div className="grid grid-cols-2 gap-4">
<div className="p-4 rounded-lg bg-white/5 hover:bg-white/10 transition-colors cursor-pointer">
<div className="text-sm font-medium mb-1"></div>
<div className="text-xs text-white/60"></div>
</div>
<div className="p-4 rounded-lg bg-white/5 hover:bg-white/10 transition-colors cursor-pointer">
<div className="text-sm font-medium mb-1"></div>
<div className="text-xs text-white/60"></div>
</div>
</div>
</div>
<div className="space-y-4">
<h4 className="text-sm font-medium text-white/90"></h4>
<div className="grid grid-cols-2 gap-4">
<div className="p-4 rounded-lg bg-white/5 hover:bg-white/10 transition-colors cursor-pointer">
<div className="text-sm font-medium mb-1"></div>
<div className="text-xs text-white/60"></div>
</div>
<div className="p-4 rounded-lg bg-white/5 hover:bg-white/10 transition-colors cursor-pointer">
<div className="text-sm font-medium mb-1"></div>
<div className="text-xs text-white/60"></div>
</div>
</div>
</div>
</div>
</SheetContent>
</Sheet>
</div>
<div className="flex-1 overflow-y-auto p-4">
{generateObj.scripts && selectedVideoIndex !== null ? (
<div className="space-y-4">
{/* 基础操作按钮 */}
<div className="grid grid-cols-2 gap-4">
<Button
variant="ghost"
className="w-full h-20 rounded-lg bg-white/5 hover:bg-white/10 transition-colors flex flex-col items-center justify-center gap-2"
onClick={() => handleDeleteScene(selectedVideoIndex)}
>
<X className="w-5 h-5" />
<span className="text-sm"></span>
</Button>
<Button
variant="ghost"
className="w-full h-20 rounded-lg bg-white/5 hover:bg-white/10 transition-colors flex flex-col items-center justify-center gap-2"
onClick={() => handleRegenerateScene(selectedVideoIndex)}
>
<RefreshCw className="w-5 h-5" />
<span className="text-sm"></span>
</Button>
</div>
{/* 音量调节 */}
<div className="p-4 rounded-lg bg-white/5">
<div className="text-sm font-medium mb-3"></div>
<input
type="range"
min="0"
max="100"
value={volume[selectedVideoIndex] || 100}
onChange={(e) => handleVolumeChange(selectedVideoIndex, Number(e.target.value))}
className="w-full h-1 bg-white/10 rounded-lg appearance-none cursor-pointer"
/>
<div className="mt-2 text-right text-sm text-white/60">
{volume[selectedVideoIndex] || 100}%
</div>
</div>
{/* 转场效果 */}
<div className="p-4 rounded-lg bg-white/5">
<div className="text-sm font-medium mb-3"></div>
<div className="grid grid-cols-3 gap-2">
{['淡入淡出', '溶解', '划入', '百叶窗', '放大', '旋转'].map((effect) => (
<div
key={effect}
className={`p-2 rounded text-center text-sm cursor-pointer transition-colors ${
transition[selectedVideoIndex] === effect
? 'bg-blue-400/20 text-blue-400'
: 'bg-white/10 hover:bg-white/20'
}`}
onClick={() => handleTransitionChange(selectedVideoIndex, effect)}
>
{effect}
</div>
))}
</div>
</div>
{/* 其他生成的视频 */}
{alternativeVideos[selectedVideoIndex]?.length > 0 && (
<div className="p-4 rounded-lg bg-white/5">
<div className="text-sm font-medium mb-3"></div>
<div className="grid grid-cols-2 gap-4">
{alternativeVideos[selectedVideoIndex].map((videoUrl, index) => (
<div
key={index}
className={`relative aspect-video rounded-lg overflow-hidden cursor-pointer ${
currentVideoIndex[selectedVideoIndex] === index
? 'ring-2 ring-blue-400'
: ''
}`}
onClick={() => handleSwitchVideo(selectedVideoIndex, index)}
>
<video
src={videoUrl}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 flex items-center justify-center bg-black/40">
<Play className="w-8 h-8 text-white/90" />
</div>
</div>
))}
</div>
</div>
)}
</div>
) : (
<div className="space-y-4">
<Skeleton className="h-8 w-full" />
<Skeleton className="h-8 w-3/4" />
<Skeleton className="h-8 w-1/2" />
</div>
)}
</div>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
</div>
)}
{/* 点击生成按钮后,暂停/继续按钮 */}
{isLoading && (
<div
ref={buttonRef}
className={`fixed z-50 cursor-move ${isDragging ? 'select-none' : ''}`}
style={{
left: `${buttonPosition.x}px`,
top: `${buttonPosition.y}px`,
transform: 'translate(-50%, -50%)',
transition: isDragging ? 'none' : 'all 0.2s ease'
}}
onMouseDown={handleDragStart}
>
<button
className={`
flex items-center justify-center w-12 h-12 rounded-full
bg-gradient-to-br from-blue-400/20 to-blue-600/20 backdrop-blur-lg
hover:from-blue-400/30 hover:to-blue-600/30
transition-all duration-300
${isDragging ? 'scale-90' : 'scale-100 hover:scale-110'}
`}
onClick={handlePauseResume}
>
{isPaused ? (
<Play className="w-5 h-5 text-white/70 hover:text-white/90 transition-colors" />
) : (
<Pause className="w-5 h-5 text-white/70 hover:text-white/90 transition-colors" />
)}
</button>
</div>
)}
{/* 工具栏 */}
<div className='video-tool-component relative w-[1080px]'>
<div className='video-storyboard-tools grid gap-4 rounded-[20px] bg-[#fff3] backdrop-blur-[15px]'>
{isExpanded ? (
<div className='absolute top-0 bottom-0 left-0 right-0 z-[9] grid justify-items-center place-content-center rounded-[20px] bg-[#191B1E] bg-opacity-[0.3] backdrop-blur-[1.5px] cursor-pointer' onClick={() => setIsExpanded(false)}>
{/* 图标 展开按钮 */}
<ChevronUp className='w-4 h-4' />
<span className='text-sm'>Click to create</span>
</div>
) : (
<div className='absolute top-[-8px] left-[50%] z-[10] flex items-center justify-center w-[46px] h-4 rounded-[15px] bg-[#fff3] translate-x-[-50%] cursor-pointer hover:bg-[#ffffff1a]' onClick={() => setIsExpanded(true)}>
{/* 图标 折叠按钮 */}
<ChevronDown className='w-4 h-4' />
</div>
)}
<div className='storyboard-tools-tab relative flex gap-8 px-4 py-[10px]'>
<div className={`tab-item ${activeTab === 'clone' ? 'active' : ''} cursor-pointer`} onClick={() => setActiveTab('clone')}>
<span className='text-sm opacity-60'></span>
</div>
<div className={`tab-item ${activeTab === 'script' ? 'active' : ''} cursor-pointer`} onClick={() => setActiveTab('script')}>
<span className='text-sm opacity-60'></span>
</div>
</div>
<div className={`flex-shrink-0 p-4 overflow-hidden transition-all duration-300 pt-0 ${isExpanded ? 'h-[16px]' : 'h-[162px]'}`}>
<div className='video-creation-tool-container flex flex-col gap-4'>
{activeTab === 'clone' && (
<div className='relative flex items-center gap-4 h-[94px]'>
<div className='relative w-[72px] rounded-[6px] overflow-hidden cursor-pointer ant-dropdown-trigger' onClick={handleUploadVideo}>
<div className='relative flex items-center justify-center w-[72px] h-[72px] rounded-[6px 6px 0 0] bg-white/[0.05] transition-all overflow-hidden hover:bg-white/[0.10]'>
{/* 图标 添加视频 */}
<Video className='w-4 h-4' />
</div>
<div className='w-full h-[22px] flex items-center justify-center rounded-[0 0 6px 6px] bg-white/[0.03]'>
<span className='text-xs cursor-[inherit]'>Add Video</span>
</div>
</div>
{videoUrl && (
<div className='relative w-[72px] rounded-[6px] overflow-hidden cursor-pointer ant-dropdown-trigger'>
<div className='relative flex items-center justify-center w-[72px] h-[72px] rounded-[6px 6px 0 0] bg-white/[0.05] transition-all overflow-hidden hover:bg-white/[0.10]'>
<video src={videoUrl} className='w-full h-full object-cover' />
</div>
</div>
)}
</div>
)}
{activeTab === 'script' && (
<div className='relative flex items-center gap-4 h-[94px]'>
</div>
)}
</div>
<div className='flex gap-3 justify-end'>
<div className='flex items-center gap-3'>
<div className='disabled tool-submit-button'>Stop</div>
<div className={`tool-submit-button ${videoUrl ? '' : 'disabled'}`} onClick={handleCreateVideo}>Create</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}