加入主题、工作台loading(开发中)

This commit is contained in:
北枳 2025-07-17 23:06:36 +08:00
parent 7719425e67
commit 834263dbc8
29 changed files with 4231 additions and 153 deletions

View File

@ -1 +1 @@
export const BASE_URL = "https://77.smartvideo.py.qikongjian.com" export const BASE_URL = "https://pre.movieflow.api.huiying.video"

View File

@ -183,37 +183,19 @@ export const convertVideoToScene = async (
// 新-获取剧集详情 // 新-获取剧集详情
export const detailScriptEpisodeNew = async (data: { project_id: string }): Promise<ApiResponse<any>> => { export const detailScriptEpisodeNew = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/get_movie_project_detail', data); return post<ApiResponse<any>>('/movie/get_movie_project_detail', data);
return {
code: 0,
successful: true,
message: 'success',
data: {
project_id: 'uuid',
name: '没有返回就调 name 接口',
status: 'running',
step: 'sketch',
last_message: 'loading detail info...',
data: null,
mode: 'auto',
resolution: '1080p',
}
}
}; };
// 获取 title 接口 // 获取 title 接口
export const getScriptTitle = async (data: { project_id: string }): Promise<ApiResponse<any>> => { export const getScriptTitle = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/get_movie_project_name', data); return post<ApiResponse<any>>('/movie/get_movie_project_description', data);
return {
code: 0,
successful: true,
message: 'success',
data: {
name: '提取出视频标题'
}
}
} }
// 获取 数据 全量(需轮询) // 获取 数据 全量(需轮询)
export const getRunningStreamData = async (data: { project_id: string }): Promise<ApiResponse<any>> => { export const getRunningStreamData = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/get_status', data); return post<ApiResponse<any>>('/movie/get_status', data);
}; };
// 获取 脚本 接口
export const getScriptTags = async (data: { project_id: string }): Promise<any> => {
return post<any>('/movie/text_to_script_tags', data);
};

112
app/api/stream/route.ts Normal file
View File

@ -0,0 +1,112 @@
import { parseMDXContent } from '@/lib/mdx';
import { BASE_URL } from '@/api/constants';
export const dynamic = 'force-dynamic';
export const runtime = 'edge';
export async function GET() {
try {
const content = `[分析场景核心] 一位舞者对<Annot type="circle">完美</Annot>的执念,<Annot type="highlight" color="#ef4444">使她的美丽舞蹈转化为一种自我毁灭的仪式</Annot>。
### <Annot type="highlight"></Annot>
· -
<Annot type="box" color="#a855f7">ELARA</Annot>20姿穿
线<Annot type="highlight" color="#84cc16"></Annot>
<Annot type="highlight" color="#f59e0b"></Annot><Annot type="highlight" color="#f59e0b"></Annot>
姿
<Annot type="highlight" color="#ef4444">姿</Annot>
齿
<Annot type="highlight" color="#ef4444"></Annot>
<Annot type="highlight" color="#ef4444"></Annot>
<Annot type="box" color="#a855f7">Elara</Annot>
<Annot type="highlight" color="#06b6d4"></Annot>穿
### <Annot type="highlight"></Annot>
<Annot type="highlight"></Annot>
使
### <Annot type="highlight"></Annot>
"无形评审"<Annot type="highlight" color="#a855f7">"我要强迫这个不完美的身体创造一个完美的瞬间,即使这将摧毁我。"</Annot>
### <Annot type="highlight"></Annot>
"精心营造的完美主义"
`;
console.log('开始解析 MDX 内容...');
const chunks = await parseMDXContent(content);
console.log('解析完成,获得块数:', chunks?.length);
if (!chunks || chunks.length === 0) {
console.error('没有生成有效的内容块');
return new Response(
JSON.stringify({ error: '内容解析失败' }),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
}
);
}
// 创建一个可写流来处理数据
const stream = new TransformStream();
const writer = stream.writable.getWriter();
const encoder = new TextEncoder();
// 异步处理数据流
(async () => {
try {
for (const chunk of chunks) {
const data = encoder.encode(JSON.stringify(chunk) + '\n');
await writer.write(data);
}
} catch (error) {
console.error('写入流时出错:', error);
} finally {
await writer.close();
}
})();
return new Response(stream.readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
},
});
} catch (error) {
console.error('API 路由处理出错:', error);
return new Response(
JSON.stringify({
error: '服务器处理请求时出错',
details: error instanceof Error ? error.message : '未知错误'
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
}
);
}
}

View File

@ -279,42 +279,93 @@ export function CreateToVideo2() {
} }
}; };
const handleCompositionStart = () => {
setIsComposing(true);
};
const handleCompositionEnd = (e: React.CompositionEvent<HTMLDivElement>) => {
setIsComposing(false);
handleEditorChange(e as any);
};
const handleEditorChange = (e: React.FormEvent<HTMLDivElement>) => { const handleEditorChange = (e: React.FormEvent<HTMLDivElement>) => {
// 如果正在输入中文,不要更新内容
if (isComposing) return;
const newText = e.currentTarget.textContent || ''; const newText = e.currentTarget.textContent || '';
// 如果正在输入中文,只更新内部文本,不更新状态
if (isComposing) {
return;
}
// 更新状态
setInputText(newText); setInputText(newText);
// 保存当前选区位置
const selection = window.getSelection(); const selection = window.getSelection();
if (selection && selection.rangeCount > 0) { if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
const currentPosition = range.startOffset; const currentPosition = range.startOffset;
setTimeout(() => { // 使用 requestAnimationFrame 确保在下一帧恢复光标位置
requestAnimationFrame(() => {
if (editorRef.current) { if (editorRef.current) {
const textNode = Array.from(editorRef.current.childNodes).find( // 找到或创建文本节点
let textNode = Array.from(editorRef.current.childNodes).find(
node => node.nodeType === Node.TEXT_NODE node => node.nodeType === Node.TEXT_NODE
); ) as Text;
if (textNode) { if (!textNode) {
const newRange = document.createRange(); textNode = document.createTextNode(newText);
newRange.setStart(textNode, currentPosition); editorRef.current.appendChild(textNode);
newRange.setEnd(textNode, currentPosition);
selection.removeAllRanges();
selection.addRange(newRange);
} }
// 计算正确的光标位置
const finalPosition = Math.min(currentPosition, textNode.length);
// 设置新的选区
const newRange = document.createRange();
newRange.setStart(textNode, finalPosition);
newRange.setEnd(textNode, finalPosition);
selection.removeAllRanges();
selection.addRange(newRange);
} }
}, 0); });
}
};
// 处理中文输入开始
const handleCompositionStart = () => {
setIsComposing(true);
};
// 处理中文输入结束
const handleCompositionEnd = (e: React.CompositionEvent<HTMLDivElement>) => {
setIsComposing(false);
// 在输入完成后更新内容
const newText = e.currentTarget.textContent || '';
setInputText(newText);
// 保存并恢复光标位置
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const currentPosition = range.startOffset;
requestAnimationFrame(() => {
if (editorRef.current) {
let textNode = Array.from(editorRef.current.childNodes).find(
node => node.nodeType === Node.TEXT_NODE
) as Text;
if (!textNode) {
textNode = document.createTextNode(newText);
editorRef.current.appendChild(textNode);
}
// 计算正确的光标位置
const finalPosition = Math.min(currentPosition, textNode.length);
// 设置新的选区
const newRange = document.createRange();
newRange.setStart(textNode, finalPosition);
newRange.setEnd(textNode, finalPosition);
selection.removeAllRanges();
selection.addRange(newRange);
}
});
} }
}; };

View File

