"use client"; import { useState, useRef } from "react"; import { ChevronDown, ChevronUp, Video, Loader2, Lightbulb, Package, Crown, Clapperboard, Globe, AudioLines, Clock, Trash2, Plus, LayoutTemplate, ImagePlay, } from "lucide-react"; import { Dropdown, Modal, Tooltip, Upload, Image } from "antd"; import { PlusOutlined, UploadOutlined, EyeOutlined } from "@ant-design/icons"; import { StoryTemplateEntity, } from "@/app/service/domain/Entities"; import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService"; import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService"; import { useUploadFile } from "@/app/service/domain/service"; import TemplateCard from "./templateCard"; import { AudioRecorder } from "./AudioRecorder"; import { PhotoStoryMode } from "./PhotoStoryMode"; // 自定义音频播放器样式 const customAudioPlayerStyles = ` .custom-audio-player { background: rgba(255, 255, 255, 0.05) !important; border-radius: 8px !important; border: 1px solid rgba(255, 255, 255, 0.1) !important; } .custom-audio-player .rhap_main-controls-button { color: white !important; } .custom-audio-player .rhap_progress-filled { background-color: #3b82f6 !important; } .custom-audio-player .rhap_progress-indicator { background-color: #3b82f6 !important; } .custom-audio-player .rhap_time { color: rgba(255, 255, 255, 0.7) !important; } .custom-audio-player .rhap_volume-controls { color: white !important; } /* 模式选择下拉菜单样式 */ .mode-dropdown .ant-dropdown-menu { background: rgba(255, 255, 255, 0.08) !important; backdrop-filter: blur(20px) !important; border: 1px solid rgba(255, 255, 255, 0.12) !important; border-radius: 10px !important; padding: 6px !important; min-width: 160px !important; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important; } .mode-dropdown .ant-dropdown-menu-item { padding: 6px 10px !important; border-radius: 6px !important; color: rgba(255, 255, 255, 0.9) !important; transition: all 0.2s ease !important; margin-bottom: 3px !important; } .mode-dropdown .ant-dropdown-menu-item:hover { background: rgba(255, 255, 255, 0.15) !important; transform: translateX(4px) !important; } .mode-dropdown .ant-dropdown-menu-item:last-child { margin-bottom: 0 !important; } /* 模式提示tooltip样式 */ .mode-tooltip .ant-tooltip-inner { background: rgba(0, 0, 0, 0.8) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(255, 255, 255, 0.1) !important; border-radius: 8px !important; color: white !important; font-size: 12px !important; line-height: 1.4 !important; max-width: 200px !important; } .mode-tooltip .ant-tooltip-arrow::before { background: rgba(0, 0, 0, 0.8) !important; border: 1px solid rgba(255, 255, 255, 0.1) !important; } /* 模板卡片样式 */ .line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } /* 自定义滚动条 */ .template-list-scroll::-webkit-scrollbar { width: 4px; } .template-list-scroll::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.05); border-radius: 2px; } .template-list-scroll::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 2px; } .template-list-scroll::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.3); } /* 自定义缩放类 */ .scale-102 { transform: scale(1.02); } `; /**模板故事模式弹窗组件 */ const RenderTemplateStoryMode = ({ isOpen, onClose, }: { isOpen: boolean; onClose: () => void; }) => { // Mock数据 - 直接写在组件中用于渲染 const mockTemplates = [ { id: "1", name: "魔法森林冒险", generateText: "一个关于勇敢的小女孩在魔法森林中寻找失落宝藏的奇幻冒险故事。森林中充满了神秘的生物和隐藏的危险,她必须依靠智慧和勇气来克服重重困难。", imageUrl: [ "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=400&fit=crop", ], storyRole: [ { role_name: "艾莉娅", photo_url: "https://images.unsplash.com/photo-1494790108755-2616b612b786?w=200&h=200&fit=crop", voice_url: "", }, { role_name: "森林守护者", photo_url: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200&h=200&fit=crop", voice_url: "", }, { role_name: "魔法精灵", photo_url: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200&h=200&fit=crop", voice_url: "", }, ], }, { id: "2", name: "太空探索之旅", generateText: "一支勇敢的宇航员团队在探索未知星球时发现了一个古老的文明遗迹。他们必须解开这个文明的秘密,同时面对来自宇宙深处的威胁。", imageUrl: [ "https://images.unsplash.com/photo-1446776811953-b23d0bd63bc8?w=400&h=400&fit=crop", ], storyRole: [ { role_name: "船长萨拉", photo_url: "https://images.unsplash.com/photo-1508214751196-bcfd4ca60f91?w=200&h=200&fit=crop", voice_url: "", }, { role_name: "科学家马克", photo_url: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200&h=200&fit=crop", voice_url: "", }, { role_name: "工程师安娜", photo_url: "https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=200&h=200&fit=crop", voice_url: "", }, ], }, { id: "3", name: "古代王朝传奇", generateText: "在一个古老的东方王朝中,年轻的公主必须学会在复杂的宫廷政治中生存。她面临着背叛、阴谋和爱情的选择,最终成长为一位明智的统治者。", imageUrl: [ "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=400&fit=crop", ], storyRole: [ { role_name: "公主明月", photo_url: "https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=200&h=200&fit=crop", voice_url: "", }, { role_name: "大将军", photo_url: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=200&h=200&fit=crop", voice_url: "", }, { role_name: "宫廷谋士", photo_url: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200&h=200&fit=crop", voice_url: "", }, ], }, ]; // 本地状态管理 const [templates] = useState(mockTemplates); const [selectedTemplate, setSelectedTemplate] = useState(mockTemplates[0]); const [loading] = useState(false); const [selectedRoleIndex, setSelectedRoleIndex] = useState(0); const [localLoading, setLocalLoading] = useState(false); // 角色资源状态管理 - 这些是UI交互必需的,无法简化 /** 角色图片资源,key为角色索引,value为图片URL */ const [roleImages, setRoleImages] = useState<{ [key: number]: string }>({}); /** 角色音频资源,key为角色索引,value为音频URL */ const [roleAudios, setRoleAudios] = useState<{ [key: number]: string }>({}); // 处理模板选择 const handleTemplateSelect = (template: StoryTemplateEntity) => { setSelectedTemplate(template); }; // 处理确认操作 const handleConfirm = async () => { if (!selectedTemplate) return; try { setLocalLoading(true); // Mock actionStory函数 await new Promise((resolve) => setTimeout(resolve, 2000)); // 模拟2秒延迟 const projectId = "mock-project-" + Date.now(); console.log("Story action created:", projectId); onClose(); setSelectedTemplate(mockTemplates[0]); // 重置为第一个模板 // 清空角色资源状态 setRoleImages({}); setRoleAudios({}); setSelectedRoleIndex(0); } catch (error) { console.error("Failed to create story action:", error); alert("Failed to create story action. Please try again."); } finally { setLocalLoading(false); } }; // 处理角色图片上传 const handleRoleImageUpload = (roleIndex: number, file: any) => { if (file && selectedTemplate) { // 这里可以添加实际的上传逻辑 console.log("Character image uploaded:", file.name); // 模拟上传成功,设置图片URL const imageUrl = URL.createObjectURL(file); setRoleImages((prev) => ({ ...prev, [roleIndex]: imageUrl })); // 直接更新模板中的角色图片 const updatedTemplate = { ...selectedTemplate, storyRole: selectedTemplate.storyRole.map((role: any, index: number) => index === roleIndex ? { ...role, photo_url: imageUrl } : role ), }; setSelectedTemplate(updatedTemplate); } }; // 删除角色图片 const handleDeleteRoleImage = (roleIndex: number) => { setRoleImages((prev) => { const newState = { ...prev }; delete newState[roleIndex]; return newState; }); // 同时从模板中删除图片 if (selectedTemplate) { const updatedTemplate = { ...selectedTemplate, storyRole: selectedTemplate.storyRole.map((role: any, index: number) => index === roleIndex ? { ...role, photo_url: "" } : role ), }; setSelectedTemplate(updatedTemplate); } }; // 模板列表渲染 const templateListRender = () => { return (

