This commit is contained in:
海龙 2025-08-13 18:25:52 +08:00
commit 5dfa836b2e
11 changed files with 247 additions and 177 deletions

View File

@ -602,14 +602,18 @@ export interface RoleRecognitionResponse {
characters_used: CharacterUsed[]; characters_used: CharacterUsed[];
} }
export interface RoleResponse { export interface RoleResponse {
/** 角色描述 */ characters: {
character_description: string; /** 角色描述 */
/** 角色名称 */ character_description: string;
character_name: string; /** 角色名称 */
/** 高亮关键词 */ character_name: string;
highlights: string[]; /** 高亮关键词 */
/** 角色图片地址 */ highlights: string[];
image_path: string; /** 角色图片地址 */
image_path: string;
/** 角色图片地址 */
image_url: string;
}[];
/**缓存 */ /**缓存 */
character_draft: string; character_draft: string;
} }

View File

@ -918,7 +918,7 @@ export const getCharacterListByProjectWithHighlight = async (request: {
project_id: string; project_id: string;
/** 每个角色最多提取的高亮关键词数量 */ /** 每个角色最多提取的高亮关键词数量 */
max_keywords?: number; max_keywords?: number;
}): Promise<ApiResponse<RoleResponse[]>> => { }): Promise<ApiResponse<RoleResponse>> => {
return post("/character/list_by_project_with_highlight", request); return post("/character/list_by_project_with_highlight", request);
}; };

View File

