Two things.
1. The architecture_mapper proposal, applied AND made durable.
The GLM proposal (019fddd9) was accepted in full: the agent's system_prompt now
carries the Mermaid-first constraint and its brain was rewritten. Both verified
against the live row and the .h5 file.
But `apply_identity` writes `UPDATE agents SET system_prompt` and
`apply_brain_consolidation` writes that agent's brain — neither touches the team
TEMPLATE. That agent is mission-scoped, so the improvement would have died with
the mission. The model's actual insight was sharp and worth keeping: "Mermaid
diagrams beat prose" lived in the brain SEED and not in the system PROMPT, so it
only applied when the agent happened to consult its brain. That constraint is
now in templates/teams/codebase_research.toml, where every future Codebase
Research team inherits it.
(The proposal's second item mostly restated anti-patterns the seed already
lists, so the seed is unchanged. Applying an LLM's suggestion is not the same as
agreeing with all of it.)
2. Gemini is gone.
Removed: the `gemini.default` provider alias and its `is_exact_provider_match`
prefix, GEMINI_API_KEY forwarding to agent containers, the evaluator's
gemini->gemini family row, the model selectors in claws/teams/planner and in
TeamWizard + AgentComputer, and the commented provider block in the runtime
config example (whose ZEROCLAW_AGENT_MAP example still mapped a worker_gemini
that no longer existed).
`provider_alias_for("gemini")` now returns claude_cli.default via the
unrecognised-model branch, which LOGS. A stray gemini binding degrades visibly
rather than resolving to a provider row we no longer ship. A test pins that, and
another pins that GEMINI_API_KEY is forwarded in NEITHER auth mode, so adding it
back to the list is a visible change rather than an accident.
Avatar generation is DELETED, not disabled — it called Gemini's image model, and
there is no alternative: Claude and Kimi are text-only, and z.ai answers
"Unknown Model" for cogview-3-flash and cogview-4 on our plan (measured, not
assumed). AvatarModal keeps UPLOAD, which never needed a provider; only the
prompt-generation half is gone.
240 backend lib tests, 89 frontend tests, clean tsc + eslint, build succeeds.
109 lines
6.1 KiB
TypeScript
109 lines
6.1 KiB
TypeScript
"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<string> {
|
||
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<string | null>(current ?? null);
|
||
const [busy, setBusy] = useState<null | "save">(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const fileRef = useRef<HTMLInputElement>(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<HTMLInputElement>) {
|
||
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 (
|
||
<div onClick={onClose} role="presentation" style={{ position: "fixed", inset: 0, zIndex: 110, background: "rgba(0,0,0,.62)", backdropFilter: "blur(4px)", display: "flex", alignItems: "center", justifyContent: "center", padding: 24, animation: "cm-fade .18s ease" }}>
|
||
<div onClick={(e) => 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" }}>
|
||
<div style={{ display: "flex", alignItems: "flex-start", gap: 12, marginBottom: 16 }}>
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{ fontSize: 18, fontWeight: 700, color: "#f3f3f5" }}>{clawName}'s image</div>
|
||
<div style={{ fontSize: 12.5, color: "#8a8a92", marginTop: 2 }}>Upload a PNG or JPG.</div>
|
||
</div>
|
||
<button type="button" onClick={onClose} aria-label="Close" style={{ width: 30, height: 30, flex: "none", borderRadius: 8, border: "1px solid rgba(255,255,255,.12)", background: "transparent", color: "#9a9aa2", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><X size={15} /></button>
|
||
</div>
|
||
|
||
<div style={{ display: "flex", gap: 16, marginBottom: 16 }}>
|
||
<div style={{ width: 120, height: 120, flex: "none", borderRadius: 16, overflow: "hidden", border: "1px solid rgba(255,255,255,.1)", background: preview ? `center/cover no-repeat url(${preview})` : "#101014", display: "flex", alignItems: "center", justifyContent: "center", color: "#3a3a40" }}>
|
||
{preview ? null : <Camera size={26} />}
|
||
</div>
|
||
<div style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "center", gap: 8 }}>
|
||
<button type="button" onClick={() => fileRef.current?.click()} style={{ display: "inline-flex", alignItems: "center", gap: 7, justifyContent: "center", padding: "8px 10px", borderRadius: 9, border: "1px solid rgba(255,255,255,.14)", background: "transparent", color: "#dcdce2", fontSize: 12.5, cursor: "pointer" }}><Upload size={14} /> Upload image</button>
|
||
<input ref={fileRef} type="file" accept="image/*" onChange={onPick} style={{ display: "none" }} />
|
||
<span style={{ fontFamily: mono, fontSize: 9, color: "#5a5a62" }}>PNG/JPG · saved at 256×256</span>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
{error ? <div role="alert" style={{ marginTop: 12, fontFamily: mono, fontSize: 10.5, color: "#ff8a7a" }}>{error}</div> : null}
|
||
|
||
<div style={{ display: "flex", gap: 8, marginTop: 16 }}>
|
||
<button type="button" onClick={onClose} style={{ flex: 1, padding: "9px 0", borderRadius: 9, border: "1px solid rgba(255,255,255,.12)", background: "transparent", color: "#cfcfd5", fontSize: 13, cursor: "pointer" }}>Cancel</button>
|
||
<button type="button" onClick={save} disabled={!preview || busy !== null} style={{ flex: 1, padding: "9px 0", borderRadius: 9, border: 0, background: !preview || busy ? "rgba(255,111,97,.4)" : "#ff6f61", color: "#2a0d0a", fontSize: 13, fontWeight: 700, cursor: !preview || busy ? "default" : "pointer" }}>{busy === "save" ? "Saving…" : "Save image"}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|