forked from 77media/video-flow
Merge branch 'dev' of https://git.qikongjian.com/77media/video-flow into dev
This commit is contained in:
commit
5dfa836b2e
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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}`,
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有已完成的数据,同步到状态
|
// 如果有已完成的数据,同步到状态
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user