diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33f399bbe..d0a23db32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,6 @@ permissions: jobs: lint: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 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/components/chat-widget/chat-dialog.tsx b/apps/nextra/components/chat-widget/chat-dialog.tsx new file mode 100644 index 000000000..d727133b9 --- /dev/null +++ b/apps/nextra/components/chat-widget/chat-dialog.tsx @@ -0,0 +1,297 @@ +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 } 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"; +import Image from "next/image"; +import aptosLogo from "../../public/favicon/favicon.png"; + +export interface ChatDialogProps extends ChatWidgetProps { + open?: boolean; + onOpenChange?: (open: boolean) => void; + showTrigger?: boolean; +} + +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, +}: 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 */} +
+
+
+ + Aptos AI + AskAptos + + {showSidebar && ( + + )} +
+
+
+ + {currentChatId && ( + + )} + + + +
+
+ + + Chat interface for interacting with Aptos AI assistant. Use this + dialog to ask questions and get responses from the AI. + + + {/* Main Content */} +
+ {/* Sidebar */} + {showSidebar && ( + + )} + + {/* Chat Area */} +
+ {/* Messages Area */} +
+ + +
+ {hasMoreMessages && ( + + )} + {messages.length === 0 && ( +
+
+ Aptos AI +
+
+

+ Ask me anything about Aptos! +

+

+ I'm here to help you with Move development, + blockchain concepts, tools, and more. +

+
+

+ 💡 Pro tip: Toggle "Fast mode" in the sidebar + for quicker responses. Note that fast responses + might be less detailed. +

+
+
+
+ )} + {messages.map((message, index) => ( + onCopyMessage?.(message.id)} + onFeedback={(feedback) => + onMessageFeedback?.(message.id, feedback) + } + className={messageClassName} + /> + ))} + {isTyping && ( +
+
+
+
+
+ )} +
+ + + + + +
+ + {/* Input Area */} +
+ +
+ + {/* Disclaimer */} +
+ By messaging AskAptos, you agree to our{" "} + + Terms + {" "} + and have read our{" "} + + Privacy Policy + +
+
+
+ + + + ); +} 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..2960f0c27 --- /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 ( +
+
+