From 84b6662a51740083de33315737cd563395cb465c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=B7=E9=BE=99?= Date: Tue, 19 Aug 2025 22:22:18 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=A8=A1=E6=9D=BF=E6=95=85?= =?UTF-8?q?=E4=BA=8B=E6=A8=A1=E5=BC=8F=EF=BC=8C=E6=B7=BB=E5=8A=A0=E9=9F=B3?= =?UTF-8?q?=E9=A2=91=E5=BD=95=E5=88=B6=E7=BB=84=E4=BB=B6=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=A7=92=E8=89=B2=E5=A4=B4=E5=83=8F=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E4=B8=8E=E5=88=86=E6=9E=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 90 ----- app/service/Interaction/ImageStoryService.ts | 39 ++ .../Interaction/templateStoryService.ts | 75 +++- app/service/usecase/imageStoryUseCase.ts | 143 +++++-- .../AudioRecorder.tsx | 232 ++++++----- .../{common => ChatInputBox}/ChatInputBox.tsx | 362 ++---------------- .../{common => ChatInputBox}/templateCard.tsx | 0 components/common/ActionButton.tsx | 33 ++ components/common/HighlightEditor.tsx | 139 +++++++ components/pages/create-to-video2.tsx | 2 +- 10 files changed, 535 insertions(+), 580 deletions(-) rename components/{common => ChatInputBox}/AudioRecorder.tsx (65%) rename components/{common => ChatInputBox}/ChatInputBox.tsx (77%) rename components/{common => ChatInputBox}/templateCard.tsx (100%) create mode 100644 components/common/ActionButton.tsx create mode 100644 components/common/HighlightEditor.tsx diff --git a/README.md b/README.md index 6e41111..772d19f 100644 --- a/README.md +++ b/README.md @@ -2,97 +2,7 @@ 一个基于Next.js的视频创作工具,支持多种创作模式和故事模板。 -## 故事模板组件 -### 功能特性 - -故事模板交互组件 (`renderTemplateStoryMode`) 提供了以下功能: - -#### 1. 模板列表显示 -- 从 `StoryTemplateEntity` API 请求模板数据 -- 以横向滚动图标列表形式展示多个模板选项 -- 每个模板显示预览图片和名称 -- 支持加载状态和错误处理 - -#### 2. 模板详情弹窗 -- 用户点击模板图标后弹出模态框 -- 弹窗宽度为80%,居中显示 -- 顶部布局: - - 左侧:大图片预览(40%宽度) - - 右侧:故事模板名称和提示词描述 -- 图片支持鼠标悬停动画效果(轻微缩放和旋转) - -#### 3. 角色自定义功能 -- 显示可演绎的角色列表(基于模板数据) -- 每个角色支持: - - 上传图片替换默认角色图像 - - 录制音频功能 - - 上传音频文件功能 -- 交互式按钮设计,支持悬停效果 - -#### 4. 确认操作 -- 弹窗底部提供"取消"和"确定"按钮 -- 确定按钮执行空函数(可后续替换为实际API调用) -- 支持点击遮罩层关闭弹窗 - -### 技术实现 - -#### 状态管理 -```typescript -const [templates, setTemplates] = useState([]); -const [selectedTemplate, setSelectedTemplate] = useState(null); -const [isModalOpen, setIsModalOpen] = useState(false); -const [loading, setLoading] = useState(false); -``` - -#### 核心功能 -- **模板数据获取**: 模拟API调用,支持异步加载 -- **模板选择**: 点击模板图标打开详情弹窗 -- **资源上传**: 支持图片和音频文件上传 -- **音频录制**: 预留音频录制接口 -- **响应式设计**: 使用Tailwind CSS实现现代UI - -#### 样式特点 -- 遵循现有组件设计风格 -- 使用毛玻璃效果和渐变背景 -- 支持悬停动画和过渡效果 -- 响应式布局,适配不同屏幕尺寸 - -### 使用方法 - -1. 在 `ChatInputBox` 组件中切换到 "template" 标签页 -2. 浏览横向滚动的模板列表 -3. 点击感兴趣的模板图标 -4. 在弹窗中查看模板详情和自定义角色 -5. 上传角色图片和音频资源 -6. 点击确定完成模板选择 - -### 数据结构 - -组件使用 `StoryTemplateEntity` 接口定义模板数据结构: - -```typescript -interface StoryTemplateEntity { - readonly id: string; - name: string; - imageUrl: string; - generateText: string; - storyRole: string[]; - userResources: { - role_name: string; - photo_url: string; - voice_url: string; - }[]; -} -``` - -### 扩展建议 - -- 集成真实的API接口替换模拟数据 -- 添加音频预览播放器功能 -- 实现上传进度条和状态提示 -- 支持模板收藏和最近使用功能 -- 添加模板搜索和分类筛选 ## 启动开发服务器 diff --git a/app/service/Interaction/ImageStoryService.ts b/app/service/Interaction/ImageStoryService.ts index 26101c3..0963bbc 100644 --- a/app/service/Interaction/ImageStoryService.ts +++ b/app/service/Interaction/ImageStoryService.ts @@ -60,6 +60,8 @@ interface UseImageStoryService { setCharactersAnalysis: Dispatch>; /** 设置原始用户描述 */ setOriginalUserDescription: Dispatch>; + /** 上传人物头像并分析特征,替换旧的角色数据 */ + uploadCharacterAvatarAndAnalyzeFeatures: (characterName: string) => Promise; } export const useImageStoryServiceHook = (): UseImageStoryService => { @@ -479,6 +481,42 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { console.error("创建电影项目失败:", error); } }, [hasAnalyzed, storyContent, charactersAnalysis, selectedCategory, activeImageUrl]); + + /** + * 上传人物头像并分析特征,替换旧的角色数据 + * @param {string} characterName - 角色名称 + */ + const uploadCharacterAvatarAndAnalyzeFeatures = useCallback(async (characterName: string): Promise => { + try { + setIsLoading(true); + + // 调用用例处理人物头像上传和特征分析 + const result = await imageStoryUseCase.uploadCharacterAvatarAndAnalyzeFeatures( + uploadFile + ); + + // 用新的头像和特征描述替换旧的角色数据 + setCharactersAnalysis((prev) => + prev.map((char) => + char.role_name === characterName + ? { + ...char, + crop_url: result.crop_url, + whisk_caption: result.whisk_caption + } + : char + ) + ); + + console.log("人物头像和特征描述更新成功:", result); + } catch (error) { + console.error("人物头像上传和特征分析失败:", error); + throw error; + } finally { + setIsLoading(false); + } + }, [imageStoryUseCase, uploadFile]); + return { imageStory, activeImageUrl, @@ -501,5 +539,6 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { resetImageStory, setOriginalUserDescription, actionMovie, + uploadCharacterAvatarAndAnalyzeFeatures, }; }; diff --git a/app/service/Interaction/templateStoryService.ts b/app/service/Interaction/templateStoryService.ts index 97ba20c..5e20e69 100644 --- a/app/service/Interaction/templateStoryService.ts +++ b/app/service/Interaction/templateStoryService.ts @@ -62,8 +62,81 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => { try { setIsLoading(true); - const templates = await templateStoryUseCase.getTemplateStoryList(); + // const templates = await templateStoryUseCase.getTemplateStoryList(); + + const templates = await new Promise((resolve) => { + setTimeout(() => { + resolve([ + { + id: '1', + name: '奇幻冒险故事', + imageUrl: ['/assets/3dr_howlcastle.png', '/assets/3dr_spirited.jpg'], + generateText: '一个关于勇气与友谊的奇幻冒险故事,主角在神秘世界中寻找失落的宝藏,结识了各种奇特的生物和伙伴。', + storyRole: [ + { + role_name: '艾莉娅', + photo_url: '/assets/3dr_chihiro.png', + voice_url:"" + }, + { + role_name: '魔法师梅林', + photo_url: '/assets/3dr_mono.png', + voice_url:"" + }, + { + role_name: '守护者龙', + photo_url: '/assets/3dr_howlbg.jpg', + voice_url:"" + } + ] + }, + { + id: '2', + name: '科幻探索之旅', + imageUrl: ['/assets/3dr_monobg.jpg'], + generateText: '未来世界的太空探索故事,人类在浩瀚宇宙中寻找新的家园,面对未知的挑战和外星文明的接触。', + storyRole: [ + { + role_name: '船长凯特', + photo_url: '/assets/3dr_chihiro.png', + voice_url:"" + }, + { + role_name: 'AI助手诺娃', + photo_url: '/assets/3dr_mono.png', + voice_url:"" + } + ] + }, + { + id: '3', + name: '温馨家庭喜剧', + imageUrl: ['/assets/3dr_spirited.jpg'], + generateText: '一个充满欢笑和温情的家庭故事,讲述家庭成员之间的日常趣事,以及如何一起面对生活中的小挑战。', + storyRole: [ + { + role_name: '妈妈莉莉', + photo_url: '/assets/3dr_chihiro.png', + voice_url:"" + }, + { + role_name: '爸爸汤姆', + photo_url: '/assets/3dr_mono.png', + voice_url:"" + }, + { + role_name: '孩子小杰', + photo_url: '/assets/3dr_howlbg.jpg', + voice_url:"" + } + ] + } + ]); + }, 1000); + }); + setTemplateStoryList(templates); + setSelectedTemplate(templates[0]); } catch (err) { console.error('获取模板列表失败:', err); } finally { diff --git a/app/service/usecase/imageStoryUseCase.ts b/app/service/usecase/imageStoryUseCase.ts index 9e598e9..8cb7f6f 100644 --- a/app/service/usecase/imageStoryUseCase.ts +++ b/app/service/usecase/imageStoryUseCase.ts @@ -1,6 +1,7 @@ import { ImageStoryEntity } from "../domain/Entities"; import { AIGenerateImageStory } from "@/api/movie_start"; import { MovieStartDTO, CharacterAnalysis } from "@/api/DTO/movie_start_dto"; +import { analyzeImageDescription } from "@/api/video_flow"; /** * 图片故事用例 @@ -8,7 +9,7 @@ import { MovieStartDTO, CharacterAnalysis } from "@/api/DTO/movie_start_dto"; */ export class ImageStoryUseCase { /** 当前图片故事数据 */ - imageStory: ImageStoryEntity = { + imageStory: ImageStoryEntity = { id: "", imageAnalysis: "", roleImage: [], @@ -18,23 +19,21 @@ export class ImageStoryUseCase { }; /** 故事梗概 */ - storyLogline: string = ""; + storyLogline: string = ""; /** 角色头像及名称数据 */ - charactersAnalysis: CharacterAnalysis[] = []; + charactersAnalysis: CharacterAnalysis[] = []; /** 分类数据 */ - potentialGenres: string[] = []; + potentialGenres: string[] = []; /** 是否正在分析图片 */ - isAnalyzing: boolean = false; + isAnalyzing: boolean = false; /** 是否正在上传 */ - isUploading: boolean = false; + isUploading: boolean = false; constructor() {} - - /** * 设置图片故事数据 * @param {Partial} data - 要设置的图片故事数据 @@ -71,13 +70,12 @@ export class ImageStoryUseCase { try { this.isUploading = false; // 图片已上传,设置上传状态为false this.isAnalyzing = true; - console.log('imageUrl', imageUrl) + console.log("imageUrl", imageUrl); // 设置上传后的图片URL this.setImageStory({ imageUrl }); // 调用AI分析接口 return await this.analyzeImageWithAI(); - } catch (error) { console.error("图片分析失败:", error); // 分析失败时清空图片URL @@ -92,8 +90,8 @@ export class ImageStoryUseCase { * 使用AI分析图片 * @returns {Promise} */ - async analyzeImageWithAI() { - console.log('this.imageStory.imageUrl', this.imageStory.imageUrl) + async analyzeImageWithAI() { + console.log("this.imageStory.imageUrl", this.imageStory.imageUrl); try { // // 调用AI分析接口 @@ -122,7 +120,7 @@ export class ImageStoryUseCase { * 解析并存储分析数据到类属性中 * @param {MovieStartDTO} data - AI分析返回的数据 */ - parseAndStoreAnalysisData(data: MovieStartDTO): void { + parseAndStoreAnalysisData(data: MovieStartDTO): void { // 存储故事梗概 this.storyLogline = data.story_logline || ""; @@ -137,25 +135,25 @@ export class ImageStoryUseCase { * 组合成ImageStoryEntity * @param {MovieStartDTO} data - AI分析返回的数据 */ - composeImageStoryEntity(data: MovieStartDTO): void { + composeImageStoryEntity(data: MovieStartDTO): void { // 将角色数据转换为ImageStoryEntity需要的格式 - const roleImage = data.characters_analysis?.map(character => ({ - name: character.role_name, - avatar_url: "", // 这里需要根据实际情况设置头像URL - region: { - x: character.region?.x || 0, - y: character.region?.y || 0, - width: character.region?.width || 0, - height: character.region?.height || 0, - }, - - })) || []; + const roleImage = + data.characters_analysis?.map((character) => ({ + name: character.role_name, + avatar_url: "", // 这里需要根据实际情况设置头像URL + region: { + x: character.region?.x || 0, + y: character.region?.y || 0, + width: character.region?.width || 0, + height: character.region?.height || 0, + }, + })) || []; // 更新ImageStoryEntity this.setImageStory({ ...this.imageStory, imageAnalysis: data.story_logline || "", - storyType: data.potential_genres[0] || "", // 使用第一个分类作为故事类型 + storyType: data.potential_genres[0] || "", // 使用第一个分类作为故事类型 roleImage, }); } @@ -180,14 +178,14 @@ export class ImageStoryUseCase { * @param {CharacterAnalysis[]} characters - 角色分析数据 */ processCharacterData(characters: CharacterAnalysis[]): void { - this.charactersAnalysis = characters.map(character => ({ + this.charactersAnalysis = characters.map((character) => ({ ...character, region: { x: character.region?.x || 0, y: character.region?.y || 0, width: character.region?.width || 0, height: character.region?.height || 0, - } + }, })); } @@ -196,8 +194,12 @@ export class ImageStoryUseCase { * @param {string} characterName - 角色名称 * @returns {CharacterAnalysis['region'] | null} 角色区域坐标,如果未找到则返回null */ - getCharacterRegion(characterName: string): CharacterAnalysis['region'] | null { - const character = this.charactersAnalysis.find(char => char.role_name === characterName); + getCharacterRegion( + characterName: string + ): CharacterAnalysis["region"] | null { + const character = this.charactersAnalysis.find( + (char) => char.role_name === characterName + ); return character ? character.region : null; } @@ -207,7 +209,9 @@ export class ImageStoryUseCase { * @param {string} avatarUrl - 头像URL */ updateCharacterAvatar(characterName: string, avatarUrl: string): void { - const character = this.charactersAnalysis.find(char => char.role_name === characterName); + const character = this.charactersAnalysis.find( + (char) => char.role_name === characterName + ); if (character) { // 更新角色头像URL(这里需要根据实际的数据结构来调整) // 由于CharacterAnalysis接口中没有avatar_url字段,这里只是示例 @@ -220,7 +224,80 @@ export class ImageStoryUseCase { * @returns {string[]} 角色名称数组 */ getAllCharacterNames(): string[] { - return this.charactersAnalysis.map(char => char.role_name); + return this.charactersAnalysis.map((char) => char.role_name); + } + + /** + * 上传人物头像并分析特征,替换旧的角色数据 + * @param {Function} uploadFile - 文件上传函数 + * @returns {Promise<{crop_url: string, whisk_caption: string}>} 返回新的头像URL和特征描述 + */ + async uploadCharacterAvatarAndAnalyzeFeatures( + uploadFile: (file: File) => Promise + ): Promise<{ crop_url: string; whisk_caption: string }> { + return new Promise((resolve, reject) => { + // 创建文件输入元素 + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = "image/*"; + fileInput.style.display = "none"; + + fileInput.onchange = async (e) => { + try { + const target = e.target as HTMLInputElement; + if (target.files && target.files[0]) { + const file = target.files[0]; + + // 直接在这里处理上传和分析逻辑 + try { + // 1. 上传人物头像图片 + const imageUrl = await uploadFile(file); + + // 2. 调用AI分析接口获取人物特征描述 + const analysisResult = await analyzeImageDescription({ + image_url: imageUrl, + }); + + if (!analysisResult.successful || !analysisResult.data) { + throw new Error("人物特征分析失败"); + } + + // 3. 返回新的头像URL和特征描述,用于替换旧数据 + const result = { + crop_url: imageUrl, + whisk_caption: analysisResult.data.description, + }; + + // 清理临时元素 + document.body.removeChild(fileInput); + resolve(result); + } catch (error) { + // 清理临时元素 + if (document.body.contains(fileInput)) { + document.body.removeChild(fileInput); + } + reject(error); + } + } else { + reject(new Error("未选择文件")); + } + } catch (error) { + // 清理临时元素 + if (document.body.contains(fileInput)) { + document.body.removeChild(fileInput); + } + reject(error); + } + }; + + fileInput.oncancel = () => { + document.body.removeChild(fileInput); + reject(new Error("用户取消选择")); + }; + + // 添加到DOM并触发点击 + document.body.appendChild(fileInput); + fileInput.click(); + }); } } - diff --git a/components/common/AudioRecorder.tsx b/components/ChatInputBox/AudioRecorder.tsx similarity index 65% rename from components/common/AudioRecorder.tsx rename to components/ChatInputBox/AudioRecorder.tsx index b88d33e..be8ec6e 100644 --- a/components/common/AudioRecorder.tsx +++ b/components/ChatInputBox/AudioRecorder.tsx @@ -139,11 +139,6 @@ export function AudioRecorder({ setIsPlaying(!isPlaying); }; - // 音量控制 - const toggleMute = () => { - setIsMuted(!isMuted); - }; - // 音量调节 const handleVolumeChange = (e: React.ChangeEvent) => { const newVolume = parseFloat(e.target.value); @@ -165,72 +160,77 @@ export function AudioRecorder({ <>
- {/* 头部 - 只显示关闭按钮 */} - {showCloseButton && ( -
- -
- )} + {/* 头部 - 只显示关闭按钮 */} + {showCloseButton && ( +
+ +
+ )} - {/* 主要内容区域 */} -
+ {/* 主要内容区域 */} +
{mode === "upload" ? ( // 上传模式
- -
- false} - customRequest={async ({ file, onSuccess, onError }) => { - try { - const fileObj = file as File; - console.log("开始上传文件:", fileObj.name, fileObj.type, fileObj.size); + +
+ 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); + 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"} -
-
-
-
+ // 上传成功后,调用回调函数 + 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"} +
+
+
+
) : ( // 录制模式 @@ -264,19 +264,19 @@ export function AudioRecorder({
Click to start recording
- - - + + +
)}
@@ -321,24 +321,24 @@ export function AudioRecorder({ <>
- {/* 头部 - 只显示操作按钮 */} -
- - {showCloseButton && ( - - )} -
+ {/* 头部 - 只显示操作按钮 */} +
+ + {showCloseButton && ( + + )} +
{/* WaveSurfer 波形图区域 */}
@@ -369,34 +369,20 @@ export function AudioRecorder({ {/* 音频设置 */}
-
- -
- - - {Math.round(volume * 100)}% - -
+
+ + + {Math.round(volume * 100)}% +
-
1x
diff --git a/components/common/ChatInputBox.tsx b/components/ChatInputBox/ChatInputBox.tsx similarity index 77% rename from components/common/ChatInputBox.tsx rename to components/ChatInputBox/ChatInputBox.tsx index 9e184a9..759ce5d 100644 --- a/components/common/ChatInputBox.tsx +++ b/components/ChatInputBox/ChatInputBox.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useRef, useEffect } from "react"; +import { useState, useEffect } from "react"; import { ChevronDown, ChevronUp, @@ -11,140 +11,25 @@ import { 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 { Dropdown, Modal, Tooltip, Upload, Spin } 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 { 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"; import { createMovieProjectV1 } from "@/api/video_flow"; import { useLoadScriptText, useUploadFile } from "@/app/service/domain/service"; - -// 自定义音频播放器样式 -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; - } -`; +import { ActionButton } from "../common/ActionButton"; +import { HighlightEditor } from "../common/HighlightEditor"; /**模板故事模式弹窗组件 */ const RenderTemplateStoryMode = ({ @@ -223,7 +108,7 @@ const RenderTemplateStoryMode = ({ // 模板列表渲染 const templateListRender = () => { return ( -
+

Story Templates

{templateStoryList.map((template, index) => ( @@ -255,9 +140,9 @@ const RenderTemplateStoryMode = ({
) : selectedTemplate ? ( -
+
{/* 模板信息头部 - 增加顶部空间 */} -
+
{/* 左侧图片 */}

{selectedTemplate.name}

-
+

{selectedTemplate.generateText}

@@ -293,7 +178,7 @@ const RenderTemplateStoryMode = ({
{/* 角色自定义部分 - 精简布局 */} -
+

{/* 弹窗底部操作 - 只保留 Action 按钮 */} -
+ {/*
@@ -434,6 +319,13 @@ const RenderTemplateStoryMode = ({
+
*/} +
+ } + />

) : ( @@ -448,7 +340,6 @@ const RenderTemplateStoryMode = ({ }; return ( <> - { @@ -740,38 +631,6 @@ export function ChatInputBox() { ); } -// 创建视频按钮 -const ActionButton = ({ - isCreating, - handleCreateVideo, - icon, -}: { - isCreating: boolean; - handleCreateVideo: () => void; - icon: React.ReactNode; -}) => { - return ( -
-
-
-
- -
-
-
-
- ); -}; - /** * 配置选项组件 * 提供视频创建的各种配置选项,位于输入框下方 @@ -872,135 +731,6 @@ const ConfigOptions = ({ ); }; -/** - * 角色高亮编辑器组件 - * 使用 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)) - : ""; - // 匹配新的角色标签格式 Dezhong Huang - const beforeMatch = textBefore.match(/]*>[^<]*$/); - const afterMatch = textAfter.match(/^[^>]*<\/role>/); - - // 如果光标在角色标签内,阻止输入(只允许删除操作) - 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>/g, - '$1' - ); - editor.commands.setContent(htmlContent, { emitUpdate: false }); - } - }, [content, editor]); - - return ( -
- - -
- ); -}; - /** * 图片故事弹窗组件 * 提供图片上传、AI分析和故事生成功能,支持动态UI变化 @@ -1042,6 +772,7 @@ const PhotoStoryModal = ({ setCharactersAnalysis, originalUserDescription, actionMovie, + uploadCharacterAvatarAndAnalyzeFeatures, } = useImageStoryServiceHook(); const { loadingText } = useLoadScriptText(isLoading); const { uploadFile } = useUploadFile(); @@ -1222,47 +953,12 @@ const PhotoStoryModal = ({ +
+
+
+
+ ); +} diff --git a/components/common/HighlightEditor.tsx b/components/common/HighlightEditor.tsx new file mode 100644 index 0000000..943bfb0 --- /dev/null +++ b/components/common/HighlightEditor.tsx @@ -0,0 +1,139 @@ +import Placeholder from "@tiptap/extension-placeholder"; +import { useEditor, EditorContent } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import { useEffect } from "react"; +import { HighlightTextExtension } from "../ui/main-editor/HighlightText"; + +/** + * 高亮编辑器组件 + * 使用 Tiptap 实现高亮和文本编辑功能 + * 让 文本中的xxxx标签高亮 + */ +export const HighlightEditor = ({ + content, + onContentChange, + type, + placeholder, + }: { + /** 内容 */ + content: string; + /** 内容变化回调 */ + onContentChange: (content: string) => void; + /** 标签类型*/ + type: string; + /**提示语 */ + placeholder: string; + }) => { + const editor = useEditor({ + extensions: [ + StarterKit, + HighlightTextExtension, + Placeholder.configure({ + placeholder, + emptyEditorClass: "is-editor-empty", + }), + ], + content: "", + 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)) + : ""; + const beforeMatch = textBefore.match(new RegExp(`<${type}[^>]*>[^<]*$`)); + const afterMatch = textAfter.match(new RegExp(`^[^>]*<\\/${type}>`)); + + // 只允许删除操作) + 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( + new RegExp(`<${type}[^>]*>([^<]+)<\/${type}>`, "g"), + '$1' + ); + editor.commands.setContent(htmlContent, { emitUpdate: false }); + } + }, [content, editor]); + + return ( +
+ + +
+ ); + }; diff --git a/components/pages/create-to-video2.tsx b/components/pages/create-to-video2.tsx index f488822..2022262 100644 --- a/components/pages/create-to-video2.tsx +++ b/components/pages/create-to-video2.tsx @@ -7,7 +7,7 @@ import './style/create-to-video2.css'; import { createScriptEpisodeNew, getScriptEpisodeListNew, CreateScriptEpisodeRequest } from "@/api/script_episode"; import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation2'; -import { ChatInputBox } from '@/components/common/ChatInputBox'; +import { ChatInputBox } from '@/components/ChatInputBox/ChatInputBox'; // ideaText已迁移到ChatInputBox组件中