Patchpass V1
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { agents as agentsApi } from "../api/client";
|
||||
import type { Agent } from "../api/types";
|
||||
import { Button, Card, EmptyState, PageHeader, Spinner } from "../components/ui";
|
||||
import { AgentAvatar } from "../components/AgentAvatar";
|
||||
import { AgentFormModal, ConnectModal } from "../components/AgentModals";
|
||||
|
||||
export function AgentsPage() {
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<Agent | null>(null);
|
||||
const [connectAgent, setConnectAgent] = useState<Agent | null>(null);
|
||||
const [revealedKey, setRevealedKey] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
setAgents(await agentsApi.list());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const onSaved = (agent: Agent, created: boolean) => {
|
||||
load();
|
||||
if (created && agent.api_key) {
|
||||
setRevealedKey(agent.api_key);
|
||||
setConnectAgent(agent);
|
||||
}
|
||||
};
|
||||
|
||||
const regenerate = async (agent: Agent) => {
|
||||
if (!confirm(`Regenerate the API key for "${agent.name}"? The old key stops working immediately.`)) return;
|
||||
try {
|
||||
const updated = await agentsApi.regenerateKey(agent.id);
|
||||
toast.success("API key regenerated");
|
||||
setRevealedKey(updated.api_key ?? null);
|
||||
setConnectAgent(updated);
|
||||
load();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "Failed");
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDisabled = async (agent: Agent) => {
|
||||
try {
|
||||
await agentsApi.setDisabled(agent.id, !agent.disabled);
|
||||
toast.success(agent.disabled ? "Agent enabled" : "Agent disabled");
|
||||
load();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "Failed");
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (agent: Agent) => {
|
||||
if (!confirm(`Delete "${agent.name}"? All its requests will be permanently deleted.`)) return;
|
||||
try {
|
||||
await agentsApi.remove(agent.id);
|
||||
toast.success("Agent deleted");
|
||||
load();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "Failed");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title="Agents"
|
||||
subtitle="Each agent has its own API key. You may own up to 5 agents."
|
||||
actions={
|
||||
<Button
|
||||
onClick={() => {
|
||||
setEditing(null);
|
||||
setFormOpen(true);
|
||||
}}
|
||||
disabled={agents.length >= 5}
|
||||
>
|
||||
+ New agent
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-16 text-primary">
|
||||
<Spinner className="h-6 w-6" />
|
||||
</div>
|
||||
) : agents.length === 0 ? (
|
||||
<EmptyState
|
||||
icon="🤖"
|
||||
title="No agents yet"
|
||||
subtitle="Create an agent, then connect it via OpenClaw, MCP, or the REST API."
|
||||
/>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{agents.map((a) => (
|
||||
<Card key={a.id} className="p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AgentAvatar name={a.name} iconUrl={a.icon_url} size={44} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="truncate font-semibold text-text">{a.name}</h3>
|
||||
{a.disabled && (
|
||||
<span className="rounded-full bg-rejected/15 px-2 py-0.5 text-[10px] font-semibold text-rejected">
|
||||
DISABLED
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{a.description && <p className="mt-0.5 line-clamp-2 text-xs text-muted">{a.description}</p>}
|
||||
<p className="mt-1 text-xs text-faint">
|
||||
{a.pending_count ?? 0}/{a.max_pending_requests} pending ·{" "}
|
||||
<code>{a.api_key_masked}</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<Button variant="secondary" onClick={() => setConnectAgent(a)}>
|
||||
Connect
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setEditing(a);
|
||||
setFormOpen(true);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={() => regenerate(a)}>
|
||||
Regenerate key
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={() => toggleDisabled(a)}>
|
||||
{a.disabled ? "Enable" : "Disable"}
|
||||
</Button>
|
||||
<Button variant="ghost" className="text-rejected" onClick={() => remove(a)}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AgentFormModal
|
||||
open={formOpen}
|
||||
onClose={() => setFormOpen(false)}
|
||||
agent={editing}
|
||||
onSaved={onSaved}
|
||||
/>
|
||||
{connectAgent && (
|
||||
<ConnectModal
|
||||
open={!!connectAgent}
|
||||
onClose={() => {
|
||||
setConnectAgent(null);
|
||||
setRevealedKey(null);
|
||||
}}
|
||||
agent={connectAgent}
|
||||
revealedKey={revealedKey}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user