studio/PersonaManager.tsx

205 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;