@ -119,8 +119,8 @@
.info-UUGkPJ { .info-UUGkPJ {
box-sizing: border-box; box-sizing: border-box;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 10px;
display: flex display: flex;
; ;
} }
.title-JtMejk { .title-JtMejk {

View File

@ -1,8 +1,8 @@
'use client'; 'use client';
import React from 'react'; import React, { useState, useEffect, useRef, useMemo } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Skeleton } from '@/components/ui/skeleton'; import { ScriptModal } from '@/components/ui/script-modal';
import { import {
Image, Image,
Video, Video,
@ -11,7 +11,8 @@ import {
Loader2, Loader2,
User, User,
Scissors, Scissors,
Tv Tv,
Airplay
} from 'lucide-react'; } from 'lucide-react';
interface TaskInfoProps { interface TaskInfoProps {
@ -44,8 +45,29 @@ const getStageIcon = (loadingText: string) => {
} }
}; };
const TAG_COLORS = ['#FF5733', '#126821', '#8d3913', '#FF33A1', '#A133FF', '#FF3333', '#3333FF', '#A1A1A1', '#a1115e', '#30527f'];
export function TaskInfo({ isLoading, taskObject, currentLoadingText, dataLoadError }: TaskInfoProps) { export function TaskInfo({ isLoading, taskObject, currentLoadingText, dataLoadError }: TaskInfoProps) {
const StageIcon = getStageIcon(currentLoadingText); const StageIcon = getStageIcon(currentLoadingText);
const [isScriptModalOpen, setIsScriptModalOpen] = useState(false);
// 使用 useMemo 缓存标签颜色映射
const tagColors = useMemo(() => {
if (!taskObject?.tags) return {};
return taskObject.tags.reduce((acc: Record<string, string>, tag: string) => {
acc[tag] = TAG_COLORS[Math.floor(Math.random() * TAG_COLORS.length)];
return acc;
}, {});
}, [taskObject?.tags]); // 只在 tags 改变时重新计算
// 自动触发打开 剧本 弹窗 延迟5秒
useEffect(() => {
if (taskObject?.title && currentLoadingText !== 'Task completed') {
setTimeout(() => {
setIsScriptModalOpen(true);
}, 5000);
}
}, [taskObject?.title]);
if (isLoading) { if (isLoading) {
return ( return (
@ -56,7 +78,7 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText, dataLoadEr
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
> >
{taskObject?.title || '正在加载项目信息...'} {taskObject?.title || 'loading project info...'}
</motion.div> </motion.div>
{/* 加载状态显示 */} {/* 加载状态显示 */}
@ -153,10 +175,43 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText, dataLoadEr
return ( return (
<> <>
<div className="title-JtMejk"> <div className="title-JtMejk flex items-center justify-center gap-2">
{taskObject?.title || '正在加载项目信息...'} {taskObject?.title ? (
<>
<span>
{taskObject?.title || 'loading project info...'}
</span>
{/* <div className="flex items-center gap-2">
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={() => setIsScriptModalOpen(true)}
>
<Airplay className="w-4 h-4 text-blue-500 cursor-pointer" />
</motion.div>
</div> */}
</>
) : 'loading project info...'}
</div> </div>
{/* 主题 彩色标签tags */}
<div className="flex items-center justify-center gap-2">
{taskObject?.tags?.map((tag: string) => (
<div
key={tag}
className="text-sm text-white rounded-full px-2 py-1"
style={{ backgroundColor: tagColors[tag] }}
>
{tag}
</div>
))}
</div>
<ScriptModal
isOpen={isScriptModalOpen}
onClose={() => setIsScriptModalOpen(false)}
/>
{currentLoadingText === 'Task completed' ? ( {currentLoadingText === 'Task completed' ? (
<motion.div <motion.div
className="flex items-center gap-3 justify-center" className="flex items-center gap-3 justify-center"
@ -164,61 +219,10 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText, dataLoadEr
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
> >
<motion.div <div className="flex items-center gap-2">
className="w-2 h-2 rounded-full bg-emerald-500" <CheckCircle className="w-5 h-5 text-emerald-500" />
animate={{ <span className="text-emerald-500 font-medium">{currentLoadingText}</span>
scale: [1, 1.5, 1], </div>
opacity: [1, 0.5, 1]
}}
transition={{
duration: 1,
repeat: Infinity,
repeatDelay: 0.2
}}
/>
<motion.div
className="flex items-center gap-2"
initial="hidden"
animate="visible"
variants={{
hidden: { opacity: 0 },
visible: { opacity: 1 }
}}
>
<motion.div
className="text-emerald-500"
variants={{
hidden: { opacity: 0, scale: 0.8 },
visible: { opacity: 1, scale: 1 }
}}
transition={{ duration: 0.5 }}
>
<CheckCircle className="w-5 h-5" />
</motion.div>
<motion.span
className="text-emerald-500 font-medium"
variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
}}
transition={{ duration: 0.5 }}
>
{currentLoadingText}
</motion.span>
</motion.div>
<motion.div
className="w-2 h-2 rounded-full bg-emerald-500"
animate={{
scale: [1, 1.5, 1],
opacity: [1, 0.5, 1]
}}
transition={{
duration: 1,
repeat: Infinity,
repeatDelay: 0.2,
delay: 0.3
}}
/>
</motion.div> </motion.div>
) : ( ) : (
<motion.div <motion.div

View File

@ -20,6 +20,8 @@ const LOADING_TEXT_MAP = {
sketchComplete: 'Sketch generation complete', sketchComplete: 'Sketch generation complete',
character: 'Drawing characters...', character: 'Drawing characters...',
newCharacter: (count: number, total: number) => `Drawing character ${count + 1 > total ? total : count + 1}/${total}...`, newCharacter: (count: number, total: number) => `Drawing character ${count + 1 > total ? total : count + 1}/${total}...`,
getShotSketchStatus: 'Getting shot sketch status...',
shotSketch: (count: number, total: number) => `Generating shot sketch ${count + 1 > total ? total : count + 1}/${total}...`,
getVideoStatus: 'Getting video status...', getVideoStatus: 'Getting video status...',
video: (count: number, total: number) => `Generating video ${count + 1 > total ? total : count + 1}/${total}...`, video: (count: number, total: number) => `Generating video ${count + 1 > total ? total : count + 1}/${total}...`,
videoComplete: 'Video generation complete', videoComplete: 'Video generation complete',
@ -43,6 +45,7 @@ interface TaskObject {
roles?: any[]; roles?: any[];
music?: any[]; music?: any[];
final?: any; final?: any;
tags?: any[];
} }
export function useWorkflowData() { export function useWorkflowData() {
@ -78,7 +81,7 @@ export function useWorkflowData() {
} }
let loadingText: any = LOADING_TEXT_MAP.initializing; let loadingText: any = LOADING_TEXT_MAP.initializing;
let finalStep = '1', sketchCount = 0; let finalStep = '1', sketchCount = 0, isChange = false;
const all_task_data = response.data; const all_task_data = response.data;
// all_task_data 下标0 和 下标1 换位置 // all_task_data 下标0 和 下标1 换位置
const temp = all_task_data[0]; const temp = all_task_data[0];
@ -90,7 +93,7 @@ export function useWorkflowData() {
// 如果有已完成的数据,同步到状态 // 如果有已完成的数据,同步到状态
if (task.task_name === 'generate_sketch' && task.task_result) { if (task.task_name === 'generate_sketch' && task.task_result) {
if (task.task_result.data.length >= 0 && taskSketch.length !== task.task_result.data.length) { if (task.task_result.data.length >= 0 && taskSketch.length < task.task_result.data.length) {
// 正在生成草图中 替换 sketch 数据 // 正在生成草图中 替换 sketch 数据
const sketchList = []; const sketchList = [];
for (const sketch of task.task_result.data) { for (const sketch of task.task_result.data) {
@ -125,7 +128,7 @@ export function useWorkflowData() {
url: character.image_path, url: character.image_path,
sound: null, sound: null,
soundDescription: '', soundDescription: '',
roleDescription: '' roleDescription: character.character_description
}); });
} }
setRoles(characterList); setRoles(characterList);
@ -136,18 +139,46 @@ export function useWorkflowData() {
// 角色生成完成 // 角色生成完成
finalStep = '3'; finalStep = '3';
loadingText = LOADING_TEXT_MAP.getVideoStatus; loadingText = LOADING_TEXT_MAP.getShotSketchStatus;
} }
} }
if (task.task_name === 'generate_shot_sketch' && task.task_result) {
if (task.task_result.data.length >= 0 && taskSketch.length < task.task_result.data.length) {
console.log('----------正在生成草图中 替换 sketch 数据');
// 正在生成草图中 替换 sketch 数据
const sketchList = [];
for (const sketch of task.task_result.data) {
sketchList.push({
url: sketch.url,
script: sketch.description
});
}
setTaskSketch(sketchList);
setSketchCount(sketchList.length);
setIsGeneratingSketch(true);
setCurrentSketchIndex(sketchList.length - 1);
loadingText = LOADING_TEXT_MAP.shotSketch(sketchList.length, task.task_result.total_count);
}
if (task.task_status === 'COMPLETED') {
// 草图生成完成
setIsGeneratingSketch(false);
sketchCount = task.task_result.total_count;
console.log('----------草图生成完成', sketchCount);
loadingText = LOADING_TEXT_MAP.video(0, task.task_result.total_count);
finalStep = '2';
}
setTotalSketchCount(task.task_result.total_count);
}
if (task.task_name === 'generate_videos' && task.task_result) { if (task.task_name === 'generate_videos' && task.task_result) {
if (task.task_result.data.length >= 0 && taskVideos.length !== task.task_result.data.length) { if (task.task_result.data.length >= 0 && taskVideos.length !== task.task_result.data.length) {
console.log('----------正在生成视频中-发生变化才触发');
// 正在生成视频中 替换视频数据 // 正在生成视频中 替换视频数据
const videoList = []; const videoList = [];
for (const video of task.task_result.data) { for (const video of task.task_result.data) {
// 每一项 video 有多个视频 先默认取第一个 // 每一项 video 有多个视频 先默认取第一个
videoList.push({ videoList.push({
url: video[0].qiniuVideoUrl, url: video.urls[0],
script: video[0].operation.metadata.video.prompt, script: video.description,
audio: null, audio: null,
}); });
} }
@ -232,14 +263,15 @@ export function useWorkflowData() {
throw new Error(response.message); throw new Error(response.message);
} }
const { name, status, data } = response.data; const { name, status, data, tags } = response.data;
setIsLoading(false); setIsLoading(false);
// 设置初始数据 // 设置初始数据
setTaskObject({ setTaskObject({
taskStatus: '0', taskStatus: '0',
title: name || 'generating...', title: name || 'generating...',
currentLoadingText: status === 'COMPLETED' ? LOADING_TEXT_MAP.complete : LOADING_TEXT_MAP.initializing currentLoadingText: status === 'COMPLETED' ? LOADING_TEXT_MAP.complete : LOADING_TEXT_MAP.initializing,
tags: tags || []
}); });
// 设置标题 // 设置标题
@ -250,7 +282,8 @@ export function useWorkflowData() {
if (titleResponse.successful) { if (titleResponse.successful) {
setTaskObject((prev: TaskObject | null) => ({ setTaskObject((prev: TaskObject | null) => ({
...(prev || {}), ...(prev || {}),
title: titleResponse.data.name title: titleResponse.data.title,
tags: titleResponse.data.tags || []
} as TaskObject)); } as TaskObject));
} }
} }
@ -294,7 +327,7 @@ export function useWorkflowData() {
url: character.image_path, url: character.image_path,
sound: null, sound: null,
soundDescription: '', soundDescription: '',
roleDescription: '' roleDescription: character.character_description
}); });
} }
setRoles(characterList); setRoles(characterList);
@ -303,7 +336,30 @@ export function useWorkflowData() {
} else { } else {
finalStep = '3'; finalStep = '3';
if (!data.video || !data.video.data || !data.video.data.length) { if (!data.video || !data.video.data || !data.video.data.length) {
loadingText = LOADING_TEXT_MAP.getVideoStatus; loadingText = LOADING_TEXT_MAP.getShotSketchStatus;
}
}
}
if (data.shot_sketch && data.shot_sketch.data && data.shot_sketch.data.length > 0) {
const sketchList = [];
for (const sketch of data.shot_sketch.data) {
sketchList.push({
url: sketch.url,
script: sketch.description,
});
}
setTaskSketch(sketchList);
setSketchCount(sketchList.length);
setTotalSketchCount(data.shot_sketch.total_count);
// 设置为最后一个草图
if (data.shot_sketch.total_count > data.shot_sketch.data.length) {
setIsGeneratingSketch(true);
setCurrentSketchIndex(data.shot_sketch.data.length - 1);
loadingText = LOADING_TEXT_MAP.shotSketch(data.shot_sketch.data.length, data.shot_sketch.total_count);
} else {
finalStep = '2';
if (!data.character || !data.character.data || !data.character.data.length) {
loadingText = LOADING_TEXT_MAP.video(0, data.shot_sketch.data.length);
} }
} }
} }
@ -312,8 +368,8 @@ export function useWorkflowData() {
for (const video of data.video.data) { for (const video of data.video.data) {
// 每一项 video 有多个视频 先默认取第一个 // 每一项 video 有多个视频 先默认取第一个
videoList.push({ videoList.push({
url: video[0].qiniuVideoUrl, url: video.urls[0],
script: video[0].operation.metadata.video.prompt, script: video.description,
audio: null, audio: null,
}); });
} }

View File

@ -2,7 +2,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { X, FileText, Users, Video, Music, Scissors, Settings } from 'lucide-react'; import { X, Image, Users, Video, Music, Settings } from 'lucide-react';
import { cn } from '@/public/lib/utils'; import { cn } from '@/public/lib/utils';
import { ScriptTabContent } from './script-tab-content'; import { ScriptTabContent } from './script-tab-content';
import { VideoTabContent } from './video-tab-content'; import { VideoTabContent } from './video-tab-content';
@ -24,9 +24,9 @@ interface EditModalProps {
} }
const tabs = [ const tabs = [
{ id: '1', label: 'Script', icon: FileText }, { id: '1', label: 'Shot Sketch', icon: Image },
{ id: '2', label: 'Character', icon: Users }, { id: '2', label: 'Character', icon: Users },
{ id: '3', label: 'Sketch video', icon: Video }, { id: '3', label: 'Shot video', icon: Video },
{ id: '4', label: 'Music', icon: Music }, { id: '4', label: 'Music', icon: Music },
// { id: '5', label: '剪辑', icon: Scissors }, // { id: '5', label: '剪辑', icon: Scissors },
{ id: 'settings', label: 'Settings', icon: Settings }, { id: 'settings', label: 'Settings', icon: Settings },
@ -59,7 +59,12 @@ export function EditModal({
const isTabDisabled = (tabId: string) => { const isTabDisabled = (tabId: string) => {
if (tabId === 'settings') return false; if (tabId === 'settings') return false;
return parseInt(tabId) > parseInt(taskStatus); // 换成 如果对应标签下 数据存在 就不禁用
if (tabId === '1') return taskSketch.length === 0;
if (tabId === '2') return roles.length === 0;
if (tabId === '3') return sketchVideo.length === 0;
if (tabId === '4') return false;
return false;
}; };
const hanldeChangeSelect = (index: number) => { const hanldeChangeSelect = (index: number) => {

View File

@ -59,7 +59,7 @@ export function GenerateVideoModal({
> >
<ChevronDown className="w-5 h-5" /> <ChevronDown className="w-5 h-5" />
</button> </button>
<h2 className="text-lg font-medium"></h2> <h2 className="text-lg font-medium">generate video</h2>
</div> </div>
</div> </div>
@ -67,11 +67,11 @@ export function GenerateVideoModal({
<div className="p-6 space-y-6 h-[80vh] flex flex-col overflow-y-auto hide-scrollbar"> <div className="p-6 space-y-6 h-[80vh] flex flex-col overflow-y-auto hide-scrollbar">
{/* 文本输入区域 */} {/* 文本输入区域 */}
<div className="space-y-2 flex-shrink-0"> <div className="space-y-2 flex-shrink-0">
<label className="text-sm text-white/70"></label> <label className="text-sm text-white/70">describe the scene</label>
<textarea <textarea
className="w-full h-32 p-4 bg-white/5 border border-white/10 rounded-lg className="w-full h-32 p-4 bg-white/5 border border-white/10 rounded-lg
text-white placeholder-white/30 resize-none focus:outline-none focus:border-blue-500" text-white placeholder-white/30 resize-none focus:outline-none focus:border-blue-500"
placeholder="描述你想要生成的视频场景..." placeholder="describe the scene you want to generate..."
value={text} value={text}
onChange={(e) => setText(e.target.value)} onChange={(e) => setText(e.target.value)}
/> />
@ -110,7 +110,7 @@ export function GenerateVideoModal({
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
> >
generate video
</motion.button> </motion.button>
</div> </div>
@ -131,14 +131,14 @@ export function GenerateVideoModal({
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
onClick={onClose} onClick={onClose}
> >
back
</motion.button> </motion.button>
<motion.button <motion.button
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors" className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
> >
apply
</motion.button> </motion.button>
</div> </div>
</div> </div>

View File

@ -0,0 +1,87 @@
'use client';
import { motion, AnimatePresence } from 'framer-motion';
import { X } from 'lucide-react';
import WorkOffice from '@/components/workflow/work-office/work-office';
interface ScriptModalProps {
isOpen: boolean;
onClose: () => void;
}
export function ScriptModal({ isOpen, onClose }: ScriptModalProps) {
return (
<AnimatePresence mode="wait">
{isOpen && (
<>
{/* 背景遮罩 */}
<motion.div
className="fixed inset-0 bg-black/20 backdrop-blur-sm z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
onClick={onClose}
/>
{/* 弹窗内容 */}
<motion.div
className="fixed inset-0 z-50 flex items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
<motion.div
className="relative w-11/12 h-[90vh] bg-white/80 dark:bg-gray-900/80 backdrop-blur-xl rounded-2xl shadow-2xl overflow-hidden"
initial={{ scale: 0.95, y: 10, opacity: 0 }}
animate={{
scale: 1,
y: 0,
opacity: 1,
transition: {
type: "spring",
duration: 0.3,
bounce: 0.15,
stiffness: 300,
damping: 25
}
}}
exit={{
scale: 0.95,
y: 10,
opacity: 0,
transition: {
type: "tween",
duration: 0.1,
ease: "easeOut"
}
}}
>
{/* 关闭按钮 */}
<motion.button
className="absolute z-50 top-4 right-4 p-2 rounded-full bg-gray-100/80 dark:bg-gray-800/80 hover:bg-gray-200/80 dark:hover:bg-gray-700/80 transition-colors"
onClick={onClose}
whileHover={{ rotate: 90 }}
whileTap={{ scale: 0.9 }}
transition={{ duration: 0.1 }}
>
<X className="w-5 h-5 text-gray-600 dark:text-gray-300" />
</motion.button>
{/* 内容区域 */}
<motion.div
className="h-full overflow-auto p-6 relative"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.1, duration: 0.2 }}
>
<WorkOffice />
</motion.div>
</motion.div>
</motion.div>
</>
)}
</AnimatePresence>
);
}

View File

@ -71,7 +71,7 @@ export function ScriptTabContent({
<div className="relative"> <div className="relative">
<div <div
ref={thumbnailsRef} ref={thumbnailsRef}
className="flex gap-4 overflow-x-auto pb-2 pt-2 hide-scrollbar" className="flex gap-4 overflow-x-auto p-2 hide-scrollbar"
> >
{sketches.map((sketch, index) => ( {sketches.map((sketch, index) => (
<motion.div <motion.div

View File

@ -107,7 +107,7 @@ export function VideoTabContent({
<div className="relative"> <div className="relative">
<div <div
ref={thumbnailsRef} ref={thumbnailsRef}
className="flex gap-4 overflow-x-auto pb-2 pt-2 hide-scrollbar" className="flex gap-4 overflow-x-auto p-2 hide-scrollbar"
> >
{sketches.map((sketch, index) => ( {sketches.map((sketch, index) => (
<motion.div <motion.div

View File

@ -0,0 +1,20 @@
import React from 'react';
import { motion } from 'framer-motion';
interface ContentCardProps {
children: React.ReactNode;
className?: string;
}
export const ContentCard: React.FC<ContentCardProps> = ({ children, className = "" }) => {
return (
<motion.div
className={className}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
{children}
</motion.div>
);
};

View File

@ -0,0 +1,43 @@
import React from 'react';
import { motion } from 'framer-motion';
interface DotLoadingProps {
isActive: boolean;
color?: string;
size?: 'small' | 'medium' | 'large';
}
export const DotLoading: React.FC<DotLoadingProps> = ({
isActive,
color = '#ffffff',
size = 'small'
}) => {
if (!isActive) return null;
const dotSize = {
small: 'w-1 h-1',
medium: 'w-1.5 h-1.5',
large: 'w-2 h-2'
}[size];
return (
<div className="flex space-x-1">
{[0, 1, 2].map((i) => (
<motion.div
key={i}
className={`rounded-full ${dotSize}`}
style={{ backgroundColor: color }}
animate={{
scale: [1, 1.5, 1],
opacity: [1, 0.5, 1]
}}
transition={{
duration: 1,
repeat: Infinity,
delay: i * 0.2
}}
/>
))}
</div>
);
};

View File

@ -0,0 +1,43 @@
import React from 'react';
import { motion } from 'framer-motion';
import { LucideIcon } from 'lucide-react';
interface IconLoadingProps {
icon: LucideIcon;
isActive: boolean;
color: string;
}
export const IconLoading: React.FC<IconLoadingProps> = ({ icon: Icon, isActive, color }) => {
if (!isActive) return null;
return (
<div className="flex items-center space-x-2">
<motion.div
animate={{ rotate: [0, 360] }}
transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
>
<Icon className="w-4 h-4" style={{ color }} />
</motion.div>
<div className="flex space-x-1">
{[0, 1, 2].map((i) => (
<motion.div
key={i}
className="w-1 h-1 rounded-full"
style={{ backgroundColor: color }}
animate={{
y: [0, -4, 0],
opacity: [0.4, 1, 0.4]
}}
transition={{
duration: 1.2,
repeat: Infinity,
delay: i * 0.2,
ease: "easeInOut"
}}
/>
))}
</div>
</div>
);
};

View File

@ -0,0 +1,30 @@
import React from 'react';
import { motion } from 'framer-motion';
interface ProgressBarProps {
progress: number;
color?: string;
label?: string;
}
export const ProgressBar: React.FC<ProgressBarProps> = ({ progress, color = '#10b981', label }) => {
return (
<div className="w-full">
<div className="relative h-2 bg-black/30 rounded-full overflow-hidden">
<motion.div
className="absolute top-0 left-0 h-full rounded-full"
style={{ backgroundColor: color }}
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.5 }}
/>
</div>
{label && (
<div className="flex justify-between items-center mt-1">
<span className="text-xs text-gray-400">{label}</span>
<span className="text-xs" style={{ color }}>{progress}%</span>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,36 @@
import React from 'react';
import { motion } from 'framer-motion';
interface SkeletonCardProps {
className?: string;
}
export const SkeletonCard: React.FC<SkeletonCardProps> = ({ className = "" }) => {
return (
<div className={`bg-gray-700/20 rounded-lg p-3 relative overflow-hidden ${className}`}>
<div className="space-y-2">
<div className="h-4 bg-gray-600 rounded w-3/4 relative overflow-hidden">
<motion.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent"
animate={{ x: ['-100%', '100%'] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "linear" }}
/>
</div>
<div className="h-3 bg-gray-600 rounded w-full relative overflow-hidden">
<motion.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent"
animate={{ x: ['-100%', '100%'] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "linear" }}
/>
</div>
<div className="h-3 bg-gray-600 rounded w-1/2 relative overflow-hidden">
<motion.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent"
animate={{ x: ['-100%', '100%'] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "linear" }}
/>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,87 @@
import React, { useState, useEffect, useRef } from 'react';
import { motion } from 'framer-motion';
interface TypewriterTextProps {
text: string;
stableId: string;
}
export const TypewriterText: React.FC<TypewriterTextProps> = ({ text, stableId }) => {
const [displayState, setDisplayState] = useState({
displayText: '',
isTyping: false,
isComplete: false
});
const animationRef = useRef<NodeJS.Timeout | null>(null);
// 重置动画的函数
const startTypingAnimation = () => {
if (!text || !stableId) {
return;
}
// 清除现有的动画
if (animationRef.current) {
clearTimeout(animationRef.current);
}
setDisplayState({
displayText: '',
isTyping: true,
isComplete: false
});
let currentIndex = 0;
const typeNextChar = () => {
if (currentIndex < text.length) {
const newText = text.slice(0, currentIndex + 1);
setDisplayState({
displayText: newText,
isTyping: true,
isComplete: false
});
currentIndex++;
animationRef.current = setTimeout(typeNextChar, 30);
} else {
setDisplayState({
displayText: text,
isTyping: false,
isComplete: true
});
}
};
animationRef.current = setTimeout(typeNextChar, 100);
};
// 当 text 或 stableId 变化时重新开始动画
useEffect(() => {
startTypingAnimation();
return () => {
if (animationRef.current) {
clearTimeout(animationRef.current);
}
};
}, [text, stableId]);
// 如果没有文本,直接返回 null
if (!text) {
return null;
}
return (
<span className="relative inline-block">
{displayState.displayText}
{displayState.isTyping && (
<motion.span
className="inline-block w-0.5 h-4 bg-current ml-1 align-middle"
animate={{ opacity: [0, 1, 0] }}
transition={{ duration: 0.8, repeat: Infinity }}
/>
)}
</span>
);
};

View File

@ -0,0 +1,300 @@
import React from 'react';
import { Scissors } from 'lucide-react';
import { TypewriterText } from './common/TypewriterText';
import { ContentCard } from './common/ContentCard';
import { ProgressBar } from './common/ProgressBar';
import { DotLoading } from './common/DotLoading';
import { SkeletonCard } from './common/SkeletonCard';
import { IconLoading } from './common/IconLoading';
interface EditingContent {
rhythm?: {
stableId: string;
concept: string;
application: string;
current: string;
progress: number;
};
audioVideo?: Array<{
id: string;
stableId: string;
aspect: string;
details: string;
sync?: string;
balance?: string;
progress: number;
}>;
emotionProgression?: {
stableId: string;
stages: string;
techniques: string;
current: string;
progress: number;
};
transitions?: Array<{
id: string;
stableId: string;
type: string;
usage: string;
}>;
styleUnity?: {
stableId: string;
colorGrading: string;
toneCurve: string;
progress: number;
};
finalOutput?: {
format: string;
resolution: string;
bitrate: string;
audio: string;
duration: string;
status: string;
progress: number;
};
}
interface EditorProps {
currentContent: EditingContent;
isPlaying: boolean;
}
const Editor: React.FC<EditorProps> = ({ currentContent, isPlaying }) => {
return (
<div className="space-y-4 h-full overflow-y-auto">
{/* 剪辑节奏把控 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Scissors} isActive={!currentContent.rhythm && isPlaying} color="#f59e0b" />
</h3>
{currentContent.rhythm ? (
<ContentCard className="space-y-3">
<div>
<div className="text-amber-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.rhythm.concept} stableId={`${currentContent.rhythm.stableId}-concept`} />
</div>
</div>
<div>
<div className="text-amber-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.rhythm.application} stableId={`${currentContent.rhythm.stableId}-application`} />
</div>
</div>
<div>
<div className="text-amber-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.rhythm.current} stableId={`${currentContent.rhythm.stableId}-current`} />
</div>
</div>
<ProgressBar progress={currentContent.rhythm.progress} color="#f59e0b" label="节奏分析" />
</ContentCard>
) : (
<div className="space-y-3">
{Array.from({length: 3}, (_, i) => (
<SkeletonCard key={i} className="bg-amber-500/20 rounded-lg p-3 border border-amber-500/30" />
))}
<SkeletonCard className="h-2 bg-amber-500/20 rounded-full border border-amber-500/30" />
</div>
)}
</div>
{/* 音画关系处理 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Scissors} isActive={!currentContent.audioVideo && isPlaying} color="#f59e0b" />
</h3>
<div className="space-y-3">
{currentContent.audioVideo ? (
currentContent.audioVideo.map((av) => (
<ContentCard
key={av.stableId}
className="bg-amber-500/20 rounded-lg p-3"
>
<div className="text-amber-300 font-medium text-sm mb-1">{av.aspect}</div>
<div className="text-gray-300 text-xs mb-2">
<TypewriterText text={av.details} stableId={av.stableId} />
</div>
<div className="text-amber-200 text-xs mb-2">
{av.sync && `同步精度: ${av.sync}`}
{av.balance && `平衡策略: ${av.balance}`}
</div>
<ProgressBar progress={av.progress} color="#f59e0b" />
</ContentCard>
))
) : (
Array.from({length: 3}, (_, i) => (
<SkeletonCard key={i} className="bg-amber-500/20 rounded-lg p-3 border border-amber-500/30" />
))
)}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
{/* 情绪递进调校 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Scissors} isActive={!currentContent.emotionProgression && isPlaying} color="#f59e0b" />
</h3>
{currentContent.emotionProgression ? (
<ContentCard className="space-y-3">
<div>
<div className="text-amber-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.emotionProgression.stages} stableId={`${currentContent.emotionProgression.stableId}-stages`} />
</div>
</div>
<div>
<div className="text-amber-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.emotionProgression.techniques} stableId={`${currentContent.emotionProgression.stableId}-techniques`} />
</div>
</div>
<div>
<div className="text-amber-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.emotionProgression.current} stableId={`${currentContent.emotionProgression.stableId}-current`} />
</div>
</div>
<ProgressBar progress={currentContent.emotionProgression.progress} color="#f59e0b" label="情绪调校" />
</ContentCard>
) : (
<div className="space-y-3">
{Array.from({length: 3}, (_, i) => (
<SkeletonCard key={i} className="bg-amber-500/20 rounded-lg p-3 border border-amber-500/30" />
))}
<SkeletonCard className="h-2 bg-amber-500/20 rounded-full border border-amber-500/30" />
</div>
)}
</div>
{/* 转场效果选择 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Scissors} isActive={!currentContent.transitions && isPlaying} color="#f59e0b" />
</h3>
<div className="space-y-2">
{currentContent.transitions ? (
currentContent.transitions.map((trans) => (
<ContentCard
key={trans.stableId}
className="bg-amber-400/20 rounded p-2"
>
<div className="text-amber-200 text-sm font-medium">{trans.type}</div>
<div className="text-gray-300 text-xs">
<TypewriterText text={trans.usage} stableId={trans.stableId} />
</div>
</ContentCard>
))
) : (
Array.from({length: 4}, (_, i) => (
<SkeletonCard key={i} className="bg-amber-500/20 rounded-lg p-2 border border-amber-500/30" />
))
)}
</div>
</div>
</div>
{/* 整体风格统一 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Scissors} isActive={!currentContent.styleUnity && isPlaying} color="#f59e0b" />
</h3>
{currentContent.styleUnity ? (
<ContentCard className="space-y-3">
<div>
<div className="text-amber-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.styleUnity.colorGrading} stableId={`${currentContent.styleUnity.stableId}-colorGrading`} />
</div>
</div>
<div>
<div className="text-amber-300 text-sm font-medium mb-1">线</div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.styleUnity.toneCurve} stableId={`${currentContent.styleUnity.stableId}-toneCurve`} />
</div>
</div>
<ProgressBar progress={currentContent.styleUnity.progress} color="#f59e0b" label="风格统一" />
</ContentCard>
) : (
<div className="space-y-3">
{Array.from({length: 2}, (_, i) => (
<SkeletonCard key={i} className="bg-amber-500/20 rounded-lg p-3 border border-amber-500/30" />
))}
<SkeletonCard className="h-2 bg-amber-500/20 rounded-full border border-amber-500/30" />
</div>
)}
</div>
{/* 最终输出 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
{currentContent.finalOutput ? (
<DotLoading isActive={currentContent.finalOutput.progress < 100} color="#f59e0b" size="medium" />
) : (
<IconLoading icon={Scissors} isActive={isPlaying} color="#f59e0b" />
)}
</h3>
{currentContent.finalOutput ? (
<>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
{Object.entries({
'格式': currentContent.finalOutput.format,
'分辨率': currentContent.finalOutput.resolution,
'码率': currentContent.finalOutput.bitrate
}).map(([key, value]) => (
<div key={key} className="flex justify-between text-xs">
<span className="text-gray-400">{key}:</span>
<span className="text-amber-300">{value}</span>
</div>
))}
</div>
<div className="space-y-2">
{Object.entries({
'音频': currentContent.finalOutput.audio,
'时长': currentContent.finalOutput.duration,
'状态': currentContent.finalOutput.status
}).map(([key, value]) => (
<div key={key} className="flex justify-between text-xs">
<span className="text-gray-400">{key}:</span>
<span className="text-amber-300">{value}</span>
</div>
))}
</div>
</div>
<div className="mt-4">
<ProgressBar progress={currentContent.finalOutput.progress} color="#f59e0b" label="输出进度" />
</div>
</>
) : (
<>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
{Array.from({length: 3}, (_, i) => (
<SkeletonCard key={i} className="h-6 bg-amber-500/20 rounded border border-amber-500/30" />
))}
</div>
<div className="space-y-2">
{Array.from({length: 3}, (_, i) => (
<SkeletonCard key={i} className="h-6 bg-amber-500/20 rounded border border-amber-500/30" />
))}
</div>
</div>
<div className="mt-4">
<SkeletonCard className="h-2 bg-amber-500/20 rounded-full border border-amber-500/30" />
</div>
</>
)}
</div>
</div>
);
};
export default Editor;

View File

@ -0,0 +1,320 @@
// 编剧工作台数据
export const scriptwriterData = {
acts: [
{
id: '1',
stableId: 'act1',
title: '第一幕:开端',
desc: '故事背景设定与主要人物介绍',
beats: ['人物登场', '冲突埋设', '情节推进']
},
{
id: '2',
stableId: 'act2',
title: '第二幕:发展',
desc: '矛盾冲突的展开与升级',
beats: ['冲突加剧', '危机显现', '情节转折']
},
{
id: '3',
stableId: 'act3',
title: '第三幕:高潮',
desc: '故事达到最高潮并迎来结局',
beats: ['最终对决', '问题解决', '情节收束']
}
],
characters: [
{
id: '1',
stableId: 'char1',
name: '主角',
role: '核心人物',
arc: '成长蜕变',
desc: '内心独白与行为动机的深入刻画',
color: '#8b5cf6'
},
{
id: '2',
stableId: 'char2',
name: '对手',
role: '反派角色',
arc: '阴谋败露',
desc: '反派形象的立体化塑造',
color: '#ec4899'
}
],
dialogue: {
stableId: 'dialogue1',
rhythm: '对白节奏的快慢变化',
style: '人物语言风格的统一'
},
themes: [
{
id: '1',
stableId: 'theme1',
theme: '主题探索',
desc: '故事核心主题的层层深入',
depth: '通过情节与对白的双重展现'
},
{
id: '2',
stableId: 'theme2',
theme: '情感刻画',
desc: '人物情感的细腻表达',
depth: '借助环境与氛围的烘托'
}
],
dramaticLine: {
stableId: 'dramaticLine1',
points: [
{
id: '1',
stableId: 'point1',
title: '开场',
desc: '故事开始',
intensity: 20
},
{
id: '2',
stableId: 'point2',
title: '引子',
desc: '背景介绍',
intensity: 35
},
{
id: '3',
stableId: 'point3',
title: '发展',
desc: '冲突显现',
intensity: 60
},
{
id: '4',
stableId: 'point4',
title: '高潮',
desc: '矛盾爆发',
intensity: 85
},
{
id: '5',
stableId: 'point5',
title: '结局',
desc: '问题解决',
intensity: 45
},
{
id: '6',
stableId: 'point6',
title: '尾声',
desc: '故事收束',
intensity: 30
}
]
}
};
// 分镜设计台数据
export const storyboardData = {
shotLanguage: [
{
id: '1',
stableId: 'shot1',
type: '远景镜头',
purpose: '展现场景全貌',
usage: '用于开场和转场,建立空间感和氛围'
},
{
id: '2',
stableId: 'shot2',
type: '特写镜头',
purpose: '突出细节表情',
usage: '用于情感渲染和关键道具展示'
}
],
composition: {
stableId: 'comp1',
principles: '黄金分割和三分法则的运用',
aesthetics: '画面构图的美感营造',
framing: '取景框的合理设置'
},
cameraMovement: [
{
id: '1',
stableId: 'cam1',
type: '推轨',
purpose: '渲染情绪',
application: '角色情感的空间表达'
},
{
id: '2',
stableId: 'cam2',
type: '摇臂',
purpose: '场景转换',
application: '空间层次的流畅过渡'
}
],
visualNarrative: {
stableId: 'visual1',
logic: '视觉叙事的连贯性',
progression: '故事节奏的视觉把控',
emphasis: '重点情节的视觉强调'
},
editingPoints: [
{
id: '1',
stableId: 'edit1',
moment: '场景转换',
cut: '通过物体运动的自然切换'
},
{
id: '2',
stableId: 'edit2',
moment: '情感高潮',
cut: '快速剪辑的节奏渲染'
}
]
};
// 制作渲染台数据
export const productionData = {
composition: [
{
id: '1',
stableId: 'comp1',
element: '场景布局',
details: '场景元素的空间排布与层次关系',
status: '渲染中',
progress: 65
},
{
id: '2',
stableId: 'comp2',
element: '角色位置',
details: '人物在场景中的站位与动线设计',
status: '优化中',
progress: 80
}
],
lighting: {
stableId: 'light1',
ambient: '自然光效果的模拟与调节',
artificial: '人工光源的位置布置',
mood: '通过光影营造场景氛围',
progress: 75
},
performance: [
{
id: '1',
stableId: 'perf1',
aspect: '面部表情',
details: '细微表情的精确捕捉',
quality: '高品质',
progress: 90
},
{
id: '2',
stableId: 'perf2',
aspect: '肢体动作',
details: '动作的流畅性与自然度',
quality: '优化中',
progress: 85
}
],
sceneDetails: {
stableId: 'scene1',
textures: '材质细节的精细处理',
objects: '场景道具的细节优化',
atmosphere: '整体氛围的烘托渲染',
progress: 70
},
technical: [
{
param: '分辨率',
value: '4K',
status: 'optimized'
},
{
param: '帧率',
value: '60fps',
status: 'processing'
},
{
param: '渲染引擎',
value: 'Cycles',
status: 'active'
}
],
renderOutput: {
currentFrame: 1500,
totalFrames: 2400,
quality: '最终质量',
estimated: '预计15分钟'
}
};
// 剪辑调色台数据
export const editorData = {
rhythm: {
stableId: 'rhythm1',
concept: '节奏的整体规划与设计',
application: '快慢节奏的合理搭配',
current: '正在优化转场节奏',
progress: 85
},
audioVideo: [
{
id: '1',
stableId: 'av1',
aspect: '音画同步',
details: '确保声画精确匹配',
sync: '帧级精度',
balance: '优',
progress: 90
},
{
id: '2',
stableId: 'av2',
aspect: '音效处理',
details: '环境音效的自然融合',
sync: '毫秒级',
balance: '良好',
progress: 85
}
],
emotionProgression: {
stableId: 'emotion1',
stages: '情感递进的节奏控制',
techniques: '通过剪辑手法强化情感',
current: '高潮段落调校中',
progress: 75
},
transitions: [
{
id: '1',
stableId: 'trans1',
type: '淡入淡出',
usage: '用于时空转换的柔和过渡'
},
{
id: '2',
stableId: 'trans2',
type: '快速切换',
usage: '用于紧张氛围的营造'
}
],
styleUnity: {
stableId: 'style1',
colorGrading: '色彩基调的统一处理',
toneCurve: '明暗对比的整体调校',
progress: 80
},
finalOutput: {
format: 'MP4 H.265',
resolution: '4K UHD',
bitrate: '50Mbps',
audio: '5.1声道',
duration: '15:30',
status: '渲染中',
progress: 65
}
};

View File

@ -0,0 +1,289 @@
import React from 'react';
import { Heart } from 'lucide-react';
import { motion } from 'framer-motion';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { TypewriterText } from './common/TypewriterText';
import { ContentCard } from './common/ContentCard';
import { SkeletonCard } from './common/SkeletonCard';
import { IconLoading } from './common/IconLoading';
interface ScriptContent {
acts?: Array<{
id: string;
stableId: string;
title: string;
desc: string;
beats: string[];
}>;
characters?: Array<{
id: string;
stableId: string;
name: string;
role: string;
arc: string;
desc: string;
color: string;
}>;
dialogue?: {
stableId: string;
rhythm: string;
style: string;
};
themes?: Array<{
id: string;
stableId: string;
theme: string;
desc: string;
depth: string;
}>;
dramaticLine?: {
stableId: string;
points: Array<{
id: string;
stableId: string;
title: string;
desc: string;
intensity: number; // 0-100 情感强度
}>;
};
}
interface ScriptwriterProps {
currentContent: ScriptContent;
isPlaying: boolean;
}
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="bg-black/80 backdrop-blur-sm p-2 rounded-lg border border-purple-500/30">
<p className="text-purple-300 text-xs font-medium">{payload[0].payload.title}</p>
<p className="text-white text-xs">{`情感强度: ${payload[0].value}`}</p>
</div>
);
}
return null;
};
const Scriptwriter: React.FC<ScriptwriterProps> = ({ currentContent, isPlaying }) => {
return (
<div className="grid grid-cols-2 gap-4 h-full">
{/* 左侧:三幕结构和角色弧光 */}
<div className="space-y-4 overflow-y-auto">
{/* 三幕结构 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Heart} isActive={!currentContent.acts && isPlaying} color="#8b5cf6" />
</h3>
<div className="space-y-3">
{currentContent.acts && currentContent.acts.length > 0 ? (
currentContent.acts.map((act) => (
<ContentCard
key={act.stableId}
className="bg-purple-500/20 rounded-lg p-3 border border-purple-500/30"
>
<div className="text-purple-300 font-medium text-sm mb-2">
{act.title}
</div>
<div className="text-gray-300 text-xs leading-relaxed mb-2">
<TypewriterText text={act.desc} stableId={act.stableId} />
</div>
<div className="flex flex-wrap gap-1">
{act.beats?.map((beat, index) => (
<span key={index} className="text-xs bg-purple-500/30 px-2 py-1 rounded">
{beat}
</span>
))}
</div>
</ContentCard>
))
) : (
Array.from({length: 3}, (_, i) => (
<SkeletonCard key={i} className="bg-purple-500/20 rounded-lg p-3 border border-purple-500/30" />
))
)}
</div>
</div>
{/* 角色弧光设计 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Heart} isActive={!currentContent.characters && isPlaying} color="#8b5cf6" />
</h3>
<div className="space-y-3">
{currentContent.characters && currentContent.characters.length > 0 ? (
currentContent.characters.map((char) => (
<ContentCard
key={char.stableId}
className="bg-slate-700/50 rounded-lg p-3 border border-slate-600/50"
>
<div className="flex items-center space-x-2 mb-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: char.color }}
/>
<span className="text-white font-medium text-sm">{char.name}</span>
<span className="text-gray-400 text-xs">({char.role})</span>
</div>
<div className="text-xs mb-2" style={{ color: char.color }}>
{char.arc}
</div>
<div className="text-gray-300 text-xs leading-relaxed">
<TypewriterText text={char.desc} stableId={char.stableId} />
</div>
</ContentCard>
))
) : (
Array.from({length: 3}, (_, i) => (
<SkeletonCard key={i} className="bg-purple-500/20 rounded-lg p-3 border border-purple-500/30" />
))
)}
</div>
</div>
</div>
{/* 右侧:对白节奏和主题深化 */}
<div className="space-y-4 overflow-y-auto">
{/* 对白节奏感 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Heart} isActive={!currentContent.dialogue && isPlaying} color="#8b5cf6" />
</h3>
{currentContent.dialogue ? (
<div className="space-y-3">
<div>
<div className="text-purple-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.dialogue.rhythm} stableId={`${currentContent.dialogue.stableId}-rhythm`} />
</div>
</div>
<div>
<div className="text-purple-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.dialogue.style} stableId={`${currentContent.dialogue.stableId}-style`} />
</div>
</div>
</div>
) : (
<SkeletonCard className="bg-purple-500/20 rounded-lg p-3 border border-purple-500/30" />
)}
</div>
{/* 主题深化过程 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Heart} isActive={!currentContent.themes && isPlaying} color="#8b5cf6" />
</h3>
<div className="space-y-3">
{currentContent.themes ? (
<>
{currentContent.themes.map((theme) => (
<ContentCard
key={theme.stableId}
className="bg-purple-400/10 rounded p-3"
>
<div className="text-purple-200 text-sm font-medium mb-1">{theme.theme}</div>
<div className="text-gray-300 text-xs mb-2">
<TypewriterText text={theme.desc} stableId={`${theme.stableId}-desc`} />
</div>
<div className="text-gray-400 text-xs">
<TypewriterText text={theme.depth} stableId={`${theme.stableId}-depth`} />
</div>
</ContentCard>
))}
</>
) : (
Array.from({length: 2}, (_, i) => (
<SkeletonCard key={i} className="bg-purple-500/20 rounded-lg p-3 border border-purple-500/30" />
))
)}
</div>
</div>
{/* 剧情起伏戏演线 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span>线</span>
<IconLoading icon={Heart} isActive={!currentContent.dramaticLine && isPlaying} color="#8b5cf6" />
</h3>
{currentContent.dramaticLine ? (
<div className="space-y-4">
{/* 情感强度曲线图 */}
<div className="h-48 bg-purple-500/10 rounded-lg p-2">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={currentContent.dramaticLine?.points || []}
margin={{ top: 10, right: 10, left: 0, bottom: 10 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis
dataKey="title"
tick={{ fill: '#9CA3AF', fontSize: 10 }}
stroke="#4B5563"
/>
<YAxis
domain={[0, 100]}
tick={{ fill: '#9CA3AF', fontSize: 10 }}
stroke="#4B5563"
label={{
value: '情感强度',
angle: -90,
position: 'insideLeft',
fill: '#9CA3AF',
fontSize: 12
}}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="intensity"
stroke="#8B5CF6"
strokeWidth={2}
dot={{
fill: '#8B5CF6',
stroke: '#C4B5FD',
strokeWidth: 2,
r: 4
}}
activeDot={{
fill: '#8B5CF6',
stroke: '#C4B5FD',
strokeWidth: 2,
r: 6
}}
/>
</LineChart>
</ResponsiveContainer>
</div>
{/* 关键点说明 */}
<div className="space-y-2">
{currentContent.dramaticLine?.points?.map((point) => (
<ContentCard
key={point.stableId}
className="bg-purple-400/10 rounded p-2"
>
<div className="text-purple-200 text-sm font-medium mb-1">{point.title}</div>
<div className="text-gray-300 text-xs">
<TypewriterText text={point.desc} stableId={point.stableId} />
</div>
</ContentCard>
))}
</div>
</div>
) : (
<div className="space-y-4">
{/* 骨架屏曲线图 */}
<SkeletonCard className="h-48 bg-purple-500/20 rounded-lg border border-purple-500/30" />
</div>
)}
</div>
</div>
</div>
);
};
export default Scriptwriter;

View File

@ -0,0 +1,219 @@
import React from 'react';
import { Camera } from 'lucide-react';
import { TypewriterText } from './common/TypewriterText';
import { ContentCard } from './common/ContentCard';
import { SkeletonCard } from './common/SkeletonCard';
import { IconLoading } from './common/IconLoading';
interface StoryboardContent {
shotLanguage?: Array<{
id: string;
stableId: string;
type: string;
purpose: string;
usage: string;
}>;
composition?: {
stableId: string;
principles: string;
aesthetics: string;
framing: string;
};
cameraMovement?: Array<{
id: string;
stableId: string;
type: string;
purpose: string;
application: string;
}>;
visualNarrative?: {
stableId: string;
logic: string;
progression: string;
emphasis: string;
};
editingPoints?: Array<{
id: string;
stableId: string;
moment: string;
cut: string;
}>;
}
interface StoryboardArtistProps {
currentContent: StoryboardContent;
isPlaying: boolean;
}
const StoryboardArtist: React.FC<StoryboardArtistProps> = ({ currentContent, isPlaying }) => {
return (
<div className="grid grid-cols-2 gap-4 h-full">
{/* 左侧:镜头语言和构图美学 */}
<div className="space-y-4 overflow-y-auto">
{/* 镜头语言选择 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Camera} isActive={!currentContent.shotLanguage && isPlaying} color="#06b6d4" />
</h3>
<div className="space-y-3">
{currentContent.shotLanguage && currentContent.shotLanguage.length > 0 ? (
currentContent.shotLanguage.map((shot) => (
<ContentCard
key={shot.stableId}
className="bg-cyan-500/20 rounded-lg p-3 border border-cyan-500/30"
>
<div className="flex justify-between items-center mb-2">
<span className="text-cyan-300 font-medium text-sm">{shot.type}</span>
<span className="text-gray-400 text-xs">{shot.purpose}</span>
</div>
<div className="text-gray-300 text-xs leading-relaxed">
<TypewriterText text={shot.usage} stableId={shot.stableId} />
</div>
</ContentCard>
))
) : (
Array.from({length: 4}, (_, i) => (
<SkeletonCard key={i} className="bg-cyan-500/20 rounded-lg p-3 border border-cyan-500/30" />
))
)}
</div>
</div>
{/* 构图美学运用 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Camera} isActive={!currentContent.composition && isPlaying} color="#06b6d4" />
</h3>
{currentContent.composition ? (
<ContentCard className="space-y-3">
<div>
<div className="text-cyan-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.composition.principles} stableId={`${currentContent.composition.stableId}-principles`} />
</div>
</div>
<div>
<div className="text-cyan-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.composition.aesthetics} stableId={`${currentContent.composition.stableId}-aesthetics`} />
</div>
</div>
<div>
<div className="text-cyan-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.composition.framing} stableId={`${currentContent.composition.stableId}-framing`} />
</div>
</div>
</ContentCard>
) : (
<div className="space-y-3">
{Array.from({length: 3}, (_, i) => (
<SkeletonCard key={i} className="bg-cyan-500/20 rounded-lg p-3 border border-cyan-500/30" />
))}
</div>
)}
</div>
</div>
{/* 右侧:摄影机运动和叙事逻辑 */}
<div className="space-y-4 overflow-y-auto">
{/* 摄影机运动设计 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Camera} isActive={!currentContent.cameraMovement && isPlaying} color="#06b6d4" />
</h3>
<div className="space-y-3">
{currentContent.cameraMovement ? (
currentContent.cameraMovement.map((move) => (
<ContentCard
key={move.stableId}
className="bg-cyan-400/20 rounded-lg p-3"
>
<div className="text-cyan-200 font-medium text-sm mb-1">{move.type}</div>
<div className="text-gray-300 text-xs mb-2">
<TypewriterText text={move.purpose} stableId={`${move.stableId}-purpose`} />
</div>
<div className="text-cyan-300 text-xs">
<TypewriterText text={move.application} stableId={`${move.stableId}-application`} />
</div>
</ContentCard>
))
) : (
Array.from({length: 3}, (_, i) => (
<SkeletonCard key={i} className="bg-cyan-500/20 rounded-lg p-3 border border-cyan-500/30" />
))
)}
</div>
</div>
{/* 视觉叙事逻辑 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Camera} isActive={!currentContent.visualNarrative && isPlaying} color="#06b6d4" />
</h3>
{currentContent.visualNarrative ? (
<ContentCard className="space-y-3">
<div>
<div className="text-cyan-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.visualNarrative.logic} stableId={`${currentContent.visualNarrative.stableId}-logic`} />
</div>
</div>
<div>
<div className="text-cyan-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.visualNarrative.progression} stableId={`${currentContent.visualNarrative.stableId}-progression`} />
</div>
</div>
<div>
<div className="text-cyan-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.visualNarrative.emphasis} stableId={`${currentContent.visualNarrative.stableId}-emphasis`} />
</div>
</div>
</ContentCard>
) : (
<div className="space-y-3">
{Array.from({length: 3}, (_, i) => (
<SkeletonCard key={i} className="bg-cyan-500/20 rounded-lg p-3 border border-cyan-500/30" />
))}
</div>
)}
</div>
{/* 剪辑点预设 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Camera} isActive={!currentContent.editingPoints && isPlaying} color="#06b6d4" />
</h3>
<div className="space-y-2">
{currentContent.editingPoints ? (
currentContent.editingPoints.map((point) => (
<ContentCard
key={point.stableId}
className="bg-cyan-300/20 rounded p-2"
>
<div className="text-cyan-200 text-sm font-medium">{point.moment}</div>
<div className="text-gray-300 text-xs">
<TypewriterText text={point.cut} stableId={point.stableId} />
</div>
</ContentCard>
))
) : (
Array.from({length: 4}, (_, i) => (
<SkeletonCard key={i} className="bg-cyan-500/20 rounded-lg p-3 border border-cyan-500/30" />
))
)}
</div>
</div>
</div>
</div>
);
};
export default StoryboardArtist;

View File

@ -0,0 +1,264 @@
import React from 'react';
import { Film } from 'lucide-react';
import { TypewriterText } from './common/TypewriterText';
import { ContentCard } from './common/ContentCard';
import { SkeletonCard } from './common/SkeletonCard';
import { IconLoading } from './common/IconLoading';
import { ProgressBar } from './common/ProgressBar';
interface ProductionContent {
composition?: Array<{
id: string;
stableId: string;
element: string;
details: string;
status: string;
progress: number;
}>;
lighting?: {
stableId: string;
ambient: string;
artificial: string;
mood: string;
progress: number;
};
performance?: Array<{
id: string;
stableId: string;
aspect: string;
details: string;
quality: string;
progress: number;
}>;
sceneDetails?: {
stableId: string;
textures: string;
objects: string;
atmosphere: string;
progress: number;
};
technical?: Array<{
param: string;
value: string;
status: string;
}>;
renderOutput?: {
currentFrame: number;
totalFrames: number;
quality: string;
estimated: string;
};
}
interface VisualDirectorProps {
currentContent: ProductionContent;
isPlaying: boolean;
}
const VisualDirector: React.FC<VisualDirectorProps> = ({ currentContent, isPlaying }) => {
return (
<div className="grid grid-cols-2 gap-4 h-full">
{/* 左侧:画面构成和光影氛围 */}
<div className="space-y-4 overflow-y-auto">
{/* 画面构成要素 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Film} isActive={!currentContent.composition && isPlaying} color="#10b981" />
</h3>
<div className="space-y-3">
{currentContent.composition && currentContent.composition.length > 0 ? (
currentContent.composition.map((comp) => (
<ContentCard
key={comp.stableId}
className="bg-emerald-500/20 rounded-lg p-3"
>
<div className="flex justify-between items-center mb-2">
<span className="text-emerald-300 font-medium text-sm">{comp.element}</span>
<span className="text-emerald-400 text-xs">{comp.status}</span>
</div>
<div className="text-gray-300 text-xs mb-2">
<TypewriterText text={comp.details} stableId={comp.stableId} />
</div>
<ProgressBar progress={comp.progress} color="#10b981" />
</ContentCard>
))
) : (
Array.from({length: 3}, (_, i) => (
<SkeletonCard key={i} className="bg-emerald-500/20 rounded-lg p-3 border border-emerald-500/30" />
))
)}
</div>
</div>
{/* 光影氛围营造 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Film} isActive={!currentContent.lighting && isPlaying} color="#10b981" />
</h3>
{currentContent.lighting ? (
<ContentCard className="space-y-3">
<div>
<div className="text-emerald-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.lighting.ambient} stableId={`${currentContent.lighting.stableId}-ambient`} />
</div>
</div>
<div>
<div className="text-emerald-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.lighting.artificial} stableId={`${currentContent.lighting.stableId}-artificial`} />
</div>
</div>
<div>
<div className="text-emerald-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.lighting.mood} stableId={`${currentContent.lighting.stableId}-mood`} />
</div>
</div>
<ProgressBar progress={currentContent.lighting.progress} color="#10b981" label="光影渲染" />
</ContentCard>
) : (
<div className="space-y-3">
{Array.from({length: 3}, (_, i) => (
<SkeletonCard key={i} className="bg-emerald-500/20 rounded-lg p-3 border border-emerald-500/30" />
))}
<SkeletonCard className="h-2 bg-emerald-500/20 rounded-full border border-emerald-500/30" />
</div>
)}
</div>
</div>
{/* 右侧:角色表演和技术参数 */}
<div className="space-y-4 overflow-y-auto">
{/* 角色表演捕捉 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Film} isActive={!currentContent.performance && isPlaying} color="#10b981" />
</h3>
<div className="space-y-3">
{currentContent.performance ? (
currentContent.performance.map((perf) => (
<ContentCard
key={perf.stableId}
className="bg-emerald-400/20 rounded-lg p-3"
>
<div className="text-emerald-200 font-medium text-sm mb-1">{perf.aspect}</div>
<div className="text-gray-300 text-xs mb-2">
<TypewriterText text={perf.details} stableId={perf.stableId} />
</div>
<div className="text-emerald-300 text-xs mb-2">{perf.quality}</div>
<ProgressBar progress={perf.progress} color="#10b981" />
</ContentCard>
))
) : (
Array.from({length: 3}, (_, i) => (
<SkeletonCard key={i} className="bg-emerald-500/20 rounded-lg p-3 border border-emerald-500/30" />
))
)}
</div>
</div>
{/* 场景细节渲染 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Film} isActive={!currentContent.sceneDetails && isPlaying} color="#10b981" />
</h3>
{currentContent.sceneDetails ? (
<ContentCard className="space-y-3">
<div>
<div className="text-emerald-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.sceneDetails.textures} stableId={`${currentContent.sceneDetails.stableId}-textures`} />
</div>
</div>
<div>
<div className="text-emerald-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.sceneDetails.objects} stableId={`${currentContent.sceneDetails.stableId}-objects`} />
</div>
</div>
<div>
<div className="text-emerald-300 text-sm font-medium mb-1"></div>
<div className="text-gray-300 text-xs">
<TypewriterText text={currentContent.sceneDetails.atmosphere} stableId={`${currentContent.sceneDetails.stableId}-atmosphere`} />
</div>
</div>
<ProgressBar progress={currentContent.sceneDetails.progress} color="#10b981" label="细节渲染" />
</ContentCard>
) : (
<div className="space-y-3">
{Array.from({length: 3}, (_, i) => (
<SkeletonCard key={i} className="bg-emerald-500/20 rounded-lg p-3 border border-emerald-500/30" />
))}
<SkeletonCard className="h-2 bg-emerald-500/20 rounded-full border border-emerald-500/30" />
</div>
)}
</div>
{/* 技术参数控制 */}
<div className="bg-black/30 rounded-lg p-4">
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
<span></span>
<IconLoading icon={Film} isActive={!currentContent.technical && isPlaying} color="#10b981" />
</h3>
{currentContent.technical ? (
<>
<div className="space-y-2">
{currentContent.technical.map((tech, index) => (
<div key={index} className="flex justify-between items-center p-2 bg-emerald-500/10 rounded">
<span className="text-emerald-300 text-sm">{tech.param}</span>
<span className="text-white text-sm font-mono">{tech.value}</span>
<span className={`text-xs px-2 py-1 rounded flex items-center space-x-1 ${
tech.status === 'optimized' ? 'bg-green-500/20 text-green-400' :
tech.status === 'processing' ? 'bg-yellow-500/20 text-yellow-400' :
'bg-blue-500/20 text-blue-400'
}`}>
<span>{tech.status}</span>
</span>
</div>
))}
</div>
{/* 渲染进度 */}
{currentContent.renderOutput && (
<div className="mt-4 pt-4 border-t border-emerald-500/30">
<div className="text-center mb-3">
<div className="text-2xl font-mono text-emerald-400 mb-1">
{Math.round((currentContent.renderOutput.currentFrame / currentContent.renderOutput.totalFrames) * 100)}%
</div>
<div className="text-sm text-gray-400"></div>
</div>
<div className="text-xs text-center space-y-1">
<div className="text-white">
: {currentContent.renderOutput.currentFrame}/{currentContent.renderOutput.totalFrames}
</div>
<div className="text-emerald-400">
: {currentContent.renderOutput.quality} | {currentContent.renderOutput.estimated}
</div>
</div>
</div>
)}
</>
) : (
<>
<div className="space-y-2">
{Array.from({length: 4}, (_, i) => (
<SkeletonCard key={i} className="h-10 bg-emerald-500/20 rounded border border-emerald-500/30" />
))}
</div>
<div className="mt-4 pt-4 border-t border-emerald-500/30">
<SkeletonCard className="h-20 bg-emerald-500/20 rounded-lg border border-emerald-500/30" />
</div>
</>
)}
</div>
</div>
</div>
);
};
export default VisualDirector;

View File

@ -0,0 +1,250 @@
import React, { useState, useEffect } from 'react';
import { Heart, Camera, Film, Scissors } from 'lucide-react';
import { motion } from 'framer-motion';
import Scriptwriter from './scriptwriter';
import StoryboardArtist from './storyboard-artist';
import VisualDirector from './visual-director';
import Editor from './editor';
import { scriptwriterData, storyboardData, productionData, editorData } from './mock-data';
interface Stage {
id: string;
title: string;
icon: React.ElementType;
color: string;
profession: string;
duration: number; // 加载持续时间(毫秒)
}
const stages: Stage[] = [
{
id: 'script',
title: '编剧工作台',
icon: Heart,
color: '#8b5cf6',
profession: '编剧',
duration: 3 * 60 * 1000 // 3分钟
},
{
id: 'storyboard',
title: '分镜设计台',
icon: Camera,
color: '#06b6d4',
profession: '分镜师',
duration: 8 * 60 * 1000 // 8分钟
},
{
id: 'production',
title: '制作渲染台',
icon: Film,
color: '#10b981',
profession: '视觉导演',
duration: 10 * 60 * 1000 // 10分钟
},
{
id: 'editing',
title: '剪辑调色台',
icon: Scissors,
color: '#f59e0b',
profession: '剪辑师',
duration: 15 * 60 * 1000 // 15分钟
}
];
// 思考指示器组件
const ThinkingDots = ({ show, text, color }: { show: boolean; text: string; color: string }) => {
if (!show) return null;
return (
<div className="flex items-center space-x-2">
<div className="flex space-x-1">
{[0, 1, 2].map((i) => (
<motion.div
key={i}
className="w-2 h-2 rounded-full"
style={{ backgroundColor: color }}
animate={{
scale: [1, 1.2, 1],
opacity: [0.5, 1, 0.5]
}}
transition={{
duration: 1.5,
repeat: Infinity,
delay: i * 0.2
}}
/>
))}
</div>
<span className="text-white text-sm">{text}</span>
</div>
);
};
const WorkOffice: React.FC = () => {
const [currentStage, setCurrentStage] = useState(0);
const [isPlaying, setIsPlaying] = useState(true);
const [progress, setProgress] = useState(0);
const [currentContent, setCurrentContent] = useState<Record<string, any>>(scriptwriterData);
const [thinkingText, setThinkingText] = useState(`${stages[0].profession}正在思考...`);
const [startTime, setStartTime] = useState<number | null>(null);
// 模拟数据加载过程
useEffect(() => {
if (!isPlaying) return;
// 记录开始时间
if (startTime === null) {
setStartTime(Date.now());
}
const currentDuration = stages[currentStage].duration;
const updateInterval = 50; // 更新间隔(毫秒)
const interval = setInterval(() => {
const now = Date.now();
const elapsed = now - (startTime || now);
const newProgress = Math.min((elapsed / currentDuration) * 100, 100);
setProgress(newProgress);
if (newProgress >= 100) {
setIsPlaying(false);
setStartTime(null);
}
}, updateInterval);
return () => clearInterval(interval);
}, [isPlaying, currentStage, startTime]);
// 根据当前阶段加载对应数据
useEffect(() => {
let data: Record<string, any> = {};
switch (currentStage) {
case 0:
data = scriptwriterData;
break;
case 1:
data = storyboardData;
break;
case 2:
data = productionData;
break;
case 3:
data = editorData;
break;
}
// 重置状态并开始新的加载
setProgress(0);
setIsPlaying(true);
setStartTime(Date.now());
setCurrentContent({});
setThinkingText(`${stages[currentStage].profession}正在思考...`);
// 模拟数据加载延迟
const loadingTimeout = setTimeout(() => {
setCurrentContent(data);
}, 1000);
return () => clearTimeout(loadingTimeout);
}, [currentStage]);
// 渲染当前工作台组件
const renderCurrentWorkstation = () => {
switch (currentStage) {
case 0:
return <Scriptwriter currentContent={currentContent} isPlaying={isPlaying} />;
case 1:
return <StoryboardArtist currentContent={currentContent} isPlaying={isPlaying} />;
case 2:
return <VisualDirector currentContent={currentContent} isPlaying={isPlaying} />;
case 3:
return <Editor currentContent={currentContent} isPlaying={isPlaying} />;
default:
return null;
}
};
// 计算剩余时间
const getRemainingTime = () => {
if (!isPlaying || startTime === null) return '0:00';
const currentDuration = stages[currentStage].duration;
const elapsed = Date.now() - startTime;
const remaining = Math.max(0, currentDuration - elapsed);
const minutes = Math.floor(remaining / 60000);
const seconds = Math.floor((remaining % 60000) / 1000);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
return (
<div className="h-full rounded-2xl overflow-hidden shadow-2xl relative">
{/* 正在加载的部分 文字显示 */}
<div className="absolute top-[0] left-1/2 -translate-x-1/2 z-10">
<ThinkingDots
show={isPlaying}
text={thinkingText}
color={stages[currentStage].color}
/>
</div>
{/* 工作台内容区域 */}
<div className="absolute left-0 right-0 top-[2rem] w-full aspect-video overflow-y-auto" style={{height: 'calc(100% - 11rem'}}>
{renderCurrentWorkstation()}
</div>
{/* 底部控制栏 */}
<div className="p-6 absolute bottom-0 left-0 right-0">
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<span className="text-white font-medium">
{stages[currentStage].title}
</span>
<div className="flex items-center space-x-4">
<span className="text-gray-400 text-sm">
: {getRemainingTime()}
</span>
<span className="text-gray-400 text-sm">
{Math.round(progress)}%
</span>
</div>
</div>
<div className="w-full bg-gray-700 rounded-full h-3">
<motion.div
className="h-3 rounded-full transition-all duration-300"
style={{
width: `${progress}%`,
backgroundColor: stages[currentStage].color
}}
/>
</div>
</div>
{/* 工作台切换按钮组 */}
<div className="flex justify-center space-x-4">
{stages.map((stage, index) => (
<button
key={stage.id}
onClick={() => {
if (!isPlaying) {
setCurrentStage(index);
}
}}
disabled={isPlaying}
className={`w-12 h-12 rounded-full flex items-center justify-center transition-all ${
currentStage === index ? 'ring-2 ring-white/50' : 'hover:bg-white/10'
} ${isPlaying ? 'opacity-50 cursor-not-allowed' : ''}`}
style={{ backgroundColor: `${stage.color}40` }}
>
<stage.icon className="w-6 h-6 text-white" />
</button>
))}
</div>
</div>
</div>
);
};
export default WorkOffice;

View File

@ -1,7 +1,7 @@
import { getToken, clearAuthData } from './auth'; import { getToken, clearAuthData } from './auth';
// API基础URL // API基础URL
const API_BASE_URL = 'https://77.api.qikongjian.com'; const API_BASE_URL = 'https://pre.movieflow.api.huiying.video';
/** /**
* API请求方法 * API请求方法

183
lib/mdx.ts Normal file
View File

@ -0,0 +1,183 @@
import { compile } from '@mdx-js/mdx';
import { fromMarkdown } from 'mdast-util-from-markdown';
import { mdxFromMarkdown } from 'mdast-util-mdx';
import { mdxjs } from 'micromark-extension-mdxjs';
interface StreamChunk {
type: 'text' | 'annotation' | 'html' | 'block-start' | 'block-end';
content: string;
annotation?: {
type: string;
color?: string;
multiline?: boolean;
};
tag?: string;
className?: string;
}
// 自定义插件:将文本内容转换为流式块
function remarkStreamChunks() {
return (tree: any) => {
const chunks: StreamChunk[] = [];
function processNode(node: any) {
try {
if (node.type === 'heading') {
// 处理标题
chunks.push({
type: 'block-start',
content: '',
tag: 'div',
className: `text-${4-node.depth}xl font-bold mb-4`
});
node.children?.forEach(processNode);
chunks.push({
type: 'block-end',
content: '',
tag: 'div'
});
} else if (node.type === 'paragraph') {
// 处理段落
chunks.push({
type: 'block-start',
content: '',
tag: 'div',
className: 'text-base mb-4'
});
node.children?.forEach(processNode);
chunks.push({
type: 'block-end',
content: '',
tag: 'div'
});
} else if (node.type === 'mdxJsxTextElement' && node.name === 'Annot') {
// 处理 Annot 注释
const annotType = node.attributes?.find((attr: any) => attr.name === 'type')?.value;
const color = node.attributes?.find((attr: any) => attr.name === 'color')?.value;
if (!annotType) {
console.warn('Annot 标签缺少 type 属性');
return;
}
chunks.push({
type: 'annotation',
content: node.children?.[0]?.value || '',
annotation: {
type: annotType,
color: color,
multiline: false
},
tag: 'span'
});
} else if (node.type === 'text') {
// 处理普通文本
if (node.value?.trim()) {
chunks.push({
type: 'text',
content: node.value,
tag: 'span'
});
}
} else if (node.type === 'list') {
// 处理列表
chunks.push({
type: 'block-start',
content: '',
tag: 'ul',
className: 'list-disc list-inside mb-4'
});
node.children?.forEach(processNode);
chunks.push({
type: 'block-end',
content: '',
tag: 'ul'
});
} else if (node.type === 'listItem') {
// 处理列表项
chunks.push({
type: 'block-start',
content: '',
tag: 'li',
className: 'mb-2'
});
node.children?.forEach(processNode);
chunks.push({
type: 'block-end',
content: '',
tag: 'li'
});
} else if (node.children) {
// 递归处理其他节点
node.children.forEach(processNode);
}
} catch (error) {
console.error('处理节点时出错:', error, '节点类型:', node.type);
}
}
try {
tree.children?.forEach(processNode);
if (chunks.length === 0) {
throw new Error('解析结果为空');
}
return { chunks };
} catch (error) {
console.error('处理 MDX 树时出错:', error);
throw error;
}
};
}
export async function parseMDXContent(content: string): Promise<StreamChunk[]> {
try {
if (!content || typeof content !== 'string') {
throw new Error('无效的内容输入');
}
console.log('开始处理 MDX 内容...');
// 使用 fromMarkdown 直接解析 MDX
const tree = fromMarkdown(content, {
extensions: [mdxjs()],
mdastExtensions: [mdxFromMarkdown()]
});
// 应用我们的自定义处理器
const chunks: StreamChunk[] = [];
const processor = remarkStreamChunks();
const result = processor(tree);
if (!result || !result.chunks || result.chunks.length === 0) {
throw new Error('解析 MDX 内容失败:没有生成有效的块');
}
console.log('MDX 处理完成,生成块数:', result.chunks.length);
return result.chunks;
} catch (error) {
console.error('解析 MDX 内容时出错:', error);
throw error;
}
}
// 用于客户端渲染的编译函数
export async function compileMDX(source: string) {
const compiled = await compile(source, {
outputFormat: 'function-body',
development: false,
jsx: true,
jsxImportSource: 'react'
});
return String(compiled);
}

1714
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@formatjs/intl-localematcher": "^0.6.1", "@formatjs/intl-localematcher": "^0.6.1",
"@hookform/resolvers": "^3.9.0", "@hookform/resolvers": "^3.9.0",
"@mdx-js/mdx": "^3.1.0",
"@next/swc-wasm-nodejs": "13.5.1", "@next/swc-wasm-nodejs": "13.5.1",
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-alert-dialog": "^1.1.1",
@ -68,6 +69,9 @@
"input-otp": "^1.2.4", "input-otp": "^1.2.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.446.0", "lucide-react": "^0.446.0",
"mdast-util-from-markdown": "^2.0.2",
"mdast-util-mdx": "^3.0.0",
"micromark-extension-mdxjs": "^3.0.0",
"motion": "^12.18.1", "motion": "^12.18.1",
"negotiator": "^1.0.0", "negotiator": "^1.0.0",
"next": "13.5.1", "next": "13.5.1",
@ -83,8 +87,9 @@
"react-joyride": "^2.9.3", "react-joyride": "^2.9.3",
"react-resizable": "^3.0.5", "react-resizable": "^3.0.5",
"react-resizable-panels": "^2.1.3", "react-resizable-panels": "^2.1.3",
"react-rough-notation": "^1.0.5",
"react-textarea-autosize": "^8.5.9", "react-textarea-autosize": "^8.5.9",
"recharts": "^2.12.7", "recharts": "^2.15.4",
"sonner": "^1.5.0", "sonner": "^1.5.0",
"styled-components": "^6.1.19", "styled-components": "^6.1.19",
"swiper": "^11.2.8", "swiper": "^11.2.8",

View File

@ -49,3 +49,11 @@ Content-Type: application/json
{ {
"project_id": "dbbc1f06-2459-4d3e-bbff-7b11ecf0f293" "project_id": "dbbc1f06-2459-4d3e-bbff-7b11ecf0f293"
} }
### Text to script tags
POST https://pre.movieflow.api.huiying.video/movie/text_to_script_tags
Content-Type: application/json
{
"project_id": "244e01ae-01e8-4854-b56d-76a7bb57b2bb"
}