根据接口 再次调整相关交互逻辑

This commit is contained in:
海龙 2025-08-18 21:09:08 +08:00
parent 6c32afbe73
commit d7fba252d0
4 changed files with 130 additions and 39 deletions

View File

@ -23,6 +23,8 @@ interface UseImageStoryService {
hasAnalyzed: boolean; hasAnalyzed: boolean;
/** 计算后的角色头像数据 */ /** 计算后的角色头像数据 */
avatarComputed: Array<{ name: string; url: string }>; avatarComputed: Array<{ name: string; url: string }>;
/** 原始用户描述 */
originalUserDescription: string;
/** 上传图片并分析 */ /** 上传图片并分析 */
uploadAndAnalyzeImage: () => Promise<void>; uploadAndAnalyzeImage: () => Promise<void>;
/** 触发文件选择 */ /** 触发文件选择 */
@ -40,13 +42,14 @@ interface UseImageStoryService {
/** 重置图片故事数据 */ /** 重置图片故事数据 */
resetImageStory: (showAnalysisState?: boolean) => void; resetImageStory: (showAnalysisState?: boolean) => void;
setCharactersAnalysis: Dispatch<SetStateAction<CharacterAnalysis[]>> setCharactersAnalysis: Dispatch<SetStateAction<CharacterAnalysis[]>>
setOriginalUserDescription: Dispatch<SetStateAction<string>>
} }
export const useImageStoryServiceHook = (): UseImageStoryService => { export const useImageStoryServiceHook = (): UseImageStoryService => {
// 基础状态 // 基础状态
const [imageStory, setImageStory] = useState<Partial<ImageStoryEntity>>({ const [imageStory, setImageStory] = useState<Partial<ImageStoryEntity>>({
imageUrl: "", imageUrl: "",
storyType: "auto", storyType: "",
}); });
// 图片相关状态 // 图片相关状态
@ -54,6 +57,8 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
// 故事内容状态统一管理用户输入和AI分析结果 // 故事内容状态统一管理用户输入和AI分析结果
const [storyContent, setStoryContent] = useState<string>(""); const [storyContent, setStoryContent] = useState<string>("");
// 原始用户描述
const [originalUserDescription, setOriginalUserDescription] = useState<string>("");
// 分析结果状态 // 分析结果状态
/** 角色头像及名称 */ /** 角色头像及名称 */
@ -64,7 +69,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
const [potentialGenres, setPotentialGenres] = useState<string[]>([]); const [potentialGenres, setPotentialGenres] = useState<string[]>([]);
// 分类状态 // 分类状态
const [selectedCategory, setSelectedCategory] = useState<string>("Auto"); const [selectedCategory, setSelectedCategory] = useState<string>("");
// 流程状态 // 流程状态
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -216,18 +221,17 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
const uploadAndAnalyzeImage = useCallback( const uploadAndAnalyzeImage = useCallback(
async (): Promise<void> => { async (): Promise<void> => {
try { try {
console.log('123123123', 123123123)
setIsLoading(true); setIsLoading(true);
// 调用用例处理图片上传和分析 // 调用用例处理图片上传和分析
await imageStoryUseCase.handleImageUpload(activeImageUrl); const newImageStory = await imageStoryUseCase.handleImageUpload(activeImageUrl);
setOriginalUserDescription(storyContent)
// 获取更新后的数据 // 获取更新后的数据
const updatedStory = imageStoryUseCase.storyLogline; const updatedStory = imageStoryUseCase.storyLogline;
const updatedCharacters = imageStoryUseCase.charactersAnalysis; const updatedCharacters = imageStoryUseCase.charactersAnalysis;
const updatedGenres = imageStoryUseCase.potentialGenres; const updatedGenres = imageStoryUseCase.potentialGenres;
const updatedImageStory = imageStoryUseCase.imageStory; const updatedImageStory = imageStoryUseCase.imageStory;
setSelectedCategory(imageStoryUseCase.potentialGenres[0]);
// 更新所有响应式状态 // 更新所有响应式状态
setCharactersAnalysis(updatedCharacters); setCharactersAnalysis(updatedCharacters);
setPotentialGenres(updatedGenres); setPotentialGenres(updatedGenres);
@ -235,18 +239,20 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
// 将AI分析的故事内容直接更新到统一的故事内容字段 // 将AI分析的故事内容直接更新到统一的故事内容字段
updateStoryContent(updatedStory || ""); updateStoryContent(updatedStory || "");
setSelectedCategory("Auto");
// 标记已分析 // 标记已分析
setHasAnalyzed(true); setHasAnalyzed(true);
} catch (error) { } catch (error) {
console.error("图片上传分析失败:", error); console.error("图片上传分析失败:", error);
setHasAnalyzed(false);
throw error; throw error;
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, },
[activeImageUrl, imageStoryUseCase] [activeImageUrl, imageStoryUseCase,storyContent,setOriginalUserDescription]
); );
/** /**
@ -314,8 +320,9 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
(oldName: string, newName: string) => { (oldName: string, newName: string) => {
// 更新故事内容中的角色标签 // 更新故事内容中的角色标签
setStoryContent((prev) => { setStoryContent((prev) => {
const regex = new RegExp(`<role_name>${oldName}<\/role_name>`, "g"); // 匹配新的角色标签格式 <role id="C1">Dezhong Huang</role>
const content = prev.replace(regex, `<role_name>${newName}</role_name>`); const regex = new RegExp(`<role[^>]*>${oldName}<\/role>`, "g");
const content = prev.replace(regex, `<role >${newName}</role>`);
imageStoryUseCase.updateStoryContent(content); imageStoryUseCase.updateStoryContent(content);
return content; return content;
}); });
@ -363,14 +370,15 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
// 重置所有状态 // 重置所有状态
setImageStory({ setImageStory({
imageUrl: "", imageUrl: "",
storyType: "auto", storyType: "",
}); });
setActiveImageUrl(""); setActiveImageUrl("");
updateStoryContent(""); updateStoryContent("");
setPotentialGenres([]); setPotentialGenres([]);
setSelectedCategory("auto"); setSelectedCategory("");
setHasAnalyzed(false); setHasAnalyzed(false);
setIsLoading(false); setIsLoading(false);
setOriginalUserDescription("");
}, [imageStoryUseCase]); }, [imageStoryUseCase]);
/** /**
@ -437,6 +445,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
isLoading, isLoading,
hasAnalyzed, hasAnalyzed,
avatarComputed, avatarComputed,
originalUserDescription,
setCharactersAnalysis, setCharactersAnalysis,
uploadAndAnalyzeImage, uploadAndAnalyzeImage,
triggerFileSelection, triggerFileSelection,
@ -446,5 +455,6 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
updateCharacterName, updateCharacterName,
syncRoleNameToContent, syncRoleNameToContent,
resetImageStory, resetImageStory,
setOriginalUserDescription
}; };
}; };

View File

@ -1,6 +1,5 @@
import { getUploadToken, uploadToQiniu } from "@/api/common"; import { getUploadToken, uploadToQiniu } from "@/api/common";
import { useState, useCallback } from "react"; import { useState, useCallback, useEffect } from "react";
import { ScriptEditKey } from "../usecase/ScriptEditUseCase"; import { ScriptEditKey } from "../usecase/ScriptEditUseCase";
/** /**
* *
@ -13,7 +12,7 @@ export function parseScriptBlock(
key: ScriptEditKey, key: ScriptEditKey,
headerName: string, headerName: string,
scriptText: string, scriptText: string,
contentType?: 'paragraph' | 'bold' | 'italic' | 'heading' | 'tag', contentType?: "paragraph" | "bold" | "italic" | "heading" | "tag"
) { ) {
return { return {
id: key, id: key,
@ -27,8 +26,6 @@ export function parseScriptBlock(
}; };
} }
/** /**
* Hook * Hook
* @returns {object} - * @returns {object} -
@ -45,14 +42,17 @@ export function useUploadFile() {
* @throws {Error} - * @throws {Error} -
*/ */
const uploadFile = useCallback( const uploadFile = useCallback(
async (file: File, onProgress?: (progress: number) => void): Promise<string> => { async (
file: File,
onProgress?: (progress: number) => void
): Promise<string> => {
try { try {
setIsUploading(true); setIsUploading(true);
const { token } = await getUploadToken(); const { token } = await getUploadToken();
const fileUrl = await uploadToQiniu(file, token, onProgress); const fileUrl = await uploadToQiniu(file, token, onProgress);
return fileUrl; return fileUrl;
} catch (err) { } catch (err) {
console.error('文件上传失败:', err); console.error("文件上传失败:", err);
throw err; throw err;
} finally { } finally {
setIsUploading(false); setIsUploading(false);
@ -63,3 +63,65 @@ export function useUploadFile() {
return { uploadFile, isUploading }; return { uploadFile, isUploading };
} }
/**加载文案定时变 */
export function useLoadScriptText(loading: boolean): { loadingText: string } {
// 如果loading 为true 则每五秒切换一次文本如果变false 则停止切换,且重置文本位置
const tests = [
"loading...",
"Brainstorming initial story concepts and themes.",
"Drafting the screenplay's first outline.",
"Refining character arcs and plot points.",
"Finalizing the script with dialogue polish.",
"Creating detailed storyboards for key scenes.",
"Scouting potential filming locations.",
"Designing mood boards for visual aesthetics.",
"Casting actors to bring characters to life.",
"Scheduling production timelines and shoots.",
"Securing permits for on-location filming.",
"Building sets to match the storys vision.",
"Designing costumes for character authenticity.",
"Planning lighting setups for each scene.",
"Renting equipment for high-quality production.",
"Rehearsing actors for seamless performances.",
"Setting up cameras for the first shot.",
"Filming establishing shots for scene context.",
"Capturing key dialogue scenes with precision.",
"Recording action sequences with dynamic angles.",
"Filming close-ups to capture emotions.",
"Wrapping principal photography on set.",
"Reviewing dailies for quality assurance.",
"Organizing raw footage for editing.",
"Editing scenes for narrative flow.",
"Adding sound effects to enhance immersion.",
"Composing the films musical score.",
"Mixing audio for balanced sound design.",
"Applying color grading for visual consistency.",
"Rendering visual effects for final polish.",
"Exporting the final cut for distribution.",
];
const [currentIndex, setCurrentIndex] = useState(0);
const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null);
useEffect(() => {
if (loading) {
const interval = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % tests.length);
}, 5000);
setIntervalId(interval);
} else {
if (intervalId) {
clearInterval(intervalId);
setIntervalId(null);
setCurrentIndex(0);
}
}
return () => {
if (intervalId) {
clearInterval(intervalId);
setIntervalId(null);
setCurrentIndex(0);
}
};
}, [loading, tests.length]);
return { loadingText: tests[currentIndex] };
}

View File

@ -8,10 +8,13 @@ import { MovieStartDTO, CharacterAnalysis } from "@/api/DTO/movie_start_dto";
*/ */
export class ImageStoryUseCase { export class ImageStoryUseCase {
/** 当前图片故事数据 */ /** 当前图片故事数据 */
imageStory: Partial<ImageStoryEntity> = { imageStory: ImageStoryEntity = {
id: "",
imageAnalysis: "",
roleImage: [],
imageUrl: "", imageUrl: "",
imageStory: "", imageStory: "",
storyType: "Auto", storyType: "",
}; };
/** 故事梗概 */ /** 故事梗概 */
@ -28,7 +31,6 @@ export class ImageStoryUseCase {
/** 是否正在上传 */ /** 是否正在上传 */
isUploading: boolean = false; isUploading: boolean = false;
constructor() {} constructor() {}
@ -46,9 +48,12 @@ export class ImageStoryUseCase {
*/ */
resetImageStory(): void { resetImageStory(): void {
this.imageStory = { this.imageStory = {
id: "",
imageAnalysis: "",
roleImage: [],
imageUrl: "", imageUrl: "",
imageStory: "", imageStory: "",
storyType: "Auto", storyType: "",
}; };
this.storyLogline = ""; this.storyLogline = "";
this.charactersAnalysis = []; this.charactersAnalysis = [];
@ -62,7 +67,7 @@ export class ImageStoryUseCase {
* @param {string} imageUrl - URL * @param {string} imageUrl - URL
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async handleImageUpload(imageUrl: string): Promise<void> { async handleImageUpload(imageUrl: string) {
try { try {
this.isUploading = false; // 图片已上传设置上传状态为false this.isUploading = false; // 图片已上传设置上传状态为false
this.isAnalyzing = true; this.isAnalyzing = true;
@ -71,7 +76,7 @@ export class ImageStoryUseCase {
this.setImageStory({ imageUrl }); this.setImageStory({ imageUrl });
// 调用AI分析接口 // 调用AI分析接口
await this.analyzeImageWithAI(); return await this.analyzeImageWithAI();
} catch (error) { } catch (error) {
console.error("图片分析失败:", error); console.error("图片分析失败:", error);
@ -87,7 +92,7 @@ export class ImageStoryUseCase {
* 使AI分析图片 * 使AI分析图片
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async analyzeImageWithAI(): Promise<void> { async analyzeImageWithAI() {
console.log('this.imageStory.imageUrl', this.imageStory.imageUrl) console.log('this.imageStory.imageUrl', this.imageStory.imageUrl)
try { try {
// 调用AI分析接口 // 调用AI分析接口
@ -102,6 +107,7 @@ export class ImageStoryUseCase {
// 组合成ImageStoryEntity // 组合成ImageStoryEntity
this.composeImageStoryEntity(response.data); this.composeImageStoryEntity(response.data);
return this.imageStory;
} else { } else {
throw new Error("AI分析失败"); throw new Error("AI分析失败");
} }
@ -140,13 +146,15 @@ export class ImageStoryUseCase {
y: character.region?.y || 0, y: character.region?.y || 0,
width: character.region?.width || 0, width: character.region?.width || 0,
height: character.region?.height || 0, height: character.region?.height || 0,
} },
})) || []; })) || [];
// 更新ImageStoryEntity // 更新ImageStoryEntity
this.setImageStory({ this.setImageStory({
...this.imageStory,
imageAnalysis: data.story_logline || "", imageAnalysis: data.story_logline || "",
storyType: "Auto", // 使用第一个分类作为故事类型 storyType: data.potential_genres[0] || "", // 使用第一个分类作为故事类型
roleImage, roleImage,
}); });
} }

View File

@ -35,6 +35,7 @@ import StarterKit from "@tiptap/starter-kit";
import { HighlightTextExtension } from "@/components/ui/main-editor/HighlightText"; import { HighlightTextExtension } from "@/components/ui/main-editor/HighlightText";
import Placeholder from "@tiptap/extension-placeholder"; import Placeholder from "@tiptap/extension-placeholder";
import { createMovieProjectV1 } from "@/api/video_flow"; import { createMovieProjectV1 } from "@/api/video_flow";
import { useLoadScriptText } from "@/app/service/domain/service";
// 自定义音频播放器样式 // 自定义音频播放器样式
const customAudioPlayerStyles = ` const customAudioPlayerStyles = `
@ -914,10 +915,11 @@ const RoleHighlightEditor = ({
to < doc.content.size to < doc.content.size
? doc.textBetween(to, Math.min(doc.content.size, to + 50)) ? doc.textBetween(to, Math.min(doc.content.size, to + 50))
: ""; : "";
// TODO role id 的结构 // 匹配新的角色标签格式 <role id="C1">Dezhong Huang</role>
const beforeMatch = textBefore.match(/<role_name>[^<]*$/); const beforeMatch = textBefore.match(/<role[^>]*>[^<]*$/);
const afterMatch = textAfter.match(/^[^>]*<\/role_name>/); const afterMatch = textAfter.match(/^[^>]*<\/role>/);
// 如果光标在角色标签内,阻止输入(只允许删除操作)
if (beforeMatch || afterMatch) { if (beforeMatch || afterMatch) {
if (event.key !== "Backspace" && event.key !== "Delete") { if (event.key !== "Backspace" && event.key !== "Delete") {
event.preventDefault(); event.preventDefault();
@ -938,9 +940,9 @@ const RoleHighlightEditor = ({
return; return;
} }
// 将带标签的内容转换为高亮显示 // 将带标签的内容转换为高亮显示(支持新的角色标签格式)
const htmlContent = content.replace( const htmlContent = content.replace(
/<role_name>([^<]+)<\/role_name>/g, /<role[^>]*>([^<]+)<\/role>/g,
'<highlight-text type="role" text="$1" color="blue">$1</highlight-text>' '<highlight-text type="role" text="$1" color="blue">$1</highlight-text>'
); );
editor.commands.setContent(htmlContent, { emitUpdate: false }); editor.commands.setContent(htmlContent, { emitUpdate: false });
@ -1028,8 +1030,9 @@ const PhotoStoryModal = ({
avatarComputed, avatarComputed,
uploadAndAnalyzeImage, uploadAndAnalyzeImage,
setCharactersAnalysis, setCharactersAnalysis,
originalUserDescription
} = useImageStoryServiceHook(); } = useImageStoryServiceHook();
const { loadingText } = useLoadScriptText(isLoading);
// 重置状态 // 重置状态
const handleClose = () => { const handleClose = () => {
resetImageStory(); resetImageStory();
@ -1068,7 +1071,7 @@ const PhotoStoryModal = ({
</div> </div>
} }
> >
<Spin spinning={isLoading} tip="Processing..."> <Spin spinning={isLoading} tip={loadingText}>
<div className="rounded-2xl"> <div className="rounded-2xl">
{/* 弹窗头部 */} {/* 弹窗头部 */}
<div className="flex items-center gap-3 p-2 border-b border-white/[0.1]"> <div className="flex items-center gap-3 p-2 border-b border-white/[0.1]">
@ -1083,7 +1086,7 @@ const PhotoStoryModal = ({
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<div <div
data-alt="image-upload-area" data-alt="image-upload-area"
className={`w-20 h-20 rounded-lg flex flex-col items-center justify-center transition-all duration-300 cursor-pointer ${ className={`w-24 h-24 rounded-lg flex flex-col items-center justify-center transition-all duration-300 cursor-pointer ${
activeImageUrl activeImageUrl
? "border-2 border-white/20 bg-white/[0.05]" ? "border-2 border-white/20 bg-white/[0.05]"
: "border-2 border-dashed border-white/20 bg-white/[0.02] hover:border-white/40 hover:bg-white/[0.05] hover:scale-105" : "border-2 border-dashed border-white/20 bg-white/[0.02] hover:border-white/40 hover:bg-white/[0.05] hover:scale-105"
@ -1154,8 +1157,12 @@ const PhotoStoryModal = ({
); );
return updatedCharacters; return updatedCharacters;
}); });
// 从故事内容中删除该角色名称 // 从故事内容中删除该角色的所有标签和引用
const updatedStory = storyContent const updatedStory = storyContent
.replace(
new RegExp(`<role[^>]*>${avatar.name}<\/role>`, "g"),
""
)
.replace( .replace(
new RegExp(`\\b${avatar.name}\\b`, "g"), new RegExp(`\\b${avatar.name}\\b`, "g"),
"" ""
@ -1164,7 +1171,6 @@ const PhotoStoryModal = ({
.trim(); .trim();
// 更新状态 // 更新状态
updateStoryContent(updatedStory); updateStoryContent(updatedStory);
// 注意:这里需要直接更新 charactersAnalysis但 hook 中没有提供 setter
}} }}
className="absolute top-1 right-1 w-4 h-4 bg-black/[0.05] border border-black/[0.1] text-white rounded-full flex items-center justify-center transition-colors opacity-0 group-hover:opacity-100 z-10" className="absolute top-1 right-1 w-4 h-4 bg-black/[0.05] border border-black/[0.1] text-white rounded-full flex items-center justify-center transition-colors opacity-0 group-hover:opacity-100 z-10"
> >
@ -1196,7 +1202,7 @@ const PhotoStoryModal = ({
{hasAnalyzed && potentialGenres.length > 0 && ( {hasAnalyzed && potentialGenres.length > 0 && (
<div className="flex-shrink-0 animate-in fade-in-0 slide-in-from-right-4 duration-300"> <div className="flex-shrink-0 animate-in fade-in-0 slide-in-from-right-4 duration-300">
<div className="flex gap-2"> <div className="flex gap-2">
{["Auto", ...potentialGenres].map((genre) => ( {[ ...potentialGenres].map((genre) => (
<button <button
key={genre} key={genre}
onClick={() => updateStoryType(genre)} onClick={() => updateStoryType(genre)}
@ -1213,6 +1219,11 @@ const PhotoStoryModal = ({
</div> </div>
)} )}
</div> </div>
{/* 原始用户描述的展示 */}
{originalUserDescription && (
<div className="mt-2 text-sm text-white/30 italic">Your Provided Text:{originalUserDescription}</div>
)}
<div className="flex items-start gap-4 mt-2 relative"> <div className="flex items-start gap-4 mt-2 relative">
{/* 文本输入框 */} {/* 文本输入框 */}
<RoleHighlightEditor <RoleHighlightEditor