Story Templates

{templates.map((template, index) => (
handleTemplateSelect(template)} >
))}
); }; // 故事编辑器渲染 const storyEditorRender = () => { return loading ? (
) : selectedTemplate ? (
{/* 模板信息头部 - 增加顶部空间 */}
{/* 左侧图片 */}
{selectedTemplate.name}
{/* 右侧信息 - 增加文本渲染空间 */}

{selectedTemplate.name}

{selectedTemplate.generateText}

{/* 角色自定义部分 - 精简布局 */}

Character Customization

{/* 紧凑布局 */}
{/* 左侧:当前选中角色的音频与照片更改 - 精简版本 */}
{/* 图片上传部分 - 精简 */}
false} onChange={(info) => { if (info.file.status === "done") { handleRoleImageUpload( selectedRoleIndex, info.file.originFileObj ); } }} > {roleImages[selectedRoleIndex] || selectedTemplate.storyRole[selectedRoleIndex] ?.photo_url ? (
Character Portrait {roleImages[selectedRoleIndex] && ( )}
Change Photo
) : (
Upload Photo
)}
{/* 音频部分 - 精简版本 */}
{/* 音频操作区域 - 使用新的 AudioRecorder 组件 */}
{ // 保存到状态 setRoleAudios((prev) => ({ ...prev, [selectedRoleIndex]: audioUrl, })); // 保存到模板中 if (selectedTemplate) { const updatedTemplate = { ...selectedTemplate, storyRole: selectedTemplate.storyRole.map( (role: any, index: number) => index === selectedRoleIndex ? { ...role, voice_url: audioUrl } : role ), }; setSelectedTemplate(updatedTemplate); } }} onAudioDeleted={() => { // 从状态中删除 setRoleAudios((prev) => { const newState = { ...prev }; delete newState[selectedRoleIndex]; return newState; }); // 从模板中删除 if (selectedTemplate) { const updatedTemplate = { ...selectedTemplate, storyRole: selectedTemplate.storyRole.map( (role: any, index: number) => index === selectedRoleIndex ? { ...role, voice_url: "" } : role ), }; setSelectedTemplate(updatedTemplate); } }} />
{/* 右侧:角色图片缩略图列表 - 精简 */}

Characters

{selectedTemplate.storyRole.map((role, index: number) => ( ))}
{/* 弹窗底部操作 - 只保留 Action 按钮 */}
) : (

No templates available

Please try again later

); }; return ( <> { // 清空所有选中的内容数据 setRoleImages({}); setRoleAudios({}); setSelectedTemplate(mockTemplates[0]); // 重置为第一个模板 setSelectedRoleIndex(0); onClose(); }} footer={null} width="60%" style={{ maxWidth: "800px" }} className="template-modal" closeIcon={
×
} >
{/* 弹窗头部 */}

Template Story Selection

{templateListRender()}
{storyEditorRender()}
); }; /** * 视频工具面板组件 * 提供脚本输入和视频克隆两种模式,支持展开/收起功能 */ export function ChatInputBox() { // 控制面板展开/收起状态 const [isExpanded, setIsExpanded] = useState(false); // 模板故事弹窗状态 const [isTemplateModalOpen, setIsTemplateModalOpen] = useState(false); // 图片故事模式状态 const [isPhotoStoryMode, setIsPhotoStoryMode] = useState(false); // 使用图片故事服务hook const { activeImageUrl, activeTextContent, isAnalyzing, isUploading: isImageUploading, uploadAndAnalyzeImage, updateStoryContent, resetImageStory, } = useImageStoryServiceHook(); // 使用上传文件Hook const { uploadFile, isUploading } = useUploadFile(); // 共享状态 - 需要在不同渲染函数间共享 const [script, setScript] = useState(""); // 用户输入的脚本内容 const [loadingIdea, setLoadingIdea] = useState(false); // 获取创意建议时的加载状态 const [isCreating, setIsCreating] = useState(false); // 视频创建过程中的加载状态 // 配置选项状态 - 整合所有配置项到一个对象 const [configOptions, setConfigOptions] = useState({ mode: "auto", resolution: "720p", language: "english", videoDuration: "1min", }); // 处理图片上传和分析 const handlePhotoUpload = async (file: File) => { try { // 先上传图片到服务器 const uploadedImageUrl = await uploadFile(file, (progress) => { console.log("上传进度:", progress); }); // 使用hook处理图片上传和分析 await uploadAndAnalyzeImage(uploadedImageUrl); // 等待一小段时间确保hook状态更新完成,然后同步到输入框 setTimeout(() => { setScript(activeTextContent); }, 100); } catch (error) { console.error("Photo upload or analysis failed:", error); } }; // 退出图片故事模式 const exitPhotoStoryMode = () => { setIsPhotoStoryMode(false); resetImageStory(); }; const handleGetIdea = () => { if (loadingIdea) return; setLoadingIdea(true); const ideaText = "a cute capybara with an orange on its head, staring into the distance and walking forward"; setTimeout(() => { setScript(ideaText); setLoadingIdea(false); }, 3000); }; // Handle editor content changes const handleEditorChange = (e: React.FormEvent) => { const newText = e.currentTarget.textContent || ""; setScript(newText); // 如果在图片故事模式下,同步更新hook中的故事内容 if (isPhotoStoryMode) { updateStoryContent(newText); } }; // 当进入图片故事模式时,清空输入框内容并直接触发文件选择 const enterPhotoStoryMode = () => { setIsPhotoStoryMode(true); setScript(""); resetImageStory(); // 直接触发文件选择器 const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = "image/*"; fileInput.style.display = "none"; fileInput.onchange = (e) => { const target = e.target as HTMLInputElement; if (target.files && target.files[0]) { handlePhotoUpload(target.files[0]); } // 清理DOM document.body.removeChild(fileInput); }; document.body.appendChild(fileInput); fileInput.click(); }; // Handle creating video const handleCreateVideo = async () => { setIsCreating(true); // 这里可以添加实际的创建逻辑 console.log("创建视频:", { script, ...configOptions, ...(isPhotoStoryMode && { imageUrl: activeImageUrl, imageStory: activeTextContent, isAnalyzing, isUploading: isImageUploading }), }); setTimeout(() => { setIsCreating(false); }, 2000); }; return (
{/* 视频故事板工具面板 - 毛玻璃效果背景 */}
{/* 展开/收起控制区域 */} {isExpanded ? ( // 展开状态:显示收起按钮和提示
setIsExpanded(false)} > Click to action
) : ( // 收起状态:显示展开按钮
setIsExpanded(true)} >
)} {/* 主要内容区域 - 简化层级,垂直居中 */}
{/* 输入框和Action按钮 - 只在展开状态显示 */} {!isExpanded && (
{/* 模式选择下拉菜单 */} Template Story
), }, { key: "photo", label: (
Photo Story
), }, ], onClick: ({ key }) => { if (key === "template") { setIsTemplateModalOpen(true); } else if (key === "photo") { enterPhotoStoryMode(); } console.log("Selected mode:", key); }, }} trigger={["click"]} placement="topLeft" overlayClassName="mode-dropdown" > {/* 图片故事模式UI */} {isPhotoStoryMode && }
{/* 可编辑的脚本输入区域 */}
{isPhotoStoryMode && isAnalyzing ? (
Analyzing your image, please wait...
) : isPhotoStoryMode && activeTextContent ? (
{activeTextContent}
) : ( script )}
{/* 占位符文本和获取创意按钮 */}
Describe the content you want to action. Get an handleGetIdea()} > {loadingIdea ? ( ) : ( <> idea )}
{/* Action按钮 */}
)}
{/* 配置选项区域 */} setConfigOptions((prev) => ({ ...prev, [key]: value })) } /> {/* 模板故事弹窗 */} setIsTemplateModalOpen(false)} />
); } /** * 配置选项组件 * 提供视频创建的各种配置选项,位于输入框下方 */ const ConfigOptions = ({ config, onConfigChange, }: { config: { mode: string; resolution: string; language: string; videoDuration: string; }; onConfigChange: (key: string, value: string) => void; }) => { const configItems = [ { key: "mode", icon: Package, options: [ { value: "auto", label: "Auto", isVip: false }, { value: "manual", label: "Manual", isVip: true }, ], }, { key: "resolution", icon: Video, options: [ { value: "720p", label: "720P", isVip: false }, { value: "1080p", label: "1080P", isVip: true }, { value: "2k", label: "2K", isVip: true }, { value: "4k", label: "4K", isVip: true }, ], }, { key: "language", icon: Globe, options: [ { value: "english", label: "English", isVip: false }, { value: "chinese", label: "Chinese", isVip: true }, { value: "japanese", label: "Japanese", isVip: true }, { value: "korean", label: "Korean", isVip: true }, ], }, { key: "videoDuration", icon: Clock, options: [ { value: "1min", label: "1 Min", isVip: false }, { value: "2min", label: "2 Min", isVip: true }, { value: "3min", label: "3 Min", isVip: true }, ], }, ]; return (
{configItems.map((item) => { const IconComponent = item.icon; const currentOption = item.options.find( (opt) => opt.value === config[item.key as keyof typeof config] ); return ( ({ key: option.value, label: (
{option.label} {option.isVip && ( )}
), })), onClick: ({ key }) => onConfigChange(item.key, key), }} trigger={["click"]} placement="bottomLeft" >
); })}
); };