video-flow-b/components/ui/script-tab-content.tsx

284 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useCallback, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Check, ChevronDown } from 'lucide-react';
import * as Popover from '@radix-ui/react-popover';
import { mockSceneOptions, mockCharacterOptions } from '@/app/model/enums';
import { Button } from './button';
import { Input } from './input';
import { useScriptService } from '@/app/service/Interaction/ScriptService';
const ScriptTabContent: React.FC = () => {
// 获取当前项目ID这里需要根据实际项目路由或上下文获取
const projectId = 'current-project-id'; // TODO: 从路由或上下文获取实际项目ID
const {
scriptSlices,
userPrompt,
loading,
error,
updateUserPrompt,
fetchScriptData,
setFocusedSlice,
updateScriptSliceText,
resetScript,
applyScript,
fetchProjectScript
} = useScriptService();
// 组件挂载时获取项目剧本数据
useEffect(() => {
const initializeScript = async () => {
try {
await fetchProjectScript(projectId);
} catch (error) {
console.error('初始化剧本数据失败:', error);
}
};
initializeScript();
}, [projectId, fetchProjectScript]);
// 处理AI生成按钮点击
const handleAiGenerate = useCallback(async () => {
if (!userPrompt.trim()) return;
try {
await fetchScriptData(userPrompt);
} catch (error) {
console.error('生成剧本失败:', error);
}
}, [userPrompt, fetchScriptData]);
// 处理重置按钮点击
const handleReset = useCallback(() => {
resetScript();
}, [resetScript]);
// 处理确认按钮点击
const handleConfirm = useCallback(async () => {
try {
await applyScript();
} catch (error) {
console.error('应用剧本失败:', error);
}
}, [applyScript]);
// 处理提示词输入变化
const handlePromptChange = useCallback((value: string) => {
updateUserPrompt(value);
}, [updateUserPrompt]);
// 处理剧本片段文本变化
const handleScriptSliceChange = useCallback((sliceId: string, text: string) => {
setFocusedSlice(sliceId);
updateScriptSliceText(text);
}, [setFocusedSlice, updateScriptSliceText]);
return (
<div className="flex flex-col h-full">
<motion.div
className="relative w-full h-[90vh] backdrop-blur-xl rounded-2xl shadow-2xl overflow-hidden flex flex-col"
initial={{ scale: 0.95, y: 10, opacity: 0 }}
animate={{
scale: 1,
y: 0,
opacity: 1,
transition: {
type: "spring",
duration: 0.3,
bounce: 0.15,
stiffness: 300,
damping: 25
}
}}
exit={{
scale: 0.95,
y: 10,
opacity: 0,
transition: {
type: "tween",
duration: 0.1,
ease: "easeOut"
}
}}
>
{/* 标题 */}
<motion.div
className="flex-none px-6 py-4"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
Edit Script
</h2>
</motion.div>
{/* 内容区域 */}
<motion.div
className="flex-1 overflow-auto p-6 pt-0 pb-0"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.1, duration: 0.2 }}
>
{/* 剧本片段渲染区域 */}
<motion.div
style={{
height: 'calc(100% - 88px)'
}}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="space-y-4"
>
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
<p className="text-red-600 text-sm">{error}</p>
</div>
)}
{/* 剧本片段列表 */}
<div className="space-y-3">
{scriptSlices.map((slice, index) => (
<motion.div
key={slice.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="relative"
>
<Input
value={slice.text}
onChange={(e) => handleScriptSliceChange(slice.id, e.target.value)}
placeholder={`输入${slice.type}内容...`}
className="w-full bg-white/50 dark:bg-[#5b75ac20] border border-gray-200 dark:border-gray-600 focus:ring-2 focus:ring-blue-500/20 transition-all duration-200"
/>
<div className="absolute top-2 right-2">
<span className="text-xs px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400 rounded">
{slice.type}
</span>
</div>
</motion.div>
))}
</div>
{/* 加载状态 */}
{loading && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex items-center justify-center py-8"
>
<div className="flex items-center space-x-2">
<div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
<span className="text-sm text-gray-600 dark:text-gray-400">
...
</span>
</div>
</motion.div>
)}
</motion.div>
{/* 修改建议输入区域 */}
<motion.div
className="sticky bottom-0 bg-gradient-to-t from-white via-white to-transparent dark:from-[#5b75ac4d] dark:via-[#5b75ac4d] dark:to-transparent pt-8 pb-4 rounded-sm"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<div className="flex items-center space-x-2 px-4">
<div className="flex-1">
<Input
value={userPrompt}
onChange={(e) => handlePromptChange(e.target.value)}
placeholder="输入提示词然后点击AI生成按钮..."
className="outline-none box-shadow-none bg-white/50 dark:bg-[#5b75ac20] border-0 focus:ring-2 focus:ring-blue-500/20 transition-all duration-200"
disabled={loading}
/>
</div>
<motion.div
initial={false}
animate={{
scale: userPrompt.trim() && !loading ? 1 : 0.8,
opacity: userPrompt.trim() && !loading ? 1 : 0.5,
}}
transition={{
type: "spring",
stiffness: 500,
damping: 30
}}
>
<Button
variant="ghost"
size="icon"
disabled={!userPrompt.trim() || loading}
onClick={handleAiGenerate}
className="aiGenerate relative w-9 h-9 rounded-full bg-blue-500/10 hover:bg-blue-500/20 text-blue-600 dark:text-blue-400"
>
<motion.span
initial={false}
animate={{
opacity: loading ? 0 : 1,
scale: loading ? 0.5 : 1,
}}
>
<svg
className="w-5 h-5 transform rotate-90"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
/>
</svg>
</motion.span>
{loading && (
<motion.div
className="absolute inset-0 flex items-center justify-center"
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.5 }}
>
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
</motion.div>
)}
</Button>
</motion.div>
</div>
</motion.div>
</motion.div>
{/* 底部按钮 */}
<motion.div
className="flex-none px-6 py-4 flex justify-end space-x-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
>
<Button
variant="ghost"
className="min-w-[80px] bg-white/10 text-white hover:bg-white/20 transition-colors"
onClick={handleReset}
disabled={loading}
>
Reset
</Button>
<Button
className="min-w-[80px] bg-blue-500 text-white hover:bg-blue-600 transition-colors"
onClick={handleConfirm}
disabled={loading || scriptSlices.length === 0}
>
Confirm
</Button>
</motion.div>
</motion.div>
</div>
);
};
export default ScriptTabContent;