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[];
|
||||
}
|
||||
export interface RoleResponse {
|
||||
/** 角色描述 */
|
||||
character_description: string;
|
||||
/** 角色名称 */
|
||||
character_name: string;
|
||||
/** 高亮关键词 */
|
||||
highlights: string[];
|
||||
/** 角色图片地址 */
|
||||
image_path: string;
|
||||
characters: {
|
||||
/** 角色描述 */
|
||||
character_description: string;
|
||||
/** 角色名称 */
|
||||
character_name: string;
|
||||
/** 高亮关键词 */
|
||||
highlights: string[];
|
||||
/** 角色图片地址 */
|
||||
image_path: string;
|
||||
/** 角色图片地址 */
|
||||
image_url: string;
|
||||
}[];
|
||||
/**缓存 */
|
||||
character_draft: string;
|
||||
}
|
||||
|
||||
@ -918,7 +918,7 @@ export const getCharacterListByProjectWithHighlight = async (request: {
|
||||
project_id: string;
|
||||
/** 每个角色最多提取的高亮关键词数量 */
|
||||
max_keywords?: number;
|
||||
}): Promise<ApiResponse<RoleResponse[]>> => {
|
||||
}): Promise<ApiResponse<RoleResponse>> => {
|
||||
return post("/character/list_by_project_with_highlight", request);
|
||||
};
|
||||
|
||||
|
||||
@ -296,11 +296,11 @@ export const useScriptService = (): UseScriptService => {
|
||||
|
||||
// 封装的setter函数,同时更新hook状态和scriptEditUseCase中的值对象
|
||||
const setSynopsisWrapper = useCallback(
|
||||
(value: SetStateAction<string>) => {
|
||||
(value: SetStateAction<string>, needUpdate: boolean=true) => {
|
||||
const newValue = typeof value === "function" ? value(synopsis) : value;
|
||||
console.log('setSynopsisWrapper', newValue);
|
||||
setSynopsis(newValue);
|
||||
if (scriptEditUseCase) {
|
||||
if (scriptEditUseCase && needUpdate) {
|
||||
scriptEditUseCase.updateStoryField("synopsis", newValue);
|
||||
}
|
||||
},
|
||||
@ -308,10 +308,10 @@ export const useScriptService = (): UseScriptService => {
|
||||
);
|
||||
|
||||
const setCategoriesWrapper = useCallback(
|
||||
(value: SetStateAction<string[]>) => {
|
||||
(value: SetStateAction<string[]>, needUpdate: boolean=true) => {
|
||||
const newValue = typeof value === "function" ? value(categories) : value;
|
||||
setCategories(newValue);
|
||||
if (scriptEditUseCase) {
|
||||
if (scriptEditUseCase && needUpdate) {
|
||||
scriptEditUseCase.updateStoryField("categories", newValue);
|
||||
}
|
||||
},
|
||||
@ -319,10 +319,10 @@ export const useScriptService = (): UseScriptService => {
|
||||
);
|
||||
|
||||
const setProtagonistWrapper = useCallback(
|
||||
(value: SetStateAction<string>) => {
|
||||
(value: SetStateAction<string>, needUpdate: boolean=true) => {
|
||||
const newValue = typeof value === "function" ? value(protagonist) : value;
|
||||
setProtagonist(newValue);
|
||||
if (scriptEditUseCase) {
|
||||
if (scriptEditUseCase && needUpdate) {
|
||||
scriptEditUseCase.updateStoryField("protagonist", newValue);
|
||||
}
|
||||
},
|
||||
@ -330,11 +330,11 @@ export const useScriptService = (): UseScriptService => {
|
||||
);
|
||||
|
||||
const setIncitingIncidentWrapper = useCallback(
|
||||
(value: SetStateAction<string>) => {
|
||||
(value: SetStateAction<string>, needUpdate: boolean=true) => {
|
||||
const newValue =
|
||||
typeof value === "function" ? value(incitingIncident) : value;
|
||||
setIncitingIncident(newValue);
|
||||
if (scriptEditUseCase) {
|
||||
if (scriptEditUseCase && needUpdate) {
|
||||
scriptEditUseCase.updateStoryField("incitingIncident", newValue);
|
||||
}
|
||||
},
|
||||
@ -342,10 +342,10 @@ export const useScriptService = (): UseScriptService => {
|
||||
);
|
||||
|
||||
const setProblemWrapper = useCallback(
|
||||
(value: SetStateAction<string>) => {
|
||||
(value: SetStateAction<string>, needUpdate: boolean=true) => {
|
||||
const newValue = typeof value === "function" ? value(problem) : value;
|
||||
setProblem(newValue);
|
||||
if (scriptEditUseCase) {
|
||||
if (scriptEditUseCase && needUpdate) {
|
||||
scriptEditUseCase.updateStoryField("problem", newValue);
|
||||
}
|
||||
},
|
||||
@ -353,10 +353,10 @@ export const useScriptService = (): UseScriptService => {
|
||||
);
|
||||
|
||||
const setConflictWrapper = useCallback(
|
||||
(value: SetStateAction<string>) => {
|
||||
(value: SetStateAction<string>, needUpdate: boolean=true) => {
|
||||
const newValue = typeof value === "function" ? value(conflict) : value;
|
||||
setConflict(newValue);
|
||||
if (scriptEditUseCase) {
|
||||
if (scriptEditUseCase && needUpdate) {
|
||||
scriptEditUseCase.updateStoryField("conflict", newValue);
|
||||
}
|
||||
},
|
||||
@ -364,10 +364,10 @@ export const useScriptService = (): UseScriptService => {
|
||||
);
|
||||
|
||||
const setStakesWrapper = useCallback(
|
||||
(value: SetStateAction<string>) => {
|
||||
(value: SetStateAction<string>, needUpdate: boolean=true) => {
|
||||
const newValue = typeof value === "function" ? value(stakes) : value;
|
||||
setStakes(newValue);
|
||||
if (scriptEditUseCase) {
|
||||
if (scriptEditUseCase && needUpdate) {
|
||||
scriptEditUseCase.updateStoryField("stakes", newValue);
|
||||
}
|
||||
},
|
||||
@ -375,11 +375,11 @@ export const useScriptService = (): UseScriptService => {
|
||||
);
|
||||
|
||||
const setCharacterArcWrapper = useCallback(
|
||||
(value: SetStateAction<string>) => {
|
||||
(value: SetStateAction<string>, needUpdate: boolean=true) => {
|
||||
const newValue =
|
||||
typeof value === "function" ? value(characterArc) : value;
|
||||
setCharacterArc(newValue);
|
||||
if (scriptEditUseCase) {
|
||||
if (scriptEditUseCase && needUpdate) {
|
||||
scriptEditUseCase.updateStoryField("characterArc", newValue);
|
||||
}
|
||||
},
|
||||
@ -387,40 +387,40 @@ export const useScriptService = (): UseScriptService => {
|
||||
);
|
||||
|
||||
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);
|
||||
if (type === 'synopsis') {
|
||||
setFieldOld(synopsis)
|
||||
scriptEditUseCase.replaceScript(fieldOld,value )
|
||||
setSynopsisWrapper(value);
|
||||
setSynopsisWrapper(value, needUpdate);
|
||||
} else if (type === 'categories') {
|
||||
setFieldOld(categories.join(','))
|
||||
scriptEditUseCase.replaceScript(fieldOld,value)
|
||||
setCategoriesWrapper(value.split(',') || []);
|
||||
setCategoriesWrapper(value.split(',') || [], needUpdate);
|
||||
} else if (type === 'protagonist') {
|
||||
setFieldOld(protagonist)
|
||||
scriptEditUseCase.replaceScript(fieldOld,value)
|
||||
setProtagonistWrapper(value);
|
||||
setProtagonistWrapper(value, needUpdate);
|
||||
} else if (type === 'incitingIncident') {
|
||||
setFieldOld(incitingIncident)
|
||||
scriptEditUseCase.replaceScript(fieldOld,value)
|
||||
setIncitingIncidentWrapper(value);
|
||||
setIncitingIncidentWrapper(value, needUpdate);
|
||||
} else if (type === 'problem') {
|
||||
setFieldOld(problem)
|
||||
scriptEditUseCase.replaceScript(fieldOld,value)
|
||||
setProblemWrapper(value);
|
||||
setProblemWrapper(value, needUpdate);
|
||||
} else if (type === 'conflict') {
|
||||
setFieldOld(conflict)
|
||||
scriptEditUseCase.replaceScript(fieldOld,value)
|
||||
setConflictWrapper(value);
|
||||
setConflictWrapper(value, needUpdate);
|
||||
} else if (type === 'stakes') {
|
||||
setFieldOld(stakes)
|
||||
scriptEditUseCase.replaceScript(fieldOld,value)
|
||||
setStakesWrapper(value);
|
||||
setStakesWrapper(value, needUpdate);
|
||||
} else if (type === 'characterArc') {
|
||||
setFieldOld(characterArc)
|
||||
scriptEditUseCase.replaceScript(fieldOld,value)
|
||||
setCharacterArcWrapper(value);
|
||||
setCharacterArcWrapper(value, needUpdate);
|
||||
}
|
||||
callback(fieldOld)
|
||||
},
|
||||
|
||||
@ -86,16 +86,16 @@ export class RoleEditUseCase {
|
||||
* @returns {RoleEntity[]} 角色实体数组
|
||||
* @throws {Error} 如果数据格式不正确则抛出异常
|
||||
*/
|
||||
parseProjectRoleList(projectRoleData: RoleResponse[]): RoleEntity[] {
|
||||
if (!Array.isArray(projectRoleData)) {
|
||||
throw new Error('项目角色数据格式错误');
|
||||
}
|
||||
parseProjectRoleList(projectRoleData: RoleResponse): RoleEntity[] {
|
||||
// if (!Array.isArray(projectRoleData)) {
|
||||
// throw new Error('项目角色数据格式错误');
|
||||
// }
|
||||
|
||||
return projectRoleData.map((char, index) => {
|
||||
if(char.character_draft){
|
||||
const roleEntity: RoleEntity = JSON.parse(char.character_draft);
|
||||
return roleEntity;
|
||||
}
|
||||
if(projectRoleData.character_draft){
|
||||
const roleEntity: RoleEntity[] = JSON.parse(projectRoleData.character_draft);
|
||||
return roleEntity;
|
||||
}
|
||||
return projectRoleData.characters.map((char, index) => {
|
||||
/** 角色实体对象 */
|
||||
const roleEntity: RoleEntity = {
|
||||
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 点击没有任何事件效果,页面没变化
|
||||
setUserHasInteracted(true);
|
||||
onEditModalOpen(tab);
|
||||
@ -348,7 +352,7 @@ export function MediaViewer({
|
||||
<GlassIconButton
|
||||
icon={Edit3}
|
||||
tooltip="Edit sketch"
|
||||
onClick={() => handleEditClick('4')}
|
||||
onClick={() => handleEditClick('4', 'final')}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@ -80,7 +80,7 @@ export function useWorkflowData() {
|
||||
const [dataLoadError, setDataLoadError] = useState<string | null>(null);
|
||||
const [needStreamData, setNeedStreamData] = 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 = {
|
||||
sketch: { data: [], total_count: -1 },
|
||||
@ -102,13 +102,15 @@ export function useWorkflowData() {
|
||||
} = useScriptService();
|
||||
// 初始化剧本
|
||||
useEffect(() => {
|
||||
console.log('开始初始化剧本', originalText,episodeId);
|
||||
// TODO 为什么一开始没项目id
|
||||
originalText && initializeFromProject(episodeId, originalText).then(() => {
|
||||
console.log('应用剧本');
|
||||
// 自动模式下 应用剧本;手动模式 需要点击 下一步 触发
|
||||
mode.includes('auto') && applyScript();
|
||||
});
|
||||
if (currentStep !== '0') {
|
||||
console.log('开始初始化剧本', originalText,episodeId);
|
||||
// TODO 为什么一开始没项目id
|
||||
originalText && initializeFromProject(episodeId, originalText).then(() => {
|
||||
console.log('应用剧本');
|
||||
// 自动模式下 应用剧本;手动模式 需要点击 下一步 触发
|
||||
mode.includes('auto') && applyScript();
|
||||
});
|
||||
}
|
||||
}, [originalText]);
|
||||
// 监听剧本加载完毕
|
||||
useEffect(() => {
|
||||
@ -176,7 +178,7 @@ export function useWorkflowData() {
|
||||
// 替换原有的 setSketchCount 和 setVideoCount 调用
|
||||
useEffect(() => {
|
||||
console.log('sketchCount 已更新:', sketchCount);
|
||||
setCurrentSketchIndex(sketchCount - 1);
|
||||
currentStep !== '3' && setCurrentSketchIndex(sketchCount - 1);
|
||||
}, [sketchCount]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -446,6 +448,7 @@ export function useWorkflowData() {
|
||||
|
||||
if (status === 'COMPLETED') {
|
||||
loadingText = LOADING_TEXT_MAP.complete;
|
||||
taskData.status = '6';
|
||||
}
|
||||
|
||||
// 如果有已完成的数据,同步到状态
|
||||
|
||||
@ -15,9 +15,10 @@ interface ScriptRendererProps {
|
||||
applyScript: any;
|
||||
mode: 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 [hoveredBlockId, setHoveredBlockId] = useState<string | null>(null);
|
||||
const contentRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
|
||||
@ -114,14 +115,17 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
|
||||
if (contentEditableRef.current) {
|
||||
const text = contentEditableRef.current.innerText;
|
||||
console.log('contentEditableRef---text', text);
|
||||
if (from !== 'tab') {
|
||||
setAnyAttribute(block.id, text,(old: string)=>{
|
||||
if(old!==text){
|
||||
mode.includes('auto') && applyScript();
|
||||
setIsPauseWorkFlow(false);
|
||||
}
|
||||
});
|
||||
console.log('contentEditableRef---block', block.id, block);
|
||||
if (block.content[0].text !== text) {
|
||||
setIsUpdate(true);
|
||||
}
|
||||
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;
|
||||
}
|
||||
setAddThemeTag(value);
|
||||
if (from !== 'tab') {
|
||||
setIsPauseWorkFlow(true);
|
||||
setAnyAttribute('categories', value.join(','),(old: string)=>{
|
||||
if(old!==value.join(',')){
|
||||
mode.includes('auto') && applyScript();
|
||||
setIsPauseWorkFlow(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
setIsUpdate(true);
|
||||
from !== 'tab' && setIsPauseWorkFlow(true);
|
||||
setAnyAttribute('categories', value.join(','),from !== 'tab',(old: string)=>{
|
||||
if(old!==value.join(',')){
|
||||
mode.includes('auto') && applyScript();
|
||||
setIsPauseWorkFlow(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditBlock = (block: ScriptBlock) => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, SetStateAction } from 'react';
|
||||
import React, { useState, useEffect, SetStateAction, useRef } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { X, Image, Users, Video, Music, Settings, FileText, Undo2, TriangleAlert } from 'lucide-react';
|
||||
import { cn } from '@/public/lib/utils';
|
||||
@ -63,6 +63,10 @@ export function EditModal({
|
||||
const [isRemindFallbackOpen, setIsRemindFallbackOpen] = useState(false);
|
||||
const [isRemindResetOpen, setIsRemindResetOpen] = useState(false);
|
||||
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(() => {
|
||||
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) => {
|
||||
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);
|
||||
setCurrentIndex(0);
|
||||
}
|
||||
@ -112,6 +135,12 @@ export function EditModal({
|
||||
}
|
||||
}
|
||||
const handleCloseRemindFallbackPanel = () => {
|
||||
if (pendingSwitchTabId) {
|
||||
// 如果是从切换tab触发的提醒,关闭后执行tab切换
|
||||
setActiveTab(pendingSwitchTabId);
|
||||
setCurrentIndex(0);
|
||||
setPendingSwitchTabId(null); // 清除记录
|
||||
}
|
||||
setIsRemindFallbackOpen(false);
|
||||
}
|
||||
|
||||
@ -132,6 +161,7 @@ export function EditModal({
|
||||
case '0':
|
||||
return (
|
||||
<ScriptTabContent
|
||||
ref={scriptTabContentRef}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
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 items-center gap-3">
|
||||
<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 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"
|
||||
>
|
||||
<Undo2 className="w-4 h-4" />
|
||||
Continue
|
||||
Apply
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
||||
@ -17,13 +17,25 @@ interface HighlightTextOptions {
|
||||
}
|
||||
|
||||
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 (
|
||||
<NodeViewWrapper
|
||||
as="span"
|
||||
data-alt="highlight-text"
|
||||
contentEditable={true}
|
||||
suppressContentEditableWarning={true}
|
||||
onInput={handleInput}
|
||||
className={`relative inline text-${color}-400 hover:text-${color}-300 transition-colors duration-200`}
|
||||
>
|
||||
{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 { FileText } from 'lucide-react';
|
||||
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
|
||||
@ -11,13 +11,24 @@ interface ScriptTabContentProps {
|
||||
originalText?: string;
|
||||
}
|
||||
|
||||
export function ScriptTabContent({
|
||||
setIsPauseWorkFlow,
|
||||
isPauseWorkFlow,
|
||||
originalText,
|
||||
}: ScriptTabContentProps) {
|
||||
export const ScriptTabContent = forwardRef<
|
||||
{ checkUpdate: () => boolean },
|
||||
ScriptTabContentProps
|
||||
>((props, ref) => {
|
||||
const { setIsPauseWorkFlow, isPauseWorkFlow, originalText } = props;
|
||||
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状态
|
||||
if (loading) {
|
||||
return (
|
||||
@ -52,8 +63,9 @@ export function ScriptTabContent({
|
||||
applyScript={applyScript}
|
||||
mode='manual'
|
||||
from='tab'
|
||||
setIsUpdate={setIsUpdate}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@ -293,105 +293,107 @@ export function ShotTabContent({
|
||||
|
||||
|
||||
{/* 下部分 */}
|
||||
<motion.div
|
||||
className="grid grid-cols-2 gap-4 w-full"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
{/* 视频预览和操作 */}
|
||||
<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">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||
<span className="text-white/50">Loading...</span>
|
||||
</div>
|
||||
)}
|
||||
{shotData[selectedIndex]?.status === 1 && (
|
||||
<motion.div
|
||||
className="aspect-video rounded-lg overflow-hidden relative group"
|
||||
layoutId={`video-preview-${selectedIndex}`}
|
||||
>
|
||||
<PersonDetectionScene
|
||||
videoSrc={shotData[selectedIndex]?.videoUrl[0]}
|
||||
detections={detections}
|
||||
triggerScan={scanState === 'scanning'}
|
||||
triggerSuccess={scanState === 'detected'}
|
||||
onScanTimeout={handleScanTimeout}
|
||||
onScanExit={handleScanExit}
|
||||
onDetectionsChange={handleDetectionsChange}
|
||||
onPersonClick={handlePersonClick}
|
||||
/>
|
||||
<motion.div className='absolute top-4 right-4 flex gap-2'>
|
||||
{/* 人物替换按钮 */}
|
||||
<motion.button
|
||||
onClick={() => handleScan()}
|
||||
className={`p-2 backdrop-blur-sm transition-colors z-10 rounded-full
|
||||
${scanState === 'detected'
|
||||
? '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 }}
|
||||
>
|
||||
{scanState === 'scanning' ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : scanState === 'detected' ? (
|
||||
<X className="w-4 h-4" />
|
||||
) : (
|
||||
<User className="w-4 h-4" />
|
||||
)}
|
||||
</motion.button>
|
||||
{shotData[selectedIndex] && (
|
||||
<motion.div
|
||||
className="grid grid-cols-2 gap-4 w-full"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
{/* 视频预览和操作 */}
|
||||
<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">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||
<span className="text-white/50">Loading...</span>
|
||||
</div>
|
||||
)}
|
||||
{shotData[selectedIndex]?.status === 1 && (
|
||||
<motion.div
|
||||
className="aspect-video rounded-lg overflow-hidden relative group"
|
||||
layoutId={`video-preview-${selectedIndex}`}
|
||||
>
|
||||
<PersonDetectionScene
|
||||
videoSrc={shotData[selectedIndex]?.videoUrl[0]}
|
||||
detections={detections}
|
||||
triggerScan={scanState === 'scanning'}
|
||||
triggerSuccess={scanState === 'detected'}
|
||||
onScanTimeout={handleScanTimeout}
|
||||
onScanExit={handleScanExit}
|
||||
onDetectionsChange={handleDetectionsChange}
|
||||
onPersonClick={handlePersonClick}
|
||||
/>
|
||||
<motion.div className='absolute top-4 right-4 flex gap-2'>
|
||||
{/* 人物替换按钮 */}
|
||||
<motion.button
|
||||
onClick={() => handleScan()}
|
||||
className={`p-2 backdrop-blur-sm transition-colors z-10 rounded-full
|
||||
${scanState === 'detected'
|
||||
? '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 }}
|
||||
>
|
||||
{scanState === 'scanning' ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : scanState === 'detected' ? (
|
||||
<X className="w-4 h-4" />
|
||||
) : (
|
||||
<User className="w-4 h-4" />
|
||||
)}
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
{shotData[selectedIndex]?.status === 2 && (
|
||||
<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" />
|
||||
<span className="text-white/50">任务失败,点击重新生成</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
|
||||
{/* 基础配置 */}
|
||||
<div className='space-y-4 col-span-1' key={selectedIndex}>
|
||||
<ShotsEditor
|
||||
ref={shotsEditorRef}
|
||||
roles={roles}
|
||||
shotInfo={shotData[selectedIndex].lens}
|
||||
style={{height: 'calc(100% - 4rem)'}}
|
||||
/>
|
||||
|
||||
{/* 重新生成按钮、新增分镜按钮 */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<motion.button
|
||||
onClick={() => handleAddShot()}
|
||||
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"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Add Shot</span>
|
||||
</motion.button>
|
||||
<motion.button
|
||||
onClick={() => handleRegenerate()}
|
||||
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"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
<span>Regenerate</span>
|
||||
</motion.button>
|
||||
)}
|
||||
{shotData[selectedIndex]?.status === 2 && (
|
||||
<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" />
|
||||
<span className="text-white/50">任务失败,点击重新生成</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
|
||||
{/* 基础配置 */}
|
||||
<div className='space-y-4 col-span-1' key={selectedIndex}>
|
||||
<ShotsEditor
|
||||
ref={shotsEditorRef}
|
||||
roles={roles}
|
||||
shotInfo={shotData[selectedIndex].lens}
|
||||
style={{height: 'calc(100% - 4rem)'}}
|
||||
/>
|
||||
|
||||
{/* 重新生成按钮、新增分镜按钮 */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<motion.button
|
||||
onClick={() => handleAddShot()}
|
||||
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"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Add Shot</span>
|
||||
</motion.button>
|
||||
<motion.button
|
||||
onClick={() => handleRegenerate()}
|
||||
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"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
<span>Regenerate</span>
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<FloatingGlassPanel
|
||||
open={isReplacePanelOpen}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user