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[];
}
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;
}

View File

@ -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);
};

View File

@ -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)
},

View File

@ -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}`,

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 点击没有任何事件效果,页面没变化
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>
)}

View File

@ -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';
}
// 如果有已完成的数据,同步到状态

View File

@ -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) => {

View File

@ -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

View File

@ -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}

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 { 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>
);
};
});

View File

@ -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}