Skip to content

Commit 7d94480

Browse files
committed
Add agent creation functionality and UI components
- Implemented a new API endpoint for creating agents, allowing users to initialize agents with specified IDs. - Added the `CreateAgentDialog` component for a user-friendly interface to input agent IDs and handle creation logic. - Integrated the agent creation dialog into the `Sidebar` and `Overview` components, enhancing accessibility for users to create new agents. - Updated the API client to include the `createAgent` method for seamless interaction with the new endpoint. - Enhanced error handling and user feedback during the agent creation process, improving overall user experience.
1 parent 6b1aeb4 commit 7d94480

File tree

7 files changed

+560
-20
lines changed

7 files changed

+560
-20
lines changed

interface/src/api/client.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,18 @@ export const api = {
982982
}
983983
return response.json() as Promise<IdentityFiles>;
984984
},
985+
createAgent: async (agentId: string) => {
986+
const response = await fetch(`${API_BASE}/agents`, {
987+
method: "POST",
988+
headers: { "Content-Type": "application/json" },
989+
body: JSON.stringify({ agent_id: agentId }),
990+
});
991+
if (!response.ok) {
992+
throw new Error(`API error: ${response.status}`);
993+
}
994+
return response.json() as Promise<{ success: boolean; agent_id: string; message: string }>;
995+
},
996+
985997
agentConfig: (agentId: string) =>
986998
fetchJson<AgentConfigResponse>(`/agents/config?agent_id=${encodeURIComponent(agentId)}`),
987999
updateAgentConfig: async (request: AgentConfigUpdateRequest) => {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import {useState} from "react";
2+
import {useMutation, useQueryClient} from "@tanstack/react-query";
3+
import {useNavigate} from "@tanstack/react-router";
4+
import {api} from "@/api/client";
5+
import {Button, Input, Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter} from "@/ui";
6+
7+
interface CreateAgentDialogProps {
8+
open: boolean;
9+
onOpenChange: (open: boolean) => void;
10+
}
11+
12+
export function CreateAgentDialog({open, onOpenChange}: CreateAgentDialogProps) {
13+
const [agentId, setAgentId] = useState("");
14+
const [error, setError] = useState<string | null>(null);
15+
const queryClient = useQueryClient();
16+
const navigate = useNavigate();
17+
18+
const createMutation = useMutation({
19+
mutationFn: (id: string) => api.createAgent(id),
20+
onSuccess: (result) => {
21+
if (result.success) {
22+
queryClient.invalidateQueries({queryKey: ["agents"]});
23+
queryClient.invalidateQueries({queryKey: ["overview"]});
24+
onOpenChange(false);
25+
setAgentId("");
26+
setError(null);
27+
navigate({to: "/agents/$agentId", params: {agentId: result.agent_id}});
28+
} else {
29+
setError(result.message);
30+
}
31+
},
32+
onError: (err) => setError(`Failed: ${err.message}`),
33+
});
34+
35+
function handleSubmit() {
36+
const trimmed = agentId.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "");
37+
if (!trimmed) {
38+
setError("Agent ID is required");
39+
return;
40+
}
41+
setError(null);
42+
createMutation.mutate(trimmed);
43+
}
44+
45+
return (
46+
<Dialog open={open} onOpenChange={(v) => { if (!v) { setError(null); setAgentId(""); } onOpenChange(v); }}>
47+
<DialogContent className="max-w-sm">
48+
<DialogHeader>
49+
<DialogTitle>Create Agent</DialogTitle>
50+
</DialogHeader>
51+
<div className="flex flex-col gap-3">
52+
<div>
53+
<label className="mb-1.5 block text-sm font-medium text-ink-dull">Agent ID</label>
54+
<Input
55+
size="lg"
56+
value={agentId}
57+
onChange={(e) => setAgentId(e.target.value)}
58+
placeholder="e.g. research, support, dev"
59+
onKeyDown={(e) => { if (e.key === "Enter") handleSubmit(); }}
60+
autoFocus
61+
/>
62+
<p className="mt-1.5 text-tiny text-ink-faint">
63+
Lowercase letters, numbers, hyphens, and underscores only.
64+
</p>
65+
</div>
66+
{error && (
67+
<div className="rounded-md border border-red-500/20 bg-red-500/10 px-3 py-2 text-sm text-red-400">
68+
{error}
69+
</div>
70+
)}
71+
</div>
72+
<DialogFooter>
73+
<Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}>
74+
Cancel
75+
</Button>
76+
<Button size="sm" onClick={handleSubmit} loading={createMutation.isPending}>
77+
Create
78+
</Button>
79+
</DialogFooter>
80+
</DialogContent>
81+
</Dialog>
82+
);
83+
}

interface/src/components/Sidebar.tsx

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo } from "react";
1+
import { useMemo, useState } from "react";
22
import { Link, useMatchRoute } from "@tanstack/react-router";
33
import { useQuery } from "@tanstack/react-query";
44
import { motion } from "framer-motion";
@@ -7,6 +7,7 @@ import type { ChannelLiveState } from "@/hooks/useChannelLiveState";
77
import { Button } from "@/ui";
88
import { ArrowLeft01Icon, DashboardSquare01Icon, LeftToRightListBulletIcon, Settings01Icon } from "@hugeicons/core-free-icons";
99
import { HugeiconsIcon } from "@hugeicons/react";
10+
import { CreateAgentDialog } from "@/components/CreateAgentDialog";
1011

