studio/PersonaManager.tsx

205 lines
11 KiB
TypeScript
Raw Permalink Normal View History

2025-12-10 09:43:39 +00:00
import React, { useState } from 'react';
import { InfluencerPersona } from '../types';
import { User, Plus, Save, Trash2, Smartphone, Sparkles, ChevronRight } from 'lucide-react';
interface PersonaManagerProps {
savedPersonas: InfluencerPersona[];
activePersona: InfluencerPersona | null;
onUpdatePersonas: (personas: InfluencerPersona[]) => void;
onSelectPersona: (persona: InfluencerPersona) => void;
onGeneratePlan: (campaignInput: string) => void;
isAnalyzing: boolean;
}
const PersonaManager: React.FC<PersonaManagerProps> = ({
savedPersonas,
activePersona,
onUpdatePersonas,
onSelectPersona,
onGeneratePlan,
isAnalyzing
}) => {
const [isEditing, setIsEditing] = useState(false);
const [form, setForm] = useState<InfluencerPersona>({
id: '', name: '', gender: '', age: '', appearance: '', style: '', traits: ''
});
const [campaignInput, setCampaignInput] = useState('');
const handleCreate = () => {
const newPersona: InfluencerPersona = {
id: Date.now().toString(),
name: '妍妍',
gender: '女性',
age: '20',
appearance: '綁著高馬尾清新的韓國女生臉孔大眼睛身高170公分身材結實勻稱像是專業的網球選手',
style: '穿著時尚的運動套裝 (Athletic Wear) 與緊身瑜珈褲 (Leggings),搭配白色運動鞋,展現健康活力',
traits: '活潑、充滿能量、喜歡戶外運動、陽光笑容'
};
setForm(newPersona);
setIsEditing(true);
onSelectPersona(newPersona); // Temporarily select for preview
};
const handleEdit = (persona: InfluencerPersona) => {
setForm(persona);
setIsEditing(true);
onSelectPersona(persona);
};
const handleSave = () => {
let newPersonas = [...savedPersonas];
const index = newPersonas.findIndex(p => p.id === form.id);
if (index >= 0) {
newPersonas[index] = form;
} else {
newPersonas.push(form);
}
onUpdatePersonas(newPersonas);
onSelectPersona(form);
setIsEditing(false);
};
const handleDelete = (id: string, e: React.MouseEvent) => {
e.stopPropagation();
if(confirm('確定要刪除此人設嗎?')) {
const newPersonas = savedPersonas.filter(p => p.id !== id);
onUpdatePersonas(newPersonas);
if (activePersona?.id === id) {
onSelectPersona(null as any);
setIsEditing(false);
}
}
};
return (
<div className="animate-fade-in grid grid-cols-1 md:grid-cols-12 gap-6 max-w-6xl mx-auto">
{/* LEFT: List */}
<div className="md:col-span-4 space-y-4">
<div className="glass-card rounded-2xl p-6 min-h-[500px] flex flex-col">
<div className="flex justify-between items-center mb-6">
<h3 className="text-xl font-bold text-white flex items-center gap-2">
<User className="w-5 h-5 text-cyan-400" />
</h3>
<button onClick={handleCreate} className="glass-btn p-2 rounded-lg text-cyan-400 hover:bg-cyan-500/10">
<Plus className="w-4 h-4" />
</button>
</div>
<div className="space-y-3 flex-1 overflow-y-auto pr-2">
{savedPersonas.length === 0 && (
<div className="text-center text-zinc-500 py-10 text-sm">
<br/>+
</div>
)}
{savedPersonas.map(p => (
<div
key={p.id}
onClick={() => { setIsEditing(false); onSelectPersona(p); }}
className={`p-4 rounded-xl cursor-pointer border transition-all ${activePersona?.id === p.id ? 'bg-cyan-500/10 border-cyan-500/50' : 'bg-white/5 border-transparent hover:border-white/20'}`}
>
<div className="flex justify-between items-start">
<div className="font-bold text-white">{p.name}</div>
<div className="flex gap-1">
<button onClick={(e) => { e.stopPropagation(); handleEdit(p); }} className="p-1 hover:text-white text-zinc-500"><Sparkles className="w-3 h-3" /></button>
<button onClick={(e) => handleDelete(p.id, e)} className="p-1 hover:text-red-400 text-zinc-500"><Trash2 className="w-3 h-3" /></button>
</div>
</div>
<div className="text-xs text-zinc-400 mt-1 line-clamp-2">{p.appearance}</div>
</div>
))}
</div>
</div>
</div>
{/* RIGHT: Detail / Edit */}
<div className="md:col-span-8">
<div className="glass-card rounded-2xl p-8 min-h-[500px] flex flex-col">
{isEditing ? (
// EDIT MODE
<div className="space-y-4 animate-fade-in">
<h3 className="text-xl font-bold text-white mb-4 border-b border-white/10 pb-4"></h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs font-bold text-zinc-500 uppercase"></label>
<input type="text" value={form.name} onChange={e => setForm({...form, name: e.target.value})} className="glass-input w-full rounded-lg p-2" />
</div>
<div>
<label className="text-xs font-bold text-zinc-500 uppercase"> / </label>
<div className="flex gap-2">
<input type="text" value={form.gender} onChange={e => setForm({...form, gender: e.target.value})} className="glass-input w-full rounded-lg p-2" placeholder="女性" />
<input type="text" value={form.age} onChange={e => setForm({...form, age: e.target.value})} className="glass-input w-full rounded-lg p-2" placeholder="20" />
</div>
</div>
</div>
<div>
<label className="text-xs font-bold text-zinc-500 uppercase"> (Prompt )</label>
<textarea value={form.appearance} onChange={e => setForm({...form, appearance: e.target.value})} className="glass-input w-full rounded-lg p-3 h-24" placeholder="例如粉色波浪捲髮身高165公分右眼角有淚痣..." />
</div>
<div>
<label className="text-xs font-bold text-zinc-500 uppercase">穿</label>
<input type="text" value={form.style} onChange={e => setForm({...form, style: e.target.value})} className="glass-input w-full rounded-lg p-2" placeholder="Y2K, Cyberpunk, 簡約風..." />
</div>
<div className="pt-4 flex justify-end gap-3">
<button onClick={() => setIsEditing(false)} className="text-zinc-400 hover:text-white px-4"></button>
<button onClick={handleSave} className="glass-btn bg-cyan-600/20 text-cyan-300 border-cyan-500/50 px-6 py-2 rounded-lg font-bold flex items-center gap-2">
<Save className="w-4 h-4" />
</button>
</div>
</div>
) : activePersona ? (
// PREVIEW & CAMPAIGN MODE
<div className="flex flex-col h-full animate-fade-in">
<div className="flex items-start gap-6 mb-8">
<div className="w-24 h-24 rounded-full bg-cyan-500/20 flex items-center justify-center border-2 border-cyan-500/30 shadow-[0_0_20px_rgba(6,182,212,0.3)]">
<User className="w-10 h-10 text-cyan-300" />
</div>
<div>
<h2 className="text-3xl font-bold text-white mb-2">{activePersona.name}</h2>
<div className="flex flex-wrap gap-2 text-sm text-zinc-300">
<span className="bg-white/10 px-2 py-1 rounded">{activePersona.gender}</span>
<span className="bg-white/10 px-2 py-1 rounded">{activePersona.age}</span>
<span className="bg-white/10 px-2 py-1 rounded">{activePersona.style}</span>
</div>
<p className="mt-3 text-zinc-400 text-sm max-w-lg">{activePersona.appearance}</p>
</div>
</div>
<div className="mt-auto pt-8 border-t border-white/10">
<h4 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
<Smartphone className="w-5 h-5 text-pink-400" />
</h4>
<p className="text-xs text-zinc-400 mb-3"> Vlog</p>
<textarea
value={campaignInput}
onChange={e => setCampaignInput(e.target.value)}
className="glass-input w-full rounded-xl p-4 text-sm h-32 mb-4"
placeholder="輸入企劃內容..."
/>
<div className="flex justify-end">
<button
onClick={() => onGeneratePlan(campaignInput)}
disabled={isAnalyzing || !campaignInput}
className="glass-btn bg-gradient-to-r from-cyan-600/40 to-blue-600/40 border-cyan-500/50 text-white px-8 py-3 rounded-xl font-bold flex items-center gap-2 hover:shadow-lg hover:shadow-cyan-500/20"
>
{isAnalyzing ? '正在規劃分鏡...' : '生成社群貼文規劃'}
{!isAnalyzing && <ChevronRight className="w-4 h-4" />}
</button>
</div>
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center h-full text-zinc-500">
<User className="w-16 h-16 mb-4 opacity-20" />
<p></p>
</div>
)}
</div>
</div>
</div>
);
};
export default PersonaManager;