import React, { useCallback, useEffect, useState } from "react"; import { Check, Copy, Link2, Trash2 } from "lucide-react"; import { PanelShell, Label, Field, Grid, IS, SS, focusIn, focusOut, HelperText, RadioGroup } from "../panels/PanelShell"; import { aiAvatarApi, uploadMediaFile, type AiAvatarConfig, type AiAvatarEventHeader, type AiAvatarTemplate, } from "../../../services/api"; import { aiAvatarVisitorUrl } from "../../utils/aiAvatarUrls"; import { EVENT_HEADER_TOGGLE_LABELS, eventHeaderFromApi, eventHeaderToApi, type EventHeaderToggleKey, } from "../../utils/formEventHeaderDisplay"; type Tab = "settings" | "avatars" | "links"; const DEFAULT_HEADER = eventHeaderFromApi(null); export function AiAvatarManagePanel({ eventId, eventName, canWrite, onClose, onChanged, }: { eventId: string; eventName?: string; canWrite: boolean; onClose: () => void; onChanged?: () => void; }) { const [tab, setTab] = useState("settings"); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [config, setConfig] = useState(null); const [avatars, setAvatars] = useState([]); const [copied, setCopied] = useState(false); const [editingAvatarId, setEditingAvatarId] = useState(null); const [uploadingImage, setUploadingImage] = useState(false); const [avatarDraft, setAvatarDraft] = useState({ name: "Avatar", enabled: true, imageUrl: "" as string, }); const [draft, setDraft] = useState({ accessMode: "open" as "open" | "login", allowDownload: true, eventHeader: DEFAULT_HEADER, sharePlatforms: { linkedin: false, facebook: false, instagram: false, snapchat: false, x: false, whatsapp: false, }, }); const load = useCallback(async () => { setLoading(true); setError(null); try { const res = await aiAvatarApi.get(eventId); if (res.error || !res.data) { setError(res.error || "Failed to load AI Avatar"); setConfig(null); setAvatars([]); return; } const data = res.data; setConfig(data); setAvatars(data.avatars || []); setDraft({ accessMode: data.accessMode || "open", allowDownload: data.allowDownload !== false, eventHeader: eventHeaderFromApi(data.eventHeader as AiAvatarEventHeader | null), sharePlatforms: { linkedin: Boolean(data.sharePlatforms?.linkedin), facebook: Boolean(data.sharePlatforms?.facebook), instagram: Boolean(data.sharePlatforms?.instagram), snapchat: Boolean(data.sharePlatforms?.snapchat), x: Boolean(data.sharePlatforms?.x), whatsapp: Boolean(data.sharePlatforms?.whatsapp), }, }); } catch (e) { setError(e instanceof Error ? e.message : "Failed to load"); } finally { setLoading(false); } }, [eventId]); useEffect(() => { void load(); }, [load]); const openAvatarEditor = (avatar: AiAvatarTemplate) => { setEditingAvatarId(avatar._id); setAvatarDraft({ name: avatar.name || "Avatar", enabled: avatar.enabled !== false, imageUrl: avatar.imageUrl || "", }); }; const saveSettings = async () => { if (!canWrite) return; setSaving(true); setError(null); try { const res = await aiAvatarApi.update(eventId, { accessMode: draft.accessMode, allowDownload: draft.allowDownload, sharePlatforms: draft.sharePlatforms, eventHeader: eventHeaderToApi(draft.eventHeader), }); if (res.error || !res.data) { setError(res.error || "Failed to save"); return; } setConfig(res.data); setAvatars(res.data.avatars || []); onChanged?.(); } catch (e) { setError(e instanceof Error ? e.message : "Failed to save"); } finally { setSaving(false); } }; const saveAvatar = async () => { if (!canWrite || !editingAvatarId) return; setSaving(true); setError(null); try { const res = await aiAvatarApi.updateAvatar(eventId, editingAvatarId, { name: avatarDraft.name, enabled: avatarDraft.enabled, imageUrl: avatarDraft.imageUrl || null, }); if (res.error || !res.data) { setError(res.error || "Failed to save avatar"); return; } setAvatars((prev) => prev.map((a) => (a._id === res.data!._id ? res.data! : a))); onChanged?.(); } catch (e) { setError(e instanceof Error ? e.message : "Failed to save avatar"); } finally { setSaving(false); } }; const addAvatar = async () => { if (!canWrite) return; setSaving(true); setError(null); try { const res = await aiAvatarApi.createAvatar(eventId, { name: `Avatar ${avatars.length + 1}`, }); if (res.error || !res.data) { setError(res.error || "Failed to create avatar"); return; } setAvatars((prev) => [...prev, res.data!]); openAvatarEditor(res.data); onChanged?.(); } catch (e) { setError(e instanceof Error ? e.message : "Failed to create avatar"); } finally { setSaving(false); } }; const deleteAvatar = async (avatarId: string) => { if (!canWrite) return; if (!window.confirm("Delete this avatar?")) return; setError(null); try { await aiAvatarApi.deleteAvatar(eventId, avatarId); setAvatars((prev) => prev.filter((a) => a._id !== avatarId)); if (editingAvatarId === avatarId) setEditingAvatarId(null); onChanged?.(); } catch (e) { setError(e instanceof Error ? e.message : "Failed to delete"); } }; const uploadAvatarImage = async (file: File | null) => { if (!file || !canWrite) return; setUploadingImage(true); setError(null); try { const r = await uploadMediaFile(file, { eventId, module: "ai-avatar/avatars" }); const url = r.url || ""; if (!url) throw new Error("Upload succeeded but no URL returned"); setAvatarDraft((d) => ({ ...d, imageUrl: url })); } catch (e) { setError(e instanceof Error ? e.message : "Avatar upload failed"); } finally { setUploadingImage(false); } }; const copyLink = async () => { if (!config?.uploadToken) return; try { await navigator.clipboard.writeText(aiAvatarVisitorUrl(config.uploadToken)); setCopied(true); window.setTimeout(() => setCopied(false), 1500); } catch { /* ignore */ } }; const tabBtn = (id: Tab, label: string) => ( ); const ctaLabel = tab === "avatars" && editingAvatarId ? "Save avatar" : tab === "settings" ? "Save settings" : "Save"; const showCta = tab === "settings" || (tab === "avatars" && Boolean(editingAvatarId)); return ( { if (tab === "avatars" && editingAvatarId) void saveAvatar(); else void saveSettings(); }} panelWidth="min(720px,100vw)" >
{tabBtn("settings", "Settings")} {tabBtn("avatars", "Avatars")} {tabBtn("links", "Links")}
{error ? (

{error}

) : null} {loading ? (

Loading…

) : null} {!loading && tab === "links" && config ? (
Visitor upload URL

Guests pick an avatar, upload a selfie, then download or share

{config.uploadToken ? aiAvatarVisitorUrl(config.uploadToken) : "Enable AI Avatar to generate link"}
{canWrite ? ( ) : null}
) : null} {!loading && tab === "settings" ? (
) : null} {!loading && tab === "avatars" ? ( editingAvatarId ? (
) : (

{avatars.length} avatar{avatars.length === 1 ? "" : "s"}

{canWrite ? ( ) : null}
{!avatars.length ? (

No avatars yet. Add an image template for visitors to choose.

) : (
{avatars.map((avatar) => (
{avatar.imageUrl ? ( {avatar.name} ) : null}

{avatar.name}

{avatar.enabled !== false ? "Enabled" : "Disabled"} {!avatar.imageUrl ? " · No image" : ""}

{canWrite ? ( ) : null}
))}
)}
) ) : null}
); }