Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add agent selection, router and sidebar layout in React client #536

Merged
merged 2 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@
},
"dependencies": {
"@ai16z/eliza": "workspace:*",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@tanstack/react-query": "^5.61.0",
"class-variance-authority": "^0.7.0",
"clsx": "2.1.0",
"lucide-react": "^0.460.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "6.22.1",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"vite-plugin-top-level-await": "^1.4.4",
Expand All @@ -25,8 +30,8 @@
"devDependencies": {
"@eslint/js": "^9.13.0",
"@types/node": "22.8.4",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.20",
"eslint": "^9.13.0",
Expand All @@ -37,6 +42,6 @@
"tailwindcss": "^3.4.15",
"typescript": "~5.6.2",
"typescript-eslint": "^8.11.0",
"vite": "^5.4.10"
"vite": "link:@tanstack/router-plugin/vite"
}
}
10 changes: 10 additions & 0 deletions client/src/Agent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default function Agent() {
return (
<div className="min-h-screen flex flex-col items-center justify-center p-4">
<p className="text-lg text-gray-600">
Select an option from the sidebar to configure, view, or chat
with your ELIZA agent
</p>
</div>
);
}
47 changes: 47 additions & 0 deletions client/src/Agents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useQuery } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { useNavigate } from "react-router-dom";
import "./App.css";

type Agent = {
id: string;
name: string;
};

function Agents() {
const navigate = useNavigate();
const { data: agents, isLoading } = useQuery({
queryKey: ["agents"],
queryFn: async () => {
const res = await fetch("/api/agents");
const data = await res.json();
return data.agents as Agent[];
},
});

return (
<div className="min-h-screen flex flex-col items-center justify-center p-4">
<h1 className="text-2xl font-bold mb-8">Select your agent:</h1>

{isLoading ? (
<div>Loading agents...</div>
) : (
<div className="grid gap-4 w-full max-w-md">
{agents?.map((agent) => (
<Button
key={agent.id}
className="w-full text-lg py-6"
onClick={() => {
navigate(`/${agent.id}`);
}}
>
{agent.name}
</Button>
))}
</div>
)}
</div>
);
}

export default Agents;
1 change: 0 additions & 1 deletion client/src/App.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}

Expand Down
65 changes: 2 additions & 63 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,10 @@
import { useState } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import "./App.css";
import { stringToUuid } from "@ai16z/eliza";

type TextResponse = {
text: string;
user: string;
};
import Agents from "./Agents";

function App() {
const [input, setInput] = useState("");
const [response, setResponse] = useState<TextResponse[]>([]);
const [loading, setLoading] = useState(false);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);

try {
const res = await fetch(`/api/${stringToUuid("Eliza")}/message`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
text: input,
userId: "user",
roomId: `default-room-${stringToUuid("Eliza")}`,
}),
});

const data: TextResponse[] = await res.json();

console.log(data);
setResponse(data);
setInput("");
} catch (error) {
console.error("Error:", error);
setResponse([{ text: "An error occurred", user: "system" }]);
} finally {
setLoading(false);
}
};

return (
<div className="min-h-screen flex flex-col items-center justify-center p-4">
<h1 className="text-2xl font-bold mb-4">Chat with Eliza</h1>
<form onSubmit={handleSubmit} className="w-full max-w-md space-y-4">
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Enter your message..."
className="w-full"
/>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? "Sending..." : "Send"}
</Button>
</form>

{(loading || response) && (
<div className="mt-8 p-4 w-full max-w-md bg-gray-100 rounded-lg">
{response.map((r) => (
<p key={r.text}>{r.text}</p>
))}
</div>
)}
<Agents />
</div>
);
}
Expand Down
7 changes: 7 additions & 0 deletions client/src/Character.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Character() {
return (
<div className="min-h-screen w-full flex flex-col items-center justify-center p-4">
<p className="text-lg text-gray-600">WIP</p>
</div>
);
}
104 changes: 104 additions & 0 deletions client/src/Chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useState } from "react";
import { useParams } from "react-router-dom";
import { useMutation } from "@tanstack/react-query";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import "./App.css";

type TextResponse = {
text: string;
user: string;
};

export default function Chat() {
const { agentId } = useParams();
const [input, setInput] = useState("");
const [messages, setMessages] = useState<TextResponse[]>([]);

const mutation = useMutation({
mutationFn: async (text: string) => {
const res = await fetch(`/api/${agentId}/message`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
text,
userId: "user",
roomId: `default-room-${agentId}`,
}),
});
return res.json() as Promise<TextResponse[]>;
},
onSuccess: (data) => {
setMessages((prev) => [...prev, ...data]);
},
});

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;

// Add user message immediately to state
const userMessage: TextResponse = {
text: input,
user: "user",
};
setMessages((prev) => [...prev, userMessage]);

mutation.mutate(input);
setInput("");
};

return (
<div className="flex flex-col h-screen max-h-screen w-full">
<div className="flex-1 min-h-0 overflow-y-auto p-4">
<div className="max-w-3xl mx-auto space-y-4">
{messages.length > 0 ? (
messages.map((message, index) => (
<div
key={index}
className={`flex ${
message.user === "user"
? "justify-end"
: "justify-start"
}`}
>
<div
className={`max-w-[80%] rounded-lg px-4 py-2 ${
message.user === "user"
? "bg-primary text-primary-foreground"
: "bg-muted"
}`}
>
{message.text}
</div>
</div>
))
) : (
<div className="text-center text-muted-foreground">
No messages yet. Start a conversation!
</div>
)}
</div>
</div>

<div className="border-t p-4 bg-background">
<div className="max-w-3xl mx-auto">
<form onSubmit={handleSubmit} className="flex gap-2">
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
className="flex-1"
disabled={mutation.isPending}
/>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "..." : "Send"}
</Button>
</form>
</div>
</div>
</div>
);
}
12 changes: 12 additions & 0 deletions client/src/Layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { SidebarProvider } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/app-sidebar";
import { Outlet } from "react-router-dom";

export default function Layout() {
return (
<SidebarProvider>
<AppSidebar />
<Outlet />
</SidebarProvider>
);
}
56 changes: 56 additions & 0 deletions client/src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Calendar, Home, Inbox, Search, Settings } from "lucide-react";
import { useParams } from "react-router-dom";

import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarTrigger,
} from "@/components/ui/sidebar";

// Menu items.
const items = [
{
title: "Chat",
url: "chat",
icon: Inbox,
},
{
title: "Character Overview",
url: "character",
icon: Calendar,
},
];

export function AppSidebar() {
const { agentId } = useParams();

return (
<Sidebar>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Application</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<a href={`/${agentId}/${item.url}`}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
);
}
33 changes: 33 additions & 0 deletions client/src/components/ui/separator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use client";

import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";

import { cn } from "@/lib/utils";

const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal"
? "h-[1px] w-full"
: "h-full w-[1px]",
className
)}
{...props}
/>
)
);
Separator.displayName = SeparatorPrimitive.Root.displayName;

export { Separator };
Loading