Skip to content

Commit d9180cf

Browse files
committed
add ai chatbot
1 parent 47f5fd2 commit d9180cf

File tree

16 files changed

+2287
-43
lines changed

16 files changed

+2287
-43
lines changed

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
auto-install-peers=true
22
strict-peer-dependencies=false
33
registry=https://registry.npmjs.org
4+
node-linker=hoisted

apps/nextra/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
NEXT_PUBLIC_ORIGIN="http://localhost:3030"
2+
NEXT_PUBLIC_API_URL="http://localhost:8080"
3+
NEXT_PUBLIC_FIREBASE_API_KEY=""
4+
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=""
5+
NEXT_PUBLIC_ADMIN_API_URL="http://localhost:4343/api/rspc"

apps/nextra/.npmrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@aptos-internal:registry=https://us-npm.pkg.dev/aptos-registry/npm/
2+
//us-npm.pkg.dev/aptos-registry/npm/:always-auth=true
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
import type { ComponentProps } from "react";
2+
import * as Dialog from "@radix-ui/react-dialog";
3+
import * as ScrollArea from "@radix-ui/react-scroll-area";
4+
import {
5+
PenLine,
6+
Trash2,
7+
X,
8+
ChevronLeft,
9+
ChevronRight,
10+
LogOut,
11+
} from "lucide-react";
12+
import type { LucideIcon } from "lucide-react";
13+
import { ChatInput } from "./chat-input";
14+
import { ChatMessage } from "./chat-message";
15+
import { ChatSidebar } from "./chat-sidebar";
16+
import type { ChatWidgetProps } from "@aptos-labs/ai-chatbot-client";
17+
import { useState, useRef, useEffect } from "react";
18+
import { cn } from "utils/cn";
19+
20+
export interface ChatDialogProps extends ChatWidgetProps {
21+
open?: boolean;
22+
onOpenChange?: (open: boolean) => void;
23+
showTrigger?: boolean;
24+
user?: {
25+
displayName?: string | null;
26+
email?: string | null;
27+
photoURL?: string | null;
28+
} | null;
29+
onSignOut?: () => void;
30+
}
31+
32+
const IconComponent = ({
33+
icon: Icon,
34+
...props
35+
}: { icon: LucideIcon } & ComponentProps<"svg">) => {
36+
return <Icon {...props} />;
37+
};
38+
39+
export function ChatDialog({
40+
open,
41+
onOpenChange,
42+
messages = [],
43+
isLoading,
44+
isGenerating,
45+
isTyping,
46+
hasMoreMessages,
47+
onSendMessage,
48+
onStopGenerating,
49+
onLoadMore,
50+
onCopyMessage,
51+
onMessageFeedback,
52+
onNewChat,
53+
className,
54+
messageClassName,
55+
fastMode,
56+
showSidebar = true,
57+
showTrigger = true,
58+
chats = [],
59+
currentChatId,
60+
onSelectChat,
61+
onDeleteChat,
62+
onUpdateChatTitle,
63+
onToggleFastMode,
64+
user,
65+
onSignOut,
66+
}: ChatDialogProps) {
67+
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
68+
const chatInputRef = useRef<HTMLTextAreaElement>(null);
69+
const viewportRef = useRef<HTMLDivElement>(null);
70+
71+
const scrollToBottom = () => {
72+
if (viewportRef.current) {
73+
viewportRef.current.scrollTo({
74+
top: viewportRef.current.scrollHeight,
75+
behavior: "smooth",
76+
});
77+
}
78+
};
79+
80+
useEffect(() => {
81+
const timeoutId = setTimeout(scrollToBottom, 100);
82+
return () => clearTimeout(timeoutId);
83+
}, [messages, isTyping]);
84+
85+
const handleNewChat = () => {
86+
onNewChat?.();
87+
setTimeout(() => {
88+
chatInputRef.current?.focus();
89+
}, 100);
90+
};
91+
92+
return (
93+
<Dialog.Root open={open} onOpenChange={onOpenChange}>
94+
{showTrigger && (
95+
<Dialog.Trigger asChild>
96+
<button className="flex items-center gap-2 text-sm font-medium text-text-primary hover:text-text-link">
97+
<svg
98+
xmlns="http://www.w3.org/2000/svg"
99+
fill="none"
100+
viewBox="0 0 24 24"
101+
strokeWidth={1.5}
102+
stroke="currentColor"
103+
className="w-5 h-5"
104+
>
105+
<path
106+
strokeLinecap="round"
107+
strokeLinejoin="round"
108+
d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
109+
/>
110+
</svg>
111+
Chat with AI
112+
</button>
113+
</Dialog.Trigger>
114+
)}
115+
<Dialog.Portal>
116+
<Dialog.Overlay className="fixed inset-0 z-[99999] bg-black/80 data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out" />
117+
<Dialog.Content
118+
className={cn(
119+
"fixed z-[100000] flex flex-col overflow-hidden rounded-xl bg-[#0F0F0F] shadow-xl",
120+
"left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2",
121+
"h-[85vh] w-[85vw] max-w-[1200px]",
122+
"data-[state=open]:animate-zoom-in data-[state=closed]:animate-zoom-out",
123+
className,
124+
)}
125+
style={
126+
{
127+
"--header-height": "3.5rem",
128+
"--footer-height": "4.5rem",
129+
} as React.CSSProperties
130+
}
131+
aria-describedby="dialog-description"
132+
>
133+
{/* Header */}
134+
<div
135+
className="flex shrink-0 items-center justify-between border-b border-[#1F1F1F] px-4"
136+
style={{ height: "var(--header-height)" }}
137+
>
138+
<div className="flex items-center gap-4">
139+
<div className="flex items-center gap-2">
140+
<Dialog.Title className="text-lg font-medium text-white">
141+
Ask AI
142+
</Dialog.Title>
143+
{showSidebar && user && (
144+
<button
145+
onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
146+
className="rounded p-1 text-gray-400 hover:bg-[#1F1F1F] hover:text-white"
147+
>
148+
<IconComponent
149+
icon={isSidebarCollapsed ? ChevronRight : ChevronLeft}
150+
className="h-4 w-4"
151+
/>
152+
</button>
153+
)}
154+
</div>
155+
</div>
156+
<div className="flex items-center gap-4">
157+
{user ? (
158+
<>
159+
<div className="flex items-center gap-3 border-r border-[#1F1F1F] pr-4">
160+
<div className="flex items-center gap-2">
161+
<span className="text-sm text-gray-300">
162+
{user.displayName || user.email}
163+
</span>
164+
</div>
165+
{onSignOut && (
166+
<button
167+
onClick={onSignOut}
168+
className="rounded p-1.5 text-gray-400 hover:bg-[#1F1F1F] hover:text-white"
169+
title="Sign out"
170+
>
171+
<IconComponent icon={LogOut} className="h-4 w-4" />
172+
</button>
173+
)}
174+
</div>
175+
<button
176+
onClick={handleNewChat}
177+
className="rounded-lg bg-white px-4 py-1.5 text-sm font-medium text-black hover:bg-gray-100"
178+
>
179+
<div className="flex items-center gap-2">
180+
<IconComponent icon={PenLine} className="h-4 w-4" />
181+
New chat
182+
</div>
183+
</button>
184+
{currentChatId && (
185+
<button
186+
onClick={() => onDeleteChat?.(currentChatId)}
187+
className="rounded p-2 text-gray-400 hover:bg-[#1F1F1F] hover:text-white"
188+
>
189+
<IconComponent icon={Trash2} className="h-5 w-5" />
190+
</button>
191+
)}
192+
</>
193+
) : null}
194+
<Dialog.Close className="rounded p-2 text-gray-400 hover:bg-[#1F1F1F] hover:text-white">
195+
<IconComponent icon={X} className="h-5 w-5" />
196+
</Dialog.Close>
197+
</div>
198+
</div>
199+
200+
<Dialog.Description id="dialog-description" className="sr-only">
201+
Chat interface for interacting with Aptos AI assistant. Use this
202+
dialog to ask questions and get responses from the AI.
203+
</Dialog.Description>
204+
205+
{/* Main Content */}
206+
<div className="flex min-h-0 flex-1">
207+
{/* Sidebar */}
208+
{showSidebar && user && (
209+
<ChatSidebar
210+
chats={chats}
211+
currentChatId={currentChatId || undefined}
212+
onSelectChat={onSelectChat}
213+
onDeleteChat={onDeleteChat}
214+
onUpdateChatTitle={onUpdateChatTitle}
215+
onNewChat={onNewChat}
216+
fastMode={fastMode}
217+
onToggleFastMode={onToggleFastMode}
218+
isCollapsed={isSidebarCollapsed}
219+
className="shrink-0 transition-all duration-200"
220+
/>
221+
)}
222+
223+
{/* Chat Area */}
224+
<div className="flex min-h-0 flex-1 flex-col bg-black">
225+
{/* Messages Area */}
226+
<div className="min-h-0 flex-1 overflow-hidden">
227+
{!user ? (
228+
<div className="flex h-full items-center justify-center">
229+
<div className="text-center">
230+
<h2 className="mb-4 text-xl font-semibold text-white">
231+
Sign in to Start Chatting
232+
</h2>
233+
<button
234+
onClick={onSignOut}
235+
className="rounded-lg bg-blue-600 px-6 py-2 text-white hover:bg-blue-700"
236+
>
237+
Sign in with Google
238+
</button>
239+
</div>
240+
</div>
241+
) : (
242+
<ScrollArea.Root className="h-full">
243+
<ScrollArea.Viewport
244+
ref={viewportRef}
245+
className="h-full w-full"
246+
>
247+
<div className="flex flex-col gap-4 p-4">
248+
{hasMoreMessages && (
249+
<button
250+
onClick={onLoadMore}
251+
className="text-sm text-gray-400 hover:text-white"
252+
>
253+
Load more
254+
</button>
255+
)}
256+
{messages.map((message) => (
257+
<ChatMessage
258+
key={message.id}
259+
message={message}
260+
onCopy={() => onCopyMessage?.(message.id)}
261+
onFeedback={(feedback) =>
262+
onMessageFeedback?.(message.id, feedback)
263+
}
264+
className={messageClassName}
265+
/>
266+
))}
267+
{(isLoading || isTyping) && (
268+
<div className="flex items-center text-gray-400">
269+
<div className="animate-pulse">...</div>
270+
</div>
271+
)}
272+
</div>
273+
</ScrollArea.Viewport>
274+
<ScrollArea.Scrollbar orientation="vertical">
275+
<ScrollArea.Thumb className="z-50 w-1.5 rounded-full bg-gray-700" />
276+
</ScrollArea.Scrollbar>
277+
</ScrollArea.Root>
278+
)}
279+
</div>
280+
281+
{/* Input Area */}
282+
{user && (
283+
<div
284+
className="shrink-0 border-t border-[#1F1F1F] bg-[#0F0F0F] px-4"
285+
style={{ height: "var(--footer-height)" }}
286+
>
287+
<ChatInput
288+
ref={chatInputRef}
289+
onSend={onSendMessage}
290+
onStop={onStopGenerating}
291+
isLoading={isGenerating}
292+
className="h-full py-3"
293+
/>
294+
</div>
295+
)}
296+
</div>
297+
</div>
298+
</Dialog.Content>
299+
</Dialog.Portal>
300+
</Dialog.Root>
301+
);
302+
}

0 commit comments

Comments
 (0)