Skip to content

Commit 27d0504

Browse files
committed
upd: added initial chatbox
1 parent 7593320 commit 27d0504

19 files changed

+2310
-168
lines changed

.vscode/settings.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"editor.defaultFormatter": "esbenp.prettier-vscode"
2222
},
2323
"[typescriptreact]": {
24-
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
24+
"editor.defaultFormatter": "esbenp.prettier-vscode"
2525
},
2626
"[javascriptreact]": {
2727
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
@@ -44,4 +44,4 @@
4444
"[shellscript]": {
4545
"editor.defaultFormatter": "foxundermoon.shell-format"
4646
}
47-
}
47+
}

client/app/components/app-sidebar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function AppSidebar() {
4040
<SidebarMenu>
4141
{agents?.map((agent) => (
4242
<SidebarMenuItem key={agent.id}>
43-
<Link to={`/agent/${agent.id}`}>
43+
<Link to={`/chat/${agent.id}`}>
4444
<SidebarMenuButton>
4545
<div className="flex items-center gap-2">
4646
<div className="w-8 bg-muted rounded-lg uppercase aspect-square grid place-items-center">

client/app/components/chat.tsx

+233-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,235 @@
1-
export default function Chat(){
1+
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
2+
import { Button } from "~/components/ui/button";
3+
import {
4+
ChatBubble,
5+
ChatBubbleAction,
6+
ChatBubbleAvatar,
7+
ChatBubbleMessage,
8+
} from "~/components/ui/chat/chat-bubble";
9+
import { ChatInput } from "~/components/ui/chat/chat-input";
10+
import { ChatMessageList } from "~/components/ui/chat/chat-message-list";
11+
import { AnimatePresence, motion } from "framer-motion";
12+
import {
13+
CopyIcon,
14+
CornerDownLeft,
15+
Mic,
16+
Paperclip,
17+
RefreshCcw,
18+
Volume2,
19+
} from "lucide-react";
20+
import { useEffect, useRef, useState } from "react";
21+
22+
const ChatAiIcons = [
23+
{
24+
icon: CopyIcon,
25+
label: "Copy",
26+
},
27+
{
28+
icon: RefreshCcw,
29+
label: "Refresh",
30+
},
31+
{
32+
icon: Volume2,
33+
label: "Volume",
34+
},
35+
];
36+
37+
export default function Page() {
38+
const [messages, setMessages]: any[] = useState([]);
39+
const selectedUser = {
40+
name: "AAA",
41+
avatar: null,
42+
};
43+
const [input, setInput] = useState("");
44+
const [isLoading, setisLoading] = useState(false);
45+
46+
const messagesContainerRef = useRef<HTMLDivElement>(null);
47+
48+
const inputRef = useRef<HTMLTextAreaElement>(null);
49+
const formRef = useRef<HTMLFormElement>(null);
50+
51+
const getMessageVariant = (role: string) =>
52+
role === "ai" ? "received" : "sent";
53+
useEffect(() => {
54+
if (messagesContainerRef.current) {
55+
messagesContainerRef.current.scrollTop =
56+
messagesContainerRef.current.scrollHeight;
57+
}
58+
}, [messages]);
59+
60+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
61+
if (e.key === "Enter" && !e.shiftKey) {
62+
handleSendMessage(e as unknown as React.FormEvent<HTMLFormElement>);
63+
}
64+
};
65+
66+
const handleSendMessage = (e: React.FormEvent<HTMLFormElement>) => {
67+
e.preventDefault();
68+
if (!input) return;
69+
70+
setMessages((messages) => [
71+
...messages,
72+
{
73+
id: messages.length + 1,
74+
avatar: selectedUser.avatar,
75+
name: selectedUser.name,
76+
role: "user",
77+
message: input,
78+
},
79+
]);
80+
81+
setInput("");
82+
formRef.current?.reset();
83+
};
84+
85+
useEffect(() => {
86+
if (inputRef.current) {
87+
inputRef.current.focus();
88+
}
89+
}, []);
90+
291
return (
3-
<div>
4-
Chat
92+
<div className="flex flex-col w-full h-[calc(100dvh)]">
93+
<div className="flex-1 overflow-y-auto">
94+
<ChatMessageList ref={messagesContainerRef}>
95+
{/* Chat messages */}
96+
<AnimatePresence>
97+
{messages.map((message, index) => {
98+
const variant = getMessageVariant(message.role!);
99+
return (
100+
<motion.div
101+
key={index}
102+
layout
103+
initial={{
104+
opacity: 0,
105+
scale: 1,
106+
y: 50,
107+
x: 0,
108+
}}
109+
animate={{
110+
opacity: 1,
111+
scale: 1,
112+
y: 0,
113+
x: 0,
114+
}}
115+
exit={{ opacity: 0, scale: 1, y: 1, x: 0 }}
116+
transition={{
117+
opacity: { duration: 0.1 },
118+
layout: {
119+
type: "spring",
120+
bounce: 0.3,
121+
duration: index * 0.05 + 0.2,
122+
},
123+
}}
124+
style={{ originX: 0.5, originY: 0.5 }}
125+
className="flex flex-col gap-2 p-4"
126+
>
127+
<ChatBubble key={index} variant={variant}>
128+
<Avatar>
129+
<AvatarImage
130+
src={
131+
message.role === "ai"
132+
? ""
133+
: message.avatar
134+
}
135+
alt="Avatar"
136+
className={
137+
message.role === "ai"
138+
? "dark:invert"
139+
: ""
140+
}
141+
/>
142+
<AvatarFallback>
143+
{message.role === "ai"
144+
? "🤖"
145+
: "GG"}
146+
</AvatarFallback>
147+
</Avatar>
148+
<ChatBubbleMessage
149+
isLoading={message.isLoading}
150+
>
151+
{message.message}
152+
{message.role === "ai" && (
153+
<div className="flex items-center mt-1.5 gap-1">
154+
{!message.isLoading && (
155+
<>
156+
{ChatAiIcons.map(
157+
(
158+
icon,
159+
index
160+
) => {
161+
const Icon =
162+
icon.icon;
163+
return (
164+
<ChatBubbleAction
165+
variant="outline"
166+
className="size-6"
167+
key={
168+
index
169+
}
170+
icon={
171+
<Icon className="size-3" />
172+
}
173+
onClick={() =>
174+
console.log(
175+
"Action " +
176+
icon.label +
177+
" clicked for message " +
178+
index
179+
)
180+
}
181+
/>
182+
);
183+
}
184+
)}
185+
</>
186+
)}
187+
</div>
188+
)}
189+
</ChatBubbleMessage>
190+
</ChatBubble>
191+
</motion.div>
192+
);
193+
})}
194+
</AnimatePresence>
195+
</ChatMessageList>
196+
</div>
197+
<div className="px-4 pb-4">
198+
<form
199+
ref={formRef}
200+
onSubmit={handleSendMessage}
201+
className="relative rounded-lg border bg-background"
202+
>
203+
<ChatInput
204+
ref={inputRef}
205+
onKeyDown={handleKeyDown}
206+
onChange={({ target }) => setInput(target.value)}
207+
placeholder="Type your message here..."
208+
className="min-h-12 resize-none rounded-lg bg-background border-0 p-3 shadow-none focus-visible:ring-0"
209+
/>
210+
<div className="flex items-center p-3 pt-0">
211+
<Button variant="ghost" size="icon">
212+
<Paperclip className="size-4" />
213+
<span className="sr-only">Attach file</span>
214+
</Button>
215+
216+
{/* <Button variant="ghost" size="icon">
217+
<Mic className="size-4" />
218+
<span className="sr-only">Use Microphone</span>
219+
</Button> */}
220+
221+
<Button
222+
disabled={!input || isLoading}
223+
type="submit"
224+
size="sm"
225+
className="ml-auto gap-1.5"
226+
>
227+
Send Message
228+
<CornerDownLeft className="size-3.5" />
229+
</Button>
230+
</div>
231+
</form>
232+
</div>
5233
</div>
6-
)
7-
}
234+
);
235+
}

client/app/components/overview.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { Character } from "@elizaos/core";
2-
import { Fragment } from "react/jsx-runtime";
32
import ArrayInput from "~/components/array-input";
43
import InputCopy from "~/components/input-copy";
54
import PageTitle from "./page-title";
65

76
export default function Overview({ character }: { character: Character }) {
87
return (
9-
<Fragment>
8+
<div className="p-4">
109
<PageTitle
1110
title="Overview"
1211
subtitle="An overview of your selected AI Agent."
@@ -35,6 +34,6 @@ export default function Overview({ character }: { character: Character }) {
3534
}
3635
/>
3736
</div>
38-
</Fragment>
37+
</div>
3938
);
4039
}

0 commit comments

Comments
 (0)