From be366b5b5b2523034763574a4e47a8e2750172ab Mon Sep 17 00:00:00 2001 From: Jill Xu Date: Thu, 12 Jun 2025 14:56:38 -0700 Subject: [PATCH 1/7] add ai chatbot --- .npmrc | 1 + apps/nextra/.env.example | 4 + apps/nextra/.npmrc | 2 + .../components/chat-widget/chat-dialog.tsx | 302 +++++ .../components/chat-widget/chat-input.tsx | 99 ++ .../components/chat-widget/chat-message.tsx | 147 +++ .../components/chat-widget/chat-sidebar.tsx | 249 ++++ apps/nextra/components/chat-widget/index.tsx | 146 +++ apps/nextra/components/chat-widget/types.ts | 42 + apps/nextra/components/index.tsx | 1 + apps/nextra/next.config.mjs | 1 + apps/nextra/package.json | 9 + apps/nextra/services/aptosBuildApi.ts | 214 ++++ apps/nextra/theme.config.tsx | 6 + pnpm-lock.yaml | 1104 ++++++++++++++++- 15 files changed, 2285 insertions(+), 42 deletions(-) create mode 100644 apps/nextra/.npmrc create mode 100644 apps/nextra/components/chat-widget/chat-dialog.tsx create mode 100644 apps/nextra/components/chat-widget/chat-input.tsx create mode 100644 apps/nextra/components/chat-widget/chat-message.tsx create mode 100644 apps/nextra/components/chat-widget/chat-sidebar.tsx create mode 100644 apps/nextra/components/chat-widget/index.tsx create mode 100644 apps/nextra/components/chat-widget/types.ts create mode 100644 apps/nextra/services/aptosBuildApi.ts diff --git a/.npmrc b/.npmrc index 714c59009..e48b4c115 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1,4 @@ auto-install-peers=true strict-peer-dependencies=false registry=https://registry.npmjs.org +node-linker=hoisted diff --git a/apps/nextra/.env.example b/apps/nextra/.env.example index b7a675858..fd07d36f2 100644 --- a/apps/nextra/.env.example +++ b/apps/nextra/.env.example @@ -1 +1,5 @@ NEXT_PUBLIC_ORIGIN="http://localhost:3030" +NEXT_PUBLIC_API_URL="http://localhost:8080" +NEXT_PUBLIC_FIREBASE_API_KEY="" +NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="" +NEXT_PUBLIC_ADMIN_API_URL="http://localhost:4343/api/rspc" diff --git a/apps/nextra/.npmrc b/apps/nextra/.npmrc new file mode 100644 index 000000000..593bb3774 --- /dev/null +++ b/apps/nextra/.npmrc @@ -0,0 +1,2 @@ +@aptos-internal:registry=https://us-npm.pkg.dev/aptos-registry/npm/ +//us-npm.pkg.dev/aptos-registry/npm/:always-auth=true diff --git a/apps/nextra/components/chat-widget/chat-dialog.tsx b/apps/nextra/components/chat-widget/chat-dialog.tsx new file mode 100644 index 000000000..1d6a25e05 --- /dev/null +++ b/apps/nextra/components/chat-widget/chat-dialog.tsx @@ -0,0 +1,302 @@ +import type { ComponentProps } from "react"; +import * as Dialog from "@radix-ui/react-dialog"; +import * as ScrollArea from "@radix-ui/react-scroll-area"; +import { + PenLine, + Trash2, + X, + ChevronLeft, + ChevronRight, + LogOut, +} from "lucide-react"; +import type { LucideIcon } from "lucide-react"; +import { ChatInput } from "./chat-input"; +import { ChatMessage } from "./chat-message"; +import { ChatSidebar } from "./chat-sidebar"; +import type { ChatWidgetProps } from "@aptos-labs/ai-chatbot-client"; +import { useState, useRef, useEffect } from "react"; +import { cn } from "utils/cn"; + +export interface ChatDialogProps extends ChatWidgetProps { + open?: boolean; + onOpenChange?: (open: boolean) => void; + showTrigger?: boolean; + user?: { + displayName?: string | null; + email?: string | null; + photoURL?: string | null; + } | null; + onSignOut?: () => void; +} + +const IconComponent = ({ + icon: Icon, + ...props +}: { icon: LucideIcon } & ComponentProps<"svg">) => { + return ; +}; + +export function ChatDialog({ + open, + onOpenChange, + messages = [], + isLoading, + isGenerating, + isTyping, + hasMoreMessages, + onSendMessage, + onStopGenerating, + onLoadMore, + onCopyMessage, + onMessageFeedback, + onNewChat, + className, + messageClassName, + fastMode, + showSidebar = true, + showTrigger = true, + chats = [], + currentChatId, + onSelectChat, + onDeleteChat, + onUpdateChatTitle, + onToggleFastMode, + user, + onSignOut, +}: ChatDialogProps) { + const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); + const chatInputRef = useRef(null); + const viewportRef = useRef(null); + + const scrollToBottom = () => { + if (viewportRef.current) { + viewportRef.current.scrollTo({ + top: viewportRef.current.scrollHeight, + behavior: "smooth", + }); + } + }; + + useEffect(() => { + const timeoutId = setTimeout(scrollToBottom, 100); + return () => clearTimeout(timeoutId); + }, [messages, isTyping]); + + const handleNewChat = () => { + onNewChat?.(); + setTimeout(() => { + chatInputRef.current?.focus(); + }, 100); + }; + + return ( + + {showTrigger && ( + + + + )} + + + + {/* Header */} +
+
+
+ + Ask AI + + {showSidebar && user && ( + + )} +
+
+
+ {user ? ( + <> +
+
+ + {user.displayName || user.email} + +
+ {onSignOut && ( + + )} +
+ + {currentChatId && ( + + )} + + ) : null} + + + +
+
+ + + Chat interface for interacting with Aptos AI assistant. Use this + dialog to ask questions and get responses from the AI. + + + {/* Main Content */} +
+ {/* Sidebar */} + {showSidebar && user && ( + + )} + + {/* Chat Area */} +
+ {/* Messages Area */} +
+ {!user ? ( +
+
+

+ Sign in to Start Chatting +

+ +
+
+ ) : ( + + +
+ {hasMoreMessages && ( + + )} + {messages.map((message) => ( + onCopyMessage?.(message.id)} + onFeedback={(feedback) => + onMessageFeedback?.(message.id, feedback) + } + className={messageClassName} + /> + ))} + {(isLoading || isTyping) && ( +
+
...
+
+ )} +
+
+ + + +
+ )} +
+ + {/* Input Area */} + {user && ( +
+ +
+ )} +
+
+
+
+
+ ); +} diff --git a/apps/nextra/components/chat-widget/chat-input.tsx b/apps/nextra/components/chat-widget/chat-input.tsx new file mode 100644 index 000000000..aea1339d9 --- /dev/null +++ b/apps/nextra/components/chat-widget/chat-input.tsx @@ -0,0 +1,99 @@ +import * as React from "react"; +import { ArrowRight, StopCircle } from "lucide-react"; +import { cn } from "utils/cn"; + +export interface ChatInputProps { + onSend: (message: string) => void; + onStop?: () => void; + isLoading?: boolean; + className?: string; +} + +const ChatInput = React.forwardRef( + ({ onSend, onStop, isLoading, className }, ref) => { + const [message, setMessage] = React.useState(""); + const textareaRef = React.useRef(null); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const trimmedMessage = message.trim(); + if (trimmedMessage) { + onSend(trimmedMessage); + setMessage(""); + if (textareaRef.current) { + textareaRef.current.style.height = "auto"; + } + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e); + } + }; + + const handleInput = (e: React.ChangeEvent) => { + setMessage(e.target.value); + if (textareaRef.current) { + textareaRef.current.style.height = "auto"; + const maxHeight = 120; // Maximum height before scrolling + const newHeight = Math.min(textareaRef.current.scrollHeight, maxHeight); + textareaRef.current.style.height = `${newHeight}px`; + } + }; + + return ( +
+
+