"use client"; // Avatar editor modal for a claw: upload an image, preview it, then Save // (downscaled to 256² and persisted via PATCH /api/claws/{id}). // // Prompt-based generation was removed with the rest of the Gemini dependency: // it called Gemini's image model, and no provider we use can generate images // (Claude and Kimi are text-only; z.ai returns "Unknown Model" for cogview on // our plan). Upload is untouched — it never needed a provider. Re-add a // generate path here if an image provider is ever wired. import { useEffect, useRef, useState } from "react"; import { Camera, Upload, X } from "lucide-react"; const mono = "'JetBrains Mono', ui-monospace, monospace"; // Cover-fit to a square and re-encode small so avatars stay lightweight. function downscale(dataUrl: string, size = 256): Promise { return new Promise((resolve) => { const img = new Image(); img.onload = () => { const c = document.createElement("canvas"); c.width = size; c.height = size; const ctx = c.getContext("2d"); if (!ctx) { resolve(dataUrl); return; } const scale = Math.max(size / img.width, size / img.height); const w = img.width * scale; const h = img.height * scale; ctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h); resolve(c.toDataURL("image/jpeg", 0.85)); }; img.onerror = () => resolve(dataUrl); img.src = dataUrl; }); } export function AvatarModal({ clawId, clawName, current, onClose, onSaved }: { clawId: string; clawName: string; current?: string | null; onClose: () => void; onSaved: (dataUrl: string) => void }) { const [preview, setPreview] = useState(current ?? null); const [busy, setBusy] = useState(null); const [error, setError] = useState(null); const fileRef = useRef(null); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); function onPick(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { setPreview(String(reader.result)); setError(null); }; reader.readAsDataURL(file); } async function save() { if (!preview || busy) return; setBusy("save"); setError(null); try { const small = await downscale(preview); const res = await fetch(`/api/claws/${clawId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ avatar: small }) }); if (!res.ok) { setError("could not save image"); setBusy(null); return; } onSaved(small); onClose(); } catch { setError("could not save image"); setBusy(null); } } return (
e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Agent image" style={{ width: "100%", maxWidth: 460, borderRadius: 16, background: "#0d0d10", border: "1px solid rgba(255,255,255,.1)", boxShadow: "0 30px 90px rgba(0,0,0,.6)", padding: 22, animation: "scale-in .18s ease" }}>
{clawName}'s image
Upload a PNG or JPG.
{preview ? null : }
PNG/JPG · saved at 256×256
{error ?
{error}
: null}
); }