2026-04-14 13:59:23 +00:00
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
|
|
|
import { createAgent, getAgent, updateAgent } from "../../lib/api";
|
|
|
|
|
import { getErrorMessage } from "../../lib/errors";
|
|
|
|
|
import type { AgentRole, AgentTool } from "../../lib/types";
|
|
|
|
|
|
|
|
|
|
export default function AgentForm() {
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const { agentId } = useParams<{ agentId: string }>();
|
|
|
|
|
const isEditing = Boolean(agentId);
|
|
|
|
|
|
|
|
|
|
const [name, setName] = useState("");
|
|
|
|
|
const [role, setRole] = useState<AgentRole>("analyst");
|
|
|
|
|
const [tool, setTool] = useState<AgentTool>("codex");
|
|
|
|
|
const [customPrompt, setCustomPrompt] = useState("");
|
2026-04-14 14:07:50 +00:00
|
|
|
const [isDefaultAgent, setIsDefaultAgent] = useState(false);
|
2026-04-14 13:59:23 +00:00
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [initializing, setInitializing] = useState(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
async function loadAgent() {
|
|
|
|
|
if (!agentId) return;
|
|
|
|
|
setInitializing(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
try {
|
|
|
|
|
const agent = await getAgent(agentId);
|
|
|
|
|
setName(agent.name);
|
|
|
|
|
setRole(agent.role);
|
|
|
|
|
setTool(agent.tool);
|
|
|
|
|
setCustomPrompt(agent.custom_prompt);
|
2026-04-14 14:07:50 +00:00
|
|
|
setIsDefaultAgent(agent.is_default);
|
2026-04-14 13:59:23 +00:00
|
|
|
} catch (err: unknown) {
|
|
|
|
|
setError(getErrorMessage(err));
|
|
|
|
|
} finally {
|
|
|
|
|
setInitializing(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void loadAgent();
|
|
|
|
|
}, [agentId]);
|
|
|
|
|
|
|
|
|
|
async function handleSubmit(event: React.FormEvent) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (isEditing && agentId) {
|
|
|
|
|
await updateAgent(agentId, name, role, tool, customPrompt);
|
|
|
|
|
} else {
|
|
|
|
|
await createAgent(name, role, tool, customPrompt);
|
|
|
|
|
}
|
|
|
|
|
navigate("/agents");
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
setError(getErrorMessage(err));
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="mx-auto max-w-2xl p-8">
|
|
|
|
|
<h2 className="mb-6 text-xl font-bold">{isEditing ? "Edit agent" : "New agent"}</h2>
|
|
|
|
|
|
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
|
|
|
{initializing && <div className="text-sm text-gray-500">Loading agent...</div>}
|
2026-04-14 14:07:50 +00:00
|
|
|
{isEditing && isDefaultAgent && (
|
|
|
|
|
<div className="rounded border border-blue-200 bg-blue-50 p-3 text-sm text-blue-700">
|
|
|
|
|
This is a default agent. Only its script/prompt can be modified.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-04-14 13:59:23 +00:00
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="mb-1 block text-sm font-medium text-gray-700">Name</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={name}
|
|
|
|
|
onChange={(e) => setName(e.target.value)}
|
2026-04-14 14:07:50 +00:00
|
|
|
disabled={isEditing && isDefaultAgent}
|
2026-04-14 13:59:23 +00:00
|
|
|
required
|
|
|
|
|
className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="mb-1 block text-sm font-medium text-gray-700">Role</label>
|
|
|
|
|
<select
|
|
|
|
|
value={role}
|
|
|
|
|
onChange={(e) => setRole(e.target.value as AgentRole)}
|
2026-04-14 14:07:50 +00:00
|
|
|
disabled={isEditing && isDefaultAgent}
|
2026-04-14 13:59:23 +00:00
|
|
|
className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
|
|
>
|
|
|
|
|
<option value="analyst">Analyst</option>
|
|
|
|
|
<option value="developer">Developer</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="mb-1 block text-sm font-medium text-gray-700">Tool</label>
|
|
|
|
|
<select
|
|
|
|
|
value={tool}
|
|
|
|
|
onChange={(e) => setTool(e.target.value as AgentTool)}
|
2026-04-14 14:07:50 +00:00
|
|
|
disabled={isEditing && isDefaultAgent}
|
2026-04-14 13:59:23 +00:00
|
|
|
className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
|
|
>
|
|
|
|
|
<option value="codex">Codex</option>
|
|
|
|
|
<option value="claude_code">Claude Code</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="mb-1 block text-sm font-medium text-gray-700">
|
2026-04-14 14:07:50 +00:00
|
|
|
Script / custom prompt (appended to built-in prompt)
|
2026-04-14 13:59:23 +00:00
|
|
|
</label>
|
|
|
|
|
<textarea
|
|
|
|
|
rows={12}
|
|
|
|
|
value={customPrompt}
|
|
|
|
|
onChange={(e) => setCustomPrompt(e.target.value)}
|
|
|
|
|
className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
|
|
placeholder="Extra instructions for this agent..."
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
<div className="rounded border border-red-200 bg-red-50 p-2 text-sm text-red-600">
|
|
|
|
|
{error}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<button
|
|
|
|
|
type="submit"
|
|
|
|
|
disabled={loading || initializing}
|
|
|
|
|
className="rounded bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
|
|
|
|
|
>
|
|
|
|
|
{loading ? "Saving..." : isEditing ? "Save" : "Create"}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => navigate(-1)}
|
|
|
|
|
className="rounded bg-gray-200 px-4 py-2 text-sm hover:bg-gray-300"
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|