import { useCallback, useEffect, useMemo, useState } from "react"; import { Edit3, Film, Image, Sparkles, UserRound } from "lucide-react"; import { MediaTile } from "./MediaTile"; import type { MediaItem } from "../types"; interface CharacterRecord { id: string; name: string; kind: string | null; trigger: string | null; description: string | null; source_images: string[]; loras: Record[]; defaults: Record; } interface CharacterProfileViewProps { characterId: string; items: MediaItem[]; onOpen: (url: string) => void; onDelete: (item: MediaItem) => Promise | void; onGenerate: () => void; } function resolveImageUrl(img: string): string { return img.startsWith("/") ? img : `/media/${img}`; } function isVideo(item: MediaItem) { return item.type === "video" || item.url.endsWith(".mp4") || item.url.endsWith(".webm"); } async function fetchJson(url: string): Promise { const response = await fetch(url); if (!response.ok) throw new Error(`${url} returned ${response.status}`); return await response.json(); } export function CharacterProfileView({ characterId, items, onOpen, onDelete, onGenerate }: CharacterProfileViewProps) { const [character, setCharacter] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [tab, setTab] = useState<"images" | "videos">("images"); const load = useCallback(async () => { try { setError(null); const data = await fetchJson<{ character: CharacterRecord }>(`/api/characters/${characterId}`); setCharacter(data.character || data); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load character"); } finally { setLoading(false); } }, [characterId]); useEffect(() => { load(); }, [load]); const related = useMemo(() => { if (!character) return []; const terms = [character.id, character.name, character.trigger].filter(Boolean).map((value) => String(value).toLowerCase()); return items.filter((item) => { const haystack = [item.name, item.filename, item.prompt].join(" ").toLowerCase(); return terms.some((term) => haystack.includes(term)); }); }, [character, items]); const fallbackItems = related.length > 0 ? related : items; const images = fallbackItems.filter((item) => !isVideo(item)); const videos = fallbackItems.filter(isVideo); const shown = tab === "images" ? images : videos; if (loading) return
Loading character...
; if (error) return
{error}
; if (!character) return null; const avatarUrl = character.source_images[0] ? resolveImageUrl(character.source_images[0]) : null; const loraCount = character.loras.length; return (
{avatarUrl && }
{avatarUrl ? ( {character.name} ) : (
)}
{character.kind && {character.kind}} {character.trigger && trigger: {character.trigger}}

{character.name}

{character.description || "Reusable character identity for agent-generated images and videos."}

Images

{images.length}

Videos

{videos.length}

LoRAs

{loraCount}

{shown.length === 0 ? (
No {tab} found for this character yet.
) : (
{shown.map((item) => ( onOpen(item.url)} onDelete={onDelete} /> ))}
)}
); }