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

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

View File

@ -1,6 +1,5 @@
import { getUploadToken, uploadToQiniu } from "@/api/common";
import { useState, useCallback } from "react";
import { useState, useCallback, useEffect } from "react";
import { ScriptEditKey } from "../usecase/ScriptEditUseCase";
/**
*
@ -13,7 +12,7 @@ export function parseScriptBlock(
key: ScriptEditKey,
headerName: string,
scriptText: string,
contentType?: 'paragraph' | 'bold' | 'italic' | 'heading' | 'tag',
contentType?: "paragraph" | "bold" | "italic" | "heading" | "tag"
) {
return {
id: key,
@ -27,8 +26,6 @@ export function parseScriptBlock(
};
}
/**
* Hook
* @returns {object} -
@ -45,14 +42,17 @@ export function useUploadFile() {
* @throws {Error} -
*/
const uploadFile = useCallback(
async (file: File, onProgress?: (progress: number) => void): Promise<string> => {
async (
file: File,
onProgress?: (progress: number) => void
): Promise<string> => {
try {
setIsUploading(true);
const { token } = await getUploadToken();
const fileUrl = await uploadToQiniu(file, token, onProgress);
return fileUrl;
} catch (err) {
console.error('文件上传失败:', err);
console.error("文件上传失败:", err);
throw err;
} finally {
setIsUploading(false);
@ -63,3 +63,65 @@ export function useUploadFile() {
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 {
/** 当前图片故事数据 */
imageStory: Partial<ImageStoryEntity> = {
imageStory: ImageStoryEntity = {
id: "",
imageAnalysis: "",
roleImage: [],
imageUrl: "",
imageStory: "",
storyType: "Auto",
storyType: "",
};
/** 故事梗概 */
@ -28,7 +31,6 @@ export class ImageStoryUseCase {
/** 是否正在上传 */
isUploading: boolean = false;
constructor() {}
@ -46,9 +48,12 @@ export class ImageStoryUseCase {
*/
resetImageStory(): void {
this.imageStory = {
id: "",
imageAnalysis: "",
roleImage: [],
imageUrl: "",
imageStory: "",
storyType: "Auto",
storyType: "",
};
this.storyLogline = "";
this.charactersAnalysis = [];
@ -62,7 +67,7 @@ export class ImageStoryUseCase {
* @param {string} imageUrl - URL
* @returns {Promise<void>}
*/
async handleImageUpload(imageUrl: string): Promise<void> {
async handleImageUpload(imageUrl: string) {
try {
this.isUploading = false; // 图片已上传设置上传状态为false
this.isAnalyzing = true;
@ -71,7 +76,7 @@ export class ImageStoryUseCase {
this.setImageStory({ imageUrl });
// 调用AI分析接口
await this.analyzeImageWithAI();
return await this.analyzeImageWithAI();
} catch (error) {
console.error("图片分析失败:", error);
@ -87,7 +92,7 @@ export class ImageStoryUseCase {
* 使AI分析图片
* @returns {Promise<void>}
*/
async analyzeImageWithAI(): Promise<void> {
async analyzeImageWithAI() {
console.log('this.imageStory.imageUrl', this.imageStory.imageUrl)
try {
// 调用AI分析接口
@ -102,6 +107,7 @@ export class ImageStoryUseCase {
// 组合成ImageStoryEntity
this.composeImageStoryEntity(response.data);
return this.imageStory;
} else {
throw new Error("AI分析失败");
}
@ -140,13 +146,15 @@ export class ImageStoryUseCase {
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: "Auto", // 使用第一个分类作为故事类型
storyType: data.potential_genres[0] || "", // 使用第一个分类作为故事类型
roleImage,
});
}

View File

@ -35,6 +35,7 @@ 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 } from "@/app/service/domain/service";
// 自定义音频播放器样式
const customAudioPlayerStyles = `
@ -914,10 +915,11 @@ const RoleHighlightEditor = ({
to < doc.content.size
? doc.textBetween(to, Math.min(doc.content.size, to + 50))
: "";
// TODO role id 的结构
const beforeMatch = textBefore.match(/<role_name>[^<]*$/);
const afterMatch = textAfter.match(/^[^>]*<\/role_name>/);
// 匹配新的角色标签格式 <role id="C1">Dezhong Huang</role>
const beforeMatch = textBefore.match(/<role[^>]*>[^<]*$/);
const afterMatch = textAfter.match(/^[^>]*<\/role>/);
// 如果光标在角色标签内,阻止输入(只允许删除操作)
if (beforeMatch || afterMatch) {
if (event.key !== "Backspace" && event.key !== "Delete") {
event.preventDefault();
@ -938,9 +940,9 @@ const RoleHighlightEditor = ({
return;
}
// 将带标签的内容转换为高亮显示
// 将带标签的内容转换为高亮显示(支持新的角色标签格式)
const htmlContent = content.replace(
/<role_name>([^<]+)<\/role_name>/g,
/<role[^>]*>([^<]+)<\/role>/g,
'<highlight-text type="role" text="$1" color="blue">$1</highlight-text>'
);
editor.commands.setContent(htmlContent, { emitUpdate: false });
@ -1028,8 +1030,9 @@ const PhotoStoryModal = ({
avatarComputed,
uploadAndAnalyzeImage,
setCharactersAnalysis,
originalUserDescription
} = useImageStoryServiceHook();
const { loadingText } = useLoadScriptText(isLoading);
// 重置状态
const handleClose = () => {
resetImageStory();
@ -1068,7 +1071,7 @@ const PhotoStoryModal = ({
</div>
}
>
<Spin spinning={isLoading} tip="Processing...">
<Spin spinning={isLoading} tip={loadingText}>
<div className="rounded-2xl">
{/* 弹窗头部 */}
<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
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
? "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"
@ -1154,8 +1157,12 @@ const PhotoStoryModal = ({
);
return updatedCharacters;
});
// 从故事内容中删除该角色名称
// 从故事内容中删除该角色的所有标签和引用
const updatedStory = storyContent
.replace(
new RegExp(`<role[^>]*>${avatar.name}<\/role>`, "g"),
""
)
.replace(
new RegExp(`\\b${avatar.name}\\b`, "g"),
""
@ -1164,7 +1171,6 @@ const PhotoStoryModal = ({
.trim();
// 更新状态
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"
>
@ -1196,7 +1202,7 @@ const PhotoStoryModal = ({
{hasAnalyzed && potentialGenres.length > 0 && (
<div className="flex-shrink-0 animate-in fade-in-0 slide-in-from-right-4 duration-300">
<div className="flex gap-2">
{["Auto", ...potentialGenres].map((genre) => (
{[ ...potentialGenres].map((genre) => (
<button
key={genre}
onClick={() => updateStoryType(genre)}
@ -1213,6 +1219,11 @@ const PhotoStoryModal = ({
</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">
{/* 文本输入框 */}
<RoleHighlightEditor