Skip to content

Commit baaa696

Browse files
committed
feat: add agent selection, router and sidebar layout in React client
1 parent afb7cc1 commit baaa696

23 files changed

+2541
-553
lines changed

client/package.json

+8-3
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,17 @@
1111
},
1212
"dependencies": {
1313
"@ai16z/eliza": "workspace:*",
14+
"@radix-ui/react-dialog": "^1.1.2",
15+
"@radix-ui/react-separator": "^1.1.0",
1416
"@radix-ui/react-slot": "^1.1.0",
17+
"@radix-ui/react-tooltip": "^1.1.4",
18+
"@tanstack/react-query": "^5.61.0",
1519
"class-variance-authority": "^0.7.0",
1620
"clsx": "2.1.0",
1721
"lucide-react": "^0.460.0",
1822
"react": "^18.3.1",
1923
"react-dom": "^18.3.1",
24+
"react-router-dom": "6.22.1",
2025
"tailwind-merge": "^2.5.4",
2126
"tailwindcss-animate": "^1.0.7",
2227
"vite-plugin-top-level-await": "^1.4.4",
@@ -25,8 +30,8 @@
2530
"devDependencies": {
2631
"@eslint/js": "^9.13.0",
2732
"@types/node": "22.8.4",
28-
"@types/react": "18.3.12",
29-
"@types/react-dom": "18.3.1",
33+
"@types/react": "^18.3.12",
34+
"@types/react-dom": "^18.3.1",
3035
"@vitejs/plugin-react": "^4.3.3",
3136
"autoprefixer": "^10.4.20",
3237
"eslint": "^9.13.0",
@@ -37,6 +42,6 @@
3742
"tailwindcss": "^3.4.15",
3843
"typescript": "~5.6.2",
3944
"typescript-eslint": "^8.11.0",
40-
"vite": "^5.4.10"
45+
"vite": "link:@tanstack/router-plugin/vite"
4146
}
4247
}

client/src/Agent.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export default function Agent() {
2+
return (
3+
<div className="min-h-screen flex flex-col items-center justify-center p-4">
4+
<p className="text-lg text-gray-600">
5+
Select an option from the sidebar to configure, view, or chat
6+
with your ELIZA agent
7+
</p>
8+
</div>
9+
);
10+
}

client/src/Agents.tsx

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { Button } from "@/components/ui/button";
3+
import { useNavigate } from "react-router-dom";
4+
import "./App.css";
5+
6+
type Agent = {
7+
id: string;
8+
name: string;
9+
};
10+
11+
function Agents() {
12+
const navigate = useNavigate();
13+
const { data: agents, isLoading } = useQuery({
14+
queryKey: ["agents"],
15+
queryFn: async () => {
16+
const res = await fetch("/api/agents");
17+
const data = await res.json();
18+
return data.agents as Agent[];
19+
},
20+
});
21+
22+
return (
23+
<div className="min-h-screen flex flex-col items-center justify-center p-4">
24+
<h1 className="text-2xl font-bold mb-8">Select your agent:</h1>
25+
26+
{isLoading ? (
27+
<div>Loading agents...</div>
28+
) : (
29+
<div className="grid gap-4 w-full max-w-md">
30+
{agents?.map((agent) => (
31+
<Button
32+
key={agent.id}
33+
className="w-full text-lg py-6"
34+
onClick={() => {
35+
navigate(`/${agent.id}`);
36+
}}
37+
>
38+
{agent.name}
39+
</Button>
40+
))}
41+
</div>
42+
)}
43+
</div>
44+
);
45+
}
46+
47+
export default Agents;

client/src/App.css

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#root {
22
max-width: 1280px;
33
margin: 0 auto;
4-
padding: 2rem;
54
text-align: center;
65
}
76

client/src/App.tsx

+2-63
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,10 @@
1-
import { useState } from "react";
2-
import { Input } from "@/components/ui/input";
3-
import { Button } from "@/components/ui/button";
41
import "./App.css";
5-
import { stringToUuid } from "@ai16z/eliza";
6-
7-
type TextResponse = {
8-
text: string;
9-
user: string;
10-
};
2+
import Agents from "./Agents";
113

