diff --git a/api/DTO/movie_start_dto.ts b/api/DTO/movie_start_dto.ts index 299f01b..655e9ec 100644 --- a/api/DTO/movie_start_dto.ts +++ b/api/DTO/movie_start_dto.ts @@ -19,7 +19,7 @@ export interface CharacterAnalysis { /** 角色名称 */ role_name: string; /** 角色区域 */ - region: CharacterRegion; + region: CharacterRegion|null; /** 角色头像URL(可选,用于存储裁剪后的头像) */ avatarUrl?: string; } diff --git a/api/movie_start.ts b/api/movie_start.ts index 1753b88..50ac479 100644 --- a/api/movie_start.ts +++ b/api/movie_start.ts @@ -27,7 +27,7 @@ export const actionTemplateStory = async (template: StoryTemplateEntity) => { * AI分析图片,生成分析结果 */ export const AIGenerateImageStory = async (request: { - imageUrl: string; + image_url: string; user_text: string; }) => { return await post>( diff --git a/app/globals.css b/app/globals.css index 5e7fcab..19716a9 100644 --- a/app/globals.css +++ b/app/globals.css @@ -145,4 +145,33 @@ body { outline: none !important; outline-offset: 0 !important; box-shadow: none !important; +} + +/* Tiptap 编辑器焦点样式覆盖 */ +.ProseMirror:focus, +.ProseMirror:focus-visible, +.ProseMirror-focused { + outline: none !important; + border: none !important; + box-shadow: none !important; +} + +.ProseMirror { + outline: none !important; + border: none !important; + box-shadow: none !important; +} + +/* 确保编辑器内容区域没有默认样式 */ +.ProseMirror p { + margin: 0; + padding: 0; +} + +.ProseMirror p.is-editor-empty:first-child::before { + color: rgba(255, 255, 255, 0.4); + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; } \ No newline at end of file diff --git a/app/service/Interaction/ImageStoryService.ts b/app/service/Interaction/ImageStoryService.ts index ffbb253..0b2148b 100644 --- a/app/service/Interaction/ImageStoryService.ts +++ b/app/service/Interaction/ImageStoryService.ts @@ -1,7 +1,7 @@ import { ImageStoryEntity } from "../domain/Entities"; import { useUploadFile } from "../domain/service"; import { ImageStoryUseCase } from "../usecase/imageStoryUseCase"; -import { useState, useCallback, useMemo } from "react"; +import { useState, useCallback, useMemo, Dispatch, SetStateAction } from "react"; import { CharacterAnalysis } from "@/api/DTO/movie_start_dto"; interface UseImageStoryService { @@ -24,9 +24,9 @@ interface UseImageStoryService { /** 计算后的角色头像数据 */ avatarComputed: Array<{ name: string; url: string }>; /** 上传图片并分析 */ - uploadAndAnalyzeImage: (imageUrl: string) => Promise; - /** 触发文件选择并自动分析 */ - triggerFileSelectionAndAnalyze: () => Promise; + uploadAndAnalyzeImage: () => Promise; + /** 触发文件选择 */ + triggerFileSelection: () => Promise; /** 触发生成剧本函数 */ generateScript: () => Promise; /** 更新故事类型 */ @@ -35,10 +35,11 @@ interface UseImageStoryService { updateStoryContent: (content: string) => void; /** 更新角色名称并同步到相关数据 */ updateCharacterName: (oldName: string, newName: string) => void; + /** 同步角色名称到故事内容 */ + syncRoleNameToContent: (oldName: string, newName: string) => void; /** 重置图片故事数据 */ - resetImageStory: () => void; - /** 完全重置到初始状态(包括预置数据) */ - resetToInitialState: () => void; + resetImageStory: (showAnalysisState?: boolean) => void; + setCharactersAnalysis: Dispatch> } export const useImageStoryServiceHook = (): UseImageStoryService => { @@ -49,62 +50,25 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { }); // 图片相关状态 - const [activeImageUrl, setActiveImageUrl] = useState("https://cdn.qikongjian.com/1755316261_gmw4yq.mp4?vframe/jpg/offset/1"); + const [activeImageUrl, setActiveImageUrl] = useState(""); - // 故事内容状态(统一管理用户输入和AI分析结果),预置假数据 - const [storyContent, setStoryContent] = useState( - "在一个遥远的未来城市,机器人与人类共存,但一场突如其来的危机打破了平衡。艾米丽,阿尔法,博士互相帮助,共同解决危机。" - ); + // 故事内容状态(统一管理用户输入和AI分析结果) + const [storyContent, setStoryContent] = useState(""); // 分析结果状态 - /** 角色头像及名称,预置假数据 */ + /** 角色头像及名称 */ const [charactersAnalysis, setCharactersAnalysis] = useState< CharacterAnalysis[] - >([ - { - role_name: "艾米丽", - region: { - x: 0.2, - y: 0.2, - width: 0.2, - height: 0.2, - }, - id: "1" - }, - { - role_name: "阿尔法", - region: { - x: 0.4, - y: 0.4, - width: 0.2, - height: 0.2, - }, - id: "2" - }, - { - role_name: "博士", - region: { - x: 0.6, - y: 0.6, - width: 0.2, - height: 0.2, - }, - id: "3" - }, - ]); - /** 分类数组,预置假数据 */ - const [potentialGenres, setPotentialGenres] = useState([ - "科幻", - "冒险", - "悬疑", - ]); + >([]); + /** 分类数组 */ + const [potentialGenres, setPotentialGenres] = useState([]); // 分类状态 const [selectedCategory, setSelectedCategory] = useState("Auto"); // 流程状态 const [isLoading, setIsLoading] = useState(false); - const [hasAnalyzed, setHasAnalyzed] = useState(true); + const [hasAnalyzed, setHasAnalyzed] = useState(false); // 使用上传文件Hook const { uploadFile } = useUploadFile(); @@ -112,93 +76,111 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { /** 图片故事用例实例 */ const imageStoryUseCase = useMemo(() => new ImageStoryUseCase(), []); - /** + /** * 根据角色区域信息生成头像URL * @param character - 角色信息 * @param imageUrl - 源图片URL */ - const generateAvatarFromRegion = useCallback((character: CharacterAnalysis, imageUrl: string) => { - // 创建图片对象 - const img = new Image(); - img.crossOrigin = 'anonymous'; // 处理跨域问题 - - img.onload = () => { - try { - // 根据百分比计算实际的像素坐标 - const cropX = Math.round(character.region.x * img.width); - const cropY = Math.round(character.region.y * img.height); - const cropWidth = Math.round(character.region.width * img.width); - const cropHeight = Math.round(character.region.height * img.height); - console.log(cropX, cropY, cropWidth, cropHeight); - - - // 验证裁剪区域是否有效 - if (cropWidth <= 0 || cropHeight <= 0) { - console.error('裁剪区域无效:', { cropWidth, cropHeight }); - return; - } - - if (cropX + cropWidth > img.width || cropY + cropHeight > img.height) { - console.error('裁剪区域超出图片边界'); - return; - } - - // 创建canvas元素用于图片裁剪 - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - - if (!ctx) { - console.error('无法创建canvas上下文'); - return; - } - - // 设置canvas尺寸为裁剪后的尺寸 - canvas.width = cropWidth; - canvas.height = cropHeight; - - // 清除canvas内容 - ctx.clearRect(0, 0, cropWidth, cropHeight); - - // 在canvas上绘制裁剪后的图片部分 - ctx.drawImage( - img, - cropX, cropY, cropWidth, cropHeight, // 源图片裁剪区域 - 0, 0, cropWidth, cropHeight // 目标canvas区域 - ); - - // 将canvas转换为blob并创建临时URL - canvas.toBlob((blob) => { - if (blob) { - const url = URL.createObjectURL(blob); - console.log('成功生成头像URL:', url, '大小:', blob.size); - - // 更新角色头像URL - setCharactersAnalysis(prev => - prev.map(char => - char.role_name === character.role_name - ? { ...char, avatarUrl: url } - : char - ) - ); - } else { - console.error('Canvas转Blob失败'); - } - }, 'image/jpeg', 0.9); - - // 清理canvas - canvas.remove(); - } catch (error) { - console.error('生成角色头像失败:', error); + const generateAvatarFromRegion = useCallback( + (character: CharacterAnalysis, imageUrl: string) => { + if (!character.region || !character.region.width || !character.region.height) { + return; } - }; + // 创建图片对象 + const img = new Image(); + img.crossOrigin = "anonymous"; // 处理跨域问题 - img.onerror = () => { - console.error('加载图片失败:', imageUrl); - }; + img.onload = () => { + try { + // 根据百分比计算实际的像素坐标 + const cropX = Math.round(character.region!.x * img.width); + const cropY = Math.round(character.region!.y * img.height); + const cropWidth = Math.round(character.region!.width * img.width); + const cropHeight = Math.round(character.region!.height * img.height); + console.log(cropX, cropY, cropWidth, cropHeight); - // 开始加载图片 - img.src = imageUrl; - }, [setCharactersAnalysis]); + // 验证裁剪区域是否有效 + if (cropWidth <= 0 || cropHeight <= 0) { + console.error("裁剪区域无效:", { cropWidth, cropHeight }); + return; + } + + if ( + cropX + cropWidth > img.width || + cropY + cropHeight > img.height + ) { + console.error("裁剪区域超出图片边界"); + return; + } + + // 创建canvas元素用于图片裁剪 + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + if (!ctx) { + console.error("无法创建canvas上下文"); + return; + } + + // 设置canvas尺寸为裁剪后的尺寸 + canvas.width = cropWidth; + canvas.height = cropHeight; + + // 清除canvas内容 + ctx.clearRect(0, 0, cropWidth, cropHeight); + + // 在canvas上绘制裁剪后的图片部分 + ctx.drawImage( + img, + cropX, + cropY, + cropWidth, + cropHeight, // 源图片裁剪区域 + 0, + 0, + cropWidth, + cropHeight // 目标canvas区域 + ); + + // 将canvas转换为blob并创建临时URL + canvas.toBlob( + (blob) => { + if (blob) { + const url = URL.createObjectURL(blob); + console.log("成功生成头像URL:", url, "大小:", blob.size); + + // 更新角色头像URL + setCharactersAnalysis((prev) => + prev.map((char) => + char.role_name === character.role_name + ? { ...char, avatarUrl: url } + : char + ) + ); + } else { + console.error("Canvas转Blob失败"); + } + }, + "image/jpeg", + 0.9 + ); + + // 清理canvas + canvas.remove(); + } catch (error) { + console.error("生成角色头像失败:", error); + } + }; + + img.onerror = () => { + console.error("加载图片失败:", imageUrl); + }; + + // 开始加载图片 + img.src = imageUrl; + }, + [setCharactersAnalysis] + ); /** * 根据角色框选数据计算头像URL @@ -223,7 +205,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { return { name: character.role_name, - url: '', // 初始为空,异步生成完成后会更新 + url: "", // 初始为空,异步生成完成后会更新 }; }); }, [charactersAnalysis, activeImageUrl, generateAvatarFromRegion]); @@ -232,18 +214,19 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { * @param {string} imageUrl - 已上传的图片URL */ const uploadAndAnalyzeImage = useCallback( - async (imageUrl: string): Promise => { + async (): Promise => { try { + console.log('123123123', 123123123) setIsLoading(true); // 调用用例处理图片上传和分析 - await imageStoryUseCase.handleImageUpload(imageUrl); + await imageStoryUseCase.handleImageUpload(activeImageUrl); // 获取更新后的数据 - const updatedStory = imageStoryUseCase.getStoryLogline(); - const updatedCharacters = imageStoryUseCase.getCharactersAnalysis(); - const updatedGenres = imageStoryUseCase.getPotentialGenres(); - const updatedImageStory = imageStoryUseCase.getImageStory(); + const updatedStory = imageStoryUseCase.storyLogline; + const updatedCharacters = imageStoryUseCase.charactersAnalysis; + const updatedGenres = imageStoryUseCase.potentialGenres; + const updatedImageStory = imageStoryUseCase.imageStory; // 更新所有响应式状态 setCharactersAnalysis(updatedCharacters); @@ -251,12 +234,8 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { setImageStory(updatedImageStory); // 将AI分析的故事内容直接更新到统一的故事内容字段 - setStoryContent(updatedStory || ""); - - // 设置第一个分类为默认选中 - if (updatedGenres.length > 0) { - setSelectedCategory(updatedGenres[0]); - } + updateStoryContent(updatedStory || ""); + setSelectedCategory("Auto"); // 标记已分析 setHasAnalyzed(true); @@ -267,7 +246,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { setIsLoading(false); } }, - [imageStoryUseCase] + [activeImageUrl, imageStoryUseCase] ); /** @@ -323,30 +302,47 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { */ const updateStoryContent = useCallback((content: string): void => { setStoryContent(content); - }, []); + imageStoryUseCase.updateStoryContent(content); + }, [imageStoryUseCase]); + + /** + * 同步角色名称到故事内容 + * @param {string} oldName - 旧的角色名称 + * @param {string} newName - 新的角色名称 + */ + const syncRoleNameToContent = useCallback( + (oldName: string, newName: string) => { + // 更新故事内容中的角色标签 + setStoryContent((prev) => { + const regex = new RegExp(`${oldName}<\/role_name>`, "g"); + const content = prev.replace(regex, `${newName}`); + imageStoryUseCase.updateStoryContent(content); + return content; + }); + + }, + [imageStoryUseCase] + ); /** * 更新角色名称并同步到相关数据 * @param {string} oldName - 旧的角色名称 * @param {string} newName - 新的角色名称 */ - const updateCharacterName = useCallback((oldName: string, newName: string): void => { - // 更新角色分析数据中的名称 - setCharactersAnalysis(prev => - prev.map(char => - char.role_name === oldName - ? { ...char, role_name: newName } - : char - ) - ); + const updateCharacterName = useCallback( + (oldName: string, newName: string): void => { + // 更新角色分析数据中的名称 + setCharactersAnalysis((prev) => + prev.map((char) => + char.role_name === oldName ? { ...char, role_name: newName } : char + ) + ); - // 同步更新故事内容中的角色名称 - setStoryContent(prev => { - // 使用正则表达式进行全局替换,确保大小写匹配 - const regex = new RegExp(`\\b${oldName}\\b`, 'g'); - return prev.replace(regex, newName); - }); - }, []); + // 同步更新故事内容中的角色名称 + syncRoleNameToContent(oldName, newName); + }, + [syncRoleNameToContent] + ); /** * 重置图片故事数据 @@ -355,8 +351,8 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { imageStoryUseCase.resetImageStory(); // 清理生成的头像URL,避免内存泄漏 - setCharactersAnalysis(prev => { - prev.forEach(char => { + setCharactersAnalysis((prev) => { + prev.forEach((char) => { if (char.avatarUrl) { URL.revokeObjectURL(char.avatarUrl); } @@ -370,76 +366,17 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { storyType: "auto", }); setActiveImageUrl(""); - setStoryContent(""); + updateStoryContent(""); setPotentialGenres([]); setSelectedCategory("auto"); setHasAnalyzed(false); setIsLoading(false); }, [imageStoryUseCase]); - /** - * 完全重置到初始状态(包括预置数据) - */ - const resetToInitialState = useCallback((): void => { - // 清理生成的头像URL,避免内存泄漏 - setCharactersAnalysis(prev => { - prev.forEach(char => { - if (char.avatarUrl) { - URL.revokeObjectURL(char.avatarUrl); - } - }); - return []; - }); - - // 重置所有状态到初始值 - setImageStory({ - imageUrl: "", - storyType: "auto", - }); - setActiveImageUrl("https://cdn.qikongjian.com/1755316261_gmw4yq.mp4?vframe/jpg/offset/1"); - setStoryContent("在一个遥远的未来城市,机器人与人类共存,但一场突如其来的危机打破了平衡。艾米丽,阿尔法,博士互相帮助,共同解决危机。"); - setCharactersAnalysis([ - { - role_name: "艾米丽", - region: { - x: 20, - y: 20, - width: 20, - height: 20, - }, - id: "1" - }, - { - role_name: "阿尔法", - region: { - x: 40, - y: 40, - width: 20, - height: 20, - }, - id: "2" - }, - { - role_name: "博士", - region: { - x: 60, - y: 60, - width: 20, - height: 20, - }, - id: "3" - }, - ]); - setPotentialGenres(["科幻", "冒险", "悬疑"]); - setSelectedCategory("auto"); - setHasAnalyzed(true); - setIsLoading(false); - }, []); - /** * 触发文件选择并自动分析 */ - const triggerFileSelectionAndAnalyze = + const triggerFileSelection = useCallback(async (): Promise => { return new Promise((resolve, reject) => { // 创建文件输入元素 @@ -469,8 +406,6 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { imageUrl: uploadedImageUrl, })); - // 自动开始分析 - await uploadAndAnalyzeImage(uploadedImageUrl); } resolve(); } catch (error) { @@ -490,7 +425,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { document.body.appendChild(fileInput); fileInput.click(); }); - }, [uploadFile, uploadAndAnalyzeImage]); + }, [uploadFile]); return { imageStory, @@ -502,13 +437,14 @@ export const useImageStoryServiceHook = (): UseImageStoryService => { isLoading, hasAnalyzed, avatarComputed, + setCharactersAnalysis, uploadAndAnalyzeImage, - triggerFileSelectionAndAnalyze, + triggerFileSelection, generateScript, updateStoryType, updateStoryContent, updateCharacterName, + syncRoleNameToContent, resetImageStory, - resetToInitialState, }; }; diff --git a/app/service/domain/Entities.ts b/app/service/domain/Entities.ts index 9fb5941..b7e9f06 100644 --- a/app/service/domain/Entities.ts +++ b/app/service/domain/Entities.ts @@ -112,7 +112,7 @@ export interface ImageStoryEntity { width: number; /** 高度 */ height: number; - }; + } | null; }[]; } /** diff --git a/app/service/usecase/imageStoryUseCase.ts b/app/service/usecase/imageStoryUseCase.ts index 812f4ca..8216d23 100644 --- a/app/service/usecase/imageStoryUseCase.ts +++ b/app/service/usecase/imageStoryUseCase.ts @@ -11,7 +11,7 @@ export class ImageStoryUseCase { imageStory: Partial = { imageUrl: "", imageStory: "", - storyType: "auto", + storyType: "Auto", }; /** 故事梗概 */ @@ -31,53 +31,7 @@ export class ImageStoryUseCase { constructor() {} - /** - * 获取当前图片故事数据 - * @returns {Partial} 图片故事数据 - */ - getImageStory(): Partial { - return { ...this.imageStory }; - } - /** - * 获取故事梗概 - * @returns {string} 故事梗概 - */ - getStoryLogline(): string { - return this.storyLogline; - } - - /** - * 获取角色分析数据 - * @returns {CharacterAnalysis[]} 角色分析数据 - */ - getCharactersAnalysis(): CharacterAnalysis[] { - return [...this.charactersAnalysis]; - } - - /** - * 获取分类数据 - * @returns {string[]} 分类数据 - */ - getPotentialGenres(): string[] { - return [...this.potentialGenres]; - } - - /** - * 获取分析状态 - * @returns {boolean} 是否正在分析 - */ - getAnalyzingStatus(): boolean { - return this.isAnalyzing; - } - - /** - * 获取上传状态 - * @returns {boolean} 是否正在上传 - */ - getUploadingStatus(): boolean { - return this.isUploading; - } /** * 设置图片故事数据 @@ -94,7 +48,7 @@ export class ImageStoryUseCase { this.imageStory = { imageUrl: "", imageStory: "", - storyType: "auto", + storyType: "Auto", }; this.storyLogline = ""; this.charactersAnalysis = []; @@ -112,7 +66,7 @@ export class ImageStoryUseCase { try { this.isUploading = false; // 图片已上传,设置上传状态为false this.isAnalyzing = true; - + console.log('imageUrl', imageUrl) // 设置上传后的图片URL this.setImageStory({ imageUrl }); @@ -134,10 +88,11 @@ export class ImageStoryUseCase { * @returns {Promise} */ async analyzeImageWithAI(): Promise { + console.log('this.imageStory.imageUrl', this.imageStory.imageUrl) try { // 调用AI分析接口 const response = await AIGenerateImageStory({ - imageUrl: this.imageStory.imageUrl || "", + image_url: this.imageStory.imageUrl || "", user_text: this.imageStory.imageStory || "", }); @@ -181,17 +136,17 @@ export class ImageStoryUseCase { name: character.role_name, avatar_url: "", // 这里需要根据实际情况设置头像URL region: { - x: character.region.x, - y: character.region.y, - width: character.region.width, - height: character.region.height, + x: character.region?.x || 0, + y: character.region?.y || 0, + width: character.region?.width || 0, + height: character.region?.height || 0, } })) || []; // 更新ImageStoryEntity this.setImageStory({ imageAnalysis: data.story_logline || "", - storyType: data.potential_genres?.[0] || "auto", // 使用第一个分类作为故事类型 + storyType: "Auto", // 使用第一个分类作为故事类型 roleImage, }); } @@ -211,22 +166,6 @@ export class ImageStoryUseCase { 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" }, - ]; - } - /** * 处理角色数据,解析并存储到类属性中 * @param {CharacterAnalysis[]} characters - 角色分析数据 @@ -235,10 +174,10 @@ export class ImageStoryUseCase { this.charactersAnalysis = characters.map(character => ({ ...character, region: { - x: character.region.x, - y: character.region.y, - width: character.region.width, - height: character.region.height, + x: character.region?.x || 0, + y: character.region?.y || 0, + width: character.region?.width || 0, + height: character.region?.height || 0, } })); } diff --git a/components/common/ChatInputBox.tsx b/components/common/ChatInputBox.tsx index 9ea8656..aca6f60 100644 --- a/components/common/ChatInputBox.tsx +++ b/components/common/ChatInputBox.tsx @@ -30,6 +30,10 @@ 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 = ` @@ -453,7 +457,7 @@ const RenderTemplateStoryMode = ({ }} footer={null} width="60%" - style={{ maxWidth: "800px" }} + style={{ maxWidth: "800px", marginTop: "10vh" }} className="template-modal" closeIcon={
@@ -591,37 +595,33 @@ export function ChatInputBox() { {/* 右上角齿轮图标和配置项 */} {!isExpanded && (
- {/* 配置项显示区域 - 从齿轮图标右侧边缘滑入 */} -
( +
+ + setConfigOptions((prev) => ({ ...prev, [key]: value })) + } + /> +
+ )} + placement={"left" as any} + trigger={["click"]} > - - setConfigOptions((prev) => ({ ...prev, [key]: value })) - } - compact={true} - /> -
- - {/* 配置项显示控制按钮 - 齿轮图标,位置完全固定 */} - - - + {/* 配置项显示控制按钮 - 齿轮图标 */} + + + +
)} @@ -711,6 +711,7 @@ export function ChatInputBox() { } />
@@ -739,28 +740,27 @@ export function ChatInputBox() { const ActionButton = ({ isCreating, handleCreateVideo, + icon, }: { isCreating: boolean; handleCreateVideo: () => void; + icon: React.ReactNode; }) => { return (
@@ -776,7 +776,6 @@ const ActionButton = ({ const ConfigOptions = ({ config, onConfigChange, - compact = false, }: { config: { mode: string; @@ -785,7 +784,6 @@ const ConfigOptions = ({ videoDuration: string; }; onConfigChange: (key: string, value: string) => void; - compact?: boolean; }) => { const configItems = [ { @@ -828,7 +826,7 @@ const ConfigOptions = ({ ]; return ( -
+
{configItems.map((item) => { const IconComponent = item.icon; const currentOption = item.options.find( @@ -856,22 +854,12 @@ const ConfigOptions = ({ > @@ -881,6 +869,134 @@ 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)) + : ""; + // 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变化 @@ -907,9 +1023,10 @@ const PhotoStoryModal = ({ updateStoryContent, updateCharacterName, resetImageStory, - resetToInitialState, - triggerFileSelectionAndAnalyze, + triggerFileSelection, avatarComputed, + uploadAndAnalyzeImage, + setCharactersAnalysis, } = useImageStoryServiceHook(); // 重置状态 @@ -921,7 +1038,7 @@ const PhotoStoryModal = ({ // 处理图片上传 const handleImageUpload = async () => { try { - await triggerFileSelectionAndAnalyze(); + await triggerFileSelection(); } catch (error) { console.error("Failed to upload image:", error); } @@ -940,7 +1057,7 @@ const PhotoStoryModal = ({ onCancel={handleClose} footer={null} width="80%" - style={{ maxWidth: "1000px" }} + style={{ maxWidth: "1000px", marginTop: "10vh" }} className="photo-story-modal" closeIcon={
@@ -953,232 +1070,183 @@ const PhotoStoryModal = ({
{/* 弹窗头部 */} -
+

- Photo Story Creation + Movie Generation from Image

- -
-
-
-
- {/* 左侧:图片上传 */} -
-
- {activeImageUrl ? ( -
+
+
+ {/* 左侧:图片上传 */} +
+
+ {activeImageUrl ? ( +
+ Story inspiration + + + +
+ ) : ( +
+ +

Upload

+
+ )} +
+
+
+ {/* 中间:头像展示(分析后显示) */} + {hasAnalyzed && avatarComputed.length > 0 && ( +
+ {avatarComputed.map((avatar, index) => ( +
+
Story inspiration { + // 如果裁剪的头像加载失败,回退到原图 + const target = e.target as HTMLImageElement; + target.src = activeImageUrl; + }} /> - + {/* 删除角色按钮 - 使用Tooltip并调整z-index避免被遮挡 */} +
- ) : ( -
- -

Upload

+
+ { + const newName = e.target.value.trim(); + if (newName && newName !== avatar.name) { + updateCharacterName(avatar.name, newName); + } + }} + className="w-16 text-center text-sm text-white/80 bg-transparent border-none outline-none focus:ring-1 focus:ring-blue-400/50 rounded px-1 py-0.5 transition-all duration-200" + style={{ textAlign: "center" }} + /> +
- )} -
+
+ ))} +
+ )} +
+ {/* 右侧:分类选择(分析后显示) */} + {hasAnalyzed && potentialGenres.length > 0 && ( +
+
+ {["Auto", ...potentialGenres].map((genre) => ( + + ))}
- - {/* 中间:头像展示(分析后显示) */} - {hasAnalyzed && avatarComputed.length > 0 && ( -
-
- {avatarComputed.map((avatar, index) => ( -
-
- {avatar.name} { - // 如果裁剪的头像加载失败,回退到原图 - const target = e.target as HTMLImageElement; - target.src = activeImageUrl; - }} - /> - {/* 删除角色按钮 - 使用Tooltip并调整z-index避免被遮挡 */} - - - -
-
- { - const newName = e.target.value.trim(); - if (newName && newName !== avatar.name) { - updateCharacterName(avatar.name, newName); - } - }} - onBlur={(e) => { - const newName = e.target.value.trim(); - if (newName && newName !== avatar.name) { - updateCharacterName(avatar.name, newName); - } - }} - className="w-16 text-center text-sm text-white/80 bg-transparent border-none outline-none focus:ring-1 focus:ring-blue-400/50 rounded px-1 py-0.5 transition-all duration-200" - style={{ textAlign: "center" }} - /> -
-
-
- ))} -
-
- )} - - {/* 右侧:分类选择(分析后显示) */} - {hasAnalyzed && potentialGenres.length > 0 && ( -
-
- {["Auto", ...potentialGenres].map((genre) => ( - - ))} -
-
- )}
-
- {/* 文本输入框 */} -
-