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) */}
+ {
+ setIsOpen(true);
+ setHasBeenOpened(true);
+ }}
+ variant="default"
+ >
+
+ Ask AI
+
+
+ {/* Popup/Modal */}
+
+ {/* Header with close button */}
+
+
+ Ask AI
+
+
+
+ setId((x) => x + 1)}
+ size="icon"
+ aria-label="Reset chat"
+ variant="ghost"
+ >
+
+
+
+
+
+
+
+
+ {/* 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) => (
+
+ ))}
+
+
+
+ )}
+
-
-
-
{
- const currentInput = input;
- setInput("");
- handleSendMessage(currentInput);
- }}
- type="submit"
- size="sm"
- variant="default"
- >
-
-
+
+
+
{
+ const currentInput = input;
+ setInput("");
+ handleSendMessage(currentInput);
+ }}
+ type="submit"
+ size="sm"
+ variant="default"
+ >
+
+
+
-
+
);
}
@@ -272,7 +283,7 @@ function RenderAIResponse(props: {
return (
{aiIcon}
-
+
{userIcon}
-
+
);
}
@@ -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"
+ />
+ ),
+ )}