124
function App() {
13-
const [input, setInput] = useState("");
14-
const [response, setResponse] = useState<TextResponse[]>([]);
15-
const [loading, setLoading] = useState(false);
16-
17-
const handleSubmit = async (e: React.FormEvent) => {
18-
e.preventDefault();
19-
setLoading(true);
20-
21-
try {
22-
const res = await fetch(`/api/${stringToUuid("Eliza")}/message`, {
23-
method: "POST",
24-
headers: {
25-
"Content-Type": "application/json",
26-
},
27-
body: JSON.stringify({
28-
text: input,
29-
userId: "user",
30-
roomId: `default-room-${stringToUuid("Eliza")}`,
31-
}),
32-
});
33-
34-
const data: TextResponse[] = await res.json();
35-
36-
console.log(data);
37-
setResponse(data);
38-
setInput("");
39-
} catch (error) {
40-
console.error("Error:", error);
41-
setResponse([{ text: "An error occurred", user: "system" }]);
42-
} finally {
43-
setLoading(false);
44-
}
45-
};
46-
475
return (
486
<div className="min-h-screen flex flex-col items-center justify-center p-4">
49-
<h1 className="text-2xl font-bold mb-4">Chat with Eliza</h1>
50-
<form onSubmit={handleSubmit} className="w-full max-w-md space-y-4">
51-
<Input
52-
value={input}
53-
onChange={(e) => setInput(e.target.value)}
54-
placeholder="Enter your message..."
55-
className="w-full"
56-
/>
57-
<Button type="submit" className="w-full" disabled={loading}>
58-
{loading ? "Sending..." : "Send"}
59-
</Button>
60-
</form>
61-
62-
{(loading || response) && (
63-
<div className="mt-8 p-4 w-full max-w-md bg-gray-100 rounded-lg">
64-
{response.map((r) => (
65-
<p key={r.text}>{r.text}</p>
66-
))}
67-
</div>
68-
)}
7+
<Agents />
698
</div>
709
);
7110
}

client/src/Character.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Character() {
2+
return (
3+
<div className="min-h-screen w-full flex flex-col items-center justify-center p-4">
4+
<p className="text-lg text-gray-600">WIP</p>
5+
</div>
6+
);
7+
}

client/src/Chat.tsx

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { useState } from "react";
2+
import { useParams } from "react-router-dom";
3+
import { useMutation } from "@tanstack/react-query";
4+
import { Input } from "@/components/ui/input";
5+
import { Button } from "@/components/ui/button";
6+
import "./App.css";
7+
8+
type TextResponse = {
9+
text: string;
10+
user: string;
11+
};
12+
13+
export default function Chat() {
14+
const { agentId } = useParams();
15+
const [input, setInput] = useState("");
16+
const [messages, setMessages] = useState<TextResponse[]>([]);
17+
18+
const mutation = useMutation({
19+
mutationFn: async (text: string) => {
20+
const res = await fetch(`/api/${agentId}/message`, {
21+
method: "POST",
22+
headers: {
23+
"Content-Type": "application/json",
24+
},
25+
body: JSON.stringify({
26+
text,
27+
userId: "user",
28+
roomId: `default-room-${agentId}`,
29+
}),
30+
});
31+
return res.json() as Promise<TextResponse[]>;
32+
},
33+
onSuccess: (data) => {
34+
setMessages((prev) => [...prev, ...data]);
35+
},
36+
});
37+
38+
const handleSubmit = async (e: React.FormEvent) => {
39+
e.preventDefault();
40+
if (!input.trim()) return;
41+
42+
// Add user message immediately to state
43+
const userMessage: TextResponse = {
44+
text: input,
45+
user: "user",
46+
};
47+
setMessages((prev) => [...prev, userMessage]);
48+
49+
mutation.mutate(input);
50+
setInput("");
51+
};
52+
53+
return (
54+
<div className="flex flex-col h-screen max-h-screen w-full">
55+
<div className="flex-1 min-h-0 overflow-y-auto p-4">
56+
<div className="max-w-3xl mx-auto space-y-4">
57+
{messages.length > 0 ? (
58+
messages.map((message, index) => (
59+
<div
60+
key={index}
61+
className={`flex ${
62+
message.user === "user"
63+
? "justify-end"
64+
: "justify-start"
65+
}`}
66+
>
67+
<div
68+
className={`max-w-[80%] rounded-lg px-4 py-2 ${
69+
message.user === "user"
70+
? "bg-primary text-primary-foreground"
71+
: "bg-muted"
72+
}`}
73+
>
74+
{message.text}
75+
</div>
76+
</div>
77+
))
78+
) : (
79+
<div className="text-center text-muted-foreground">
80+
No messages yet. Start a conversation!
81+
</div>
82+
)}
83+
</div>
84+
</div>
85+
86+
<div className="border-t p-4 bg-background">
87+
<div className="max-w-3xl mx-auto">
88+
<form onSubmit={handleSubmit} className="flex gap-2">
89+
<Input
90+
value={input}
91+
onChange={(e) => setInput(e.target.value)}
92+
placeholder="Type a message..."
93+
className="flex-1"
94+
disabled={mutation.isPending}
95+
/>
96+
<Button type="submit" disabled={mutation.isPending}>
97+
{mutation.isPending ? "..." : "Send"}
98+
</Button>
99+
</form>
100+
</div>
101+
</div>
102+
</div>
103+
);
104+
}

