From 39a3de215cc235d0927af7a2fb67ae6695655e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Tue, 29 Jul 2025 10:57:06 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=96=B0=E8=AE=BE=E8=AE=A1=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/constants.ts | 2 +- app/globals.css | 8 + app/model/enums.ts | 92 ++++- components/pages/create-to-video2.tsx | 12 +- components/pages/work-flow.tsx | 3 + components/pages/work-flow/media-viewer.tsx | 2 +- .../pages/work-flow/use-workflow-data.tsx | 6 + components/portal.tsx | 22 ++ components/script-edit-dialog.tsx | 227 +++++++++++ components/ui/character-editor.tsx | 213 ++++++++++ components/ui/character-tab-content.tsx | 373 ++++++++---------- components/ui/dialogue-item.tsx | 167 ++++++++ components/ui/edit-modal.tsx | 48 ++- components/ui/filter-bar.tsx | 176 +++++++++ components/ui/keyword-text.tsx | 137 +++++++ components/ui/media-properties-modal.tsx | 6 +- components/ui/scene-editor.tsx | 308 +++++++++++++++ components/ui/scene-tab-content.tsx | 314 +++++++++++++++ components/ui/script-tab-content.tsx | 314 +++++++-------- components/ui/shot-tab-content.tsx | 305 ++++++++++++++ components/ui/storyboard-card.tsx | 284 +++++++++++++ components/ui/style/dialogue-item.css | 3 + components/ui/video-tab-content.tsx | 216 ++++------ lib/api.ts | 2 +- next.config.js | 2 +- package-lock.json | 14 + package.json | 1 + test/movie.http | 16 +- 28 files changed, 2714 insertions(+), 559 deletions(-) create mode 100644 components/portal.tsx create mode 100644 components/script-edit-dialog.tsx create mode 100644 components/ui/character-editor.tsx create mode 100644 components/ui/dialogue-item.tsx create mode 100644 components/ui/filter-bar.tsx create mode 100644 components/ui/keyword-text.tsx create mode 100644 components/ui/scene-editor.tsx create mode 100644 components/ui/scene-tab-content.tsx create mode 100644 components/ui/shot-tab-content.tsx create mode 100644 components/ui/storyboard-card.tsx create mode 100644 components/ui/style/dialogue-item.css diff --git a/api/constants.ts b/api/constants.ts index 7089bbb..02fd0d9 100644 --- a/api/constants.ts +++ b/api/constants.ts @@ -1 +1 @@ -export const BASE_URL = "https://pre.movieflow.api.huiying.video" +export const BASE_URL = "https://77.smartvideo.py.qikongjian.com" diff --git a/app/globals.css b/app/globals.css index b23fdb9..5e7fcab 100644 --- a/app/globals.css +++ b/app/globals.css @@ -20,6 +20,8 @@ /* 3dwave */ --index: calc(1vh + 1vw); --transition: cubic-bezier(0.1, 0.7, 0, 1); + + --gradient-color: linear-gradient(120deg, rgb(79, 222, 255, 0.75) -1%, rgb(230, 117, 255, 0.75) 100%); } @media (prefers-color-scheme: dark) { @@ -137,4 +139,10 @@ body { .bg-muted { width: 100%; +} + +.focus-visible\:outline-none:focus-visible { + outline: none !important; + outline-offset: 0 !important; + box-shadow: none !important; } \ No newline at end of file diff --git a/app/model/enums.ts b/app/model/enums.ts index 6e15184..8d331d5 100644 --- a/app/model/enums.ts +++ b/app/model/enums.ts @@ -133,4 +133,94 @@ export const TaskStatusMap = { value: "failed", label: "失败" } -} as const; \ No newline at end of file +} as const; + +// 分镜脚本编辑器类型定义 +export interface StoryboardCard { + id: string; + shotId: string; // 分镜ID + scene?: SceneOption; // 场景 + characters: CharacterOption[]; // 出现人物 + description: string; // 分镜描述 + shotType: string; // 分镜类型 + cameraMove: string; // 相机运动 + dialogues: DialogueItem[]; + notes?: string; +} + +export interface CharacterRef { + id: string; + name: string; +} + +export interface DialogueItem { + id: string; + speaker: string; + text: string; // 对话内容,支持关键词标记,如 #角色# [场景] +} + +export interface CharacterInfo { + avatar: string; + gender: string; + age: string; + description: string; +} + +export interface SceneInfo { + image: string; + location: string; + time: string; +} + +export interface SceneOption { + sceneId: string; + name: string; + image: string; + location: string; + time: string; +} + +export interface CharacterOption { + characterId: string; + name: string; + image: string; + gender: string; + age: string; + description: string; +} + +// Mock 数据 +export const mockSceneOptions: SceneOption[] = [ + { sceneId: '1', name: '暮色森林', image: 'https://c.huiying.video/images/7fd3f2d6-840a-46ac-a97d-d3d1b37ec4e0.jpg', location: '西境边陲', time: '傍晚' } +]; + +export const mockCharacterOptions: CharacterOption[] = [ + { characterId: '1', name: '艾琳', image: 'https://c.huiying.video/images/32f6b07c-bceb-4b63-8a13-4749703ab08d.jpg', gender: '女', age: '24', description: '银发女剑士' }, + { characterId: '2', name: '影子猎手', image: 'https://c.huiying.video/images/97c6c59a-50cc-4159-aacd-94ab9d208150.jpg', gender: '男', age: '35', description: '神秘追踪者' }, +]; + +export const mockStoryboards: StoryboardCard[] = [ + { + id: '1', + shotId: 'SC-01', + scene: mockSceneOptions[0], + characters: [mockCharacterOptions[0], mockCharacterOptions[1]], + description: '艾琳警惕地穿过森林,影子猎手的身影若隐若现艾琳警惕地穿过森林,影子猎手的身影若隐若现艾琳警惕地穿过森林,影子猎手的身影若隐若现艾琳警惕地穿过森林,影子猎手的身影若隐若现', + shotType: '中景', + cameraMove: '缓慢推进', + dialogues: [ + { id: 'd1', speaker: '艾琳', text: '我们必须在 #影子猎手# 到来前穿过 [暮色森林]。' }, + { id: 'd2', speaker: '影子猎手', text: '你以为能逃出这里?' }, + ], + notes: '镜头缓慢推进至人物背影', + }, +]; + +export const characterInfoMap: Record = { + '艾琳': { avatar: 'https://c.huiying.video/images/32f6b07c-bceb-4b63-8a13-4749703ab08d.jpg', gender: '女', age: '24', description: '银发女剑士' }, + '影子猎手': { avatar: 'https://c.huiying.video/images/97c6c59a-50cc-4159-aacd-94ab9d208150.jpg', gender: '男', age: '35', description: '神秘追踪者' }, +}; + +export const sceneInfoMap: Record = { + '暮色森林': { image: 'https://c.huiying.video/images/7fd3f2d6-840a-46ac-a97d-d3d1b37ec4e0.jpg', location: '西境边陲', time: '傍晚' }, +}; \ No newline at end of file diff --git a/components/pages/create-to-video2.tsx b/components/pages/create-to-video2.tsx index 418c77d..694a783 100644 --- a/components/pages/create-to-video2.tsx +++ b/components/pages/create-to-video2.tsx @@ -14,6 +14,7 @@ import { ModeEnum, ResolutionEnum } from "@/app/model/enums"; import { createScriptEpisodeNew, getScriptEpisodeListNew, CreateScriptEpisodeRequest } from "@/api/script_episode"; import { getUploadToken, uploadToQiniu } from "@/api/common"; import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation2'; +import { ScriptEditDialog } from '@/components/script-edit-dialog'; const ideaText = 'a cute capybara with an orange on its head, staring into the distance and walking forward'; @@ -52,7 +53,7 @@ export function CreateToVideo2() { const [isSmartAssistantExpanded, setIsSmartAssistantExpanded] = useState(false); const [userId, setUserId] = useState(0); const [isComposing, setIsComposing] = useState(false); - + const [isScriptEditDialogOpen, setIsScriptEditDialogOpen] = useState(false); // 在客户端挂载后读取localStorage useEffect(() => { if (typeof window !== 'undefined') { @@ -134,6 +135,8 @@ export function CreateToVideo2() { } const handleCreateVideo = async () => { + setIsScriptEditDialogOpen(true); + return; setIsCreating(true); // 创建剧集数据 let episodeData: any = { @@ -695,6 +698,13 @@ export function CreateToVideo2() { )} + + {isScriptEditDialogOpen && ( + setIsScriptEditDialogOpen(false)} + /> + )} ); } \ No newline at end of file diff --git a/components/pages/work-flow.tsx b/components/pages/work-flow.tsx index 62e1f8f..3d148a6 100644 --- a/components/pages/work-flow.tsx +++ b/components/pages/work-flow.tsx @@ -22,6 +22,8 @@ export default function WorkFlow() { const { taskObject, taskSketch, + taskScenes, + taskShotSketch, taskVideos, sketchCount, isLoading, @@ -224,6 +226,7 @@ export default function WorkFlow() { taskStatus={taskObject?.taskStatus || '1'} taskSketch={taskSketch} sketchVideo={taskVideos} + taskScenes={taskScenes} currentSketchIndex={currentSketchIndex} onSketchSelect={setCurrentSketchIndex} roles={roles} diff --git a/components/pages/work-flow/media-viewer.tsx b/components/pages/work-flow/media-viewer.tsx index 445b9cd..4224bb5 100644 --- a/components/pages/work-flow/media-viewer.tsx +++ b/components/pages/work-flow/media-viewer.tsx @@ -746,7 +746,7 @@ export function MediaViewer({ handleEditClick('1')} + onClick={() => handleEditClick('2')} /> )} diff --git a/components/pages/work-flow/use-workflow-data.tsx b/components/pages/work-flow/use-workflow-data.tsx index cf51291..5f258cf 100644 --- a/components/pages/work-flow/use-workflow-data.tsx +++ b/components/pages/work-flow/use-workflow-data.tsx @@ -58,6 +58,7 @@ export function useWorkflowData() { // 更新 taskObject 的类型 const [taskObject, setTaskObject] = useState(null); const [taskSketch, setTaskSketch] = useState([]); + const [taskScenes, setTaskScenes] = useState([]); const [taskShotSketch, setTaskShotSketch] = useState([]); const [taskVideos, setTaskVideos] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -176,6 +177,7 @@ export function useWorkflowData() { }); } setTaskSketch(sketchList); + setTaskScenes(sketchList); updateSketchCount(sketchList.length); setIsGeneratingSketch(true); loadingText = LOADING_TEXT_MAP.sketch(sketchList.length, task.task_result.total_count); @@ -393,6 +395,7 @@ export function useWorkflowData() { }); } setTaskSketch(sketchList); + setTaskScenes(sketchList); updateSketchCount(sketchList.length); // 设置为最后一个草图 if (data.sketch.total_count > realSketchResultData.length) { @@ -528,6 +531,7 @@ export function useWorkflowData() { setDataLoadError(null); // 重置所有状态 setTaskSketch([]); + setTaskScenes([]); setTaskVideos([]); updateSketchCount(0); updateVideoCount(0); @@ -548,6 +552,8 @@ export function useWorkflowData() { return { taskObject, taskSketch, + taskScenes, + taskShotSketch, taskVideos, sketchCount, isLoading, diff --git a/components/portal.tsx b/components/portal.tsx new file mode 100644 index 0000000..aaf2f70 --- /dev/null +++ b/components/portal.tsx @@ -0,0 +1,22 @@ +import { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; + +interface PortalProps { + children: React.ReactNode; +} + +export function Portal({ children }: PortalProps) { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + return () => setMounted(false); + }, []); + + if (!mounted) return null; + + return createPortal( + children, + document.body + ); +} \ No newline at end of file diff --git a/components/script-edit-dialog.tsx b/components/script-edit-dialog.tsx new file mode 100644 index 0000000..a17e625 --- /dev/null +++ b/components/script-edit-dialog.tsx @@ -0,0 +1,227 @@ +'use client'; + +import { motion, AnimatePresence } from 'framer-motion'; +import { X } from 'lucide-react'; +import { useState } from 'react'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; + +interface ScriptEditDialogProps { + isOpen: boolean; + onClose: () => void; + onConfirm?: (content: string) => void; +} + +export function ScriptEditDialog({ isOpen, onClose, onConfirm }: ScriptEditDialogProps) { + const [suggestion, setSuggestion] = useState(''); + const [isUpdating, setIsUpdating] = useState(false); + + const handleUpdate = () => { + if (!suggestion.trim()) return; + setIsUpdating(true); + // 模拟更新延迟 + setTimeout(() => { + setIsUpdating(false); + setSuggestion(''); + }, 1000); + }; + + const handleReset = () => { + setSuggestion(''); + }; + + return ( + + {isOpen && ( + <> + {/* 背景遮罩 */} + + + {/* 弹窗内容 */} + + + {/* 关闭按钮 */} + + + + + {/* 标题 */} + +

