Agents page: collapse anatomy into icon cards → pretty editable modal
The agent command center's BRAIN+SURFACE columns are now a grid of compact icon
cards (system prompt / how it operates / personality / skills / capabilities /
tools / memory / safety). Clicking a card opens a modal with the full content,
nicely formatted (markdown/persona/tags), editable for the four brain text files
(system_prompt, agent_md, persona, skills_md) and saved back to the .brain via
PATCH /api/claws/{id}/brain. Capabilities/tools/memory/safety render read-only
(tools keeps its Add affordance). LIVE column unchanged.
- frontend: new AnatomyGrid (icon cards + SectionModal); ClawCommandCenter swaps the
two verbose columns for it; skills_md added to RawBrain types.
- backend: cm-brain skills_md() getter + skills_md in ClawBrainResponse so the skills
editor pre-fills (avoids blank-overwrite).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
66c4a80985
commit
d9ee9f5328
@@ -0,0 +1,199 @@
|
||||
"use client";
|
||||
|
||||
// The agent's "anatomy" as a grid of compact icon cards. Each card opens a modal
|
||||
// with the full, pretty-formatted section content. The four brain text files
|
||||
// (system prompt / how it operates / personality / skills) are editable in the
|
||||
// modal and saved back to the .brain via PATCH /api/claws/{id}/brain; the derived
|
||||
// sections (capabilities / tools / memory / safety) render read-only (tools keeps
|
||||
// its "add tool" affordance).
|
||||
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { Check, Cpu, Database, Drama, Pencil, Plus, ScrollText, ShieldCheck, Workflow, Wrench, X, Zap } from "lucide-react";
|
||||
|
||||
import type { DemoAgent } from "@/lib/dashboard-demo";
|
||||
import { MarkdownText, PersonalityBody, mono, tag, type RawBrain } from "./anatomy-cards";
|
||||
|
||||
type EditField = "system_prompt" | "agent_md" | "persona" | "skills_md";
|
||||
|
||||
interface Section {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: ReactNode;
|
||||
tint: string;
|
||||
status?: string; // small hint on the card (count / "editable")
|
||||
field?: EditField; // present ⇒ editable + saved to the brain
|
||||
value?: string; // editable source text
|
||||
placeholder?: string;
|
||||
body: ReactNode; // formatted, read-only display for the modal
|
||||
}
|
||||
|
||||
function IconCard({ section, onClick }: { section: Section; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
title={`Open ${section.label}`}
|
||||
style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 11, padding: 15, borderRadius: 13, background: "#0f0f13", border: `1px solid ${section.tint}38`, boxShadow: "0 8px 22px rgba(0,0,0,.4)", cursor: "pointer", textAlign: "left", minHeight: 104, transition: "border-color .15s ease, transform .1s ease" }}
|
||||
>
|
||||
<span style={{ width: 36, height: 36, flex: "none", borderRadius: 10, background: `${section.tint}26`, border: `1px solid ${section.tint}45`, display: "inline-flex", alignItems: "center", justifyContent: "center", color: section.tint }}>{section.icon}</span>
|
||||
<span style={{ fontFamily: mono, fontSize: 12, fontWeight: 600, letterSpacing: ".04em", color: section.tint, lineHeight: 1.2 }}>{section.label}</span>
|
||||
<span style={{ marginTop: "auto", display: "flex", alignItems: "center", gap: 6, fontFamily: mono, fontSize: 9.5, color: "#6a6a72" }}>
|
||||
{section.status ? <span>{section.status}</span> : null}
|
||||
{section.field ? <span style={{ display: "inline-flex", alignItems: "center", gap: 3, color: "#7a7a82" }}><Pencil size={9} />edit</span> : null}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionModal({ section, clawId, onClose, onSaved, onAddTool }: { section: Section; clawId: string; onClose: () => void; onSaved?: () => void; onAddTool?: () => void }) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draft, setDraft] = useState(section.value ?? "");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const save = async () => {
|
||||
if (!section.field) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await fetch(`/api/claws/${clawId}/brain`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ [section.field]: draft }) });
|
||||
setEditing(false);
|
||||
onSaved?.();
|
||||
} catch {
|
||||
/* keep the editor open on failure */
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={section.label}
|
||||
onClick={onClose}
|
||||
style={{ position: "fixed", inset: 0, zIndex: 200, background: "rgba(0,0,0,.62)", backdropFilter: "blur(2px)", display: "flex", alignItems: "center", justifyContent: "center", padding: 24 }}
|
||||
>
|
||||
<div onClick={(e) => e.stopPropagation()} style={{ width: "min(760px, 94vw)", maxHeight: "84vh", display: "flex", flexDirection: "column", borderRadius: 16, background: "#0f0f13", border: `1px solid ${section.tint}45`, boxShadow: "0 30px 80px rgba(0,0,0,.6)" }}>
|
||||
{/* Header */}
|
||||
<div style={{ flex: "none", display: "flex", alignItems: "center", gap: 11, padding: "16px 18px", borderBottom: "1px solid rgba(255,255,255,.06)" }}>
|
||||
<span style={{ width: 30, height: 30, flex: "none", borderRadius: 8, background: `${section.tint}26`, border: `1px solid ${section.tint}45`, display: "inline-flex", alignItems: "center", justifyContent: "center", color: section.tint }}>{section.icon}</span>
|
||||
<span style={{ fontFamily: mono, fontSize: 13, fontWeight: 600, letterSpacing: ".05em", color: section.tint }}>{section.label}</span>
|
||||
<span style={{ flex: 1 }} />
|
||||
{section.field && !editing ? (
|
||||
<button type="button" onClick={() => { setDraft(section.value ?? ""); setEditing(true); }} style={{ display: "inline-flex", alignItems: "center", gap: 5, padding: "5px 11px", borderRadius: 7, border: "1px solid rgba(255,255,255,.14)", background: "transparent", color: "#cfcfd5", fontSize: 12, cursor: "pointer" }}><Pencil size={12} />Edit</button>
|
||||
) : null}
|
||||
<button type="button" onClick={onClose} aria-label="Close" style={{ width: 30, height: 30, flex: "none", borderRadius: 8, border: "1px solid rgba(255,255,255,.1)", background: "transparent", color: "#8a8a92", cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center" }}><X size={15} /></button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div style={{ minHeight: 0, flex: 1, overflowY: "auto", padding: "18px 20px" }}>
|
||||
{editing ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<textarea value={draft} onChange={(e) => setDraft(e.target.value)} placeholder={section.placeholder} autoFocus
|
||||
style={{ width: "100%", minHeight: 320, resize: "vertical", boxSizing: "border-box", borderRadius: 9, border: "1px solid rgba(255,255,255,.14)", background: "#0b0b0e", color: "#e6e6ea", padding: 12, fontFamily: mono, fontSize: 12.5, lineHeight: 1.6 }} />
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<button type="button" onClick={save} disabled={saving} style={{ display: "inline-flex", alignItems: "center", gap: 5, padding: "7px 14px", borderRadius: 7, border: 0, background: "#5fd08a", color: "#06140c", fontSize: 12.5, fontWeight: 700, cursor: "pointer" }}><Check size={13} />{saving ? "Saving…" : "Save to brain"}</button>
|
||||
<button type="button" onClick={() => setEditing(false)} style={{ display: "inline-flex", alignItems: "center", gap: 5, padding: "7px 14px", borderRadius: 7, border: "1px solid rgba(255,255,255,.12)", background: "transparent", color: "#cfcfd5", fontSize: 12.5, cursor: "pointer" }}><X size={13} />Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
section.body
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer affordance (tools) */}
|
||||
{section.key === "tools" && onAddTool && !editing ? (
|
||||
<div style={{ flex: "none", padding: "12px 20px", borderTop: "1px solid rgba(255,255,255,.06)" }}>
|
||||
<button type="button" onClick={() => { onClose(); onAddTool(); }} style={{ width: "100%", padding: "9px 0", borderRadius: 8, border: "1px dashed rgba(94,200,216,.4)", background: "transparent", color: "#5ec8d8", fontSize: 12.5, fontFamily: mono, cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 6 }}><Plus size={13} />Add tool</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const muted = (t: string): ReactNode => <span style={{ fontFamily: mono, fontSize: 12.5, color: "#6a6a72" }}>{t}</span>;
|
||||
const row = (a: string, b: string, bc = "#cfcfd5"): ReactNode => (
|
||||
<div style={{ display: "flex", fontFamily: mono, fontSize: 12.5 }}><span style={{ flex: 1, color: "#9a9aa2" }}>{a}</span><span style={{ color: bc }}>{b}</span></div>
|
||||
);
|
||||
|
||||
export function AnatomyGrid({ agent, brain, onSaved, onAddTool }: { agent: DemoAgent; brain?: RawBrain; onSaved?: () => void; onAddTool?: () => void }) {
|
||||
const [open, setOpen] = useState<string | null>(null);
|
||||
const c = agent.compartments;
|
||||
|
||||
const systemPrompt = brain?.system_prompt ?? agent.systemPrompt ?? "";
|
||||
const skillsList = brain?.skills ?? [];
|
||||
|
||||
const sections: Section[] = [
|
||||
{
|
||||
key: "system_prompt", label: "SYSTEM PROMPT", icon: <ScrollText size={17} />, tint: "#ff6f61",
|
||||
field: "system_prompt", value: systemPrompt, placeholder: "System prompt / job description…",
|
||||
body: systemPrompt.trim() ? <MarkdownText text={systemPrompt} baseSize={14} /> : muted("No system prompt set — click Edit to write one."),
|
||||
},
|
||||
{
|
||||
key: "agent_md", label: "HOW IT OPERATES", icon: <Workflow size={17} />, tint: "#ff8a7a",
|
||||
field: "agent_md", value: brain?.agent_md ?? "", placeholder: "AGENTS.md — how this agent operates (workflow, rules)…",
|
||||
body: brain?.agent_md?.trim() ? <MarkdownText text={brain.agent_md} baseSize={14} /> : muted("No AGENTS.md yet — click Edit to define how this agent operates."),
|
||||
},
|
||||
{
|
||||
key: "personality", label: "PERSONALITY", icon: <Drama size={17} />, tint: "#c98af0",
|
||||
field: "persona", value: brain?.personality ?? "", placeholder: "Personality — tone, traits, voice…",
|
||||
body: <PersonalityBody raw={brain?.personality} fallback={c.personality} />,
|
||||
},
|
||||
{
|
||||
key: "skills", label: "SKILLS", icon: <Zap size={17} />, tint: "#ff6f61",
|
||||
status: String(skillsList.length || c.skills.length), field: "skills_md", value: brain?.skills_md ?? "",
|
||||
placeholder: "Skills — a narrative of what this agent is good at…",
|
||||
body: brain?.skills_md?.trim() ? (
|
||||
<MarkdownText text={brain.skills_md} baseSize={14} />
|
||||
) : skillsList.length ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
{skillsList.map((s) => (
|
||||
<div key={s.name}>
|
||||
<div style={{ fontFamily: mono, fontSize: 12.5, fontWeight: 700, color: "#ff8a7a", marginBottom: 4 }}>{s.name}</div>
|
||||
<div style={{ fontSize: 13.5, color: "#d6d6dc", lineHeight: 1.55 }}>{s.body}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : c.skills.length ? (
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{c.skills.map((s) => <span key={s} style={tag("#ff6f61")}>{s}</span>)}</div>
|
||||
) : muted("No skills yet — click Edit to describe this agent's skills."),
|
||||
},
|
||||
{
|
||||
key: "capabilities", label: "CAPABILITIES", icon: <Cpu size={17} />, tint: "#e8b465",
|
||||
status: String(c.capabilities.length),
|
||||
body: c.capabilities.length ? <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>{c.capabilities.map((p) => <span key={p} style={tag("#e8b465")}>{p}</span>)}</div> : muted("No capabilities listed."),
|
||||
},
|
||||
{
|
||||
key: "tools", label: "TOOLS", icon: <Wrench size={17} />, tint: "#5ec8d8",
|
||||
status: String(c.tools.length),
|
||||
body: c.tools.length ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 7 }}>
|
||||
{c.tools.map((t) => row(t.name, t.state === "gated" ? "gated ✓" : "blocked ⨯", t.state === "gated" ? "#5fd08a" : "#e8b465"))}
|
||||
</div>
|
||||
) : muted("No tools yet — use Add tool below."),
|
||||
},
|
||||
{
|
||||
key: "memory", label: "MEMORY", icon: <Database size={17} />, tint: "#5fd08a",
|
||||
status: brain ? `${brain.stats.memories}` : undefined,
|
||||
body: brain && brain.memory.length ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 7, fontFamily: mono, fontSize: 12.5, color: "#9a9aa2" }}>
|
||||
{brain.memory.map((m, i) => <div key={i} style={{ lineHeight: 1.5 }}>{m}</div>)}
|
||||
</div>
|
||||
) : muted("No memories yet — they accrue as this agent runs and chats."),
|
||||
},
|
||||
{
|
||||
key: "safety", label: "SAFETY · §15", icon: <ShieldCheck size={17} />, tint: "#6fd0c0",
|
||||
body: <div style={{ display: "flex", flexDirection: "column", gap: 7 }}>{row("Sandbox", c.safety.sandbox, "#6fd0c0")}{row("Network", c.safety.network, "#6fd0c0")}</div>,
|
||||
},
|
||||
];
|
||||
|
||||
const active = sections.find((s) => s.key === open) ?? null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(150px, 1fr))", gap: 12, alignContent: "start" }}>
|
||||
{sections.map((s) => <IconCard key={s.key} section={s} onClick={() => setOpen(s.key)} />)}
|
||||
</div>
|
||||
{active ? <SectionModal section={active} clawId={agent.id} onClose={() => setOpen(null)} onSaved={onSaved} onAddTool={onAddTool} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user