video-flow-b/components/ui/character-tab-content.tsx
2025-07-30 16:15:25 +08:00

337 lines
12 KiB
TypeScript

import React, { useState, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Upload, Library, Play, Pause, RefreshCw, Wand2, Users, Check, ReplaceAll, 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';
import ImageBlurTransition from './ImageBlurTransition';
import FloatingGlassPanel from './FloatingGlassPanel';
import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel';
import { ImageWave } from '@/components/ui/ImageWave';
interface Appearance {
hairStyle: string;
skinTone: string;
facialFeatures: string;
bodyType: string;
}
interface Role {
name: string;
url: string;
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;
onSketchSelect: (index: number) => void;
roles: Role[];
}
const imageUrls = [
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
];
export function CharacterTabContent({
taskSketch,
currentRoleIndex,
onSketchSelect,
roles = [mockRole]
}: CharacterTabContentProps) {
const [isReplaceModalOpen, setIsReplaceModalOpen] = useState(false);
const [activeReplaceMethod, setActiveReplaceMethod] = useState('upload');
const [newTag, setNewTag] = useState('');
const [localRole, setLocalRole] = useState(mockRole);
const [currentRole, setCurrentRole] = useState(roles[currentRoleIndex]);
const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false);
const [replacePanelKey, setReplacePanelKey] = useState(0);
const [ignoreReplace, setIgnoreReplace] = useState(false);
const [isReplaceLibraryOpen, setIsReplaceLibraryOpen] = useState(false);
const [replaceLibraryKey, setReplaceLibraryKey] = useState(0);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const handleReplaceCharacter = (url: string) => {
setCurrentRole({
...currentRole,
url: url
});
setIsReplacePanelOpen(true);
};
const handleConfirmReplace = (selectedShots: string[], addToLibrary: boolean) => {
// 处理替换确认逻辑
console.log('Selected shots:', selectedShots);
console.log('Add to library:', addToLibrary);
setIsReplacePanelOpen(false);
};
const handleCloseReplacePanel = () => {
setIsReplacePanelOpen(false);
setIgnoreReplace(true);
};
const handleChangeRole = (index: number) => {
if (currentRole.url !== roles[currentRoleIndex].url && !ignoreReplace) {
// 提示 角色已修改,弹出替换角色面板
if (isReplacePanelOpen) {
setReplacePanelKey(replacePanelKey + 1);
} else {
setIsReplacePanelOpen(true);
}
return;
}
onSketchSelect(index);
setCurrentRole(roles[index]);
};
// 如果没有角色数据,显示占位内容
if (!roles || roles.length === 0) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
<Users className="w-16 h-16 mb-4" />
<p>No character data</p>
</div>
);
}
return (
<div className="flex flex-col gap-6">
{/* 上部分:角色缩略图 */}
<motion.div
className="space-y-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<div className="relative">
<div className="flex gap-4 overflow-x-auto p-2 hide-scrollbar">
{roles.map((role, index) => (
<motion.div
key={`role-${index}`}
className={cn(
'relative flex-shrink-0 w-24 rounded-lg overflow-hidden cursor-pointer',
'aspect-[9/16]',
currentRoleIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
)}
onClick={() => handleChangeRole(index)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<img
src={role.url}
alt={role.name}
className="w-full h-full object-cover"
/>
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90 line-clamp-1">{role.name}</span>
</div>
</motion.div>
))}
</div>
</div>
</motion.div>
{/* 下部分:角色详情 */}
<motion.div
className="grid grid-cols-2 gap-6 p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
{/* 左列:角色预览 */}
<div className="space-y-4">
{/* 角色预览图 */}
<div className="w-full h-full mx-auto rounded-lg relative group">
<ImageBlurTransition
src={currentRole.url}
alt={currentRole.name}
width='100%'
height='100%'
/>
{/* 应用角色按钮 */}
<div className='absolute top-3 right-3 flex gap-2'>
<motion.button
className="p-2 bg-black/50 hover:bg-black/70
text-white rounded-full backdrop-blur-sm transition-colors z-10"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setIsReplaceLibraryOpen(true)}
>
<Library className="w-4 h-4" />
</motion.button>
</div>
</div>
</div>
{/* 右列:角色信息 */}
<div className="space-y-4">
<CharacterEditor
initialDescription={localRole.roleDescription}
onDescriptionChange={(description) => {
setLocalRole({
...localRole,
roleDescription: description
});
}}
onAttributesChange={(attributes) => {
const newRole = { ...localRole };
attributes.forEach(attr => {
switch (attr.key) {
case 'age':
newRole.age = parseInt(attr.value);
break;
case 'gender':
if (attr.value === '男性') {
newRole.gender = 'male';
} else if (attr.value === '女性') {
newRole.gender = 'female';
} else {
newRole.gender = 'other';
}
break;
case 'hair':
newRole.appearance.hairStyle = attr.value;
break;
case 'skin':
newRole.appearance.skinTone = attr.value;
break;
case 'build':
newRole.appearance.bodyType = attr.value;
break;
}
});
setLocalRole(newRole);
}}
onReplaceCharacter={(url) => {
handleReplaceCharacter(url);
}}
/>
{/* 重新生成按钮、替换形象按钮 */}
<div className="grid grid-cols-2 gap-2">
<motion.button
onClick={() => console.log('Replace')}
className="flex items-center justify-center gap-2 px-4 py-3 bg-pink-500/10 hover:bg-pink-500/20
text-pink-500 rounded-lg transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<ReplaceAll className="w-4 h-4" />
<span>Replace</span>
</motion.button>
<motion.button
onClick={() => console.log('Regenerate')}
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
text-blue-500 rounded-lg transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<RefreshCw className="w-4 h-4" />
<span>Regenerate</span>
</motion.button>
</div>
</div>
</motion.div>
<FloatingGlassPanel open={isReplacePanelOpen} width='500px' r_key={replacePanelKey}>
<ReplaceCharacterPanel
shots={mockShots}
character={mockCharacter}
onClose={() => handleCloseReplacePanel()}
onConfirm={handleConfirmReplace}
/>
</FloatingGlassPanel>
{/* 从角色库中选择角色 */}
<FloatingGlassPanel open={isReplaceLibraryOpen} width='90vw' r_key={replaceLibraryKey}>
{/* 标题 从角色库中选择角色 */}
<div className="text-2xl font-semibold text-white text-center">Role Library</div>
{/* 内容 */}
<ImageWave
images={imageUrls}
containerWidth="90vw"
containerHeight="calc(var(--index) * 15)"
itemWidth="calc(var(--index) * 2)"
itemHeight="calc(var(--index) * 12)"
gap="0.1rem"
autoAnimate={true}
autoAnimateInterval={100}
onClick={(index) => {
console.log('index', index);
}}
/>
{/* 操作按钮 */}
<div className="flex justify-end gap-4">
<button
onClick={() => setIsReplaceLibraryOpen(false)}
className="px-4 py-2 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
>
</button>
<button
onClick={() => {
console.log('replace');
}}
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
>
</button>
</div>
</FloatingGlassPanel>
</div>
);
}