1112
interface SidebarProps {
1213
liveStates: Record<string, ChannelLiveState>;
@@ -15,6 +16,8 @@ interface SidebarProps {
1516
}
1617

1718
export function Sidebar({ liveStates, collapsed, onToggle }: SidebarProps) {
19+
const [createOpen, setCreateOpen] = useState(false);
20+
1821
const { data: agentsData } = useQuery({
1922
queryKey: ["agents"],
2023
queryFn: api.agents,
@@ -116,6 +119,13 @@ export function Sidebar({ liveStates, collapsed, onToggle }: SidebarProps) {
116119
</Link>
117120
);
118121
})}
122+
<button
123+
onClick={() => setCreateOpen(true)}
124+
className="flex h-8 w-8 items-center justify-center rounded-md text-sidebar-inkFaint hover:bg-sidebar-selected/50 hover:text-sidebar-inkDull"
125+
title="New Agent"
126+
>
127+
+
128+
</button>
119129
</div>
120130
) : (
121131
<>
@@ -189,16 +199,18 @@ export function Sidebar({ liveStates, collapsed, onToggle }: SidebarProps) {
189199
})}
190200
</div>
191201
)}
192-
<Button
193-
variant="outline"
194-
size="sm"
195-
className="mx-2 mt-1 w-auto justify-center border-dashed border-sidebar-line text-sidebar-inkFaint hover:border-sidebar-inkFaint hover:text-sidebar-inkDull"
196-
>
197-
+ New Agent
198-
</Button>
202+
<Button
203+
variant="outline"
204+
size="sm"
205+
onClick={() => setCreateOpen(true)}
206+
className="mx-2 mt-1 w-auto justify-center border-dashed border-sidebar-line text-sidebar-inkFaint hover:border-sidebar-inkFaint hover:text-sidebar-inkDull"
207+
>
208+
+ New Agent
209+
</Button>
199210
</div>
200211
</>
201212
)}
213+
<CreateAgentDialog open={createOpen} onOpenChange={setCreateOpen} />
202214
</motion.nav>
203215
);
204216
}

interface/src/routes/Overview.tsx

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { useMemo } from "react";
1+
import { useMemo, useState } from "react";
22
import { Link } from "@tanstack/react-router";
33
import { useQuery } from "@tanstack/react-query";
44
import { api, type AgentSummary } from "@/api/client";
5+
import { CreateAgentDialog } from "@/components/CreateAgentDialog";
56
import type { ChannelLiveState } from "@/hooks/useChannelLiveState";
67
import { formatTimeAgo, formatUptime } from "@/lib/format";
78
import { ResponsiveContainer, AreaChart, Area } from "recharts";
@@ -11,6 +12,8 @@ interface OverviewProps {
1112
}
1213

1314
export function Overview({ liveStates }: OverviewProps) {
15+
const [createOpen, setCreateOpen] = useState(false);
16+
1417
const { data: statusData } = useQuery({
1518
queryKey: ["status"],
1619
queryFn: api.status,
@@ -86,12 +89,27 @@ export function Overview({ liveStates }: OverviewProps) {
8689
) : agents.length === 0 ? (
8790
<div className="rounded-lg border border-dashed border-app-line p-8 text-center">
8891
<p className="text-sm text-ink-faint">No agents configured.</p>
92+
<button
93+
onClick={() => setCreateOpen(true)}
94+
className="mt-3 text-sm text-accent hover:text-accent/80 transition-colors"
95+
>
96+
Create your first agent
97+
</button>
98+
<CreateAgentDialog open={createOpen} onOpenChange={setCreateOpen} />
8999
</div>
90100
) : (
91101
<div className="flex flex-col gap-6">
92102
{/* Agent Cards */}
93103
<section>
94-
<h2 className="mb-4 font-plex text-sm font-medium text-ink-dull">Agents</h2>
104+
<div className="mb-4 flex items-center justify-between">
105+
<h2 className="font-plex text-sm font-medium text-ink-dull">Agents</h2>
106+
<button
107+
onClick={() => setCreateOpen(true)}
108+
className="text-sm text-ink-faint hover:text-ink transition-colors"
109+
>
110+
+ New Agent
111+
</button>
112+
</div>
95113
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 xl:grid-cols-3">
96114
{agents.map((agent) => (
97115
<AgentCard
@@ -101,6 +119,7 @@ export function Overview({ liveStates }: OverviewProps) {
101119
/>
102120
))}
103121
</div>
122+
<CreateAgentDialog open={createOpen} onOpenChange={setCreateOpen} />
104123
</section>
105124

106125
{/* Recent Channels */}
@@ -328,19 +347,25 @@ function AgentCard({
328347
</div>
329348

330349
{/* Bio — full text, no truncation */}
331-
{profile?.bio && (
350+
{profile?.bio ? (
332351
<p className="px-5 mt-3 text-sm leading-relaxed text-ink-dull">
333352
{profile.bio}
334353
</p>
354+
) : (
355+
<p className="px-5 mt-3 text-sm leading-relaxed text-ink-faint">
356+
This agent will fill out its own profile as it develops a personality through conversations.
357+
</p>
335358
)}
336359

337360
{/* Spacer pushes footer down when bio is short */}
338361
<div className="flex-1" />
339362

340363
{/* Sparkline */}
341-
<div className="mx-5 mt-4 h-10">
342-
<SparklineChart data={agent.activity_sparkline} />
343-
</div>
364+
{agent.activity_sparkline?.some((v) => v > 0) && (
365+
<div className="mx-5 mt-4 h-10">
366+
<SparklineChart data={agent.activity_sparkline} />
367+
</div>
368+
)}
344369

345370
{/* Stats footer */}
346371
<div className="flex items-center gap-4 px-5 py-3.5 mt-3 border-t border-app-line/50 text-tiny">

0 commit comments

Comments
 (0)