205 lines
11 KiB
TypeScript
205 lines
11 KiB
TypeScript
|
|
|
|||
|
|
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;
|