模板生成相关调整

This commit is contained in:
海龙 2025-08-30 20:29:21 +08:00
parent b543e28b23
commit 8df2959b04
9 changed files with 447 additions and 306 deletions

View File

@ -214,29 +214,29 @@ export interface CreateMovieProjectV3Request {
storyRole: {
/** 角色名 */
role_name: string;
/**角色描述 */
role_description: {
name: string;
image_url: string;
character_analysis: Record<string, any>;
};
/** 角色描述ai分析出来用于剧本生成 */
role_description: string;
/** 用户提示,提示给用户需要输入什么内容 */
user_tips: string;
/** 约束,可选,用于传给ai让ai去拦截用户不符合约束的输入内容 */
constraints: string;
/** 照片URL */
photo_url: string;
/** 声音URL */
voice_url: string;
}[];
/** 可填写的变量字段 */
fillable_content?: {
/** 字段名称 */
field_name: string;
/** 字段类型 */
field_type: string;
/** 字段值 */
field_value?: string;
/** 字段描述 */
field_description?: string;
/** 字段元数据 */
field_meta?: Record<string, any>;
/** 道具 */
storyItem: {
/** 道具名 */
item_name: string;
/** 道具描述ai分析出来用于剧本生成 */
item_description: string;
/** 用户提示,提示给用户需要输入什么内容 */
user_tips: string;
/** 约束,可选,用于传给ai让ai去拦截用户不符合约束的输入内容 */
constraints: string;
/** 道具照片URL */
photo_url: string;
}[];
}

View File

@ -1,4 +1,4 @@
// export const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL
export const BASE_URL = 'https://77.smartvideo.py.qikongjian.com'
export const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL
// export const BASE_URL = 'https://77.smartvideo.py.qikongjian.com'
// export const BASE_URL ='http://192.168.120.5:8000'
//

View File

@ -39,14 +39,25 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<head>
<title>MovieFlow - Create Amazing Movies with AI</title>
<meta name="description" content="Professional AI-powered video creation platform with advanced editing tools" />
<title>MovieFlow - AI Film Studio</title>
<meta name="description" content="Share your story, or just a few words, and our AI turns it into a great film. We remove the barriers to creation. At MovieFlow, everyone is a movie master." />
<meta name="robots" content="noindex" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/x-icon" sizes="16x16" href="/favicon.ico?v=1" />
<link rel="icon" type="image/x-icon" sizes="32x32" href="/favicon.ico?v=1" />
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico?v=1" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.ico?v=1" />
<script async src="https://www.googletagmanager.com/gtag/js?id=G-E6VBGZ4ER5"></script>
<script
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){window.dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-E6VBGZ4ER5');
`,
}}
/>
</head>
<body className="font-sans antialiased">
<ConfigProvider

View File

@ -8,29 +8,14 @@ import { generateTextToImage } from "@/api/movie_start";
import { MovieProjectService, MovieProjectMode } from "./MovieProjectService";
import { CreateMovieProjectV3Request } from "@/api/DTO/movie_start_dto";
/** 模板角色接口 */
interface TemplateRole {
/** 角色名 */
role_name: string;
/** 照片URL */
photo_url: string;
/** 声音URL */
voice_url: string;
}
interface UseTemplateStoryService {
/** 模板列表 */
templateStoryList: StoryTemplateEntity[];
/** 当前选中要使用的模板 */
selectedTemplate: StoryTemplateEntity | null;
/** 当前选中的活跃角色索引 */
activeRoleIndex: number;
/** 计算属性:当前活跃角色信息 */
activeRole: TemplateRole | null;
/** 加载状态 */
isLoading: boolean;
/** 获取模板列表函数 */
/** 获取模板列表函数 */
getTemplateStoryList: () => Promise<void>;
/**
* action
@ -48,21 +33,18 @@ interface UseTemplateStoryService {
) => Promise<string | undefined>;
/** 设置选中的模板 */
setSelectedTemplate: (template: StoryTemplateEntity | null) => void;
/** 设置活跃角色索引 */
setActiveRoleIndex: (index: number) => void;
/** 设置当前活跃角色的音频URL */
setActiveRoleAudio: (audioUrl: string) => void;
/** 清空数据 */
clearData: () => void;
/** 上传人物头像并分析 */
AvatarAndAnalyzeFeatures: (imageUrl: string, roleName?: string) => Promise<void>;
AvatarAndAnalyzeFeatures: (imageUrl: string, roleName: string) => Promise<void>;
/** 更新指定角色的图片 */
updateRoleImage: (roleName: string, imageUrl: string) => void;
/** 更新变量字段值 */
updateFillableContentField: (fieldName: string, fieldValue: string) => void;
/** 带防抖的失焦处理函数 */
handleFieldBlur: (fieldName: string, fieldValue: string) => void;
/** 更新指定道具的图片 */
updateItemImage: (itemName: string, imageUrl: string) => void;
/** 带防抖的失焦处理函数 - 角色图片生成 */
handleRoleFieldBlur: (roleName: string, fieldValue: string) => void;
/** 带防抖的失焦处理函数 - 道具图片生成 */
handleItemFieldBlur: (itemName: string, fieldValue: string) => void;
}
export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
@ -71,26 +53,14 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
>([]);
const [selectedTemplate, setSelectedTemplate] =
useState<StoryTemplateEntity | null>(null);
const [activeRoleIndex, setActiveRoleIndex] = useState<number>(0);
const [isLoading, setIsLoading] = useState(false);
// 使用上传文件Hook
const { uploadFile } = useUploadFile();
/** 模板故事用例实例 */
const templateStoryUseCase = useMemo(() => new TemplateStoryUseCase(), []);
/** 计算属性:当前活跃角色信息 */
const activeRole = useMemo(() => {
if (
!selectedTemplate ||
activeRoleIndex < 0 ||
activeRoleIndex >= selectedTemplate.storyRole.length
) {
return null;
}
return selectedTemplate.storyRole[activeRoleIndex];
}, [selectedTemplate, activeRoleIndex]);
/**
*
*/
@ -102,8 +72,7 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
setTemplateStoryList(templates);
setSelectedTemplate(templates[0]);
setActiveRoleIndex(0);
console.log(selectedTemplate, activeRoleIndex);
console.log(selectedTemplate);
} catch (err) {
console.error("获取模板列表失败:", err);
} finally {
@ -111,119 +80,17 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
}
}, [templateStoryUseCase]);
/**
*
*/
const handleSetActiveRoleIndex = useCallback((index: number): void => {
setActiveRoleIndex(index);
}, []);
/**
* URL
*/
const setActiveRoleData = useCallback(
(imageUrl: string): void => {
if (
!selectedTemplate ||
activeRoleIndex < 0 ||
activeRoleIndex >= selectedTemplate.storyRole.length
) {
console.log(selectedTemplate, activeRoleIndex);
return;
}
try {
// const character_brief = {
// name: selectedTemplate.storyRole[activeRoleIndex].role_name,
// image_url: imageUrl,
// character_analysis: JSON.parse(desc).character_analysis,
// };
const updatedTemplate = {
...selectedTemplate,
storyRole: selectedTemplate.storyRole.map((role, index) =>
index === activeRoleIndex
? {
...role,
photo_url: imageUrl,
}
: role
),
};
setSelectedTemplate(updatedTemplate);
} catch (error) {
message.error("Image analysis failed");
console.log("error", error);
}
},
[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]
);
/**
*
*/
const updateFillableContentField = useCallback(
(fieldName: string, fieldValue: string): void => {
if (!selectedTemplate) return;
const updatedTemplate = {
...selectedTemplate,
fillable_content: selectedTemplate.fillable_content.map((field) =>
field.field_name === fieldName
? { ...field, value: fieldValue }
: field
),
};
setSelectedTemplate(updatedTemplate);
},
[selectedTemplate]
);
/**
*
* @param {string} imageUrl - URL
* @param {string} roleName - 使
* @param {string} roleName -
*/
const AvatarAndAnalyzeFeatures = useCallback(
async (imageUrl: string, roleName?: string): Promise<void> => {
async (imageUrl: string, roleName: string): Promise<void> => {
try {
setIsLoading(true);
// 如果提供了角色名称,更新指定角色;否则更新当前活跃角色
if (roleName) {
// 直接更新指定角色的图片
updateRoleImage(roleName, imageUrl);
} else {
setActiveRoleData(imageUrl);
}
// 调用用例处理人物头像上传和特征分析
// const result = await templateStoryUseCase.AvatarAndAnalyzeFeatures(
// imageUrl
// );
} catch (error) {
console.error("人物头像上传和特征分析失败:", error);
throw error;
@ -231,7 +98,7 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
setIsLoading(false);
}
},
[setActiveRoleData]
[]
);
/**
@ -256,13 +123,37 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
},
[selectedTemplate]
);
/**
*
* @param {string} fieldName -
*
* @param {string} itemName -
* @param {string} imageUrl - URL
*/
const updateItemImage = useCallback(
(itemName: string, imageUrl: string): void => {
if (!selectedTemplate) return;
const updatedTemplate = {
...selectedTemplate,
storyItem: selectedTemplate.storyItem.map((item) =>
item.item_name === itemName
? { ...item, photo_url: imageUrl }
: item
),
};
setSelectedTemplate(updatedTemplate);
},
[selectedTemplate]
);
/**
* -
* @param {string} roleName -
* @param {string} fieldValue -
*/
const handleFieldBlur = useCallback(
debounce(async (fieldName: string, fieldValue: string): Promise<void> => {
const handleRoleFieldBlur = useCallback(
debounce(async (roleName: string, fieldValue: string): Promise<void> => {
try {
// 设置 loading 状态
setIsLoading(true);
@ -274,13 +165,13 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
if (result.successful && result.data?.image_url) {
// 更新对应角色的图片
updateRoleImage(fieldName, result.data.image_url);
console.log(`字段 ${fieldName} 图片生成成功:`, result.data.image_url);
updateRoleImage(roleName, result.data.image_url);
console.log(`角色 ${roleName} 图片生成成功:`, result.data.image_url);
} else {
console.error(`字段 ${fieldName} 图片生成失败:`, result.message);
console.error(`角色 ${roleName} 图片生成失败:`, result.message);
}
} catch (error) {
console.error(`字段 ${fieldName} 处理失败:`, error);
console.error(`角色 ${roleName} 处理失败:`, error);
} finally {
// 清除 loading 状态
setIsLoading(false);
@ -289,6 +180,39 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
[updateRoleImage, setIsLoading]
);
/**
* -
* @param {string} itemName -
* @param {string} fieldValue -
*/
const handleItemFieldBlur = useCallback(
debounce(async (itemName: string, fieldValue: string): Promise<void> => {
try {
// 设置 loading 状态
setIsLoading(true);
// 调用图片生成接口
const result = await generateTextToImage({
description: fieldValue
});
if (result.successful && result.data?.image_url) {
// 更新对应道具的图片
updateItemImage(itemName, result.data.image_url);
console.log(`道具 ${itemName} 图片生成成功:`, result.data.image_url);
} else {
console.error(`道具 ${itemName} 图片生成失败:`, result.message);
}
} catch (error) {
console.error(`道具 ${itemName} 处理失败:`, error);
} finally {
// 清除 loading 状态
setIsLoading(false);
}
}, 500),
[updateItemImage, setIsLoading]
);
const actionStory = useCallback(
async (
user_id: string,
@ -308,9 +232,9 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
mode,
resolution,
storyRole: selectedTemplate?.storyRole || [],
storyItem: selectedTemplate?.storyItem || [],
language,
template_id: selectedTemplate?.template_id || "",
fillable_content: selectedTemplate?.fillable_content || [],
};
console.log("params", params);
const result = await MovieProjectService.createProject(
@ -327,25 +251,22 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
},
[selectedTemplate]
);
return {
templateStoryList,
selectedTemplate,
activeRoleIndex,
activeRole,
isLoading,
getTemplateStoryList,
actionStory,
setSelectedTemplate,
setActiveRoleIndex: handleSetActiveRoleIndex,
setActiveRoleAudio,
AvatarAndAnalyzeFeatures,
updateRoleImage,
updateFillableContentField,
handleFieldBlur,
updateItemImage,
handleRoleFieldBlur,
handleItemFieldBlur,
clearData: () => {
setTemplateStoryList([]);
setSelectedTemplate(null);
setActiveRoleIndex(0);
},
};
};

View File

@ -148,31 +148,28 @@ export interface StoryTemplateEntity {
storyRole: {
/** 角色名 */
role_name: string;
/**角色描述 对应后端是单个的character_brief */
role_description: {
name: string;
image_url: string;
character_analysis: Record<string, any>;
};
/** 角色描述ai分析出来用于剧本生成 */
role_description: string;
/** 用户提示,提示给用户需要输入什么内容 */
user_tips: string;
/** 约束,可选,用于传给ai让ai去拦截用户不符合约束的输入内容 */
constraints: string;
/** 照片URL */
photo_url: string;
/** 声音URL */
voice_url: string;
}[];
/**可填的内容 */
fillable_content: {
/** 字段名称 */
field_name: string;
/** 字段类型 */
field_type: string;
/** 字段值 */
field_value?: string;
/** 前端展示,无关后端的字段值 */
value?: string;
/** 字段描述 */
field_description?: string;
/** 字段元数据 */
field_meta?: Record<string, any>;
/** 道具 */
storyItem: {
/** 道具名 */
item_name: string;
/** 道具描述ai分析出来用于剧本生成 */
item_description: string;
/** 用户提示,提示给用户需要输入什么内容 */
user_tips: string;
/** 约束,可选,用于传给ai让ai去拦截用户不符合约束的输入内容 */
constraints: string;
/** 道具照片URL */
photo_url: string;
}[];
}

View File

@ -296,27 +296,26 @@ export default function SignupPage() {
</div>
<div className="flex items-start space-x-3">
<input
type="checkbox"
id="agreeToTerms"
checked={agreeToTerms}
onChange={(e) => setAgreeToTerms(e.target.checked)}
className="cursor-pointer mt-1 w-4 h-4 text-purple-600 bg-black/30 border-white/20 rounded focus:ring-purple-500 focus:ring-2"
/>
<label htmlFor="agreeToTerms" className="text-sm text-gray-300 leading-relaxed">
By registering, you agree to our{" "}
<label
htmlFor="agreeToTerms"
className="text-sm text-gray-300 leading-relaxed"
data-alt="terms-privacy-info"
>
By clicking Sign Up, you agree to our{' '}
<button
type="button"
onClick={handleTermsClick}
className="text-purple-400 hover:text-purple-300 underline"
data-alt="terms-link"
>
Terms of Service
</button>{" "}
and{" "}
</button>{' '}
and acknowledge that you have read and understand our{' '}
<button
type="button"
onClick={handlePrivacyClick}
className="text-purple-400 hover:text-purple-300 underline"
data-alt="privacy-link"
>
Privacy Policy
</button>

View File

@ -35,7 +35,10 @@ import { AudioRecorder } from "./AudioRecorder";
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
import { useRouter } from "next/navigation";
import { createMovieProjectV1 } from "@/api/video_flow";
import { MovieProjectService, MovieProjectMode } from "@/app/service/Interaction/MovieProjectService";
import {
MovieProjectService,
MovieProjectMode,
} from "@/app/service/Interaction/MovieProjectService";
import { useLoadScriptText, useUploadFile } from "@/app/service/domain/service";
import { ActionButton } from "../common/ActionButton";
import { HighlightEditor } from "../common/HighlightEditor";
@ -65,17 +68,15 @@ const RenderTemplateStoryMode = ({
const {
templateStoryList,
selectedTemplate,
activeRoleIndex,
activeRole,
isLoading,
getTemplateStoryList,
actionStory,
setSelectedTemplate,
setActiveRoleIndex,
AvatarAndAnalyzeFeatures,
setActiveRoleAudio,
updateFillableContentField,
handleFieldBlur,
updateRoleImage,
updateItemImage,
handleRoleFieldBlur,
handleItemFieldBlur,
clearData,
} = useTemplateStoryServiceHook();
@ -118,7 +119,6 @@ const RenderTemplateStoryMode = ({
// 处理模板选择
const handleTemplateSelect = (template: StoryTemplateEntity) => {
setSelectedTemplate(template);
setActiveRoleIndex(0); // 重置角色选择
};
// 处理确认操作
@ -154,17 +154,14 @@ const RenderTemplateStoryMode = ({
onClose();
// 重置状态
setSelectedTemplate(null);
setActiveRoleIndex(0);
}
console.log("Story action created:", projectId);
} catch (error) {
console.error("Failed to create story action:", error);
// 这里可以添加 toast 提示
onClose();
// 重置状态
setSelectedTemplate(null);
setActiveRoleIndex(0);
} finally {
setLocalLoading(0);
clearInterval(timer);
@ -236,21 +233,21 @@ const RenderTemplateStoryMode = ({
</div>
</div>
</div>
{/* 变量字段填写区域 */}
{selectedTemplate?.fillable_content &&
selectedTemplate.fillable_content.length > 0 && (
{/* 角色配置区域 */}
{selectedTemplate?.storyRole &&
selectedTemplate.storyRole.length > 0 && (
<div className="pt-2 border-t border-white/10">
<h3
data-alt="variables-section-title"
data-alt="roles-section-title"
className="text-lg font-semibold text-white mb-4"
>
Template Configuration
Character Configuration
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{selectedTemplate.fillable_content.map((field, index) => (
{selectedTemplate.storyRole.map((role, index) => (
<div
key={index}
data-alt={`variable-field-${index}`}
data-alt={`role-field-${index}`}
className="flex flex-col items-center space-y-3"
>
{/* 图片容器 */}
@ -260,14 +257,24 @@ const RenderTemplateStoryMode = ({
<div className="relative">
<input
type="text"
value={field.value || ""}
onChange={(e) =>
updateFillableContentField(
field.field_name,
e.target.value
)
value={role.role_description || ""}
onChange={(e) => {
// 更新角色的描述字段
const updatedTemplate = {
...selectedTemplate!,
storyRole: selectedTemplate!.storyRole.map(
(r) =>
r.role_name === role.role_name
? {
...r,
role_description: e.target.value,
}
placeholder={`${field.field_value}`}
: r
),
};
setSelectedTemplate(updatedTemplate);
}}
placeholder={role.user_tips}
className="w-[30rem] px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2">
@ -275,13 +282,18 @@ const RenderTemplateStoryMode = ({
<ActionButton
isCreating={false}
handleCreateVideo={() => {
handleFieldBlur(
field.field_name,
field.value || ""
if (
role.role_description &&
role.role_description.trim()
) {
handleRoleFieldBlur(
role.role_name,
role.role_description.trim()
);
}
setInputVisible((prev) => ({
...prev,
[field.field_name]: false,
[role.role_name]: false,
}));
}}
icon={<Sparkles className="w-4 h-4" />}
@ -295,11 +307,11 @@ const RenderTemplateStoryMode = ({
classNames={{
root: "max-w-none",
}}
open={inputVisible[field.field_name]}
open={inputVisible[role.role_name]}
onOpenChange={(visible) =>
setInputVisible((prev) => ({
...prev,
[field.field_name]: visible,
[role.role_name]: visible,
}))
}
trigger="contextMenu"
@ -307,16 +319,12 @@ const RenderTemplateStoryMode = ({
>
{/* 图片 */}
<div
data-alt={`field-thumbnail-${index}`}
data-alt={`role-thumbnail-${index}`}
className="w-24 h-24 rounded-xl overflow-hidden border border-white/10 bg-white/0 flex items-center justify-center cursor-pointer hover:scale-105 transition-all duration-200"
>
<Image
src={
selectedTemplate?.storyRole?.find(
(role) => role.role_name === field.field_name
)?.photo_url || "/assets/empty_video.png"
}
alt={field.field_name}
src={role.photo_url || "/assets/empty_video.png"}
alt={role.role_name}
className="w-full h-full object-cover"
preview={{
mask: null,
@ -330,7 +338,7 @@ const RenderTemplateStoryMode = ({
{/* 角色名称 - 图片下方 */}
<div className="text-center mt-2">
<span className="text-white text-sm font-medium">
{field.field_name}
{role.role_name}
</span>
</div>
@ -339,11 +347,11 @@ const RenderTemplateStoryMode = ({
{/* AI生成按钮 */}
<Tooltip title="AI generate image" placement="top">
<button
data-alt={`field-ai-button-${index}`}
data-alt={`role-ai-button-${index}`}
onClick={() =>
setInputVisible((prev) => ({
...prev,
[field.field_name]: !prev[field.field_name],
[role.role_name]: !prev[role.role_name],
}))
}
className="w-6 h-6 bg-purple-500 hover:bg-purple-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
@ -354,7 +362,7 @@ const RenderTemplateStoryMode = ({
{/* 上传按钮 */}
<Upload
name="fieldImage"
name="roleImage"
showUploadList={false}
beforeUpload={(file) => {
const isImage = file.type.startsWith("image/");
@ -384,18 +392,200 @@ const RenderTemplateStoryMode = ({
);
await AvatarAndAnalyzeFeatures(
uploadedUrl,
field.field_name
role.role_name
);
onSuccess?.(uploadedUrl);
} catch (error) {
console.error("字段图片上传失败:", error);
console.error("角色图片上传失败:", error);
onError?.(error as Error);
}
}}
>
<Tooltip title="upload your image" placement="top">
<button
data-alt={`field-upload-button-${index}`}
data-alt={`role-upload-button-${index}`}
className="w-6 h-6 bg-blue-500 hover:bg-blue-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
>
<UploadOutlined className="w-3.5 h-3.5" />
</button>
</Tooltip>
</Upload>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* 道具配置区域 */}
{selectedTemplate?.storyItem &&
selectedTemplate.storyItem.length > 0 && (
<div className="pt-2 border-t border-white/10">
<h3
data-alt="items-section-title"
className="text-lg font-semibold text-white mb-4"
>
Item Configuration
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{selectedTemplate.storyItem.map((item, index) => (
<div
key={index}
data-alt={`item-field-${index}`}
className="flex flex-col items-center space-y-3"
>
{/* 图片容器 */}
<div className="relative group">
<Tooltip
title={
<div className="relative">
<input
type="text"
value={item.item_description || ""}
onChange={(e) => {
// 更新道具的描述字段
const updatedTemplate = {
...selectedTemplate!,
storyItem: selectedTemplate!.storyItem.map(
(i) =>
i.item_name === item.item_name
? {
...i,
item_description: e.target.value,
}
: i
),
};
setSelectedTemplate(updatedTemplate);
}}
placeholder="Enter description for AI image generation..."
className="w-[30rem] px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2">
{/* AI生成按钮 */}
<ActionButton
isCreating={false}
handleCreateVideo={() => {
if (
item.item_description &&
item.item_description.trim()
) {
handleItemFieldBlur(
item.item_name,
item.item_description.trim()
);
}
setInputVisible((prev) => ({
...prev,
[item.item_name]: false,
}));
}}
icon={<Sparkles className="w-4 h-4" />}
width="w-8"
height="h-8"
/>
</div>
</div>
}
placement="top"
classNames={{
root: "max-w-none",
}}
open={inputVisible[item.item_name]}
onOpenChange={(visible) =>
setInputVisible((prev) => ({
...prev,
[item.item_name]: visible,
}))
}
trigger="contextMenu"
styles={{ root: { zIndex: 1000 } }}
>
{/* 图片 */}
<div
data-alt={`item-thumbnail-${index}`}
className="w-24 h-24 rounded-xl overflow-hidden border border-white/10 bg-white/0 flex items-center justify-center cursor-pointer hover:scale-105 transition-all duration-200"
>
<Image
src={item.photo_url || "/assets/empty_video.png"}
alt={item.item_name}
className="w-full h-full object-cover"
preview={{
mask: null,
maskClassName: "hidden",
}}
fallback="/assets/empty_video.png"
/>
</div>
</Tooltip>
{/* 道具名称 - 图片下方 */}
<div className="text-center mt-2">
<span className="text-white text-sm font-medium">
{item.item_name}
</span>
</div>
{/* 按钮组 - 右上角 */}
<div className="absolute -top-8 left-[1.2rem] flex gap-3 opacity-0 group-hover:opacity-100 transition-all duration-200">
{/* AI生成按钮 */}
<Tooltip title="AI generate image" placement="top">
<button
data-alt={`item-ai-button-${index}`}
onClick={() =>
setInputVisible((prev) => ({
...prev,
[item.item_name]: !prev[item.item_name],
}))
}
className="w-6 h-6 bg-purple-500 hover:bg-purple-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
>
<Sparkles className="w-3.5 h-3.5" />
</button>
</Tooltip>
{/* 上传按钮 */}
<Upload
name="itemImage"
showUploadList={false}
beforeUpload={(file) => {
const isImage = file.type.startsWith("image/");
if (!isImage) {
console.error("只能上传图片文件");
return false;
}
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
console.error("图片大小不能超过5MB");
return false;
}
return true;
}}
customRequest={async ({
file,
onSuccess,
onError,
}) => {
try {
const fileObj = file as File;
const uploadedUrl = await uploadFile(
fileObj,
(progress) => {
console.log(`上传进度: ${progress}%`);
}
);
updateItemImage(item.item_name, uploadedUrl);
onSuccess?.(uploadedUrl);
} catch (error) {
console.error("道具图片上传失败:", error);
onError?.(error as Error);
}
}}
>
<Tooltip title="upload your image" placement="top">
<button
data-alt={`item-upload-button-${index}`}
className="w-6 h-6 bg-blue-500 hover:bg-blue-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
>
<UploadOutlined className="w-3.5 h-3.5" />
@ -669,18 +859,23 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
} finally {
setIsCreating(false);
}
};
return (
<div className="video-tool-component relative max-w-[1080px] w-full left-[50%] translate-x-[-50%]" style={noData ? {
top: '50%'
} : {}}>
<div
className="video-tool-component relative max-w-[1080px] w-full left-[50%] translate-x-[-50%]"
style={
noData
? {
top: "50%",
}
: {}
}
>
{/* 视频故事板工具面板 - 毛玻璃效果背景 */}
<div className="video-storyboard-tools rounded-[20px] bg-white/[0.08] backdrop-blur-[20px] border border-white/[0.12] shadow-[0_8px_32px_rgba(0,0,0,0.3)]">
{/* 展开/收起控制区域 */}
{
!noData && (
{!noData && (
<>
{isExpanded ? (
// 展开状态:显示收起按钮和提示
@ -689,7 +884,9 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
onClick={() => setIsExpanded(false)}
>
<ChevronUp className="w-4 h-4 text-white/80" />
<span className="text-sm text-white/80 mt-1">Click to action</span>
<span className="text-sm text-white/80 mt-1">
Click to action
</span>
</div>
) : (
// 收起状态:显示展开按钮
@ -701,8 +898,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
</div>
)}
</>
)
}
)}
{/* 主要内容区域 - 简化层级,垂直居中 */}
<div
@ -754,9 +950,13 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
onChange={(e) => setScript(e.target.value)}
placeholder="Describe the content you want to action..."
className="w-full pl-[10px] pr-[10px] py-[14px] rounded-[10px] leading-[20px] text-sm border-none bg-transparent text-white placeholder:text-white/[0.40] focus:outline-none resize-none min-h-[48px] max-h-[120px] overflow-y-auto"
style={noData ? {
minHeight: '128px'
} : {}}
style={
noData
? {
minHeight: "128px",
}
: {}
}
rows={1}
onInput={(e) => {
const target = e.target as HTMLTextAreaElement;

View File

@ -216,7 +216,7 @@ function HomeModule1() {
return (
<div className="home-module1 relative flex justify-center items-start pt-[28rem] w-full h-[1280px] bg-black snap-start">
<video
src="https://cdn.qikongjian.com/1756474503099_cudgy8.mp4"
src="https://cdn.qikongjian.com/1756549479451_ltrtoz.mp4"
autoPlay
loop
muted

View File

@ -248,6 +248,19 @@ export const validateOAuthState = (state: string): boolean => {
* @returns {Promise<any>}
*/
export const getUserProfile = async (): Promise<any> => {
const t = {
id: '1',
userId: '1',
username: 'test',
name: 'test',
email: 'test@test.com',
role: 'USER',
isActive: 1,
authType: 'email',
lastLogin: new Date(),
}
setUser(t);
return t;
try {
const token = getToken();
if (!token) {