diff --git a/apps/portal/src/app/Header.tsx b/apps/portal/src/app/Header.tsx index 4d91309b59c..a7ad861a7a1 100644 --- a/apps/portal/src/app/Header.tsx +++ b/apps/portal/src/app/Header.tsx @@ -1,12 +1,7 @@ "use client"; import clsx from "clsx"; -import { - ChevronDownIcon, - MenuIcon, - MessageCircleIcon, - TableOfContentsIcon, -} from "lucide-react"; +import { ChevronDownIcon, MenuIcon, TableOfContentsIcon } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useState } from "react"; @@ -18,6 +13,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { ChatButton } from "../components/AI/chat-button"; import { GithubIcon } from "../components/Document/GithubButtonLink"; import { CustomAccordion } from "../components/others/CustomAccordion"; import { ThemeSwitcher } from "../components/others/theme/ThemeSwitcher"; @@ -242,22 +238,13 @@ export function Header() {
- +
- + +
diff --git a/apps/portal/src/components/AI/chat-button.tsx b/apps/portal/src/components/AI/chat-button.tsx new file mode 100644 index 00000000000..5b8051ba87b --- /dev/null +++ b/apps/portal/src/components/AI/chat-button.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { MessageCircleIcon, RefreshCcwIcon, XIcon } from "lucide-react"; +import { lazy, Suspense, useCallback, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { Spinner } from "../ui/Spinner/Spinner"; + +const Chat = lazy(() => + import("./chat").then((mod) => ({ default: mod.Chat })), +); + +export function ChatButton() { + const [isOpen, setIsOpen] = useState(false); + const [hasBeenOpened, setHasBeenOpened] = useState(false); + const closeModal = useCallback(() => setIsOpen(false), []); + const [id, setId] = useState(0); + + return ( + <> + {/* Inline Button (not floating) */} + + + {/* Popup/Modal */} +
+ {/* Header with close button */} +
+
+ Ask AI +
+ +
+ + + +
+
+ {/* Chat Content */} +
+ {hasBeenOpened && ( + }> + + + )} +
+
+ + ); +} + +function ChatLoading() { + return ( +
+ +
+ ); +} diff --git a/apps/portal/src/components/AI/chat.tsx b/apps/portal/src/components/AI/chat.tsx index 4a1bec015c7..d3590f4fcde 100644 --- a/apps/portal/src/components/AI/chat.tsx +++ b/apps/portal/src/components/AI/chat.tsx @@ -1,6 +1,10 @@ "use client"; -import { useMutation } from "@tanstack/react-query"; +import { + QueryClient, + QueryClientProvider, + useMutation, +} from "@tanstack/react-query"; import { ArrowUpIcon, ThumbsDownIcon, @@ -41,6 +45,8 @@ const predefinedPrompts = [ "How do I send a transaction in Unity?", ]; +const queryClient = new QueryClient(); + // Empty State Component function ChatEmptyState({ onPromptClick, @@ -156,13 +162,16 @@ export function Chat() { [conversationId, posthog], ); + const lastMessageLength = messages[messages.length - 1]?.content.length ?? 0; + + // biome-ignore lint/correctness/useExhaustiveDependencies: need both the number of messages and the last message length to trigger the scroll useEffect(() => { if (scrollAnchorRef.current && messages.length > 0) { scrollAnchorRef.current.scrollIntoView({ behavior: "smooth", }); } - }, [messages.length]); + }, [messages.length, lastMessageLength]); const handleInputChange = (e: ChangeEvent) => { setInput(e.target.value); @@ -178,57 +187,59 @@ export function Chat() { }; return ( -
- -
- {messages.length === 0 ? ( - - ) : ( - -
- {messages.map((message) => ( - - ))} -
-
- - )} -
+ +
+ +
+ {messages.length === 0 ? ( + + ) : ( + +
+ {messages.map((message) => ( + + ))} +
+
+ + )} +
-
- - +
+ + +
-
+ ); } @@ -272,7 +283,7 @@ function RenderAIResponse(props: { return (
{aiIcon} -
+
{userIcon} -
+
{aiIcon} - +
+ +
); } @@ -371,7 +384,7 @@ function StyledMarkdownRenderer(props: { }) { return (
- {getCodeSnippet(selectedAuth, platform.id).map((code) => ( - } - className="text-sm" - /> - ))} + {getCodeSnippet(selectedAuth, platform.id).map( + (code, i) => ( + } + className="text-sm" + /> + ), + )}