client/src/Layout.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { SidebarProvider } from "@/components/ui/sidebar";
2+
import { AppSidebar } from "@/components/app-sidebar";
3+
import { Outlet } from "react-router-dom";
4+
5+
export default function Layout() {
6+
return (
7+
<SidebarProvider>
8+
<AppSidebar />
9+
<Outlet />
10+
</SidebarProvider>
11+
);
12+
}

client/src/components/app-sidebar.tsx

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Calendar, Home, Inbox, Search, Settings } from "lucide-react";
2+
import { useParams } from "react-router-dom";
3+
4+
import {
5+
Sidebar,
6+
SidebarContent,
7+
SidebarGroup,
8+
SidebarGroupContent,
9+
SidebarGroupLabel,
10+
SidebarMenu,
11+
SidebarMenuButton,
12+
SidebarMenuItem,
13+
SidebarTrigger,
14+
} from "@/components/ui/sidebar";
15+
16+
// Menu items.
17+
const items = [
18+
{
19+
title: "Chat",
20+
url: "chat",
21+
icon: Inbox,
22+
},
23+
{
24+
title: "Character Overview",
25+
url: "character",
26+
icon: Calendar,
27+
},
28+
];
29+
30+
export function AppSidebar() {
31+
const { agentId } = useParams();
32+
33+
return (
34+
<Sidebar>
35+
<SidebarContent>
36+
<SidebarGroup>
37+
<SidebarGroupLabel>Application</SidebarGroupLabel>
38+
<SidebarGroupContent>
39+
<SidebarMenu>
40+
{items.map((item) => (
41+
<SidebarMenuItem key={item.title}>
42+
<SidebarMenuButton asChild>
43+
<a href={`/${agentId}/${item.url}`}>
44+
<item.icon />
45+
<span>{item.title}</span>
46+
</a>
47+
</SidebarMenuButton>
48+
</SidebarMenuItem>
49+
))}
50+
</SidebarMenu>
51+
</SidebarGroupContent>
52+
</SidebarGroup>
53+
</SidebarContent>
54+
</Sidebar>
55+
);
56+
}
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import * as SeparatorPrimitive from "@radix-ui/react-separator";
5+
6+
import { cn } from "@/lib/utils";
7+
8+
const Separator = React.forwardRef<
9+
React.ElementRef<typeof SeparatorPrimitive.Root>,
10+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
11+
>(
12+
(
13+
{ className, orientation = "horizontal", decorative = true, ...props },
14+
ref
15+
) => (
16+
<SeparatorPrimitive.Root
17+
ref={ref}
18+
decorative={decorative}
19+
orientation={orientation}
20+
className={cn(
21+
"shrink-0 bg-border",
22+
orientation === "horizontal"
23+
? "h-[1px] w-full"
24+
: "h-full w-[1px]",
25+
className
26+
)}
27+
{...props}
28+
/>
29+
)
30+
);
31+
Separator.displayName = SeparatorPrimitive.Root.displayName;
32+
33+
export { Separator };

0 commit comments

Comments
 (0)