video-flow-b/components/pages/create-to-video2.tsx
2025-07-01 23:06:07 +08:00

547 lines
21 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, Lightbulb, Package, Crown, ArrowUp } from 'lucide-react';
import { useRouter, useSearchParams } 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-video2.css';
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { Skeleton } from "@/components/ui/skeleton";
import LiquidGlass from '@/plugins/liquid-glass/index'
import { Dropdown } from 'antd';
import type { MenuProps } from 'antd';
import Image from 'next/image';
import dynamic from 'next/dynamic';
import { ProjectTypeEnum, ModeEnum, ResolutionEnum } from "@/api/enums";
import { createScriptEpisode, CreateScriptEpisodeRequest, updateScriptEpisode, UpdateScriptEpisodeRequest } from "@/api/script_episode";
import { getUploadTokenWithDomain, uploadToQiniu } from "@/api/common";
import { convertScriptToScene, convertVideoToScene } from "@/api/video_flow";
import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation';
const JoyrideNoSSR = dynamic(() => import('react-joyride'), {
ssr: false,
});
// 导入Step类型
import type { Step } from 'react-joyride';
// interface Step {
// target: string;
// content: string;
// placement?: 'top' | 'bottom' | 'left' | 'right';
// }
// 添加自定义滚动条样式
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;
}
const ideaText = 'a cute capybara with an orange on its head, staring into the distance and walking forward';
export function CreateToVideo2() {
const router = useRouter();
const searchParams = useSearchParams();
const projectId = searchParams.get('projectId') ? parseInt(searchParams.get('projectId')!) : 0;
const [isClient, setIsClient] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [videoUrl, setVideoUrl] = useState('');
const [isUploading, setIsUploading] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const [activeTab, setActiveTab] = useState('script');
const [isFocus, setIsFocus] = useState(false);
const [selectedMode, setSelectedMode] = useState<ModeEnum>(ModeEnum.AUTOMATIC);
const [selectedResolution, setSelectedResolution] = useState<ResolutionEnum>(ResolutionEnum.HD_720P);
const [script, setInputText] = useState('');
const editorRef = useRef<HTMLDivElement>(null);
const [runTour, setRunTour] = useState(true);
const [episodeId, setEpisodeId] = useState<number>(0);
const [isCreating, setIsCreating] = useState(false);
const [generatedVideoList, setGeneratedVideoList] = useState<any[]>([]);
const [projectName, setProjectName] = useState(localStorage.getItem('projectName') || '默认名称');
const handleUploadVideo = async () => {
console.log('upload video');
// 打开文件选择器
const input = document.createElement('input');
input.type = 'file';
input.accept = 'video/*';
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
try {
setIsUploading(true);
// 获取上传token
const { token } = await getUploadTokenWithDomain();
// 上传到七牛云
const videoUrl = await uploadToQiniu(file, token);
// 上传成功设置视频URL
setVideoUrl(videoUrl);
console.log('视频上传成功:', videoUrl);
} catch (error) {
console.error('上传错误:', error);
alert('上传失败,请稍后重试');
} finally {
setIsUploading(false);
}
}
}
input.click();
}
const handleCreateVideo = async () => {
// 创建剧集数据
const episodeData: CreateScriptEpisodeRequest = {
title: "episode 1",
script_id: projectId,
status: 1,
summary: script
};
// 调用创建剧集API
const episodeResponse = await createScriptEpisode(episodeData);
if (episodeResponse.code !== 0) {
console.error(`创建剧集失败: ${episodeResponse.message}`);
alert(`创建剧集失败: ${episodeResponse.message}`);
return;
}
let episodeId = episodeResponse.data.id;
if (videoUrl || script) {
try {
setIsCreating(true);
let convertResponse;
// 根据选中的选项卡调用相应的API
if (activeTab === 'script') {
// 剧本模式调用convertScriptToScene (第43-56行)
if (!script.trim()) {
alert('请输入剧本内容');
return;
}
convertResponse = await convertScriptToScene(script, episodeId, projectId);
} else {
// 视频模式调用convertVideoToScene (第56-69行)
if (!videoUrl) {
alert('请先上传视频');
return;
}
if (!episodeId) {
alert('Episode ID not available');
return;
}
convertResponse = await convertVideoToScene(videoUrl, episodeId, projectId);
}
// 更新剧集
const updateEpisodeData: UpdateScriptEpisodeRequest = {
id: episodeId,
atmosphere: convertResponse.data.atmosphere,
summary: convertResponse.data.summary,
scene: convertResponse.data.scene,
characters: convertResponse.data.characters,
};
const updateEpisodeResponse = await updateScriptEpisode(updateEpisodeData);
// 检查转换结果
if (convertResponse.code === 0) {
// 成功创建后跳转到work-flow页面, 并设置episodeId 和 projectType
router.push(`/create/work-flow?episodeId=${episodeResponse.data.id}`);
} else {
alert(`转换失败: ${convertResponse.message}`);
}
} catch (error) {
console.error('创建过程出错:', error);
alert("创建项目时发生错误,请稍后重试");
} finally {
setIsCreating(false);
}
}
}
// 下拉菜单项配置
const modeItems: MenuProps['items'] = [
{
type: 'group',
label: (
<div className="text-white/50 text-xs px-2 pb-2">Mode</div>
),
children: [
{
key: ModeEnum.AUTOMATIC,
label: (
<div className="flex flex-col gap-1 p-1">
<div className="flex items-center gap-2">
<span className="text-base font-medium">Auto</span>
<Crown className="w-4 h-4 text-yellow-500" />
</div>
<span className="text-sm text-gray-400">Automatically selects the best model for optimal efficiency</span>
</div>
),
},
{
key: ModeEnum.MANUAL,
label: (
<div className="flex flex-col gap-1 p-1">
<div className="flex items-center gap-2">
<span className="text-base font-medium">Manual</span>
<Crown className="w-4 h-4 text-yellow-500" />
</div>
<span className="text-sm text-gray-400">Offers reliable, consistent performance every time</span>
</div>
),
},
],
},
];
// 分辨率选项配置
const resolutionItems: MenuProps['items'] = [
{
type: 'group',
label: (
<div className="text-white/50 text-xs px-2 pb-2">Resolution</div>
),
children: [
{
key: ResolutionEnum.HD_720P,
label: (
<div className="flex items-center justify-between p-1">
<span className="text-base">720P</span>
</div>
),
},
{
key: ResolutionEnum.FULL_HD_1080P,
label: (
<div className="flex items-center justify-between p-1">
<span className="text-base">1080P</span>
<Crown className="w-4 h-4 text-yellow-500" />
</div>
),
},
{
key: ResolutionEnum.UHD_2K,
label: (
<div className="flex items-center justify-between p-1">
<span className="text-base">2K</span>
<Crown className="w-4 h-4 text-yellow-500" />
</div>
),
},
{
key: ResolutionEnum.UHD_4K,
label: (
<div className="flex items-center justify-between p-1">
<span className="text-base">4K</span>
<Crown className="w-4 h-4 text-yellow-500" />
</div>
),
},
],
},
];
// 处理模式选择
const handleModeSelect: MenuProps['onClick'] = ({ key }) => {
setSelectedMode(Number(key) as ModeEnum);
};
// 处理分辨率选择
const handleResolutionSelect: MenuProps['onClick'] = ({ key }) => {
setSelectedResolution(Number(key) as ResolutionEnum);
};
const handleStartCreating = () => {
setActiveTab('script');
setInputText(ideaText);
}
// 处理编辑器聚焦
const handleEditorFocus = () => {
setIsFocus(true);
if (editorRef.current && script) {
// 创建范围对象
const range = document.createRange();
const selection = window.getSelection();
// 获取编辑器内的文本节点
const textNode = Array.from(editorRef.current.childNodes).find(
node => node.nodeType === Node.TEXT_NODE
) || editorRef.current.appendChild(document.createTextNode(script));
// 设置范围到文本末尾
range.setStart(textNode, script.length);
range.setEnd(textNode, script.length);
// 应用选择
selection?.removeAllRanges();
selection?.addRange(range);
}
};
// 处理编辑器内容变化
const handleEditorChange = (e: React.FormEvent<HTMLDivElement>) => {
const script = e.currentTarget.textContent || '';
setInputText(script);
};
// 引导步骤
const steps: Step[] = [
{
target: '.video-storyboard-tools',
content: 'Welcome to AI Video Creation Tool! This is the main creation area.',
placement: 'top',
},
{
target: '.storyboard-tools-tab',
content: 'Choose between Script mode to create videos from text, or Clone mode to recreate existing videos.',
placement: 'bottom',
},
{
target: '.video-prompt-editor',
content: 'Describe your video content here. Our AI will generate videos based on your description.',
placement: 'top',
},
{
target: '.tool-operation-button',
content: 'Select different creation modes and video resolutions to customize your output.',
placement: 'top',
},
{
target: '.tool-submit-button',
content: 'Click here to start creating your video once you are ready!',
placement: 'left',
},
];
// 处理引导结束
const handleJoyrideCallback = (data: any) => {
const { status } = data;
if (status === 'finished' || status === 'skipped') {
setRunTour(false);
// 可以在这里存储用户已完成引导的状态
localStorage.setItem('hasCompletedTour', 'true');
}
};
// 检查是否需要显示引导
useEffect(() => {
const hasCompletedTour = localStorage.getItem('hasCompletedTour');
if (hasCompletedTour) {
setRunTour(false);
}
}, []);
useEffect(() => {
setIsClient(true);
}, []);
return (
<div
ref={containerRef}
className="container mx-auto overflow-hidden custom-scrollbar"
style={isExpanded ? { height: 'calc(100vh - 12rem)' } : { height: 'calc(100vh - 20rem)' }}
>
{isClient && (
<JoyrideNoSSR
steps={steps}
run={runTour}
continuous
showSkipButton
showProgress
styles={{
options: {
primaryColor: '#4F46E5',
zIndex: 10000,
backgroundColor: '#1F2937',
overlayColor: 'rgba(0, 0, 0, 0.5)',
textColor: '#F3F4F6',
arrowColor: '#1F2937',
}
}}
disableOverlayClose
spotlightClicks
hideCloseButton
callback={handleJoyrideCallback}
locale={{
back: 'Back',
close: 'Close',
last: 'Finish',
next: 'Next',
skip: 'Skip'
}}
/>
)}
<div className='min-h-[100%] flex flex-col justify-center items-center'>
{/* 空状态 */}
<EmptyStateAnimation />
{/* 工具栏 */}
<div className='video-tool-component relative w-[1080px]'>
<div className='video-storyboard-tools grid gap-4 rounded-[20px] bg-[#0C0E11] backdrop-blur-[15px]'>
{isExpanded ? (
<div className='absolute top-0 bottom-0 left-0 right-0 z-[1] 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-[2] 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 === 'script' ? 'active' : ''} cursor-pointer`} onClick={() => setActiveTab('script')}>
<span className='text-lg opacity-60'>script</span>
</div>
<div className={`tab-item ${activeTab === 'clone' ? 'active' : ''} cursor-pointer`} onClick={() => setActiveTab('clone')}>
<span className='text-lg opacity-60'>clone</span>
</div>
</div>
<div className={`flex-shrink-0 p-4 overflow-hidden transition-all duration-300 pt-0 gap-4 ${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 opacity-30 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 className={`video-prompt-editor relative flex flex-1 self-stretch items-center w-0 rounded-[6px] ${isFocus ? 'focus' : ''}`}>
<div
ref={editorRef}
className='editor-content flex-1 w-0 max-h-[78px] min-h-[26px] h-auto gap-4 pl-[10px] rounded-[10px] leading-[26px] text-sm border-none overflow-y-auto cursor-text'
contentEditable
style={{ paddingRight: '10px' }}
onFocus={handleEditorFocus}
onBlur={() => setIsFocus(false)}
onInput={handleEditorChange}
suppressContentEditableWarning
>
{script}
</div>
<div
className={`custom-placeholder absolute top-[50%] left-[10px] z-10 translate-y-[-50%] flex items-center gap-1 pointer-events-none text-[14px] leading-[26px] text-white/[0.40] ${script ? 'opacity-0' : 'opacity-100'}`}
>
<span>Describe the content you want to create. Get an </span>
<b
className='idea-link inline-flex items-center gap-0.5 text-white/[0.50] font-normal cursor-pointer pointer-events-auto underline'
onClick={() => setInputText(ideaText)}
>
<Lightbulb className='w-4 h-4' />idea
</b>
</div>
</div>
</div>
)}
<div className='flex gap-3'>
<div className='tool-scroll-box relative flex-1 w-0'>
<div className='tool-scroll-box-content overflow-x-auto scrollbar-hide'>
<div className='flex items-center flex-1 gap-3'>
<Dropdown
menu={{
items: modeItems,
onClick: handleModeSelect,
selectedKeys: [selectedMode.toString()],
}}
trigger={['click']}
overlayClassName="mode-dropdown"
placement="bottomLeft"
>
<div className='tool-operation-button ant-dropdown-trigger'>
<Package className='w-4 h-4' />
<span className='text-nowrap opacity-70'>
{selectedMode === ModeEnum.AUTOMATIC ? 'Auto' : 'Manual'}
</span>
<Crown className='w-4 h-4 text-yellow-500' />
</div>
</Dropdown>
<Dropdown
menu={{
items: resolutionItems,
onClick: handleResolutionSelect,
selectedKeys: [selectedResolution.toString()],
}}
trigger={['click']}
overlayClassName="mode-dropdown"
placement="bottomLeft"
>
<div className='tool-operation-button ant-dropdown-trigger'>
<Video className='w-4 h-4' />
<span className='text-nowrap opacity-70'>
{selectedResolution === ResolutionEnum.HD_720P ? '720P' :
selectedResolution === ResolutionEnum.FULL_HD_1080P ? '1080P' :
selectedResolution === ResolutionEnum.UHD_2K ? '2K' : '4K'}
</span>
<Crown className='w-4 h-4 text-yellow-500' />
</div>
</Dropdown>
</div>
</div>
</div>
<div className='flex items-center gap-3'>
<div className={`tool-submit-button ${videoUrl || script ? '' : 'disabled'} ${isCreating ? 'loading' : ''}`} onClick={isCreating ? undefined : handleCreateVideo}>
{isCreating ? (
<>
<Loader2 className='w-4 h-4 animate-spin' />
Creating...
</>
) : (
<>
<ArrowUp className='w-4 h-4' />
Create
</>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}