forked from 77media/video-flow
更新 ScriptService 以支持解析脚本块的内容类型,新增 useScriptData 自定义 Hook 以管理剧本数据,优化工作流组件以集成新的剧本数据处理逻辑,调整 MediaViewer 组件以支持暂停工作流功能,重构 mock 数据以符合新的结构。
This commit is contained in:
parent
6df87250fb
commit
0c006cfd5e
@ -385,7 +385,7 @@ export const useScriptService = (): UseScriptService => {
|
||||
const scriptBlocksMemo = useMemo((): ScriptBlock[] => {
|
||||
return [
|
||||
parseScriptBlock('synopsis', 'Logline', synopsis || ''),
|
||||
parseScriptBlock('categories', 'GENRE', categories.join(', ') || ''),
|
||||
parseScriptBlock('categories', 'GENRE', categories.join(', ') || '', 'tag'),
|
||||
parseScriptBlock('protagonist', 'Core Identity', protagonist || ''),
|
||||
parseScriptBlock('incitingIncident', 'The Inciting Incident', incitingIncident || ''),
|
||||
parseScriptBlock('problem', 'The Problem & New Goal', problem || ''),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ScriptBlock, ScriptData } from "@/components/script-renderer/types";
|
||||
|
||||
import { ScriptEditKey } from "../usecase/ScriptEditUseCase";
|
||||
/**
|
||||
* 渲染数据转换器
|
||||
@ -10,15 +10,15 @@ import { ScriptEditKey } from "../usecase/ScriptEditUseCase";
|
||||
export function parseScriptBlock(
|
||||
key: ScriptEditKey,
|
||||
headerName: string,
|
||||
scriptText: string
|
||||
): ScriptBlock {
|
||||
scriptText: string,
|
||||
contentType?: 'paragraph' | 'bold' | 'italic' | 'heading' | 'tag',
|
||||
) {
|
||||
return {
|
||||
id: key,
|
||||
title: headerName,
|
||||
type: "core",
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
type: contentType || "paragraph",
|
||||
text: scriptText,
|
||||
},
|
||||
],
|
||||
|
||||
@ -13,6 +13,7 @@ import { usePlaybackControls } from "./work-flow/use-playback-controls";
|
||||
import { AlertCircle, RefreshCw, Pause, Play, ChevronLast } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
||||
import { useScriptData } from "./work-flow/use-script-data";
|
||||
|
||||
export default function WorkFlow() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@ -22,7 +23,6 @@ export default function WorkFlow() {
|
||||
// 使用自定义 hooks 管理状态
|
||||
const {
|
||||
taskObject,
|
||||
scriptData,
|
||||
taskSketch,
|
||||
taskScenes,
|
||||
taskShotSketch,
|
||||
@ -57,6 +57,10 @@ export default function WorkFlow() {
|
||||
playTimerRef,
|
||||
} = usePlaybackControls(taskSketch, taskVideos, currentStep);
|
||||
|
||||
const {
|
||||
scriptData
|
||||
} = useScriptData();
|
||||
|
||||
// 跟踪是否已经自动开始播放过,避免重复触发
|
||||
const hasAutoStartedRef = useRef(false);
|
||||
|
||||
@ -188,12 +192,13 @@ export default function WorkFlow() {
|
||||
onToggleVideoPlay={toggleVideoPlay}
|
||||
onTogglePlay={togglePlay}
|
||||
final={final}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="imageGrid-ymZV9z hide-scrollbar" style={{ display: currentStep === '6' ? 'none' : 'block' }}>
|
||||
<div className="imageGrid-ymZV9z hide-scrollbar" style={{ display: (currentStep === '6' || currentStep === '0') ? 'none' : 'block' }}>
|
||||
<ErrorBoundary>
|
||||
<ThumbnailGrid
|
||||
isLoading={isLoading}
|
||||
|
||||
@ -25,6 +25,7 @@ interface MediaViewerProps {
|
||||
onToggleVideoPlay: () => void;
|
||||
onTogglePlay: () => void;
|
||||
final?: any;
|
||||
setIsPauseWorkFlow: (isPause: boolean) => void;
|
||||
}
|
||||
|
||||
export function MediaViewer({
|
||||
@ -42,7 +43,8 @@ export function MediaViewer({
|
||||
onEditModalOpen,
|
||||
onToggleVideoPlay,
|
||||
onTogglePlay,
|
||||
final
|
||||
final,
|
||||
setIsPauseWorkFlow
|
||||
}: MediaViewerProps) {
|
||||
const mainVideoRef = useRef<HTMLVideoElement>(null);
|
||||
const finalVideoRef = useRef<HTMLVideoElement>(null);
|
||||
@ -806,7 +808,7 @@ export function MediaViewer({
|
||||
<div className="relative w-full h-full bg-white/10 rounded-lg overflow-hidden p-2">
|
||||
{
|
||||
mockScriptData ? (
|
||||
<ScriptRenderer data={mockScriptData} />
|
||||
<ScriptRenderer data={mockScriptData} setIsPauseWorkFlow={setIsPauseWorkFlow} />
|
||||
) : (
|
||||
<div className="flex gap-2 w-full h-full">
|
||||
<div className="w-[70%] h-full rounded-lg gap-2 flex flex-col">
|
||||
|
||||
44
components/pages/work-flow/use-script-data.tsx
Normal file
44
components/pages/work-flow/use-script-data.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useScriptService } from "@/app/service/Interaction/ScriptService";
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export const useScriptData = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const projectId = searchParams.get('episodeId') || '';
|
||||
|
||||
const {
|
||||
loading, // 加载状态
|
||||
synopsis, //故事梗概
|
||||
categories, //故事分类
|
||||
protagonist, //主角
|
||||
incitingIncident, //激励事件
|
||||
problem, //问题与新目标
|
||||
conflict, //冲突与障碍
|
||||
stakes, //赌注
|
||||
characterArc, //人物弧线完成
|
||||
planId, //计划ID
|
||||
aiOptimizing, //AI优化要求
|
||||
scriptBlocksMemo, // 渲染数据
|
||||
initializeFromProject,
|
||||
} = useScriptService();
|
||||
|
||||
const [scriptData, setScriptData] = useState<any>(null);
|
||||
|
||||
// 初始化剧本
|
||||
useEffect(() => {
|
||||
initializeFromProject(projectId);
|
||||
}, []);
|
||||
|
||||
// 监听剧本加载完毕
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
console.log('scriptBlocksMemo', scriptBlocksMemo);
|
||||
}
|
||||
}, [loading, scriptBlocksMemo]);
|
||||
|
||||
return {
|
||||
scriptData
|
||||
}
|
||||
}
|
||||
@ -57,7 +57,6 @@ export function useWorkflowData() {
|
||||
|
||||
// 更新 taskObject 的类型
|
||||
const [taskObject, setTaskObject] = useState<TaskObject | null>(null);
|
||||
const [scriptData, setScriptData] = useState<any>(null);
|
||||
const [taskSketch, setTaskSketch] = useState<any[]>([]);
|
||||
const [taskScenes, setTaskScenes] = useState<any[]>([]);
|
||||
const [taskShotSketch, setTaskShotSketch] = useState<any[]>([]);
|
||||
@ -557,7 +556,6 @@ export function useWorkflowData() {
|
||||
|
||||
return {
|
||||
taskObject,
|
||||
scriptData,
|
||||
taskSketch,
|
||||
taskScenes,
|
||||
taskShotSketch,
|
||||
|
||||
@ -9,9 +9,10 @@ import { TypewriterText } from '@/components/workflow/work-office/common/Typewri
|
||||
|
||||
interface ScriptRendererProps {
|
||||
data: ScriptData;
|
||||
setIsPauseWorkFlow: (isPause: boolean) => void;
|
||||
}
|
||||
|
||||
export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
|
||||
export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPauseWorkFlow }) => {
|
||||
const [activeBlockId, setActiveBlockId] = useState<string | null>(null);
|
||||
const [hoveredBlockId, setHoveredBlockId] = useState<string | null>(null);
|
||||
const contentRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
|
||||
@ -21,9 +22,11 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
|
||||
const [isInit, setIsInit] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const themeBlock = data.blocks.find(block => block.type === 'theme');
|
||||
if (themeBlock) {
|
||||
setAddThemeTag(themeBlock.content.map(item => item.text || ''));
|
||||
const themeBlock = data.blocks.find(block => block.id === 'categories');
|
||||
if (themeBlock && themeBlock.content.length > 0) {
|
||||
const themeTag = themeBlock.content[0].text.split(',').map(item => item.trim());
|
||||
console.log('themeTag', themeTag);
|
||||
setAddThemeTag(themeTag);
|
||||
}
|
||||
}, [data.blocks]);
|
||||
|
||||
@ -118,6 +121,7 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
|
||||
};
|
||||
|
||||
const handleEditBlock = (block: ScriptBlock) => {
|
||||
setIsPauseWorkFlow(true);
|
||||
setIsInit(false);
|
||||
setEditBlockId(block.id);
|
||||
setActiveBlockId(block.id);
|
||||
@ -145,8 +149,8 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
|
||||
};
|
||||
|
||||
const renderTypeBlock = (block: ScriptBlock, isHovered: boolean, isActive: boolean, isEditing: boolean) => {
|
||||
switch (block.type) {
|
||||
case 'theme':
|
||||
switch (block.id) {
|
||||
case 'categories':
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{addThemeTag.map((item, index) => (
|
||||
@ -168,7 +172,7 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
|
||||
placeholder="Select Theme Type"
|
||||
onChange={(value) => {
|
||||
console.log('主题标签更改', value);
|
||||
handleThemeTagChange(value);
|
||||
handleThemeTagChange(value as string[]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -5,7 +5,6 @@ export const mockScriptData: ScriptData = {
|
||||
{
|
||||
id: 'core',
|
||||
title: "SUMMARY",
|
||||
type: 'core',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
@ -14,52 +13,28 @@ export const mockScriptData: ScriptData = {
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'theme',
|
||||
id: 'categories',
|
||||
title: 'THEME',
|
||||
type: 'theme',
|
||||
content: [
|
||||
{
|
||||
type: 'tag',
|
||||
text: 'Satire'
|
||||
},
|
||||
{
|
||||
type: 'tag',
|
||||
text: 'Absurdist Comedy'
|
||||
},
|
||||
{
|
||||
type: 'tag',
|
||||
text: 'Disaster'
|
||||
text: 'Satire, Absurdist Comedy, Disaster'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'roles',
|
||||
title: 'ROLES',
|
||||
type: 'roles',
|
||||
content: [
|
||||
{
|
||||
type: 'bold',
|
||||
text: 'Anna'
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
text: 'Anna is a young woman who is trying to find her place in the world. She is a bit of a mess, but she is also a bit of a mess.'
|
||||
},
|
||||
{
|
||||
type: 'bold',
|
||||
text: 'Mark'
|
||||
},
|
||||
{
|
||||
type: 'card',
|
||||
text: 'Mark starts as resentful and aimless, harboring guilt over past family conflicts. His involvement in Anna’s quest rekindles his sense of responsibility and belonging. He moves from avoidance and cynicism to active support, ultimately risking his own safety for Anna and the creature. Mark’s arc is one of redemption and reconnection.'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'scene1',
|
||||
title: 'SCENE 1',
|
||||
type: 'scene',
|
||||
sceneNumber: 1,
|
||||
content: [
|
||||
{
|
||||
type: 'heading',
|
||||
@ -98,7 +73,6 @@ export const mockScriptData: ScriptData = {
|
||||
{
|
||||
id: 'summary',
|
||||
title: '总结',
|
||||
type: 'summary',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
|
||||
@ -2,20 +2,12 @@ export interface ScriptBlock {
|
||||
id: string;
|
||||
title: string;
|
||||
content: ScriptContent[];
|
||||
type: 'core' | 'scene' | 'summary' | 'theme' | 'roles';
|
||||
sceneNumber?: number;
|
||||
}
|
||||
|
||||
export interface ScriptContent {
|
||||
type: 'paragraph' | 'bold' | 'italic' | 'heading' | 'tag' | 'card';
|
||||
text?: string;
|
||||
roleInfo?: {
|
||||
name: string;
|
||||
gender: string;
|
||||
role: string;
|
||||
desc: string;
|
||||
color: string;
|
||||
};
|
||||
type: 'paragraph' | 'bold' | 'italic' | 'heading' | 'tag';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ScriptData {
|
||||
|
||||
@ -102,179 +102,7 @@ const ScriptTabContent: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<motion.div
|
||||
className="flex-none px-6 py-4"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
Edit Script
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<motion.div
|
||||
className="flex-1 overflow-auto p-6 pt-0 pb-0"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.1, duration: 0.2 }}
|
||||
>
|
||||
{/* 剧本片段渲染区域 */}
|
||||
<motion.div
|
||||
style={{
|
||||
height: 'calc(100% - 88px)'
|
||||
}}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
||||
<p className="text-red-600 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 剧本片段列表 */}
|
||||
<div className="space-y-3">
|
||||
{scriptSlices.map((slice, index) => (
|
||||
<motion.div
|
||||
key={slice.id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="relative"
|
||||
>
|
||||
<Input
|
||||
value={slice.text}
|
||||
onChange={(e) => handleScriptSliceChange(slice.id, e.target.value)}
|
||||
placeholder={`输入${slice.type}内容...`}
|
||||
className="w-full bg-white/50 dark:bg-[#5b75ac20] border border-gray-200 dark:border-gray-600 focus:ring-2 focus:ring-blue-500/20 transition-all duration-200"
|
||||
/>
|
||||
<div className="absolute top-2 right-2">
|
||||
<span className="text-xs px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400 rounded">
|
||||
{slice.type}
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 加载状态 */}
|
||||
{loading && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex items-center justify-center py-8"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
正在生成剧本...
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* 修改建议输入区域 */}
|
||||
<motion.div
|
||||
className="sticky bottom-0 bg-gradient-to-t from-white via-white to-transparent dark:from-[#5b75ac4d] dark:via-[#5b75ac4d] dark:to-transparent pt-8 pb-4 rounded-sm"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
<div className="flex items-center space-x-2 px-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
value={userPrompt}
|
||||
onChange={(e) => handlePromptChange(e.target.value)}
|
||||
placeholder="输入提示词,然后点击AI生成按钮..."
|
||||
className="outline-none box-shadow-none bg-white/50 dark:bg-[#5b75ac20] border-0 focus:ring-2 focus:ring-blue-500/20 transition-all duration-200"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{
|
||||
scale: userPrompt.trim() && !loading ? 1 : 0.8,
|
||||
opacity: userPrompt.trim() && !loading ? 1 : 0.5,
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 500,
|
||||
damping: 30
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={!userPrompt.trim() || loading}
|
||||
onClick={handleAiGenerate}
|
||||
className="aiGenerate relative w-9 h-9 rounded-full bg-blue-500/10 hover:bg-blue-500/20 text-blue-600 dark:text-blue-400"
|
||||
>
|
||||
<motion.span
|
||||
initial={false}
|
||||
animate={{
|
||||
opacity: loading ? 0 : 1,
|
||||
scale: loading ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5 transform rotate-90"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
|
||||
/>
|
||||
</svg>
|
||||
</motion.span>
|
||||
{loading && (
|
||||
<motion.div
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
initial={{ opacity: 0, scale: 0.5 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.5 }}
|
||||
>
|
||||
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
||||
</motion.div>
|
||||
)}
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<motion.div
|
||||
className="flex-none px-6 py-4 flex justify-end space-x-4"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="min-w-[80px] bg-white/10 text-white hover:bg-white/20 transition-colors"
|
||||
onClick={handleReset}
|
||||
disabled={loading}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
className="min-w-[80px] bg-blue-500 text-white hover:bg-blue-600 transition-colors"
|
||||
onClick={handleConfirm}
|
||||
disabled={loading || scriptSlices.length === 0}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user