forked from 77media/video-flow
加入主题、工作台loading(开发中)
This commit is contained in:
parent
7719425e67
commit
834263dbc8
@ -1 +1 @@
|
||||
export const BASE_URL = "https://77.smartvideo.py.qikongjian.com"
|
||||
export const BASE_URL = "https://pre.movieflow.api.huiying.video"
|
||||
|
||||
@ -183,37 +183,19 @@ export const convertVideoToScene = async (
|
||||
// 新-获取剧集详情
|
||||
export const detailScriptEpisodeNew = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
|
||||
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 接口
|
||||
export const getScriptTitle = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
|
||||
return post<ApiResponse<any>>('/movie/get_movie_project_name', data);
|
||||
return {
|
||||
code: 0,
|
||||
successful: true,
|
||||
message: 'success',
|
||||
data: {
|
||||
name: '提取出视频标题'
|
||||
}
|
||||
}
|
||||
return post<ApiResponse<any>>('/movie/get_movie_project_description', data);
|
||||
}
|
||||
|
||||
// 获取 数据 全量(需轮询)
|
||||
export const getRunningStreamData = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
|
||||
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
112
app/api/stream/route.ts
Normal 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' }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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>) => {
|
||||
// 如果正在输入中文,不要更新内容
|
||||
if (isComposing) return;
|
||||
|
||||
const newText = e.currentTarget.textContent || '';
|
||||
|
||||
// 如果正在输入中文,只更新内部文本,不更新状态
|
||||
if (isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
setInputText(newText);
|
||||
|
||||
// 保存当前选区位置
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
const currentPosition = range.startOffset;
|
||||
|
||||
setTimeout(() => {
|
||||
// 使用 requestAnimationFrame 确保在下一帧恢复光标位置
|
||||
requestAnimationFrame(() => {
|
||||
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
|
||||
);
|
||||
) as Text;
|
||||
|
||||
if (textNode) {
|
||||
const newRange = document.createRange();
|
||||
newRange.setStart(textNode, currentPosition);
|
||||
newRange.setEnd(textNode, currentPosition);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(newRange);
|
||||
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);
|
||||
}
|
||||
}, 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -119,8 +119,8 @@
|
||||
.info-UUGkPJ {
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
display: flex
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
;
|
||||
}
|
||||
.title-JtMejk {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { ScriptModal } from '@/components/ui/script-modal';
|
||||
import {
|
||||
Image,
|
||||
Video,
|
||||
@ -11,7 +11,8 @@ import {
|
||||
Loader2,
|
||||
User,
|
||||
Scissors,
|
||||
Tv
|
||||
Tv,
|
||||
Airplay
|
||||
} from 'lucide-react';
|
||||
|
||||
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) {
|
||||
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) {
|
||||
return (
|
||||
@ -56,7 +78,7 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText, dataLoadEr
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{taskObject?.title || '正在加载项目信息...'}
|
||||
{taskObject?.title || 'loading project info...'}
|
||||
</motion.div>
|
||||
|
||||
{/* 加载状态显示 */}
|
||||
@ -153,10 +175,43 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText, dataLoadEr
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="title-JtMejk">
|
||||
{taskObject?.title || '正在加载项目信息...'}
|
||||
<div className="title-JtMejk flex items-center justify-center gap-2">
|
||||
{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>
|
||||
|
||||
{/* 主题 彩色标签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' ? (
|
||||
<motion.div
|
||||
className="flex items-center gap-3 justify-center"
|
||||
@ -164,61 +219,10 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText, dataLoadEr
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<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
|
||||
}}
|
||||
/>
|
||||
<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
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-5 h-5 text-emerald-500" />
|
||||
<span className="text-emerald-500 font-medium">{currentLoadingText}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
|
||||
@ -20,6 +20,8 @@ const LOADING_TEXT_MAP = {
|
||||
sketchComplete: 'Sketch generation complete',
|
||||
character: 'Drawing characters...',
|
||||
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...',
|
||||
video: (count: number, total: number) => `Generating video ${count + 1 > total ? total : count + 1}/${total}...`,
|
||||
videoComplete: 'Video generation complete',
|
||||
@ -43,6 +45,7 @@ interface TaskObject {
|
||||
roles?: any[];
|
||||
music?: any[];
|
||||
final?: any;
|
||||
tags?: any[];
|
||||
}
|
||||
|
||||
export function useWorkflowData() {
|
||||
@ -78,7 +81,7 @@ export function useWorkflowData() {
|
||||
}
|
||||
|
||||
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;
|
||||
// all_task_data 下标0 和 下标1 换位置
|
||||
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_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 数据
|
||||
const sketchList = [];
|
||||
for (const sketch of task.task_result.data) {
|
||||
@ -125,7 +128,7 @@ export function useWorkflowData() {
|
||||
url: character.image_path,
|
||||
sound: null,
|
||||
soundDescription: '',
|
||||
roleDescription: ''
|
||||
roleDescription: character.character_description
|
||||
});
|
||||
}
|
||||
setRoles(characterList);
|
||||
@ -136,18 +139,46 @@ export function useWorkflowData() {
|
||||
// 角色生成完成
|
||||
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_result.data.length >= 0 && taskVideos.length !== task.task_result.data.length) {
|
||||
console.log('----------正在生成视频中-发生变化才触发');
|
||||
// 正在生成视频中 替换视频数据
|
||||
const videoList = [];
|
||||
for (const video of task.task_result.data) {
|
||||
// 每一项 video 有多个视频 先默认取第一个
|
||||
videoList.push({
|
||||
url: video[0].qiniuVideoUrl,
|
||||
script: video[0].operation.metadata.video.prompt,
|
||||
url: video.urls[0],
|
||||
script: video.description,
|
||||
audio: null,
|
||||
});
|
||||
}
|
||||
@ -232,14 +263,15 @@ export function useWorkflowData() {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
|
||||
const { name, status, data } = response.data;
|
||||
const { name, status, data, tags } = response.data;
|
||||
setIsLoading(false);
|
||||
|
||||
// 设置初始数据
|
||||
setTaskObject({
|
||||
taskStatus: '0',
|
||||
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) {
|
||||
setTaskObject((prev: TaskObject | null) => ({
|
||||
...(prev || {}),
|
||||
title: titleResponse.data.name
|
||||
title: titleResponse.data.title,
|
||||
tags: titleResponse.data.tags || []
|
||||
} as TaskObject));
|
||||
}
|
||||
}
|
||||
@ -294,7 +327,7 @@ export function useWorkflowData() {
|
||||
url: character.image_path,
|
||||
sound: null,
|
||||
soundDescription: '',
|
||||
roleDescription: ''
|
||||
roleDescription: character.character_description
|
||||
});
|
||||
}
|
||||
setRoles(characterList);
|
||||
@ -303,7 +336,30 @@ export function useWorkflowData() {
|
||||
} else {
|
||||
finalStep = '3';
|
||||
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) {
|
||||
// 每一项 video 有多个视频 先默认取第一个
|
||||
videoList.push({
|
||||
url: video[0].qiniuVideoUrl,
|
||||
script: video[0].operation.metadata.video.prompt,
|
||||
url: video.urls[0],
|
||||
script: video.description,
|
||||
audio: null,
|
||||
});
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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 { ScriptTabContent } from './script-tab-content';
|
||||
import { VideoTabContent } from './video-tab-content';
|
||||
@ -24,9 +24,9 @@ interface EditModalProps {
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ id: '1', label: 'Script', icon: FileText },
|
||||
{ id: '1', label: 'Shot Sketch', icon: Image },
|
||||
{ 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: '5', label: '剪辑', icon: Scissors },
|
||||
{ id: 'settings', label: 'Settings', icon: Settings },
|
||||
@ -59,7 +59,12 @@ export function EditModal({
|
||||
|
||||
const isTabDisabled = (tabId: string) => {
|
||||
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) => {
|
||||
|
||||
@ -59,7 +59,7 @@ export function GenerateVideoModal({
|
||||
>
|
||||
<ChevronDown className="w-5 h-5" />
|
||||
</button>
|
||||
<h2 className="text-lg font-medium">生成视频</h2>
|
||||
<h2 className="text-lg font-medium">generate video</h2>
|
||||
</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="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
|
||||
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"
|
||||
placeholder="描述你想要生成的视频场景..."
|
||||
placeholder="describe the scene you want to generate..."
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
/>
|
||||
@ -110,7 +110,7 @@ export function GenerateVideoModal({
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
生成视频
|
||||
generate video
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
@ -131,14 +131,14 @@ export function GenerateVideoModal({
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={onClose}
|
||||
>
|
||||
返回
|
||||
back
|
||||
</motion.button>
|
||||
<motion.button
|
||||
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
应用
|
||||
apply
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
87
components/ui/script-modal.tsx
Normal file
87
components/ui/script-modal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -71,7 +71,7 @@ export function ScriptTabContent({
|
||||
<div className="relative">
|
||||
<div
|
||||
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) => (
|
||||
<motion.div
|
||||
|
||||
@ -107,7 +107,7 @@ export function VideoTabContent({
|
||||
<div className="relative">
|
||||
<div
|
||||
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) => (
|
||||
<motion.div
|
||||
|
||||
20
components/workflow/work-office/common/ContentCard.tsx
Normal file
20
components/workflow/work-office/common/ContentCard.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
43
components/workflow/work-office/common/DotLoading.tsx
Normal file
43
components/workflow/work-office/common/DotLoading.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
43
components/workflow/work-office/common/IconLoading.tsx
Normal file
43
components/workflow/work-office/common/IconLoading.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
30
components/workflow/work-office/common/ProgressBar.tsx
Normal file
30
components/workflow/work-office/common/ProgressBar.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
36
components/workflow/work-office/common/SkeletonCard.tsx
Normal file
36
components/workflow/work-office/common/SkeletonCard.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
87
components/workflow/work-office/common/TypewriterText.tsx
Normal file
87
components/workflow/work-office/common/TypewriterText.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
300
components/workflow/work-office/editor.tsx
Normal file
300
components/workflow/work-office/editor.tsx
Normal 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;
|
||||
320
components/workflow/work-office/mock-data.ts
Normal file
320
components/workflow/work-office/mock-data.ts
Normal 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
|
||||
}
|
||||
};
|
||||
289
components/workflow/work-office/scriptwriter.tsx
Normal file
289
components/workflow/work-office/scriptwriter.tsx
Normal 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;
|
||||
219
components/workflow/work-office/storyboard-artist.tsx
Normal file
219
components/workflow/work-office/storyboard-artist.tsx
Normal 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;
|
||||
264
components/workflow/work-office/visual-director.tsx
Normal file
264
components/workflow/work-office/visual-director.tsx
Normal 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;
|
||||
250
components/workflow/work-office/work-office.tsx
Normal file
250
components/workflow/work-office/work-office.tsx
Normal 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;
|
||||
@ -1,7 +1,7 @@
|
||||
import { getToken, clearAuthData } from './auth';
|
||||
|
||||
// API基础URL
|
||||
const API_BASE_URL = 'https://77.api.qikongjian.com';
|
||||
const API_BASE_URL = 'https://pre.movieflow.api.huiying.video';
|
||||
|
||||
/**
|
||||
* 统一的API请求方法
|
||||
|
||||
183
lib/mdx.ts
Normal file
183
lib/mdx.ts
Normal 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
1714
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,6 +14,7 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@formatjs/intl-localematcher": "^0.6.1",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@mdx-js/mdx": "^3.1.0",
|
||||
"@next/swc-wasm-nodejs": "13.5.1",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
@ -68,6 +69,9 @@
|
||||
"input-otp": "^1.2.4",
|
||||
"lodash": "^4.17.21",
|
||||
"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",
|
||||
"negotiator": "^1.0.0",
|
||||
"next": "13.5.1",
|
||||
@ -83,8 +87,9 @@
|
||||
"react-joyride": "^2.9.3",
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-resizable-panels": "^2.1.3",
|
||||
"react-rough-notation": "^1.0.5",
|
||||
"react-textarea-autosize": "^8.5.9",
|
||||
"recharts": "^2.12.7",
|
||||
"recharts": "^2.15.4",
|
||||
"sonner": "^1.5.0",
|
||||
"styled-components": "^6.1.19",
|
||||
"swiper": "^11.2.8",
|
||||
|
||||
@ -49,3 +49,11 @@ Content-Type: application/json
|
||||
{
|
||||
"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"
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user