+ Edit Script +

+
+ + {/* 内容区域 */} + + {/* TypingEditor */} + + + + {/* 修改建议输入区域 */} + +
+
+ setSuggestion(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleUpdate(); + } + }} + className="outline-none box-shadow-none bg-white/50 dark:bg-[#5b75ac20] border-0 focus:ring-2 focus:ring-blue-500/20 transition-all duration-200" + /> +
+ + + +
+
+
+ + {/* 底部按钮 */} + + + + +
+
+ + )} +
+ ); +} \ No newline at end of file diff --git a/components/ui/character-editor.tsx b/components/ui/character-editor.tsx new file mode 100644 index 0000000..da9ae81 --- /dev/null +++ b/components/ui/character-editor.tsx @@ -0,0 +1,213 @@ +import { useState, useRef } from "react"; +import { motion } from "framer-motion"; +import { Button } from "@/components/ui/button"; +import { Sparkles, X, Plus } from 'lucide-react'; +import { cn } from "@/public/lib/utils"; +import ContentEditable from 'react-contenteditable'; + +interface CharacterAttribute { + key: string; + label: string; + value: string; + type: 'text' | 'number' | 'select'; + options?: string[]; +} + +interface CharacterEditorProps { + initialDescription?: string; + onDescriptionChange?: (description: string) => void; + onAttributesChange?: (attributes: CharacterAttribute[]) => void; +} + +const mockParse = (text: string): CharacterAttribute[] => { + // 模拟结构化解析结果 + return [ + { key: "age", label: "年龄", value: "20", type: "number" }, + { key: "gender", label: "性别", value: "女性", type: "select", options: ["男性", "女性", "其他"] }, + { key: "hair", label: "发型", value: "银白短发", type: "text" }, + { key: "race", label: "种族", value: "精灵", type: "text" }, + { key: "skin", label: "肤色", value: "白皙", type: "text" }, + { key: "build", label: "体型", value: "高挑", type: "text" }, + { key: "costume", label: "服装", value: "白色连衣裙", type: "text" }, + ]; +}; + + +export default function CharacterEditor({ + initialDescription = "一个银白短发的精灵女性,大约20岁,肤色白皙,身材高挑,身着白色连衣裙", + onDescriptionChange, + onAttributesChange, +}: CharacterEditorProps) { + const [inputText, setInputText] = useState(initialDescription); + const [isOptimizing, setIsOptimizing] = useState(false); + const [customTags, setCustomTags] = useState([]); + const [newTag, setNewTag] = useState(""); + const attributesRef = useRef(mockParse(initialDescription)); + const contentEditableRef = useRef(null); + + const handleTextChange = (e: { target: { value: string } }) => { + // 移除 HTML 标签,保留换行 + const value = e.target.value; + setInputText(value); + onDescriptionChange?.(value); + }; + + // 格式化文本为 HTML + const formatTextToHtml = (text: string) => { + return text + .split('\n') + .map(line => line || '
') + .join('
'); + }; + + const handleSmartPolish = async () => { + setIsOptimizing(true); + try { + const polishedText = "一位拥有银白短发、白皙肌肤的高挑精灵少女,年龄约二十岁,气质神秘优雅。举手投足间散发着独特的精灵族气质,眼神中透露出智慧与沧桑。"; + setInputText(polishedText); + attributesRef.current = mockParse(polishedText); + onDescriptionChange?.(polishedText); + onAttributesChange?.(attributesRef.current); + } finally { + setIsOptimizing(false); + } + }; + + const handleAttributeChange = (attr: CharacterAttribute, newValue: string) => { + // 移除 HTML 标签 + newValue = newValue.replace(/<[^>]*>/g, ''); + + // 更新描述文本 + let newText = inputText; + if (attr.type === "number" && attr.key === "age") { + newText = newText.replace(/\d+岁/, `${newValue}岁`); + } else { + newText = newText.replace(new RegExp(attr.value, 'g'), newValue); + } + + // 更新属性值 + const newAttr = { ...attr, value: newValue }; + attributesRef.current = attributesRef.current.map(a => + a.key === attr.key ? newAttr : a + ); + + setInputText(newText); + onDescriptionChange?.(newText); + onAttributesChange?.(attributesRef.current); + }; + + return ( +
+ {/* 自由输入区域 */} +
+ + + {/* 智能润色按钮 */} + + + {isOptimizing ? "优化中..." : "智能优化"} + +
+ + {/* 结构化属性标签 */} +
+ {attributesRef.current.map((attr) => ( + + {attr.label}: + handleAttributeChange(attr, e.target.value)} + className="text-sm text-white/90 min-w-[1em] focus:outline-none + border-b border-transparent focus:border-white/30 + hover:border-white/20" + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + (e.target as HTMLElement).blur(); + } + }} + /> + + ))} +
+ + {/* 自定义标签区域 */} +
+
+ {customTags.map((tag) => ( + + {tag} + + + ))} +
+
+ setNewTag(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter' && newTag.trim()) { + setCustomTags(tags => [...tags, newTag.trim()]); + setInputText((text: string) => text + (text.endsWith("。") ? "" : ",") + newTag.trim()); + setNewTag(""); + } + }} + placeholder="添加自定义标签..." + className="flex-1 px-3 py-2 bg-white/5 border-none rounded-lg text-sm + focus:outline-none focus:ring-2 focus:ring-blue-500 placeholder:text-white/30" + /> + +
+
+
+ ); +} diff --git a/components/ui/character-tab-content.tsx b/components/ui/character-tab-content.tsx index 8cf642b..a9aac41 100644 --- a/components/ui/character-tab-content.tsx +++ b/components/ui/character-tab-content.tsx @@ -1,9 +1,18 @@ import React, { useState, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { Upload, Library, Play, Pause, RefreshCw, Wand2, Users } from 'lucide-react'; +import { Upload, Library, Play, Pause, RefreshCw, Wand2, Users, Check, Sparkles, Plus, X } from 'lucide-react'; import { cn } from '@/public/lib/utils'; import { GlassIconButton } from './glass-icon-button'; import { ReplaceCharacterModal } from './replace-character-modal'; +import { Slider } from './slider'; +import CharacterEditor from './character-editor'; + +interface Appearance { + hairStyle: string; + skinTone: string; + facialFeatures: string; + bodyType: string; +} interface Role { name: string; @@ -11,8 +20,33 @@ interface Role { sound: string; soundDescription: string; roleDescription: string; + age: number; + gender: 'male' | 'female' | 'other'; + ethnicity: string; + appearance: Appearance; + // 新增标签数组 + tags: string[]; } +// Mock 数据 +const mockRole: Role = { + name: "青春女学生", + url: "/assets/3dr_chihiro.png", + sound: "", + soundDescription: "", + roleDescription: "一位充满活力和梦想的高中女生,蓝色长发随风飘扬,眼神中透露着对未来的憧憬。她身着整洁的校服,举止优雅而不失活力。", + age: 16, + gender: 'female', + ethnicity: '亚洲人', + appearance: { + hairStyle: "鲜艳蓝色长发", + skinTone: "白皙", + facialFeatures: "大眼睛,清秀五官", + bodyType: "苗条" + }, + tags: ['高中生', '校服', '蓝色长发', '大眼睛', '清秀五官', '苗条'] +}; + interface CharacterTabContentProps { taskSketch: any[]; currentRoleIndex: number; @@ -24,17 +58,74 @@ export function CharacterTabContent({ taskSketch, currentRoleIndex, onSketchSelect, - roles = [] + roles = [mockRole] }: CharacterTabContentProps) { const [isReplaceModalOpen, setIsReplaceModalOpen] = useState(false); const [activeReplaceMethod, setActiveReplaceMethod] = useState('upload'); - const [isPlaying, setIsPlaying] = useState(false); - const [progress, setProgress] = useState(0); - const [editingField, setEditingField] = useState<{ - type: 'name' | 'voiceDescription' | 'characterDescription' | null; - value: string; - }>({ type: null, value: '' }); - const audioRef = useRef(null); + const [newTag, setNewTag] = useState(''); + const [localRole, setLocalRole] = useState(mockRole); + + const textareaRef = useRef(null); + + // 处理标签添加 + const handleAddTag = () => { + if (newTag.trim() && !localRole.tags.includes(newTag.trim())) { + const newTagText = newTag.trim(); + // 更新标签数组 + const updatedTags = [...localRole.tags, newTagText]; + // 更新角色描述文本 + const updatedDescription = localRole.roleDescription + (localRole.roleDescription ? ',' : '') + newTagText; + + setLocalRole({ + ...localRole, + tags: updatedTags, + roleDescription: updatedDescription + }); + setNewTag(''); + + // 自动调整文本框高度 + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'; + } + } + }; + + // 处理标签删除 + const handleRemoveTag = (tagToRemove: string) => { + setLocalRole({ + ...localRole, + tags: localRole.tags.filter(tag => tag !== tagToRemove) + }); + }; + + // 处理年龄滑块变化 + const handleAgeChange = (value: number[]) => { + setLocalRole({ + ...localRole, + age: value[0] + }); + }; + + // 处理描述更新 + const handleDescriptionChange = (e: React.ChangeEvent) => { + setLocalRole({ + ...localRole, + roleDescription: e.target.value + }); + + // 自动调整文本框高度 + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'; + } + }; + + // 新增智能优化处理函数 + const handleSmartOptimize = () => { + console.log('Optimizing character description...'); + // TODO: 调用 AI 优化接口 + }; // 如果没有角色数据,显示占位内容 if (!roles || roles.length === 0) { @@ -46,38 +137,6 @@ export function CharacterTabContent({ ); } - // 处理音频播放进度 - const handleTimeUpdate = () => { - if (audioRef.current) { - const progress = (audioRef.current.currentTime / audioRef.current.duration) * 100; - setProgress(progress); - } - }; - - // 处理播放/暂停 - const togglePlay = () => { - if (audioRef.current) { - if (isPlaying) { - audioRef.current.pause(); - } else { - audioRef.current.play(); - } - setIsPlaying(!isPlaying); - } - }; - - // 处理进度条点击 - const handleProgressClick = (e: React.MouseEvent) => { - if (audioRef.current) { - const rect = e.currentTarget.getBoundingClientRect(); - const x = e.clientX - rect.left; - const percentage = (x / rect.width) * 100; - const time = (percentage / 100) * audioRef.current.duration; - audioRef.current.currentTime = time; - setProgress(percentage); - } - }; - // 获取当前选中的角色 const currentRole = roles[currentRoleIndex]; @@ -117,163 +176,16 @@ export function CharacterTabContent({
- {/* 中间部分:替换角色 */} - -

Replace character

-
- { - setActiveReplaceMethod('upload'); - setIsReplaceModalOpen(true); - }} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > - - Upload character - - - { - setActiveReplaceMethod('library'); - setIsReplaceModalOpen(true); - }} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > - - Character library - - - { - setActiveReplaceMethod('generate'); - setIsReplaceModalOpen(true); - }} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > - - Generate character - -
-
- {/* 下部分:角色详情 */} - {/* 左列:角色信息 */} + {/* 左列:角色预览 */}
- {/* 角色姓名 */} -
- - console.log('name changed:', e.target.value)} - className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg - focus:outline-none focus:border-blue-500" - /> -
- {/* 声音描述 */} -
- -