forked from 77media/video-flow
业务逻辑初步完成,等接口对接
This commit is contained in:
parent
188379c93c
commit
d54f26371c
@ -1,4 +1,5 @@
|
||||
import { ImageStoryEntity } from "../domain/Entities";
|
||||
import { useUploadFile } from "../domain/service";
|
||||
import { ImageStoryUseCase } from "../usecase/imageStoryUseCase";
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
|
||||
@ -7,8 +8,8 @@ interface UseImageStoryService {
|
||||
imageStory: Partial<ImageStoryEntity>;
|
||||
/** 当前活跃的图片地址 */
|
||||
activeImageUrl: string;
|
||||
/** 当前活跃的文本信息 */
|
||||
activeTextContent: string;
|
||||
/** 分析故事结果内容 */
|
||||
analyzedStoryContent: string;
|
||||
/** 当前选中的分类 */
|
||||
selectedCategory: string;
|
||||
/** 是否正在分析图片 */
|
||||
@ -19,6 +20,8 @@ interface UseImageStoryService {
|
||||
storyTypeOptions: Array<{ key: string; label: string }>;
|
||||
/** 上传图片并分析 */
|
||||
uploadAndAnalyzeImage: (imageUrl: string) => Promise<void>;
|
||||
/** 触发文件选择并自动分析 */
|
||||
triggerFileSelectionAndAnalyze: () => Promise<void>;
|
||||
/** 触发生成剧本函数 */
|
||||
generateScript: () => Promise<string>;
|
||||
/** 更新故事类型 */
|
||||
@ -29,7 +32,8 @@ interface UseImageStoryService {
|
||||
resetImageStory: () => void;
|
||||
}
|
||||
|
||||
export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
export const useImageStoryServiceHook = (
|
||||
): UseImageStoryService => {
|
||||
const [imageStory, setImageStory] = useState<Partial<ImageStoryEntity>>({
|
||||
imageUrl: "",
|
||||
imageStory: "",
|
||||
@ -37,18 +41,20 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
});
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
// 使用上传文件Hook
|
||||
const { uploadFile } = useUploadFile();
|
||||
|
||||
/** 图片故事用例实例 */
|
||||
const imageStoryUseCase = useMemo(() => new ImageStoryUseCase(), []);
|
||||
|
||||
/** 当前活跃的图片地址 */
|
||||
const activeImageUrl = imageStory.imageUrl || "";
|
||||
const [activeImageUrl, setActiveImageUrl] = useState<string>("");
|
||||
|
||||
/** 当前活跃的文本信息 */
|
||||
const activeTextContent = imageStory.imageStory || "";
|
||||
/** 分析故事结果内容 */
|
||||
const [analyzedStoryContent, setAnalyzedStoryContent] = useState<string>("");
|
||||
|
||||
/** 当前选中的分类 */
|
||||
const selectedCategory = imageStory.storyType || "auto";
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>("auto");
|
||||
|
||||
/** 故事类型选项 */
|
||||
const storyTypeOptions = useMemo(() => imageStoryUseCase.getStoryTypeOptions(), [imageStoryUseCase]);
|
||||
@ -69,6 +75,13 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
const updatedStory = imageStoryUseCase.getImageStory();
|
||||
setImageStory(updatedStory);
|
||||
|
||||
// 更新活跃状态
|
||||
setActiveImageUrl(imageUrl);
|
||||
setAnalyzedStoryContent(updatedStory.imageStory || "");
|
||||
setSelectedCategory(updatedStory.storyType || "auto");
|
||||
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('图片上传分析失败:', error);
|
||||
throw error;
|
||||
@ -87,7 +100,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
throw new Error('请先上传图片');
|
||||
}
|
||||
|
||||
if (!activeTextContent) {
|
||||
if (!analyzedStoryContent) {
|
||||
throw new Error('请先输入或生成故事内容');
|
||||
}
|
||||
|
||||
@ -109,7 +122,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
} finally {
|
||||
setIsAnalyzing(false);
|
||||
}
|
||||
}, [activeImageUrl, activeTextContent, imageStory]);
|
||||
}, [activeImageUrl, analyzedStoryContent, imageStory]);
|
||||
|
||||
/**
|
||||
* 更新故事类型
|
||||
@ -118,6 +131,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
const updateStoryType = useCallback((storyType: string): void => {
|
||||
imageStoryUseCase.updateStoryType(storyType);
|
||||
setImageStory(prev => ({ ...prev, storyType }));
|
||||
setSelectedCategory(storyType);
|
||||
}, [imageStoryUseCase]);
|
||||
|
||||
/**
|
||||
@ -127,6 +141,9 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
const updateStoryContent = useCallback((content: string): void => {
|
||||
imageStoryUseCase.updateStoryContent(content);
|
||||
setImageStory(prev => ({ ...prev, imageStory: content }));
|
||||
setAnalyzedStoryContent(content);
|
||||
|
||||
|
||||
}, [imageStoryUseCase]);
|
||||
|
||||
/**
|
||||
@ -139,19 +156,68 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||
imageStory: "",
|
||||
storyType: "auto",
|
||||
});
|
||||
// 重置活跃状态
|
||||
setActiveImageUrl("");
|
||||
setAnalyzedStoryContent("");
|
||||
setSelectedCategory("auto");
|
||||
setIsAnalyzing(false);
|
||||
setIsUploading(false);
|
||||
|
||||
|
||||
}, [imageStoryUseCase]);
|
||||
|
||||
/**
|
||||
* 触发文件选择并自动分析
|
||||
*/
|
||||
const triggerFileSelectionAndAnalyze = useCallback(async (): Promise<void> => {
|
||||
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 uploadedImageUrl = await uploadFile(target.files[0], (progress) => {
|
||||
console.log("上传进度:", progress);
|
||||
});
|
||||
console.log('uploadedImageUrl', uploadedImageUrl)
|
||||
// await uploadAndAnalyzeImage(uploadedImageUrl);
|
||||
setActiveImageUrl(uploadedImageUrl);
|
||||
}
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
} finally {
|
||||
// 清理DOM
|
||||
document.body.removeChild(fileInput);
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.oncancel = () => {
|
||||
document.body.removeChild(fileInput);
|
||||
reject();
|
||||
};
|
||||
|
||||
document.body.appendChild(fileInput);
|
||||
fileInput.click();
|
||||
});
|
||||
}, [uploadFile]);
|
||||
|
||||
return {
|
||||
imageStory,
|
||||
activeImageUrl,
|
||||
activeTextContent,
|
||||
analyzedStoryContent,
|
||||
selectedCategory,
|
||||
isAnalyzing,
|
||||
isUploading,
|
||||
storyTypeOptions,
|
||||
uploadAndAnalyzeImage,
|
||||
triggerFileSelectionAndAnalyze,
|
||||
generateScript,
|
||||
updateStoryType,
|
||||
updateStoryContent,
|
||||
|
||||
@ -1,4 +1,14 @@
|
||||
import { StoryTemplateEntity, RoleEntity } from "../domain/Entities";
|
||||
import { StoryTemplateEntity } from "../domain/Entities";
|
||||
|
||||
/** 模板角色接口 */
|
||||
interface TemplateRole {
|
||||
/** 角色名 */
|
||||
role_name: string;
|
||||
/** 照片URL */
|
||||
photo_url: string;
|
||||
/** 声音URL */
|
||||
voice_url: string;
|
||||
}
|
||||
import { TemplateStoryUseCase } from "../usecase/templateStoryUseCase";
|
||||
import { getUploadToken, uploadToQiniu } from "@/api/common";
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
@ -8,8 +18,10 @@ interface UseTemplateStoryService {
|
||||
templateStoryList: StoryTemplateEntity[];
|
||||
/** 当前选中要使用的模板 */
|
||||
selectedTemplate: StoryTemplateEntity | null;
|
||||
/** 当前选中的活跃的角色 */
|
||||
activeRole: RoleEntity | null;
|
||||
/** 当前选中的活跃角色索引 */
|
||||
activeRoleIndex: number;
|
||||
/** 计算属性:当前活跃角色信息 */
|
||||
activeRole: TemplateRole | null;
|
||||
/** 加载状态 */
|
||||
isLoading: boolean;
|
||||
/** 获取模板列表函数 */
|
||||
@ -18,19 +30,31 @@ interface UseTemplateStoryService {
|
||||
actionStory: () => Promise<string>;
|
||||
/** 设置选中的模板 */
|
||||
setSelectedTemplate: (template: StoryTemplateEntity | null) => void;
|
||||
/** 设置活跃角色 */
|
||||
setActiveRole: (role: RoleEntity | null) => void;
|
||||
/** 设置活跃角色索引 */
|
||||
setActiveRoleIndex: (index: number) => void;
|
||||
/** 设置当前活跃角色的图片URL */
|
||||
setActiveRoleImage: (imageUrl: string) => void;
|
||||
/** 设置当前活跃角色的音频URL */
|
||||
setActiveRoleAudio: (audioUrl: string) => void;
|
||||
}
|
||||
|
||||
export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
|
||||
const [templateStoryList, setTemplateStoryList] = useState<StoryTemplateEntity[]>([]);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<StoryTemplateEntity | null>(null);
|
||||
const [activeRole, setActiveRole] = useState<RoleEntity | null>(null);
|
||||
const [activeRoleIndex, setActiveRoleIndex] = useState<number>(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
/** 模板故事用例实例 */
|
||||
const templateStoryUseCase = useMemo(() => new TemplateStoryUseCase(), []);
|
||||
|
||||
/** 计算属性:当前活跃角色信息 */
|
||||
const activeRole = useMemo(() => {
|
||||
if (!selectedTemplate || activeRoleIndex < 0 || activeRoleIndex >= selectedTemplate.storyRole.length) {
|
||||
return null;
|
||||
}
|
||||
return selectedTemplate.storyRole[activeRoleIndex];
|
||||
}, [selectedTemplate, activeRoleIndex]);
|
||||
|
||||
/**
|
||||
* 获取模板列表函数
|
||||
*/
|
||||
@ -68,15 +92,58 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
|
||||
}
|
||||
}, [selectedTemplate, templateStoryUseCase]);
|
||||
|
||||
/**
|
||||
* 设置活跃角色索引
|
||||
*/
|
||||
const handleSetActiveRoleIndex = useCallback((index: number): void => {
|
||||
setActiveRoleIndex(index);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 设置当前活跃角色的图片URL
|
||||
*/
|
||||
const setActiveRoleImage = useCallback((imageUrl: string): void => {
|
||||
if (!selectedTemplate || activeRoleIndex < 0 || activeRoleIndex >= selectedTemplate.storyRole.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedTemplate = {
|
||||
...selectedTemplate,
|
||||
storyRole: selectedTemplate.storyRole.map((role, index) =>
|
||||
index === activeRoleIndex ? { ...role, photo_url: imageUrl } : role
|
||||
),
|
||||
};
|
||||
setSelectedTemplate(updatedTemplate);
|
||||
}, [selectedTemplate, activeRoleIndex]);
|
||||
|
||||
/**
|
||||
* 设置当前活跃角色的音频URL
|
||||
*/
|
||||
const setActiveRoleAudio = useCallback((audioUrl: string): void => {
|
||||
if (!selectedTemplate || activeRoleIndex < 0 || activeRoleIndex >= selectedTemplate.storyRole.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedTemplate = {
|
||||
...selectedTemplate,
|
||||
storyRole: selectedTemplate.storyRole.map((role, index) =>
|
||||
index === activeRoleIndex ? { ...role, voice_url: audioUrl } : role
|
||||
),
|
||||
};
|
||||
setSelectedTemplate(updatedTemplate);
|
||||
}, [selectedTemplate, activeRoleIndex]);
|
||||
|
||||
return {
|
||||
templateStoryList,
|
||||
selectedTemplate,
|
||||
activeRoleIndex,
|
||||
activeRole,
|
||||
isLoading,
|
||||
getTemplateStoryList,
|
||||
actionStory,
|
||||
setSelectedTemplate,
|
||||
setActiveRole,
|
||||
setActiveRoleIndex: handleSetActiveRoleIndex,
|
||||
setActiveRoleImage,
|
||||
setActiveRoleAudio,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef } from "react";
|
||||
import {
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
@ -20,15 +26,11 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Dropdown, Modal, Tooltip, Upload, Image } from "antd";
|
||||
import { PlusOutlined, UploadOutlined, EyeOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
StoryTemplateEntity,
|
||||
} from "@/app/service/domain/Entities";
|
||||
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
|
||||
import { StoryTemplateEntity } from "@/app/service/domain/Entities";
|
||||
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";
|
||||
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
|
||||
|
||||
// 自定义音频播放器样式
|
||||
const customAudioPlayerStyles = `
|
||||
@ -139,113 +141,35 @@ const RenderTemplateStoryMode = ({
|
||||
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: [
|
||||
{
|
||||
role_name: "艾莉娅",
|
||||
photo_url:
|
||||
"https://images.unsplash.com/photo-1494790108755-2616b612b786?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: "",
|
||||
},
|
||||
{
|
||||
role_name: "魔法精灵",
|
||||
photo_url:
|
||||
"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200&h=200&fit=crop",
|
||||
voice_url: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
// 使用 hook 管理状态
|
||||
const {
|
||||
templateStoryList,
|
||||
selectedTemplate,
|
||||
activeRoleIndex,
|
||||
activeRole,
|
||||
isLoading,
|
||||
getTemplateStoryList,
|
||||
actionStory,
|
||||
setSelectedTemplate,
|
||||
setActiveRoleIndex,
|
||||
setActiveRoleImage,
|
||||
setActiveRoleAudio,
|
||||
} = useTemplateStoryServiceHook();
|
||||
|
||||
// 本地状态管理
|
||||
const [templates] = useState(mockTemplates);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState(mockTemplates[0]);
|
||||
const [loading] = useState(false);
|
||||
const [selectedRoleIndex, setSelectedRoleIndex] = useState<number>(0);
|
||||
// 本地加载状态,用于 UI 反馈
|
||||
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 }>({});
|
||||
// 组件挂载时获取模板列表
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
getTemplateStoryList();
|
||||
}
|
||||
}, [isOpen, getTemplateStoryList]);
|
||||
|
||||
// 处理模板选择
|
||||
const handleTemplateSelect = (template: StoryTemplateEntity) => {
|
||||
setSelectedTemplate(template);
|
||||
setActiveRoleIndex(0); // 重置角色选择
|
||||
};
|
||||
|
||||
// 处理确认操作
|
||||
@ -254,19 +178,15 @@ const RenderTemplateStoryMode = ({
|
||||
|
||||
try {
|
||||
setLocalLoading(true);
|
||||
// Mock actionStory函数
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000)); // 模拟2秒延迟
|
||||
const projectId = "mock-project-" + Date.now();
|
||||
const projectId = await actionStory();
|
||||
console.log("Story action created:", projectId);
|
||||
onClose();
|
||||
setSelectedTemplate(mockTemplates[0]); // 重置为第一个模板
|
||||
// 清空角色资源状态
|
||||
setRoleImages({});
|
||||
setRoleAudios({});
|
||||
setSelectedRoleIndex(0);
|
||||
// 重置状态
|
||||
setSelectedTemplate(null);
|
||||
setActiveRoleIndex(0);
|
||||
} catch (error) {
|
||||
console.error("Failed to create story action:", error);
|
||||
alert("Failed to create story action. Please try again.");
|
||||
// 这里可以添加 toast 提示
|
||||
} finally {
|
||||
setLocalLoading(false);
|
||||
}
|
||||
@ -275,41 +195,16 @@ const RenderTemplateStoryMode = ({
|
||||
// 处理角色图片上传
|
||||
const handleRoleImageUpload = (roleIndex: number, file: any) => {
|
||||
if (file && selectedTemplate) {
|
||||
// 这里可以添加实际的上传逻辑
|
||||
console.log("Character image uploaded:", file.name);
|
||||
// 模拟上传成功,设置图片URL
|
||||
const imageUrl = URL.createObjectURL(file);
|
||||
setRoleImages((prev) => ({ ...prev, [roleIndex]: imageUrl }));
|
||||
|
||||
// 直接更新模板中的角色图片
|
||||
const updatedTemplate = {
|
||||
...selectedTemplate,
|
||||
storyRole: selectedTemplate.storyRole.map((role: any, index: number) =>
|
||||
index === roleIndex ? { ...role, photo_url: imageUrl } : role
|
||||
),
|
||||
};
|
||||
|
||||
setSelectedTemplate(updatedTemplate);
|
||||
setActiveRoleImage(imageUrl);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除角色图片
|
||||
const handleDeleteRoleImage = (roleIndex: number) => {
|
||||
setRoleImages((prev) => {
|
||||
const newState = { ...prev };
|
||||
delete newState[roleIndex];
|
||||
return newState;
|
||||
});
|
||||
|
||||
// 同时从模板中删除图片
|
||||
if (selectedTemplate) {
|
||||
const updatedTemplate = {
|
||||
...selectedTemplate,
|
||||
storyRole: selectedTemplate.storyRole.map((role: any, index: number) =>
|
||||
index === roleIndex ? { ...role, photo_url: "" } : role
|
||||
),
|
||||
};
|
||||
setSelectedTemplate(updatedTemplate);
|
||||
setActiveRoleImage("");
|
||||
}
|
||||
};
|
||||
// 模板列表渲染
|
||||
@ -318,7 +213,7 @@ const RenderTemplateStoryMode = ({
|
||||
<div className="w-1/3 p-6 border-r border-white/[0.1]">
|
||||
<h3 className="text-xl font-bold text-white mb-6">Story Templates</h3>
|
||||
<div className="space-y-4 max-h-[700px] overflow-y-auto pr-3 template-list-scroll">
|
||||
{templates.map((template, index) => (
|
||||
{templateStoryList.map((template, index) => (
|
||||
<div
|
||||
key={template.id}
|
||||
data-alt={`template-card-${index}`}
|
||||
@ -342,7 +237,7 @@ const RenderTemplateStoryMode = ({
|
||||
};
|
||||
// 故事编辑器渲染
|
||||
const storyEditorRender = () => {
|
||||
return loading ? (
|
||||
return isLoading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
|
||||
</div>
|
||||
@ -414,37 +309,29 @@ const RenderTemplateStoryMode = ({
|
||||
onChange={(info) => {
|
||||
if (info.file.status === "done") {
|
||||
handleRoleImageUpload(
|
||||
selectedRoleIndex,
|
||||
activeRoleIndex,
|
||||
info.file.originFileObj
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{roleImages[selectedRoleIndex] ||
|
||||
selectedTemplate.storyRole[selectedRoleIndex]
|
||||
?.photo_url ? (
|
||||
{activeRole?.photo_url ? (
|
||||
<div className="relative w-32 h-32 rounded-lg overflow-hidden">
|
||||
<img
|
||||
src={
|
||||
roleImages[selectedRoleIndex] ||
|
||||
selectedTemplate.storyRole[selectedRoleIndex]
|
||||
?.photo_url
|
||||
}
|
||||
src={activeRole.photo_url}
|
||||
alt="Character Portrait"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{roleImages[selectedRoleIndex] && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteRoleImage(selectedRoleIndex);
|
||||
}}
|
||||
className="absolute top-1 right-1 w-5 h-5 bg-red-500/80 hover:bg-red-500 text-white rounded-full flex items-center justify-center transition-colors"
|
||||
title="Delete Photo"
|
||||
>
|
||||
<Trash2 className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteRoleImage(activeRoleIndex);
|
||||
}}
|
||||
className="absolute top-1 right-1 w-5 h-5 bg-red-500/80 hover:bg-red-500 text-white rounded-full flex items-center justify-center transition-colors"
|
||||
title="Delete Photo"
|
||||
>
|
||||
<Trash2 className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
<div className="absolute inset-0 bg-black/50 opacity-0 hover:opacity-100 transition-opacity duration-200 flex items-center justify-center">
|
||||
<div className="text-white text-center">
|
||||
<UploadOutlined className="w-4 h-4 mb-1" />
|
||||
@ -468,52 +355,12 @@ const RenderTemplateStoryMode = ({
|
||||
{/* 音频操作区域 - 使用新的 AudioRecorder 组件 */}
|
||||
<div className="space-y-2">
|
||||
<AudioRecorder
|
||||
audioUrl={
|
||||
roleAudios[selectedRoleIndex] ||
|
||||
selectedTemplate.storyRole[selectedRoleIndex]?.voice_url
|
||||
}
|
||||
audioUrl={activeRole?.voice_url || ""}
|
||||
onAudioRecorded={(audioBlob, audioUrl) => {
|
||||
// 保存到状态
|
||||
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);
|
||||
}
|
||||
setActiveRoleAudio(audioUrl);
|
||||
}}
|
||||
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);
|
||||
}
|
||||
setActiveRoleAudio("");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -530,11 +377,11 @@ const RenderTemplateStoryMode = ({
|
||||
<button
|
||||
data-alt={`character-thumbnail-${index}`}
|
||||
className={`w-full aspect-square rounded-lg overflow-hidden border-2 transition-all duration-200 hover:scale-105 ${
|
||||
selectedRoleIndex === index
|
||||
activeRoleIndex === index
|
||||
? "border-blue-500 shadow-lg shadow-blue-500/30"
|
||||
: "border-white/20 hover:border-white/40"
|
||||
}`}
|
||||
onClick={() => setSelectedRoleIndex(index)}
|
||||
onClick={() => setActiveRoleIndex(index)}
|
||||
>
|
||||
<img
|
||||
src={role.photo_url}
|
||||
@ -593,10 +440,8 @@ const RenderTemplateStoryMode = ({
|
||||
open={isOpen}
|
||||
onCancel={() => {
|
||||
// 清空所有选中的内容数据
|
||||
setRoleImages({});
|
||||
setRoleAudios({});
|
||||
setSelectedTemplate(mockTemplates[0]); // 重置为第一个模板
|
||||
setSelectedRoleIndex(0);
|
||||
setSelectedTemplate(null);
|
||||
setActiveRoleIndex(0);
|
||||
onClose();
|
||||
}}
|
||||
footer={null}
|
||||
@ -642,22 +487,25 @@ export function ChatInputBox() {
|
||||
// 图片故事模式状态
|
||||
const [isPhotoStoryMode, setIsPhotoStoryMode] = useState(false);
|
||||
|
||||
// 使用图片故事服务hook
|
||||
const {
|
||||
activeImageUrl,
|
||||
activeTextContent,
|
||||
isAnalyzing,
|
||||
isUploading: isImageUploading,
|
||||
uploadAndAnalyzeImage,
|
||||
updateStoryContent,
|
||||
resetImageStory,
|
||||
} = useImageStoryServiceHook();
|
||||
|
||||
// 使用上传文件Hook
|
||||
const { uploadFile, isUploading } = useUploadFile();
|
||||
|
||||
// 共享状态 - 需要在不同渲染函数间共享
|
||||
const [script, setScript] = useState(""); // 用户输入的脚本内容
|
||||
|
||||
// 子组件引用,用于调用子组件方法
|
||||
const photoStoryModeRef = useRef<{
|
||||
enterPhotoStoryMode: () => void;
|
||||
getStoryContent: () => string;
|
||||
}>(null);
|
||||
|
||||
// 响应式管理输入框内容:当在图片故事模式下,从子组件获取故事内容
|
||||
useEffect(() => {
|
||||
if (isPhotoStoryMode && photoStoryModeRef.current) {
|
||||
const storyContent = photoStoryModeRef.current.getStoryContent();
|
||||
if (storyContent) {
|
||||
setScript(storyContent);
|
||||
}
|
||||
}
|
||||
}, [isPhotoStoryMode]);
|
||||
|
||||
const [loadingIdea, setLoadingIdea] = useState(false); // 获取创意建议时的加载状态
|
||||
const [isCreating, setIsCreating] = useState(false); // 视频创建过程中的加载状态
|
||||
|
||||
@ -669,32 +517,9 @@ export function ChatInputBox() {
|
||||
videoDuration: "1min",
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 处理图片上传和分析
|
||||
const handlePhotoUpload = async (file: File) => {
|
||||
try {
|
||||
// 先上传图片到服务器
|
||||
const uploadedImageUrl = await uploadFile(file, (progress) => {
|
||||
console.log("上传进度:", progress);
|
||||
});
|
||||
|
||||
// 使用hook处理图片上传和分析
|
||||
await uploadAndAnalyzeImage(uploadedImageUrl);
|
||||
|
||||
// 等待一小段时间确保hook状态更新完成,然后同步到输入框
|
||||
setTimeout(() => {
|
||||
setScript(activeTextContent);
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error("Photo upload or analysis failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 退出图片故事模式
|
||||
const exitPhotoStoryMode = () => {
|
||||
setIsPhotoStoryMode(false);
|
||||
resetImageStory();
|
||||
};
|
||||
|
||||
const handleGetIdea = () => {
|
||||
@ -712,36 +537,16 @@ export function ChatInputBox() {
|
||||
const handleEditorChange = (e: React.FormEvent<HTMLDivElement>) => {
|
||||
const newText = e.currentTarget.textContent || "";
|
||||
setScript(newText);
|
||||
|
||||
// 如果在图片故事模式下,同步更新hook中的故事内容
|
||||
if (isPhotoStoryMode) {
|
||||
updateStoryContent(newText);
|
||||
}
|
||||
};
|
||||
|
||||
// 当进入图片故事模式时,清空输入框内容并直接触发文件选择
|
||||
// 当进入图片故事模式时,调用子组件方法
|
||||
const enterPhotoStoryMode = () => {
|
||||
setIsPhotoStoryMode(true);
|
||||
setScript("");
|
||||
resetImageStory();
|
||||
|
||||
// 直接触发文件选择器
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "file";
|
||||
fileInput.accept = "image/*";
|
||||
fileInput.style.display = "none";
|
||||
|
||||
fileInput.onchange = (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.files && target.files[0]) {
|
||||
handlePhotoUpload(target.files[0]);
|
||||
}
|
||||
// 清理DOM
|
||||
document.body.removeChild(fileInput);
|
||||
};
|
||||
|
||||
document.body.appendChild(fileInput);
|
||||
fileInput.click();
|
||||
// 调用子组件的进入方法
|
||||
if (photoStoryModeRef.current) {
|
||||
photoStoryModeRef.current.enterPhotoStoryMode();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle creating video
|
||||
@ -752,10 +557,8 @@ export function ChatInputBox() {
|
||||
script,
|
||||
...configOptions,
|
||||
...(isPhotoStoryMode && {
|
||||
imageUrl: activeImageUrl,
|
||||
imageStory: activeTextContent,
|
||||
isAnalyzing,
|
||||
isUploading: isImageUploading
|
||||
// 从子组件获取图片故事相关信息
|
||||
isPhotoStoryMode: true,
|
||||
}),
|
||||
});
|
||||
setTimeout(() => {
|
||||
@ -844,39 +647,28 @@ export function ChatInputBox() {
|
||||
</button>
|
||||
</Dropdown>
|
||||
|
||||
{/* 图片故事模式UI */}
|
||||
{isPhotoStoryMode && <PhotoStoryMode />}
|
||||
{/* 图片故事模式UI - 始终渲染,通过ref调用方法 */}
|
||||
<PhotoStoryMode
|
||||
ref={photoStoryModeRef}
|
||||
onExitMode={exitPhotoStoryMode}
|
||||
isVisible={isPhotoStoryMode}
|
||||
/>
|
||||
|
||||
<div className="video-prompt-editor relative flex flex-1 items-center rounded-[6px]">
|
||||
{/* 可编辑的脚本输入区域 */}
|
||||
<div
|
||||
className={`editor-content flex-1 w-0 max-h-[48px] min-h-[40px] h-auto pl-[10px] pr-[10px] py-[10px] rounded-[10px] leading-[20px] text-sm border-none overflow-y-auto cursor-text flex items-center ${
|
||||
isPhotoStoryMode && isAnalyzing ? "opacity-50" : ""
|
||||
}`}
|
||||
contentEditable={!isAnalyzing}
|
||||
className="editor-content flex-1 w-0 max-h-[48px] min-h-[40px] h-auto pl-[10px] pr-[10px] py-[10px] rounded-[10px] leading-[20px] text-sm border-none overflow-y-auto cursor-text flex items-center"
|
||||
contentEditable={true}
|
||||
onInput={handleEditorChange}
|
||||
suppressContentEditableWarning
|
||||
>
|
||||
{isPhotoStoryMode && isAnalyzing ? (
|
||||
<div className="flex items-center gap-2 text-white/60">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>Analyzing your image, please wait...</span>
|
||||
</div>
|
||||
) : isPhotoStoryMode && activeTextContent ? (
|
||||
<div className="text-white/90">{activeTextContent}</div>
|
||||
) : (
|
||||
script
|
||||
)}
|
||||
{script}
|
||||
</div>
|
||||
|
||||
{/* 占位符文本和获取创意按钮 */}
|
||||
<div
|
||||
className={`custom-placeholder absolute top-[50%] left-[10px] z-10 translate-y-[-50%] flex items-center gap-1 pointer-events-none text-[14px] leading-[20px] text-white/[0.40] ${
|
||||
script ||
|
||||
(isPhotoStoryMode && isAnalyzing) ||
|
||||
(isPhotoStoryMode && activeTextContent)
|
||||
? "hidden"
|
||||
: "block"
|
||||
script ? "hidden" : "block"
|
||||
}`}
|
||||
>
|
||||
<span>Describe the content you want to action. Get an </span>
|
||||
@ -1045,3 +837,130 @@ const ConfigOptions = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 图片故事模式组件
|
||||
* 显示图片预览、分析状态和故事类型选择器
|
||||
* 使用自己的hook管理状态,通过ref暴露方法给父组件
|
||||
*/
|
||||
const PhotoStoryMode = forwardRef<
|
||||
{
|
||||
enterPhotoStoryMode: () => void;
|
||||
getStoryContent: () => string;
|
||||
},
|
||||
{
|
||||
onExitMode: () => void;
|
||||
isVisible: boolean;
|
||||
}
|
||||
>(({ onExitMode, isVisible }, ref) => {
|
||||
// 使用图片故事服务hook管理自己的状态
|
||||
const {
|
||||
activeImageUrl,
|
||||
selectedCategory,
|
||||
isAnalyzing,
|
||||
isUploading,
|
||||
storyTypeOptions,
|
||||
analyzedStoryContent,
|
||||
updateStoryType,
|
||||
resetImageStory,
|
||||
updateStoryContent,
|
||||
triggerFileSelectionAndAnalyze,
|
||||
} = useImageStoryServiceHook();
|
||||
|
||||
// 通过ref暴露方法给父组件
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
enterPhotoStoryMode: () => {
|
||||
// 触发文件选择和分析
|
||||
triggerFileSelectionAndAnalyze().catch((error: unknown) => {
|
||||
console.error("Failed to enter photo story mode:", error);
|
||||
});
|
||||
},
|
||||
getStoryContent: () => analyzedStoryContent || "",
|
||||
}),
|
||||
[triggerFileSelectionAndAnalyze, analyzedStoryContent]
|
||||
);
|
||||
|
||||
// 处理退出模式
|
||||
const handleExitMode = () => {
|
||||
resetImageStory();
|
||||
onExitMode();
|
||||
};
|
||||
|
||||
// 如果不显示,返回null
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute top-[-110px] left-14 right-0 flex items-center justify-between">
|
||||
{/* 左侧:图片预览区域和分析状态指示器 */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 图片预览区域 - 使用Ant Design Image组件 */}
|
||||
{activeImageUrl && (
|
||||
<div className="relative w-24 h-24 rounded-lg overflow-hidden bg-white/[0.05] border border-white/[0.1] shadow-[0_4px_16px_rgba(0,0,0,0.2)]">
|
||||
<Image
|
||||
src={activeImageUrl}
|
||||
alt="Story inspiration"
|
||||
className="w-full h-full object-cover"
|
||||
preview={{
|
||||
mask: <EyeOutlined className="w-6 h-6 text-white/80" />,
|
||||
maskClassName:
|
||||
"flex items-center justify-center bg-black/50 hover:bg-black/70 transition-colors",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 删除图片按钮 - 简洁样式 */}
|
||||
{activeImageUrl && (
|
||||
<button
|
||||
onClick={handleExitMode}
|
||||
className="absolute -top-2 left-24 w-6 h-6 bg-black/60 hover:bg-black/80 rounded-full flex items-center justify-center text-white/80 hover:text-white transition-all duration-200 z-10"
|
||||
title="删除图片并退出图片故事模式"
|
||||
>
|
||||
<Trash2 className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 分析状态指示器 */}
|
||||
{isAnalyzing && (
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-white/[0.1] rounded-lg">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-white/80" />
|
||||
<span className="text-sm text-white/80">
|
||||
{isUploading ? "Uploading image..." : "Analyzing image..."}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧:故事类型选择器 */}
|
||||
{activeImageUrl && (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: storyTypeOptions.map((type) => ({
|
||||
key: type.key,
|
||||
label: (
|
||||
<div className="px-3 py-2 text-sm text-white/90">
|
||||
{type.label}
|
||||
</div>
|
||||
),
|
||||
})),
|
||||
onClick: ({ key }) => updateStoryType(key),
|
||||
}}
|
||||
trigger={["click"]}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<button className="px-3 py-2 bg-white/[0.1] hover:bg-white/[0.15] border border-white/[0.2] rounded-lg text-white/80 text-sm transition-colors flex items-center gap-2">
|
||||
<span>
|
||||
{storyTypeOptions.find((t) => t.key === selectedCategory)
|
||||
?.label || "Auto"}
|
||||
</span>
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
</button>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@ -1,100 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2, Trash2, ChevronDown } from "lucide-react";
|
||||
import { Dropdown, Image } from "antd";
|
||||
import { EyeOutlined } from "@ant-design/icons";
|
||||
import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService";
|
||||
|
||||
/**
|
||||
* 图片故事模式组件
|
||||
* 显示图片预览、分析状态和故事类型选择器
|
||||
* 使用ImageStoryService hook管理状态和业务逻辑
|
||||
*/
|
||||
export function PhotoStoryMode() {
|
||||
// 使用图片故事服务hook
|
||||
const {
|
||||
activeImageUrl,
|
||||
selectedCategory,
|
||||
isAnalyzing,
|
||||
isUploading,
|
||||
storyTypeOptions,
|
||||
updateStoryType,
|
||||
resetImageStory,
|
||||
} = useImageStoryServiceHook();
|
||||
|
||||
return (
|
||||
<div className="absolute top-[-110px] left-14 right-0 flex items-center justify-between">
|
||||
{/* 左侧:图片预览区域和分析状态指示器 */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 图片预览区域 - 使用Ant Design Image组件 */}
|
||||
<div className="relative w-24 h-24 rounded-lg overflow-hidden bg-white/[0.05] border border-white/[0.1] shadow-[0_4px_16px_rgba(0,0,0,0.2)]">
|
||||
{activeImageUrl && (
|
||||
<Image
|
||||
src={activeImageUrl}
|
||||
alt="Story inspiration"
|
||||
className="w-full h-full object-cover"
|
||||
preview={{
|
||||
mask: (
|
||||
<EyeOutlined className="w-6 h-6 text-white/80" />
|
||||
),
|
||||
maskClassName:
|
||||
"flex items-center justify-center bg-black/50 hover:bg-black/70 transition-colors",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 删除图片按钮 - 简洁样式 */}
|
||||
{activeImageUrl && (
|
||||
<button
|
||||
onClick={resetImageStory}
|
||||
className="absolute -top-2 left-24 w-6 h-6 bg-black/60 hover:bg-black/80 rounded-full flex items-center justify-center text-white/80 hover:text-white transition-all duration-200 z-10"
|
||||
title="删除图片并退出图片故事模式"
|
||||
>
|
||||
<Trash2 className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 分析状态指示器 */}
|
||||
{isAnalyzing && (
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-white/[0.1] rounded-lg">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-white/80" />
|
||||
<span className="text-sm text-white/80">
|
||||
{isUploading
|
||||
? "Uploading image..."
|
||||
: "Analyzing image..."}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧:故事类型选择器 */}
|
||||
{activeImageUrl && (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: storyTypeOptions.map((type) => ({
|
||||
key: type.key,
|
||||
label: (
|
||||
<div className="px-3 py-2 text-sm text-white/90">
|
||||
{type.label}
|
||||
</div>
|
||||
),
|
||||
})),
|
||||
onClick: ({ key }) => updateStoryType(key),
|
||||
}}
|
||||
trigger={["click"]}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<button className="px-3 py-2 bg-white/[0.1] hover:bg-white/[0.15] border border-white/[0.2] rounded-lg text-white/80 text-sm transition-colors flex items-center gap-2">
|
||||
<span>
|
||||
{storyTypeOptions.find(
|
||||
(t) => t.key === selectedCategory
|
||||
)?.label || "Auto"}
|
||||
</span>
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
</button>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user