@ -296,11 +296,11 @@ export const useScriptService = (): UseScriptService => {
// 封装的setter函数同时更新hook状态和scriptEditUseCase中的值对象 // 封装的setter函数同时更新hook状态和scriptEditUseCase中的值对象
const setSynopsisWrapper = useCallback( const setSynopsisWrapper = useCallback(
(value: SetStateAction<string>) => { (value: SetStateAction<string>, needUpdate: boolean=true) => {
const newValue = typeof value === "function" ? value(synopsis) : value; const newValue = typeof value === "function" ? value(synopsis) : value;
console.log('setSynopsisWrapper', newValue); console.log('setSynopsisWrapper', newValue);
setSynopsis(newValue); setSynopsis(newValue);
if (scriptEditUseCase) { if (scriptEditUseCase && needUpdate) {
scriptEditUseCase.updateStoryField("synopsis", newValue); scriptEditUseCase.updateStoryField("synopsis", newValue);
} }
}, },
@ -308,10 +308,10 @@ export const useScriptService = (): UseScriptService => {
); );
const setCategoriesWrapper = useCallback( const setCategoriesWrapper = useCallback(
(value: SetStateAction<string[]>) => { (value: SetStateAction<string[]>, needUpdate: boolean=true) => {
const newValue = typeof value === "function" ? value(categories) : value; const newValue = typeof value === "function" ? value(categories) : value;
setCategories(newValue); setCategories(newValue);
if (scriptEditUseCase) { if (scriptEditUseCase && needUpdate) {
scriptEditUseCase.updateStoryField("categories", newValue); scriptEditUseCase.updateStoryField("categories", newValue);
} }
}, },
@ -319,10 +319,10 @@ export const useScriptService = (): UseScriptService => {
); );
const setProtagonistWrapper = useCallback( const setProtagonistWrapper = useCallback(
(value: SetStateAction<string>) => { (value: SetStateAction<string>, needUpdate: boolean=true) => {
const newValue = typeof value === "function" ? value(protagonist) : value; const newValue = typeof value === "function" ? value(protagonist) : value;
setProtagonist(newValue); setProtagonist(newValue);
if (scriptEditUseCase) { if (scriptEditUseCase && needUpdate) {
scriptEditUseCase.updateStoryField("protagonist", newValue); scriptEditUseCase.updateStoryField("protagonist", newValue);
} }
}, },
@ -330,11 +330,11 @@ export const useScriptService = (): UseScriptService => {
); );
const setIncitingIncidentWrapper = useCallback( const setIncitingIncidentWrapper = useCallback(
(value: SetStateAction<string>) => { (value: SetStateAction<string>, needUpdate: boolean=true) => {
const newValue = const newValue =
typeof value === "function" ? value(incitingIncident) : value; typeof value === "function" ? value(incitingIncident) : value;
setIncitingIncident(newValue); setIncitingIncident(newValue);
if (scriptEditUseCase) { if (scriptEditUseCase && needUpdate) {
scriptEditUseCase.updateStoryField("incitingIncident", newValue); scriptEditUseCase.updateStoryField("incitingIncident", newValue);
} }
}, },
@ -342,10 +342,10 @@ export const useScriptService = (): UseScriptService => {
); );
const setProblemWrapper = useCallback( const setProblemWrapper = useCallback(
(value: SetStateAction<string>) => { (value: SetStateAction<string>, needUpdate: boolean=true) => {
const newValue = typeof value === "function" ? value(problem) : value; const newValue = typeof value === "function" ? value(problem) : value;
setProblem(newValue); setProblem(newValue);
if (scriptEditUseCase) { if (scriptEditUseCase && needUpdate) {
scriptEditUseCase.updateStoryField("problem", newValue); scriptEditUseCase.updateStoryField("problem", newValue);
} }
}, },
@ -353,10 +353,10 @@ export const useScriptService = (): UseScriptService => {
); );
const setConflictWrapper = useCallback( const setConflictWrapper = useCallback(
(value: SetStateAction<string>) => { (value: SetStateAction<string>, needUpdate: boolean=true) => {
const newValue = typeof value === "function" ? value(conflict) : value; const newValue = typeof value === "function" ? value(conflict) : value;
setConflict(newValue); setConflict(newValue);
if (scriptEditUseCase) { if (scriptEditUseCase && needUpdate) {
scriptEditUseCase.updateStoryField("conflict", newValue); scriptEditUseCase.updateStoryField("conflict", newValue);
} }
}, },
@ -364,10 +364,10 @@ export const useScriptService = (): UseScriptService => {
); );
const setStakesWrapper = useCallback( const setStakesWrapper = useCallback(
(value: SetStateAction<string>) => { (value: SetStateAction<string>, needUpdate: boolean=true) => {
const newValue = typeof value === "function" ? value(stakes) : value; const newValue = typeof value === "function" ? value(stakes) : value;
setStakes(newValue); setStakes(newValue);
if (scriptEditUseCase) { if (scriptEditUseCase && needUpdate) {
scriptEditUseCase.updateStoryField("stakes", newValue); scriptEditUseCase.updateStoryField("stakes", newValue);
} }
}, },
@ -375,11 +375,11 @@ export const useScriptService = (): UseScriptService => {
); );
const setCharacterArcWrapper = useCallback( const setCharacterArcWrapper = useCallback(
(value: SetStateAction<string>) => { (value: SetStateAction<string>, needUpdate: boolean=true) => {
const newValue = const newValue =
typeof value === "function" ? value(characterArc) : value; typeof value === "function" ? value(characterArc) : value;
setCharacterArc(newValue); setCharacterArc(newValue);
if (scriptEditUseCase) { if (scriptEditUseCase && needUpdate) {
scriptEditUseCase.updateStoryField("characterArc", newValue); scriptEditUseCase.updateStoryField("characterArc", newValue);
} }
}, },
@ -387,40 +387,40 @@ export const useScriptService = (): UseScriptService => {
); );
const setAnyAttributeWrapper = useCallback( const setAnyAttributeWrapper = useCallback(
(type: string, value: string,callback: (old: string) => void=() => {}) => { (type: string, value: string,needUpdate: boolean=true,callback: (old: string) => void=() => {}) => {
console.log('setAnyAttributeWrapper', type); console.log('setAnyAttributeWrapper', type);
if (type === 'synopsis') { if (type === 'synopsis') {
setFieldOld(synopsis) setFieldOld(synopsis)
scriptEditUseCase.replaceScript(fieldOld,value ) scriptEditUseCase.replaceScript(fieldOld,value )
setSynopsisWrapper(value); setSynopsisWrapper(value, needUpdate);
} else if (type === 'categories') { } else if (type === 'categories') {
setFieldOld(categories.join(',')) setFieldOld(categories.join(','))
scriptEditUseCase.replaceScript(fieldOld,value) scriptEditUseCase.replaceScript(fieldOld,value)
setCategoriesWrapper(value.split(',') || []); setCategoriesWrapper(value.split(',') || [], needUpdate);
} else if (type === 'protagonist') { } else if (type === 'protagonist') {
setFieldOld(protagonist) setFieldOld(protagonist)
scriptEditUseCase.replaceScript(fieldOld,value) scriptEditUseCase.replaceScript(fieldOld,value)
setProtagonistWrapper(value); setProtagonistWrapper(value, needUpdate);
} else if (type === 'incitingIncident') { } else if (type === 'incitingIncident') {
setFieldOld(incitingIncident) setFieldOld(incitingIncident)
scriptEditUseCase.replaceScript(fieldOld,value) scriptEditUseCase.replaceScript(fieldOld,value)
setIncitingIncidentWrapper(value); setIncitingIncidentWrapper(value, needUpdate);
} else if (type === 'problem') { } else if (type === 'problem') {
setFieldOld(problem) setFieldOld(problem)
scriptEditUseCase.replaceScript(fieldOld,value) scriptEditUseCase.replaceScript(fieldOld,value)
setProblemWrapper(value); setProblemWrapper(value, needUpdate);
} else if (type === 'conflict') { } else if (type === 'conflict') {
setFieldOld(conflict) setFieldOld(conflict)
scriptEditUseCase.replaceScript(fieldOld,value) scriptEditUseCase.replaceScript(fieldOld,value)
setConflictWrapper(value); setConflictWrapper(value, needUpdate);
} else if (type === 'stakes') { } else if (type === 'stakes') {
setFieldOld(stakes) setFieldOld(stakes)
scriptEditUseCase.replaceScript(fieldOld,value) scriptEditUseCase.replaceScript(fieldOld,value)
setStakesWrapper(value); setStakesWrapper(value, needUpdate);
} else if (type === 'characterArc') { } else if (type === 'characterArc') {
setFieldOld(characterArc) setFieldOld(characterArc)
scriptEditUseCase.replaceScript(fieldOld,value) scriptEditUseCase.replaceScript(fieldOld,value)
setCharacterArcWrapper(value); setCharacterArcWrapper(value, needUpdate);
} }
callback(fieldOld) callback(fieldOld)
}, },

View File

@ -86,16 +86,16 @@ export class RoleEditUseCase {
* @returns {RoleEntity[]} * @returns {RoleEntity[]}
* @throws {Error} * @throws {Error}
*/ */
parseProjectRoleList(projectRoleData: RoleResponse[]): RoleEntity[] { parseProjectRoleList(projectRoleData: RoleResponse): RoleEntity[] {
if (!Array.isArray(projectRoleData)) { // if (!Array.isArray(projectRoleData)) {
throw new Error('项目角色数据格式错误'); // throw new Error('项目角色数据格式错误');
} // }
return projectRoleData.map((char, index) => { if(projectRoleData.character_draft){
if(char.character_draft){ const roleEntity: RoleEntity[] = JSON.parse(projectRoleData.character_draft);
const roleEntity: RoleEntity = JSON.parse(char.character_draft); return roleEntity;
return roleEntity; }
} return projectRoleData.characters.map((char, index) => {
/** 角色实体对象 */ /** 角色实体对象 */
const roleEntity: RoleEntity = { const roleEntity: RoleEntity = {
id: `role_${index + 1}`, id: `role_${index + 1}`,

View File

@ -147,7 +147,11 @@ export function MediaViewer({
}; };
// 包装编辑按钮点击事件 // 包装编辑按钮点击事件
const handleEditClick = (tab: string) => { const handleEditClick = (tab: string, from?: string) => {
if (from === 'final') {
// 暂停视频播放
finalVideoRef.current?.pause();
}
// TODO 点击没有任何事件效果,页面没变化 // TODO 点击没有任何事件效果,页面没变化
setUserHasInteracted(true); setUserHasInteracted(true);
onEditModalOpen(tab); onEditModalOpen(tab);
@ -348,7 +352,7 @@ export function MediaViewer({
<GlassIconButton <GlassIconButton
icon={Edit3} icon={Edit3}
tooltip="Edit sketch" tooltip="Edit sketch"
onClick={() => handleEditClick('4')} onClick={() => handleEditClick('4', 'final')}
/> />
</motion.div> </motion.div>
)} )}

View File

@ -80,7 +80,7 @@ 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 [isPauseWorkFlow, setIsPauseWorkFlow] = useState(false); const [isPauseWorkFlow, setIsPauseWorkFlow] = useState(false);
const [mode, setMode] = useState<'automatic' | 'manual'>('automatic'); const [mode, setMode] = useState<'automatic' | 'manual' | 'auto'>('automatic');
const taskData: any = { const taskData: any = {
sketch: { data: [], total_count: -1 }, sketch: { data: [], total_count: -1 },
@ -102,13 +102,15 @@ export function useWorkflowData() {
} = useScriptService(); } = useScriptService();
// 初始化剧本 // 初始化剧本
useEffect(() => { useEffect(() => {
console.log('开始初始化剧本', originalText,episodeId); if (currentStep !== '0') {
// TODO 为什么一开始没项目id console.log('开始初始化剧本', originalText,episodeId);
originalText && initializeFromProject(episodeId, originalText).then(() => { // TODO 为什么一开始没项目id
console.log('应用剧本'); originalText && initializeFromProject(episodeId, originalText).then(() => {
// 自动模式下 应用剧本;手动模式 需要点击 下一步 触发 console.log('应用剧本');
mode.includes('auto') && applyScript(); // 自动模式下 应用剧本;手动模式 需要点击 下一步 触发
}); mode.includes('auto') && applyScript();
});
}
}, [originalText]); }, [originalText]);
// 监听剧本加载完毕 // 监听剧本加载完毕
useEffect(() => { useEffect(() => {
@ -176,7 +178,7 @@ export function useWorkflowData() {
// 替换原有的 setSketchCount 和 setVideoCount 调用 // 替换原有的 setSketchCount 和 setVideoCount 调用
useEffect(() => { useEffect(() => {
console.log('sketchCount 已更新:', sketchCount); console.log('sketchCount 已更新:', sketchCount);
setCurrentSketchIndex(sketchCount - 1); currentStep !== '3' && setCurrentSketchIndex(sketchCount - 1);
}, [sketchCount]); }, [sketchCount]);
useEffect(() => { useEffect(() => {
@ -446,6 +448,7 @@ export function useWorkflowData() {
if (status === 'COMPLETED') { if (status === 'COMPLETED') {
loadingText = LOADING_TEXT_MAP.complete; loadingText = LOADING_TEXT_MAP.complete;
taskData.status = '6';
} }
// 如果有已完成的数据,同步到状态 // 如果有已完成的数据,同步到状态

View File

@ -15,9 +15,10 @@ interface ScriptRendererProps {
applyScript: any; applyScript: any;
mode: string; mode: string;
from: string; from: string;
setIsUpdate?: (isUpdate: boolean) => void;
} }
export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPauseWorkFlow, setAnyAttribute, isPauseWorkFlow, applyScript, mode, from }) => { export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPauseWorkFlow, setAnyAttribute, isPauseWorkFlow, applyScript, mode, from, setIsUpdate }) => {
const [activeBlockId, setActiveBlockId] = useState<string | null>(null); const [activeBlockId, setActiveBlockId] = useState<string | null>(null);
const [hoveredBlockId, setHoveredBlockId] = useState<string | null>(null); const [hoveredBlockId, setHoveredBlockId] = useState<string | null>(null);
const contentRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); const contentRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
@ -114,14 +115,17 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
if (contentEditableRef.current) { if (contentEditableRef.current) {
const text = contentEditableRef.current.innerText; const text = contentEditableRef.current.innerText;
console.log('contentEditableRef---text', text); console.log('contentEditableRef---text', text);
if (from !== 'tab') { console.log('contentEditableRef---block', block.id, block);
setAnyAttribute(block.id, text,(old: string)=>{ if (block.content[0].text !== text) {
if(old!==text){ setIsUpdate(true);
mode.includes('auto') && applyScript();
setIsPauseWorkFlow(false);
}
});
} }
setAnyAttribute(block.id, text,from !== 'tab',(old: string)=>{
if(old!==text){
console.log('contentEditableRef---change?')
mode.includes('auto') && applyScript();
setIsPauseWorkFlow(false);
}
});
} }
}; };
@ -136,15 +140,14 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
return; return;
} }
setAddThemeTag(value); setAddThemeTag(value);
if (from !== 'tab') { setIsUpdate(true);
setIsPauseWorkFlow(true); from !== 'tab' && setIsPauseWorkFlow(true);
setAnyAttribute('categories', value.join(','),(old: string)=>{ setAnyAttribute('categories', value.join(','),from !== 'tab',(old: string)=>{
if(old!==value.join(',')){ if(old!==value.join(',')){
mode.includes('auto') && applyScript(); mode.includes('auto') && applyScript();
setIsPauseWorkFlow(false); setIsPauseWorkFlow(false);
} }
}); });
}
}; };
const handleEditBlock = (block: ScriptBlock) => { const handleEditBlock = (block: ScriptBlock) => {

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useState, useEffect, SetStateAction } from 'react'; import React, { useState, useEffect, SetStateAction, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { X, Image, Users, Video, Music, Settings, FileText, Undo2, TriangleAlert } from 'lucide-react'; import { X, Image, Users, Video, Music, Settings, FileText, Undo2, TriangleAlert } from 'lucide-react';
import { cn } from '@/public/lib/utils'; import { cn } from '@/public/lib/utils';
@ -63,6 +63,10 @@ export function EditModal({
const [isRemindFallbackOpen, setIsRemindFallbackOpen] = useState(false); const [isRemindFallbackOpen, setIsRemindFallbackOpen] = useState(false);
const [isRemindResetOpen, setIsRemindResetOpen] = useState(false); const [isRemindResetOpen, setIsRemindResetOpen] = useState(false);
const [resetKey, setResetKey] = useState(0); const [resetKey, setResetKey] = useState(0);
const [remindFallbackText, setRemindFallbackText] = useState('The task will be regenerated and edited. Do you want to continue?');
const scriptTabContentRef = useRef<any>(null);
// 添加一个状态来标记是否是从切换tab触发的提醒
const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
setCurrentIndex(currentSketchIndex); setCurrentIndex(currentSketchIndex);
@ -91,8 +95,27 @@ export function EditModal({
} }
} }
const checkUpdate = (tabId: string) => {
if (tabId === '0') {
const scriptTabContent = scriptTabContentRef.current;
if (scriptTabContent) {
return scriptTabContent.checkUpdate();
}
}
return false;
}
const handleChangeTab = (tabId: string, disabled: boolean) => { const handleChangeTab = (tabId: string, disabled: boolean) => {
if (disabled) return; if (disabled) return;
// 切换前 检查是否更新
const isUpdate = checkUpdate(activeTab);
console.log('contentEditableRef---isUpdate', isUpdate);
if (isUpdate) {
setPendingSwitchTabId(tabId); // 记录要切换到的目标tab
setRemindFallbackText('You must click Apply button to save the current changes.');
setIsRemindFallbackOpen(true);
return;
}
setActiveTab(tabId); setActiveTab(tabId);
setCurrentIndex(0); setCurrentIndex(0);
} }
@ -112,6 +135,12 @@ export function EditModal({
} }
} }
const handleCloseRemindFallbackPanel = () => { const handleCloseRemindFallbackPanel = () => {
if (pendingSwitchTabId) {
// 如果是从切换tab触发的提醒关闭后执行tab切换
setActiveTab(pendingSwitchTabId);
setCurrentIndex(0);
setPendingSwitchTabId(null); // 清除记录
}
setIsRemindFallbackOpen(false); setIsRemindFallbackOpen(false);
} }
@ -132,6 +161,7 @@ export function EditModal({
case '0': case '0':
return ( return (
<ScriptTabContent <ScriptTabContent
ref={scriptTabContentRef}
setIsPauseWorkFlow={setIsPauseWorkFlow} setIsPauseWorkFlow={setIsPauseWorkFlow}
isPauseWorkFlow={isPauseWorkFlow} isPauseWorkFlow={isPauseWorkFlow}
originalText={originalText} originalText={originalText}
@ -311,7 +341,7 @@ export function EditModal({
<div className="flex flex-col items-center gap-4 text-white py-4"> <div className="flex flex-col items-center gap-4 text-white py-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<TriangleAlert className="w-6 h-6 text-yellow-400" /> <TriangleAlert className="w-6 h-6 text-yellow-400" />
<p className="text-lg font-medium">The task will be regenerated and edited. Do you want to continue?</p> <p className="text-lg font-medium">{remindFallbackText}</p>
</div> </div>
<div className="flex gap-3 mt-2"> <div className="flex gap-3 mt-2">
@ -321,7 +351,7 @@ export function EditModal({
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors duration-200 flex items-center gap-2" className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors duration-200 flex items-center gap-2"
> >
<Undo2 className="w-4 h-4" /> <Undo2 className="w-4 h-4" />
Continue Apply
</button> </button>
<button <button

View File

@ -17,13 +17,25 @@ interface HighlightTextOptions {
} }
export function HighlightText(props: ReactNodeViewProps) { export function HighlightText(props: ReactNodeViewProps) {
const { text, color } = props.node.attrs as HighlightTextAttributes const { text: initialText, color } = props.node.attrs as HighlightTextAttributes
const [text, setText] = useState(initialText)
const handleInput = (e: React.FormEvent<HTMLSpanElement>) => {
const newText = e.currentTarget.textContent || ''
setText(newText)
// 通知Tiptap更新内容
props.updateAttributes({
text: newText
})
}
return ( return (
<NodeViewWrapper <NodeViewWrapper
as="span" as="span"
data-alt="highlight-text" data-alt="highlight-text"
contentEditable={true} contentEditable={true}
suppressContentEditableWarning={true}
onInput={handleInput}
className={`relative inline text-${color}-400 hover:text-${color}-300 transition-colors duration-200`} className={`relative inline text-${color}-400 hover:text-${color}-300 transition-colors duration-200`}
> >
{text} {text}

View File

@ -1,4 +1,4 @@
import React, { useState, useCallback, useEffect, SetStateAction } from 'react'; import React, { useState, useCallback, useEffect, SetStateAction, forwardRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { FileText } from 'lucide-react'; import { FileText } from 'lucide-react';
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer'; import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
@ -11,13 +11,24 @@ interface ScriptTabContentProps {
originalText?: string; originalText?: string;
} }
export function ScriptTabContent({ export const ScriptTabContent = forwardRef<
setIsPauseWorkFlow, { checkUpdate: () => boolean },
isPauseWorkFlow, ScriptTabContentProps
originalText, >((props, ref) => {
}: ScriptTabContentProps) { const { setIsPauseWorkFlow, isPauseWorkFlow, originalText } = props;
const { loading, scriptData, setAnyAttribute, applyScript } = useEditData('script', originalText); const { loading, scriptData, setAnyAttribute, applyScript } = useEditData('script', originalText);
const [isUpdate, setIsUpdate] = useState(false);
useEffect(() => {
console.log('contentEditableRef---scriptTabContentIsChange', isUpdate);
}, [isUpdate]);
// 暴露方法给父组件
React.useImperativeHandle(ref, () => ({
checkUpdate: () => isUpdate
}));
// 如果loading 显示loading状态 // 如果loading 显示loading状态
if (loading) { if (loading) {
return ( return (
@ -52,8 +63,9 @@ export function ScriptTabContent({
applyScript={applyScript} applyScript={applyScript}
mode='manual' mode='manual'
from='tab' from='tab'
setIsUpdate={setIsUpdate}
/> />
</motion.div> </motion.div>
</div> </div>
); );
}; });

View File

@ -293,105 +293,107 @@ export function ShotTabContent({
{/* 下部分 */} {/* 下部分 */}
<motion.div {shotData[selectedIndex] && (
className="grid grid-cols-2 gap-4 w-full" <motion.div
initial={{ opacity: 0, y: 20 }} className="grid grid-cols-2 gap-4 w-full"
animate={{ opacity: 1, y: 0 }} initial={{ opacity: 0, y: 20 }}
transition={{ delay: 0.2 }} animate={{ opacity: 1, y: 0 }}
> transition={{ delay: 0.2 }}
{/* 视频预览和操作 */} >
<div className="space-y-4 col-span-1"> {/* 视频预览和操作 */}
{/* 选中的视频预览 */} <div className="space-y-4 col-span-1">
<> {/* 选中的视频预览 */}
{shotData[selectedIndex]?.status === 0 && ( <>
<div className="w-full h-full flex items-center gap-1 justify-center rounded-lg bg-black/30"> {shotData[selectedIndex]?.status === 0 && (
<Loader2 className="w-4 h-4 animate-spin text-blue-500" /> <div className="w-full h-full flex items-center gap-1 justify-center rounded-lg bg-black/30">
<span className="text-white/50">Loading...</span> <Loader2 className="w-4 h-4 animate-spin text-blue-500" />
</div> <span className="text-white/50">Loading...</span>
)} </div>
{shotData[selectedIndex]?.status === 1 && ( )}
<motion.div {shotData[selectedIndex]?.status === 1 && (
className="aspect-video rounded-lg overflow-hidden relative group" <motion.div
layoutId={`video-preview-${selectedIndex}`} className="aspect-video rounded-lg overflow-hidden relative group"
> layoutId={`video-preview-${selectedIndex}`}
<PersonDetectionScene >
videoSrc={shotData[selectedIndex]?.videoUrl[0]} <PersonDetectionScene
detections={detections} videoSrc={shotData[selectedIndex]?.videoUrl[0]}
triggerScan={scanState === 'scanning'} detections={detections}
triggerSuccess={scanState === 'detected'} triggerScan={scanState === 'scanning'}
onScanTimeout={handleScanTimeout} triggerSuccess={scanState === 'detected'}
onScanExit={handleScanExit} onScanTimeout={handleScanTimeout}
onDetectionsChange={handleDetectionsChange} onScanExit={handleScanExit}
onPersonClick={handlePersonClick} onDetectionsChange={handleDetectionsChange}
/> onPersonClick={handlePersonClick}
<motion.div className='absolute top-4 right-4 flex gap-2'> />
{/* 人物替换按钮 */} <motion.div className='absolute top-4 right-4 flex gap-2'>
<motion.button {/* 人物替换按钮 */}
onClick={() => handleScan()} <motion.button
className={`p-2 backdrop-blur-sm transition-colors z-10 rounded-full onClick={() => handleScan()}
${scanState === 'detected' className={`p-2 backdrop-blur-sm transition-colors z-10 rounded-full
? 'bg-cyan-500/50 hover:bg-cyan-500/70 text-white' ${scanState === 'detected'
: 'bg-black/50 hover:bg-black/70 text-white' ? 'bg-cyan-500/50 hover:bg-cyan-500/70 text-white'
}`} : 'bg-black/50 hover:bg-black/70 text-white'
whileHover={{ scale: 1.05 }} }`}
whileTap={{ scale: 0.95 }} whileHover={{ scale: 1.05 }}
> whileTap={{ scale: 0.95 }}
{scanState === 'scanning' ? ( >
<Loader2 className="w-4 h-4 animate-spin" /> {scanState === 'scanning' ? (
) : scanState === 'detected' ? ( <Loader2 className="w-4 h-4 animate-spin" />
<X className="w-4 h-4" /> ) : scanState === 'detected' ? (
) : ( <X className="w-4 h-4" />
<User className="w-4 h-4" /> ) : (
)} <User className="w-4 h-4" />
</motion.button> )}
</motion.button>
</motion.div>
</motion.div> </motion.div>
</motion.div> )}
)} {shotData[selectedIndex]?.status === 2 && (
{shotData[selectedIndex]?.status === 2 && ( <div className="w-full h-full flex gap-1 items-center justify-center rounded-lg bg-red-500/10">
<div className="w-full h-full flex gap-1 items-center justify-center rounded-lg bg-red-500/10"> <CircleX className="w-4 h-4 text-red-500" />
<CircleX className="w-4 h-4 text-red-500" /> <span className="text-white/50"></span>
<span className="text-white/50"></span> </div>
</div> )}
)} </>
</> </div>
</div>
{/* 基础配置 */}
{/* 基础配置 */} <div className='space-y-4 col-span-1' key={selectedIndex}>
<div className='space-y-4 col-span-1' key={selectedIndex}> <ShotsEditor
<ShotsEditor ref={shotsEditorRef}
ref={shotsEditorRef} roles={roles}
roles={roles} shotInfo={shotData[selectedIndex].lens}
shotInfo={shotData[selectedIndex].lens} style={{height: 'calc(100% - 4rem)'}}
style={{height: 'calc(100% - 4rem)'}} />
/>
{/* 重新生成按钮、新增分镜按钮 */}
{/* 重新生成按钮、新增分镜按钮 */} <div className="grid grid-cols-2 gap-2">
<div className="grid grid-cols-2 gap-2"> <motion.button
<motion.button onClick={() => handleAddShot()}
onClick={() => handleAddShot()} className="flex items-center justify-center gap-2 px-4 py-3 bg-pink-500/10 hover:bg-pink-500/20
className="flex items-center justify-center gap-2 px-4 py-3 bg-pink-500/10 hover:bg-pink-500/20 text-pink-500 rounded-lg transition-colors"
text-pink-500 rounded-lg transition-colors" whileHover={{ scale: 1.02 }}
whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}
whileTap={{ scale: 0.98 }} >
> <Plus className="w-4 h-4" />
<Plus className="w-4 h-4" /> <span>Add Shot</span>
<span>Add Shot</span> </motion.button>
</motion.button> <motion.button
<motion.button onClick={() => handleRegenerate()}
onClick={() => handleRegenerate()} className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20 text-blue-500 rounded-lg transition-colors"
text-blue-500 rounded-lg transition-colors" whileHover={{ scale: 1.02 }}
whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}
whileTap={{ scale: 0.98 }} >
> <RefreshCw className="w-4 h-4" />
<RefreshCw className="w-4 h-4" /> <span>Regenerate</span>
<span>Regenerate</span> </motion.button>
</motion.button> </div>
</div> </div>
</div>
</motion.div> </motion.div>
)}
<FloatingGlassPanel <FloatingGlassPanel
open={isReplacePanelOpen} open={isReplacePanelOpen}