"use client";
import { useState, useRef, useEffect } from "react";
import {
ChevronDown,
ChevronUp,
Video,
Loader2,
Lightbulb,
Package,
Crown,
Clapperboard,
Globe,
AudioLines,
Clock,
Trash2,
Plus,
LayoutTemplate,
ImagePlay,
Sparkles,
RotateCcw,
Settings,
} from "lucide-react";
import { Dropdown, Modal, Tooltip, Upload, Image, Spin } from "antd";
import { PlusOutlined, UploadOutlined, EyeOutlined } from "@ant-design/icons";
import { StoryTemplateEntity } from "@/app/service/domain/Entities";
import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService";
import TemplateCard from "./templateCard";
import { AudioRecorder } from "./AudioRecorder";
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
import { createScriptEpisodeNew } from "@/api/script_episode";
import { useRouter } from "next/navigation";
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { HighlightTextExtension } from "@/components/ui/main-editor/HighlightText";
import Placeholder from "@tiptap/extension-placeholder";
// 自定义音频播放器样式
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);
}
/* 文本截断类 */
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
`;
/**模板故事模式弹窗组件 */
const RenderTemplateStoryMode = ({
isOpen,
onClose,
}: {
isOpen: boolean;
onClose: () => void;
}) => {
// 使用 hook 管理状态
const {
templateStoryList,
selectedTemplate,
activeRoleIndex,
activeRole,
isLoading,
getTemplateStoryList,
actionStory,
setSelectedTemplate,
setActiveRoleIndex,
setActiveRoleImage,
setActiveRoleAudio,
} = useTemplateStoryServiceHook();
// 本地加载状态,用于 UI 反馈
const [localLoading, setLocalLoading] = useState(false);
// 组件挂载时获取模板列表
useEffect(() => {
if (isOpen) {
getTemplateStoryList();
}
}, [isOpen, getTemplateStoryList]);
// 处理模板选择
const handleTemplateSelect = (template: StoryTemplateEntity) => {
setSelectedTemplate(template);
setActiveRoleIndex(0); // 重置角色选择
};
// 处理确认操作
const handleConfirm = async () => {
if (!selectedTemplate) return;
try {
setLocalLoading(true);
const projectId = await actionStory();
console.log("Story action created:", projectId);
onClose();
// 重置状态
setSelectedTemplate(null);
setActiveRoleIndex(0);
} catch (error) {
console.error("Failed to create story action:", error);
// 这里可以添加 toast 提示
} finally {
setLocalLoading(false);
}
};
// 处理角色图片上传
const handleRoleImageUpload = (roleIndex: number, file: any) => {
if (file && selectedTemplate) {
// 模拟上传成功,设置图片URL
const imageUrl = URL.createObjectURL(file);
setActiveRoleImage(imageUrl);
}
};
// 删除角色图片
const handleDeleteRoleImage = (roleIndex: number) => {
if (selectedTemplate) {
setActiveRoleImage("");
}
};
// 模板列表渲染
const templateListRender = () => {
return (
Story Templates
{templateStoryList.map((template, index) => (
handleTemplateSelect(template)}
>
))}
);
};
// 故事编辑器渲染
const storyEditorRender = () => {
return isLoading ? (
) : selectedTemplate ? (
{/* 模板信息头部 - 增加顶部空间 */}
{/* 左侧图片 */}
{/* 右侧信息 - 增加文本渲染空间 */}
{selectedTemplate.name}
{selectedTemplate.generateText}
{/* 角色自定义部分 - 精简布局 */}
Character Customization
{/* 紧凑布局 */}
{/* 左侧:当前选中角色的音频与照片更改 - 精简版本 */}
{/* 图片上传部分 - 精简 */}
false}
onChange={(info) => {
if (info.file.status === "done") {
handleRoleImageUpload(
activeRoleIndex,
info.file.originFileObj
);
}
}}
>
{activeRole?.photo_url ? (
) : (
Upload Photo
)}
{/* 音频部分 - 精简版本 */}
{/* 音频操作区域 - 使用新的 AudioRecorder 组件 */}
{
setActiveRoleAudio(audioUrl);
}}
onAudioDeleted={() => {
setActiveRoleAudio("");
}}
/>
{/* 右侧:角色图片缩略图列表 - 精简 */}
Characters
{selectedTemplate.storyRole.map((role, index: number) => (
))}
{/* 弹窗底部操作 - 只保留 Action 按钮 */}
) : (
No templates available
Please try again later
);
};
return (
<>
{
// 清空所有选中的内容数据
setSelectedTemplate(null);
setActiveRoleIndex(0);
onClose();
}}
footer={null}
width="60%"
style={{ maxWidth: "800px", marginTop: "10vh" }}
className="template-modal"
closeIcon={
×
}
>
{/* 弹窗头部 */}
Template Story Selection
{templateListRender()}
{storyEditorRender()}
>
);
};
/**
* 视频工具面板组件
* 提供脚本输入和视频克隆两种模式,支持展开/收起功能
*/
export function ChatInputBox() {
// 控制面板展开/收起状态
const [isExpanded, setIsExpanded] = useState(false);
// 模板故事弹窗状态
const [isTemplateModalOpen, setIsTemplateModalOpen] = useState(false);
// 图片故事弹窗状态
const [isPhotoStoryModalOpen, setIsPhotoStoryModalOpen] = useState(false);
// 共享状态 - 需要在不同渲染函数间共享
const [script, setScript] = useState(""); // 用户输入的脚本内容
const router = useRouter();
const [loadingIdea, setLoadingIdea] = useState(false); // 获取创意建议时的加载状态
const [isCreating, setIsCreating] = useState(false); // 视频创建过程中的加载状态
// 配置选项状态 - 整合所有配置项到一个对象
const [configOptions, setConfigOptions] = useState({
mode: "auto",
resolution: "720p",
language: "english",
videoDuration: "1min",
});
// 配置项显示控制状态
const [showConfigOptions, setShowConfigOptions] = useState(false);
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 creating video
const handleCreateVideo = async () => {
setIsCreating(true);
if (!script) {
setIsCreating(false);
return;
}
const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
// 创建剧集数据
let episodeData: any = {
user_id: String(User.id),
script: script,
mode: configOptions.mode,
resolution: configOptions.resolution,
language: configOptions.language,
video_duration: configOptions.videoDuration,
};
// 调用创建剧集API
const episodeResponse = await createScriptEpisodeNew(episodeData);
console.log("episodeResponse", episodeResponse);
if (episodeResponse.code !== 0) {
console.error(`创建剧集失败: ${episodeResponse.message}`);
alert(`创建剧集失败: ${episodeResponse.message}`);
return;
}
let episodeId = episodeResponse.data.project_id;
// let episodeId = '9c34fcc4-c8d8-44fc-879e-9bd56f608c76';
router.push(`/create/work-flow?episodeId=${episodeId}`);
setIsCreating(false);
};
return (
{/* 视频故事板工具面板 - 毛玻璃效果背景 */}
{/* 展开/收起控制区域 */}
{isExpanded ? (
// 展开状态:显示收起按钮和提示
setIsExpanded(false)}
>
Click to action
) : (
// 收起状态:显示展开按钮
setIsExpanded(true)}
>
)}
{/* 主要内容区域 - 简化层级,垂直居中 */}
{/* 右上角齿轮图标和配置项 */}
{!isExpanded && (
{/* 使用 Dropdown 替代手动控制显示/隐藏 */}
(
setConfigOptions((prev) => ({ ...prev, [key]: value }))
}
/>
)}
placement={"left" as any}
trigger={["click"]}
>
{/* 配置项显示控制按钮 - 齿轮图标 */}
)}
{/* 输入框和Action按钮 - 只在展开状态显示 */}
{!isExpanded && (
{/* 第一行:输入框 */}
{/* 文本输入框 - 改为textarea */}
{/* 第二行:功能按钮和Action按钮 - 同一行 */}
{/* 左侧功能按钮区域 */}
{/* 获取创意按钮 */}
{/* 分隔线 */}
{/* 模板故事按钮 */}
{/* 分隔线 */}
{/* 图片故事按钮 */}
{/* 图片故事弹窗 */}
setIsPhotoStoryModalOpen(false)}
onConfirm={(storyContent, category) => {
setScript(storyContent);
setIsPhotoStoryModalOpen(false);
}}
/>
{/* 右侧Action按钮 */}
}
/>
)}
{/* 配置选项区域 - 已移至右上角 */}
{/*
setConfigOptions((prev) => ({ ...prev, [key]: value }))
}
/> */}
{/* 模板故事弹窗 */}
setIsTemplateModalOpen(false)}
/>
);
}
// 创建视频按钮
const ActionButton = ({
isCreating,
handleCreateVideo,
icon,
}: {
isCreating: boolean;
handleCreateVideo: () => void;
icon: React.ReactNode;
}) => {
return (
);
};
/**
* 配置选项组件
* 提供视频创建的各种配置选项,位于输入框下方
*/
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"
>
);
})}
);
};
/**
* 角色高亮编辑器组件
* 使用 Tiptap 实现角色名称高亮和文本编辑功能
*/
const RoleHighlightEditor = ({
content,
onContentChange,
}: {
content: string;
onContentChange: (content: string) => void;
}) => {
const editor = useEditor({
extensions: [
StarterKit,
HighlightTextExtension,
Placeholder.configure({
placeholder:
"Share your creative ideas about the image and let AI create a movie story for you...",
emptyEditorClass: "is-editor-empty",
}),
],
content: "",
// 简化:移除复杂的 onUpdate 逻辑,只处理基本的文本变化
onUpdate: ({ editor }) => {
const textContent = editor.getText();
if (!textContent.trim()) {
onContentChange("");
return;
}
// 直接传递文本内容,不进行复杂的标签重建
onContentChange(textContent);
},
editorProps: {
handleKeyDown: (view, event) => {
const { from, to } = view.state.selection;
const doc = view.state.doc;
// 检查光标前后是否有角色标签
const textBefore =
from > 0 ? doc.textBetween(Math.max(0, from - 50), from) : "";
const textAfter =
to < doc.content.size
? doc.textBetween(to, Math.min(doc.content.size, to + 50))
: "";
// TODO role id 的结构
const beforeMatch = textBefore.match(/[^<]*$/);
const afterMatch = textAfter.match(/^[^>]*<\/role_name>/);
if (beforeMatch || afterMatch) {
if (event.key !== "Backspace" && event.key !== "Delete") {
event.preventDefault();
return true;
}
}
return false;
},
},
immediatelyRender: false,
});
useEffect(() => {
if (editor) {
if (!content || content.trim() === "") {
editor.commands.clearContent(true);
return;
}
// 将带标签的内容转换为高亮显示
const htmlContent = content.replace(
/([^<]+)<\/role_name>/g,
'$1'
);
editor.commands.setContent(htmlContent, { emitUpdate: false });
}
}, [content, editor]);
return (
);
};
/**
* 图片故事弹窗组件
* 提供图片上传、AI分析和故事生成功能,支持动态UI变化
*/
const PhotoStoryModal = ({
isOpen,
onClose,
onConfirm,
}: {
isOpen: boolean;
onClose: () => void;
onConfirm: (storyContent: string, category: string) => void;
}) => {
// 使用图片故事服务hook管理状态
const {
activeImageUrl,
storyContent,
charactersAnalysis,
potentialGenres,
selectedCategory,
isLoading,
hasAnalyzed,
updateStoryType,
updateStoryContent,
updateCharacterName,
resetImageStory,
triggerFileSelection,
avatarComputed,
uploadAndAnalyzeImage,
setCharactersAnalysis,
} = useImageStoryServiceHook();
// 重置状态
const handleClose = () => {
resetImageStory();
onClose();
};
// 处理图片上传
const handleImageUpload = async () => {
try {
await triggerFileSelection();
} catch (error) {
console.error("Failed to upload image:", error);
}
};
// 处理确认
const handleConfirm = () => {
if (storyContent.trim()) {
onConfirm(storyContent, selectedCategory);
}
};
return (
×
}
>
{/* 弹窗头部 */}
Movie Generation from Image
{/* 左侧:图片上传 */}
{activeImageUrl ? (
) : (
)}
{/* 中间:头像展示(分析后显示) */}
{hasAnalyzed && avatarComputed.length > 0 && (
{avatarComputed.map((avatar, index) => (

{
// 如果裁剪的头像加载失败,回退到原图
const target = e.target as HTMLImageElement;
target.src = activeImageUrl;
}}
/>
{/* 删除角色按钮 - 使用Tooltip并调整z-index避免被遮挡 */}
))}
)}
{/* 右侧:分类选择(分析后显示) */}
{hasAnalyzed && potentialGenres.length > 0 && (
{["Auto", ...potentialGenres].map((genre) => (
))}
)}
{/* 文本输入框 */}
{!hasAnalyzed ? (
// 分析按钮 - 使用ActionButton样式
}
/>
) : (
<>
{/* Action按钮 - 使用ActionButton样式 */}
}
/>
>
)}
);
};