From 188379c93c09ab7a6086db6ba0e3000f454c14fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=B7=E9=BE=99?= Date: Sat, 16 Aug 2025 21:38:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=AA=E5=AE=B3=E6=80=95=E4=BA=86=EF=BC=8C?= =?UTF-8?q?=E5=B7=AE=E7=82=B9=E4=BB=A3=E7=A0=81=E5=B0=B1=E6=B2=A1=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/movie_start.ts | 33 + app/service/Interaction/ImageStoryService.ts | 160 ++ .../Interaction/templateStoryService.ts | 82 + app/service/domain/Entities.ts | 18 +- app/service/domain/service.ts | 35 + app/service/usecase/imageStoryUseCase.ts | 153 ++ app/service/usecase/templateStoryUseCase.ts | 56 + components/common/AudioRecorder.tsx | 472 ++++ components/common/ChatInputBox.tsx | 2278 +++++++---------- components/common/PhotoStoryMode.tsx | 100 + components/common/templateCard.tsx | 264 ++ components/pages/create-to-video2.tsx | 7 +- package-lock.json | 56 +- package.json | 4 +- 14 files changed, 2280 insertions(+), 1438 deletions(-) create mode 100644 api/movie_start.ts create mode 100644 app/service/Interaction/ImageStoryService.ts create mode 100644 app/service/Interaction/templateStoryService.ts create mode 100644 app/service/usecase/imageStoryUseCase.ts create mode 100644 app/service/usecase/templateStoryUseCase.ts create mode 100644 components/common/AudioRecorder.tsx create mode 100644 components/common/PhotoStoryMode.tsx create mode 100644 components/common/templateCard.tsx diff --git a/api/movie_start.ts b/api/movie_start.ts new file mode 100644 index 0000000..99c2423 --- /dev/null +++ b/api/movie_start.ts @@ -0,0 +1,33 @@ +import { ApiResponse } from "./common"; +import { get, post } from "./request"; +import { + StoryTemplateEntity, + ImageStoryEntity, +} from "@/app/service/domain/Entities"; + +/** + * 获取故事模板列表 + */ +export const getTemplateStoryList = async () => { + return await get>("/template-story/list"); +}; + +/** + * 执行故事模板操作,生成电影项目 + */ +export const actionTemplateStory = async (template: StoryTemplateEntity) => { + return await post>( + "/template-story/action", + template + ); +}; + +/** + * AI分析图片,生成分析结果 + */ +export const AIGenerateImageStory = async (imageStory: ImageStoryEntity) => { + return await post>( + "/image-story/ai-generate", + imageStory + ); +}; diff --git a/app/service/Interaction/ImageStoryService.ts b/app/service/Interaction/ImageStoryService.ts new file mode 100644 index 0000000..c05c2f3 --- /dev/null +++ b/app/service/Interaction/ImageStoryService.ts @@ -0,0 +1,160 @@ +import { ImageStoryEntity } from "../domain/Entities"; +import { ImageStoryUseCase } from "../usecase/imageStoryUseCase"; +import { useState, useCallback, useMemo } from "react"; + +interface UseImageStoryService { + /** 当前图片故事数据 */ + imageStory: Partial; + /** 当前活跃的图片地址 */ + activeImageUrl: string; + /** 当前活跃的文本信息 */ + activeTextContent: string; + /** 当前选中的分类 */ + selectedCategory: string; + /** 是否正在分析图片 */ + isAnalyzing: boolean; + /** 是否正在上传 */ + isUploading: boolean; + /** 故事类型选项 */ + storyTypeOptions: Array<{ key: string; label: string }>; + /** 上传图片并分析 */ + uploadAndAnalyzeImage: (imageUrl: string) => Promise; + /** 触发生成剧本函数 */ + generateScript: () => Promise; + /** 更新故事类型 */ + updateStoryType: (storyType: string) => void; + /** 更新故事内容 */ + updateStoryContent: (content: string) => void; + /** 重置图片故事数据 */ + resetImageStory: () => void; +} + +export const useImageStoryServiceHook = (): UseImageStoryService => { + const [imageStory, setImageStory] = useState>({ + imageUrl: "", + imageStory: "", + storyType: "auto", + }); + const [isAnalyzing, setIsAnalyzing] = useState(false); + const [isUploading, setIsUploading] = useState(false); + + /** 图片故事用例实例 */ + const imageStoryUseCase = useMemo(() => new ImageStoryUseCase(), []); + + /** 当前活跃的图片地址 */ + const activeImageUrl = imageStory.imageUrl || ""; + + /** 当前活跃的文本信息 */ + const activeTextContent = imageStory.imageStory || ""; + + /** 当前选中的分类 */ + const selectedCategory = imageStory.storyType || "auto"; + + /** 故事类型选项 */ + const storyTypeOptions = useMemo(() => imageStoryUseCase.getStoryTypeOptions(), [imageStoryUseCase]); + + /** + * 上传图片并分析 + * @param {string} imageUrl - 已上传的图片URL + */ + const uploadAndAnalyzeImage = useCallback(async (imageUrl: string): Promise => { + try { + setIsUploading(true); + setIsAnalyzing(true); + + // 调用用例处理图片上传和分析 + await imageStoryUseCase.handleImageUpload(imageUrl); + + // 获取更新后的数据 + const updatedStory = imageStoryUseCase.getImageStory(); + setImageStory(updatedStory); + + } catch (error) { + console.error('图片上传分析失败:', error); + throw error; + } finally { + setIsUploading(false); + setIsAnalyzing(false); + } + }, [imageStoryUseCase]); + + /** + * 触发生成剧本函数 + * @returns {Promise} 生成的剧本ID或内容 + */ + const generateScript = useCallback(async (): Promise => { + if (!activeImageUrl) { + throw new Error('请先上传图片'); + } + + if (!activeTextContent) { + throw new Error('请先输入或生成故事内容'); + } + + try { + setIsAnalyzing(true); + + // 这里可以调用后端API生成剧本 + // 暂时返回一个模拟的剧本ID + const scriptId = `script_${Date.now()}`; + + // TODO: 实现实际的剧本生成逻辑 + // const response = await generateScriptFromImage(imageStory); + // return response.scriptId; + + return scriptId; + } catch (error) { + console.error('生成剧本失败:', error); + throw error; + } finally { + setIsAnalyzing(false); + } + }, [activeImageUrl, activeTextContent, imageStory]); + + /** + * 更新故事类型 + * @param {string} storyType - 新的故事类型 + */ + const updateStoryType = useCallback((storyType: string): void => { + imageStoryUseCase.updateStoryType(storyType); + setImageStory(prev => ({ ...prev, storyType })); + }, [imageStoryUseCase]); + + /** + * 更新故事内容 + * @param {string} content - 新的故事内容 + */ + const updateStoryContent = useCallback((content: string): void => { + imageStoryUseCase.updateStoryContent(content); + setImageStory(prev => ({ ...prev, imageStory: content })); + }, [imageStoryUseCase]); + + /** + * 重置图片故事数据 + */ + const resetImageStory = useCallback((): void => { + imageStoryUseCase.resetImageStory(); + setImageStory({ + imageUrl: "", + imageStory: "", + storyType: "auto", + }); + setIsAnalyzing(false); + setIsUploading(false); + }, [imageStoryUseCase]); + + return { + imageStory, + activeImageUrl, + activeTextContent, + selectedCategory, + isAnalyzing, + isUploading, + storyTypeOptions, + uploadAndAnalyzeImage, + generateScript, + updateStoryType, + updateStoryContent, + resetImageStory, + }; +}; diff --git a/app/service/Interaction/templateStoryService.ts b/app/service/Interaction/templateStoryService.ts new file mode 100644 index 0000000..fa22b75 --- /dev/null +++ b/app/service/Interaction/templateStoryService.ts @@ -0,0 +1,82 @@ +import { StoryTemplateEntity, RoleEntity } from "../domain/Entities"; +import { TemplateStoryUseCase } from "../usecase/templateStoryUseCase"; +import { getUploadToken, uploadToQiniu } from "@/api/common"; +import { useState, useCallback, useMemo } from "react"; + +interface UseTemplateStoryService { + /** 模板列表 */ + templateStoryList: StoryTemplateEntity[]; + /** 当前选中要使用的模板 */ + selectedTemplate: StoryTemplateEntity | null; + /** 当前选中的活跃的角色 */ + activeRole: RoleEntity | null; + /** 加载状态 */ + isLoading: boolean; + /** 获取模板列表函数 */ + getTemplateStoryList: () => Promise; + /** action 生成电影函数 */ + actionStory: () => Promise; + /** 设置选中的模板 */ + setSelectedTemplate: (template: StoryTemplateEntity | null) => void; + /** 设置活跃角色 */ + setActiveRole: (role: RoleEntity | null) => void; +} + +export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { + const [templateStoryList, setTemplateStoryList] = useState([]); + const [selectedTemplate, setSelectedTemplate] = useState(null); + const [activeRole, setActiveRole] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + /** 模板故事用例实例 */ + const templateStoryUseCase = useMemo(() => new TemplateStoryUseCase(), []); + + /** + * 获取模板列表函数 + */ + const getTemplateStoryList = useCallback(async (): Promise => { + try { + setIsLoading(true); + + const templates = await templateStoryUseCase.getTemplateStoryList(); + setTemplateStoryList(templates); + } catch (err) { + console.error('获取模板列表失败:', err); + } finally { + setIsLoading(false); + } + }, [templateStoryUseCase]); + + /** + * action 生成电影函数 + */ + const actionStory = useCallback(async (): Promise => { + if (!selectedTemplate) { + throw new Error('请先选择一个故事模板'); + } + + try { + setIsLoading(true); + + const projectId = await templateStoryUseCase.actionStory(selectedTemplate); + return projectId; + } catch (err) { + console.error('生成电影失败:', err); + throw err; + } finally { + setIsLoading(false); + } + }, [selectedTemplate, templateStoryUseCase]); + + + return { + templateStoryList, + selectedTemplate, + activeRole, + isLoading, + getTemplateStoryList, + actionStory, + setSelectedTemplate, + setActiveRole, + }; +}; diff --git a/app/service/domain/Entities.ts b/app/service/domain/Entities.ts index c6977cf..afe79cb 100644 --- a/app/service/domain/Entities.ts +++ b/app/service/domain/Entities.ts @@ -90,12 +90,10 @@ export interface ImageStoryEntity { readonly id: string; /** 图片URL */ imageUrl: string; - /** 图片故事内容 */ + /** 图片故事用户描述 */ imageStory: string; - /** 图片故事剧本 */ - imageScript: string; - /** 故事涉及的角色 */ - storyRole: RoleEntity[]; + /** 图片故事分析结果 */ + imageAnalysis: string; /** 故事分类 */ storyType: string; } @@ -109,14 +107,12 @@ export interface StoryTemplateEntity { /** 故事模板名称 */ name: string; /** 故事模板图片 */ - imageUrl: string; - /** 故事模板提示词 */ + imageUrl: string[]; + /** 故事模板概览*/ generateText: string; /**故事角色 */ - storyRole: string[]; - /**用户自定义演绎资源 */ - userResources: { - /**对应角色名 */ + storyRole: { + /**角色名 */ role_name: string; /**照片URL */ photo_url: string; diff --git a/app/service/domain/service.ts b/app/service/domain/service.ts index 05f0007..647f1b6 100644 --- a/app/service/domain/service.ts +++ b/app/service/domain/service.ts @@ -1,4 +1,6 @@ +import { getUploadToken, uploadToQiniu } from "@/api/common"; +import { useState, useCallback } from "react"; import { ScriptEditKey } from "../usecase/ScriptEditUseCase"; /** * 渲染数据转换器 @@ -27,4 +29,37 @@ export function parseScriptBlock( +/** + * 用于上传文件到七牛云的自定义 Hook + * @returns {object} - 包含上传函数和加载状态 + */ +export function useUploadFile() { + /** 加载状态 */ + const [isUploading, setIsUploading] = useState(false); + /** + * 上传文件到七牛云 + * @param {File} file - 要上传的文件 + * @param {(progress: number) => void} [onProgress] - 上传进度回调 + * @returns {Promise} - 上传后文件的 URL + * @throws {Error} - 上传失败时抛出异常 + */ + const uploadFile = useCallback( + async (file: File, onProgress?: (progress: number) => void): Promise => { + try { + setIsUploading(true); + const { token } = await getUploadToken(); + const fileUrl = await uploadToQiniu(file, token, onProgress); + return fileUrl; + } catch (err) { + console.error('文件上传失败:', err); + throw err; + } finally { + setIsUploading(false); + } + }, + [] + ); + + return { uploadFile, isUploading }; +} diff --git a/app/service/usecase/imageStoryUseCase.ts b/app/service/usecase/imageStoryUseCase.ts new file mode 100644 index 0000000..30b314d --- /dev/null +++ b/app/service/usecase/imageStoryUseCase.ts @@ -0,0 +1,153 @@ +import { ImageStoryEntity } from "../domain/Entities"; +import { AIGenerateImageStory } from "@/api/movie_start"; + +/** + * 图片故事用例 + * 负责管理图片故事模式的业务逻辑,包括图片上传、AI分析和故事生成 + */ +export class ImageStoryUseCase { + /** 当前图片故事数据 */ + private imageStory: Partial = { + imageUrl: "", + imageStory: "", + storyType: "auto", + }; + + /** 是否正在分析图片 */ + private isAnalyzing: boolean = false; + + /** 是否正在上传 */ + private isUploading: boolean = false; + + constructor() {} + + /** + * 获取当前图片故事数据 + * @returns {Partial} 图片故事数据 + */ + getImageStory(): Partial { + return { ...this.imageStory }; + } + + /** + * 获取分析状态 + * @returns {boolean} 是否正在分析 + */ + getAnalyzingStatus(): boolean { + return this.isAnalyzing; + } + + /** + * 获取上传状态 + * @returns {boolean} 是否正在上传 + */ + getUploadingStatus(): boolean { + return this.isUploading; + } + + /** + * 设置图片故事数据 + * @param {Partial} data - 要设置的图片故事数据 + */ + setImageStory(data: Partial): void { + this.imageStory = { ...this.imageStory, ...data }; + } + + /** + * 重置图片故事数据 + */ + resetImageStory(): void { + this.imageStory = { + imageUrl: "", + imageStory: "", + storyType: "auto", + }; + this.isAnalyzing = false; + this.isUploading = false; + } + + /** + * 处理图片上传 + * @param {string} imageUrl - 已上传的图片URL + * @returns {Promise} + */ + async handleImageUpload(imageUrl: string): Promise { + try { + this.isUploading = false; // 图片已上传,设置上传状态为false + this.isAnalyzing = true; + + // 设置上传后的图片URL + this.setImageStory({ imageUrl }); + + // 调用AI分析接口 + await this.analyzeImageWithAI(); + + } catch (error) { + console.error("图片分析失败:", error); + // 分析失败时清空图片URL + this.setImageStory({ imageUrl: "" }); + throw error; + } finally { + this.isAnalyzing = false; + } + } + + /** + * 使用AI分析图片 + * @returns {Promise} + */ + private async analyzeImageWithAI(): Promise { + try { + // 调用AI分析接口 + const response = await AIGenerateImageStory(this.imageStory as ImageStoryEntity); + + if (response.successful && response.data) { + const { imageAnalysis, category } = response.data; + + // 更新分析结果和分类 + this.setImageStory({ + imageAnalysis, + storyType: category || "auto", + imageStory: imageAnalysis, // 将AI分析结果作为默认故事内容 + }); + } else { + throw new Error("AI分析失败"); + } + } catch (error) { + console.error("AI分析失败:", error); + throw error; + } + } + + /** + * 更新故事类型 + * @param {string} storyType - 新的故事类型 + */ + updateStoryType(storyType: string): void { + this.setImageStory({ storyType }); + } + + /** + * 更新故事内容 + * @param {string} storyContent - 新的故事内容 + */ + updateStoryContent(storyContent: string): void { + this.setImageStory({ imageStory: storyContent }); + } + + /** + * 获取故事类型选项 + * @returns {Array<{key: string, label: string}>} 故事类型选项数组 + */ + getStoryTypeOptions(): Array<{ key: string; label: string }> { + return [ + { key: "auto", label: "Auto" }, + { key: "adventure", label: "Adventure" }, + { key: "romance", label: "Romance" }, + { key: "mystery", label: "Mystery" }, + { key: "fantasy", label: "Fantasy" }, + { key: "comedy", label: "Comedy" }, + ]; + } +} + diff --git a/app/service/usecase/templateStoryUseCase.ts b/app/service/usecase/templateStoryUseCase.ts new file mode 100644 index 0000000..77ce594 --- /dev/null +++ b/app/service/usecase/templateStoryUseCase.ts @@ -0,0 +1,56 @@ +import { StoryTemplateEntity } from "../domain/Entities"; +import { getTemplateStoryList, actionTemplateStory } from "@/api/movie_start"; + +/** + * 模板故事用例 + * 负责管理故事模板的获取与操作 + */ +export class TemplateStoryUseCase { + /** 故事模板列表 */ + templateStoryList: StoryTemplateEntity[] = []; + /** 当前选中的故事模板 */ + selectedTemplate: StoryTemplateEntity | null = null; + + constructor() {} + + /** + * 获取故事模板列表 + * @returns {Promise} - 故事模板实体数组 + */ + async getTemplateStoryList(): Promise { + try { + const response = await getTemplateStoryList(); + if (response.successful && response.data) { + this.templateStoryList = response.data; + return response.data; + } + throw new Error(response.message || '获取故事模板列表失败'); + } catch (error) { + console.error('获取故事模板列表失败:', error); + throw error; + } + } + + /** + * 执行故事模板相关操作 + * @param {StoryTemplateEntity} template - 选中的故事模板 + * @returns {Promise} - 项目id + */ + async actionStory(template: StoryTemplateEntity): Promise { + try { + if (!template) { + throw new Error('故事模板不能为空'); + } + + const response = await actionTemplateStory(template); + if (response.successful && response.data) { + this.selectedTemplate = template; + return response.data.projectId; + } + throw new Error(response.message || '执行故事模板操作失败'); + } catch (error) { + console.error('执行故事模板操作失败:', error); + throw error; + } + } +} diff --git a/components/common/AudioRecorder.tsx b/components/common/AudioRecorder.tsx new file mode 100644 index 0000000..b88d33e --- /dev/null +++ b/components/common/AudioRecorder.tsx @@ -0,0 +1,472 @@ +"use client"; + +import React, { useState, useRef, useEffect } from "react"; +import { Mic, MicOff, Upload, Play, Pause, Trash2, X } from "lucide-react"; +import { Tooltip, Upload as AntdUpload } from "antd"; +import { InboxOutlined } from "@ant-design/icons"; +import WaveSurfer from "wavesurfer.js"; + +import { useUploadFile } from "../../app/service/domain/service"; + +// 自定义样式 +const audioRecorderStyles = ` + .slider { + -webkit-appearance: none; + appearance: none; + background: transparent; + cursor: pointer; + } + + .slider::-webkit-slider-track { + background: rgba(255, 255, 255, 0.2); + height: 4px; + border-radius: 2px; + } + + .slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + background: #3b82f6; + height: 12px; + width: 12px; + border-radius: 50%; + cursor: pointer; + transition: all 0.2s ease; + } + + .slider::-webkit-slider-thumb:hover { + background: #2563eb; + transform: scale(1.1); + } + + .slider::-moz-range-track { + background: rgba(255, 255, 255, 0.2); + height: 4px; + border-radius: 2px; + border: none; + } + + .slider::-moz-range-thumb { + background: #3b82f6; + height: 12px; + width: 12px; + border-radius: 50%; + cursor: pointer; + border: none; + transition: all 0.2s ease; + } + + .slider::-moz-range-thumb:hover { + background: #2563eb; + transform: scale(1.1); + } +`; + +interface AudioRecorderProps { + /** 当前音频URL */ + audioUrl?: string; + /** 录制完成回调 */ + onAudioRecorded: (audioBlob: Blob, audioUrl: string) => void; + /** 删除音频回调 */ + onAudioDeleted: () => void; + /** 组件标题 */ + title?: string; + /** 是否显示关闭按钮 */ + showCloseButton?: boolean; + /** 关闭回调 */ + onClose?: () => void; +} + +/** + * 音频录制组件,支持录制、上传和播放功能 + */ +export function AudioRecorder({ + audioUrl, + onAudioRecorded, + onAudioDeleted, + title = "请上传参考音频", + showCloseButton = false, + onClose, +}: AudioRecorderProps) { + const [mode, setMode] = useState<"upload" | "record">("upload"); // 当前模式:上传或录制 + const [isRecording, setIsRecording] = useState(false); + const [isPlaying, setIsPlaying] = useState(false); + const [volume, setVolume] = useState(1); + const [isMuted, setIsMuted] = useState(false); + const { uploadFile, isUploading } = useUploadFile(); + + const mediaRecorderRef = useRef(null); + const chunksRef = useRef([]); + + // 开始录制 + const startRecording = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + const mediaRecorder = new MediaRecorder(stream); + mediaRecorderRef.current = mediaRecorder; + chunksRef.current = []; + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + chunksRef.current.push(event.data); + } + }; + + mediaRecorder.onstop = () => { + const audioBlob = new Blob(chunksRef.current, { type: "audio/wav" }); + const audioUrl = URL.createObjectURL(audioBlob); + onAudioRecorded(audioBlob, audioUrl); + stream.getTracks().forEach((track) => track.stop()); + }; + + mediaRecorder.start(); + setIsRecording(true); + } catch (error) { + console.error("录制失败:", error); + } + }; + + // 停止录制 + const stopRecording = () => { + if (mediaRecorderRef.current && isRecording) { + mediaRecorderRef.current.stop(); + setIsRecording(false); + } + }; + + // 播放/暂停控制 + const togglePlay = () => { + setIsPlaying(!isPlaying); + }; + + // 音量控制 + const toggleMute = () => { + setIsMuted(!isMuted); + }; + + // 音量调节 + const handleVolumeChange = (e: React.ChangeEvent) => { + const newVolume = parseFloat(e.target.value); + setVolume(newVolume); + if (isMuted && newVolume > 0) { + setIsMuted(false); + } + }; + + // 删除音频 + const handleDelete = () => { + setIsPlaying(false); + onAudioDeleted(); + }; + + // 渲染上传/录制状态 + if (!audioUrl) { + return ( + <> + +
+ {/* 头部 - 只显示关闭按钮 */} + {showCloseButton && ( +
+ +
+ )} + + {/* 主要内容区域 */} +
+ {mode === "upload" ? ( + // 上传模式 +
+ +
+ false} + customRequest={async ({ file, onSuccess, onError }) => { + try { + const fileObj = file as File; + console.log("开始上传文件:", fileObj.name, fileObj.type, fileObj.size); + + if (fileObj && fileObj.type.startsWith("audio/")) { + // 使用 hook 上传文件到七牛云 + console.log("调用 uploadFile hook..."); + const uploadedUrl = await uploadFile(fileObj); + console.log("上传成功,URL:", uploadedUrl); + + // 上传成功后,调用回调函数 + onAudioRecorded(fileObj, uploadedUrl); + onSuccess?.(uploadedUrl); + } else { + console.log("文件类型不是音频:", fileObj?.type); + const error = new Error("文件类型不是音频文件"); + onError?.(error); + } + } catch (error) { + console.error("上传失败:", error); + // 上传失败时直接报告错误,不使用本地文件作为备选 + onError?.(error as Error); + } + }} + showUploadList={false} + className="bg-transparent border-dashed border-white/20 hover:border-white/40" + disabled={isUploading} + > +
+ +
+
+ {isUploading + ? "Uploading..." + : "Drag audio file here or click to upload"} +
+
+
+
+
+ ) : ( + // 录制模式 +
+ {isRecording ? ( + // 录制中状态 +
+
+ +
Recording...
+
+ + {/* 录制状态指示器 */} +
+
+
+ Recording... +
+
+
+ ) : ( + // 录制准备状态 +
+
🎙️
+
+ Click to start recording +
+ + + +
+ )} +
+ )} +
+ + {/* 底部模式切换图标 */} +
+ + + + + + + +
+
+ + ); + } + + // 渲染播放状态 + return ( + <> + +
+ {/* 头部 - 只显示操作按钮 */} +
+ + {showCloseButton && ( + + )} +
+ + {/* WaveSurfer 波形图区域 */} +
+
+ +
+
+ + {/* 播放控制 */} +
+ +
+ + {/* 音频设置 */} +
+
+ +
+ + + {Math.round(volume * 100)}% + +
+
+ +
1x
+
+
+ + ); +} + +interface WaveformPlayerProps { + audioUrl: string; + isPlaying: boolean; + onPlayStateChange: (isPlaying: boolean) => void; + volume?: number; + isMuted?: boolean; +} + +function WaveformPlayer({ + audioUrl, + isPlaying, + onPlayStateChange, + volume = 1, + isMuted = false, +}: WaveformPlayerProps) { + const containerRef = useRef(null); + const wavesurferRef = useRef(null); + + useEffect(() => { + if (!containerRef.current || !audioUrl) return; + + const ws = WaveSurfer.create({ + container: containerRef.current, + waveColor: "#3b82f6", + progressColor: "#1d4ed8", + cursorColor: "#1e40af", + height: 64, + barWidth: 2, + barGap: 1, + normalize: true, + url: audioUrl, + }); + + // 监听播放状态变化 + ws.on("play", () => onPlayStateChange(true)); + ws.on("pause", () => onPlayStateChange(false)); + ws.on("finish", () => onPlayStateChange(false)); + + wavesurferRef.current = ws; + + return () => { + ws.destroy(); + }; + }, [audioUrl, onPlayStateChange]); + + // 同步外部播放状态和音量 + useEffect(() => { + if (!wavesurferRef.current) return; + + if (isPlaying && !wavesurferRef.current.isPlaying()) { + wavesurferRef.current.play(); + } else if (!isPlaying && wavesurferRef.current.isPlaying()) { + wavesurferRef.current.pause(); + } + + // 设置音量 + const currentVolume = isMuted ? 0 : volume; + wavesurferRef.current.setVolume(currentVolume); + }, [isPlaying, volume, isMuted]); + + return ( +
+
+
+ ); +} diff --git a/components/common/ChatInputBox.tsx b/components/common/ChatInputBox.tsx index 9f01c3f..7298acc 100644 --- a/components/common/ChatInputBox.tsx +++ b/components/common/ChatInputBox.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useRef, useEffect } from "react"; +import { useState, useRef } from "react"; import { ChevronDown, ChevronUp, @@ -9,23 +9,26 @@ import { Lightbulb, Package, Crown, - ArrowUp, + Clapperboard, Globe, AudioLines, - Info, Clock, - Trash2 + Trash2, + Plus, + LayoutTemplate, + ImagePlay, } from "lucide-react"; -import { Dropdown, Modal, Tooltip, Upload } from "antd"; -import { PlusOutlined, UploadOutlined } from "@ant-design/icons"; -import type { MenuProps } from "antd"; -import { ModeEnum, ResolutionEnum, VideoDurationEnum } from "@/app/model/enums"; +import { Dropdown, Modal, Tooltip, Upload, Image } from "antd"; +import { PlusOutlined, UploadOutlined, EyeOutlined } from "@ant-design/icons"; import { StoryTemplateEntity, - ImageStoryEntity, } from "@/app/service/domain/Entities"; -import AudioPlayer from "react-h5-audio-player"; -import "react-h5-audio-player/lib/styles.css"; +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 = ` @@ -49,77 +52,224 @@ const customAudioPlayerStyles = ` .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 = () => { - const [templates, setTemplates] = useState([]); - const [selectedTemplate, setSelectedTemplate] = - useState(null); - const [isModalOpen, setIsModalOpen] = useState(false); - const [loading, setLoading] = useState(false); - // 角色资源状态管理 - const [roleImages, setRoleImages] = useState<{ [key: number]: string }>({}); - const [roleAudios, setRoleAudios] = useState<{ [key: number]: string }>({}); - const [selectedRoleIndex, setSelectedRoleIndex] = useState(0); - // 模拟API请求获取模板数据 - const fetchTemplates = async () => { - setLoading(true); - try { - // 模拟API调用,实际项目中替换为真实API - const mockTemplates: StoryTemplateEntity[] = [ +/**模板故事模式弹窗组件 */ +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: [ { - id: "1", - name: "Three Little Dwarves", - imageUrl: "https://picsum.photos/200/200?random=1", - generateText: - "A story about courage and friendship, where the protagonist faces numerous challenges...", - storyRole: ["Brave Warrior", "Wise Mage", "Loyal Companion"], - userResources: [], + role_name: "艾莉娅", + photo_url: + "https://images.unsplash.com/photo-1494790108755-2616b612b786?w=200&h=200&fit=crop", + voice_url: "", }, { - id: "2", - name: "Seven Old Women", - imageUrl: "https://picsum.photos/200/200?random=2", - generateText: - "In a futuristic world where technology and humanity intertwine, exploring the unknown mysteries of the universe...", - storyRole: ["Space Explorer", "AI Assistant", "Alien Creature"], - userResources: [], + role_name: "森林守护者", + photo_url: + "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200&h=200&fit=crop", + voice_url: "", }, { - id: "3", - name: "Nine Wooden Men", - imageUrl: "https://picsum.photos/200/200?random=3", - generateText: - "A magical fairy tale world where good battles evil, leading to a beautiful ending...", - storyRole: ["Princess", "Prince", "Magician"], - userResources: [], + role_name: "魔法精灵", + photo_url: + "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200&h=200&fit=crop", + voice_url: "", }, - ]; - setTemplates(mockTemplates); - } catch (error) { - console.error("Failed to fetch templates:", error); - } finally { - setLoading(false); - } - }; + ], + }, + { + 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: "", + }, + ], + }, + ]; - useEffect(() => { - fetchTemplates(); - }, []); + // 本地状态管理 + 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); - setIsModalOpen(true); }; // 处理确认操作 - const handleConfirm = () => { - // 这里可以添加实际的API调用逻辑 - console.log("Template confirmed:", selectedTemplate); - setIsModalOpen(false); - setSelectedTemplate(null); + 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); + } }; // 处理角色图片上传 @@ -131,40 +281,13 @@ const RenderTemplateStoryMode = () => { const imageUrl = URL.createObjectURL(file); setRoleImages((prev) => ({ ...prev, [roleIndex]: imageUrl })); - // 保存到模板资源中 - const roleName = selectedTemplate.storyRole[roleIndex]; - - // 查找是否已存在该角色的资源记录 - const existingResourceIndex = selectedTemplate.userResources.findIndex( - (resource) => resource.role_name === roleName - ); - - let updatedTemplate; - if (existingResourceIndex >= 0) { - // 如果已存在,更新现有记录 - const updatedResources = [...selectedTemplate.userResources]; - updatedResources[existingResourceIndex] = { - ...updatedResources[existingResourceIndex], - photo_url: imageUrl, - }; - updatedTemplate = { - ...selectedTemplate, - userResources: updatedResources, - }; - } else { - // 如果不存在,创建新记录 - updatedTemplate = { - ...selectedTemplate, - userResources: [ - ...selectedTemplate.userResources, - { - role_name: roleName, - photo_url: imageUrl, - voice_url: "", - }, - ], - }; - } + // 直接更新模板中的角色图片 + const updatedTemplate = { + ...selectedTemplate, + storyRole: selectedTemplate.storyRole.map((role: any, index: number) => + index === roleIndex ? { ...role, photo_url: imageUrl } : role + ), + }; setSelectedTemplate(updatedTemplate); } @@ -178,764 +301,330 @@ const RenderTemplateStoryMode = () => { return newState; }); - // 同时从模板资源中删除图片 + // 同时从模板中删除图片 if (selectedTemplate) { - const roleName = selectedTemplate.storyRole[roleIndex]; - const existingResourceIndex = selectedTemplate.userResources.findIndex( - (resource) => resource.role_name === roleName - ); - - if (existingResourceIndex >= 0) { - const updatedResources = [...selectedTemplate.userResources]; - updatedResources[existingResourceIndex] = { - ...updatedResources[existingResourceIndex], - photo_url: "", - }; - - const updatedTemplate = { - ...selectedTemplate, - userResources: updatedResources, - }; - setSelectedTemplate(updatedTemplate); - } - } - }; - - // 处理音频录制 - const handleAudioRecord = async (roleIndex: number) => { - try { - // 请求麦克风权限 - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - - // 创建 MediaRecorder - const mediaRecorder = new MediaRecorder(stream); - const audioChunks: Blob[] = []; - - mediaRecorder.ondataavailable = (event) => { - audioChunks.push(event.data); + const updatedTemplate = { + ...selectedTemplate, + storyRole: selectedTemplate.storyRole.map((role: any, index: number) => + index === roleIndex ? { ...role, photo_url: "" } : role + ), }; - - mediaRecorder.onstop = () => { - // 停止所有音轨 - stream.getTracks().forEach((track) => track.stop()); - - // 创建音频 Blob - const audioBlob = new Blob(audioChunks, { type: "audio/wav" }); - const audioUrl = URL.createObjectURL(audioBlob); - - // 保存到状态 - setRoleAudios((prev) => ({ ...prev, [roleIndex]: audioUrl })); - - // 保存到模板资源中 - if (selectedTemplate) { - const roleName = selectedTemplate.storyRole[roleIndex]; - - // 查找是否已存在该角色的资源记录 - const existingResourceIndex = - selectedTemplate.userResources.findIndex( - (resource) => resource.role_name === roleName - ); - - let updatedTemplate; - if (existingResourceIndex >= 0) { - // 如果已存在,更新现有记录 - const updatedResources = [...selectedTemplate.userResources]; - updatedResources[existingResourceIndex] = { - ...updatedResources[existingResourceIndex], - voice_url: audioUrl, - }; - updatedTemplate = { - ...selectedTemplate, - userResources: updatedResources, - }; - } else { - // 如果不存在,创建新记录 - updatedTemplate = { - ...selectedTemplate, - userResources: [ - ...selectedTemplate.userResources, - { - role_name: roleName, - photo_url: "", - voice_url: audioUrl, - }, - ], - }; - } - - setSelectedTemplate(updatedTemplate); - } - }; - - // 开始录制 - mediaRecorder.start(); - - // 显示录制状态(可以添加一个录制指示器) - console.log("Started recording audio for role:", roleIndex); - - // 5秒后自动停止录制(或者可以添加手动停止按钮) - setTimeout(() => { - if (mediaRecorder.state === "recording") { - mediaRecorder.stop(); - } - }, 5000); - } catch (error) { - console.error("Failed to start recording:", error); - alert("Failed to access microphone. Please check permissions."); + setSelectedTemplate(updatedTemplate); } }; - - // 处理音频上传 - const handleAudioUpload = (roleIndex: number) => { - const input = document.createElement("input"); - input.type = "file"; - input.accept = "audio/*"; - input.onchange = (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (file) { - // 这里可以添加实际的上传逻辑 - console.log("Audio file uploaded:", file.name); - // 模拟上传成功,设置音频URL - const audioUrl = URL.createObjectURL(file); - setRoleAudios((prev) => ({ ...prev, [roleIndex]: audioUrl })); - - // 保存到模板资源中 - if (selectedTemplate) { - const roleName = selectedTemplate.storyRole[roleIndex]; - - // 查找是否已存在该角色的资源记录 - const existingResourceIndex = - selectedTemplate.userResources.findIndex( - (resource) => resource.role_name === roleName - ); - - let updatedTemplate; - if (existingResourceIndex >= 0) { - // 如果已存在,更新现有记录 - const updatedResources = [...selectedTemplate.userResources]; - updatedResources[existingResourceIndex] = { - ...updatedResources[existingResourceIndex], - voice_url: audioUrl, - }; - updatedTemplate = { - ...selectedTemplate, - userResources: updatedResources, - }; - } else { - // 如果不存在,创建新记录 - updatedTemplate = { - ...selectedTemplate, - userResources: [ - ...selectedTemplate.userResources, - { - role_name: roleName, - photo_url: "", - voice_url: audioUrl, - }, - ], - }; - } - - setSelectedTemplate(updatedTemplate); - } - } - }; - input.click(); + // 模板列表渲染 + const templateListRender = () => { + return ( +
+

Story Templates

+
+ {templates.map((template, index) => ( +
handleTemplateSelect(template)} + > + +
+ ))} +
+
+ ); }; + // 故事编辑器渲染 + const storyEditorRender = () => { + return loading ? ( +
+
+
+ ) : selectedTemplate ? ( +
+ {/* 模板信息头部 - 增加顶部空间 */} +
+ {/* 左侧图片 */} +
+
+ {selectedTemplate.name} +
+
+
- // 删除角色音频 - const handleDeleteRoleAudio = (roleIndex: number) => { - setRoleAudios((prev) => { - const newState = { ...prev }; - delete newState[roleIndex]; - return newState; - }); + {/* 右侧信息 - 增加文本渲染空间 */} +
+

+ {selectedTemplate.name} +

+
+

+ {selectedTemplate.generateText} +

+
+
+
- // 同时从模板资源中删除音频 - if (selectedTemplate) { - const roleName = selectedTemplate.storyRole[roleIndex]; - const existingResourceIndex = selectedTemplate.userResources.findIndex( - (resource) => resource.role_name === roleName - ); + {/* 角色自定义部分 - 精简布局 */} +
+

+ Character Customization +

- if (existingResourceIndex >= 0) { - const updatedResources = [...selectedTemplate.userResources]; - updatedResources[existingResourceIndex] = { - ...updatedResources[existingResourceIndex], - voice_url: "", - }; + {/* 紧凑布局 */} +
+ {/* 左侧:当前选中角色的音频与照片更改 - 精简版本 */} +
+ {/* 图片上传部分 - 精简 */} +
+
+ + 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 +
+ )} +
+
+
+
- const updatedTemplate = { - ...selectedTemplate, - userResources: updatedResources, - }; - setSelectedTemplate(updatedTemplate); - } - } + {/* 音频部分 - 精简版本 */} +
+ {/* 音频操作区域 - 使用新的 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 ( <> -
- {/* 使用 Ant Design Tooltip */} - -
- ? -
-
- {/* 模板列表 - 横向滚动 */} -
-
- {loading ? ( - // Loading state -
-
-
- ) : ( - // Template icon list - templates.map((template) => ( -
handleTemplateSelect(template)} - > - {/* 模板卡片容器 */} -
- {/* 图片区域 - 占满整个容器 */} -
- {template.name} -
-
- - {/* 鼠标悬停信息覆盖层 */} -
- {/* 故事名称 */} -

- {template.name} -

- {/* 角色数量 */} -
- {template.storyRole.length} Characters -
-
-
-
- )) - )} -
-
- - {/* 模板详情弹窗 */} - { - // 清空所有选中的内容数据 - setRoleImages({}); - setRoleAudios({}); - setSelectedTemplate(null); - setSelectedRoleIndex(0); - setIsModalOpen(false); - }} - footer={null} - width="50%" - style={{ maxWidth: "600px" }} - className="template-modal" - closeIcon={ -
- × -
- } - > - {selectedTemplate && ( -
- {/* 弹窗头部 */} -
- {/* 左侧图片 - 减小尺寸 */} -
-
- {selectedTemplate.name} -
-
-
- - {/* 右侧信息 - 垂直从上排列,支持滚动 */} -
-

- {selectedTemplate.name} -

-
-

- {selectedTemplate.generateText} -

-
-
-
- - {/* 角色自定义部分 */} -
-

- Character Customization -

- - {/* 角色Tab切换 */} -
-
- {selectedTemplate.storyRole.map((role: string, index: number) => ( - - ))} -
- - {/* 当前选中角色的自定义内容 */} - {selectedRoleIndex !== null && ( -
- {/* 图片上传部分 */} -
- {/* 图片上传提示 */} -
-
-
- -
-
-

- Character Photo -

-

- Upload a portrait photo to replace this character's appearance in the movie. -

-
-
-
- - {/* 图片上传区域 - 放大尺寸 */} -
- {roleImages[selectedRoleIndex] ? ( -
- Character - -
- ) : ( - false} - onChange={(info) => { - if (info.file.status === "done") { - handleRoleImageUpload( - selectedRoleIndex, - info.file.originFileObj - ); - } - }} - > -
- - Upload Photo -
-
- )} -
-
- - {/* 音频部分 */} -
- {/* 音频录制提示 */} -
-
-
- -
-
-

- Character Voice -

-
-

- Record your voice to replace this character's voice in the movie. - Aim for 15 seconds of clear, natural speech. -

-
-

- "The sun sets slowly behind the mountain, casting a warm glow over the calm valley." -

-
-
-
-
-
- - {/* 音频操作区域 */} -
- {/* 音频播放器或占位符 */} - {roleAudios[selectedRoleIndex] ? ( -
- - {/* 删除按钮集成到播放器中 - 更小且位于右上角 */} - -
- ) : ( -
- - No Audio Recorded - -
- )} - - {/* 音频操作按钮 - 只在没有音频时显示 */} - {!roleAudios[selectedRoleIndex] && ( -
- {/* 录制音频按钮 */} - - - - - {/* 上传音频按钮 */} - - - -
- )} -
-
-
- )} -
-
- - {/* 弹窗底部操作 - 只保留 Action 按钮 */} -
-
- {loading ? ( - <> - - Actioning... - - ) : ( - <> - - Action - - )} -
-
-
- )} - -
- - ); -}; -/**照片故事模式 */ -const RenderPhotoStoryMode = ({ - photoStory, - setPhotoStory, - hasGenerated, - setHasGenerated, -}: { - photoStory: Partial; - setPhotoStory: React.Dispatch< - React.SetStateAction> - >; - hasGenerated: boolean; - setHasGenerated: React.Dispatch>; -}) => { - const [isGenerating, setIsGenerating] = useState(false); - const [isUploading, setIsUploading] = useState(false); - - // 故事分类选项 - const storyTypes = [ - { key: "auto", label: "Auto" }, - { key: "adventure", label: "Adventure" }, - { key: "romance", label: "Romance" }, - { key: "mystery", label: "Mystery" }, - { key: "fantasy", label: "Fantasy" }, - { key: "comedy", label: "Comedy" }, - ]; - - // 处理图片上传 - const handleImageUpload = async () => { - const input = document.createElement("input"); - input.type = "file"; - input.accept = "image/*"; - input.onchange = async (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (file) { - try { - setIsUploading(true); - // 模拟图片上传,实际项目中替换为真实API - const imageUrl = URL.createObjectURL(file); - setPhotoStory((prev) => ({ ...prev, imageUrl })); - console.log("Image uploaded successfully:", file.name); - } catch (error) { - console.error("Upload error:", error); - alert("Upload failed, please try again"); - } finally { - setIsUploading(false); - } - } - }; - input.click(); - }; - - // 处理AI生成故事 - const handleGenerateStory = async () => { - if (!photoStory.imageUrl || !photoStory.imageStory) { - alert("Please upload an image and enter story inspiration first"); - return; - } - - setIsGenerating(true); - try { - // 模拟AI生成故事,实际项目中替换为真实API - await new Promise((resolve) => setTimeout(resolve, 2000)); - - const generatedStory = `Based on your image and inspiration "${photoStory.imageStory}", I've created a captivating story that captures the essence of your vision. The narrative weaves together elements of mystery and wonder, creating an engaging tale that will captivate your audience.`; - - setPhotoStory((prev) => ({ ...prev, imageStory: generatedStory })); - setHasGenerated(true); - } catch (error) { - console.error("Story generation failed:", error); - alert("Story generation failed, please try again"); - } finally { - setIsGenerating(false); - } - }; - - // 处理重置 - const handleReset = () => { - setPhotoStory({ - imageUrl: "", - imageStory: "", - storyType: "auto", - }); - setHasGenerated(false); - }; - - // 处理故事文本变化 - const handleStoryChange = (e: React.ChangeEvent) => { - setPhotoStory((prev) => ({ ...prev, imageStory: e.target.value })); - }; - - // 处理分类选择 - const handleTypeChange = (value: string) => { - setPhotoStory((prev) => ({ ...prev, storyType: value })); - }; - - return ( -
- - {/* 图片上传区域 */} -
- {/* 图片上传按钮 */} -
-
- {isUploading ? ( - - ) : photoStory.imageUrl ? ( - Story Image - ) : ( - - )} -
-
- - {isUploading - ? "Uploading..." - : photoStory.imageUrl - ? "Image" - : "Add Image"} + { + // 清空所有选中的内容数据 + setRoleImages({}); + setRoleAudios({}); + setSelectedTemplate(mockTemplates[0]); // 重置为第一个模板 + setSelectedRoleIndex(0); + onClose(); + }} + footer={null} + width="60%" + style={{ maxWidth: "800px" }} + className="template-modal" + closeIcon={ +
+ + ×
+ } + > +
+ {/* 弹窗头部 */} +
+

+ Template Story Selection +

+
+
+ {templateListRender()} +
{storyEditorRender()}
+
- - {/* 文本输入和分类选择区域 */} -
- {/* 文本输入框 */} -