forked from 77media/video-flow
loading剧本真实化数据
This commit is contained in:
parent
debc584dd9
commit
c434dd71da
@ -84,6 +84,95 @@ export const del = <T>(url: string, config?: AxiosRequestConfig): Promise<T> =>
|
|||||||
return request.delete(url, config);
|
return request.delete(url, config);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// utils/streamJsonPost.ts
|
||||||
|
export async function streamJsonPost<T = any>(
|
||||||
|
url: string,
|
||||||
|
body: any,
|
||||||
|
onJson: (json: T) => void
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token') || 'mock-token';
|
||||||
|
const response = await fetch(`${BASE_URL}${url}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.body) {
|
||||||
|
throw new Error('Stream not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder('utf-8');
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
// Process any remaining data in the buffer
|
||||||
|
if (buffer.trim()) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(buffer.trim());
|
||||||
|
onJson(parsed);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Final JSON parse error:', err, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the current chunk and add to buffer
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
// Process complete JSON objects
|
||||||
|
let boundary = buffer.indexOf('\n');
|
||||||
|
while (boundary !== -1) {
|
||||||
|
const chunk = buffer.slice(0, boundary).trim();
|
||||||
|
buffer = buffer.slice(boundary + 1);
|
||||||
|
|
||||||
|
if (chunk) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(chunk);
|
||||||
|
onJson(parsed);
|
||||||
|
} catch (err) {
|
||||||
|
// Only log if it's not an empty line
|
||||||
|
if (chunk !== '') {
|
||||||
|
console.warn('JSON parse error:', err, chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boundary = buffer.indexOf('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Stream processing error:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// Ensure reader is released
|
||||||
|
reader.releaseLock();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Stream request error:', error);
|
||||||
|
// Handle specific error types
|
||||||
|
if (error instanceof TypeError) {
|
||||||
|
console.error('Network error - check your connection');
|
||||||
|
}
|
||||||
|
if (error instanceof SyntaxError) {
|
||||||
|
console.error('Invalid JSON in the stream');
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 封装流式数据请求
|
// 封装流式数据请求
|
||||||
export const stream = async <T>({
|
export const stream = async <T>({
|
||||||
url,
|
url,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { post } from './request';
|
import { post } from './request';
|
||||||
import { ProjectTypeEnum } from '@/app/model/enums';
|
import { ProjectTypeEnum } from '@/app/model/enums';
|
||||||
import { ApiResponse } from '@/api/common';
|
import { ApiResponse } from '@/api/common';
|
||||||
|
import { BASE_URL } from './constants'
|
||||||
|
|
||||||
// API 响应类型
|
// API 响应类型
|
||||||
interface BaseApiResponse<T> {
|
interface BaseApiResponse<T> {
|
||||||
@ -199,3 +200,18 @@ export const getRunningStreamData = async (data: { project_id: string }): Promis
|
|||||||
export const getScriptTags = async (data: { project_id: string }): Promise<any> => {
|
export const getScriptTags = async (data: { project_id: string }): Promise<any> => {
|
||||||
return post<any>('/movie/text_to_script_tags', data);
|
return post<any>('/movie/text_to_script_tags', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取 loading-场景 接口
|
||||||
|
export const getSceneJson = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
|
||||||
|
return post<any>('/movie/scene_json', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取 loading-分镜 接口
|
||||||
|
export const getShotSketchJson = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
|
||||||
|
return post<any>('/movie/shot_sketch_json', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取 loading-视频 接口
|
||||||
|
export const getVideoJson = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
|
||||||
|
return post<any>('/movie/video_json', data);
|
||||||
|
};
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import WorkFlow from '@/components/pages/work-flow';
|
import WorkFlow from '@/components/pages/work-flow';
|
||||||
|
|
||||||
export default function ScriptWorkFlowPage() {
|
export default function ScriptWorkFlowPage() {
|
||||||
|
|||||||
@ -1,26 +1,6 @@
|
|||||||
import './globals.css';
|
import './globals.css';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { ThemeProvider } from '@/components/theme-provider';
|
import { Providers } from '@/components/providers';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
// Import the OAuthCallbackHandler dynamically to ensure it only runs on the client
|
|
||||||
const OAuthCallbackHandler = dynamic(
|
|
||||||
() => import('@/components/ui/oauth-callback-handler'),
|
|
||||||
{ ssr: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Import AuthGuard dynamically for client-side only
|
|
||||||
const AuthGuard = dynamic(
|
|
||||||
() => import('@/components/auth/auth-guard'),
|
|
||||||
{ ssr: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Import dev helper in development environment only
|
|
||||||
const DevHelper = dynamic(
|
|
||||||
() => import('@/utils/dev-helper').then(() => ({ default: () => null })),
|
|
||||||
{ ssr: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'AI Movie Flow - Create Amazing Videos with AI',
|
title: 'AI Movie Flow - Create Amazing Videos with AI',
|
||||||
@ -30,24 +10,14 @@ export const metadata: Metadata = {
|
|||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body className="font-sans antialiased">
|
<body className="font-sans antialiased">
|
||||||
<ThemeProvider
|
<Providers>
|
||||||
attribute="class"
|
{children}
|
||||||
defaultTheme="dark"
|
</Providers>
|
||||||
enableSystem
|
|
||||||
disableTransitionOnChange
|
|
||||||
>
|
|
||||||
<AuthGuard>
|
|
||||||
{children}
|
|
||||||
</AuthGuard>
|
|
||||||
<Toaster />
|
|
||||||
<OAuthCallbackHandler />
|
|
||||||
{process.env.NODE_ENV === 'development' && <DevHelper />}
|
|
||||||
</ThemeProvider>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,14 +4,7 @@ import React, { useState, useEffect, useMemo, useRef } from 'react';
|
|||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { ScriptModal } from '@/components/ui/script-modal';
|
import { ScriptModal } from '@/components/ui/script-modal';
|
||||||
import {
|
import {
|
||||||
Image,
|
|
||||||
Video,
|
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Music,
|
|
||||||
Loader2,
|
|
||||||
User,
|
|
||||||
Tv,
|
|
||||||
Airplay,
|
|
||||||
Heart,
|
Heart,
|
||||||
Camera,
|
Camera,
|
||||||
Film,
|
Film,
|
||||||
@ -56,12 +49,6 @@ const StageIcons = ({ currentStage, isExpanded }: { currentStage: number, isExpa
|
|||||||
...data
|
...data
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 将当前阶段移到第一位
|
|
||||||
// return stages.sort((a, b) => {
|
|
||||||
// if (a.stage === currentStage) return -1;
|
|
||||||
// if (b.stage === currentStage) return 1;
|
|
||||||
// return a.stage - b.stage;
|
|
||||||
// });
|
|
||||||
return stages;
|
return stages;
|
||||||
}, [currentStage]);
|
}, [currentStage]);
|
||||||
|
|
||||||
@ -214,21 +201,21 @@ export function TaskInfo({
|
|||||||
setIsScriptModalOpen(true);
|
setIsScriptModalOpen(true);
|
||||||
setCurrentStage(1);
|
setCurrentStage(1);
|
||||||
}
|
}
|
||||||
if (currentLoadingText.includes('character')) {
|
// if (currentLoadingText.includes('character')) {
|
||||||
console.log('isScriptModalOpen-character', currentLoadingText, isScriptModalOpen);
|
// console.log('isScriptModalOpen-character', currentLoadingText, isScriptModalOpen);
|
||||||
if (isScriptModalOpen) {
|
// if (isScriptModalOpen) {
|
||||||
setIsScriptModalOpen(false);
|
// setIsScriptModalOpen(false);
|
||||||
|
|
||||||
// 延迟8s 再次打开
|
// // 延迟8s 再次打开
|
||||||
timerRef.current = setTimeout(() => {
|
// timerRef.current = setTimeout(() => {
|
||||||
setIsScriptModalOpen(true);
|
// setIsScriptModalOpen(true);
|
||||||
setCurrentStage(0);
|
// setCurrentStage(0);
|
||||||
}, 8000);
|
// }, 8000);
|
||||||
} else {
|
// } else {
|
||||||
setIsScriptModalOpen(true);
|
// setIsScriptModalOpen(true);
|
||||||
setCurrentStage(0);
|
// setCurrentStage(0);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
if (currentLoadingText.includes('initializing')) {
|
if (currentLoadingText.includes('initializing')) {
|
||||||
console.log('isScriptModalOpen-initializing', currentLoadingText, isScriptModalOpen);
|
console.log('isScriptModalOpen-initializing', currentLoadingText, isScriptModalOpen);
|
||||||
setIsScriptModalOpen(true);
|
setIsScriptModalOpen(true);
|
||||||
@ -251,15 +238,6 @@ export function TaskInfo({
|
|||||||
}, {});
|
}, {});
|
||||||
}, [taskObject?.tags]); // 只在 tags 改变时重新计算
|
}, [taskObject?.tags]); // 只在 tags 改变时重新计算
|
||||||
|
|
||||||
// 自动触发打开 剧本 弹窗 延迟5秒
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (taskObject?.title && currentLoadingText !== 'Task completed') {
|
|
||||||
// setTimeout(() => {
|
|
||||||
// setIsScriptModalOpen(true);
|
|
||||||
// }, 5000);
|
|
||||||
// }
|
|
||||||
// }, [taskObject?.title]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="title-JtMejk flex items-center justify-center gap-2">
|
<div className="title-JtMejk flex items-center justify-center gap-2">
|
||||||
@ -293,6 +271,7 @@ export function TaskInfo({
|
|||||||
}}
|
}}
|
||||||
currentStage={currentStage}
|
currentStage={currentStage}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
|
currentLoadingText={currentLoadingText}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{currentLoadingText === 'Task completed' ? (
|
{currentLoadingText === 'Task completed' ? (
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData } from '@/api/video_flow';
|
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData } from '@/api/video_flow';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/lib/store/hooks';
|
||||||
|
import { setSketchCount, setVideoCount } from '@/lib/store/workflowSlice';
|
||||||
|
|
||||||
// 步骤映射
|
// 步骤映射
|
||||||
const STEP_MAP = {
|
const STEP_MAP = {
|
||||||
@ -16,6 +18,7 @@ const STEP_MAP = {
|
|||||||
// 执行loading文字映射
|
// 执行loading文字映射
|
||||||
const LOADING_TEXT_MAP = {
|
const LOADING_TEXT_MAP = {
|
||||||
initializing: 'initializing...',
|
initializing: 'initializing...',
|
||||||
|
getSketchStatus: 'Getting sketch status...',
|
||||||
sketch: (count: number, total: number) => `Generating sketch ${count + 1 > total ? total : count + 1}/${total}...`,
|
sketch: (count: number, total: number) => `Generating sketch ${count + 1 > total ? total : count + 1}/${total}...`,
|
||||||
sketchComplete: 'Sketch generation complete',
|
sketchComplete: 'Sketch generation complete',
|
||||||
character: 'Drawing characters...',
|
character: 'Drawing characters...',
|
||||||
@ -57,14 +60,12 @@ export function useWorkflowData() {
|
|||||||
const [taskSketch, setTaskSketch] = useState<any[]>([]);
|
const [taskSketch, setTaskSketch] = useState<any[]>([]);
|
||||||
const [taskShotSketch, setTaskShotSketch] = useState<any[]>([]);
|
const [taskShotSketch, setTaskShotSketch] = useState<any[]>([]);
|
||||||
const [taskVideos, setTaskVideos] = useState<any[]>([]);
|
const [taskVideos, setTaskVideos] = useState<any[]>([]);
|
||||||
const [sketchCount, setSketchCount] = useState(0);
|
|
||||||
const [videoCount, setVideoCount] = useState(0);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [currentStep, setCurrentStep] = useState('0');
|
const [currentStep, setCurrentStep] = useState('0');
|
||||||
const [currentSketchIndex, setCurrentSketchIndex] = useState(0);
|
const [currentSketchIndex, setCurrentSketchIndex] = useState(0);
|
||||||
const [isGeneratingSketch, setIsGeneratingSketch] = useState(false);
|
const [isGeneratingSketch, setIsGeneratingSketch] = useState(false);
|
||||||
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
|
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
|
||||||
const [currentLoadingText, setCurrentLoadingText] = useState('正在加载项目数据...');
|
const [currentLoadingText, setCurrentLoadingText] = useState('loading project info...');
|
||||||
const [totalSketchCount, setTotalSketchCount] = useState(0);
|
const [totalSketchCount, setTotalSketchCount] = useState(0);
|
||||||
const [roles, setRoles] = useState<any[]>([]);
|
const [roles, setRoles] = useState<any[]>([]);
|
||||||
const [music, setMusic] = useState<any[]>([]);
|
const [music, setMusic] = useState<any[]>([]);
|
||||||
@ -72,6 +73,9 @@ export function useWorkflowData() {
|
|||||||
const [dataLoadError, setDataLoadError] = useState<string | null>(null);
|
const [dataLoadError, setDataLoadError] = useState<string | null>(null);
|
||||||
const [needStreamData, setNeedStreamData] = useState(false);
|
const [needStreamData, setNeedStreamData] = useState(false);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { sketchCount, videoCount } = useAppSelector((state) => state.workflow);
|
||||||
|
|
||||||
// 自动开始播放一轮
|
// 自动开始播放一轮
|
||||||
const autoPlaySketch = useCallback(() => {
|
const autoPlaySketch = useCallback(() => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
@ -108,6 +112,17 @@ export function useWorkflowData() {
|
|||||||
handleAutoPlay();
|
handleAutoPlay();
|
||||||
}, [sketchCount, totalSketchCount, isGeneratingSketch, autoPlaySketch]);
|
}, [sketchCount, totalSketchCount, isGeneratingSketch, autoPlaySketch]);
|
||||||
|
|
||||||
|
// 更新 setSketchCount
|
||||||
|
const updateSketchCount = useCallback((count: number) => {
|
||||||
|
dispatch(setSketchCount(count));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
// 更新 setVideoCount
|
||||||
|
const updateVideoCount = useCallback((count: number) => {
|
||||||
|
dispatch(setVideoCount(count));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
// 替换原有的 setSketchCount 和 setVideoCount 调用
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('sketchCount 已更新:', sketchCount);
|
console.log('sketchCount 已更新:', sketchCount);
|
||||||
setCurrentSketchIndex(sketchCount - 1);
|
setCurrentSketchIndex(sketchCount - 1);
|
||||||
@ -118,6 +133,8 @@ export function useWorkflowData() {
|
|||||||
setCurrentSketchIndex(videoCount - 1);
|
setCurrentSketchIndex(videoCount - 1);
|
||||||
}, [videoCount]);
|
}, [videoCount]);
|
||||||
|
|
||||||
|
// 将 sketchCount 和 videoCount 放到 redux 中 每一次变化也要更新
|
||||||
|
|
||||||
// 添加手动播放控制
|
// 添加手动播放控制
|
||||||
const handleManualPlay = useCallback(async () => {
|
const handleManualPlay = useCallback(async () => {
|
||||||
if (!isGeneratingSketch && taskSketch.length > 0) {
|
if (!isGeneratingSketch && taskSketch.length > 0) {
|
||||||
@ -148,17 +165,18 @@ export function useWorkflowData() {
|
|||||||
|
|
||||||
// 如果有已完成的数据,同步到状态
|
// 如果有已完成的数据,同步到状态
|
||||||
if (task.task_name === 'generate_sketch' && task.task_result) {
|
if (task.task_name === 'generate_sketch' && task.task_result) {
|
||||||
if (task.task_result.data.length >= 0 && taskSketch.length < task.task_result.data.length) {
|
const realSketchResultData = task.task_result.data.filter((item: any) => item.image_path);
|
||||||
|
if (realSketchResultData.length >= 0) {
|
||||||
// 正在生成草图中 替换 sketch 数据
|
// 正在生成草图中 替换 sketch 数据
|
||||||
const sketchList = [];
|
const sketchList = [];
|
||||||
for (const sketch of task.task_result.data) {
|
for (const sketch of realSketchResultData) {
|
||||||
sketchList.push({
|
sketchList.push({
|
||||||
url: sketch.image_path,
|
url: sketch.image_path,
|
||||||
script: sketch.sketch_name
|
script: sketch.sketch_name
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setTaskSketch(sketchList);
|
setTaskSketch(sketchList);
|
||||||
setSketchCount(sketchList.length);
|
updateSketchCount(sketchList.length);
|
||||||
setIsGeneratingSketch(true);
|
setIsGeneratingSketch(true);
|
||||||
loadingText = LOADING_TEXT_MAP.sketch(sketchList.length, task.task_result.total_count);
|
loadingText = LOADING_TEXT_MAP.sketch(sketchList.length, task.task_result.total_count);
|
||||||
}
|
}
|
||||||
@ -197,11 +215,13 @@ export function useWorkflowData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (task.task_name === 'generate_shot_sketch' && task.task_result) {
|
if (task.task_name === 'generate_shot_sketch' && task.task_result) {
|
||||||
if (task.task_result.data.length >= 0 && taskShotSketch.length < task.task_result.data.length) {
|
const realShotResultData = task.task_result.data.filter((item: any) => item.url);
|
||||||
console.log('----------正在生成草图中 替换 sketch 数据', taskShotSketch.length, task.task_result.data.length);
|
if (realShotResultData.length >= 0) {
|
||||||
|
finalStep = '1';
|
||||||
|
console.log('----------正在生成草图中 替换 sketch 数据', taskShotSketch.length, realShotResultData.length);
|
||||||
// 正在生成草图中 替换 sketch 数据
|
// 正在生成草图中 替换 sketch 数据
|
||||||
const sketchList = [];
|
const sketchList = [];
|
||||||
for (const sketch of task.task_result.data) {
|
for (const sketch of realShotResultData) {
|
||||||
sketchList.push({
|
sketchList.push({
|
||||||
url: sketch.url,
|
url: sketch.url,
|
||||||
script: sketch.description
|
script: sketch.description
|
||||||
@ -209,7 +229,7 @@ export function useWorkflowData() {
|
|||||||
}
|
}
|
||||||
setTaskSketch(sketchList);
|
setTaskSketch(sketchList);
|
||||||
setTaskShotSketch(sketchList);
|
setTaskShotSketch(sketchList);
|
||||||
setSketchCount(sketchList.length);
|
updateSketchCount(sketchList.length);
|
||||||
setIsGeneratingSketch(true);
|
setIsGeneratingSketch(true);
|
||||||
loadingText = LOADING_TEXT_MAP.shotSketch(sketchList.length, task.task_result.total_count);
|
loadingText = LOADING_TEXT_MAP.shotSketch(sketchList.length, task.task_result.total_count);
|
||||||
}
|
}
|
||||||
@ -221,15 +241,13 @@ export function useWorkflowData() {
|
|||||||
console.log('----------草图生成完成', sketchCount);
|
console.log('----------草图生成完成', sketchCount);
|
||||||
loadingText = LOADING_TEXT_MAP.getVideoStatus;
|
loadingText = LOADING_TEXT_MAP.getVideoStatus;
|
||||||
finalStep = '3';
|
finalStep = '3';
|
||||||
} else {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
setTotalSketchCount(task.task_result.total_count);
|
setTotalSketchCount(task.task_result.total_count);
|
||||||
}
|
}
|
||||||
if (task.task_name === 'generate_videos' && task.task_result) {
|
if (task.task_name === 'generate_videos' && task.task_result) {
|
||||||
const realTaskResultData = task.task_result.data.filter((item: any) => item.urls && item.urls.length > 0);
|
const realTaskResultData = task.task_result.data.filter((item: any) => item.urls && item.urls.length > 0);
|
||||||
if (realTaskResultData.length >= 0 && taskVideos.length !== realTaskResultData.length) {
|
if (realTaskResultData.length >= 0) {
|
||||||
console.log('----------正在生成视频中-发生变化才触发', taskVideos.length);
|
console.log('----------正在生成视频中', realTaskResultData.length);
|
||||||
// 正在生成视频中 替换视频数据
|
// 正在生成视频中 替换视频数据
|
||||||
const videoList = [];
|
const videoList = [];
|
||||||
for (const video of realTaskResultData) {
|
for (const video of realTaskResultData) {
|
||||||
@ -241,7 +259,7 @@ export function useWorkflowData() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
setTaskVideos(videoList);
|
setTaskVideos(videoList);
|
||||||
setVideoCount(videoList.length);
|
updateVideoCount(videoList.length);
|
||||||
setIsGeneratingVideo(true);
|
setIsGeneratingVideo(true);
|
||||||
loadingText = LOADING_TEXT_MAP.video(videoList.length, task.task_result.total_count);
|
loadingText = LOADING_TEXT_MAP.video(videoList.length, task.task_result.total_count);
|
||||||
}
|
}
|
||||||
@ -365,21 +383,21 @@ export function useWorkflowData() {
|
|||||||
// 如果有已完成的数据,同步到状态
|
// 如果有已完成的数据,同步到状态
|
||||||
let finalStep = '1';
|
let finalStep = '1';
|
||||||
if (data) {
|
if (data) {
|
||||||
if (data.sketch && data.sketch.data && data.sketch.data.length > 0) {
|
if (data.sketch && data.sketch.data) {
|
||||||
|
const realSketchResultData = data.sketch.data.filter((item: any) => item.image_path);
|
||||||
const sketchList = [];
|
const sketchList = [];
|
||||||
for (const sketch of data.sketch.data) {
|
for (const sketch of realSketchResultData) {
|
||||||
sketchList.push({
|
sketchList.push({
|
||||||
url: sketch.image_path,
|
url: sketch.image_path,
|
||||||
script: sketch.sketch_name,
|
script: sketch.sketch_name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setTaskSketch(sketchList);
|
setTaskSketch(sketchList);
|
||||||
setSketchCount(sketchList.length);
|
updateSketchCount(sketchList.length);
|
||||||
setTotalSketchCount(data.sketch.total_count);
|
|
||||||
// 设置为最后一个草图
|
// 设置为最后一个草图
|
||||||
if (data.sketch.total_count > data.sketch.data.length) {
|
if (data.sketch.total_count > realSketchResultData.length) {
|
||||||
setIsGeneratingSketch(true);
|
setIsGeneratingSketch(true);
|
||||||
loadingText = LOADING_TEXT_MAP.sketch(data.sketch.data.length, data.sketch.total_count);
|
loadingText = LOADING_TEXT_MAP.sketch(realSketchResultData.length, data.sketch.total_count);
|
||||||
} else {
|
} else {
|
||||||
finalStep = '2';
|
finalStep = '2';
|
||||||
if (!data.character || !data.character.data || !data.character.data.length) {
|
if (!data.character || !data.character.data || !data.character.data.length) {
|
||||||
@ -408,9 +426,10 @@ export function useWorkflowData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.shot_sketch && data.shot_sketch.data && data.shot_sketch.data.length > 0) {
|
if (data.shot_sketch && data.shot_sketch.data) {
|
||||||
|
const realShotResultData = data.shot_sketch.data.filter((item: any) => item.url);
|
||||||
const sketchList = [];
|
const sketchList = [];
|
||||||
for (const sketch of data.shot_sketch.data) {
|
for (const sketch of realShotResultData) {
|
||||||
sketchList.push({
|
sketchList.push({
|
||||||
url: sketch.url,
|
url: sketch.url,
|
||||||
script: sketch.description,
|
script: sketch.description,
|
||||||
@ -418,12 +437,11 @@ export function useWorkflowData() {
|
|||||||
}
|
}
|
||||||
setTaskSketch(sketchList);
|
setTaskSketch(sketchList);
|
||||||
setTaskShotSketch(sketchList);
|
setTaskShotSketch(sketchList);
|
||||||
setSketchCount(sketchList.length);
|
updateSketchCount(sketchList.length);
|
||||||
setTotalSketchCount(data.shot_sketch.total_count);
|
|
||||||
// 设置为最后一个草图
|
// 设置为最后一个草图
|
||||||
if (data.shot_sketch.total_count > data.shot_sketch.data.length) {
|
if (data.shot_sketch.total_count > realShotResultData.length) {
|
||||||
setIsGeneratingSketch(true);
|
setIsGeneratingSketch(true);
|
||||||
loadingText = LOADING_TEXT_MAP.shotSketch(data.shot_sketch.data.length, data.shot_sketch.total_count);
|
loadingText = LOADING_TEXT_MAP.shotSketch(realShotResultData.length, data.shot_sketch.total_count);
|
||||||
} else {
|
} else {
|
||||||
finalStep = '3';
|
finalStep = '3';
|
||||||
setIsGeneratingVideo(true);
|
setIsGeneratingVideo(true);
|
||||||
@ -434,6 +452,9 @@ export function useWorkflowData() {
|
|||||||
}
|
}
|
||||||
if (data.video.data) {
|
if (data.video.data) {
|
||||||
const realDataVideoData = data.video.data.filter((item: any) => item.urls && item.urls.length > 0);
|
const realDataVideoData = data.video.data.filter((item: any) => item.urls && item.urls.length > 0);
|
||||||
|
if (realDataVideoData.length === 0 && finalStep === '3') {
|
||||||
|
loadingText = LOADING_TEXT_MAP.video(0, data.video.total_count);
|
||||||
|
}
|
||||||
if (realDataVideoData.length > 0) {
|
if (realDataVideoData.length > 0) {
|
||||||
const videoList = [];
|
const videoList = [];
|
||||||
console.log('----------data.video.data', data.video.data);
|
console.log('----------data.video.data', data.video.data);
|
||||||
@ -446,7 +467,7 @@ export function useWorkflowData() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
setTaskVideos(videoList);
|
setTaskVideos(videoList);
|
||||||
setVideoCount(videoList.length);
|
updateVideoCount(videoList.length);
|
||||||
// 如果在视频步骤,设置为最后一个视频
|
// 如果在视频步骤,设置为最后一个视频
|
||||||
if (data.video.total_count > realDataVideoData.length) {
|
if (data.video.total_count > realDataVideoData.length) {
|
||||||
setIsGeneratingVideo(true);
|
setIsGeneratingVideo(true);
|
||||||
@ -508,8 +529,8 @@ export function useWorkflowData() {
|
|||||||
// 重置所有状态
|
// 重置所有状态
|
||||||
setTaskSketch([]);
|
setTaskSketch([]);
|
||||||
setTaskVideos([]);
|
setTaskVideos([]);
|
||||||
setSketchCount(0);
|
updateSketchCount(0);
|
||||||
setTotalSketchCount(0);
|
updateVideoCount(0);
|
||||||
setRoles([]);
|
setRoles([]);
|
||||||
setMusic([]);
|
setMusic([]);
|
||||||
setFinal(null);
|
setFinal(null);
|
||||||
|
|||||||
363
components/pages/work-flow/use-workoffice-data.tsx
Normal file
363
components/pages/work-flow/use-workoffice-data.tsx
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState, useRef, useCallback } from "react";
|
||||||
|
import { streamJsonPost } from "@/api/request";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { Heart, Camera, Film, Scissors } from "lucide-react";
|
||||||
|
import { getSceneJson, getShotSketchJson, getVideoJson } from "@/api/video_flow";
|
||||||
|
import { useAppSelector } from "@/lib/store/hooks";
|
||||||
|
|
||||||
|
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 Stage {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
icon: React.ElementType;
|
||||||
|
color: string;
|
||||||
|
profession: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stages: Stage[] = [
|
||||||
|
{
|
||||||
|
id: 'script',
|
||||||
|
title: 'Scriptwriter',
|
||||||
|
icon: Heart,
|
||||||
|
color: '#8b5cf6',
|
||||||
|
profession: 'Scriptwriter'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'storyboard',
|
||||||
|
title: 'Storyboard artist',
|
||||||
|
icon: Camera,
|
||||||
|
color: '#06b6d4',
|
||||||
|
profession: 'Storyboard artist'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'production',
|
||||||
|
title: 'Visual director',
|
||||||
|
icon: Film,
|
||||||
|
color: '#10b981',
|
||||||
|
profession: 'Visual director'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'editing',
|
||||||
|
title: 'Editor',
|
||||||
|
icon: Scissors,
|
||||||
|
color: '#f59e0b',
|
||||||
|
profession: 'Editor'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const detailThinkingText = [
|
||||||
|
{
|
||||||
|
default: 'is thinking...',
|
||||||
|
acts: 'is thinking about the story structure...',
|
||||||
|
characters: 'is thinking about the characters...',
|
||||||
|
dialogue: 'is thinking about the dialogue...',
|
||||||
|
themes: 'is thinking about the themes...',
|
||||||
|
dramaticLine: 'is thinking about the dramatic line...',
|
||||||
|
another: 'is thinking about the another...'
|
||||||
|
}, {
|
||||||
|
default: 'is thinking...',
|
||||||
|
}, {
|
||||||
|
default: 'is thinking...',
|
||||||
|
}, {
|
||||||
|
default: 'is thinking...',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export function useWorkofficeData(currentStage: number, isOpen: boolean, currentLoadingText: string) {
|
||||||
|
const [scriptwriterData, setScriptwriterData] = useState<ScriptContent>({
|
||||||
|
acts: [],
|
||||||
|
characters: [],
|
||||||
|
dialogue: undefined,
|
||||||
|
themes: [],
|
||||||
|
dramaticLine: undefined
|
||||||
|
});
|
||||||
|
const [thinkingColor, setThinkingColor] = useState<string>('');
|
||||||
|
const [thinkingText, setThinkingText] = useState<string>('');
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const project_id = searchParams.get('episodeId') || '';
|
||||||
|
const [sceneData, setSceneData] = useState<any>([]);
|
||||||
|
const [shotSketchData, setShotSketchData] = useState<any>([]);
|
||||||
|
const [videoData, setVideoData] = useState<any>([]);
|
||||||
|
const [storyboardData, setStoryboardData] = useState<any>({});
|
||||||
|
const [sketchType, setSketchType] = useState<string>('');
|
||||||
|
const { sketchCount, videoCount } = useAppSelector((state) => state.workflow);
|
||||||
|
|
||||||
|
// 使用 ref 存储临时数据,避免重复创建对象
|
||||||
|
const tempDataRef = useRef<ScriptContent>({
|
||||||
|
acts: [],
|
||||||
|
characters: [],
|
||||||
|
dialogue: undefined,
|
||||||
|
themes: [],
|
||||||
|
dramaticLine: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
// 深度比较两个对象是否相等
|
||||||
|
const isEqual = (obj1: any, obj2: any): boolean => {
|
||||||
|
if (obj1 === obj2) return true;
|
||||||
|
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return obj1 === obj2;
|
||||||
|
if (obj1 === null || obj2 === null) return obj1 === obj2;
|
||||||
|
|
||||||
|
const keys1 = Object.keys(obj1);
|
||||||
|
const keys2 = Object.keys(obj2);
|
||||||
|
|
||||||
|
if (keys1.length !== keys2.length) return false;
|
||||||
|
|
||||||
|
for (const key of keys1) {
|
||||||
|
if (!isEqual(obj1[key], obj2[key])) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新思考文本的函数
|
||||||
|
const updateThinkingText = useCallback((data: ScriptContent) => {
|
||||||
|
console.log('updateThinkingText', currentStage, isOpen);
|
||||||
|
if (currentStage === 0) {
|
||||||
|
if (!data.acts || data.acts.length === 0) {
|
||||||
|
return `${stages[currentStage].profession} ${detailThinkingText[currentStage].acts}`;
|
||||||
|
}
|
||||||
|
if (!data.characters || data.characters.length === 0) {
|
||||||
|
return `${stages[currentStage].profession} ${detailThinkingText[currentStage].characters}`;
|
||||||
|
}
|
||||||
|
if (!data.dialogue) {
|
||||||
|
return `${stages[currentStage].profession} ${detailThinkingText[currentStage].dialogue}`;
|
||||||
|
}
|
||||||
|
if (!data.themes || data.themes.length === 0) {
|
||||||
|
return `${stages[currentStage].profession} ${detailThinkingText[currentStage].themes}`;
|
||||||
|
}
|
||||||
|
if (!data.dramaticLine) {
|
||||||
|
return `${stages[currentStage].profession} ${detailThinkingText[currentStage].dramaticLine}`;
|
||||||
|
}
|
||||||
|
return `${stages[currentStage].profession} ${detailThinkingText[currentStage].another}`;
|
||||||
|
}
|
||||||
|
return `${stages[currentStage].profession} ${detailThinkingText[currentStage].default}`;
|
||||||
|
}, [currentStage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('currentStage---changed', currentStage);
|
||||||
|
if (!isOpen) return;
|
||||||
|
setThinkingColor(stages[currentStage].color);
|
||||||
|
if (currentStage !== 0) return; // 暂时仅支持剧本创作阶段获取真实数据
|
||||||
|
// 剧本创作阶段
|
||||||
|
if (currentStage === 0) {
|
||||||
|
// 重置临时数据
|
||||||
|
tempDataRef.current = {
|
||||||
|
acts: [],
|
||||||
|
characters: [],
|
||||||
|
dialogue: undefined,
|
||||||
|
themes: [],
|
||||||
|
dramaticLine: undefined
|
||||||
|
};
|
||||||
|
handleStreamData();
|
||||||
|
} else if (currentStage === 1) {
|
||||||
|
// 不需要每次isOpen为true都请求,只需要请求一次,所以这里需要判断是否已经请求过
|
||||||
|
// 场景设计、分镜设计
|
||||||
|
if (!sceneData.length) {
|
||||||
|
let tempSceneData: any[] = [];
|
||||||
|
// 场景设计
|
||||||
|
getSceneJson({ project_id: project_id }).then((res: any) => {
|
||||||
|
console.log('sceneJson', res);
|
||||||
|
if (res.successful) {
|
||||||
|
for (const [index, scene] of res.data.sketches.entries()) {
|
||||||
|
tempSceneData.push({
|
||||||
|
id: `SC-${index + 1}`,
|
||||||
|
location: scene.sketch_name,
|
||||||
|
description: scene.sketch_description,
|
||||||
|
core_atmosphere: scene.core_atmosphere
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setSceneData(tempSceneData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!shotSketchData.length) {
|
||||||
|
let tempShotSketchData: any[] = [];
|
||||||
|
// 分镜设计
|
||||||
|
getShotSketchJson({ project_id: project_id }).then((res: any) => {
|
||||||
|
console.log('shotSketchJson', res);
|
||||||
|
if (res.successful) {
|
||||||
|
for (const [index, shot] of res.data.shot_sketches.entries()) {
|
||||||
|
tempShotSketchData.push({
|
||||||
|
id: index + 1,
|
||||||
|
shotLanguage: shot.shot_type.split(', '),
|
||||||
|
frame_description: shot.frame_description,
|
||||||
|
atmosphere: shot.atmosphere.split(', '),
|
||||||
|
camera_motion: shot.cinematography_blueprint_camera_motion.split(', '),
|
||||||
|
composition: shot.cinematography_blueprint_composition,
|
||||||
|
key_action: shot.key_action,
|
||||||
|
dialogue_performance: {
|
||||||
|
speaker: shot.dialogue_performance_speaker,
|
||||||
|
language: shot.dialogue_performance_language,
|
||||||
|
delivery: shot.dialogue_performance_delivery,
|
||||||
|
line: shot.dialogue_performance_line
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setShotSketchData(tempShotSketchData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (currentStage === 2) {
|
||||||
|
// 视觉导演
|
||||||
|
getVideoJson({ project_id: project_id }).then((res: any) => {
|
||||||
|
console.log('videoJson', res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [currentStage, isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newThinkingText = updateThinkingText(scriptwriterData);
|
||||||
|
if (newThinkingText !== thinkingText) {
|
||||||
|
setThinkingText(newThinkingText);
|
||||||
|
}
|
||||||
|
}, [scriptwriterData, updateThinkingText]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('------sketchCount, sceneData, shotSketchData', sketchCount, sceneData, shotSketchData);
|
||||||
|
if (!isOpen) return;
|
||||||
|
if (!currentLoadingText.includes('shot sketch')) {
|
||||||
|
if (!sceneData.length || sketchCount > sceneData.length) return;
|
||||||
|
setSketchType('scene');
|
||||||
|
setStoryboardData(sceneData[sketchCount ? sketchCount - 1 : 0]);
|
||||||
|
setThinkingText(`Drawing scene sketch: ${sceneData[sketchCount ? sketchCount - 1 : 0].id}...`);
|
||||||
|
} else {
|
||||||
|
if (!shotSketchData.length || sketchCount > shotSketchData.length) return;
|
||||||
|
setSketchType('shot');
|
||||||
|
setStoryboardData(shotSketchData[sketchCount ? sketchCount - 1 : 0]);
|
||||||
|
setThinkingText(`Drawing shot sketch: ${shotSketchData[sketchCount ? sketchCount - 1 : 0].id}...`);
|
||||||
|
}
|
||||||
|
}, [sceneData, shotSketchData, currentLoadingText, sketchCount, isOpen]);
|
||||||
|
|
||||||
|
const updateStateIfChanged = useCallback((newData: Partial<ScriptContent>) => {
|
||||||
|
const hasChanges = Object.entries(newData).some(([key, value]) => {
|
||||||
|
return !isEqual(value, tempDataRef.current[key as keyof ScriptContent]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
const updatedData = {
|
||||||
|
...tempDataRef.current,
|
||||||
|
...newData
|
||||||
|
};
|
||||||
|
|
||||||
|
// 只有在数据真正变化时才更新状态
|
||||||
|
if (!isEqual(updatedData, tempDataRef.current)) {
|
||||||
|
tempDataRef.current = updatedData;
|
||||||
|
setScriptwriterData(updatedData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function handleStreamData() {
|
||||||
|
await streamJsonPost('/movie/analyze_movie_script_stream', { project_id: project_id }, (data: any) => {
|
||||||
|
console.log('workoffice---chunk', data);
|
||||||
|
const newData: Partial<ScriptContent> = {};
|
||||||
|
|
||||||
|
// 三幕结构
|
||||||
|
if (data.name === 'Three-act Structure') {
|
||||||
|
newData.acts = data.acts.map((act: any, index: number) => ({
|
||||||
|
id: String(index),
|
||||||
|
stableId: `act${index}`,
|
||||||
|
title: act.name,
|
||||||
|
desc: act.description,
|
||||||
|
beats: act.tags
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// 人物设定
|
||||||
|
if (data.name === 'Character Arc Design') {
|
||||||
|
newData.characters = data.characters.map((protagonist: any, index: number) => ({
|
||||||
|
id: String(index),
|
||||||
|
stableId: `protagonist${index}`,
|
||||||
|
name: protagonist.name,
|
||||||
|
role: protagonist.role,
|
||||||
|
arc: 'Growth and transformation',
|
||||||
|
desc: protagonist.description,
|
||||||
|
color: protagonist.gender === 'male' ? '#8b5cf6' : '#ec4899'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// 对话节奏
|
||||||
|
if (data.name === 'Dialogue Rhythm') {
|
||||||
|
newData.dialogue = {
|
||||||
|
stableId: 'dialogue1',
|
||||||
|
rhythm: data.rhythm_and_pacing,
|
||||||
|
style: data.style_and_voice
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 主题深化过程
|
||||||
|
if (data.name === 'Thematic Development') {
|
||||||
|
newData.themes = data.themes.map((theme: any, index: number) => ({
|
||||||
|
id: String(index),
|
||||||
|
stableId: `theme${index}`,
|
||||||
|
theme: theme.name,
|
||||||
|
desc: theme.desc,
|
||||||
|
depth: theme.depth
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// 戏演线
|
||||||
|
if (data.name === 'Dramatic Line') {
|
||||||
|
newData.dramaticLine = {
|
||||||
|
stableId: 'dramaticLine1',
|
||||||
|
points: data.stages.map((point: any, index: number) => ({
|
||||||
|
id: String(index),
|
||||||
|
stableId: `point${index}`,
|
||||||
|
title: point.stage,
|
||||||
|
desc: point.desc,
|
||||||
|
intensity: point.score
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只在数据真正变化时更新状态
|
||||||
|
if (Object.keys(newData).length > 0) {
|
||||||
|
updateStateIfChanged(newData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scriptwriterData,
|
||||||
|
thinkingText,
|
||||||
|
thinkingColor,
|
||||||
|
storyboardData,
|
||||||
|
sketchType
|
||||||
|
};
|
||||||
|
}
|
||||||
39
components/providers.tsx
Normal file
39
components/providers.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { store } from '@/lib/store/store';
|
||||||
|
import { ThemeProvider } from 'next-themes';
|
||||||
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
|
import AuthGuard from './auth/auth-guard';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
// 动态导入 OAuthCallbackHandler 和 DevHelper
|
||||||
|
const OAuthCallbackHandler = dynamic(
|
||||||
|
() => import('./ui/oauth-callback-handler').then(mod => mod.default),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const DevHelper = dynamic(
|
||||||
|
() => import('@/utils/dev-helper').then(mod => mod.default),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="dark"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
<AuthGuard>
|
||||||
|
{children}
|
||||||
|
</AuthGuard>
|
||||||
|
<Toaster />
|
||||||
|
<OAuthCallbackHandler />
|
||||||
|
{process.env.NODE_ENV === 'development' && <DevHelper />}
|
||||||
|
</ThemeProvider>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -151,6 +151,24 @@ export function CharacterTabContent({
|
|||||||
<Library className="w-6 h-6" />
|
<Library className="w-6 h-6" />
|
||||||
<span>Character library</span>
|
<span>Character library</span>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className={cn(
|
||||||
|
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
|
||||||
|
activeReplaceMethod === 'generate' && isReplaceModalOpen
|
||||||
|
? 'border-blue-500 bg-blue-500/10'
|
||||||
|
: 'border-white/10 hover:border-white/20'
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveReplaceMethod('generate');
|
||||||
|
setIsReplaceModalOpen(true);
|
||||||
|
}}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Wand2 className="w-6 h-6" />
|
||||||
|
<span>Generate character</span>
|
||||||
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
401
components/ui/drama-line-chart.tsx
Normal file
401
components/ui/drama-line-chart.tsx
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { TrendingUp, Eye, EyeOff } from 'lucide-react';
|
||||||
|
|
||||||
|
interface DataPoint {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DramaLineChartProps {
|
||||||
|
data?: DataPoint[];
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
onDataChange?: (data: DataPoint[]) => void;
|
||||||
|
className?: string;
|
||||||
|
showLabels?: boolean;
|
||||||
|
title?: string;
|
||||||
|
showToggleButton?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DramaLineChart({
|
||||||
|
data: initialData,
|
||||||
|
width,
|
||||||
|
height = 120,
|
||||||
|
onDataChange,
|
||||||
|
className = '',
|
||||||
|
showLabels = true,
|
||||||
|
title = '戏剧张力线',
|
||||||
|
showToggleButton = true
|
||||||
|
}: DramaLineChartProps) {
|
||||||
|
// Mock 数据 - 模拟一个经典的戏剧结构,x轴表示时间进度
|
||||||
|
const mockData: DataPoint[] = [
|
||||||
|
{ x: 0, y: 20, label: '0s' },
|
||||||
|
{ x: 15, y: 35, label: '1' },
|
||||||
|
{ x: 30, y: 45, label: '2s' },
|
||||||
|
{ x: 45, y: 65, label: '3s' },
|
||||||
|
{ x: 60, y: 85, label: '4s' },
|
||||||
|
{ x: 75, y: 70, label: '5s' },
|
||||||
|
{ x: 90, y: 40, label: '6s' },
|
||||||
|
{ x: 100, y: 25, label: '7s' },
|
||||||
|
{ x: 100, y: 25, label: '8s' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const [data, setData] = useState<DataPoint[]>(initialData || mockData);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [dragIndex, setDragIndex] = useState<number | null>(null);
|
||||||
|
const [isVisible, setIsVisible] = useState(!showToggleButton);
|
||||||
|
const [hoveredPoint, setHoveredPoint] = useState<number | null>(null);
|
||||||
|
const [containerWidth, setContainerWidth] = useState(width || 320);
|
||||||
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 动态计算容器宽度
|
||||||
|
useEffect(() => {
|
||||||
|
const updateWidth = () => {
|
||||||
|
if (containerRef.current && !width) {
|
||||||
|
setContainerWidth(containerRef.current.offsetWidth - 8); // 减去padding
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateWidth();
|
||||||
|
window.addEventListener('resize', updateWidth);
|
||||||
|
return () => window.removeEventListener('resize', updateWidth);
|
||||||
|
}, [width]);
|
||||||
|
|
||||||
|
// 计算SVG坐标
|
||||||
|
const padding = 20;
|
||||||
|
let chartWidth = (width || containerWidth) - padding * 4;
|
||||||
|
chartWidth = showToggleButton ? chartWidth : (width || containerWidth) - padding * 2;
|
||||||
|
const chartHeight = height - padding * 2;
|
||||||
|
|
||||||
|
const getPointPosition = useCallback((point: DataPoint) => {
|
||||||
|
return {
|
||||||
|
x: padding + (point.x / 100) * chartWidth,
|
||||||
|
y: padding + (1 - point.y / 100) * chartHeight
|
||||||
|
};
|
||||||
|
}, [chartWidth, chartHeight, padding]);
|
||||||
|
|
||||||
|
// 根据鼠标位置计算数据点
|
||||||
|
const getDataPointFromMouse = useCallback((clientX: number, clientY: number) => {
|
||||||
|
if (!svgRef.current) return null;
|
||||||
|
|
||||||
|
const rect = svgRef.current.getBoundingClientRect();
|
||||||
|
const x = clientX - rect.left;
|
||||||
|
const y = clientY - rect.top;
|
||||||
|
|
||||||
|
const dataX = Math.max(0, Math.min(100, ((x - padding) / chartWidth) * 100));
|
||||||
|
const dataY = Math.max(0, Math.min(100, (1 - (y - padding) / chartHeight) * 100));
|
||||||
|
|
||||||
|
return { x: dataX, y: dataY };
|
||||||
|
}, [chartWidth, chartHeight, padding]);
|
||||||
|
|
||||||
|
// 处理鼠标按下
|
||||||
|
const handleMouseDown = useCallback((e: React.MouseEvent, index: number) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
setDragIndex(index);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 处理鼠标移动
|
||||||
|
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||||
|
if (!isDragging || dragIndex === null) return;
|
||||||
|
|
||||||
|
const newPoint = getDataPointFromMouse(e.clientX, e.clientY);
|
||||||
|
if (!newPoint) return;
|
||||||
|
|
||||||
|
const newData = [...data];
|
||||||
|
// 保持x坐标不变,只允许改变y坐标
|
||||||
|
newData[dragIndex] = { ...newData[dragIndex], y: newPoint.y };
|
||||||
|
setData(newData);
|
||||||
|
onDataChange?.(newData);
|
||||||
|
}, [isDragging, dragIndex, data, getDataPointFromMouse, onDataChange]);
|
||||||
|
|
||||||
|
// 处理鼠标抬起
|
||||||
|
const handleMouseUp = useCallback(() => {
|
||||||
|
setIsDragging(false);
|
||||||
|
setDragIndex(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 添加全局事件监听
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDragging) {
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [isDragging, handleMouseMove, handleMouseUp]);
|
||||||
|
|
||||||
|
// 生成路径字符串
|
||||||
|
const pathData = data.map((point, index) => {
|
||||||
|
const pos = getPointPosition(point);
|
||||||
|
return index === 0 ? `M ${pos.x} ${pos.y}` : `L ${pos.x} ${pos.y}`;
|
||||||
|
}).join(' ');
|
||||||
|
|
||||||
|
// 生成平滑曲线路径
|
||||||
|
const smoothPathData = React.useMemo(() => {
|
||||||
|
if (data.length < 2) return pathData;
|
||||||
|
|
||||||
|
const points = data.map(getPointPosition);
|
||||||
|
let path = `M ${points[0].x} ${points[0].y}`;
|
||||||
|
|
||||||
|
for (let i = 1; i < points.length; i++) {
|
||||||
|
const prev = points[i - 1];
|
||||||
|
const curr = points[i];
|
||||||
|
const next = points[i + 1];
|
||||||
|
|
||||||
|
if (i === 1) {
|
||||||
|
// 第二个点
|
||||||
|
const cp1x = prev.x + (curr.x - prev.x) * 0.3;
|
||||||
|
const cp1y = prev.y;
|
||||||
|
const cp2x = curr.x - (curr.x - prev.x) * 0.3;
|
||||||
|
const cp2y = curr.y;
|
||||||
|
path += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${curr.x} ${curr.y}`;
|
||||||
|
} else if (i === points.length - 1) {
|
||||||
|
// 最后一个点
|
||||||
|
const cp1x = prev.x + (curr.x - prev.x) * 0.3;
|
||||||
|
const cp1y = prev.y;
|
||||||
|
const cp2x = curr.x - (curr.x - prev.x) * 0.3;
|
||||||
|
const cp2y = curr.y;
|
||||||
|
path += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${curr.x} ${curr.y}`;
|
||||||
|
} else {
|
||||||
|
// 中间的点
|
||||||
|
const prevPrev = points[i - 2];
|
||||||
|
const cp1x = prev.x + (curr.x - prevPrev.x) * 0.15;
|
||||||
|
const cp1y = prev.y + (curr.y - prevPrev.y) * 0.15;
|
||||||
|
const cp2x = curr.x - (next.x - prev.x) * 0.15;
|
||||||
|
const cp2y = curr.y - (next.y - prev.y) * 0.15;
|
||||||
|
path += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${curr.x} ${curr.y}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}, [data, getPointPosition, pathData]);
|
||||||
|
|
||||||
|
// 切换显示状态
|
||||||
|
const toggleVisibility = () => {
|
||||||
|
setIsVisible(!isVisible);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${className}`} ref={containerRef}>
|
||||||
|
{/* 切换按钮 */}
|
||||||
|
{showToggleButton && (
|
||||||
|
<motion.button
|
||||||
|
onClick={toggleVisibility}
|
||||||
|
className="absolute top-2 left-2 z-20 p-2 rounded-lg bg-black/20 backdrop-blur-sm
|
||||||
|
border border-white/20 text-white/80 hover:text-white hover:bg-black/30
|
||||||
|
transition-all duration-200"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
title={isVisible ? '隐藏戏剧线' : '显示戏剧线'}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: isVisible ? 0 : 180 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
{isVisible ? (
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<EyeOff className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 折线图容器 */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{isVisible && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
|
className={showToggleButton ? "absolute bottom-0 left-0 z-10 pointer-events-auto" : "relative w-full pointer-events-auto"}
|
||||||
|
>
|
||||||
|
<div className={showToggleButton ? "w-full rounded-lg bg-black/20 backdrop-blur-sm p-3" : "w-full p-2"}>
|
||||||
|
{/* 标题 */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.2, duration: 0.3 }}
|
||||||
|
className="flex items-center gap-2 mb-2"
|
||||||
|
>
|
||||||
|
<TrendingUp className="w-4 h-4 text-blue-400" />
|
||||||
|
<span className="text-sm font-medium text-white/90">{title}</span>
|
||||||
|
<span className="text-xs text-white/50">(Drag to adjust the tension value)</span>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* SVG 图表 */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.9, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
transition={{ delay: 0.1, duration: 0.4, ease: "easeOut" }}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
|
width={width || containerWidth}
|
||||||
|
height={height}
|
||||||
|
className="overflow-visible cursor-crosshair"
|
||||||
|
style={{ maxWidth: '100%', height: 'auto' }}
|
||||||
|
>
|
||||||
|
{/* 网格线 */}
|
||||||
|
<defs>
|
||||||
|
<pattern
|
||||||
|
id="grid"
|
||||||
|
width={chartWidth / 10}
|
||||||
|
height={chartHeight / 5}
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d={`M 0 0 L ${chartWidth / 10} 0 M 0 0 L 0 ${chartHeight / 5}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="rgba(255,255,255,0.1)"
|
||||||
|
strokeWidth="0.5"
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
{/* 渐变 */}
|
||||||
|
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stopColor="#fff" stopOpacity="0.4" />
|
||||||
|
<stop offset="50%" stopColor="#fff" stopOpacity="0.4" />
|
||||||
|
<stop offset="100%" stopColor="#fff" stopOpacity="0.4" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
{/* 区域渐变 */}
|
||||||
|
<linearGradient id="areaGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor="#3b82f6" stopOpacity="0.3" />
|
||||||
|
<stop offset="100%" stopColor="#3b82f6" stopOpacity="0.05" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* 主线条 */}
|
||||||
|
<motion.path
|
||||||
|
d={smoothPathData}
|
||||||
|
fill="none"
|
||||||
|
stroke="url(#lineGradient)"
|
||||||
|
strokeWidth="2"
|
||||||
|
initial={{ pathLength: 0 }}
|
||||||
|
animate={{ pathLength: 1 }}
|
||||||
|
transition={{ duration: 1.2, delay: 0.4, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 数据点 */}
|
||||||
|
{data.map((point, index) => {
|
||||||
|
const pos = getPointPosition(point);
|
||||||
|
const isHovered = hoveredPoint === index;
|
||||||
|
const isDraggingThis = dragIndex === index;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g key={index}>
|
||||||
|
{/* 点的光环效果 */}
|
||||||
|
{(isHovered || isDraggingThis) && (
|
||||||
|
<motion.circle
|
||||||
|
cx={pos.x}
|
||||||
|
cy={pos.y}
|
||||||
|
r="8"
|
||||||
|
fill="none"
|
||||||
|
stroke="#3b82f6"
|
||||||
|
strokeWidth="1"
|
||||||
|
opacity="0.5"
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: [1, 1.5, 1] }}
|
||||||
|
transition={{ duration: 1, repeat: Infinity }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 主要的点 */}
|
||||||
|
<motion.circle
|
||||||
|
cx={pos.x}
|
||||||
|
cy={pos.y}
|
||||||
|
r={isHovered || isDraggingThis ? "5" : "3"}
|
||||||
|
fill="#3b82f680"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="1"
|
||||||
|
className="cursor-grab active:cursor-grabbing"
|
||||||
|
onMouseDown={(e) => handleMouseDown(e, index)}
|
||||||
|
onMouseEnter={() => setHoveredPoint(index)}
|
||||||
|
onMouseLeave={() => setHoveredPoint(null)}
|
||||||
|
whileHover={{ scale: 1.3 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
initial={{ scale: 0, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
transition={{
|
||||||
|
delay: 0.5 + index * 0.1,
|
||||||
|
duration: 0.3,
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 300
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 数值标签 */}
|
||||||
|
{(isHovered || isDraggingThis) && showLabels && (
|
||||||
|
<motion.g
|
||||||
|
initial={{ opacity: 0, y: 5 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 5 }}
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x={pos.x - 15}
|
||||||
|
y={pos.y - 25}
|
||||||
|
width="30"
|
||||||
|
height="16"
|
||||||
|
rx="8"
|
||||||
|
fill="rgba(0,0,0,0.8)"
|
||||||
|
stroke="rgba(255,255,255,0.2)"
|
||||||
|
strokeWidth="0.5"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={pos.x}
|
||||||
|
y={pos.y - 15}
|
||||||
|
textAnchor="middle"
|
||||||
|
className="text-xs fill-white font-medium"
|
||||||
|
>
|
||||||
|
{Math.round(point.y)}
|
||||||
|
</text>
|
||||||
|
</motion.g>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* 时间轴标签 - 替代原来的底部标签 */}
|
||||||
|
{showLabels && data.map((point, index) => {
|
||||||
|
const pos = getPointPosition(point);
|
||||||
|
// 只显示部分标签避免重叠
|
||||||
|
const shouldShow = index === 0 || index === data.length - 1 || index % 2 === 0;
|
||||||
|
|
||||||
|
return shouldShow ? (
|
||||||
|
<motion.text
|
||||||
|
key={`time-${index}`}
|
||||||
|
x={pos.x}
|
||||||
|
y={height - 5}
|
||||||
|
textAnchor="middle"
|
||||||
|
className="text-xs fill-white/60"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.8 + index * 0.1 }}
|
||||||
|
>
|
||||||
|
{point.label}
|
||||||
|
</motion.text>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -48,15 +48,15 @@ export function EditModal({
|
|||||||
const [currentIndex, setCurrentIndex] = useState(currentSketchIndex);
|
const [currentIndex, setCurrentIndex] = useState(currentSketchIndex);
|
||||||
const [currentRoleIndex, setCurrentRoleIndex] = useState(0);
|
const [currentRoleIndex, setCurrentRoleIndex] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentIndex(currentSketchIndex);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
// 当 activeEditTab 改变时更新 activeTab
|
// 当 activeEditTab 改变时更新 activeTab
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveTab(activeEditTab);
|
setActiveTab(activeEditTab);
|
||||||
}, [activeEditTab]);
|
}, [activeEditTab]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCurrentIndex(currentSketchIndex);
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
const isTabDisabled = (tabId: string) => {
|
const isTabDisabled = (tabId: string) => {
|
||||||
if (tabId === 'settings') return false;
|
if (tabId === 'settings') return false;
|
||||||
// 换成 如果对应标签下 数据存在 就不禁用
|
// 换成 如果对应标签下 数据存在 就不禁用
|
||||||
@ -75,6 +75,12 @@ export function EditModal({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleChangeTab = (tabId: string, disabled: boolean) => {
|
||||||
|
if (disabled) return;
|
||||||
|
setActiveTab(tabId);
|
||||||
|
setCurrentIndex(0);
|
||||||
|
}
|
||||||
|
|
||||||
const renderTabContent = () => {
|
const renderTabContent = () => {
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case '1':
|
case '1':
|
||||||
@ -170,7 +176,7 @@ export function EditModal({
|
|||||||
activeTab === tab.id ? 'text-white' : 'text-white/50',
|
activeTab === tab.id ? 'text-white' : 'text-white/50',
|
||||||
disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-white/10',
|
disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-white/10',
|
||||||
)}
|
)}
|
||||||
onClick={() => !disabled && setActiveTab(tab.id)}
|
onClick={() => handleChangeTab(tab.id, disabled)}
|
||||||
whileHover={disabled ? undefined : { scale: 1.02 }}
|
whileHover={disabled ? undefined : { scale: 1.02 }}
|
||||||
whileTap={disabled ? undefined : { scale: 0.98 }}
|
whileTap={disabled ? undefined : { scale: 0.98 }}
|
||||||
>
|
>
|
||||||
|
|||||||
128
components/ui/generate-character-model.tsx
Normal file
128
components/ui/generate-character-model.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { X, ChevronDown } from 'lucide-react';
|
||||||
|
import { cn } from '@/public/lib/utils';
|
||||||
|
|
||||||
|
interface GenerateCharacterModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onGenerate: (params: { text: string; duration: string }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GenerateCharacterModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onGenerate
|
||||||
|
}: GenerateCharacterModalProps) {
|
||||||
|
const [text, setText] = useState('');
|
||||||
|
const [duration, setDuration] = useState('5');
|
||||||
|
const [characterUrl, setCharacterUrl] = useState('');
|
||||||
|
|
||||||
|
const handleGenerate = () => {
|
||||||
|
onGenerate({ text, duration });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
{/* 背景遮罩 */}
|
||||||
|
<motion.div
|
||||||
|
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 弹窗内容 */}
|
||||||
|
<div className="fixed inset-x-0 bottom-0 z-50 flex justify-center">
|
||||||
|
<motion.div
|
||||||
|
className="w-[66%] min-w-[800px] bg-[#1a1b1e] rounded-t-2xl overflow-hidden"
|
||||||
|
initial={{ y: '100%' }}
|
||||||
|
animate={{ y: 0 }}
|
||||||
|
exit={{ y: '100%' }}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
damping: 25,
|
||||||
|
stiffness: 200,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 标题栏 */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
className="p-1 hover:bg-white/10 rounded-full transition-colors"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<ChevronDown className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<h2 className="text-lg font-medium">generate character</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 主要内容区域 */}
|
||||||
|
<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">describe the character</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="describe the gender, age, and appearance of the character you want to generate..."
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 生成按钮 */}
|
||||||
|
<div className="flex justify-end flex-shrink-0">
|
||||||
|
<motion.button
|
||||||
|
className="px-6 py-2 bg-blue-500 hover:bg-blue-600 rounded-lg
|
||||||
|
transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
onClick={handleGenerate}
|
||||||
|
disabled={!text.trim()}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
generate character
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 角色预览区 图片 16/9 */}
|
||||||
|
<div className="flex justify-center flex-1 h-[450px]">
|
||||||
|
<div className="w-[800px] h-full bg-white/5 rounded-lg">
|
||||||
|
<img src={characterUrl} className="w-full object-contain" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部操作栏 */}
|
||||||
|
<div className="p-4 border-t border-white/10 bg-black/20">
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<motion.button
|
||||||
|
className="px-4 py-2 rounded-lg bg-blue-500/10 text-blue-400 hover:bg-blue-500/20 transition-colors"
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
add to library
|
||||||
|
</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>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -115,8 +115,8 @@ export function GenerateVideoModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 生成视频预览 */}
|
{/* 生成视频预览 */}
|
||||||
<div className="flex justify-center flex-1 min-h-[450px]">
|
<div className="flex justify-center flex-1 h-[450px]">
|
||||||
<div className="w-[80%] bg-white/5 rounded-lg">
|
<div className="w-[800px] h-full bg-white/5 rounded-lg">
|
||||||
<video src={videoUrl} className="w-full object-cover" autoPlay />
|
<video src={videoUrl} className="w-full object-cover" autoPlay />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -126,12 +126,11 @@ export function GenerateVideoModal({
|
|||||||
<div className="p-4 border-t border-white/10 bg-black/20">
|
<div className="p-4 border-t border-white/10 bg-black/20">
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
<motion.button
|
<motion.button
|
||||||
className="px-4 py-2 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
|
className="px-4 py-2 rounded-lg bg-blue-500/10 text-blue-400 hover:bg-blue-500/20 transition-colors"
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={onClose}
|
|
||||||
>
|
>
|
||||||
back
|
add to library
|
||||||
</motion.button>
|
</motion.button>
|
||||||
<motion.button
|
<motion.button
|
||||||
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
|
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { X, Upload, Library, Wand2, Search, Image, Plus, ChevronDown } from 'lucide-react';
|
import { X, Upload, Library, Wand2, Search, Image, Plus, ChevronDown } from 'lucide-react';
|
||||||
import { cn } from '@/public/lib/utils';
|
import { cn } from '@/public/lib/utils';
|
||||||
|
import { GenerateCharacterModal } from './generate-character-model';
|
||||||
|
|
||||||
interface ReplaceCharacterModalProps {
|
interface ReplaceCharacterModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -20,6 +21,7 @@ export function ReplaceCharacterModal({
|
|||||||
}: ReplaceCharacterModalProps) {
|
}: ReplaceCharacterModalProps) {
|
||||||
const [activeMethod, setActiveMethod] = useState(activeReplaceMethod);
|
const [activeMethod, setActiveMethod] = useState(activeReplaceMethod);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [isGenerateCharacterModalOpen, setIsGenerateCharacterModalOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveMethod(activeReplaceMethod);
|
setActiveMethod(activeReplaceMethod);
|
||||||
@ -121,6 +123,21 @@ export function ReplaceCharacterModal({
|
|||||||
<Library className="w-6 h-6" />
|
<Library className="w-6 h-6" />
|
||||||
<span>Library</span>
|
<span>Library</span>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className={cn(
|
||||||
|
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
|
||||||
|
activeMethod === 'generate'
|
||||||
|
? 'border-blue-500 bg-blue-500/10'
|
||||||
|
: 'border-white/10 hover:border-white/20'
|
||||||
|
)}
|
||||||
|
onClick={() => setActiveMethod('generate')}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Wand2 className="w-6 h-6" />
|
||||||
|
<span>Generate character</span>
|
||||||
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 内容区域 */}
|
{/* 内容区域 */}
|
||||||
@ -219,7 +236,7 @@ export function ReplaceCharacterModal({
|
|||||||
<motion.button
|
<motion.button
|
||||||
className="flex items-start gap-2 px-6 py-3 bg-blue-500 hover:bg-blue-600
|
className="flex items-start gap-2 px-6 py-3 bg-blue-500 hover:bg-blue-600
|
||||||
rounded-lg transition-colors"
|
rounded-lg transition-colors"
|
||||||
onClick={() => console.log('Generate character')}
|
onClick={() => setIsGenerateCharacterModalOpen(true)}
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
>
|
>
|
||||||
@ -253,6 +270,12 @@ export function ReplaceCharacterModal({
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<GenerateCharacterModal
|
||||||
|
isOpen={isGenerateCharacterModalOpen}
|
||||||
|
onClose={() => setIsGenerateCharacterModalOpen(false)}
|
||||||
|
onGenerate={() => console.log('Generate character')}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|||||||
@ -4,12 +4,16 @@ import { motion, AnimatePresence } from 'framer-motion';
|
|||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import WorkOffice from '@/components/workflow/work-office/work-office';
|
import WorkOffice from '@/components/workflow/work-office/work-office';
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useWorkofficeData } from '@/components/pages/work-flow/use-workoffice-data';
|
||||||
|
import { useAppSelector } from '@/lib/store/hooks';
|
||||||
|
import { storyboardData, productionData, editorData } from '@/components/workflow/work-office/mock-data';
|
||||||
|
|
||||||
interface ScriptModalProps {
|
interface ScriptModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
currentStage?: number;
|
currentStage?: number;
|
||||||
roles: any[];
|
roles: any[];
|
||||||
|
currentLoadingText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stages = [
|
const stages = [
|
||||||
@ -39,11 +43,38 @@ const stages = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export function ScriptModal({ isOpen, onClose, currentStage = 0, roles }: ScriptModalProps) {
|
export function ScriptModal({ isOpen, onClose, currentStage = 0, roles, currentLoadingText }: ScriptModalProps) {
|
||||||
const [isPlaying, setIsPlaying] = useState(true);
|
const [isPlaying, setIsPlaying] = useState(true);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [startTime, setStartTime] = useState<number | null>(null);
|
const [startTime, setStartTime] = useState<number | null>(null);
|
||||||
const prevStageRef = useRef(currentStage);
|
const prevStageRef = useRef(currentStage);
|
||||||
|
const [currentContent, setCurrentContent] = useState<Record<string, any>>({});
|
||||||
|
|
||||||
|
// 将 hook 调用移到组件顶层
|
||||||
|
const { scriptwriterData, thinkingText, thinkingColor, sketchType } = useWorkofficeData(currentStage, isOpen, currentLoadingText);
|
||||||
|
|
||||||
|
|
||||||
|
// 根据当前阶段加载对应数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
console.log('data', data);
|
||||||
|
setCurrentContent(data);
|
||||||
|
}, [currentStage, isOpen, scriptwriterData]);
|
||||||
|
|
||||||
// 每次打开都重置进度条和时间
|
// 每次打开都重置进度条和时间
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -185,7 +216,13 @@ export function ScriptModal({ isOpen, onClose, currentStage = 0, roles }: Script
|
|||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ delay: 0.1, duration: 0.2 }}
|
transition={{ delay: 0.1, duration: 0.2 }}
|
||||||
>
|
>
|
||||||
<WorkOffice initialStage={currentStage} roles={roles} />
|
<WorkOffice
|
||||||
|
initialStage={currentStage}
|
||||||
|
currentContent={currentContent}
|
||||||
|
thinkingText={thinkingText}
|
||||||
|
thinkingColor={thinkingColor}
|
||||||
|
sketchType={sketchType}
|
||||||
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
87
components/ui/select-dropdown.tsx
Normal file
87
components/ui/select-dropdown.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { cn } from "@/public/lib/utils";
|
||||||
|
import { Check, ChevronDown } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface SettingOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectDropdownProps {
|
||||||
|
dropdownId: string;
|
||||||
|
label: string;
|
||||||
|
options: SettingOption[];
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectDropdown = (
|
||||||
|
{
|
||||||
|
dropdownId,
|
||||||
|
label,
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
}: SelectDropdownProps
|
||||||
|
) => {
|
||||||
|
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleDropdownToggle = (dropdownId: string) => {
|
||||||
|
setOpenDropdown(openDropdown === dropdownId ? null : dropdownId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full h-full">
|
||||||
|
<motion.button
|
||||||
|
className={cn(
|
||||||
|
"w-full px-4 py-2 rounded-lg border text-left flex items-center justify-between",
|
||||||
|
openDropdown === dropdownId
|
||||||
|
? "border-blue-500 bg-blue-500/10"
|
||||||
|
: "border-white/10 hover:border-white/20"
|
||||||
|
)}
|
||||||
|
onClick={() => handleDropdownToggle(dropdownId)}
|
||||||
|
whileHover={{ scale: 1.01 }}
|
||||||
|
whileTap={{ scale: 0.99 }}
|
||||||
|
>
|
||||||
|
<span>{options.find(opt => opt.value === value)?.label || value}</span>
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: openDropdown === dropdownId ? 180 : 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
</motion.div>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{openDropdown === dropdownId && (
|
||||||
|
<motion.div
|
||||||
|
className="absolute z-10 w-full mt-1 py-1 rounded-lg border border-white/10 bg-black/90 backdrop-blur-xl"
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<motion.button
|
||||||
|
key={option.value}
|
||||||
|
className={cn(
|
||||||
|
"w-full px-4 py-2 text-left flex items-center justify-between hover:bg-white/5",
|
||||||
|
value === option.value && "text-blue-500"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(option.value);
|
||||||
|
handleDropdownToggle(dropdownId);
|
||||||
|
}}
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
{value === option.value && <Check className="w-4 h-4" />}
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
@ -7,6 +7,7 @@ import { GlassIconButton } from './glass-icon-button';
|
|||||||
import { cn } from '@/public/lib/utils';
|
import { cn } from '@/public/lib/utils';
|
||||||
import { ReplaceVideoModal } from './replace-video-modal';
|
import { ReplaceVideoModal } from './replace-video-modal';
|
||||||
import { MediaPropertiesModal } from './media-properties-modal';
|
import { MediaPropertiesModal } from './media-properties-modal';
|
||||||
|
import { DramaLineChart } from './drama-line-chart';
|
||||||
|
|
||||||
interface VideoTabContentProps {
|
interface VideoTabContentProps {
|
||||||
taskSketch: any[];
|
taskSketch: any[];
|
||||||
@ -249,43 +250,19 @@ export function VideoTabContent({
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.2 }}
|
transition={{ delay: 0.2 }}
|
||||||
>
|
>
|
||||||
{/* 左列:编辑项 截取视频、设置转场、调节音量 */}
|
{/* 左列:戏剧张力线、调节音量 */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 视频截取 */}
|
{/* 戏剧张力线 */}
|
||||||
<div className="p-4 rounded-lg bg-white/5">
|
<div className="p-4 rounded-lg bg-white/5">
|
||||||
<h3 className="text-sm font-medium mb-2">Video clip</h3>
|
<DramaLineChart
|
||||||
<div className="flex items-center gap-4">
|
title="Drama line"
|
||||||
<input
|
height={135}
|
||||||
type="text"
|
showToggleButton={false}
|
||||||
placeholder="00:00"
|
className="w-full"
|
||||||
className="w-20 px-3 py-1 bg-white/5 border border-white/10 rounded-lg
|
onDataChange={(data) => {
|
||||||
text-center focus:outline-none focus:border-blue-500"
|
console.log('视频编辑戏剧线数据更新:', data);
|
||||||
/>
|
}}
|
||||||
<span className="text-white/50">To</span>
|
/>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="00:00"
|
|
||||||
className="w-20 px-3 py-1 bg-white/5 border border-white/10 rounded-lg
|
|
||||||
text-center focus:outline-none focus:border-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 转场设置 */}
|
|
||||||
<div className="p-4 rounded-lg bg-white/5">
|
|
||||||
<h3 className="text-sm font-medium mb-2">Transition</h3>
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
{['Fade', 'Slide', 'Zoom'].map((transition) => (
|
|
||||||
<motion.button
|
|
||||||
key={transition}
|
|
||||||
className="px-3 py-1 bg-white/5 hover:bg-white/10 rounded-lg text-sm"
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
>
|
|
||||||
{transition}
|
|
||||||
</motion.button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 音量调节 */}
|
{/* 音量调节 */}
|
||||||
@ -308,7 +285,7 @@ export function VideoTabContent({
|
|||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
>
|
>
|
||||||
<div className='text-sm font-medium mb-2'>Media properties</div>
|
<div className='text-sm font-medium mb-2'>More properties</div>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Heart } from 'lucide-react';
|
import { Heart } from 'lucide-react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||||
@ -65,225 +65,249 @@ const CustomTooltip = ({ active, payload, label }: any) => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Scriptwriter: React.FC<ScriptwriterProps> = ({ currentContent, isPlaying }) => {
|
// Three-act Structure Component
|
||||||
|
const ThreeActStructure = React.memo(({ acts, isPlaying }: { acts?: ScriptContent['acts']; isPlaying: boolean }) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-black/30 rounded-lg p-4">
|
||||||
|
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
|
||||||
|
<span>Three-act structure</span>
|
||||||
|
<IconLoading icon={Heart} isActive={isPlaying} color="#8b5cf6" />
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{acts && acts.length > 0 ? (
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Character Arc Design Component
|
||||||
|
const CharacterArcDesign = React.memo(({ characters, isPlaying }: { characters?: ScriptContent['characters']; isPlaying: boolean }) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-black/30 rounded-lg p-4">
|
||||||
|
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
|
||||||
|
<span>Character arc design</span>
|
||||||
|
<IconLoading icon={Heart} isActive={isPlaying} color="#8b5cf6" />
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{characters && characters.length > 0 ? (
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dialogue Rhythm Component
|
||||||
|
const DialogueRhythm = React.memo(({ dialogue, isPlaying }: { dialogue?: ScriptContent['dialogue']; isPlaying: boolean }) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-black/30 rounded-lg p-4">
|
||||||
|
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
|
||||||
|
<span>Dialogue rhythm</span>
|
||||||
|
<IconLoading icon={Heart} isActive={isPlaying} color="#8b5cf6" />
|
||||||
|
</h3>
|
||||||
|
{dialogue ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-purple-300 text-sm font-medium mb-1">Rhythm control</div>
|
||||||
|
<div className="text-gray-300 text-xs">
|
||||||
|
<TypewriterText text={dialogue.rhythm} stableId={`${dialogue.stableId}-rhythm`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-purple-300 text-sm font-medium mb-1">Expression style</div>
|
||||||
|
<div className="text-gray-300 text-xs">
|
||||||
|
<TypewriterText text={dialogue.style} stableId={`${dialogue.stableId}-style`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SkeletonCard className="bg-purple-500/20 rounded-lg p-3 border border-purple-500/30" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Theme Development Component
|
||||||
|
const ThemeDevelopment = React.memo(({ themes, isPlaying }: { themes?: ScriptContent['themes']; isPlaying: boolean }) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-black/30 rounded-lg p-4">
|
||||||
|
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
|
||||||
|
<span>Theme deepening process</span>
|
||||||
|
<IconLoading icon={Heart} isActive={isPlaying} color="#8b5cf6" />
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{themes ? (
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dramatic Line Component
|
||||||
|
const DramaticLine = React.memo(({ dramaticLine, isPlaying }: { dramaticLine?: ScriptContent['dramaticLine']; isPlaying: boolean }) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-black/30 rounded-lg p-4">
|
||||||
|
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
|
||||||
|
<span>Dramatic line</span>
|
||||||
|
<IconLoading icon={Heart} isActive={isPlaying} color="#8b5cf6" />
|
||||||
|
</h3>
|
||||||
|
{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={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: 'Emotional intensity',
|
||||||
|
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">
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const Scriptwriter: React.FC<ScriptwriterProps> = React.memo(({ currentContent, isPlaying }) => {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 gap-4 h-full">
|
<div className="grid grid-cols-2 gap-4 h-full">
|
||||||
{/* 左侧:三幕结构和角色弧光 */}
|
{/* 左侧:三幕结构和角色弧光 */}
|
||||||
<div className="space-y-4 overflow-y-auto">
|
<div className="space-y-4 overflow-y-auto">
|
||||||
{/* 三幕结构 */}
|
<ThreeActStructure acts={currentContent.acts} isPlaying={isPlaying} />
|
||||||
<div className="bg-black/30 rounded-lg p-4">
|
<CharacterArcDesign characters={currentContent.characters} isPlaying={isPlaying} />
|
||||||
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
|
|
||||||
<span>Three-act structure</span>
|
|
||||||
<IconLoading icon={Heart} isActive={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>Character arc design</span>
|
|
||||||
<IconLoading icon={Heart} isActive={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>
|
||||||
|
|
||||||
{/* 右侧:对白节奏和主题深化 */}
|
{/* 右侧:对白节奏和主题深化 */}
|
||||||
<div className="space-y-4 overflow-y-auto">
|
<div className="space-y-4 overflow-y-auto">
|
||||||
{/* 对白节奏感 */}
|
<DialogueRhythm dialogue={currentContent.dialogue} isPlaying={isPlaying} />
|
||||||
<div className="bg-black/30 rounded-lg p-4">
|
<ThemeDevelopment themes={currentContent.themes} isPlaying={isPlaying} />
|
||||||
<h3 className="text-white font-semibold mb-3 flex items-center space-x-2">
|
<DramaticLine dramaticLine={currentContent.dramaticLine} isPlaying={isPlaying} />
|
||||||
<span>Dialogue rhythm</span>
|
|
||||||
<IconLoading icon={Heart} isActive={isPlaying} color="#8b5cf6" />
|
|
||||||
</h3>
|
|
||||||
{currentContent.dialogue ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<div className="text-purple-300 text-sm font-medium mb-1">Rhythm control</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">Expression style</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>Theme deepening process</span>
|
|
||||||
<IconLoading icon={Heart} isActive={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>Dramatic line</span>
|
|
||||||
<IconLoading icon={Heart} isActive={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: 'Emotional intensity',
|
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
Scriptwriter.displayName = 'Scriptwriter';
|
||||||
|
|
||||||
export default Scriptwriter;
|
export default Scriptwriter;
|
||||||
|
|||||||
252
components/workflow/work-office/storyboard-artist copy.tsx
Normal file
252
components/workflow/work-office/storyboard-artist copy.tsx
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
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 StoryboardArtistProps {
|
||||||
|
currentContent: any;
|
||||||
|
isPlaying: boolean;
|
||||||
|
sketchType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SceneStoryboard = (currentContent: any, isPlaying: boolean) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 h-full overflow-y-auto">
|
||||||
|
<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>Scene location selection</span>
|
||||||
|
<IconLoading icon={Camera} isActive={isPlaying} color="#06b6d4" />
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{currentContent.location ? (
|
||||||
|
<div className="text-gray-300 text-xs">
|
||||||
|
<TypewriterText text={currentContent.location} stableId={currentContent.location} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SkeletonCard 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>Scene atmosphere selection</span>
|
||||||
|
<IconLoading icon={Camera} isActive={isPlaying} color="#06b6d4" />
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{currentContent.core_atmosphere ? (
|
||||||
|
<div className="text-gray-300 text-xs">
|
||||||
|
<TypewriterText text={currentContent.core_atmosphere} stableId={currentContent.core_atmosphere} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SkeletonCard className="bg-cyan-500/20 rounded-lg p-3 border border-cyan-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>Scene description</span>
|
||||||
|
<IconLoading icon={Camera} isActive={isPlaying} color="#06b6d4" />
|
||||||
|
</h3>
|
||||||
|
<div className='space-y-3'>
|
||||||
|
{currentContent.description ? (
|
||||||
|
<div className="text-gray-300 text-xs">
|
||||||
|
<TypewriterText text={currentContent.description} stableId={currentContent.description} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SkeletonCard className="bg-cyan-500/20 rounded-lg p-3 border border-cyan-500/30" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShotSketchStoryboard = (currentContent: any, isPlaying: boolean) => {
|
||||||
|
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>Shot language selection</span>
|
||||||
|
<IconLoading icon={Camera} isActive={isPlaying} color="#06b6d4" />
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{currentContent.shotLanguage && currentContent.shotLanguage.length > 0 ? (
|
||||||
|
currentContent.shotLanguage.map((shot: any, index: number) => (
|
||||||
|
<ContentCard
|
||||||
|
key={index}
|
||||||
|
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}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-300 text-xs leading-relaxed">
|
||||||
|
<TypewriterText text={currentContent.frame_description} stableId={`shot${index}`} />
|
||||||
|
</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>Composition aesthetics</span>
|
||||||
|
<IconLoading icon={Camera} isActive={isPlaying} color="#06b6d4" />
|
||||||
|
</h3>
|
||||||
|
{currentContent.atmosphere && currentContent.atmosphere.length > 0 ? (
|
||||||
|
<ContentCard className="space-y-3">
|
||||||
|
{currentContent.atmosphere.map((atmosphere: any, index: number) => (
|
||||||
|
<div key={index}>
|
||||||
|
<div className="text-cyan-300 text-sm font-medium">
|
||||||
|
<TypewriterText text={atmosphere} stableId={`${index}-atmosphere`} />
|
||||||
|
</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>Camera movement design</span>
|
||||||
|
<IconLoading icon={Camera} isActive={isPlaying} color="#06b6d4" />
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{currentContent.camera_motion && currentContent.camera_motion.length > 0 ? (
|
||||||
|
currentContent.camera_motion.map((move: any, index: number) => (
|
||||||
|
<ContentCard
|
||||||
|
key={index}
|
||||||
|
className="bg-cyan-400/20 rounded-lg p-3"
|
||||||
|
>
|
||||||
|
<div className="text-cyan-200 font-medium text-sm mb-1">{move}</div>
|
||||||
|
<div className="text-cyan-300 text-xs">
|
||||||
|
<TypewriterText text={currentContent.composition} stableId={`${index}-composition`} />
|
||||||
|
</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>Visual storytelling logic</span>
|
||||||
|
<IconLoading icon={Camera} isActive={isPlaying} color="#06b6d4" />
|
||||||
|
</h3>
|
||||||
|
{currentContent.key_action ? (
|
||||||
|
<ContentCard className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-cyan-300 text-sm font-medium mb-1">Storytelling logic</div>
|
||||||
|
<div className="text-gray-300 text-xs">
|
||||||
|
<TypewriterText text={currentContent.frame_description} stableId={`frame-description-logic`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-cyan-300 text-sm font-medium mb-1">Key emphasis</div>
|
||||||
|
<div className="text-gray-300 text-xs">
|
||||||
|
<TypewriterText text={currentContent.key_action} stableId={`key-action-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>Dialogue performance</span>
|
||||||
|
<IconLoading icon={Camera} isActive={isPlaying} color="#06b6d4" />
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{currentContent.dialogue_performance ? (
|
||||||
|
<>
|
||||||
|
<ContentCard
|
||||||
|
className="bg-cyan-300/20 rounded p-2"
|
||||||
|
>
|
||||||
|
<div className="text-cyan-200 text-sm font-medium">Speaker</div>
|
||||||
|
<div className="text-gray-300 text-xs">
|
||||||
|
<TypewriterText text={currentContent.dialogue_performance.speaker} stableId={`speaker`} />
|
||||||
|
</div>
|
||||||
|
</ContentCard>
|
||||||
|
<ContentCard
|
||||||
|
className="bg-cyan-300/20 rounded p-2"
|
||||||
|
>
|
||||||
|
<div className="text-cyan-200 text-sm font-medium">Language</div>
|
||||||
|
<div className="text-gray-300 text-xs">
|
||||||
|
<TypewriterText text={currentContent.dialogue_performance.language} stableId={`language`} />
|
||||||
|
</div>
|
||||||
|
</ContentCard>
|
||||||
|
<ContentCard
|
||||||
|
className="bg-cyan-300/20 rounded p-2"
|
||||||
|
>
|
||||||
|
<div className="text-cyan-200 text-sm font-medium">Delivery</div>
|
||||||
|
<div className="text-gray-300 text-xs">
|
||||||
|
<TypewriterText text={currentContent.dialogue_performance.delivery} stableId={`delivery`} />
|
||||||
|
</div>
|
||||||
|
</ContentCard>
|
||||||
|
<ContentCard
|
||||||
|
className="bg-cyan-300/20 rounded p-2"
|
||||||
|
>
|
||||||
|
<div className="text-cyan-200 text-sm font-medium">Line</div>
|
||||||
|
<div className="text-gray-300 text-xs">
|
||||||
|
<TypewriterText text={currentContent.dialogue_performance.line} stableId={`line`} />
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const StoryboardArtist: React.FC<StoryboardArtistProps> = ({ currentContent, isPlaying, sketchType }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{sketchType === 'scene' ? <SceneStoryboard currentContent={currentContent} isPlaying={isPlaying} /> : <ShotSketchStoryboard currentContent={currentContent} isPlaying={isPlaying} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StoryboardArtist;
|
||||||
@ -1,56 +1,11 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { Heart, Camera, Film, Scissors } from 'lucide-react';
|
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import Scriptwriter from './scriptwriter';
|
import Scriptwriter from './scriptwriter';
|
||||||
import StoryboardArtist from './storyboard-artist';
|
import StoryboardArtist from './storyboard-artist';
|
||||||
import VisualDirector from './visual-director';
|
import VisualDirector from './visual-director';
|
||||||
import Editor from './editor';
|
import Editor from './editor';
|
||||||
import { scriptwriterData, storyboardData, productionData, editorData } from './mock-data';
|
import { storyboardData, productionData, editorData } from './mock-data';
|
||||||
|
|
||||||
interface Stage {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
icon: React.ElementType;
|
|
||||||
color: string;
|
|
||||||
profession: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stages: Stage[] = [
|
|
||||||
{
|
|
||||||
id: 'script',
|
|
||||||
title: 'Scriptwriter',
|
|
||||||
icon: Heart,
|
|
||||||
color: '#8b5cf6',
|
|
||||||
profession: 'Scriptwriter'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'storyboard',
|
|
||||||
title: 'Storyboard artist',
|
|
||||||
icon: Camera,
|
|
||||||
color: '#06b6d4',
|
|
||||||
profession: 'Storyboard artist'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'production',
|
|
||||||
title: 'Visual director',
|
|
||||||
icon: Film,
|
|
||||||
color: '#10b981',
|
|
||||||
profession: 'Visual director'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'editing',
|
|
||||||
title: 'Editor',
|
|
||||||
icon: Scissors,
|
|
||||||
color: '#f59e0b',
|
|
||||||
profession: 'Editor'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
const actionsText = [
|
|
||||||
'is thinking...',
|
|
||||||
'is drawing...',
|
|
||||||
'is directing...',
|
|
||||||
'is editing...'
|
|
||||||
]
|
|
||||||
|
|
||||||
// 思考指示器组件
|
// 思考指示器组件
|
||||||
const ThinkingDots = ({ show, text, color }: { show: boolean; text: string; color: string }) => {
|
const ThinkingDots = ({ show, text, color }: { show: boolean; text: string; color: string }) => {
|
||||||
@ -83,51 +38,30 @@ const ThinkingDots = ({ show, text, color }: { show: boolean; text: string; colo
|
|||||||
|
|
||||||
interface WorkOfficeProps {
|
interface WorkOfficeProps {
|
||||||
initialStage?: number;
|
initialStage?: number;
|
||||||
roles: any[];
|
currentContent: Record<string, any>;
|
||||||
|
thinkingText: string;
|
||||||
|
thinkingColor: string;
|
||||||
|
sketchType: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WorkOffice: React.FC<WorkOfficeProps> = ({ initialStage = 0 }) => {
|
const WorkOffice: React.FC<WorkOfficeProps> = ({ initialStage = 0, currentContent, thinkingText, thinkingColor, sketchType }) => {
|
||||||
const [currentStage, setCurrentStage] = useState(initialStage);
|
const [currentStage, setCurrentStage] = useState(initialStage);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [currentContent, setCurrentContent] = useState<Record<string, any>>(scriptwriterData);
|
|
||||||
const [thinkingText, setThinkingText] = useState(`${stages[0].profession} ${actionsText[0]}`);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// currentStage 更新 重新渲染当前工作台组件
|
|
||||||
setCurrentStage(initialStage);
|
setCurrentStage(initialStage);
|
||||||
|
setIsPlaying(true);
|
||||||
}, [initialStage]);
|
}, [initialStage]);
|
||||||
|
|
||||||
// 根据当前阶段加载对应数据
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsPlaying(true);
|
// 使用 useMemo 缓存工作台组件
|
||||||
setCurrentContent(data);
|
const currentWorkstation = useMemo(() => {
|
||||||
setThinkingText(`${stages[currentStage].profession} ${actionsText[currentStage]}`);
|
console.log('工作台组件重新渲染', currentContent);
|
||||||
|
|
||||||
}, [currentStage]);
|
|
||||||
|
|
||||||
// 渲染当前工作台组件
|
|
||||||
const renderCurrentWorkstation = () => {
|
|
||||||
switch (currentStage) {
|
switch (currentStage) {
|
||||||
case 0:
|
case 0:
|
||||||
return <Scriptwriter currentContent={currentContent} isPlaying={isPlaying} />;
|
return <Scriptwriter currentContent={currentContent} isPlaying={isPlaying} />;
|
||||||
case 1:
|
case 1:
|
||||||
return <StoryboardArtist currentContent={currentContent} isPlaying={isPlaying} />;
|
return <StoryboardArtist currentContent={currentContent} isPlaying={isPlaying} sketchType={sketchType} />;
|
||||||
case 2:
|
case 2:
|
||||||
return <VisualDirector currentContent={currentContent} isPlaying={isPlaying} />;
|
return <VisualDirector currentContent={currentContent} isPlaying={isPlaying} />;
|
||||||
case 3:
|
case 3:
|
||||||
@ -135,25 +69,31 @@ const WorkOffice: React.FC<WorkOfficeProps> = ({ initialStage = 0 }) => {
|
|||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
}, [currentStage, isPlaying, currentContent]);
|
||||||
|
|
||||||
|
// 使用 useMemo 缓存 ThinkingDots 组件
|
||||||
|
const thinkingDots = useMemo(() => (
|
||||||
|
<ThinkingDots
|
||||||
|
show={isPlaying}
|
||||||
|
text={thinkingText}
|
||||||
|
color={thinkingColor}
|
||||||
|
/>
|
||||||
|
), [isPlaying, thinkingText, thinkingColor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full rounded-2xl overflow-hidden shadow-2xl relative">
|
<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">
|
<div className="absolute top-[0] left-1/2 -translate-x-1/2 z-10">
|
||||||
<ThinkingDots
|
{thinkingDots}
|
||||||
show={isPlaying}
|
|
||||||
text={thinkingText}
|
|
||||||
color={stages[currentStage].color}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 工作台内容区域 */}
|
{/* 工作台内容区域 */}
|
||||||
<div className="absolute left-0 right-0 top-[2rem] w-full aspect-video overflow-y-auto" style={{height: 'calc(100% - 7rem'}}>
|
<div className="absolute left-0 right-0 top-[2rem] w-full aspect-video overflow-y-auto" style={{height: 'calc(100% - 7rem'}}>
|
||||||
{renderCurrentWorkstation()}
|
{currentWorkstation}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WorkOffice;
|
// 使用 React.memo 包装组件
|
||||||
|
export default React.memo(WorkOffice);
|
||||||
|
|||||||
5
lib/store/hooks.ts
Normal file
5
lib/store/hooks.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
|
||||||
|
import type { RootState, AppDispatch } from './store';
|
||||||
|
|
||||||
|
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||||
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||||
11
lib/store/store.ts
Normal file
11
lib/store/store.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
import workflowReducer from './workflowSlice';
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
workflow: workflowReducer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
28
lib/store/workflowSlice.ts
Normal file
28
lib/store/workflowSlice.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
interface WorkflowState {
|
||||||
|
sketchCount: number;
|
||||||
|
videoCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: WorkflowState = {
|
||||||
|
sketchCount: 0,
|
||||||
|
videoCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const workflowSlice = createSlice({
|
||||||
|
name: 'workflow',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setSketchCount: (state, action: PayloadAction<number>) => {
|
||||||
|
state.sketchCount = action.payload;
|
||||||
|
},
|
||||||
|
setVideoCount: (state, action: PayloadAction<number>) => {
|
||||||
|
state.videoCount = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setSketchCount, setVideoCount } = workflowSlice.actions;
|
||||||
|
|
||||||
|
export default workflowSlice.reducer;
|
||||||
1117
package-lock.json
generated
1117
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -43,6 +43,7 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
|
"@reduxjs/toolkit": "^2.8.2",
|
||||||
"@tensorflow-models/coco-ssd": "^2.2.3",
|
"@tensorflow-models/coco-ssd": "^2.2.3",
|
||||||
"@tensorflow/tfjs": "^4.22.0",
|
"@tensorflow/tfjs": "^4.22.0",
|
||||||
"@types/gsap": "^1.20.2",
|
"@types/gsap": "^1.20.2",
|
||||||
@ -85,6 +86,7 @@
|
|||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
"react-intersection-observer": "^9.16.0",
|
"react-intersection-observer": "^9.16.0",
|
||||||
"react-joyride": "^2.9.3",
|
"react-joyride": "^2.9.3",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-resizable": "^3.0.5",
|
"react-resizable": "^3.0.5",
|
||||||
"react-resizable-panels": "^2.1.3",
|
"react-resizable-panels": "^2.1.3",
|
||||||
"react-rough-notation": "^1.0.5",
|
"react-rough-notation": "^1.0.5",
|
||||||
|
|||||||
@ -55,5 +55,37 @@ POST https://pre.movieflow.api.huiying.video/movie/text_to_script_tags
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"project_id": "244e01ae-01e8-4854-b56d-76a7bb57b2bb"
|
"project_id": "54256c52-236f-45b2-8901-b696c45ad540"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Analyze movie script stream
|
||||||
|
POST https://pre.movieflow.api.huiying.video/movie/analyze_movie_script_stream
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"project_id": "54256c52-236f-45b2-8901-b696c45ad540"
|
||||||
|
}
|
||||||
|
|
||||||
|
### test scene json
|
||||||
|
POST https://pre.movieflow.api.huiying.video/movie/scene_json
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"project_id": "c3aea4e3-4f5a-4853-91af-54486568d914"
|
||||||
|
}
|
||||||
|
|
||||||
|
### test shot sketch json
|
||||||
|
POST https://pre.movieflow.api.huiying.video/movie/shot_sketch_json
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"project_id": "c3aea4e3-4f5a-4853-91af-54486568d914"
|
||||||
|
}
|
||||||
|
|
||||||
|
### test video json
|
||||||
|
POST https://pre.movieflow.api.huiying.video/movie/video_json
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"project_id": "c3aea4e3-4f5a-4853-91af-54486568d914"
|
||||||
}
|
}
|
||||||
28
utils/dev-helper.tsx
Normal file
28
utils/dev-helper.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function DevHelper() {
|
||||||
|
useEffect(() => {
|
||||||
|
// 在客户端环境下设置全局错误处理
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('error', (event) => {
|
||||||
|
if (event.error?.name === 'ChunkLoadError' || event.error?.message?.includes('Loading chunk')) {
|
||||||
|
console.warn('检测到 ChunkLoadError,尝试刷新页面');
|
||||||
|
window.location.reload();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
|
if (event.reason?.name === 'ChunkLoadError') {
|
||||||
|
console.warn('检测到未处理的 ChunkLoadError Promise 拒绝');
|
||||||
|
event.preventDefault();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user