Skip to content

Commit

Permalink
feat: add agent selection, router and sidebar layout in React client
Browse files Browse the repository at this point in the history
  • Loading branch information
vivoidos committed Nov 23, 2024
1 parent afb7cc1 commit baaa696
Show file tree
Hide file tree
Showing 23 changed files with 2,541 additions and 553 deletions.
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

0 comments on commit baaa696

Please sign in to comment.