2025-09-19 20:17:26 +08:00

511 lines
20 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.

"use client";
import { useState, useEffect, useRef } from "react";
import {
ChevronDown,
ChevronUp,
Video,
Loader2,
Lightbulb,
Package,
Crown,
Clapperboard,
Globe,
Clock,
Trash2,
LayoutTemplate,
ImagePlay,
Sparkles,
Settings,
MoreHorizontal,
} from "lucide-react";
import {
Dropdown,
Modal,
Tooltip,
Upload,
Popconfirm,
Image,
Popover,
Switch,
} from "antd";
import { UploadOutlined } 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 { useRouter } from "next/navigation";
import { createMovieProjectV1 } from "@/api/video_flow";
import {
MovieProjectService,
MovieProjectMode,
} from "@/app/service/Interaction/MovieProjectService";
import { useLoadScriptText, useUploadFile } from "@/app/service/domain/service";
import { ActionButton } from "../common/ActionButton";
import { HighlightEditor } from "../common/HighlightEditor";
import GlobalLoad from "../common/GlobalLoad";
import { useDeviceType } from '@/hooks/useDeviceType';
import { PcTemplateModal } from "./PcTemplateModal";
import { H5TemplateDrawer } from "./H5TemplateDrawer";
import { PcPhotoStoryModal } from "./PcPhotoStoryModal";
import { H5PhotoStoryDrawer } from "./H5PhotoStoryDrawer";
const LauguageOptions = [
{ value: "english", label: "English", isVip: false, code:'EN' },
{ value: "chinese", label: "Chinese", isVip: false, code:'ZH' },
{ value: "japanese", label: "Japanese", isVip: false, code:'JA' },
{ value: "spanish", label: "Spanish", isVip: false, code:'ES' },
{ value: "portuguese", label: "Portuguese", isVip: false, code:'PT' },
{ value: "hindi", label: "Hindi", isVip: false, code:'HI' },
{ value: "korean", label: "Korean", isVip: false, code:'KO' },
{ value: "arabic", label: "Arabic", isVip: false, code:'AR' },
{ value: "russian", label: "Russian", isVip: false, code:'RU' },
{ value: "thai", label: "Thai", isVip: false, code:'TH' },
{ value: "french", label: "French", isVip: false, code:'FR' },
{ value: "german", label: "German", isVip: false, code:'DE' },
{ value: "vietnamese", label: "Vietnamese", isVip: false, code:'VI' },
{ value: "indonesian", label: "Indonesian", isVip: false, code:'ID' }
]
const VideoDurationOptions = [
{ value: "8s", label: "8s" },
{ value: "1min", label: "1min" },
{ value: "2min", label: "2min" },
{ value: "unlimited", label: "unlimited" },
];
/**模板故事模式弹窗组件 */
/**
* 防抖函数
* @param {Function} func - 需要防抖的函数
* @param {number} wait - 等待时间(ms)
* @returns {Function} - 防抖后的函数
*/
const debounce = (func: Function, wait: number) => {
let timeout: NodeJS.Timeout;
return function executedFunction(...args: any[]) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
/**
* 视频工具面板组件
* 提供脚本输入和视频克隆两种模式,支持展开/收起功能
*/
export function ChatInputBox({ noData }: { noData: boolean }) {
const { isMobile, isDesktop } = useDeviceType();
// 控制面板展开/收起状态
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 [isTemplateCreating, setIsTemplateCreating] = useState(false); // 模板故事创建按钮的加载状态
const [isPhotoCreating, setIsPhotoCreating] = useState(false); // 图片故事创建按钮的加载状态
const [isRoleGenerating, setIsRoleGenerating] = useState<{[key: string]: boolean}>({}); // 角色AI生成按钮的加载状态
const [isItemGenerating, setIsItemGenerating] = useState<{[key: string]: boolean}>({}); // 道具AI生成按钮的加载状态
// 配置选项状态 - 整合所有配置项到一个对象
type ConfigOptions = {
mode: "auto" | "manual";
resolution: "720p" | "1080p" | "4k";
language: string;
videoDuration: string;
expansion_mode: boolean;
};
const [configOptions, setConfigOptions] = useState<ConfigOptions>({
mode: "auto",
resolution: "720p",
language: "english",
videoDuration: "unlimited",
expansion_mode: true,
});
// 从 localStorage 初始化配置
useEffect(() => {
const savedConfig = localStorage.getItem('videoFlowConfig');
if (savedConfig) {
try {
const parsed = JSON.parse(savedConfig);
setConfigOptions({
mode: parsed.mode || "auto",
resolution: parsed.resolution || "720p",
language: parsed.language || "english",
videoDuration: parsed.videoDuration || "1min",
expansion_mode: typeof parsed.expansion_mode === 'boolean' ? parsed.expansion_mode : false,
});
} catch (error) {
console.warn('解析保存的配置失败,使用默认配置:', error);
}
}
}, []);
const onConfigChange = <K extends keyof ConfigOptions>(key: K, value: ConfigOptions[K]) => {
setConfigOptions((prev) => ({
...prev,
[key]: value,
}));
if (key === 'videoDuration') {
// 当选择 8s 时,强制关闭剧本扩展并禁用开关
if (value === '8s') {
setConfigOptions((prev) => ({
...prev,
expansion_mode: false,
}));
} else {
setConfigOptions((prev) => ({
...prev,
expansion_mode: true,
}));
}
}
};
const handleCreateVideo = async () => {
if (isCreating) return; // 如果正在创建中,直接返回
if (!script) {
console.warn("请输入剧本内容");
return;
}
setIsCreating(true); // 设置创建状态为 true按钮会显示 loading 状态
try {
const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
if (!User.id) {
console.error("用户未登录");
return;
}
// 创建剧集数据
let episodeData: any = {
user_id: String(User.id),
script: script,
mode: configOptions.mode,
resolution: configOptions.resolution,
language: configOptions.language,
video_duration: configOptions.videoDuration,
expansion_mode: configOptions.expansion_mode,
};
// 调用创建剧集API
const result = await MovieProjectService.createProject(
MovieProjectMode.NORMAL,
episodeData
);
const episodeId = result.project_id;
router.push(`/movies/work-flow?episodeId=${episodeId}`);
} catch (error) {
console.error("创建剧集失败:", error);
} finally {
setIsCreating(false); // 无论成功还是失败,都重置创建状态
}
};
return (
<div
className="video-tool-component relative max-w-[1080px] w-full left-[50%] translate-x-[-50%]"
style={
noData
? {
top: "50%",
}
: {}
}
>
{/* 视频故事板工具面板 - 毛玻璃效果背景 */}
<div className="video-storyboard-tools rounded-[20px] bg-white/[0.08] backdrop-blur-[20px] border border-white/[0.12] shadow-[0_8px_32px_rgba(0,0,0,0.3)]">
{/* 展开/收起控制区域 */}
{!noData && (
<>
{isExpanded ? (
// 展开状态:显示收起按钮和提示
<div
className="absolute top-0 bottom-0 left-0 right-0 z-[1] flex flex-col items-center justify-center rounded-[20px] bg-[#191B1E] bg-opacity-[0.3] backdrop-blur-[1.5px] cursor-pointer"
onClick={() => setIsExpanded(false)}
>
<ChevronUp className="w-4 h-4 text-white/80" />
<span className="text-sm text-white/80">
Click to act
</span>
</div>
) : (
// 收起状态:显示展开按钮
<div
className="absolute top-[-8px] left-[50%] z-[2] flex items-center justify-center w-[46px] h-4 rounded-[15px] bg-[#fff3] translate-x-[-50%] cursor-pointer hover:bg-[#ffffff1a]"
onClick={() => setIsExpanded(true)}
>
<ChevronDown className="w-4 h-4" />
</div>
)}
</>
)}
{/* 主要内容区域 - 简化层级,垂直居中 */}
<div
data-alt="+ ---------"
className={`flex items-center justify-center p-1 transition-all duration-300 relative ${
isExpanded ? "h-[2rem]" : "h-auto"
}`}
>
{/* 输入框和Action按钮 - 只在展开状态显示 */}
{!isExpanded && (
<div className="flex flex-col gap-3 w-full">
{/* 第一行:输入框 */}
<div className="video-prompt-editor relative flex flex-col gap-3 flex-1 pr-10">
{/* 文本输入框 - 改为textarea */}
<textarea
value={script}
onChange={(e) => setScript(e.target.value)}
placeholder="Describe the story you want to make..."
className="w-full pl-[10px] pr-[10px] py-[14px] rounded-[10px] leading-[20px] text-sm border-none bg-transparent text-white placeholder:text-white/[0.40] focus:outline-none resize-none min-h-[48px] max-h-[120px] overflow-y-auto"
style={
noData
? {
minHeight: "128px",
}
: {}
}
rows={1}
onInput={(e) => {
const target = e.target as HTMLTextAreaElement;
target.style.height = "auto";
target.style.height =
Math.min(target.scrollHeight, 120) + "px";
}}
/>
</div>
{/* 第二行功能按钮和Action按钮 - 同一行 */}
<div className="flex items-center justify-between">
{/* 左侧功能按钮区域 */}
<div className="flex items-center gap-1">
{/* 获取创意按钮
<Tooltip
title="Get creative ideas for your story"
placement="top"
>
<button
data-alt="get-idea-button"
className="flex items-center gap-1.5 px-3 py-2 text-white/[0.70] hover:text-white transition-colors"
onClick={() => handleGetIdea()}
>
{loadingIdea ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Lightbulb className="w-4 h-4" />
)}
</button>
</Tooltip> */}
{/* 分隔线 */}
{/* <div className="w-px h-4 bg-white/[0.20]"></div> */}
{/* 模板故事按钮 */}
<Tooltip title="Choose from movie templates" placement="top" trigger={isDesktop ? "hover" : "contextMenu"}>
<button
data-alt="template-story-button"
className="flex items-center gap-1.5 px-2 py-2 text-white/[0.70] hover:text-white transition-colors"
onClick={() => setIsTemplateModalOpen(true)}
>
<LayoutTemplate className="w-4 h-4" />
</button>
</Tooltip>
{/* 分隔线 */}
<div className="w-px h-4 bg-white/[0.20]"></div>
{/* 图片故事按钮 */}
<Tooltip title="Create movie from image" placement="top" trigger={isDesktop ? "hover" : "contextMenu"}>
<button
data-alt="photo-story-button"
className="flex items-center gap-1.5 px-2 py-2 text-white/[0.70] hover:text-white transition-colors"
onClick={() => setIsPhotoStoryModalOpen(true)}
>
<ImagePlay className="w-4 h-4" />
</button>
</Tooltip>
{isDesktop ? (
<PcPhotoStoryModal
isOpen={isPhotoStoryModalOpen}
onClose={() => setIsPhotoStoryModalOpen(false)}
configOptions={configOptions}
isCreating={isCreating}
setIsCreating={setIsCreating}
isPhotoCreating={isPhotoCreating}
setIsPhotoCreating={setIsPhotoCreating}
/>
) : (
<H5PhotoStoryDrawer
isMobile={isMobile}
isOpen={isPhotoStoryModalOpen}
onClose={() => setIsPhotoStoryModalOpen(false)}
configOptions={configOptions}
isCreating={isCreating}
setIsCreating={setIsCreating}
isPhotoCreating={isPhotoCreating}
setIsPhotoCreating={setIsPhotoCreating}
/>
)}
{/* 分隔线 */}
<div className="w-px h-4 bg-white/[0.20]"></div>
{/* 语言配置 */}
<Dropdown
overlayClassName="language-dropdown"
menu={{
items: LauguageOptions.map((option) => ({
key: option.value,
label: (
<div className={`flex items-center justify-between px-2 py-2 ${
option.value === configOptions.language
? "bg-white/[0.12] rounded-md"
: ""
}`}>
<span className="text-sm text-white">{option.label}</span>
{option.isVip && (
<Crown className="w-4 h-4 text-yellow-500" />
)}
</div>
),
})),
onClick: ({ key }) => onConfigChange('language', key),
}}
trigger={["click"]}
placement="top"
>
<button
data-alt={`config-language`}
className={`flex items-center gap-1 text-white/80 transition-all duration-200 px-2 py-2`}
>
<Globe className={"w-4 h-4"} />
<span className="text-sm">{LauguageOptions.find((option) => option.value === configOptions.language)?.code}</span>
</button>
</Dropdown>
{/* 分隔线 */}
<div className="w-px h-4 bg-white/[0.20]"></div>
{/* 剧本扩展开关 */}
<Tooltip title="Enable script expansion" placement="top" trigger={isDesktop ? "hover" : "click"}>
<div data-alt="config-expansion-mode" className="flex items-center gap-1 px-1">
{/* 仅在未选中时显示背景色,避免覆盖选中态 */}
<div className={`${configOptions.expansion_mode ? 'bg-transparent' : 'bg-white/40'} rounded-full transition-colors flex`}>
<Switch
size="small"
checked={configOptions.expansion_mode}
disabled={configOptions.videoDuration === '8s'}
onChange={(checked) => onConfigChange('expansion_mode', checked)}
/>
</div>
<span className={`text-xs text-white`}>
{configOptions.expansion_mode ? 'On' : 'Off'}
</span>
</div>
</Tooltip>
{/* 分隔线 */}
<div className="w-px h-4 bg-white/[0.20]"></div>
{/* 时长选择 */}
<Dropdown
overlayClassName="duration-dropdown"
menu={{
items: VideoDurationOptions.map((option) => ({
key: option.value,
label: (
<div className={`flex items-center justify-between px-2 py-2 ${
option.value === configOptions.videoDuration
? "bg-white/[0.12] rounded-md"
: ""
}`}>
<span className="text-sm text-white">{option.label}</span>
</div>
),
})),
onClick: ({ key }) => onConfigChange('videoDuration', key as string),
}}
trigger={["click"]}
placement="top"
>
<button
data-alt={`config-video-duration`}
className={`flex items-center gap-1 text-white/80 transition-all duration-200 px-2 py-2`}
>
<Clock className={"w-4 h-4"} />
<span className="text-sm">{configOptions.videoDuration}</span>
</button>
</Dropdown>
</div>
{/* 右侧Action按钮 */}
<ActionButton
isCreating={isCreating}
handleCreateVideo={handleCreateVideo}
icon={<Clapperboard className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`} />}
className="mr-1 mb-1"
width={isMobile ? "w-10" : "w-12"}
height={isMobile ? "h-10" : "h-12"}
/>
</div>
</div>
)}
</div>
</div>
{/* 模板故事弹窗 */}
{isDesktop ? (
<PcTemplateModal
configOptions={configOptions}
isOpen={isTemplateModalOpen}
onClose={() => setIsTemplateModalOpen(false)}
isTemplateCreating={isTemplateCreating}
setIsTemplateCreating={setIsTemplateCreating}
isRoleGenerating={isRoleGenerating}
setIsRoleGenerating={setIsRoleGenerating}
isItemGenerating={isItemGenerating}
setIsItemGenerating={setIsItemGenerating}
/>
) : (
<H5TemplateDrawer
isMobile={isMobile}
configOptions={configOptions}
isOpen={isTemplateModalOpen}
onClose={() => setIsTemplateModalOpen(false)}
isTemplateCreating={isTemplateCreating}
setIsTemplateCreating={setIsTemplateCreating}
isRoleGenerating={isRoleGenerating}
setIsRoleGenerating={setIsRoleGenerating}
isItemGenerating={isItemGenerating}
setIsItemGenerating={setIsItemGenerating}
/>
)}
</div>
);
}