import { useCallback, useEffect, useMemo, useState } from "react"; import { Cpu, Edit3, Film, Image, Sparkles, UserRound } from "lucide-react"; import { MediaTile } from "./MediaTile"; import type { MediaItem } from "../types"; interface LoraEntry { name: string; strength: number; workflow?: string; base_model?: string; } interface Checkpoint { name: string; step: number | null; size_bytes: number; modified_at: string; } interface CheckpointsResponse { job_name: string; checkpoints: Checkpoint[]; count: number; updated_at: string; } interface CharacterRecord { id: string; name: string; kind: string | null; trigger: string | null; description: string | null; source_images: string[]; loras: LoraEntry[]; 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 [checkpoints, setCheckpoints] = useState(null); const load = useCallback(async () => { try { setError(null); const data = await fetchJson<{ character: CharacterRecord }>(`/api/characters/${characterId}`); const char: CharacterRecord = data.character || (data as unknown as CharacterRecord); setCharacter(char); if (char.loras.length > 0) { try { const ckpts = await fetchJson("/api/lora-training/checkpoints"); setCheckpoints(ckpts); } catch { // checkpoints optional — don't block character render } } } 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}

{character.loras.length > 0 && (

Model

{character.loras.map((lora, i) => { const shortName = lora.name.split("/").pop() ?? lora.name; return (

{shortName}

{lora.base_model && (

Base

{lora.base_model}

)} {lora.workflow && (

Workflow

{lora.workflow}

)}

Strength

{lora.strength}

); })}
{checkpoints && (

Training Checkpoints — {checkpoints.job_name}

checked {new Date(checkpoints.updated_at).toLocaleString()}

{checkpoints.checkpoints.map((ck, i) => ( ))}
Step File Size Date
{ck.step ?? "final"} {ck.name} {(ck.size_bytes / 1024 / 1024).toFixed(0)} MB {new Date(ck.modified_at).toLocaleDateString()}
)}
)}
{shown.length === 0 ? (
No {tab} found for this character yet.
) : (
{shown.map((item) => ( onOpen(item.url)} onDelete={onDelete} /> ))}
)}
); }