refactor: strip Gemini from the platform, and level up the architecture_mapper
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.
This commit is contained in:
@@ -240,7 +240,6 @@ pub fn provider_family(spec: &str) -> String {
|
|||||||
("glm", "glm"),
|
("glm", "glm"),
|
||||||
("kimi", "kimi"),
|
("kimi", "kimi"),
|
||||||
("moonshot", "kimi"),
|
("moonshot", "kimi"),
|
||||||
("gemini", "gemini"),
|
|
||||||
("llama", "groq"),
|
("llama", "groq"),
|
||||||
] {
|
] {
|
||||||
if s.contains(needle) {
|
if s.contains(needle) {
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ pub fn forwarded_provider_keys(auth: RuntimeAuth) -> Vec<&'static str> {
|
|||||||
// in the container. They are unrelated to the Anthropic credential and
|
// in the container. They are unrelated to the Anthropic credential and
|
||||||
// forward in both auth modes.
|
// forward in both auth modes.
|
||||||
let mut keys = vec![
|
let mut keys = vec![
|
||||||
"GEMINI_API_KEY",
|
|
||||||
"GROQ_API_KEY",
|
"GROQ_API_KEY",
|
||||||
"OPENAI_API_KEY",
|
"OPENAI_API_KEY",
|
||||||
"ZAI_API_KEY",
|
"ZAI_API_KEY",
|
||||||
@@ -226,7 +225,7 @@ fn microvm_provider_env_from(
|
|||||||
// Non-Anthropic providers a mission's tools may need, forwarded when set.
|
// Non-Anthropic providers a mission's tools may need, forwarded when set.
|
||||||
// ANTHROPIC_API_KEY is absent from this list and must stay absent — see the
|
// ANTHROPIC_API_KEY is absent from this list and must stay absent — see the
|
||||||
// doc comment above.
|
// doc comment above.
|
||||||
for k in ["GEMINI_API_KEY", "GROQ_API_KEY", "OPENAI_API_KEY"] {
|
for k in ["GROQ_API_KEY", "OPENAI_API_KEY"] {
|
||||||
if let Some(v) = lookup(k).filter(|v| !v.trim().is_empty()) {
|
if let Some(v) = lookup(k).filter(|v| !v.trim().is_empty()) {
|
||||||
env.push((k.to_string(), v));
|
env.push((k.to_string(), v));
|
||||||
}
|
}
|
||||||
@@ -1243,10 +1242,10 @@ mod tests {
|
|||||||
let pairs = provider_env_from(RuntimeAuth::Subscription, |k| match k {
|
let pairs = provider_env_from(RuntimeAuth::Subscription, |k| match k {
|
||||||
"CLAUDE_CODE_OAUTH_TOKEN" => Some(" ".into()),
|
"CLAUDE_CODE_OAUTH_TOKEN" => Some(" ".into()),
|
||||||
"OPENAI_API_KEY" => Some(String::new()),
|
"OPENAI_API_KEY" => Some(String::new()),
|
||||||
"GEMINI_API_KEY" => Some("real".into()),
|
"GROQ_API_KEY" => Some("real".into()),
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
assert_eq!(pairs, vec![("GEMINI_API_KEY".to_string(), "real".to_string())]);
|
assert_eq!(pairs, vec![("GROQ_API_KEY".to_string(), "real".to_string())]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1259,7 +1258,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
// Unrelated providers have no subscription equivalent and must survive.
|
// Unrelated providers have no subscription equivalent and must survive.
|
||||||
for k in [
|
for k in [
|
||||||
"GEMINI_API_KEY",
|
|
||||||
"GROQ_API_KEY",
|
"GROQ_API_KEY",
|
||||||
"OPENAI_API_KEY",
|
"OPENAI_API_KEY",
|
||||||
"ZAI_API_KEY",
|
"ZAI_API_KEY",
|
||||||
@@ -1297,14 +1295,19 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn api_key_mode_forwards_everything() {
|
fn api_key_mode_forwards_everything() {
|
||||||
let keys = forwarded_provider_keys(RuntimeAuth::ApiKey);
|
let keys = forwarded_provider_keys(RuntimeAuth::ApiKey);
|
||||||
for k in [
|
for k in ["ANTHROPIC_API_KEY", "GROQ_API_KEY", "OPENAI_API_KEY"] {
|
||||||
"ANTHROPIC_API_KEY",
|
|
||||||
"GEMINI_API_KEY",
|
|
||||||
"GROQ_API_KEY",
|
|
||||||
"OPENAI_API_KEY",
|
|
||||||
] {
|
|
||||||
assert!(keys.contains(&k), "{k} should be forwarded in api_key mode");
|
assert!(keys.contains(&k), "{k} should be forwarded in api_key mode");
|
||||||
}
|
}
|
||||||
|
// Gemini is stripped from the platform entirely: no provider row, no
|
||||||
|
// selector entry, and no key forwarded to agent containers. Asserted so
|
||||||
|
// a future "just add it back to the list" restores the dependency
|
||||||
|
// visibly rather than by accident.
|
||||||
|
for mode in [RuntimeAuth::ApiKey, RuntimeAuth::Subscription] {
|
||||||
|
assert!(
|
||||||
|
!forwarded_provider_keys(mode).contains(&"GEMINI_API_KEY"),
|
||||||
|
"GEMINI_API_KEY must not be forwarded in {mode:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
assert!(
|
assert!(
|
||||||
!keys.contains(&"CLAUDE_CODE_OAUTH_TOKEN"),
|
!keys.contains(&"CLAUDE_CODE_OAUTH_TOKEN"),
|
||||||
"api_key mode must not also ship the subscription token"
|
"api_key mode must not also ship the subscription token"
|
||||||
|
|||||||
@@ -1127,7 +1127,7 @@ pub async fn patch(
|
|||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct SetModelRequest {
|
pub struct SetModelRequest {
|
||||||
/// Model selector (claude / glm / glm-5.2 / kimi / gemini / groq /
|
/// Model selector (claude / glm / glm-5.2 / kimi / groq /
|
||||||
/// specific model id like `claude-sonnet-5`). Resolved through the
|
/// specific model id like `claude-sonnet-5`). Resolved through the
|
||||||
/// same RuntimeProvisioner::provider_alias_for that team creation
|
/// same RuntimeProvisioner::provider_alias_for that team creation
|
||||||
/// uses, so shorthand + fully-qualified ids both work.
|
/// uses, so shorthand + fully-qualified ids both work.
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ research tools. Grant write only to members that actually produce code or commit
|
|||||||
- glm-4.7 — strong general reasoning (Z.ai); best cost/quality default for most workers.\n\
|
- glm-4.7 — strong general reasoning (Z.ai); best cost/quality default for most workers.\n\
|
||||||
- glm-5.2 — GLM Opus-class for the hardest reasoning roles; higher cost.\n\
|
- glm-5.2 — GLM Opus-class for the hardest reasoning roles; higher cost.\n\
|
||||||
- kimi — excellent for code-heavy roles.\n\
|
- kimi — excellent for code-heavy roles.\n\
|
||||||
- gemini — Gemini 2.5 Flash: very fast; classification, summarization, high-volume tasks.\n\
|
|
||||||
- groq — fastest/cheapest; simple sequential high-throughput steps.\n\
|
- groq — fastest/cheapest; simple sequential high-throughput steps.\n\
|
||||||
AGENT TOOLS each agent can use at runtime: web.search (find sources), browser.goto (fetch a URL), \
|
AGENT TOOLS each agent can use at runtime: web.search (find sources), browser.goto (fetch a URL), \
|
||||||
files.write (build a markdown vault in the shared drive), chat.send (delegate to teammates), \
|
files.write (build a markdown vault in the shared drive), chat.send (delegate to teammates), \
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use crate::{ApiError, AppState, Authed};
|
|||||||
pub struct TeamMemberInput {
|
pub struct TeamMemberInput {
|
||||||
pub role: String,
|
pub role: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// Model selector: claude | glm | glm-5.2 | kimi | gemini | groq.
|
/// Model selector: claude | glm | glm-5.2 | kimi | groq.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub model: String,
|
pub model: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -32,8 +32,11 @@ pub fn claw_alias(claw_id: Uuid) -> String {
|
|||||||
/// Putting both on one credential would mean a single limit blinds the
|
/// Putting both on one credential would mean a single limit blinds the
|
||||||
/// verifier at exactly the moment there is most to verify.
|
/// verifier at exactly the moment there is most to verify.
|
||||||
///
|
///
|
||||||
/// Non-Claude families are unchanged: `groq.default`, `gemini.default`, and
|
/// Non-Claude families are unchanged: `groq.default` and the GLM/Kimi
|
||||||
/// the GLM/Kimi substitution below.
|
/// substitution below. Gemini was removed entirely — a `gemini*` model now
|
||||||
|
/// falls through to the unrecognised branch, which LOGS and defaults to
|
||||||
|
/// `claude_cli.default` rather than silently routing to a provider we no
|
||||||
|
/// longer configure.
|
||||||
pub fn provider_alias_for(model: &str) -> &'static str {
|
pub fn provider_alias_for(model: &str) -> &'static str {
|
||||||
let m = model.trim().to_ascii_lowercase();
|
let m = model.trim().to_ascii_lowercase();
|
||||||
// Prefix families first (covers claude-sonnet-5, claude-opus-4-8,
|
// Prefix families first (covers claude-sonnet-5, claude-opus-4-8,
|
||||||
@@ -43,9 +46,6 @@ pub fn provider_alias_for(model: &str) -> &'static str {
|
|||||||
if m.starts_with("claude") {
|
if m.starts_with("claude") {
|
||||||
return "claude_cli.default";
|
return "claude_cli.default";
|
||||||
}
|
}
|
||||||
if m.starts_with("gemini") {
|
|
||||||
return "gemini.default";
|
|
||||||
}
|
|
||||||
return "groq.default";
|
return "groq.default";
|
||||||
}
|
}
|
||||||
match m.as_str() {
|
match m.as_str() {
|
||||||
@@ -88,7 +88,6 @@ pub fn provider_alias_for(model: &str) -> &'static str {
|
|||||||
pub fn is_exact_provider_match(model: &str) -> bool {
|
pub fn is_exact_provider_match(model: &str) -> bool {
|
||||||
let m = model.trim().to_ascii_lowercase();
|
let m = model.trim().to_ascii_lowercase();
|
||||||
m.starts_with("claude")
|
m.starts_with("claude")
|
||||||
|| m.starts_with("gemini")
|
|
||||||
|| m.starts_with("llama")
|
|| m.starts_with("llama")
|
||||||
|| m.starts_with("groq")
|
|| m.starts_with("groq")
|
||||||
}
|
}
|
||||||
@@ -363,7 +362,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
for m in [
|
for m in [
|
||||||
"claude-sonnet-5",
|
"claude-sonnet-5",
|
||||||
"gemini-2.5-flash",
|
|
||||||
"groq-llama",
|
"groq-llama",
|
||||||
"llama3",
|
"llama3",
|
||||||
] {
|
] {
|
||||||
@@ -402,8 +400,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn provider_alias_mapping() {
|
fn provider_alias_mapping() {
|
||||||
assert_eq!(provider_alias_for("gemini"), "gemini.default");
|
// Gemini is gone: no provider row, so it must land on the logged
|
||||||
assert_eq!(provider_alias_for("gemini-2.0-flash"), "gemini.default");
|
// default rather than a family alias that resolves to nothing.
|
||||||
|
assert_eq!(provider_alias_for("gemini"), "claude_cli.default");
|
||||||
|
assert_eq!(provider_alias_for("gemini-2.0-flash"), "claude_cli.default");
|
||||||
|
assert!(!is_exact_provider_match("gemini-2.5-flash"));
|
||||||
// glm/kimi families fall back to Claude until their own provider
|
// glm/kimi families fall back to Claude until their own provider
|
||||||
// tables are configured in the runtime template.
|
// tables are configured in the runtime template.
|
||||||
assert_eq!(provider_alias_for("GLM-4.7"), "claude_cli.default");
|
assert_eq!(provider_alias_for("GLM-4.7"), "claude_cli.default");
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ pub async fn get(pool: &PgPool, agent_id: AgentId) -> Result<Agent, DbError> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persist the model a claw was deployed with (e.g. "claude", "gemini",
|
/// Persist the model a claw was deployed with (e.g. "claude", "glm",
|
||||||
/// "glm-5.2") — the runtime config is otherwise the only record of it.
|
/// "glm-5.2") — the runtime config is otherwise the only record of it.
|
||||||
pub async fn set_model_binding(
|
pub async fn set_model_binding(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
|
|||||||
@@ -85,15 +85,10 @@ mcp_config = "/zeroclaw-data/clawmates-mcp.json"
|
|||||||
# model_provider = "claude_cli.glm5"
|
# model_provider = "claude_cli.glm5"
|
||||||
# risk_profile = "toolfree"
|
# risk_profile = "toolfree"
|
||||||
#
|
#
|
||||||
# Gemini (Google) — built-in `gemini` API family (no image rebuild). Key via env
|
# Gemini is NOT wired. The platform stripped it: no provider alias, no key
|
||||||
# ZEROCLAW_providers__models__gemini__default__api_key=<GEMINI_API_KEY>; model in
|
# forwarded to agent containers, no selector entry. ZeroClaw still has a built-in
|
||||||
# config. gemini-2.5-flash = stable + high free-tier RPM (throttle-friendly);
|
# `gemini` family, so an operator could add it back here — but `provider_alias_for`
|
||||||
# gemini-3-pro-preview is the flagship.
|
# no longer resolves `gemini*` to it, and it would log as an unrecognised model.
|
||||||
# [providers.models.gemini.default]
|
|
||||||
# model = "gemini-2.5-flash"
|
|
||||||
# [agents.worker_gemini]
|
|
||||||
# model_provider = "gemini.default"
|
|
||||||
# risk_profile = "toolfree"
|
|
||||||
#
|
#
|
||||||
# Groq — built-in `groq` family (key via ZEROCLAW_providers__models__groq__default__api_key).
|
# Groq — built-in `groq` family (key via ZEROCLAW_providers__models__groq__default__api_key).
|
||||||
# Fast, but LOW free-tier TPM: each turn carries a ~9.5k-tok system prompt, so 2
|
# Fast, but LOW free-tier TPM: each turn carries a ~9.5k-tok system prompt, so 2
|
||||||
@@ -109,7 +104,7 @@ mcp_config = "/zeroclaw-data/clawmates-mcp.json"
|
|||||||
# env ZEROCLAW_AGENT_MAP="role=alias,role=alias" (+ ZEROCLAW_DEFAULT_AGENT for
|
# env ZEROCLAW_AGENT_MAP="role=alias,role=alias" (+ ZEROCLAW_DEFAULT_AGENT for
|
||||||
# unmapped roles). Point each semantic role at a different model to run ONE
|
# unmapped roles). Point each semantic role at a different model to run ONE
|
||||||
# topology across vendors, e.g.:
|
# topology across vendors, e.g.:
|
||||||
# ZEROCLAW_AGENT_MAP="coordinator=coordinator,researcher=worker_glm,analyst=worker_kimi,writer=worker_gemini,actor=worker_groq"
|
# ZEROCLAW_AGENT_MAP="coordinator=coordinator,researcher=worker_glm,analyst=worker_kimi,writer=worker_glm5,actor=worker_groq"
|
||||||
# Then POST /api/topologies/run {task, graph} with those role names. QUOTA CARE:
|
# Then POST /api/topologies/run {task, graph} with those role names. QUOTA CARE:
|
||||||
# GLM/Kimi plans have 5h/weekly caps + low concurrency (GLM Lite ~1 project at a
|
# GLM/Kimi plans have 5h/weekly caps + low concurrency (GLM Lite ~1 project at a
|
||||||
# time) — prefer pipeline (sequential) over swarm/mesh, keep tasks short, and ask
|
# time) — prefer pipeline (sequential) over swarm/mesh, keep tasks short, and ask
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
// Local Next route (NOT proxied — a specific path beats the /api/[...path]
|
|
||||||
// catch-all): generates an agent avatar with Gemini's image model ("Nano
|
|
||||||
// Banana", gemini-2.5-flash-image) using GEMINI_API_KEY from the frontend env,
|
|
||||||
// and returns a base64 data URL. Saving the chosen image is a separate
|
|
||||||
// PATCH /api/claws/{id} {avatar} (the existing backend route).
|
|
||||||
|
|
||||||
import { NextResponse, type NextRequest } from "next/server";
|
|
||||||
|
|
||||||
import { resolveBearer } from "@/lib/auth/bearer";
|
|
||||||
|
|
||||||
const MODEL = "gemini-2.5-flash-image";
|
|
||||||
const ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`;
|
|
||||||
|
|
||||||
interface InlineData { data?: string; mimeType?: string; mime_type?: string }
|
|
||||||
interface GeminiPart { inlineData?: InlineData; inline_data?: InlineData }
|
|
||||||
interface GeminiResponse { candidates?: Array<{ content?: { parts?: GeminiPart[] } }> }
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
const token = await resolveBearer();
|
|
||||||
if (!token) return NextResponse.json({ error: "unauthenticated" }, { status: 401 });
|
|
||||||
|
|
||||||
const key = process.env.GEMINI_API_KEY;
|
|
||||||
if (!key) return NextResponse.json({ error: "image generation is not configured (set GEMINI_API_KEY)" }, { status: 503 });
|
|
||||||
|
|
||||||
let prompt = "";
|
|
||||||
try {
|
|
||||||
const body = (await request.json()) as { prompt?: unknown };
|
|
||||||
prompt = String(body?.prompt ?? "").trim();
|
|
||||||
} catch {
|
|
||||||
/* fall through to the 400 below */
|
|
||||||
}
|
|
||||||
if (!prompt) return NextResponse.json({ error: "prompt required" }, { status: 400 });
|
|
||||||
|
|
||||||
let upstream: Response;
|
|
||||||
try {
|
|
||||||
upstream = await fetch(ENDPOINT, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json", "x-goog-api-key": key },
|
|
||||||
body: JSON.stringify({
|
|
||||||
contents: [{ parts: [{ text: `A clean, centered square avatar portrait for an AI agent. ${prompt}` }] }],
|
|
||||||
generationConfig: { responseModalities: ["IMAGE"] },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return NextResponse.json({ error: "could not reach the image service" }, { status: 502 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!upstream.ok) {
|
|
||||||
const detail = await upstream.text().catch(() => "");
|
|
||||||
return NextResponse.json({ error: `image service error (${upstream.status})`, detail: detail.slice(0, 400) }, { status: 502 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await upstream.json().catch(() => null)) as GeminiResponse | null;
|
|
||||||
const parts = data?.candidates?.[0]?.content?.parts ?? [];
|
|
||||||
const part = parts.find((p) => p.inlineData?.data || p.inline_data?.data);
|
|
||||||
const inline = part?.inlineData ?? part?.inline_data;
|
|
||||||
if (!inline?.data) {
|
|
||||||
return NextResponse.json({ error: "the model did not return an image — try a different prompt" }, { status: 502 });
|
|
||||||
}
|
|
||||||
const mime = inline.mimeType ?? inline.mime_type ?? "image/png";
|
|
||||||
return NextResponse.json({ image: `data:${mime};base64,${inline.data}` });
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,6 @@ const MODEL_OPTIONS: { value: string; label: string; family: string }[] = [
|
|||||||
{ value: "glm-4.6", label: "GLM 4.6", family: "Z.AI" },
|
{ value: "glm-4.6", label: "GLM 4.6", family: "Z.AI" },
|
||||||
{ value: "glm-5.2", label: "GLM 5.2", family: "Z.AI" },
|
{ value: "glm-5.2", label: "GLM 5.2", family: "Z.AI" },
|
||||||
{ value: "kimi-k2", label: "Kimi K2", family: "Moonshot" },
|
{ value: "kimi-k2", label: "Kimi K2", family: "Moonshot" },
|
||||||
{ value: "gemini-2.0-flash", label: "Gemini 2.0 Flash", family: "Google" },
|
|
||||||
{ value: "llama-3.3-70b-versatile", label: "Llama 3.3 70B", family: "Groq" },
|
{ value: "llama-3.3-70b-versatile", label: "Llama 3.3 70B", family: "Groq" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
// Avatar editor modal for a claw: upload an image OR generate one from a prompt
|
// Avatar editor modal for a claw: upload an image, preview it, then Save
|
||||||
// (Gemini / Nano Banana, via /api/generate-avatar — max 5 attempts), preview it,
|
// (downscaled to 256² and persisted via PATCH /api/claws/{id}).
|
||||||
// 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 { useEffect, useRef, useState } from "react";
|
||||||
import { Camera, Sparkles, Upload, X } from "lucide-react";
|
import { Camera, Upload, X } from "lucide-react";
|
||||||
|
|
||||||
const mono = "'JetBrains Mono', ui-monospace, monospace";
|
const mono = "'JetBrains Mono', ui-monospace, monospace";
|
||||||
const MAX_ATTEMPTS = 5;
|
|
||||||
|
|
||||||
// Cover-fit to a square and re-encode small so avatars stay lightweight.
|
// Cover-fit to a square and re-encode small so avatars stay lightweight.
|
||||||
function downscale(dataUrl: string, size = 256): Promise<string> {
|
function downscale(dataUrl: string, size = 256): Promise<string> {
|
||||||
@@ -33,9 +37,7 @@ function downscale(dataUrl: string, size = 256): Promise<string> {
|
|||||||
|
|
||||||
export function AvatarModal({ clawId, clawName, current, onClose, onSaved }: { clawId: string; clawName: string; current?: string | null; onClose: () => void; onSaved: (dataUrl: string) => void }) {
|
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 [preview, setPreview] = useState<string | null>(current ?? null);
|
||||||
const [prompt, setPrompt] = useState("");
|
const [busy, setBusy] = useState<null | "save">(null);
|
||||||
const [attempts, setAttempts] = useState(0);
|
|
||||||
const [busy, setBusy] = useState<null | "gen" | "save">(null);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const fileRef = useRef<HTMLInputElement>(null);
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -53,20 +55,6 @@ export function AvatarModal({ clawId, clawName, current, onClose, onSaved }: { c
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generate() {
|
|
||||||
if (busy || attempts >= MAX_ATTEMPTS || !prompt.trim()) return;
|
|
||||||
setBusy("gen");
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const res = await fetch("/api/generate-avatar", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt }) });
|
|
||||||
const j = (await res.json().catch(() => ({}))) as { image?: string; error?: string };
|
|
||||||
if (!res.ok || !j.image) setError(j.error || "generation failed");
|
|
||||||
else { setPreview(j.image); setAttempts((a) => a + 1); }
|
|
||||||
} catch {
|
|
||||||
setError("generation failed");
|
|
||||||
}
|
|
||||||
setBusy(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
if (!preview || busy) return;
|
if (!preview || busy) return;
|
||||||
@@ -84,7 +72,6 @@ export function AvatarModal({ clawId, clawName, current, onClose, onSaved }: { c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxed = attempts >= MAX_ATTEMPTS;
|
|
||||||
|
|
||||||
return (
|
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={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" }}>
|
||||||
@@ -92,7 +79,7 @@ export function AvatarModal({ clawId, clawName, current, onClose, onSaved }: { c
|
|||||||
<div style={{ display: "flex", alignItems: "flex-start", gap: 12, marginBottom: 16 }}>
|
<div style={{ display: "flex", alignItems: "flex-start", gap: 12, marginBottom: 16 }}>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={{ fontSize: 18, fontWeight: 700, color: "#f3f3f5" }}>{clawName}'s image</div>
|
<div style={{ fontSize: 18, fontWeight: 700, color: "#f3f3f5" }}>{clawName}'s image</div>
|
||||||
<div style={{ fontSize: 12.5, color: "#8a8a92", marginTop: 2 }}>Upload one, or generate from a prompt.</div>
|
<div style={{ fontSize: 12.5, color: "#8a8a92", marginTop: 2 }}>Upload a PNG or JPG.</div>
|
||||||
</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>
|
<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>
|
||||||
@@ -108,24 +95,6 @@ export function AvatarModal({ clawId, clawName, current, onClose, onSaved }: { c
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ borderTop: "1px solid rgba(255,255,255,.07)", paddingTop: 14 }}>
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 8 }}>
|
|
||||||
<Sparkles size={13} color="#c98af0" />
|
|
||||||
<span style={{ fontFamily: mono, fontSize: 10, letterSpacing: ".08em", color: "#c98af0" }}>GENERATE FROM PROMPT</span>
|
|
||||||
<span style={{ flex: 1 }} />
|
|
||||||
<span style={{ fontFamily: mono, fontSize: 9, color: maxed ? "#e8b465" : "#5a5a62" }}>{attempts}/{MAX_ATTEMPTS} attempts</span>
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
value={prompt}
|
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
|
||||||
placeholder="e.g. a calm robotic owl mascot, soft teal gradient, minimal"
|
|
||||||
rows={2}
|
|
||||||
style={{ width: "100%", resize: "none", borderRadius: 9, border: "1px solid rgba(255,255,255,.12)", background: "#101014", color: "#eaeaee", fontSize: 12.5, padding: "8px 10px", outline: "none", fontFamily: "inherit" }}
|
|
||||||
/>
|
|
||||||
<button type="button" onClick={generate} disabled={busy !== null || maxed || !prompt.trim()} style={{ marginTop: 8, width: "100%", display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 7, padding: "9px 0", borderRadius: 9, border: "1px solid rgba(201,138,240,.4)", background: maxed || !prompt.trim() ? "rgba(201,138,240,.06)" : "rgba(201,138,240,.14)", color: "#d9b6f7", fontSize: 12.5, fontWeight: 600, cursor: busy || maxed || !prompt.trim() ? "default" : "pointer", opacity: busy === "gen" ? 0.7 : 1 }}>
|
|
||||||
<Sparkles size={14} /> {busy === "gen" ? "Generating…" : maxed ? "Max attempts reached" : attempts > 0 ? "Regenerate" : "Generate"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error ? <div role="alert" style={{ marginTop: 12, fontFamily: mono, fontSize: 10.5, color: "#ff8a7a" }}>{error}</div> : null}
|
{error ? <div role="alert" style={{ marginTop: 12, fontFamily: mono, fontSize: 10.5, color: "#ff8a7a" }}>{error}</div> : null}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ interface Member {
|
|||||||
system_prompt: string;
|
system_prompt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODELS = ["claude", "glm", "gemini", "kimi", "groq"] as const;
|
const MODELS = ["claude", "glm", "kimi", "groq"] as const;
|
||||||
const ACCENTS = ["#f96565", "#65a8f9", "#65f9a8", "#f9d965", "#c465f9"];
|
const ACCENTS = ["#f96565", "#65a8f9", "#65f9a8", "#f9d965", "#c465f9"];
|
||||||
const NAME_POOL = [
|
const NAME_POOL = [
|
||||||
"Scout", "Drafter", "Ledger", "Atlas", "Quill", "Beacon", "Harbor", "Vesper",
|
"Scout", "Drafter", "Ledger", "Atlas", "Quill", "Beacon", "Harbor", "Vesper",
|
||||||
|
|||||||
@@ -65,9 +65,15 @@ what). Distinguish between:
|
|||||||
JSON/YAML schemas)
|
JSON/YAML schemas)
|
||||||
- Lifecycle dependencies (things spawned/killed together)
|
- Lifecycle dependencies (things spawned/killed together)
|
||||||
|
|
||||||
|
Mermaid diagrams beat prose for module dependency graphs: draw the
|
||||||
|
diagram FIRST, then explain in bullets. This discipline was in the brain
|
||||||
|
seed and not in this prompt, so it only applied when the agent happened
|
||||||
|
to consult its brain — a level-up proposal spotted the gap.
|
||||||
|
|
||||||
Output goes to `Codebases/<repo>/Architecture.md` as a Mermaid diagram
|
Output goes to `Codebases/<repo>/Architecture.md` as a Mermaid diagram
|
||||||
plus a table listing each module + its role + up-to-3 line notes on the
|
plus a table listing each module + its role + up-to-3 line notes on the
|
||||||
patterns it uses. Never guess; only write down what you verified in
|
patterns it uses. Name any anti-pattern from the seed list explicitly
|
||||||
|
when you find it. Never guess; only write down what you verified in
|
||||||
source.
|
source.
|
||||||
"""
|
"""
|
||||||
brain_seed = """
|
brain_seed = """
|
||||||
|
|||||||
Reference in New Issue
Block a user