forked from 77media/video-flow
重构模板故事模式,添加音频录制组件,优化角色头像上传与分析功能
This commit is contained in:
parent
47a85428f5
commit
84b6662a51
90
README.md
90
README.md
@ -2,97 +2,7 @@
|
|||||||
|
|
||||||
一个基于Next.js的视频创作工具,支持多种创作模式和故事模板。
|
一个基于Next.js的视频创作工具,支持多种创作模式和故事模板。
|
||||||
|
|
||||||
## 故事模板组件
|
|
||||||
|
|
||||||
### 功能特性
|
|
||||||
|
|
||||||
故事模板交互组件 (`renderTemplateStoryMode`) 提供了以下功能:
|
|
||||||
|
|
||||||
#### 1. 模板列表显示
|
|
||||||
- 从 `StoryTemplateEntity` API 请求模板数据
|
|
||||||
- 以横向滚动图标列表形式展示多个模板选项
|
|
||||||
- 每个模板显示预览图片和名称
|
|
||||||
- 支持加载状态和错误处理
|
|
||||||
|
|
||||||
#### 2. 模板详情弹窗
|
|
||||||
- 用户点击模板图标后弹出模态框
|
|
||||||
- 弹窗宽度为80%,居中显示
|
|
||||||
- 顶部布局:
|
|
||||||
- 左侧:大图片预览(40%宽度)
|
|
||||||
- 右侧:故事模板名称和提示词描述
|
|
||||||
- 图片支持鼠标悬停动画效果(轻微缩放和旋转)
|
|
||||||
|
|
||||||
#### 3. 角色自定义功能
|
|
||||||
- 显示可演绎的角色列表(基于模板数据)
|
|
||||||
- 每个角色支持:
|
|
||||||
- 上传图片替换默认角色图像
|
|
||||||
- 录制音频功能
|
|
||||||
- 上传音频文件功能
|
|
||||||
- 交互式按钮设计,支持悬停效果
|
|
||||||
|
|
||||||
#### 4. 确认操作
|
|
||||||
- 弹窗底部提供"取消"和"确定"按钮
|
|
||||||
- 确定按钮执行空函数(可后续替换为实际API调用)
|
|
||||||
- 支持点击遮罩层关闭弹窗
|
|
||||||
|
|
||||||
### 技术实现
|
|
||||||
|
|
||||||
#### 状态管理
|
|
||||||
```typescript
|
|
||||||
const [templates, setTemplates] = useState<StoryTemplateEntity[]>([]);
|
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<StoryTemplateEntity | null>(null);
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 核心功能
|
|
||||||
- **模板数据获取**: 模拟API调用,支持异步加载
|
|
||||||
- **模板选择**: 点击模板图标打开详情弹窗
|
|
||||||
- **资源上传**: 支持图片和音频文件上传
|
|
||||||
- **音频录制**: 预留音频录制接口
|
|
||||||
- **响应式设计**: 使用Tailwind CSS实现现代UI
|
|
||||||
|
|
||||||
#### 样式特点
|
|
||||||
- 遵循现有组件设计风格
|
|
||||||
- 使用毛玻璃效果和渐变背景
|
|
||||||
- 支持悬停动画和过渡效果
|
|
||||||
- 响应式布局,适配不同屏幕尺寸
|
|
||||||
|
|
||||||
### 使用方法
|
|
||||||
|
|
||||||
1. 在 `ChatInputBox` 组件中切换到 "template" 标签页
|
|
||||||
2. 浏览横向滚动的模板列表
|
|
||||||
3. 点击感兴趣的模板图标
|
|
||||||
4. 在弹窗中查看模板详情和自定义角色
|
|
||||||
5. 上传角色图片和音频资源
|
|
||||||
6. 点击确定完成模板选择
|
|
||||||
|
|
||||||
### 数据结构
|
|
||||||
|
|
||||||
组件使用 `StoryTemplateEntity` 接口定义模板数据结构:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface StoryTemplateEntity {
|
|
||||||
readonly id: string;
|
|
||||||
name: string;
|
|
||||||
imageUrl: string;
|
|
||||||
generateText: string;
|
|
||||||
storyRole: string[];
|
|
||||||
userResources: {
|
|
||||||
role_name: string;
|
|
||||||
photo_url: string;
|
|
||||||
voice_url: string;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 扩展建议
|
|
||||||
|
|
||||||
- 集成真实的API接口替换模拟数据
|
|
||||||
- 添加音频预览播放器功能
|
|
||||||
- 实现上传进度条和状态提示
|
|
||||||
- 支持模板收藏和最近使用功能
|
|
||||||
- 添加模板搜索和分类筛选
|
|
||||||
|
|
||||||
## 启动开发服务器
|
## 启动开发服务器
|
||||||
|
|
||||||
|
|||||||
@ -60,6 +60,8 @@ interface UseImageStoryService {
|
|||||||
setCharactersAnalysis: Dispatch<SetStateAction<CharacterAnalysis[]>>;
|
setCharactersAnalysis: Dispatch<SetStateAction<CharacterAnalysis[]>>;
|
||||||
/** 设置原始用户描述 */
|
/** 设置原始用户描述 */
|
||||||
setOriginalUserDescription: Dispatch<SetStateAction<string>>;
|
setOriginalUserDescription: Dispatch<SetStateAction<string>>;
|
||||||
|
/** 上传人物头像并分析特征,替换旧的角色数据 */
|
||||||
|
uploadCharacterAvatarAndAnalyzeFeatures: (characterName: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useImageStoryServiceHook = (): UseImageStoryService => {
|
export const useImageStoryServiceHook = (): UseImageStoryService => {
|
||||||
@ -479,6 +481,42 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
|||||||
console.error("创建电影项目失败:", error);
|
console.error("创建电影项目失败:", error);
|
||||||
}
|
}
|
||||||
}, [hasAnalyzed, storyContent, charactersAnalysis, selectedCategory, activeImageUrl]);
|
}, [hasAnalyzed, storyContent, charactersAnalysis, selectedCategory, activeImageUrl]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传人物头像并分析特征,替换旧的角色数据
|
||||||
|
* @param {string} characterName - 角色名称
|
||||||
|
*/
|
||||||
|
const uploadCharacterAvatarAndAnalyzeFeatures = useCallback(async (characterName: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
// 调用用例处理人物头像上传和特征分析
|
||||||
|
const result = await imageStoryUseCase.uploadCharacterAvatarAndAnalyzeFeatures(
|
||||||
|
uploadFile
|
||||||
|
);
|
||||||
|
|
||||||
|
// 用新的头像和特征描述替换旧的角色数据
|
||||||
|
setCharactersAnalysis((prev) =>
|
||||||
|
prev.map((char) =>
|
||||||
|
char.role_name === characterName
|
||||||
|
? {
|
||||||
|
...char,
|
||||||
|
crop_url: result.crop_url,
|
||||||
|
whisk_caption: result.whisk_caption
|
||||||
|
}
|
||||||
|
: char
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("人物头像和特征描述更新成功:", result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("人物头像上传和特征分析失败:", error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [imageStoryUseCase, uploadFile]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
imageStory,
|
imageStory,
|
||||||
activeImageUrl,
|
activeImageUrl,
|
||||||
@ -501,5 +539,6 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
|
|||||||
resetImageStory,
|
resetImageStory,
|
||||||
setOriginalUserDescription,
|
setOriginalUserDescription,
|
||||||
actionMovie,
|
actionMovie,
|
||||||
|
uploadCharacterAvatarAndAnalyzeFeatures,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -62,8 +62,81 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const templates = await templateStoryUseCase.getTemplateStoryList();
|
// const templates = await templateStoryUseCase.getTemplateStoryList();
|
||||||
|
|
||||||
|
const templates = await new Promise<StoryTemplateEntity[]>((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: '奇幻冒险故事',
|
||||||
|
imageUrl: ['/assets/3dr_howlcastle.png', '/assets/3dr_spirited.jpg'],
|
||||||
|
generateText: '一个关于勇气与友谊的奇幻冒险故事,主角在神秘世界中寻找失落的宝藏,结识了各种奇特的生物和伙伴。',
|
||||||
|
storyRole: [
|
||||||
|
{
|
||||||
|
role_name: '艾莉娅',
|
||||||
|
photo_url: '/assets/3dr_chihiro.png',
|
||||||
|
voice_url:""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role_name: '魔法师梅林',
|
||||||
|
photo_url: '/assets/3dr_mono.png',
|
||||||
|
voice_url:""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role_name: '守护者龙',
|
||||||
|
photo_url: '/assets/3dr_howlbg.jpg',
|
||||||
|
voice_url:""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: '科幻探索之旅',
|
||||||
|
imageUrl: ['/assets/3dr_monobg.jpg'],
|
||||||
|
generateText: '未来世界的太空探索故事,人类在浩瀚宇宙中寻找新的家园,面对未知的挑战和外星文明的接触。',
|
||||||
|
storyRole: [
|
||||||
|
{
|
||||||
|
role_name: '船长凯特',
|
||||||
|
photo_url: '/assets/3dr_chihiro.png',
|
||||||
|
voice_url:""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role_name: 'AI助手诺娃',
|
||||||
|
photo_url: '/assets/3dr_mono.png',
|
||||||
|
voice_url:""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: '温馨家庭喜剧',
|
||||||
|
imageUrl: ['/assets/3dr_spirited.jpg'],
|
||||||
|
generateText: '一个充满欢笑和温情的家庭故事,讲述家庭成员之间的日常趣事,以及如何一起面对生活中的小挑战。',
|
||||||
|
storyRole: [
|
||||||
|
{
|
||||||
|
role_name: '妈妈莉莉',
|
||||||
|
photo_url: '/assets/3dr_chihiro.png',
|
||||||
|
voice_url:""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role_name: '爸爸汤姆',
|
||||||
|
photo_url: '/assets/3dr_mono.png',
|
||||||
|
voice_url:""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role_name: '孩子小杰',
|
||||||
|
photo_url: '/assets/3dr_howlbg.jpg',
|
||||||
|
voice_url:""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
setTemplateStoryList(templates);
|
setTemplateStoryList(templates);
|
||||||
|
setSelectedTemplate(templates[0]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('获取模板列表失败:', err);
|
console.error('获取模板列表失败:', err);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { ImageStoryEntity } from "../domain/Entities";
|
import { ImageStoryEntity } from "../domain/Entities";
|
||||||
import { AIGenerateImageStory } from "@/api/movie_start";
|
import { AIGenerateImageStory } from "@/api/movie_start";
|
||||||
import { MovieStartDTO, CharacterAnalysis } from "@/api/DTO/movie_start_dto";
|
import { MovieStartDTO, CharacterAnalysis } from "@/api/DTO/movie_start_dto";
|
||||||
|
import { analyzeImageDescription } from "@/api/video_flow";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图片故事用例
|
* 图片故事用例
|
||||||
@ -8,7 +9,7 @@ import { MovieStartDTO, CharacterAnalysis } from "@/api/DTO/movie_start_dto";
|
|||||||
*/
|
*/
|
||||||
export class ImageStoryUseCase {
|
export class ImageStoryUseCase {
|
||||||
/** 当前图片故事数据 */
|
/** 当前图片故事数据 */
|
||||||
imageStory: ImageStoryEntity = {
|
imageStory: ImageStoryEntity = {
|
||||||
id: "",
|
id: "",
|
||||||
imageAnalysis: "",
|
imageAnalysis: "",
|
||||||
roleImage: [],
|
roleImage: [],
|
||||||
@ -18,23 +19,21 @@ export class ImageStoryUseCase {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** 故事梗概 */
|
/** 故事梗概 */
|
||||||
storyLogline: string = "";
|
storyLogline: string = "";
|
||||||
|
|
||||||
/** 角色头像及名称数据 */
|
/** 角色头像及名称数据 */
|
||||||
charactersAnalysis: CharacterAnalysis[] = [];
|
charactersAnalysis: CharacterAnalysis[] = [];
|
||||||
|
|
||||||
/** 分类数据 */
|
/** 分类数据 */
|
||||||
potentialGenres: string[] = [];
|
potentialGenres: string[] = [];
|
||||||
|
|
||||||
/** 是否正在分析图片 */
|
/** 是否正在分析图片 */
|
||||||
isAnalyzing: boolean = false;
|
isAnalyzing: boolean = false;
|
||||||
|
|
||||||
/** 是否正在上传 */
|
/** 是否正在上传 */
|
||||||
isUploading: boolean = false;
|
isUploading: boolean = false;
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置图片故事数据
|
* 设置图片故事数据
|
||||||
* @param {Partial<ImageStoryEntity>} data - 要设置的图片故事数据
|
* @param {Partial<ImageStoryEntity>} data - 要设置的图片故事数据
|
||||||
@ -71,13 +70,12 @@ export class ImageStoryUseCase {
|
|||||||
try {
|
try {
|
||||||
this.isUploading = false; // 图片已上传,设置上传状态为false
|
this.isUploading = false; // 图片已上传,设置上传状态为false
|
||||||
this.isAnalyzing = true;
|
this.isAnalyzing = true;
|
||||||
console.log('imageUrl', imageUrl)
|
console.log("imageUrl", imageUrl);
|
||||||
// 设置上传后的图片URL
|
// 设置上传后的图片URL
|
||||||
this.setImageStory({ imageUrl });
|
this.setImageStory({ imageUrl });
|
||||||
|
|
||||||
// 调用AI分析接口
|
// 调用AI分析接口
|
||||||
return await this.analyzeImageWithAI();
|
return await this.analyzeImageWithAI();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("图片分析失败:", error);
|
console.error("图片分析失败:", error);
|
||||||
// 分析失败时清空图片URL
|
// 分析失败时清空图片URL
|
||||||
@ -92,8 +90,8 @@ export class ImageStoryUseCase {
|
|||||||
* 使用AI分析图片
|
* 使用AI分析图片
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async analyzeImageWithAI() {
|
async analyzeImageWithAI() {
|
||||||
console.log('this.imageStory.imageUrl', this.imageStory.imageUrl)
|
console.log("this.imageStory.imageUrl", this.imageStory.imageUrl);
|
||||||
try {
|
try {
|
||||||
//
|
//
|
||||||
// 调用AI分析接口
|
// 调用AI分析接口
|
||||||
@ -122,7 +120,7 @@ export class ImageStoryUseCase {
|
|||||||
* 解析并存储分析数据到类属性中
|
* 解析并存储分析数据到类属性中
|
||||||
* @param {MovieStartDTO} data - AI分析返回的数据
|
* @param {MovieStartDTO} data - AI分析返回的数据
|
||||||
*/
|
*/
|
||||||
parseAndStoreAnalysisData(data: MovieStartDTO): void {
|
parseAndStoreAnalysisData(data: MovieStartDTO): void {
|
||||||
// 存储故事梗概
|
// 存储故事梗概
|
||||||
this.storyLogline = data.story_logline || "";
|
this.storyLogline = data.story_logline || "";
|
||||||
|
|
||||||
@ -137,25 +135,25 @@ export class ImageStoryUseCase {
|
|||||||
* 组合成ImageStoryEntity
|
* 组合成ImageStoryEntity
|
||||||
* @param {MovieStartDTO} data - AI分析返回的数据
|
* @param {MovieStartDTO} data - AI分析返回的数据
|
||||||
*/
|
*/
|
||||||
composeImageStoryEntity(data: MovieStartDTO): void {
|
composeImageStoryEntity(data: MovieStartDTO): void {
|
||||||
// 将角色数据转换为ImageStoryEntity需要的格式
|
// 将角色数据转换为ImageStoryEntity需要的格式
|
||||||
const roleImage = data.characters_analysis?.map(character => ({
|
const roleImage =
|
||||||
name: character.role_name,
|
data.characters_analysis?.map((character) => ({
|
||||||
avatar_url: "", // 这里需要根据实际情况设置头像URL
|
name: character.role_name,
|
||||||
region: {
|
avatar_url: "", // 这里需要根据实际情况设置头像URL
|
||||||
x: character.region?.x || 0,
|
region: {
|
||||||
y: character.region?.y || 0,
|
x: character.region?.x || 0,
|
||||||
width: character.region?.width || 0,
|
y: character.region?.y || 0,
|
||||||
height: character.region?.height || 0,
|
width: character.region?.width || 0,
|
||||||
},
|
height: character.region?.height || 0,
|
||||||
|
},
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
// 更新ImageStoryEntity
|
// 更新ImageStoryEntity
|
||||||
this.setImageStory({
|
this.setImageStory({
|
||||||
...this.imageStory,
|
...this.imageStory,
|
||||||
imageAnalysis: data.story_logline || "",
|
imageAnalysis: data.story_logline || "",
|
||||||
storyType: data.potential_genres[0] || "", // 使用第一个分类作为故事类型
|
storyType: data.potential_genres[0] || "", // 使用第一个分类作为故事类型
|
||||||
roleImage,
|
roleImage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -180,14 +178,14 @@ export class ImageStoryUseCase {
|
|||||||
* @param {CharacterAnalysis[]} characters - 角色分析数据
|
* @param {CharacterAnalysis[]} characters - 角色分析数据
|
||||||
*/
|
*/
|
||||||
processCharacterData(characters: CharacterAnalysis[]): void {
|
processCharacterData(characters: CharacterAnalysis[]): void {
|
||||||
this.charactersAnalysis = characters.map(character => ({
|
this.charactersAnalysis = characters.map((character) => ({
|
||||||
...character,
|
...character,
|
||||||
region: {
|
region: {
|
||||||
x: character.region?.x || 0,
|
x: character.region?.x || 0,
|
||||||
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,
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,8 +194,12 @@ export class ImageStoryUseCase {
|
|||||||
* @param {string} characterName - 角色名称
|
* @param {string} characterName - 角色名称
|
||||||
* @returns {CharacterAnalysis['region'] | null} 角色区域坐标,如果未找到则返回null
|
* @returns {CharacterAnalysis['region'] | null} 角色区域坐标,如果未找到则返回null
|
||||||
*/
|
*/
|
||||||
getCharacterRegion(characterName: string): CharacterAnalysis['region'] | null {
|
getCharacterRegion(
|
||||||
const character = this.charactersAnalysis.find(char => char.role_name === characterName);
|
characterName: string
|
||||||
|
): CharacterAnalysis["region"] | null {
|
||||||
|
const character = this.charactersAnalysis.find(
|
||||||
|
(char) => char.role_name === characterName
|
||||||
|
);
|
||||||
return character ? character.region : null;
|
return character ? character.region : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,7 +209,9 @@ export class ImageStoryUseCase {
|
|||||||
* @param {string} avatarUrl - 头像URL
|
* @param {string} avatarUrl - 头像URL
|
||||||
*/
|
*/
|
||||||
updateCharacterAvatar(characterName: string, avatarUrl: string): void {
|
updateCharacterAvatar(characterName: string, avatarUrl: string): void {
|
||||||
const character = this.charactersAnalysis.find(char => char.role_name === characterName);
|
const character = this.charactersAnalysis.find(
|
||||||
|
(char) => char.role_name === characterName
|
||||||
|
);
|
||||||
if (character) {
|
if (character) {
|
||||||
// 更新角色头像URL(这里需要根据实际的数据结构来调整)
|
// 更新角色头像URL(这里需要根据实际的数据结构来调整)
|
||||||
// 由于CharacterAnalysis接口中没有avatar_url字段,这里只是示例
|
// 由于CharacterAnalysis接口中没有avatar_url字段,这里只是示例
|
||||||
@ -220,7 +224,80 @@ export class ImageStoryUseCase {
|
|||||||
* @returns {string[]} 角色名称数组
|
* @returns {string[]} 角色名称数组
|
||||||
*/
|
*/
|
||||||
getAllCharacterNames(): string[] {
|
getAllCharacterNames(): string[] {
|
||||||
return this.charactersAnalysis.map(char => char.role_name);
|
return this.charactersAnalysis.map((char) => char.role_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传人物头像并分析特征,替换旧的角色数据
|
||||||
|
* @param {Function} uploadFile - 文件上传函数
|
||||||
|
* @returns {Promise<{crop_url: string, whisk_caption: string}>} 返回新的头像URL和特征描述
|
||||||
|
*/
|
||||||
|
async uploadCharacterAvatarAndAnalyzeFeatures(
|
||||||
|
uploadFile: (file: File) => Promise<string>
|
||||||
|
): Promise<{ crop_url: string; whisk_caption: string }> {
|
||||||
|
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 file = target.files[0];
|
||||||
|
|
||||||
|
// 直接在这里处理上传和分析逻辑
|
||||||
|
try {
|
||||||
|
// 1. 上传人物头像图片
|
||||||
|
const imageUrl = await uploadFile(file);
|
||||||
|
|
||||||
|
// 2. 调用AI分析接口获取人物特征描述
|
||||||
|
const analysisResult = await analyzeImageDescription({
|
||||||
|
image_url: imageUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!analysisResult.successful || !analysisResult.data) {
|
||||||
|
throw new Error("人物特征分析失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 返回新的头像URL和特征描述,用于替换旧数据
|
||||||
|
const result = {
|
||||||
|
crop_url: imageUrl,
|
||||||
|
whisk_caption: analysisResult.data.description,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理临时元素
|
||||||
|
document.body.removeChild(fileInput);
|
||||||
|
resolve(result);
|
||||||
|
} catch (error) {
|
||||||
|
// 清理临时元素
|
||||||
|
if (document.body.contains(fileInput)) {
|
||||||
|
document.body.removeChild(fileInput);
|
||||||
|
}
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error("未选择文件"));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 清理临时元素
|
||||||
|
if (document.body.contains(fileInput)) {
|
||||||
|
document.body.removeChild(fileInput);
|
||||||
|
}
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fileInput.oncancel = () => {
|
||||||
|
document.body.removeChild(fileInput);
|
||||||
|
reject(new Error("用户取消选择"));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加到DOM并触发点击
|
||||||
|
document.body.appendChild(fileInput);
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -139,11 +139,6 @@ export function AudioRecorder({
|
|||||||
setIsPlaying(!isPlaying);
|
setIsPlaying(!isPlaying);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 音量控制
|
|
||||||
const toggleMute = () => {
|
|
||||||
setIsMuted(!isMuted);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 音量调节
|
// 音量调节
|
||||||
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newVolume = parseFloat(e.target.value);
|
const newVolume = parseFloat(e.target.value);
|
||||||
@ -165,72 +160,77 @@ export function AudioRecorder({
|
|||||||
<>
|
<>
|
||||||
<style>{audioRecorderStyles}</style>
|
<style>{audioRecorderStyles}</style>
|
||||||
<div className="relative bg-white/[0.05] border border-white/[0.1] rounded-lg p-4">
|
<div className="relative bg-white/[0.05] border border-white/[0.1] rounded-lg p-4">
|
||||||
{/* 头部 - 只显示关闭按钮 */}
|
{/* 头部 - 只显示关闭按钮 */}
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<div className="flex justify-end mb-2">
|
<div className="flex justify-end mb-2">
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-white/60 hover:text-white/80 transition-colors"
|
className="text-white/60 hover:text-white/80 transition-colors"
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 主要内容区域 */}
|
{/* 主要内容区域 */}
|
||||||
<div className="flex items-center justify-center min-h-[60px]">
|
<div className="flex items-center justify-center min-h-[60px]">
|
||||||
{mode === "upload" ? (
|
{mode === "upload" ? (
|
||||||
// 上传模式
|
// 上传模式
|
||||||
<div className="text-center w-full">
|
<div className="text-center w-full">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title="Please clearly read the story description above and record a 15-second audio file for upload"
|
title="Please clearly read the story description above and record a 15-second audio file for upload"
|
||||||
placement="top"
|
placement="top"
|
||||||
overlayClassName="max-w-xs"
|
overlayClassName="max-w-xs"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<AntdUpload.Dragger
|
<AntdUpload.Dragger
|
||||||
accept="audio/*"
|
accept="audio/*"
|
||||||
beforeUpload={() => false}
|
beforeUpload={() => false}
|
||||||
customRequest={async ({ file, onSuccess, onError }) => {
|
customRequest={async ({ file, onSuccess, onError }) => {
|
||||||
try {
|
try {
|
||||||
const fileObj = file as File;
|
const fileObj = file as File;
|
||||||
console.log("开始上传文件:", fileObj.name, fileObj.type, fileObj.size);
|
console.log(
|
||||||
|
"开始上传文件:",
|
||||||
|
fileObj.name,
|
||||||
|
fileObj.type,
|
||||||
|
fileObj.size
|
||||||
|
);
|
||||||
|
|
||||||
if (fileObj && fileObj.type.startsWith("audio/")) {
|
if (fileObj && fileObj.type.startsWith("audio/")) {
|
||||||
// 使用 hook 上传文件到七牛云
|
// 使用 hook 上传文件到七牛云
|
||||||
console.log("调用 uploadFile hook...");
|
console.log("调用 uploadFile hook...");
|
||||||
const uploadedUrl = await uploadFile(fileObj);
|
const uploadedUrl = await uploadFile(fileObj);
|
||||||
console.log("上传成功,URL:", uploadedUrl);
|
console.log("上传成功,URL:", uploadedUrl);
|
||||||
|
|
||||||
// 上传成功后,调用回调函数
|
// 上传成功后,调用回调函数
|
||||||
onAudioRecorded(fileObj, uploadedUrl);
|
onAudioRecorded(fileObj, uploadedUrl);
|
||||||
onSuccess?.(uploadedUrl);
|
onSuccess?.(uploadedUrl);
|
||||||
} else {
|
} else {
|
||||||
console.log("文件类型不是音频:", fileObj?.type);
|
console.log("文件类型不是音频:", fileObj?.type);
|
||||||
const error = new Error("文件类型不是音频文件");
|
const error = new Error("文件类型不是音频文件");
|
||||||
onError?.(error);
|
onError?.(error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("上传失败:", error);
|
console.error("上传失败:", error);
|
||||||
// 上传失败时直接报告错误,不使用本地文件作为备选
|
// 上传失败时直接报告错误,不使用本地文件作为备选
|
||||||
onError?.(error as Error);
|
onError?.(error as Error);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
className="bg-transparent border-dashed border-white/20 hover:border-white/40"
|
className="bg-transparent border-dashed border-white/20 hover:border-white/40"
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
>
|
>
|
||||||
<div className="text-2xl text-white/40 mb-2">
|
<div className="text-2xl text-white/40 mb-2">
|
||||||
<InboxOutlined />
|
<InboxOutlined />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-white/60">
|
<div className="text-xs text-white/60">
|
||||||
{isUploading
|
{isUploading
|
||||||
? "Uploading..."
|
? "Uploading..."
|
||||||
: "Drag audio file here or click to upload"}
|
: "Drag audio file here or click to upload"}
|
||||||
</div>
|
</div>
|
||||||
</AntdUpload.Dragger>
|
</AntdUpload.Dragger>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// 录制模式
|
// 录制模式
|
||||||
@ -264,19 +264,19 @@ export function AudioRecorder({
|
|||||||
<div className="text-xs text-white/60 mb-3">
|
<div className="text-xs text-white/60 mb-3">
|
||||||
Click to start recording
|
Click to start recording
|
||||||
</div>
|
</div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title="Please clearly read the story description above and record a 15-second audio"
|
title="Please clearly read the story description above and record a 15-second audio"
|
||||||
placement="top"
|
placement="top"
|
||||||
overlayClassName="max-w-xs"
|
classNames={{ root: "max-w-xs" }}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={startRecording}
|
onClick={startRecording}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors text-sm mx-auto"
|
className="flex items-center justify-center w-12 h-12 bg-red-500 text-white rounded-full hover:bg-red-600 transition-all duration-200 shadow-lg hover:shadow-xl mx-auto"
|
||||||
>
|
data-alt="record-button"
|
||||||
<Mic className="w-3 h-3" />
|
>
|
||||||
<span>Record</span>
|
<Mic className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -321,24 +321,24 @@ export function AudioRecorder({
|
|||||||
<>
|
<>
|
||||||
<style>{audioRecorderStyles}</style>
|
<style>{audioRecorderStyles}</style>
|
||||||
<div className="relative bg-white/[0.05] border border-white/[0.1] rounded-lg p-4">
|
<div className="relative bg-white/[0.05] border border-white/[0.1] rounded-lg p-4">
|
||||||
{/* 头部 - 只显示操作按钮 */}
|
{/* 头部 - 只显示操作按钮 */}
|
||||||
<div className="flex justify-end gap-2 mb-3">
|
<div className="flex justify-end gap-2 mb-3">
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
className="text-white/60 hover:text-red-400 transition-colors"
|
className="text-white/60 hover:text-red-400 transition-colors"
|
||||||
title="Delete audio"
|
title="Delete audio"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-white/60 hover:text-white/80 transition-colors"
|
className="text-white/60 hover:text-white/80 transition-colors"
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* WaveSurfer 波形图区域 */}
|
{/* WaveSurfer 波形图区域 */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
@ -369,34 +369,20 @@ export function AudioRecorder({
|
|||||||
|
|
||||||
{/* 音频设置 */}
|
{/* 音频设置 */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<input
|
||||||
onClick={toggleMute}
|
type="range"
|
||||||
className="text-white/60 hover:text-white/80 transition-colors"
|
min="0"
|
||||||
title={isMuted ? "Unmute" : "Mute"}
|
max="1"
|
||||||
>
|
step="0.1"
|
||||||
{isMuted ? (
|
value={volume}
|
||||||
<MicOff className="w-4 h-4" />
|
onChange={handleVolumeChange}
|
||||||
) : (
|
className="w-16 h-1 !bg-white/20 rounded-lg appearance-none cursor-pointer slider"
|
||||||
<Mic className="w-4 h-4" />
|
/>
|
||||||
)}
|
<span className="text-xs text-white/60 w-8">
|
||||||
</button>
|
{Math.round(volume * 100)}%
|
||||||
<div className="flex items-center gap-2">
|
</span>
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
step="0.1"
|
|
||||||
value={volume}
|
|
||||||
onChange={handleVolumeChange}
|
|
||||||
className="w-16 h-1 bg-white/20 rounded-lg appearance-none cursor-pointer slider"
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-white/60 w-8">
|
|
||||||
{Math.round(volume * 100)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-white/40">1x</div>
|
<div className="text-xs text-white/40">1x</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
@ -11,140 +11,25 @@ import {
|
|||||||
Crown,
|
Crown,
|
||||||
Clapperboard,
|
Clapperboard,
|
||||||
Globe,
|
Globe,
|
||||||
AudioLines,
|
|
||||||
Clock,
|
Clock,
|
||||||
Trash2,
|
Trash2,
|
||||||
Plus,
|
|
||||||
LayoutTemplate,
|
LayoutTemplate,
|
||||||
ImagePlay,
|
ImagePlay,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
RotateCcw,
|
|
||||||
Settings,
|
Settings,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Dropdown, Modal, Tooltip, Upload, Image, Spin } from "antd";
|
import { Dropdown, Modal, Tooltip, Upload, Spin } from "antd";
|
||||||
import { PlusOutlined, UploadOutlined, EyeOutlined } from "@ant-design/icons";
|
import { UploadOutlined } from "@ant-design/icons";
|
||||||
import { StoryTemplateEntity } from "@/app/service/domain/Entities";
|
import { StoryTemplateEntity } from "@/app/service/domain/Entities";
|
||||||
import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService";
|
import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService";
|
||||||
import TemplateCard from "./templateCard";
|
import TemplateCard from "./templateCard";
|
||||||
import { AudioRecorder } from "./AudioRecorder";
|
import { AudioRecorder } from "./AudioRecorder";
|
||||||
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
|
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
|
||||||
import { createScriptEpisodeNew } from "@/api/script_episode";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { EditorContent, useEditor } from "@tiptap/react";
|
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
|
||||||
import { HighlightTextExtension } from "@/components/ui/main-editor/HighlightText";
|
|
||||||
import Placeholder from "@tiptap/extension-placeholder";
|
|
||||||
import { createMovieProjectV1 } from "@/api/video_flow";
|
import { createMovieProjectV1 } from "@/api/video_flow";
|
||||||
import { useLoadScriptText, useUploadFile } from "@/app/service/domain/service";
|
import { useLoadScriptText, useUploadFile } from "@/app/service/domain/service";
|
||||||
|
import { ActionButton } from "../common/ActionButton";
|
||||||
// 自定义音频播放器样式
|
import { HighlightEditor } from "../common/HighlightEditor";
|
||||||
const customAudioPlayerStyles = `
|
|
||||||
.custom-audio-player {
|
|
||||||
background: rgba(255, 255, 255, 0.05) !important;
|
|
||||||
border-radius: 8px !important;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
|
||||||
}
|
|
||||||
.custom-audio-player .rhap_main-controls-button {
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
.custom-audio-player .rhap_progress-filled {
|
|
||||||
background-color: #3b82f6 !important;
|
|
||||||
}
|
|
||||||
.custom-audio-player .rhap_progress-indicator {
|
|
||||||
background-color: #3b82f6 !important;
|
|
||||||
}
|
|
||||||
.custom-audio-player .rhap_time {
|
|
||||||
color: rgba(255, 255, 255, 0.7) !important;
|
|
||||||
}
|
|
||||||
.custom-audio-player .rhap_volume-controls {
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 模式选择下拉菜单样式 */
|
|
||||||
.mode-dropdown .ant-dropdown-menu {
|
|
||||||
background: rgba(255, 255, 255, 0.08) !important;
|
|
||||||
backdrop-filter: blur(20px) !important;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12) !important;
|
|
||||||
border-radius: 10px !important;
|
|
||||||
padding: 6px !important;
|
|
||||||
min-width: 160px !important;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-dropdown .ant-dropdown-menu-item {
|
|
||||||
padding: 6px 10px !important;
|
|
||||||
border-radius: 6px !important;
|
|
||||||
color: rgba(255, 255, 255, 0.9) !important;
|
|
||||||
transition: all 0.2s ease !important;
|
|
||||||
margin-bottom: 3px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-dropdown .ant-dropdown-menu-item:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.15) !important;
|
|
||||||
transform: translateX(4px) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-dropdown .ant-dropdown-menu-item:last-child {
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 模式提示tooltip样式 */
|
|
||||||
.mode-tooltip .ant-tooltip-inner {
|
|
||||||
background: rgba(0, 0, 0, 0.8) !important;
|
|
||||||
backdrop-filter: blur(10px) !important;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
|
||||||
border-radius: 8px !important;
|
|
||||||
color: white !important;
|
|
||||||
font-size: 12px !important;
|
|
||||||
line-height: 1.4 !important;
|
|
||||||
max-width: 200px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-tooltip .ant-tooltip-arrow::before {
|
|
||||||
background: rgba(0, 0, 0, 0.8) !important;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 模板卡片样式 */
|
|
||||||
.line-clamp-2 {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 自定义滚动条 */
|
|
||||||
.template-list-scroll::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-list-scroll::-webkit-scrollbar-track {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-list-scroll::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-list-scroll::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 自定义缩放类 */
|
|
||||||
.scale-102 {
|
|
||||||
transform: scale(1.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 文本截断类 */
|
|
||||||
.line-clamp-3 {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**模板故事模式弹窗组件 */
|
/**模板故事模式弹窗组件 */
|
||||||
const RenderTemplateStoryMode = ({
|
const RenderTemplateStoryMode = ({
|
||||||
@ -223,7 +108,7 @@ const RenderTemplateStoryMode = ({
|
|||||||
// 模板列表渲染
|
// 模板列表渲染
|
||||||
const templateListRender = () => {
|
const templateListRender = () => {
|
||||||
return (
|
return (
|
||||||
<div className="w-1/3 p-6 border-r border-white/[0.1]">
|
<div className="w-1/3 p-4 border-r border-white/[0.1]">
|
||||||
<h3 className="text-xl font-bold text-white mb-6">Story Templates</h3>
|
<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">
|
<div className="space-y-4 max-h-[700px] overflow-y-auto pr-3 template-list-scroll">
|
||||||
{templateStoryList.map((template, index) => (
|
{templateStoryList.map((template, index) => (
|
||||||
@ -255,9 +140,9 @@ const RenderTemplateStoryMode = ({
|
|||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
|
||||||
</div>
|
</div>
|
||||||
) : selectedTemplate ? (
|
) : selectedTemplate ? (
|
||||||
<div>
|
<div className="relative h-full">
|
||||||
{/* 模板信息头部 - 增加顶部空间 */}
|
{/* 模板信息头部 - 增加顶部空间 */}
|
||||||
<div className="flex gap-6 p-6 border-b border-white/[0.1]">
|
<div className="flex gap-3 py-4 border-b border-white/[0.1] min-h-[300px]">
|
||||||
{/* 左侧图片 */}
|
{/* 左侧图片 */}
|
||||||
<div className="w-1/3">
|
<div className="w-1/3">
|
||||||
<div
|
<div
|
||||||
@ -277,14 +162,14 @@ const RenderTemplateStoryMode = ({
|
|||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
<h2
|
<h2
|
||||||
data-alt="template-title"
|
data-alt="template-title"
|
||||||
className="text-2xl font-bold text-white mb-6"
|
className="text-2xl font-bold text-white mb-4"
|
||||||
>
|
>
|
||||||
{selectedTemplate.name}
|
{selectedTemplate.name}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex-1 overflow-y-auto max-h-80 pr-2">
|
<div className="flex-1 overflow-y-auto max-h-96 pr-3">
|
||||||
<p
|
<p
|
||||||
data-alt="template-description"
|
data-alt="template-description"
|
||||||
className="text-gray-300 text-base leading-relaxed"
|
className="text-gray-300 text-sm leading-relaxed"
|
||||||
>
|
>
|
||||||
{selectedTemplate.generateText}
|
{selectedTemplate.generateText}
|
||||||
</p>
|
</p>
|
||||||
@ -293,7 +178,7 @@ const RenderTemplateStoryMode = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 角色自定义部分 - 精简布局 */}
|
{/* 角色自定义部分 - 精简布局 */}
|
||||||
<div className="p-6">
|
<div className="p-4">
|
||||||
<h3
|
<h3
|
||||||
data-alt="roles-section-title"
|
data-alt="roles-section-title"
|
||||||
className="text-lg font-semibold text-white mb-4"
|
className="text-lg font-semibold text-white mb-4"
|
||||||
@ -409,7 +294,7 @@ const RenderTemplateStoryMode = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 弹窗底部操作 - 只保留 Action 按钮 */}
|
{/* 弹窗底部操作 - 只保留 Action 按钮 */}
|
||||||
<div className="relative group flex justify-end mt-10">
|
{/* <div className="relative group flex justify-end mt-10">
|
||||||
<div className="relative w-40 h-12 opacity-90 overflow-hidden rounded-xl bg-black z-10">
|
<div className="relative w-40 h-12 opacity-90 overflow-hidden rounded-xl bg-black z-10">
|
||||||
<div className="absolute z-10 -translate-x-32 group-hover:translate-x-[20rem] ease-in transistion-all duration-700 h-full w-32 bg-gradient-to-r from-gray-500 to-white/10 opacity-30 -skew-x-12"></div>
|
<div className="absolute z-10 -translate-x-32 group-hover:translate-x-[20rem] ease-in transistion-all duration-700 h-full w-32 bg-gradient-to-r from-gray-500 to-white/10 opacity-30 -skew-x-12"></div>
|
||||||
|
|
||||||
@ -434,6 +319,13 @@ const RenderTemplateStoryMode = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="absolute duration-1000 group-hover:animate-spin w-full h-[80px] bg-gradient-to-r from-[rgb(106,244,249)] to-[rgb(199,59,255)] blur-[30px]"></div>
|
<div className="absolute duration-1000 group-hover:animate-spin w-full h-[80px] bg-gradient-to-r from-[rgb(106,244,249)] to-[rgb(199,59,255)] blur-[30px]"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div> */}
|
||||||
|
<div className=" absolute bottom-0 right-0">
|
||||||
|
<ActionButton
|
||||||
|
isCreating={localLoading}
|
||||||
|
handleCreateVideo={handleConfirm}
|
||||||
|
icon={<Clapperboard className="w-5 h-5" />}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -448,7 +340,6 @@ const RenderTemplateStoryMode = ({
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style>{customAudioPlayerStyles}</style>
|
|
||||||
<Modal
|
<Modal
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
@ -740,38 +631,6 @@ export function ChatInputBox() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建视频按钮
|
|
||||||
const ActionButton = ({
|
|
||||||
isCreating,
|
|
||||||
handleCreateVideo,
|
|
||||||
icon,
|
|
||||||
}: {
|
|
||||||
isCreating: boolean;
|
|
||||||
handleCreateVideo: () => void;
|
|
||||||
icon: React.ReactNode;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="relative group">
|
|
||||||
<div
|
|
||||||
data-alt="action-button"
|
|
||||||
className="relative w-12 h-12 opacity-90 cursor-pointer overflow-hidden rounded-xl bg-black z-10"
|
|
||||||
onClick={isCreating ? undefined : handleCreateVideo}
|
|
||||||
>
|
|
||||||
<div className="absolute z-10 -translate-x-12 group-hover:translate-x-12 transition-all duration-700 h-full w-12 bg-gradient-to-r from-gray-500 to-white/10 opacity-30 -skew-x-12"></div>
|
|
||||||
<div className="absolute flex items-center justify-center text-white z-[1] opacity-90 rounded-xl inset-0.5 bg-black">
|
|
||||||
<button
|
|
||||||
name="text"
|
|
||||||
className="w-full h-full opacity-90 rounded-xl bg-black flex items-center justify-center"
|
|
||||||
>
|
|
||||||
{isCreating ? <Loader2 className="w-5 h-5 animate-spin" /> : icon}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="absolute duration-1000 group-hover:animate-spin w-full h-12 bg-gradient-to-r from-[rgb(106,244,249)] to-[rgb(199,59,255)] blur-[20px]"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配置选项组件
|
* 配置选项组件
|
||||||
* 提供视频创建的各种配置选项,位于输入框下方
|
* 提供视频创建的各种配置选项,位于输入框下方
|
||||||
@ -872,135 +731,6 @@ const ConfigOptions = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 角色高亮编辑器组件
|
|
||||||
* 使用 Tiptap 实现角色名称高亮和文本编辑功能
|
|
||||||
*/
|
|
||||||
const RoleHighlightEditor = ({
|
|
||||||
content,
|
|
||||||
onContentChange,
|
|
||||||
}: {
|
|
||||||
content: string;
|
|
||||||
onContentChange: (content: string) => void;
|
|
||||||
}) => {
|
|
||||||
const editor = useEditor({
|
|
||||||
extensions: [
|
|
||||||
StarterKit,
|
|
||||||
HighlightTextExtension,
|
|
||||||
Placeholder.configure({
|
|
||||||
placeholder:
|
|
||||||
"Share your creative ideas about the image and let AI create a movie story for you...",
|
|
||||||
emptyEditorClass: "is-editor-empty",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
content: "",
|
|
||||||
// 简化:移除复杂的 onUpdate 逻辑,只处理基本的文本变化
|
|
||||||
onUpdate: ({ editor }) => {
|
|
||||||
const textContent = editor.getText();
|
|
||||||
if (!textContent.trim()) {
|
|
||||||
onContentChange("");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 直接传递文本内容,不进行复杂的标签重建
|
|
||||||
onContentChange(textContent);
|
|
||||||
},
|
|
||||||
editorProps: {
|
|
||||||
handleKeyDown: (view, event) => {
|
|
||||||
const { from, to } = view.state.selection;
|
|
||||||
const doc = view.state.doc;
|
|
||||||
|
|
||||||
// 检查光标前后是否有角色标签
|
|
||||||
const textBefore =
|
|
||||||
from > 0 ? doc.textBetween(Math.max(0, from - 50), from) : "";
|
|
||||||
const textAfter =
|
|
||||||
to < doc.content.size
|
|
||||||
? doc.textBetween(to, Math.min(doc.content.size, to + 50))
|
|
||||||
: "";
|
|
||||||
// 匹配新的角色标签格式 <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();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
immediatelyRender: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (editor) {
|
|
||||||
if (!content || content.trim() === "") {
|
|
||||||
editor.commands.clearContent(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将带标签的内容转换为高亮显示(支持新的角色标签格式)
|
|
||||||
const htmlContent = content.replace(
|
|
||||||
/<role[^>]*>([^<]+)<\/role>/g,
|
|
||||||
'<highlight-text type="role" text="$1" color="blue">$1</highlight-text>'
|
|
||||||
);
|
|
||||||
editor.commands.setContent(htmlContent, { emitUpdate: false });
|
|
||||||
}
|
|
||||||
}, [content, editor]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex-1 min-w-0 relative pr-20">
|
|
||||||
<style jsx>{`
|
|
||||||
.role-name-highlight {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white !important;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin: 0 2px;
|
|
||||||
user-select: none;
|
|
||||||
cursor: default;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除 Tiptap 编辑器的默认焦点样式 */
|
|
||||||
.ProseMirror:focus {
|
|
||||||
outline: none !important;
|
|
||||||
border: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror:focus-visible {
|
|
||||||
outline: none !important;
|
|
||||||
border: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除编辑器容器的焦点样式 */
|
|
||||||
.ProseMirror-focused {
|
|
||||||
outline: none !important;
|
|
||||||
border: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保编辑器内容区域没有边框和轮廓 */
|
|
||||||
.ProseMirror {
|
|
||||||
outline: none !important;
|
|
||||||
border: none !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
<EditorContent
|
|
||||||
editor={editor}
|
|
||||||
className="w-full bg-transparent border-none outline-none resize-none text-white placeholder:text-white/40 text-sm leading-relaxed min-h-[60px] focus:outline-none focus:ring-0 focus:border-0"
|
|
||||||
style={{ outline: "none", border: "none", boxShadow: "none" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图片故事弹窗组件
|
* 图片故事弹窗组件
|
||||||
* 提供图片上传、AI分析和故事生成功能,支持动态UI变化
|
* 提供图片上传、AI分析和故事生成功能,支持动态UI变化
|
||||||
@ -1042,6 +772,7 @@ const PhotoStoryModal = ({
|
|||||||
setCharactersAnalysis,
|
setCharactersAnalysis,
|
||||||
originalUserDescription,
|
originalUserDescription,
|
||||||
actionMovie,
|
actionMovie,
|
||||||
|
uploadCharacterAvatarAndAnalyzeFeatures,
|
||||||
} = useImageStoryServiceHook();
|
} = useImageStoryServiceHook();
|
||||||
const { loadingText } = useLoadScriptText(isLoading);
|
const { loadingText } = useLoadScriptText(isLoading);
|
||||||
const { uploadFile } = useUploadFile();
|
const { uploadFile } = useUploadFile();
|
||||||
@ -1222,47 +953,12 @@ const PhotoStoryModal = ({
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// 创建隐藏的文件输入框
|
// 使用新的上传人物头像并分析特征方法
|
||||||
const input = document.createElement("input");
|
uploadCharacterAvatarAndAnalyzeFeatures(
|
||||||
input.type = "file";
|
avatar.name
|
||||||
input.accept = "image/*";
|
).catch((error) => {
|
||||||
input.style.display = "none";
|
console.error("上传人物头像失败:", error);
|
||||||
|
});
|
||||||
input.onchange = async (event) => {
|
|
||||||
const target =
|
|
||||||
event.target as HTMLInputElement;
|
|
||||||
const file = target.files?.[0];
|
|
||||||
if (file) {
|
|
||||||
try {
|
|
||||||
// 使用七牛云上传
|
|
||||||
const newImageUrl = await uploadFile(
|
|
||||||
file
|
|
||||||
);
|
|
||||||
|
|
||||||
// 更新角色分析中的图片URL
|
|
||||||
setCharactersAnalysis((prev) =>
|
|
||||||
prev.map((char) =>
|
|
||||||
char.role_name === avatar.name
|
|
||||||
? { ...char, crop_url: newImageUrl }
|
|
||||||
: char
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 清理临时元素
|
|
||||||
document.body.removeChild(input);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("上传图片失败:", error);
|
|
||||||
// 清理临时元素
|
|
||||||
if (document.body.contains(input)) {
|
|
||||||
document.body.removeChild(input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加到DOM并触发点击
|
|
||||||
document.body.appendChild(input);
|
|
||||||
input.click();
|
|
||||||
}}
|
}}
|
||||||
className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center"
|
className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
@ -1320,9 +1016,11 @@ const PhotoStoryModal = ({
|
|||||||
|
|
||||||
<div className="flex items-start gap-4 mt-2 relative">
|
<div className="flex items-start gap-4 mt-2 relative">
|
||||||
{/* 文本输入框 */}
|
{/* 文本输入框 */}
|
||||||
<RoleHighlightEditor
|
<HighlightEditor
|
||||||
content={storyContent}
|
content={storyContent}
|
||||||
onContentChange={updateStoryContent}
|
onContentChange={updateStoryContent}
|
||||||
|
type={"role"}
|
||||||
|
placeholder="Share your creative ideas about the image and let AI create a movie story for you..."
|
||||||
/>
|
/>
|
||||||
<div className="absolute bottom-1 right-0 flex gap-2">
|
<div className="absolute bottom-1 right-0 flex gap-2">
|
||||||
{!hasAnalyzed ? (
|
{!hasAnalyzed ? (
|
||||||
33
components/common/ActionButton.tsx
Normal file
33
components/common/ActionButton.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
|
// 创建流光钮
|
||||||
|
export function ActionButton({
|
||||||
|
isCreating,
|
||||||
|
handleCreateVideo,
|
||||||
|
icon,
|
||||||
|
}: {
|
||||||
|
isCreating: boolean;
|
||||||
|
handleCreateVideo: () => void;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="relative group">
|
||||||
|
<div
|
||||||
|
data-alt="action-button"
|
||||||
|
className="relative w-12 h-12 opacity-90 cursor-pointer overflow-hidden rounded-xl bg-black z-10"
|
||||||
|
onClick={isCreating ? undefined : handleCreateVideo}
|
||||||
|
>
|
||||||
|
<div className="absolute z-10 -translate-x-12 group-hover:translate-x-12 transition-all duration-700 h-full w-12 bg-gradient-to-r from-gray-500 to-white/10 opacity-30 -skew-x-12"></div>
|
||||||
|
<div className="absolute flex items-center justify-center text-white z-[1] opacity-90 rounded-xl inset-0.5 bg-black">
|
||||||
|
<button
|
||||||
|
name="text"
|
||||||
|
className="w-full h-full opacity-90 rounded-xl bg-black flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{isCreating ? <Loader2 className="w-5 h-5 animate-spin" /> : icon}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="absolute duration-1000 group-hover:animate-spin w-full h-12 bg-gradient-to-r from-[rgb(106,244,249)] to-[rgb(199,59,255)] blur-[20px]"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
139
components/common/HighlightEditor.tsx
Normal file
139
components/common/HighlightEditor.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import Placeholder from "@tiptap/extension-placeholder";
|
||||||
|
import { useEditor, EditorContent } from "@tiptap/react";
|
||||||
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { HighlightTextExtension } from "../ui/main-editor/HighlightText";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 高亮编辑器组件
|
||||||
|
* 使用 Tiptap 实现高亮和文本编辑功能
|
||||||
|
* 让 文本中的<type>xxxx</type>标签高亮
|
||||||
|
*/
|
||||||
|
export const HighlightEditor = ({
|
||||||
|
content,
|
||||||
|
onContentChange,
|
||||||
|
type,
|
||||||
|
placeholder,
|
||||||
|
}: {
|
||||||
|
/** 内容 */
|
||||||
|
content: string;
|
||||||
|
/** 内容变化回调 */
|
||||||
|
onContentChange: (content: string) => void;
|
||||||
|
/** 标签类型*/
|
||||||
|
type: string;
|
||||||
|
/**提示语 */
|
||||||
|
placeholder: string;
|
||||||
|
}) => {
|
||||||
|
const editor = useEditor({
|
||||||
|
extensions: [
|
||||||
|
StarterKit,
|
||||||
|
HighlightTextExtension,
|
||||||
|
Placeholder.configure({
|
||||||
|
placeholder,
|
||||||
|
emptyEditorClass: "is-editor-empty",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
content: "",
|
||||||
|
onUpdate: ({ editor }) => {
|
||||||
|
const textContent = editor.getText();
|
||||||
|
if (!textContent.trim()) {
|
||||||
|
onContentChange("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 直接传递文本内容,不进行复杂的标签重建
|
||||||
|
onContentChange(textContent);
|
||||||
|
},
|
||||||
|
editorProps: {
|
||||||
|
handleKeyDown: (view, event) => {
|
||||||
|
const { from, to } = view.state.selection;
|
||||||
|
const doc = view.state.doc;
|
||||||
|
// 检查光标前后是否有标签
|
||||||
|
const textBefore =
|
||||||
|
from > 0 ? doc.textBetween(Math.max(0, from - 50), from) : "";
|
||||||
|
const textAfter =
|
||||||
|
to < doc.content.size
|
||||||
|
? doc.textBetween(to, Math.min(doc.content.size, to + 50))
|
||||||
|
: "";
|
||||||
|
const beforeMatch = textBefore.match(new RegExp(`<${type}[^>]*>[^<]*$`));
|
||||||
|
const afterMatch = textAfter.match(new RegExp(`^[^>]*<\\/${type}>`));
|
||||||
|
|
||||||
|
// 只允许删除操作)
|
||||||
|
if (beforeMatch || afterMatch) {
|
||||||
|
if (event.key !== "Backspace" && event.key !== "Delete") {
|
||||||
|
event.preventDefault();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
immediatelyRender: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editor) {
|
||||||
|
if (!content || content.trim() === "") {
|
||||||
|
editor.commands.clearContent(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将带标签的内容转换为高亮显示(支持新的标签格式)
|
||||||
|
const htmlContent = content.replace(
|
||||||
|
new RegExp(`<${type}[^>]*>([^<]+)<\/${type}>`, "g"),
|
||||||
|
'<highlight-text type="' + type + '" text="$1" color="blue">$1</highlight-text>'
|
||||||
|
);
|
||||||
|
editor.commands.setContent(htmlContent, { emitUpdate: false });
|
||||||
|
}
|
||||||
|
}, [content, editor]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 min-w-0 relative pr-20">
|
||||||
|
<style jsx>{`
|
||||||
|
.${type}-name-highlight {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white !important;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0 2px;
|
||||||
|
user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除 Tiptap 编辑器的默认焦点样式 */
|
||||||
|
.ProseMirror:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror:focus-visible {
|
||||||
|
outline: none !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除编辑器容器的焦点样式 */
|
||||||
|
.ProseMirror-focused {
|
||||||
|
outline: none !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保编辑器内容区域没有边框和轮廓 */
|
||||||
|
.ProseMirror {
|
||||||
|
outline: none !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
<EditorContent
|
||||||
|
editor={editor}
|
||||||
|
className="w-full bg-transparent border-none outline-none resize-none text-white placeholder:text-white/40 text-sm leading-relaxed min-h-[60px] focus:outline-none focus:ring-0 focus:border-0"
|
||||||
|
style={{ outline: "none", border: "none", boxShadow: "none" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -7,7 +7,7 @@ import './style/create-to-video2.css';
|
|||||||
|
|
||||||
import { createScriptEpisodeNew, getScriptEpisodeListNew, CreateScriptEpisodeRequest } from "@/api/script_episode";
|
import { createScriptEpisodeNew, getScriptEpisodeListNew, CreateScriptEpisodeRequest } from "@/api/script_episode";
|
||||||
import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation2';
|
import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation2';
|
||||||
import { ChatInputBox } from '@/components/common/ChatInputBox';
|
import { ChatInputBox } from '@/components/ChatInputBox/ChatInputBox';
|
||||||
|
|
||||||
|
|
||||||
// ideaText已迁移到ChatInputBox组件中
|
// ideaText已迁移到ChatInputBox组件中
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user