Repository: thirdweb-dev/js
Pull request #7128: [Dashboard] siwa chat widget
Author: login: Yash094
Labels: name: Dashboard
description: Involves changes to the Dashboard.
Created: 2025-05-22T18:43:46Z | Merged: 2025-05-23T04:20:45Z

<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on enhancing the chat components within the application by adding new features, improving user experience, and refactoring existing code for better readability and functionality.

### Detailed summary
- Added `teamId` and `placeholder` props to various components (`Chats`, `ChatBar`, etc.).
- Introduced `FeedbackButtons` for user feedback on assistant messages.
- Implemented `CustomChatButton` for initiating chat interactions.
- Enhanced chat functionalities with improved state management.
- Refactored `ExamplePrompt` and `ExamplePromptButton` functions for cleaner syntax.
- Integrated auth token checks and redirection in layout components.
- Updated `SupportPage` to utilize new prompt structures and chat button.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
	- Introduced a new customizable chat button and modal interface labeled "Ask AI for support" with enhanced authentication and team context.
	- Added a feedback feature enabling users to submit positive or negative ratings on assistant messages within chat.
	- Launched a custom chat experience supporting authenticated sessions, message streaming, example prompts, and a sign-in prompt for unauthenticated users.
	- Added floating AI chat assistant buttons to team and project pages for quick access.

- **Improvements**
	- Updated support page chat UI and prompt labeling for clarity and personalization.
	- Added dynamic placeholder text "Ask Nebula" to chat input fields across multiple components.
	- Implemented authentication checks redirecting unauthenticated users to login on team pages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx
index f0043de7388..7c74579f324 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx
@@ -8,6 +8,7 @@ import { getContractMetadata } from "thirdweb/extensions/common";
 import { isAddress, isContractDeployed } from "thirdweb/utils";
 import { shortenIfAddress } from "utils/usedapp-external";
 import { NebulaChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
+import { examplePrompts } from "../../../../../nebula-app/(app)/data/examplePrompts";
 import {
   getAuthTokenWalletAddress,
   getUserThirdwebClient,
@@ -94,14 +95,6 @@ Users may be considering integrating the contract into their applications. Discu
 
 The following is the user's message:`;
 
-  const examplePrompts: string[] = [
-    "What does this contract do?",
-    "What permissions or roles exist in this contract?",
-    "Which functions are used the most?",
-    "Has this contract been used recently?",
-    "Who are the largest holders/users of this?",
-  ];
-
   return (
     <ContractPageLayout
       chainMetadata={chainMetadata}
@@ -124,10 +117,7 @@ The following is the user's message:`;
           chainIds: [chainId],
           wallet: accountAddress ?? undefined,
         }}
-        examplePrompts={examplePrompts.map((prompt) => ({
-          title: prompt,
-          message: prompt,
-        }))}
+        examplePrompts={examplePrompts}
       />
       {props.children}
     </ContractPageLayout>
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx
index d95e5d2032e..9530a403357 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx
@@ -1,6 +1,5 @@
 import { Button } from "@/components/ui/button";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
 import { BookOpenIcon, ChevronRightIcon } from "lucide-react";
 import type { Metadata } from "next";
 import Image from "next/image";
@@ -10,7 +9,8 @@ import contractsIcon from "../../../../../public/assets/support/contracts.png";
 import engineIcon from "../../../../../public/assets/support/engine.png";
 import miscIcon from "../../../../../public/assets/support/misc.svg";
 import connectIcon from "../../../../../public/assets/support/wallets.png";
-import { NebulaChatButton } from "../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
+import { getTeams } from "../../../../@/api/team";
+import { CustomChatButton } from "../../../nebula-app/(app)/components/CustomChat/CustomChatButton";
 import {
   getAuthToken,
   getAuthTokenWalletAddress,
@@ -118,25 +118,22 @@ const HELP_PRODUCTS = [
   },
 ] as const;
 
+export const siwaExamplePrompts = [
+  "How do I add in-app wallet with sign in with google to my react app?",
+  "How do I send a transaction in Unity?",
+  "What does this contract revert error mean?",
+  "I see thirdweb support id in my console log, can you help me?",
+  "Here is my code, can you tell me why I'm seeing this error?",
+];
+
 export default async function SupportPage() {
   const [authToken, accountAddress] = await Promise.all([
     getAuthToken(),
     getAuthTokenWalletAddress(),
   ]);
 
-  const client = getClientThirdwebClient({
-    jwt: authToken,
-    teamId: undefined,
-  });
-
-  const supportPromptPrefix =
-    "You are a Customer Success Agent at thirdweb, assisting customers with blockchain and Web3-related issues. Use the following details to craft a professional, empathetic response: ";
-  const examplePrompts = [
-    "ERC20 - Transfer Amount Exceeds Allowance",
-    "Replacement transaction underpriced / Replacement fee too low",
-    "Nonce too low: next nonce #, tx nonce #",
-    "Nonce too high",
-  ];
+  const teams = await getTeams();
+  const teamId = teams?.[0]?.id ?? undefined;
 
   return (
     <main className="flex flex-col gap-12 pb-12">
@@ -157,22 +154,16 @@ export default async function SupportPage() {
               team.
             </p>
             <div className="mt-6 flex w-full flex-col items-center gap-3">
-              <NebulaChatButton
+              <CustomChatButton
                 isLoggedIn={!!accountAddress}
                 networks="all"
                 isFloating={false}
                 pageType="support"
-                label="Ask Nebula AI for support"
-                client={client}
-                nebulaParams={{
-                  messagePrefix: supportPromptPrefix,
-                  chainIds: [],
-                  wallet: accountAddress ?? undefined,
-                }}
-                examplePrompts={examplePrompts.map((prompt) => ({
-                  title: prompt,
-                  message: prompt,
-                }))}
+                label="Ask AI for support"
+                examplePrompts={siwaExamplePrompts}
+                authToken={authToken || undefined}
+                teamId={teamId}
+                clientId={undefined}
               />
 
               <Link
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx
index 21e49665807..633ee67ebf0 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx
@@ -4,7 +4,9 @@ import { AppFooter } from "@/components/blocks/app-footer";
 import { TabPathLinks } from "@/components/ui/tabs";
 import { AnnouncementBanner } from "components/notices/AnnouncementBanner";
 import { redirect } from "next/navigation";
+import { siwaExamplePrompts } from "../../../(dashboard)/support/page";
 import { getClientThirdwebClient } from "../../../../../@/constants/thirdweb-client.client";
+import { CustomChatButton } from "../../../../nebula-app/(app)/components/CustomChat/CustomChatButton";
 import { getValidAccount } from "../../../account/settings/getAccount";
 import {
   getAuthToken,
@@ -91,6 +93,19 @@ export default async function TeamLayout(props: {
       </div>
 
       <main className="flex grow flex-col">{props.children}</main>
+      <div className="fixed right-6 bottom-6 z-50">
+        <CustomChatButton
+          clientId={undefined}
+          isLoggedIn={true}
+          networks="all"
+          isFloating={true}
+          pageType="support"
+          label="Ask AI Assistant"
+          examplePrompts={siwaExamplePrompts}
+          teamId={team.id}
+          authToken={authToken}
+        />
+      </div>
       <AppFooter />
     </div>
   );
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/layout.tsx
index 524059cef0c..aeda1c9d704 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/layout.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/layout.tsx
@@ -3,7 +3,9 @@ import { getTeams } from "@/api/team";
 import { SidebarProvider } from "@/components/ui/sidebar";
 import { AnnouncementBanner } from "components/notices/AnnouncementBanner";
 import { redirect } from "next/navigation";
+import { siwaExamplePrompts } from "../../../(dashboard)/support/page";
 import { getClientThirdwebClient } from "../../../../../@/constants/thirdweb-client.client";
+import { CustomChatButton } from "../../../../nebula-app/(app)/components/CustomChat/CustomChatButton";
 import { getValidAccount } from "../../../account/settings/getAccount";
 import {
   getAuthToken,
@@ -78,6 +80,19 @@ export default async function ProjectLayout(props: {
           {props.children}
         </ProjectSidebarLayout>
       </div>
+      <div className="fixed right-6 bottom-6 z-50">
+        <CustomChatButton
+          isLoggedIn={true}
+          networks="all"
+          isFloating={true}
+          pageType="support"
+          label="Ask AI Assistant"
+          examplePrompts={siwaExamplePrompts}
+          teamId={team.id}
+          clientId={project.publishableKey}
+          authToken={authToken}
+        />
+      </div>
       <SaveLastUsedProject projectId={project.id} teamId={team.id} />
     </SidebarProvider>
   );
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx
index eba7709d06a..ed5f8e79cd5 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx
@@ -5,6 +5,7 @@ import { ArrowRightIcon } from "lucide-react";
 import Link from "next/link";
 import { redirect } from "next/navigation";
 import { Suspense } from "react";
+import { getAuthToken } from "../../api/lib/getAuthToken";
 import { EnsureValidConnectedWalletLoginServer } from "../../components/EnsureValidConnectedWalletLogin/EnsureValidConnectedWalletLoginServer";
 import { isTeamOnboardingComplete } from "../../login/onboarding/isOnboardingRequired";
 import { SaveLastVisitedTeamPage } from "../components/last-visited-page/SaveLastVisitedPage";
@@ -18,12 +19,17 @@ export default async function RootTeamLayout(props: {
   params: Promise<{ team_slug: string }>;
 }) {
   const { team_slug } = await props.params;
+  const authToken = await getAuthToken();
   const team = await getTeamBySlug(team_slug).catch(() => null);
 
   if (!team) {
     redirect("/team");
   }
 
+  if (!authToken) {
+    redirect("/login");
+  }
+
   if (!isTeamOnboardingComplete(team)) {
     redirect(`/get-started/team/${team.slug}`);
   }
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx
index 699b69f2325..bbf73828657 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx
@@ -68,6 +68,7 @@ export function ChatBar(props: {
   isConnectingWallet: boolean;
   allowImageUpload: boolean;
   onLoginClick: undefined | (() => void);
+  placeholder: string;
 }) {
   const [message, setMessage] = useState(props.prefillMessage || "");
   const selectedChainIds = props.context?.chainIds?.map((x) => Number(x)) || [];
@@ -142,7 +143,7 @@ export function ChatBar(props: {
         <div className="p-2">
           <div className="max-h-[200px] overflow-y-auto">
             <AutoResizeTextarea
-              placeholder={"Ask Nebula"}
+              placeholder={props.placeholder}
               value={message}
               onChange={(e) => setMessage(e.target.value)}
               onKeyDown={(e) => {
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx
index 4bca9787c17..6f57961bd79 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx
@@ -339,6 +339,7 @@ export function ChatPageContent(props: {
 
               {messages.length > 0 && (
                 <Chats
+                  teamId={undefined}
                   messages={messages}
                   isChatStreaming={isChatStreaming}
                   authToken={props.authToken}
@@ -353,6 +354,7 @@ export function ChatPageContent(props: {
 
               <div className="container max-w-[800px]">
                 <ChatBar
+                  placeholder="Ask Nebula"
                   isConnectingWallet={connectionStatus === "connecting"}
                   showContextSelector={true}
                   connectedWallets={connectedWalletsMeta}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/Chatbar.stories.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/Chatbar.stories.tsx
index 582aa4d4d3a..8ed79574f64 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/Chatbar.stories.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/Chatbar.stories.tsx
@@ -166,6 +166,7 @@ function Variant(props: {
   return (
     <BadgeContainer label={props.label}>
       <ChatBar
+        placeholder={"Ask Nebula"}
         isConnectingWallet={props.isConnectingWallet || false}
         client={storybookThirdwebClient}
         abortChatStream={() => {}}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx
index cc06e959f65..0f0fac8b955 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx
@@ -227,6 +227,7 @@ function Variant(props: {
 }) {
   return (
     <Chats
+      teamId={undefined}
       enableAutoScroll={false}
       setEnableAutoScroll={() => {}}
       client={storybookThirdwebClient}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx
index 484c0e0c0d7..76ac13d49f9 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx
@@ -1,9 +1,11 @@
 import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
 import { cn } from "@/lib/utils";
 import { MarkdownRenderer } from "components/contract-components/published-contract/markdown-renderer";
-import { AlertCircleIcon } from "lucide-react";
-import { useEffect, useRef } from "react";
+import { AlertCircleIcon, ThumbsDownIcon, ThumbsUpIcon } from "lucide-react";
+import { useEffect, useRef, useState } from "react";
 import type { ThirdwebClient } from "thirdweb";
+import { Button } from "../../../../@/components/ui/button";
+import { useTrack } from "../../../../hooks/analytics/useTrack";
 import type { NebulaSwapData } from "../api/chat";
 import type { NebulaUserMessage, NebulaUserMessageContent } from "../api/types";
 import { NebulaIcon } from "../icons/NebulaIcon";
@@ -72,6 +74,7 @@ export function Chats(props: {
   enableAutoScroll: boolean;
   useSmallText?: boolean;
   sendMessage: (message: NebulaUserMessage) => void;
+  teamId: string | undefined;
 }) {
   const { messages, setEnableAutoScroll, enableAutoScroll } = props;
   const scrollAnchorRef = useRef<HTMLDivElement>(null);
@@ -153,6 +156,7 @@ export function Chats(props: {
                     nextMessage={props.messages[index + 1]}
                     authToken={props.authToken}
                     sessionId={props.sessionId}
+                    teamId={props.teamId}
                   />
                 </div>
               );
@@ -172,6 +176,7 @@ function RenderMessage(props: {
   sendMessage: (message: NebulaUserMessage) => void;
   nextMessage: ChatMessage | undefined;
   authToken: string;
+  teamId: string | undefined;
   sessionId: string | undefined;
 }) {
   const { message } = props;
@@ -224,6 +229,41 @@ function RenderMessage(props: {
     );
   }
 
+  // Feedback for assistant messages
+  if (props.message.type === "assistant") {
+    return (
+      <div className="flex flex-col gap-2">
+        <div className="flex gap-3">
+          {/* Left Icon */}
+          <div className="-translate-y-[2px] relative shrink-0">
+            <div className="flex size-9 items-center justify-center rounded-full border bg-card">
+              <NebulaIcon className="size-5 text-muted-foreground" />
+            </div>
+          </div>
+          {/* Right Message */}
+          <div className="min-w-0 grow">
+            <ScrollShadow className="rounded-lg">
+              <RenderResponse
+                message={message}
+                isMessagePending={props.isMessagePending}
+                client={props.client}
+                sendMessage={props.sendMessage}
+                nextMessage={props.nextMessage}
+                sessionId={props.sessionId}
+                authToken={props.authToken}
+              />
+            </ScrollShadow>
+            <FeedbackButtons
+              sessionId={props.sessionId}
+              authToken={props.authToken}
+              teamId={props.teamId}
+            />
+          </div>
+        </div>
+      </div>
+    );
+  }
+
   return (
     <div className="flex gap-3">
       {/* Left Icon */}
@@ -422,3 +462,81 @@ function StyledMarkdownRenderer(props: {
     />
   );
 }
+
+function FeedbackButtons({
+  sessionId,
+  authToken,
+  teamId,
+}: {
+  sessionId: string | undefined;
+  authToken: string;
+  teamId: string | undefined;
+}) {
+  const [, setFeedback] = useState<1 | -1 | null>(null);
+  const [loading, setLoading] = useState(false);
+  const [thankYou, setThankYou] = useState(false);
+  const trackEvent = useTrack();
+
+  async function sendFeedback(rating: 1 | -1) {
+    setLoading(true);
+    try {
+      trackEvent({
+        category: "siwa",
+        action: "submit-feedback",
+        rating: rating === 1 ? "good" : "bad",
+        sessionId,
+        teamId,
+      });
+      const apiUrl = process.env.NEXT_PUBLIC_SIWA_URL;
+      await fetch(`${apiUrl}/v1/chat/feedback`, {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+          Authorization: `Bearer ${authToken}`,
+          ...(teamId ? { "x-team-id": teamId } : {}),
+        },
+        body: JSON.stringify({
+          conversationId: sessionId,
+          feedbackRating: rating,
+        }),
+      });
+      setFeedback(rating);
+      setThankYou(true);
+    } catch {
+      // TODO handle error
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  if (thankYou) {
+    return (
+      <div className="mt-2 text-muted-foreground text-xs">
+        Thank you for your feedback!
+      </div>
+    );
+  }
+
+  return (
+    <div className="mt-2 flex gap-2">
+      <Button
+        className="rounded-full border p-2 hover:bg-muted-foreground/10"
+        variant="ghost"
+        onClick={() => sendFeedback(-1)}
+        disabled={loading}
+        aria-label="Thumbs down"
+      >
+        <ThumbsDownIcon className="size-4 text-red-500" />
+      </Button>
+      <Button
+        className="rounded-full border p-2 hover:bg-muted-foreground/10"
+        variant="ghost"
+        onClick={() => sendFeedback(1)}
+        disabled={loading}
+        aria-label="Thumbs up"
+      >
+        <ThumbsUpIcon className="size-4 text-green-500" />
+      </Button>
+    </div>
+  );
+}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx
new file mode 100644
index 00000000000..72801f5a5b9
--- /dev/null
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx
@@ -0,0 +1,98 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import { MessageCircleIcon, XIcon } from "lucide-react";
+import { useCallback, useRef, useState } from "react";
+import { createThirdwebClient } from "thirdweb";
+import { NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID } from "../../../../../@/constants/public-envs";
+import { useTrack } from "../../../../../hooks/analytics/useTrack";
+import CustomChatContent from "./CustomChatContent";
+
+// Create a thirdweb client for the chat functionality
+const client = createThirdwebClient({
+  clientId: NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID,
+});
+
+export function CustomChatButton(props: {
+  isLoggedIn: boolean;
+  networks: "mainnet" | "testnet" | "all" | null;
+  isFloating: boolean;
+  pageType: "chain" | "contract" | "support";
+  label: string;
+  examplePrompts: string[];
+  authToken: string | undefined;
+  teamId: string | undefined;
+  clientId: string | undefined;
+  requireLogin?: boolean;
+}) {
+  const [isOpen, setIsOpen] = useState(false);
+  const [hasBeenOpened, setHasBeenOpened] = useState(false);
+  const closeModal = useCallback(() => setIsOpen(false), []);
+  const ref = useRef<HTMLDivElement>(null);
+  const trackEvent = useTrack();
+
+  return (
+    <>
+      {/* Inline Button (not floating) */}
+      <Button
+        onClick={() => {
+          trackEvent({
+            category: "siwa",
+            action: "open-chat",
+          });
+          setIsOpen(true);
+          setHasBeenOpened(true);
+        }}
+        variant="default"
+        className="gap-2 rounded-full shadow-lg"
+      >
+        <MessageCircleIcon className="size-4" />
+        {props.label}
+      </Button>
+
+      {/* Popup/Modal */}
+      <div
+        className={cn(
+          "slide-in-from-bottom-20 zoom-in-95 fade-in-0 fixed bottom-0 left-0 z-50 flex h-[80vh] w-[100vw] animate-in flex-col overflow-hidden rounded-t-2xl border bg-background shadow-2xl duration-200 lg:right-6 lg:bottom-6 lg:left-auto lg:h-[80vh] lg:max-w-xl lg:rounded-xl",
+          !isOpen && "hidden",
+        )}
+        ref={ref}
+      >
+        {/* Header with close button */}
+        <div className="flex items-center justify-between border-b px-4 py-2">
+          <div className="flex items-center gap-2 font-semibold text-lg">
+            <MessageCircleIcon className="size-5 text-muted-foreground" />
+            {props.label}
+          </div>
+          <Button
+            variant="ghost"
+            size="icon"
+            onClick={closeModal}
+            className="h-auto w-auto p-1 text-muted-foreground"
+            aria-label="Close chat"
+          >
+            <XIcon className="size-5" />
+          </Button>
+        </div>
+        {/* Chat Content */}
+        <div className="relative flex grow flex-col overflow-hidden">
+          {hasBeenOpened && isOpen && (
+            <CustomChatContent
+              authToken={props.authToken}
+              teamId={props.teamId}
+              client={client}
+              clientId={props.clientId}
+              examplePrompts={props.examplePrompts.map((prompt) => ({
+                message: prompt,
+                title: prompt,
+              }))}
+              networks={props.networks}
+              requireLogin={props.requireLogin}
+            />
+          )}
+        </div>
+      </div>
+    </>
+  );
+}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx
new file mode 100644
index 00000000000..6eb1b3767a0
--- /dev/null
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx
@@ -0,0 +1,284 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { useTrack } from "hooks/analytics/useTrack";
+import { ArrowRightIcon } from "lucide-react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { useCallback, useState } from "react";
+import type { ThirdwebClient } from "thirdweb";
+import { useActiveWalletConnectionStatus } from "thirdweb/react";
+import type { NebulaContext } from "../../api/chat";
+import type { NebulaUserMessage } from "../../api/types";
+import type { ExamplePrompt } from "../../data/examplePrompts";
+import { NebulaIcon } from "../../icons/NebulaIcon";
+import { ChatBar } from "../ChatBar";
+import { Chats } from "../Chats";
+import type { ChatMessage } from "../Chats";
+
+export default function CustomChatContent(props: {
+  authToken: string | undefined;
+  teamId: string | undefined;
+  clientId: string | undefined;
+  client: ThirdwebClient;
+  examplePrompts: ExamplePrompt[];
+  networks: NebulaContext["networks"];
+  requireLogin?: boolean;
+}) {
+  if (props.requireLogin !== false && !props.authToken) {
+    return <LoggedOutStateChatContent />;
+  }
+
+  return (
+    <CustomChatContentLoggedIn
+      networks={props.networks}
+      teamId={props.teamId}
+      clientId={props.clientId}
+      authToken={props.authToken || ""}
+      client={props.client}
+      examplePrompts={props.examplePrompts}
+    />
+  );
+}
+
+function CustomChatContentLoggedIn(props: {
+  authToken: string;
+  teamId: string | undefined;
+  clientId: string | undefined;
+  client: ThirdwebClient;
+  examplePrompts: ExamplePrompt[];
+  networks: NebulaContext["networks"];
+}) {
+  const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false);
+  const [messages, setMessages] = useState<Array<ChatMessage>>([]);
+  // sessionId is initially undefined, will be set to conversationId from API after first response
+  const [sessionId, setSessionId] = useState<string | undefined>(undefined);
+  const [chatAbortController, setChatAbortController] = useState<
+    AbortController | undefined
+  >();
+  const trackEvent = useTrack();
+  const [isChatStreaming, setIsChatStreaming] = useState(false);
+  const [enableAutoScroll, setEnableAutoScroll] = useState(false);
+  const connectionStatus = useActiveWalletConnectionStatus();
+
+  const handleSendMessage = useCallback(
+    async (userMessage: NebulaUserMessage) => {
+      const abortController = new AbortController();
+      setUserHasSubmittedMessage(true);
+      setIsChatStreaming(true);
+      setEnableAutoScroll(true);
+
+      const textMessage = userMessage.content.find((x) => x.type === "text");
+
+      trackEvent({
+        category: "siwa",
+        action: "send-message",
+        message: textMessage?.text,
+        sessionId: sessionId,
+      });
+
+      setMessages((prev) => [
+        ...prev,
+        {
+          type: "user",
+          content: userMessage.content,
+        },
+        // instant loading indicator feedback to user
+        {
+          type: "presence",
+          texts: [],
+        },
+      ]);
+
+      // if this is first message, set the message prefix
+      // deep clone `userMessage` to avoid mutating the original message, its a pretty small object so JSON.parse is fine
+      const messageToSend = JSON.parse(
+        JSON.stringify(userMessage),
+      ) as NebulaUserMessage;
+
+      try {
+        setChatAbortController(abortController);
+        // --- Custom API call ---
+        const payload = {
+          message:
+            messageToSend.content.find((x) => x.type === "text")?.text ?? "",
+          conversationId: sessionId,
+        };
+        const apiUrl = process.env.NEXT_PUBLIC_SIWA_URL;
+        const response = await fetch(`${apiUrl}/v1/chat`, {
+          method: "POST",
+          headers: {
+            "Content-Type": "application/json",
+            Authorization: `Bearer ${props.authToken}`,
+            ...(props.teamId ? { "x-team-id": props.teamId } : {}),
+            ...(props.clientId ? { "x-client-id": props.clientId } : {}),
+          },
+          body: JSON.stringify(payload),
+          signal: abortController.signal,
+        });
+        const data = await response.json();
+        // If the response contains a conversationId, set it as the sessionId for future messages
+        if (data.conversationId && data.conversationId !== sessionId) {
+          setSessionId(data.conversationId);
+        }
+        setMessages((prev) => [
+          ...prev.slice(0, -1), // remove presence indicator
+          {
+            type: "assistant",
+            request_id: undefined,
+            text: data.data,
+          },
+        ]);
+      } catch (error) {
+        if (abortController.signal.aborted) {
+          return;
+        }
+        setMessages((prev) => [
+          ...prev.slice(0, -1),
+          {
+            type: "assistant",
+            request_id: undefined,
+            text: `Sorry, something went wrong. ${error instanceof Error ? error.message : "Unknown error"}`,
+          },
+        ]);
+      } finally {
+        setIsChatStreaming(false);
+        setEnableAutoScroll(false);
+      }
+    },
+    [props.authToken, props.clientId, props.teamId, sessionId, trackEvent],
+  );
+
+  const showEmptyState = !userHasSubmittedMessage && messages.length === 0;
+  return (
+    <div className="flex grow flex-col overflow-hidden">
+      {showEmptyState ? (
+        <EmptyStateChatPageContent
+          sendMessage={handleSendMessage}
+          examplePrompts={props.examplePrompts}
+        />
+      ) : (
+        <Chats
+          teamId={props.teamId}
+          messages={messages}
+          isChatStreaming={isChatStreaming}
+          authToken={props.authToken}
+          sessionId={sessionId}
+          className="min-w-0 pb-10"
+          client={props.client}
+          enableAutoScroll={enableAutoScroll}
+          setEnableAutoScroll={setEnableAutoScroll}
+          useSmallText
+          sendMessage={handleSendMessage}
+        />
+      )}
+      <ChatBar
+        placeholder={"Ask AI Assistant"}
+        onLoginClick={undefined}
+        client={props.client}
+        isConnectingWallet={connectionStatus === "connecting"}
+        context={undefined}
+        setContext={() => {}}
+        showContextSelector={false}
+        connectedWallets={[]}
+        setActiveWallet={() => {}}
+        abortChatStream={() => {
+          chatAbortController?.abort();
+          setChatAbortController(undefined);
+          setIsChatStreaming(false);
+          // if last message is presence, remove it
+          if (messages[messages.length - 1]?.type === "presence") {
+            setMessages((prev) => prev.slice(0, -1));
+          }
+        }}
+        isChatStreaming={isChatStreaming}
+        prefillMessage={undefined}
+        sendMessage={handleSendMessage}
+        className="rounded-none border-x-0 border-b-0"
+        allowImageUpload={false}
+      />
+    </div>
+  );
+}
+
+function LoggedOutStateChatContent() {
+  const pathname = usePathname();
+  return (
+    <div className="flex grow flex-col items-center justify-center p-4">
+      <div className="mb-4 flex justify-center">
+        <div className="rounded-full border p-1">
+          <div className="rounded-full border bg-card p-2">
+            <NebulaIcon className="size-7 text-muted-foreground" />
+          </div>
+        </div>
+      </div>
+
+      <h1 className="px-4 text-center font-semibold text-3xl tracking-tight md:text-4xl">
+        How can I help you <br className="max-sm:hidden" />
+        today?
+      </h1>
+
+      <div className="h-3" />
+      <p className="text-base text-muted-foreground">
+        Sign in to use AI Assistant
+      </p>
+      <div className="h-5" />
+
+      <Button asChild>
+        <Link
+          href={`/login?next=${encodeURIComponent(pathname)}`}
+          className="w-full max-w-96 gap-2"
+        >
+          Sign in
+          <ArrowRightIcon className="size-4" />
+        </Link>
+      </Button>
+    </div>
+  );
+}
+
+function EmptyStateChatPageContent(props: {
+  sendMessage: (message: NebulaUserMessage) => void;
+  examplePrompts: { title: string; message: string }[];
+}) {
+  return (
+    <div className="flex flex-1 flex-col items-center justify-center overflow-auto p-4 ">
+      <div className="mb-4 flex justify-center">
+        <div className="rounded-full border-[1.5px] border-nebula-pink-foreground/20 bg-[hsl(var(--nebula-pink-foreground)/5%)] p-1">
+          <div className="rounded-full border-[1.5px] border-nebula-pink-foreground/40 bg-[hsl(var(--nebula-pink-foreground)/5%)] p-2">
+            <NebulaIcon className="size-7 text-nebula-pink-foreground" />
+          </div>
+        </div>
+      </div>
+
+      <h1 className="px-4 text-center font-semibold text-3xl tracking-tight md:text-4xl">
+        How can I help you <br className="max-sm:hidden" />
+        today?
+      </h1>
+
+      <div className="h-6" />
+      <div className="flex max-w-lg flex-col flex-wrap justify-center gap-2.5">
+        {props.examplePrompts.map((prompt) => (
+          <Button
+            key={prompt.title}
+            variant="outline"
+            size="sm"
+            onClick={() =>
+              props.sendMessage({
+                role: "user",
+                content: [
+                  {
+                    type: "text",
+                    text: prompt.message,
+                  },
+                ],
+              })
+            }
+            disabled={false}
+          >
+            {prompt.title}
+          </Button>
+        ))}
+      </div>
+    </div>
+  );
+}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx
index 011c803c80f..d23f9e02ba3 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx
@@ -43,6 +43,7 @@ export function EmptyStateChatPageContent(props: {
         <div className="h-5" />
         <div className="mx-auto max-w-[600px]">
           <ChatBar
+            placeholder={"Ask Nebula"}
             onLoginClick={props.onLoginClick}
             isConnectingWallet={props.isConnectingWallet}
             showContextSelector={true}
@@ -82,10 +83,7 @@ export function EmptyStateChatPageContent(props: {
   );
 }
 
-function ExamplePrompt(props: {
-  label: string;
-  onClick: () => void;
-}) {
+function ExamplePrompt(props: { label: string; onClick: () => void }) {
   return (
     <Button
       variant="outline"
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx
index 804fb40f52b..64c5496e781 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx
@@ -202,6 +202,7 @@ function FloatingChatContentLoggedIn(props: {
         />
       ) : (
         <Chats
+          teamId={undefined}
           messages={messages}
           isChatStreaming={isChatStreaming}
           authToken={props.authToken}
@@ -215,6 +216,7 @@ function FloatingChatContentLoggedIn(props: {
         />
       )}
       <ChatBar
+        placeholder={"Ask Nebula"}
         onLoginClick={undefined}
         client={props.client}
         isConnectingWallet={connectionStatus === "connecting"}
@@ -332,10 +334,7 @@ function EmptyStateChatPageContent(props: {
   );
 }
 
-function ExamplePromptButton(props: {
-  label: string;
-  onClick: () => void;
-}) {
+function ExamplePromptButton(props: { label: string; onClick: () => void }) {
   return (
     <Button
       variant="outline"


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>🧹 Nitpick comments (10)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx (2)</summary><blockquote>

`13-13`: **Prefer path-aliases over deeply-nested relative paths**

The relative import travels 6 directory levels upward/downward.  
That becomes fragile the moment one of the segment folders is renamed or moved and is also hard to read.

```diff
-import { CustomChatButton } from "../../../nebula-app/(app)/components/CustomChat/CustomChatButton";
+import { CustomChatButton } from "@/nebula-app/components/CustomChat/CustomChatButton";
```

Consider adding a `paths` alias (e.g. `@/nebula-app`) in `tsconfig.json` so every file can import this component with a single, stable alias.  
This improves maintainability and reduces merge-conflict headaches.

---

`154-156`: **Branding inconsistency – “Nebula AI” vs “Siwa AI”**

The heading still says “Nebula AI”, while the new button and PR description refer to “Siwa AI”.

Keeping both names in the same view is confusing for users and weakens the re-branding attempt.

```diff
-Get instant answers with Nebula AI, our onchain support assistant.
+Get instant answers with Siwa AI, our on-chain support assistant.
```

</blockquote></details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx (1)</summary><blockquote>

`227-260`: **Duplicate rendering path for assistant messages**

A dedicated branch for `message.type === "assistant"` is added here, but the generic branch starting at line 262 still includes assistant-specific handling (icon, feedback, etc.).  
While the early `return` prevents double-rendering, the duplicate code increases maintenance cost and risks the two branches drifting apart.

Consider extracting shared pieces into a helper component or keeping a single rendering path with feature flags.

</blockquote></details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx (3)</summary><blockquote>

`4-5`: **Unused import**

`ArrowUpRightIcon` is imported but never used – remove to keep the bundle lean.

```diff
-import { ArrowRightIcon, ArrowUpRightIcon } from "lucide-react";
+import { ArrowRightIcon } from "lucide-react";
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 4-4: This import is unused.

Unused imports might be the result of an incomplete refactoring.
Safe fix: Remove the unused import.


(lint/correctness/noUnusedImports)

</details>

</details>

---

`92-98`: **`useCallback` dependency list contains an unused item**

`contextFilters` never appears inside the callback body but is listed as a dependency, causing unnecessary re-creations.

```diff
-  [
-    props.authToken,
-    contextFilters,
+  [
+    props.authToken,
```

Double-check if you really need `contextFilters` here; otherwise remove it to satisfy the linter and save renders.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 92-92: This hook specifies more dependencies than necessary: contextFilters

This dependency can be removed from the list.


(lint/correctness/useExhaustiveDependencies)

</details>

</details>

---

`176-180`: **Unused caught error**

`error` is caught but not referenced – prefix with `_` or log it for debugging.

```diff
-} catch (error) {
+} catch (_error) {
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 177-177: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend error with an underscore.


(lint/correctness/noUnusedVariables)

</details>

</details>

</blockquote></details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx (4)</summary><blockquote>

`29-31`: **Remove or implement commented-out code**

There's a commented section about adding an outside click handler. Either implement this functionality or remove the comment to keep the code clean.

If you decide to implement it, you could add:

```tsx
useEffect(() => {
  function handleClickOutside(event: MouseEvent) {
    if (ref.current && !ref.current.contains(event.target as Node)) {
      closeModal();
    }
  }
  
  document.addEventListener("mousedown", handleClickOutside);
  return () => {
    document.removeEventListener("mousedown", handleClickOutside);
  };
}, [closeModal]);
```

---

`62-62`: **Sort CSS classes for better readability**

The CSS classes in this line should be sorted for consistency and better maintainability.

```diff
- <div className="font-semibold text-lg flex items-center gap-2">
+ <div className="flex items-center gap-2 text-lg font-semibold">
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 62-62: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

</details>

</details>

---

`54-57`: **Consider extracting complex className strings**

The className string is quite complex and might be difficult to maintain. Consider extracting it to a separate variable or using a more structured approach.

```diff
+ const modalClasses = "slide-in-from-bottom-20 zoom-in-95 fade-in-0 fixed bottom-0 left-0 z-50 flex h-[80vh] w-[100vw] animate-in flex-col overflow-hidden rounded-t-2xl border bg-background shadow-2xl duration-200 lg:right-6 lg:bottom-6 lg:left-auto lg:h-[80vh] lg:max-w-xl lg:rounded-xl";
  <div
    className={cn(
-     "slide-in-from-bottom-20 zoom-in-95 fade-in-0 fixed bottom-0 left-0 z-50 flex h-[80vh] w-[100vw] animate-in flex-col overflow-hidden rounded-t-2xl border bg-background shadow-2xl duration-200 lg:right-6 lg:bottom-6 lg:left-auto lg:h-[80vh] lg:max-w-xl lg:rounded-xl",
+     modalClasses,
      !isOpen && "hidden"
    )}
```

---

`40-50`: **Consider accessibility improvements for the chat button**

The button would benefit from additional accessibility attributes to improve user experience for screen reader users.

```diff
  <Button
    onClick={() => {
      setIsOpen(true);
      setHasBeenOpened(true);
    }}
    variant="default"
    className="gap-2 rounded-full shadow-lg"
+   aria-label="Open chat"
+   aria-expanded={isOpen}
  >
    <MessageCircleIcon className="size-4" />
    {props.label}
  </Button>
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 526b6b54cfb4a28f4dcc16e7e3ae39d73a6eff40 and 1e918d069fe4531cff32191cc6c0286573a4eb6e.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx` (4 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx` (3 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx` (1 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx (1)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx (1)</summary>

* `CustomChatButton` (11-93)

</details>

</blockquote></details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx (1)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/icons/NebulaIcon.tsx (1)</summary>

* `NebulaIcon` (1-24)

</details>

</blockquote></details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx (3)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/data/examplePrompts.ts (1)</summary>

* `ExamplePrompt` (1-5)

</details>
<details>
<summary>apps/dashboard/src/@/components/ui/button.tsx (1)</summary>

* `Button` (67-67)

</details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx (1)</summary>

* `CustomChatContent` (22-51)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx</summary>

[error] 487-487: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 491-491: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 493-493: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 501-501: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 461-461: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend feedback with an underscore.


(lint/correctness/noUnusedVariables)

---

[error] 479-479: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend e with an underscore.


(lint/correctness/noUnusedVariables)

---

[error] 492-497: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a `form` element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

---

[error] 500-505: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a `form` element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

</details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx</summary>

[error] 144-144: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

---

[error] 301-301: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

---

[error] 4-4: This import is unused.

Unused imports might be the result of an incomplete refactoring.
Safe fix: Remove the unused import.


(lint/correctness/noUnusedImports)

---

[error] 92-92: This hook specifies more dependencies than necessary: contextFilters

This dependency can be removed from the list.


(lint/correctness/useExhaustiveDependencies)

---

[error] 177-177: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend error with an underscore.


(lint/correctness/noUnusedVariables)

</details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx</summary>

[error] 18-18: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

---

[error] 62-62: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 25-25: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend setIsDismissed with an underscore.


(lint/correctness/noUnusedVariables)

</details>

</details>
<details>
<summary>🪛 Gitleaks (8.26.0)</summary>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx</summary>

158-158: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: Unit Tests
* GitHub Check: Size
* GitHub Check: Analyze (javascript)

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx (1)</summary>

`159-176`: **Optional: remove unused prop when value is always empty**

`supportPromptPrefix` is now an empty string and passed through `customApiParams.messagePrefix`.  
If the prefix is deliberately disabled everywhere, consider dropping the prop altogether to avoid sending redundant data over the wire.

No action required if you expect this to be repopulated soon.

</details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx (2)</summary>

`33-35`: **Potentially unreachable conditional rendering**

This conditional check on `isDismissed` will never be true since `setIsDismissed` is never called, making this code block effectively unreachable.

Verify if this dismiss functionality is intentional. If so, implement a way to set the `isDismissed` state to true.

---

`11-93`: **Overall implementation looks good with minor improvements needed**

The component implements a chat button and popup modal with good separation of concerns. The props are passed correctly to the `CustomChatContent` component, and the UI states are managed appropriately. Fix the issues with the `any` type, unused state variable, and consider the accessibility improvements suggested.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 18-18: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

---

[error] 62-62: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 25-25: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend setIsDismissed with an underscore.


(lint/correctness/noUnusedVariables)

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T18:47:12Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (5)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx (1)</summary><blockquote>

`460-517`: **Fix minor issues in FeedbackButtons component**

There are several minor issues that should be addressed:

1. The `feedback` state is set but never read
2. Buttons miss an explicit `type="button"` attribute - default "submit" can cause unintended form submissions
3. The error variable `e` in the catch block is unused
4. Class lists are unsorted (style-lint)


Apply this diff to fix the issues:

```diff
-const [feedback, setFeedback] = useState<"good" | "bad" | null>(null);
+// State to track user feedback
+const [loading, setLoading] = useState(false);
+const [thankYou, setThankYou] = useState(false);

 async function sendFeedback(rating: "good" | "bad") {
   setLoading(true);
   try {
     await fetch("https://siwa-api.thirdweb-dev.com/v1/feedback", {
       method: "POST",
       headers: { "Content-Type": "application/json" },
       body: JSON.stringify({
         conversationId: sessionId,
         message: messageText,
         rating,
       }),
     });
-    setFeedback(rating);
     setThankYou(true);
-  } catch (e) {
+  } catch (_error) {
     // handle error
   } finally {
     setLoading(false);
   }
 }

 if (thankYou) {
   return (
-    <div className="mt-2 text-xs text-muted-foreground">
+    <div className="text-muted-foreground text-xs mt-2">
       Thank you for your feedback!
     </div>
   );
 }

 return (
-  <div className="flex gap-2 mt-2">
+  <div className="flex gap-2 mt-2">
     <button
-      className="p-1 rounded-full border hover:bg-muted-foreground/10"
+      className="border hover:bg-muted-foreground/10 p-1 rounded-full"
+      type="button"
       onClick={() => sendFeedback("good")}
       disabled={loading}
       aria-label="Thumbs up"
     >
       <ThumbsUpIcon className="size-4 text-green-500" />
     </button>
     <button
-      className="p-1 rounded-full border hover:bg-muted-foreground/10"
+      className="border hover:bg-muted-foreground/10 p-1 rounded-full"
+      type="button"
       onClick={() => sendFeedback("bad")}
       disabled={loading}
       aria-label="Thumbs down"
     >
       <ThumbsDownIcon className="size-4 text-red-500" />
     </button>
   </div>
 );
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 491-491: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 498-498: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 500-500: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 508-508: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 464-464: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend feedback with an underscore.


(lint/correctness/noUnusedVariables)

---

[error] 482-482: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend e with an underscore.


(lint/correctness/noUnusedVariables)

---

[error] 499-504: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a `form` element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

---

[error] 507-512: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a `form` element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

</details>

</details>

</blockquote></details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx (2)</summary><blockquote>

`144-147`: **Avoid `any` – give `payload` a type**

Using `any` drops all type-safety right before a network request.

Introduce an interface:


```diff
+interface SiwaPayload {
+  message: string;
+  authToken: string;
+  conversationId: string;
+}

-const payload: any = {
+const payload: SiwaPayload = {
  message:
    messageToSend.content.find((x) => x.type === "text")?.text ?? "",
  authToken: props.authToken,
  conversationId: sessionId ?? "25000000005",
};
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 144-144: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

</details>

</details>

---

`158-160`: _⚠️ Potential issue_

**Hard-coded CORS-sh proxy key leaked to the client**

The header
```ts
"x-cors-api-key": "temp_3f2b6d4409a86dc7f4b7c45840dbd8e9"
```
exposes what looks like an API key to every site visitor (this file is shipped to the browser).  
Anyone can now abuse the proxy quota.



Move this key to a server-side environment variable (e.g. `process.env.CORS_PROXY_KEY`) and inject it only in API routes or get rid of the proxy entirely by enabling CORS on the upstream service.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Gitleaks (8.26.0)</summary>

159-159: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

</details>

</details>

</blockquote></details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx (2)</summary><blockquote>

`18-18`: **Avoid using the `any` type in TypeScript**

The `customApiParams` prop is typed as `any`, which bypasses TypeScript's type checking and can lead to runtime errors. Based on how this prop is used in `CustomChatContent`, you should use a more specific type.


```diff
-  customApiParams: any;
+  customApiParams: {
+    messagePrefix: string;
+    chainIds: number[];
+    wallet: string | undefined;
+  } | undefined;
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 18-18: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

</details>

</details>

---

`23-35`: **Unused state variable `setIsDismissed`**

The `setIsDismissed` state setter is defined but never used in the component, while the `isDismissed` state is checked for conditional rendering. This suggests incomplete implementation.


Either implement the dismiss functionality or remove the unused state if it's not needed:

```diff
- const [isDismissed, setIsDismissed] = useState(false);
+ // If dismiss functionality is planned but not yet implemented:
+ // TODO: Implement dismiss functionality
+ const [isDismissed] = useState(false);
```

If the dismiss functionality is planned for future implementation, consider adding a TODO comment or implementing it now.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 25-25: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend setIsDismissed with an underscore.


(lint/correctness/noUnusedVariables)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx (2)</summary><blockquote>

`92-204`: **Improve error handling and remove unnecessary dependency**

The `contextFilters` dependency is specified in the `useCallback` but is not used within the callback function. Also, the error handling could be improved to provide more specific error messages.


```diff
const handleSendMessage = useCallback(
  async (userMessage: NebulaUserMessage) => {
    // function implementation...
  },
  [
    props.authToken,
-   contextFilters,
    sessionId,
    props.customApiParams?.messagePrefix,
    userHasSubmittedMessage,
    trackEvent,
    props.pageType,
  ],
);
```

Also, improve error handling:

```diff
} catch (error) {
  if (abortController.signal.aborted) {
    return;
  }
+ 
+ const errorMessage = error instanceof Error 
+   ? `Error: ${error.message}` 
+   : "Sorry, something went wrong.";
+ 
  setMessages((prev) => [
    ...prev.slice(0, -1),
    {
      type: "assistant",
      request_id: undefined,
-     text: "Sorry, something went wrong.",
+     text: errorMessage,
    },
  ]);
}
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 144-144: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

---

[error] 92-92: This hook specifies more dependencies than necessary: contextFilters

This dependency can be removed from the list.


(lint/correctness/useExhaustiveDependencies)

---

[error] 178-178: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend error with an underscore.


(lint/correctness/noUnusedVariables)

</details>
<details>
<summary>🪛 Gitleaks (8.26.0)</summary>

159-159: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

</details>

</details>

---

`154-164`: **Consider implementing a retry mechanism for API calls**

The current implementation doesn't handle temporary network failures or server issues. Adding a retry mechanism would improve reliability.


```diff
+async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3): Promise<Response> {
+  let retries = 0;
+  while (retries < maxRetries) {
+    try {
+      return await fetch(url, options);
+    } catch (error) {
+      retries++;
+      if (retries >= maxRetries) throw error;
+      // Exponential backoff
+      await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, retries - 1)));
+    }
+  }
+  throw new Error("Max retries exceeded");
+}

// Replace the fetch call with:
-const response = await fetch(
+const response = await fetchWithRetry(
  "https://proxy.cors.sh/https://siwa-slack-bot-u8ne.chainsaw-dev.zeet.app/siwa",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-cors-api-key": "temp_3f2b6d4409a86dc7f4b7c45840dbd8e9", // replace with your cors.sh API key
    },
    body: JSON.stringify(payload),
    signal: abortController.signal,
  },
);
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Gitleaks (8.26.0)</summary>

159-159: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

</details>

</details>

</blockquote></details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx (2)</summary><blockquote>

`62-65`: **Sort CSS classes for consistency**

The CSS classes in this line should be sorted for consistency with project standards.


```diff
-<div className="font-semibold text-lg flex items-center gap-2">
+<div className="flex items-center gap-2 font-semibold text-lg">
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 62-62: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

</details>

</details>

---

`37-93`: **Consider adding keyboard navigation support**

The chat modal lacks keyboard navigation support (Escape to close, focus trapping). This would improve accessibility.


Consider adding:
1. Event listener for Escape key to close modal
2. Focus trap to keep keyboard focus within the modal
3. Return focus to button when modal closes

```diff
+import { useEffect } from "react";
+import FocusTrap from "focus-trap-react"; // You may need to install this

// Inside the component:
+useEffect(() => {
+  const handleKeyDown = (e: KeyboardEvent) => {
+    if (e.key === "Escape" && isOpen) {
+      closeModal();
+    }
+  };
+  
+  window.addEventListener("keydown", handleKeyDown);
+  return () => window.removeEventListener("keydown", handleKeyDown);
+}, [isOpen, closeModal]);

// Wrap modal content with FocusTrap
+{isOpen && (
+  <FocusTrap focusTrapOptions={{ initialFocus: false }}>
     <div className={cn(...)} ref={ref}>
       {/* Modal content */}
     </div>
+  </FocusTrap>
+)}
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 62-62: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1e918d069fe4531cff32191cc6c0286573a4eb6e and 4994fbe156b8cd520e334fbb834472fd17cf7716.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx` (4 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx` (3 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx` (1 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx (1)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/icons/NebulaIcon.tsx (1)</summary>

* `NebulaIcon` (1-24)

</details>

</blockquote></details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx (3)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/data/examplePrompts.ts (1)</summary>

* `ExamplePrompt` (1-5)

</details>
<details>
<summary>apps/dashboard/src/@/components/ui/button.tsx (1)</summary>

* `Button` (67-67)

</details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx (1)</summary>

* `CustomChatContent` (22-51)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx</summary>

[error] 144-144: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

---

[error] 302-302: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

---

[error] 92-92: This hook specifies more dependencies than necessary: contextFilters

This dependency can be removed from the list.


(lint/correctness/useExhaustiveDependencies)

---

[error] 178-178: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend error with an underscore.


(lint/correctness/noUnusedVariables)

</details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx</summary>

[error] 491-491: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 498-498: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 500-500: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 508-508: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 464-464: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend feedback with an underscore.


(lint/correctness/noUnusedVariables)

---

[error] 482-482: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend e with an underscore.


(lint/correctness/noUnusedVariables)

---

[error] 499-504: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a `form` element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

---

[error] 507-512: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a `form` element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

</details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx</summary>

[error] 18-18: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

---

[error] 62-62: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 25-25: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend setIsDismissed with an underscore.


(lint/correctness/noUnusedVariables)

</details>

</details>
<details>
<summary>🪛 Gitleaks (8.26.0)</summary>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx</summary>

159-159: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx (1)</summary>

`227-259`: **Positive: Added dedicated feedback UI for assistant messages**

The new dedicated rendering for assistant messages with integrated feedback buttons creates a cleaner user experience and will help gather useful feedback.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T18:50:38Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx (2)</summary><blockquote>

`144-149`: **Fix the `any` type for payload object**

Using `any` drops all type safety right before a network request. Define a proper interface for the payload object.

```diff
-const payload: any = {
+interface SiwaPayload {
+  message: string;
+  authToken: string;
+  conversationId: string;
+}
+
+const payload: SiwaPayload = {
  message:
    messageToSend.content.find((x) => x.type === "text")?.text ?? "",
  authToken: props.authToken,
  conversationId: "25000000005",
};
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 144-144: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

</details>

</details>

---

`300-302`: **Fix `any` type in EmptyStateChatPageContent props**

The `sendMessage` prop in EmptyStateChatPageContent is typed as `any`, which reduces type safety.

```diff
function EmptyStateChatPageContent(props: {
-  sendMessage: (message: any) => void;
+  sendMessage: (message: NebulaUserMessage) => void;
  examplePrompts: { title: string; message: string }[];
}) {
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 301-301: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx (4)</summary><blockquote>

`14-14`: **Remove commented-out import with "REMOVE" comment**

The code contains a commented-out import statement with an explicit "REMOVE" comment. It's best to clean up this code to prevent confusion and maintain a clean codebase.

```diff
-// import { createSession } from "../../api/session"; // REMOVE
```

---

`148-148`: **Avoid hardcoding the default conversation ID**

The conversation ID "25000000005" is hardcoded in the payload. Consider extracting this to a constant or configuration to improve maintainability.

```diff
+const DEFAULT_CONVERSATION_ID = "25000000005";
+
 const payload: any = {
   message:
     messageToSend.content.find((x) => x.type === "text")?.text ?? "",
   authToken: props.authToken,
-  conversationId: "25000000005",
+  conversationId: DEFAULT_CONVERSATION_ID,
 };
```

---

`153-163`: **Consider extracting API URL to a configuration variable**

The API endpoint URL is hardcoded in the fetch call. Extract it to a constant or environment variable for better maintainability and flexibility across environments.

```diff
+const SIWA_API_ENDPOINT = "https://siwa-api.thirdweb-dev.com/api/chat";
+
 const response = await fetch(
-  "https://siwa-api.thirdweb-dev.com/api/chat",
+  SIWA_API_ENDPOINT,
   {
     method: "POST",
     headers: {
       "Content-Type": "application/json",
     },
     body: JSON.stringify(payload),
     signal: abortController.signal,
   },
 );
```

---

`177-178`: **Utilize the error variable in catch block**

The error variable is caught but not used. Consider logging the error or using it to provide more specific error messages to users.

```diff
-} catch (error) {
+} catch (error) {
+  console.error("Chat API error:", error);
   if (abortController.signal.aborted) {
     return;
   }
   setMessages((prev) => [
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 177-177: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend error with an underscore.


(lint/correctness/noUnusedVariables)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4994fbe156b8cd520e334fbb834472fd17cf7716 and c8843aedbce5f1e4bf824ad67bb8ae246f6a8204.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx</summary>

[error] 144-144: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

---

[error] 301-301: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

---

[error] 92-92: This hook specifies more dependencies than necessary: contextFilters

This dependency can be removed from the list.


(lint/correctness/useExhaustiveDependencies)

---

[error] 177-177: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend error with an underscore.


(lint/correctness/noUnusedVariables)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: Size
* GitHub Check: Analyze (javascript)

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx (2)</summary>

`206-226`: **The code elegantly handles conditional rendering**

The component properly handles different states: showing an empty state when no messages have been submitted, and the chat interface otherwise. The props passed to child components are comprehensive and well-structured.

---

`264-298`: **Well-designed login state with clear user guidance**

The logged-out state provides a clear call-to-action for users to sign in, with appropriate styling and a useful link that preserves the current path for redirection after login.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T18:54:43Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between c8843aedbce5f1e4bf824ad67bb8ae246f6a8204 and dca105cb8c4c8e52112cbcbbc0e57eac005ab2a5.

</details>

<details>
<summary>📒 Files selected for processing (13)</summary>

* `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx` (2 hunks)
* `apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx` (3 hunks)
* `apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx` (2 hunks)
* `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/layout.tsx` (2 hunks)
* `apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx` (2 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx` (2 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx` (1 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/Chatbar.stories.tsx` (1 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx` (6 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx` (1 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx` (1 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx` (2 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx` (2 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (3)</summary>

* apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx
* apps/dashboard/src/app/nebula-app/(app)/components/Chatbar.stories.tsx
* apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx
* apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx (1)</summary>

```
Learnt from: jnsdls
PR: thirdweb-dev/js#6929
File: apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/insight/webhooks/page.tsx:14-19
Timestamp: 2025-05-21T05:17:31.283Z
Learning: In Next.js server components, the `params` object can sometimes be a Promise that needs to be awaited, despite type annotations suggesting otherwise. In apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/insight/webhooks/page.tsx, it's necessary to await the params object before accessing its properties.
```

</details>

</details><details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx (2)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/(app)/api/lib/getAuthToken.ts (1)</summary>

* `getAuthToken` (6-14)

</details>
<details>
<summary>apps/dashboard/src/@/api/team.ts (1)</summary>

* `getTeamBySlug` (11-30)

</details>

</blockquote></details>
<details>
<summary>apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/layout.tsx (2)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx (1)</summary>

* `CustomChatButton` (16-92)

</details>
<details>
<summary>apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx (1)</summary>

* `siwaExamplePrompts` (121-126)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: Size
* GitHub Check: Analyze (javascript)

</details>

<details>
<summary>🔇 Additional comments (27)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx (2)</summary>

`71-71`: **Good addition of placeholder prop for better UI customization.**

Adding the `placeholder` prop to the ChatBar component enhances UI flexibility and enables consistent placeholder text across different chat interfaces in the application.

---

`146-146`: **Properly implemented placeholder text in the textarea.**

The placeholder is correctly applied to the AutoResizeTextarea component, completing the implementation of the new prop.

</details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx (2)</summary>

`218-218`: **Correctly implemented the required placeholder prop.**

The placeholder value "Ask Nebula" has been added to comply with the updated ChatBar component's requirements, maintaining consistency with other chat interfaces.

---

`336-336`: **Good simplification of type annotation.**

The type annotation for ExamplePromptButton props has been simplified from a multiline object type to a cleaner inline type, improving code readability without changing functionality.

</details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx (2)</summary>

`46-46`: **Correctly implemented the required placeholder prop.**

The placeholder value "Ask Nebula" has been added to comply with the updated ChatBar component's requirements, maintaining consistency with other chat interfaces.

---

`86-86`: **Good simplification of type annotation.**

The type annotation for ExamplePrompt props has been simplified from a multiline object type to a cleaner inline type, improving code readability without changing functionality.

</details>
<details>
<summary>apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx (3)</summary>

`8-8`: **Good addition of authentication token retrieval.**

The import of getAuthToken enables proper authentication verification in the team layout.

---

`22-22`: **Authentication token correctly retrieved.**

The auth token is properly fetched using the imported getAuthToken function, which retrieves it from cookies.

---

`29-31`: **Appropriate authentication check added.**

This addition properly redirects unauthenticated users to the login page, enhancing security by ensuring only authenticated users can access team pages. The check is correctly positioned after team existence verification but before the onboarding completion check.

</details>
<details>
<summary>apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx (2)</summary>

`8-9`: **Good addition of necessary imports.**

Clean import statements for the new chat functionality components.

---

`96-112`: **LGTM! Well-implemented chat button integration.**

The CustomChatButton is properly positioned in a fixed container at the bottom-right corner. All required props are correctly passed, including authentication token, team context, and appropriate configuration.

</details>
<details>
<summary>apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/layout.tsx (2)</summary>

`15-16`: **Good import declarations for chat functionality.**

Clean import of the CustomChatButton component and reuse of example prompts from the support page.

---

`83-95`: **Well-implemented project-context chat button.**

The CustomChatButton is correctly positioned and configured with project-specific context, including teamId and clientId (from project.publishableKey). This maintains consistency with the team layout while adding project-specific context.

</details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx (7)</summary>

`11-14`: **Well-implemented thirdweb client initialization.**

The client is correctly created using the public client ID constant.

---

`16-27`: **Well-designed component props interface.**

The props interface is clear, type-safe, and comprehensive, covering all necessary configuration options for the chat button.

---

`28-31`: **Good state management.**

State variables and the close modal callback are well-defined and appropriately scoped.

---

`36-46`: **Well-implemented button component.**

The button correctly handles state updates and provides appropriate visual feedback.

---

`49-54`: **Good responsive modal implementation.**

The modal uses conditional classes for responsive design and animation, with proper z-index and positioning.

---

`57-71`: **Clean header implementation with appropriate accessibility.**

The header includes proper aria-label for the close button and clear visual hierarchy.

---

`73-88`: **Efficient conditional rendering of chat content.**

The chat content is only rendered when needed (hasBeenOpened && isOpen) to improve performance, and all required props are correctly passed to CustomChatContent.

</details>
<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx (7)</summary>

`18-26`: **Well-defined component props with proper typing.**

The props interface is comprehensive with appropriate types for all required properties.

---

`27-40`: **Good conditional rendering for authentication.**

The component correctly handles the authentication state, rendering different content based on the presence of an auth token.

---

`43-50`: **Well-structured logged-in component props.**

Props are clearly defined with appropriate types for the logged-in state.

---

`51-61`: **Comprehensive state management.**

All necessary states for managing chat functionality are properly defined.

---

`63-150`: **Well-implemented message handling with proper error handling.**

The handleSendMessage callback correctly manages the chat state, API communication, and error handling. The dependencies array is appropriate for the callback.

---

`203-237`: **Well-designed logged-out state.**

The logged-out state provides a clear call-to-action for users to sign in, with appropriate styling and UX.

---

`239-284`: **Good empty state implementation with example prompts.**

The empty state component provides helpful example prompts to guide users and is well-styled with appropriate spacing and layout.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T00:09:02Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx (1)</summary><blockquote>

`230-230`: **Proper alignment with team-scoped chat features.**

Adding the explicit `teamId={undefined}` prop aligns this component with other similar components in the codebase that now pass this property, maintaining consistency with the team-scoped chat features implementation.


Consider adding a story variant that demonstrates the behavior when `teamId` is set to a valid value to showcase the team-scoped chat features.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between dca105cb8c4c8e52112cbcbbc0e57eac005ab2a5 and ae19f80389a685d97c6ba9dea558b2c54a5daeec.

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx` (2 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx` (2 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx` (1 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx` (6 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx` (1 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx` (1 hunks)
* `apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx` (3 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (6)</summary>

* apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx
* apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx
* apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx
* apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx
* apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx
* apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: E2E Tests (pnpm, esbuild)
* GitHub Check: E2E Tests (pnpm, webpack)
* GitHub Check: Unit Tests
* GitHub Check: Size
* GitHub Check: E2E Tests (pnpm, vite)
* GitHub Check: Analyze (javascript)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T03:55:40Z

Review comments:
diff_hunk: @@ -422,3 +456,55 @@ function StyledMarkdownRenderer(props: {
     />
   );
 }
+
+function FeedbackButtons({ sessionId, messageText }: { sessionId: string | undefined; messageText: string }) {
+  const [feedback, setFeedback] = useState<"good" | "bad" | null>(null);
+  const [loading, setLoading] = useState(false);
+  const [thankYou, setThankYou] = useState(false);
+
+  async function sendFeedback(rating: "good" | "bad") {
+    setLoading(true);
+    try {
+      await fetch("https://siwa-api.thirdweb-dev.com/v1/feedback", {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({
+          conversationId: sessionId,
+          message: messageText,
+          rating,
+        }),
+      });
+      setFeedback(rating);
+      setThankYou(true);
+    } catch (e) {
+      // handle error
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  if (thankYou) {
+    return <div className="mt-2 text-xs text-muted-foreground">Thank you for your feedback!</div>;
+  }
+
+  return (
+    <div className="flex gap-2 mt-2">
+      <button
+        className="p-1 rounded-full border hover:bg-muted-foreground/10"
+        onClick={() => sendFeedback("good")}
+        disabled={loading}
+        aria-label="Thumbs up"
+      >
+        <ThumbsUpIcon className="size-4 text-green-500" />
+      </button>
+      <button
+        className="p-1 rounded-full border hover:bg-muted-foreground/10"
+        onClick={() => sendFeedback("bad")}
+        disabled={loading}
+        aria-label="Thumbs down"
+      >
+        <ThumbsDownIcon className="size-4 text-red-500" />
+      </button>
+    </div>
+  );
+}
path: apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Minor clean-ups in `FeedbackButtons`**

Static analysis correctly flags a few small issues:

1. `feedback` state is set but never read.
2. Buttons miss an explicit `type="button"` – default “submit” can have side-effects inside `<form>`.
3. `e` in the `catch` block is unused.
4. Class lists are unsorted (style-lint).

```diff
-const [feedback, setFeedback] = useState<"good" | "bad" | null>(null);
+// local state removed – not used anywhere
+
 ...
   } catch (_err) {
     // TODO: surface a toast or log
   } finally {
@@
-      <button
+      <button
         className="rounded-full border p-1 hover:bg-muted-foreground/10"
+        type="button"
         onClick={() => sendFeedback("good")}
@@
-      <button
+      <button
         className="rounded-full border p-1 hover:bg-muted-foreground/10"
+        type="button"
         onClick={() => sendFeedback("bad")}
```

These tweaks silence linters and avoid unintended form submissions.  



> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 487-487: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 491-491: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 493-493: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 501-501: These CSS classes should be sorted.

Unsafe fix: Sort the classes.


(lint/nursery/useSortedClasses)

---

[error] 461-461: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend feedback with an underscore.


(lint/correctness/noUnusedVariables)

---

[error] 479-479: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend e with an underscore.


(lint/correctness/noUnusedVariables)

---

[error] 492-497: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a `form` element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

---

[error] 500-505: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a `form` element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx around lines 460
to 510, address minor clean-ups in the FeedbackButtons component by removing the
unused feedback state variable since it is set but never read, adding explicit
type="button" attributes to both buttons to prevent unintended form submissions,
removing the unused error variable 'e' from the catch block, and sorting the
className lists alphabetically to comply with style-lint rules.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit dca105c
created_at: 2025-05-22T18:47:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,345 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { useTrack } from "hooks/analytics/useTrack";
+import { ArrowRightIcon, ArrowUpRightIcon } from "lucide-react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { useCallback, useState } from "react";
+import type { ThirdwebClient } from "thirdweb";
+import {
+  useActiveWallet,
+  useActiveWalletConnectionStatus,
+} from "thirdweb/react";
+import type { NebulaContext } from "../../api/chat";
+// import { createSession } from "../../api/session"; // REMOVE
+import type { NebulaUserMessage } from "../../api/types";
+import type { ExamplePrompt } from "../../data/examplePrompts";
+import { NebulaIcon } from "../../icons/NebulaIcon";
+import { ChatBar } from "../ChatBar";
+import { Chats } from "../Chats";
+import type { ChatMessage } from "../Chats";
+
+export default function CustomChatContent(props: {
+  authToken: string | undefined;
+  client: ThirdwebClient;
+  examplePrompts: ExamplePrompt[];
+  pageType: "chain" | "contract" | "support";
+  networks: NebulaContext["networks"];
+  customApiParams:
+    | {
+        messagePrefix: string;
+        chainIds: number[];
+        wallet: string | undefined;
+      }
+    | undefined;
+  requireLogin?: boolean;
+}) {
+  if (props.requireLogin !== false && !props.authToken) {
+    return <LoggedOutStateChatContent />;
+  }
+
+  return (
+    <CustomChatContentLoggedIn
+      networks={props.networks}
+      authToken={props.authToken || ""}
+      client={props.client}
+      customApiParams={props.customApiParams}
+      examplePrompts={props.examplePrompts}
+      pageType={props.pageType}
+    />
+  );
+}
+
+function CustomChatContentLoggedIn(props: {
+  authToken: string;
+  client: ThirdwebClient;
+  pageType: "chain" | "contract" | "support";
+  examplePrompts: ExamplePrompt[];
+  networks: NebulaContext["networks"];
+  customApiParams:
+    | {
+        messagePrefix: string;
+        chainIds: number[];
+        wallet: string | undefined;
+      }
+    | undefined;
+}) {
+  const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false);
+  const [messages, setMessages] = useState<Array<ChatMessage>>([]);
+  // sessionId is initially undefined, will be set to conversationId from API after first response
+  const [sessionId, setSessionId] = useState<string | undefined>(undefined);
+  const [chatAbortController, setChatAbortController] = useState<
+    AbortController | undefined
+  >();
+  const trackEvent = useTrack();
+  const [isChatStreaming, setIsChatStreaming] = useState(false);
+  const [enableAutoScroll, setEnableAutoScroll] = useState(false);
+  const connectionStatus = useActiveWalletConnectionStatus();
+  const activeWallet = useActiveWallet();
+
+  const [contextFilters, setContextFilters] = useState<
+    NebulaContext | undefined
+  >(() => {
+    return {
+      chainIds:
+        props.customApiParams?.chainIds.map((chainId) => chainId.toString()) ||
+        null,
+      walletAddress: props.customApiParams?.wallet || null,
+      networks: props.networks,
+    };
+  });
+
+  const handleSendMessage = useCallback(
+    async (userMessage: NebulaUserMessage) => {
+      const abortController = new AbortController();
+      setUserHasSubmittedMessage(true);
+      setIsChatStreaming(true);
+      setEnableAutoScroll(true);
+
+      const textMessage = userMessage.content.find((x) => x.type === "text");
+
+      trackEvent({
+        category: "floating_siwa",
+        action: "send",
+        label: "message",
+        message: textMessage?.text,
+        page: props.pageType,
+        sessionId: sessionId,
+      });
+
+      setMessages((prev) => [
+        ...prev,
+        {
+          type: "user",
+          content: userMessage.content,
+        },
+        // instant loading indicator feedback to user
+        {
+          type: "presence",
+          texts: [],
+        },
+      ]);
+
+      const messagePrefix = props.customApiParams?.messagePrefix;
+
+      // if this is first message, set the message prefix
+      // deep clone `userMessage` to avoid mutating the original message, its a pretty small object so JSON.parse is fine
+      const messageToSend = JSON.parse(
+        JSON.stringify(userMessage),
+      ) as NebulaUserMessage;
+
+      // if this is first message, set the message prefix
+      if (messagePrefix && !userHasSubmittedMessage) {
+        const textMessage = messageToSend.content.find(
+          (x) => x.type === "text",
+        );
+        if (textMessage) {
+          textMessage.text = `${messagePrefix}\n\n${textMessage.text}`;
+        }
+      }
+
+      try {
+        setChatAbortController(abortController);
+        // --- Custom API call ---
+        const payload: any = {
+          message: messageToSend.content.find((x) => x.type === "text")?.text ?? "",
+          authToken: props.authToken,
+          conversationId: "25000000005",
path: apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Avoid `any` – give `payload` a type**

Using `any` drops all type-safety right before a network request.

Introduce an interface:

```ts
interface SiwaPayload {
  message: string;
  authToken: string;
  conversationId: string;
}
...
const payload: SiwaPayload = {
  message: ...,
  authToken: props.authToken,
  conversationId: sessionId ?? "25000000005",
};
```

This prevents accidental property omissions/typos.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 144-144: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx
around lines 144 to 147, the payload object is currently typed as any, which
removes type safety. Define a TypeScript interface named SiwaPayload with
properties message, authToken, and conversationId all as strings. Then, declare
the payload variable with this SiwaPayload type and assign the corresponding
values, replacing any usage of any. This ensures type safety and prevents
property errors.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T18:47:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,345 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { useTrack } from "hooks/analytics/useTrack";
+import { ArrowRightIcon, ArrowUpRightIcon } from "lucide-react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { useCallback, useState } from "react";
+import type { ThirdwebClient } from "thirdweb";
+import {
+  useActiveWallet,
+  useActiveWalletConnectionStatus,
+} from "thirdweb/react";
+import type { NebulaContext } from "../../api/chat";
+// import { createSession } from "../../api/session"; // REMOVE
+import type { NebulaUserMessage } from "../../api/types";
+import type { ExamplePrompt } from "../../data/examplePrompts";
+import { NebulaIcon } from "../../icons/NebulaIcon";
+import { ChatBar } from "../ChatBar";
+import { Chats } from "../Chats";
+import type { ChatMessage } from "../Chats";
+
+export default function CustomChatContent(props: {
+  authToken: string | undefined;
+  client: ThirdwebClient;
+  examplePrompts: ExamplePrompt[];
+  pageType: "chain" | "contract" | "support";
+  networks: NebulaContext["networks"];
+  customApiParams:
+    | {
+        messagePrefix: string;
+        chainIds: number[];
+        wallet: string | undefined;
+      }
+    | undefined;
+  requireLogin?: boolean;
+}) {
+  if (props.requireLogin !== false && !props.authToken) {
+    return <LoggedOutStateChatContent />;
+  }
+
+  return (
+    <CustomChatContentLoggedIn
+      networks={props.networks}
+      authToken={props.authToken || ""}
+      client={props.client}
+      customApiParams={props.customApiParams}
+      examplePrompts={props.examplePrompts}
+      pageType={props.pageType}
+    />
+  );
+}
+
+function CustomChatContentLoggedIn(props: {
+  authToken: string;
+  client: ThirdwebClient;
+  pageType: "chain" | "contract" | "support";
+  examplePrompts: ExamplePrompt[];
+  networks: NebulaContext["networks"];
+  customApiParams:
+    | {
+        messagePrefix: string;
+        chainIds: number[];
+        wallet: string | undefined;
+      }
+    | undefined;
+}) {
+  const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false);
+  const [messages, setMessages] = useState<Array<ChatMessage>>([]);
+  // sessionId is initially undefined, will be set to conversationId from API after first response
+  const [sessionId, setSessionId] = useState<string | undefined>(undefined);
+  const [chatAbortController, setChatAbortController] = useState<
+    AbortController | undefined
+  >();
+  const trackEvent = useTrack();
+  const [isChatStreaming, setIsChatStreaming] = useState(false);
+  const [enableAutoScroll, setEnableAutoScroll] = useState(false);
+  const connectionStatus = useActiveWalletConnectionStatus();
+  const activeWallet = useActiveWallet();
+
+  const [contextFilters, setContextFilters] = useState<
+    NebulaContext | undefined
+  >(() => {
+    return {
+      chainIds:
+        props.customApiParams?.chainIds.map((chainId) => chainId.toString()) ||
+        null,
+      walletAddress: props.customApiParams?.wallet || null,
+      networks: props.networks,
+    };
+  });
+
+  const handleSendMessage = useCallback(
+    async (userMessage: NebulaUserMessage) => {
+      const abortController = new AbortController();
+      setUserHasSubmittedMessage(true);
+      setIsChatStreaming(true);
+      setEnableAutoScroll(true);
+
+      const textMessage = userMessage.content.find((x) => x.type === "text");
+
+      trackEvent({
+        category: "floating_siwa",
+        action: "send",
+        label: "message",
+        message: textMessage?.text,
+        page: props.pageType,
+        sessionId: sessionId,
+      });
+
+      setMessages((prev) => [
+        ...prev,
+        {
+          type: "user",
+          content: userMessage.content,
+        },
+        // instant loading indicator feedback to user
+        {
+          type: "presence",
+          texts: [],
+        },
+      ]);
+
+      const messagePrefix = props.customApiParams?.messagePrefix;
+
+      // if this is first message, set the message prefix
+      // deep clone `userMessage` to avoid mutating the original message, its a pretty small object so JSON.parse is fine
+      const messageToSend = JSON.parse(
+        JSON.stringify(userMessage),
+      ) as NebulaUserMessage;
+
+      // if this is first message, set the message prefix
+      if (messagePrefix && !userHasSubmittedMessage) {
+        const textMessage = messageToSend.content.find(
+          (x) => x.type === "text",
+        );
+        if (textMessage) {
+          textMessage.text = `${messagePrefix}\n\n${textMessage.text}`;
+        }
+      }
+
+      try {
+        setChatAbortController(abortController);
+        // --- Custom API call ---
+        const payload: any = {
+          message: messageToSend.content.find((x) => x.type === "text")?.text ?? "",
+          authToken: props.authToken,
+          conversationId: "25000000005",
+        };
+        if (sessionId) {
+          payload.conversationId = sessionId;
+        }
+        const response = await fetch(
+          "https://proxy.cors.sh/https://siwa-slack-bot-u8ne.chainsaw-dev.zeet.app/siwa",
+          {
+            method: "POST",
+            headers: {
+              "Content-Type": "application/json",
+              "x-cors-api-key": "temp_3f2b6d4409a86dc7f4b7c45840dbd8e9", // replace with your cors.sh API key
+            },
path: apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Hard-coded CORS-sh proxy key leaked to the client**

The header

```ts
"x-cors-api-key": "temp_3f2b6d4409a86dc7f4b7c45840dbd8e9"
```

exposes what looks like an API key to every site visitor (this file is shipped to the browser).  
Anyone can now abuse the proxy quota.

Move this key to a server-side environment variable (e.g. `process.env.CORS_PROXY_KEY`) and inject it only in API routes or get rid of the proxy entirely by enabling CORS on the upstream service.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Gitleaks (8.26.0)</summary>

158-158: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx
around lines 158-159, the CORS-sh proxy API key is hard-coded and exposed to the
client, risking abuse. Remove the hard-coded key from the client code and
instead store it securely as a server-side environment variable (e.g.,
process.env.CORS_PROXY_KEY). Modify the code to inject this key only in
server-side API routes or consider enabling CORS on the upstream service to
eliminate the need for the proxy.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit c8843ae
created_at: 2025-05-22T18:47:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,93 @@
+"use client";
+
+import CustomChatContent from "./CustomChatContent";
+import type { ExamplePrompt } from "../../data/examplePrompts";
+import type { ThirdwebClient } from "thirdweb";
+import { useState, useCallback, useRef } from "react";
+import { Button } from "@/components/ui/button";
+import { XIcon, MessageCircleIcon } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+export function CustomChatButton(props: {
+  isLoggedIn: boolean;
+  networks: "mainnet" | "testnet" | "all" | null;
+  isFloating: boolean;
+  pageType: "chain" | "contract" | "support";
+  label: string;
+  client: ThirdwebClient;
+  customApiParams: any;
path: apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Avoid using the `any` type in TypeScript**

The `customApiParams` prop is typed as `any`, which bypasses TypeScript's type checking and can lead to runtime errors. Based on how this prop is used in `CustomChatContent`, you should use a more specific type.

```diff
-  customApiParams: any;
+  customApiParams: {
+    messagePrefix: string;
+    chainIds: number[];
+    wallet: string | undefined;
+  } | undefined;
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
  customApiParams: {
    messagePrefix: string;
    chainIds: number[];
    wallet: string | undefined;
  } | undefined;
```

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 18-18: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx
at line 18, the prop customApiParams is currently typed as any, which disables
type checking. Review how customApiParams is used in CustomChatContent and
replace the any type with a more specific interface or type that accurately
reflects its structure and expected properties to improve type safety.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T18:47:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,93 @@
+"use client";
+
+import CustomChatContent from "./CustomChatContent";
+import type { ExamplePrompt } from "../../data/examplePrompts";
+import type { ThirdwebClient } from "thirdweb";
+import { useState, useCallback, useRef } from "react";
+import { Button } from "@/components/ui/button";
+import { XIcon, MessageCircleIcon } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+export function CustomChatButton(props: {
+  isLoggedIn: boolean;
+  networks: "mainnet" | "testnet" | "all" | null;
+  isFloating: boolean;
+  pageType: "chain" | "contract" | "support";
+  label: string;
+  client: ThirdwebClient;
+  customApiParams: any;
+  examplePrompts: ExamplePrompt[];
+  authToken: string | undefined;
+  requireLogin?: boolean;
+}) {
+  const [isOpen, setIsOpen] = useState(false);
+  const [hasBeenOpened, setHasBeenOpened] = useState(false);
+  const [isDismissed, setIsDismissed] = useState(false);
path: apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Unused state variable `setIsDismissed`**

The `setIsDismissed` state setter is defined but never used in the component, while the `isDismissed` state is checked for conditional rendering. This suggests incomplete implementation.

Either implement the dismiss functionality or remove the unused state if it's not needed:

```diff
- const [isDismissed, setIsDismissed] = useState(false);
```

If the dismiss functionality is planned for future implementation, consider adding a TODO comment or implementing it now.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

```

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 25-25: This variable is unused.

Unused variables usually are result of incomplete refactoring, typos and other source of bugs.
Unsafe fix: If this is intentional, prepend setIsDismissed with an underscore.


(lint/correctness/noUnusedVariables)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx
at line 25, the state setter setIsDismissed is declared but never used,
indicating incomplete dismiss functionality. To fix this, either implement the
dismiss logic by adding an event handler that calls setIsDismissed to update the
state when the chat button is dismissed, or if dismissing is not needed, remove
both isDismissed and setIsDismissed state declarations. If the dismiss feature
is planned but not yet implemented, add a TODO comment explaining this for
future reference.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit dca105c
created_at: 2025-05-22T18:47:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,346 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { useTrack } from "hooks/analytics/useTrack";
+import { ArrowRightIcon } from "lucide-react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { useCallback, useState } from "react";
+import type { ThirdwebClient } from "thirdweb";
+import {
+  useActiveWallet,
+  useActiveWalletConnectionStatus,
+} from "thirdweb/react";
+import type { NebulaContext } from "../../api/chat";
+// import { createSession } from "../../api/session"; // REMOVE
+import type { NebulaUserMessage } from "../../api/types";
+import type { ExamplePrompt } from "../../data/examplePrompts";
+import { NebulaIcon } from "../../icons/NebulaIcon";
+import { ChatBar } from "../ChatBar";
+import { Chats } from "../Chats";
+import type { ChatMessage } from "../Chats";
+
+export default function CustomChatContent(props: {
+  authToken: string | undefined;
+  client: ThirdwebClient;
+  examplePrompts: ExamplePrompt[];
+  pageType: "chain" | "contract" | "support";
+  networks: NebulaContext["networks"];
+  customApiParams:
+    | {
+        messagePrefix: string;
+        chainIds: number[];
+        wallet: string | undefined;
+      }
+    | undefined;
+  requireLogin?: boolean;
+}) {
+  if (props.requireLogin !== false && !props.authToken) {
+    return <LoggedOutStateChatContent />;
+  }
+
+  return (
+    <CustomChatContentLoggedIn
+      networks={props.networks}
+      authToken={props.authToken || ""}
+      client={props.client}
+      customApiParams={props.customApiParams}
+      examplePrompts={props.examplePrompts}
+      pageType={props.pageType}
+    />
+  );
+}
+
+function CustomChatContentLoggedIn(props: {
+  authToken: string;
+  client: ThirdwebClient;
+  pageType: "chain" | "contract" | "support";
+  examplePrompts: ExamplePrompt[];
+  networks: NebulaContext["networks"];
+  customApiParams:
+    | {
+        messagePrefix: string;
+        chainIds: number[];
+        wallet: string | undefined;
+      }
+    | undefined;
+}) {
+  const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false);
+  const [messages, setMessages] = useState<Array<ChatMessage>>([]);
+  // sessionId is initially undefined, will be set to conversationId from API after first response
+  const [sessionId, setSessionId] = useState<string | undefined>(undefined);
+  const [chatAbortController, setChatAbortController] = useState<
+    AbortController | undefined
+  >();
+  const trackEvent = useTrack();
+  const [isChatStreaming, setIsChatStreaming] = useState(false);
+  const [enableAutoScroll, setEnableAutoScroll] = useState(false);
+  const connectionStatus = useActiveWalletConnectionStatus();
+  const activeWallet = useActiveWallet();
+
+  const [contextFilters, setContextFilters] = useState<
+    NebulaContext | undefined
+  >(() => {
+    return {
+      chainIds:
+        props.customApiParams?.chainIds.map((chainId) => chainId.toString()) ||
+        null,
+      walletAddress: props.customApiParams?.wallet || null,
+      networks: props.networks,
+    };
+  });
+
+  const handleSendMessage = useCallback(
+    async (userMessage: NebulaUserMessage) => {
+      const abortController = new AbortController();
+      setUserHasSubmittedMessage(true);
+      setIsChatStreaming(true);
+      setEnableAutoScroll(true);
+
+      const textMessage = userMessage.content.find((x) => x.type === "text");
+
+      trackEvent({
+        category: "floating_siwa",
+        action: "send",
+        label: "message",
+        message: textMessage?.text,
+        page: props.pageType,
+        sessionId: sessionId,
+      });
+
+      setMessages((prev) => [
+        ...prev,
+        {
+          type: "user",
+          content: userMessage.content,
+        },
+        // instant loading indicator feedback to user
+        {
+          type: "presence",
+          texts: [],
+        },
+      ]);
+
+      const messagePrefix = props.customApiParams?.messagePrefix;
+
+      // if this is first message, set the message prefix
+      // deep clone `userMessage` to avoid mutating the original message, its a pretty small object so JSON.parse is fine
+      const messageToSend = JSON.parse(
+        JSON.stringify(userMessage),
+      ) as NebulaUserMessage;
+
+      // if this is first message, set the message prefix
+      if (messagePrefix && !userHasSubmittedMessage) {
+        const textMessage = messageToSend.content.find(
+          (x) => x.type === "text",
+        );
+        if (textMessage) {
+          textMessage.text = `${messagePrefix}\n\n${textMessage.text}`;
+        }
+      }
+
+      try {
+        setChatAbortController(abortController);
+        // --- Custom API call ---
+        const payload: any = {
+          message:
+            messageToSend.content.find((x) => x.type === "text")?.text ?? "",
+          authToken: props.authToken,
+          conversationId: "25000000005",
+        };
+        if (sessionId) {
+          payload.conversationId = sessionId;
+        }
+        const response = await fetch(
+          "https://proxy.cors.sh/https://siwa-slack-bot-u8ne.chainsaw-dev.zeet.app/siwa",
+          {
+            method: "POST",
+            headers: {
+              "Content-Type": "application/json",
+              "x-cors-api-key": "temp_3f2b6d4409a86dc7f4b7c45840dbd8e9", // replace with your cors.sh API key
+            },
+            body: JSON.stringify(payload),
+            signal: abortController.signal,
+          },
+        );
+        const data = await response.json();
+        // If the response contains a conversationId, set it as the sessionId for future messages
+        if (data.conversationId && data.conversationId !== sessionId) {
+          setSessionId(data.conversationId);
+        }
+        setMessages((prev) => [
+          ...prev.slice(0, -1), // remove presence indicator
+          {
+            type: "assistant",
+            request_id: undefined,
+            text: data.data,
+          },
+        ]);
+      } catch (error) {
+        if (abortController.signal.aborted) {
+          return;
+        }
+        setMessages((prev) => [
+          ...prev.slice(0, -1),
+          {
+            type: "assistant",
+            request_id: undefined,
+            text: "Sorry, something went wrong.",
+          },
+        ]);
+      } finally {
+        setIsChatStreaming(false);
+        setEnableAutoScroll(false);
+      }
+    },
+    [
+      props.authToken,
+      contextFilters,
+      sessionId,
+      props.customApiParams?.messagePrefix,
+      userHasSubmittedMessage,
+      trackEvent,
+      props.pageType,
+    ],
+  );
+
+  const showEmptyState = !userHasSubmittedMessage && messages.length === 0;
+  return (
+    <div className="flex grow flex-col overflow-hidden">
+      {showEmptyState ? (
+        <EmptyStateChatPageContent
+          sendMessage={handleSendMessage}
+          examplePrompts={props.examplePrompts}
+        />
+      ) : (
+        <Chats
+          messages={messages}
+          isChatStreaming={isChatStreaming}
+          authToken={props.authToken}
+          sessionId={sessionId}
+          className="min-w-0 pb-10"
+          client={props.client}
+          enableAutoScroll={enableAutoScroll}
+          setEnableAutoScroll={setEnableAutoScroll}
+          useSmallText
+          sendMessage={handleSendMessage}
+        />
+      )}
+      <ChatBar
+        onLoginClick={undefined}
+        client={props.client}
+        isConnectingWallet={connectionStatus === "connecting"}
+        context={contextFilters}
+        setContext={setContextFilters}
+        showContextSelector={false}
+        connectedWallets={
+          props.customApiParams?.wallet && activeWallet
+            ? [
+                {
+                  address: props.customApiParams.wallet,
+                  walletId: activeWallet.id,
+                },
+              ]
+            : []
+        }
+        setActiveWallet={() => {}}
+        abortChatStream={() => {
+          chatAbortController?.abort();
+          setChatAbortController(undefined);
+          setIsChatStreaming(false);
+          // if last message is presence, remove it
+          if (messages[messages.length - 1]?.type === "presence") {
+            setMessages((prev) => prev.slice(0, -1));
+          }
+        }}
+        isChatStreaming={isChatStreaming}
+        prefillMessage={undefined}
+        sendMessage={handleSendMessage}
+        className="rounded-none border-x-0 border-b-0"
+        allowImageUpload={false}
+      />
+    </div>
+  );
+}
+
+function LoggedOutStateChatContent() {
+  const pathname = usePathname();
+  return (
+    <div className="flex grow flex-col items-center justify-center p-4">
+      <div className="mb-4 flex justify-center">
+        <div className="rounded-full border p-1">
+          <div className="rounded-full border bg-card p-2">
+            <NebulaIcon className="size-7 text-muted-foreground" />
+          </div>
+        </div>
+      </div>
+
+      <h1 className="px-4 text-center font-semibold text-3xl tracking-tight md:text-4xl">
+        How can I help you <br className="max-sm:hidden" />
+        onchain today?
+      </h1>
+
+      <div className="h-3" />
+      <p className="text-base text-muted-foreground">
+        Sign in to use Nebula AI
+      </p>
+      <div className="h-5" />
+
+      <Button asChild>
+        <Link
+          href={`/login?next=${encodeURIComponent(pathname)}`}
+          className="w-full max-w-96 gap-2"
+        >
+          Sign in
+          <ArrowRightIcon className="size-4" />
+        </Link>
+      </Button>
+    </div>
+  );
+}
+
+function EmptyStateChatPageContent(props: {
+  sendMessage: (message: any) => void;
+  examplePrompts: { title: string; message: string }[];
+}) {
+  return (
+    <div className="flex flex-1 flex-col items-center justify-center overflow-auto p-4 ">
+      <div className="mb-4 flex justify-center">
+        <div className="rounded-full border-[1.5px] border-nebula-pink-foreground/20 bg-[hsl(var(--nebula-pink-foreground)/5%)] p-1">
+          <div className="rounded-full border-[1.5px] border-nebula-pink-foreground/40 bg-[hsl(var(--nebula-pink-foreground)/5%)] p-2">
+            <NebulaIcon className="size-7 text-nebula-pink-foreground" />
+          </div>
+        </div>
+      </div>
+
+      <h1 className="px-4 text-center font-semibold text-3xl tracking-tight md:text-4xl">
+        How can I help you <br className="max-sm:hidden" />
+        onchain today?
+      </h1>
+
+      <div className="h-6" />
+      <div className="flex max-w-lg flex-col flex-wrap justify-center gap-2.5">
+        {props.examplePrompts.map((prompt) => (
+          <Button
+            key={prompt.title}
+            variant="outline"
+            size="sm"
+            onClick={() =>
+              props.sendMessage({
+                role: "user",
+                content: [
+                  {
+                    type: "text",
+                    text: prompt.message,
+                  },
+                ],
+              })
+            }
+            disabled={false}
+          >
+            {prompt.title}
+          </Button>
+        ))}
+      </div>
+    </div>
+  );
+}
path: apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Fix `any` type in EmptyStateChatPageContent props**

The `sendMessage` prop in EmptyStateChatPageContent is typed as `any`, which reduces type safety.


```diff
function EmptyStateChatPageContent(props: {
-  sendMessage: (message: any) => void;
+  sendMessage: (message: NebulaUserMessage) => void;
  examplePrompts: { title: string; message: string }[];
}) {
  // ...
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
function EmptyStateChatPageContent(props: {
  sendMessage: (message: NebulaUserMessage) => void;
  examplePrompts: { title: string; message: string }[];
}) {
  return (
    <div className="flex flex-1 flex-col items-center justify-center overflow-auto p-4 ">
      <div className="mb-4 flex justify-center">
        <div className="rounded-full border-[1.5px] border-nebula-pink-foreground/20 bg-[hsl(var(--nebula-pink-foreground)/5%)] p-1">
          <div className="rounded-full border-[1.5px] border-nebula-pink-foreground/40 bg-[hsl(var(--nebula-pink-foreground)/5%)] p-2">
            <NebulaIcon className="size-7 text-nebula-pink-foreground" />
          </div>
        </div>
      </div>

      <h1 className="px-4 text-center font-semibold text-3xl tracking-tight md:text-4xl">
        How can I help you <br className="max-sm:hidden" />
        onchain today?
      </h1>

      <div className="h-6" />
      <div className="flex max-w-lg flex-col flex-wrap justify-center gap-2.5">
        {props.examplePrompts.map((prompt) => (
          <Button
            key={prompt.title}
            variant="outline"
            size="sm"
            onClick={() =>
              props.sendMessage({
                role: "user",
                content: [
                  {
                    type: "text",
                    text: prompt.message,
                  },
                ],
              })
            }
            disabled={false}
          >
            {prompt.title}
          </Button>
        ))}
      </div>
    </div>
  );
}
```

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 302-302: Unexpected any. Specify a different type.

any disables many type checking rules. Its use should be avoided.

(lint/suspicious/noExplicitAny)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx
around lines 301 to 346, the sendMessage prop is currently typed as any, which
reduces type safety. Replace the any type with a more specific type that
accurately represents the message object structure expected by sendMessage, such
as defining an interface or type for the message parameter and using it in the
function signature to improve type safety and clarity.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit dca105c
created_at: 2025-05-22T18:50:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,345 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { useTrack } from "hooks/analytics/useTrack";
+import { ArrowRightIcon } from "lucide-react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { useCallback, useState } from "react";
+import type { ThirdwebClient } from "thirdweb";
+import {
+  useActiveWallet,
+  useActiveWalletConnectionStatus,
+} from "thirdweb/react";
+import type { NebulaContext } from "../../api/chat";
+// import { createSession } from "../../api/session"; // REMOVE
+import type { NebulaUserMessage } from "../../api/types";
+import type { ExamplePrompt } from "../../data/examplePrompts";
+import { NebulaIcon } from "../../icons/NebulaIcon";
+import { ChatBar } from "../ChatBar";
+import { Chats } from "../Chats";
+import type { ChatMessage } from "../Chats";
+
+export default function CustomChatContent(props: {
+  authToken: string | undefined;
+  client: ThirdwebClient;
+  examplePrompts: ExamplePrompt[];
+  pageType: "chain" | "contract" | "support";
+  networks: NebulaContext["networks"];
+  customApiParams:
+    | {
+        messagePrefix: string;
+        chainIds: number[];
+        wallet: string | undefined;
+      }
+    | undefined;
+  requireLogin?: boolean;
+}) {
+  if (props.requireLogin !== false && !props.authToken) {
+    return <LoggedOutStateChatContent />;
+  }
+
+  return (
+    <CustomChatContentLoggedIn
+      networks={props.networks}
+      authToken={props.authToken || ""}
+      client={props.client}
+      customApiParams={props.customApiParams}
+      examplePrompts={props.examplePrompts}
+      pageType={props.pageType}
+    />
+  );
+}
+
+function CustomChatContentLoggedIn(props: {
+  authToken: string;
+  client: ThirdwebClient;
+  pageType: "chain" | "contract" | "support";
+  examplePrompts: ExamplePrompt[];
+  networks: NebulaContext["networks"];
+  customApiParams:
+    | {
+        messagePrefix: string;
+        chainIds: number[];
+        wallet: string | undefined;
+      }
+    | undefined;
+}) {
+  const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false);
+  const [messages, setMessages] = useState<Array<ChatMessage>>([]);
+  // sessionId is initially undefined, will be set to conversationId from API after first response
+  const [sessionId, setSessionId] = useState<string | undefined>(undefined);
+  const [chatAbortController, setChatAbortController] = useState<
+    AbortController | undefined
+  >();
+  const trackEvent = useTrack();
+  const [isChatStreaming, setIsChatStreaming] = useState(false);
+  const [enableAutoScroll, setEnableAutoScroll] = useState(false);
+  const connectionStatus = useActiveWalletConnectionStatus();
+  const activeWallet = useActiveWallet();
+
+  const [contextFilters, setContextFilters] = useState<
+    NebulaContext | undefined
+  >(() => {
+    return {
+      chainIds:
+        props.customApiParams?.chainIds.map((chainId) => chainId.toString()) ||
+        null,
+      walletAddress: props.customApiParams?.wallet || null,
+      networks: props.networks,
+    };
+  });
+
+  const handleSendMessage = useCallback(
+    async (userMessage: NebulaUserMessage) => {
+      const abortController = new AbortController();
+      setUserHasSubmittedMessage(true);
+      setIsChatStreaming(true);
+      setEnableAutoScroll(true);
+
+      const textMessage = userMessage.content.find((x) => x.type === "text");
+
+      trackEvent({
+        category: "floating_siwa",
+        action: "send",
+        label: "message",
+        message: textMessage?.text,
+        page: props.pageType,
+        sessionId: sessionId,
+      });
+
+      setMessages((prev) => [
+        ...prev,
+        {
+          type: "user",
+          content: userMessage.content,
+        },
+        // instant loading indicator feedback to user
+        {
+          type: "presence",
+          texts: [],
+        },
+      ]);
+
+      const messagePrefix = props.customApiParams?.messagePrefix;
+
+      // if this is first message, set the message prefix
+      // deep clone `userMessage` to avoid mutating the original message, its a pretty small object so JSON.parse is fine
+      const messageToSend = JSON.parse(
+        JSON.stringify(userMessage),
+      ) as NebulaUserMessage;
+
+      // if this is first message, set the message prefix
+      if (messagePrefix && !userHasSubmittedMessage) {
+        const textMessage = messageToSend.content.find(
+          (x) => x.type === "text",
+        );
+        if (textMessage) {
+          textMessage.text = `${messagePrefix}\n\n${textMessage.text}`;
+        }
+      }
+
+      try {
+        setChatAbortController(abortController);
+        // --- Custom API call ---
+        const payload: any = {
+          message:
+            messageToSend.content.find((x) => x.type === "text")?.text ?? "",
+          authToken: props.authToken,
+          conversationId: "25000000005",
+        };
+        if (sessionId) {
+          payload.conversationId = sessionId;
+        }
+        const response = await fetch(
+          "https://siwa-api.thirdweb-dev.com/api/chat",
+          {
+            method: "POST",
+            headers: {
+              "Content-Type": "application/json",
+            },
+            body: JSON.stringify(payload),
+            signal: abortController.signal,
+          },
+        );
+        const data = await response.json();
+        // If the response contains a conversationId, set it as the sessionId for future messages
+        if (data.conversationId && data.conversationId !== sessionId) {
+          setSessionId(data.conversationId);
+        }
+        setMessages((prev) => [
+          ...prev.slice(0, -1), // remove presence indicator
+          {
+            type: "assistant",
+            request_id: undefined,
+            text: data.data,
+          },
+        ]);
+      } catch (error) {
+        if (abortController.signal.aborted) {
+          return;
+        }
+        setMessages((prev) => [
+          ...prev.slice(0, -1),
+          {
+            type: "assistant",
+            request_id: undefined,
+            text: "Sorry, something went wrong.",
+          },
+        ]);
+      } finally {
+        setIsChatStreaming(false);
+        setEnableAutoScroll(false);
+      }
+    },
+    [
+      props.authToken,
+      contextFilters,
+      sessionId,
+      props.customApiParams?.messagePrefix,
+      userHasSubmittedMessage,
+      trackEvent,
+      props.pageType,
+    ],
path: apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Remove unused dependency from useCallback**

The `contextFilters` is included in the dependency array but not used in the function body.

```diff
 [
   props.authToken,
-  contextFilters,
   sessionId,
   props.customApiParams?.messagePrefix,
   userHasSubmittedMessage,
   trackEvent,
   props.pageType,
 ],
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    [
      props.authToken,
      sessionId,
      props.customApiParams?.messagePrefix,
      userHasSubmittedMessage,
      trackEvent,
      props.pageType,
    ],
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx
around lines 194 to 202, the useCallback hook includes contextFilters in its
dependency array, but contextFilters is not used inside the callback function.
Remove contextFilters from the dependency array to avoid unnecessary
re-creations of the callback and keep dependencies accurate.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit dca105c
created_at: 2025-05-22T18:54:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,345 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { useTrack } from "hooks/analytics/useTrack";
+import { ArrowRightIcon } from "lucide-react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { useCallback, useState } from "react";
+import type { ThirdwebClient } from "thirdweb";
+import {
+  useActiveWallet,
+  useActiveWalletConnectionStatus,
+} from "thirdweb/react";
+import type { NebulaContext } from "../../api/chat";
+// import { createSession } from "../../api/session"; // REMOVE
+import type { NebulaUserMessage } from "../../api/types";
+import type { ExamplePrompt } from "../../data/examplePrompts";
+import { NebulaIcon } from "../../icons/NebulaIcon";
+import { ChatBar } from "../ChatBar";
+import { Chats } from "../Chats";
+import type { ChatMessage } from "../Chats";
+
+export default function CustomChatContent(props: {
+  authToken: string | undefined;
+  client: ThirdwebClient;
+  examplePrompts: ExamplePrompt[];
+  pageType: "chain" | "contract" | "support";
+  networks: NebulaContext["networks"];
+  customApiParams:
+    | {
+        messagePrefix: string;
+        chainIds: number[];
+        wallet: string | undefined;
+      }
+    | undefined;
+  requireLogin?: boolean;
+}) {
+  if (props.requireLogin !== false && !props.authToken) {
+    return <LoggedOutStateChatContent />;
+  }
+
+  return (
+    <CustomChatContentLoggedIn
+      networks={props.networks}
+      authToken={props.authToken || ""}
+      client={props.client}
+      customApiParams={props.customApiParams}
+      examplePrompts={props.examplePrompts}
+      pageType={props.pageType}
+    />
+  );
+}
+
+function CustomChatContentLoggedIn(props: {
+  authToken: string;
+  client: ThirdwebClient;
+  pageType: "chain" | "contract" | "support";
+  examplePrompts: ExamplePrompt[];
+  networks: NebulaContext["networks"];
+  customApiParams:
+    | {
+        messagePrefix: string;
+        chainIds: number[];
+        wallet: string | undefined;
+      }
+    | undefined;
+}) {
+  const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false);
+  const [messages, setMessages] = useState<Array<ChatMessage>>([]);
+  // sessionId is initially undefined, will be set to conversationId from API after first response
+  const [sessionId, setSessionId] = useState<string | undefined>(undefined);
+  const [chatAbortController, setChatAbortController] = useState<
+    AbortController | undefined
+  >();
+  const trackEvent = useTrack();
+  const [isChatStreaming, setIsChatStreaming] = useState(false);
+  const [enableAutoScroll, setEnableAutoScroll] = useState(false);
+  const connectionStatus = useActiveWalletConnectionStatus();
+  const activeWallet = useActiveWallet();
+
+  const [contextFilters, setContextFilters] = useState<
+    NebulaContext | undefined
+  >(() => {
+    return {
+      chainIds:
+        props.customApiParams?.chainIds.map((chainId) => chainId.toString()) ||
+        null,
+      walletAddress: props.customApiParams?.wallet || null,
+      networks: props.networks,
+    };
+  });
+
+  const handleSendMessage = useCallback(
+    async (userMessage: NebulaUserMessage) => {
+      const abortController = new AbortController();
+      setUserHasSubmittedMessage(true);
+      setIsChatStreaming(true);
+      setEnableAutoScroll(true);
+
+      const textMessage = userMessage.content.find((x) => x.type === "text");
+
+      trackEvent({
+        category: "floating_siwa",
+        action: "send",
+        label: "message",
+        message: textMessage?.text,
+        page: props.pageType,
+        sessionId: sessionId,
+      });
+
+      setMessages((prev) => [
+        ...prev,
+        {
+          type: "user",
+          content: userMessage.content,
+        },
+        // instant loading indicator feedback to user
+        {
+          type: "presence",
+          texts: [],
+        },
+      ]);
+
+      const messagePrefix = props.customApiParams?.messagePrefix;
+
+      // if this is first message, set the message prefix
+      // deep clone `userMessage` to avoid mutating the original message, its a pretty small object so JSON.parse is fine
+      const messageToSend = JSON.parse(
+        JSON.stringify(userMessage),
+      ) as NebulaUserMessage;
+
+      // if this is first message, set the message prefix
+      if (messagePrefix && !userHasSubmittedMessage) {
+        const textMessage = messageToSend.content.find(
+          (x) => x.type === "text",
+        );
+        if (textMessage) {
+          textMessage.text = `${messagePrefix}\n\n${textMessage.text}`;
+        }
+      }
+
+      try {
+        setChatAbortController(abortController);
+        // --- Custom API call ---
+        const payload: any = {
+          message:
+            messageToSend.content.find((x) => x.type === "text")?.text ?? "",
+          authToken: props.authToken,
+          conversationId: "25000000005",
+        };
+        if (sessionId) {
+          payload.conversationId = sessionId;
+        }
+        const response = await fetch(
+          "https://siwa-api.thirdweb-dev.com/api/chat",
+          {
+            method: "POST",
+            headers: {
+              "Content-Type": "application/json",
+            },
+            body: JSON.stringify(payload),
+            signal: abortController.signal,
+          },
+        );
+        const data = await response.json();
+        // If the response contains a conversationId, set it as the sessionId for future messages
+        if (data.conversationId && data.conversationId !== sessionId) {
+          setSessionId(data.conversationId);
+        }
+        setMessages((prev) => [
+          ...prev.slice(0, -1), // remove presence indicator
+          {
+            type: "assistant",
+            request_id: undefined,
+            text: data.data,
+          },
+        ]);
+      } catch (error) {
+        if (abortController.signal.aborted) {
+          return;
+        }
+        setMessages((prev) => [
+          ...prev.slice(0, -1),
+          {
+            type: "assistant",
+            request_id: undefined,
+            text: "Sorry, something went wrong.",
+          },
+        ]);
+      } finally {
+        setIsChatStreaming(false);
+        setEnableAutoScroll(false);
+      }
+    },
+    [
+      props.authToken,
+      contextFilters,
+      sessionId,
+      props.customApiParams?.messagePrefix,
+      userHasSubmittedMessage,
+      trackEvent,
+      props.pageType,
+    ],
+  );
+
+  const showEmptyState = !userHasSubmittedMessage && messages.length === 0;
+  return (
+    <div className="flex grow flex-col overflow-hidden">
+      {showEmptyState ? (
+        <EmptyStateChatPageContent
+          sendMessage={handleSendMessage}
+          examplePrompts={props.examplePrompts}
+        />
+      ) : (
+        <Chats
+          messages={messages}
+          isChatStreaming={isChatStreaming}
+          authToken={props.authToken}
+          sessionId={sessionId}
+          className="min-w-0 pb-10"
+          client={props.client}
+          enableAutoScroll={enableAutoScroll}
+          setEnableAutoScroll={setEnableAutoScroll}
+          useSmallText
+          sendMessage={handleSendMessage}
+        />
+      )}
+      <ChatBar
+        onLoginClick={undefined}
+        client={props.client}
+        isConnectingWallet={connectionStatus === "connecting"}
+        context={contextFilters}
+        setContext={setContextFilters}
+        showContextSelector={false}
+        connectedWallets={
+          props.customApiParams?.wallet && activeWallet
+            ? [
+                {
+                  address: props.customApiParams.wallet,
+                  walletId: activeWallet.id,
+                },
+              ]
+            : []
+        }
+        setActiveWallet={() => {}}
+        abortChatStream={() => {
+          chatAbortController?.abort();
+          setChatAbortController(undefined);
+          setIsChatStreaming(false);
+          // if last message is presence, remove it
+          if (messages[messages.length - 1]?.type === "presence") {
+            setMessages((prev) => prev.slice(0, -1));
+          }
+        }}
+        isChatStreaming={isChatStreaming}
+        prefillMessage={undefined}
+        sendMessage={handleSendMessage}
+        className="rounded-none border-x-0 border-b-0"
+        allowImageUpload={false}
+      />
path: apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Consider handling error states for wallet connection**

The component passes wallet information when available but doesn't handle potential error states for wallet connection. Consider showing appropriate error messages or fallbacks when wallet connection fails.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for error handling patterns in wallet-related components

# Look for error handling in wallet connection code
rg -A 10 "useActiveWallet|useActiveWalletConnectionStatus" --type ts
```

Length of output: 67063

---

I’ll inspect the CustomChatContent component to see how it’s wiring up wallet connection state and whether any error cases are surfaced.

---

🏁 Script executed:

```shell
#!/bin/bash
set -e

FILE="apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx"

echo "=== Top of file (imports + hook setup) ==="
sed -n '1,50p' "$FILE"

echo
echo "=== Looking for wallet hooks ==="
grep -nE "useActiveWallet|useActiveWalletConnectionStatus" -n "$FILE" || true

echo
echo "=== Searching for any 'error' references ==="
grep -n "error" -n "$FILE" || true
```

Length of output: 2568

---

Gathering the logged-in chat component and its error handling around wallet connection…

---

🏁 Script executed:

```shell
#!/bin/bash
set -e

FILE="apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx"

echo "=== CustomChatContentLoggedIn definition (lines 50–150) ==="
sed -n '50,150p' "$FILE"

echo
echo "=== Message send & catch block (lines 150–240) ==="
sed -n '150,240p' "$FILE"
```

Length of output: 6525

---

Investigating the `ChatBar` component’s props and whether it surfaces wallet errors:

---

🏁 Script executed:

```shell
#!/bin/bash
set -e

# Locate the ChatBar component
echo "=== ChatBar file path ==="
rg -l "export function ChatBar" -n apps/dashboard/src/app/nebula-app/(app)/components

# Print its prop definitions and error-related logic
FILE="apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx"
echo
echo "=== ChatBar props and logic (lines 1–200) ==="
sed -n '1,200p' "$FILE"

# Search for any 'error' references in ChatBar
echo
echo "=== ChatBar error handling references ==="
grep -n "error" -n "$FILE" || true
```

Length of output: 179

---

Retrieving the `ChatBar` implementation to verify error handling:

---

🏁 Script executed:

```shell
#!/bin/bash
set -e

# Locate ChatBar.tsx within the dashboard components
FILE=$(find apps/dashboard/src/app -type f -name ChatBar.tsx | head -n1)
echo "=== ChatBar file path ==="
echo "$FILE"

echo
echo "=== ChatBar props and logic (first 200 lines) ==="
sed -n '1,200p' "$FILE"

echo
echo "=== References to 'error' or similar inside ChatBar ==="
grep -nE "error|fail|warning" -n "$FILE" || true
```

Length of output: 7549

---



</details>

**Add explicit wallet connection error handling**  

Right now, we only surface the “connecting” state via a spinner, but if the wallet fails to connect (connectionStatus becomes `"disconnected"` or `"error"`), there’s no UI or fallback. We should:  

• In CustomChatContentLoggedIn (around line 226), pass a real `onLoginClick` handler (not `undefined`), and detect `connectionStatus === "error"` to render an error message or retry button.  
• In ChatBar (in `ChatBar.tsx`), accept and render an error state when `isConnectingWallet === false && connectedWallets.length === 0` or via a new `hasConnectionError: boolean` prop.  

Example diff in CustomChatContentLoggedIn:  
```diff
- <ChatBar
-   onLoginClick={undefined}
+ <ChatBar
+   onLoginClick={() => /* trigger wallet modal or retry */ }
    client={props.client}
-   isConnectingWallet={connectionStatus === "connecting"}
+   isConnectingWallet={connectionStatus === "connecting"}
+   hasConnectionError={connectionStatus === "error"}
    …  
/>
```

And in ChatBar render near the selector:  
```tsx
{props.hasConnectionError && (
  <Badge variant="destructive" className="px-2">
    Wallet connection failed. <Button onClick={props.onLoginClick}>Retry</Button>
  </Badge>
)}
```  
This ensures users see and can recover from wallet connection failures.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx
around lines 226 to 259, the component currently passes undefined for
onLoginClick and does not handle wallet connection errors. Fix this by
implementing a real onLoginClick handler to trigger wallet login and detect when
connectionStatus is "error" to render an error message or retry UI. Also, update
the ChatBar component to accept a new prop like hasConnectionError and render an
error badge with a retry button when wallet connection fails or no wallets are
connected, ensuring users can see and recover from connection issues.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-22T18:54:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,284 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { useTrack } from "hooks/analytics/useTrack";
+import { ArrowRightIcon } from "lucide-react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { useCallback, useState } from "react";
+import type { ThirdwebClient } from "thirdweb";
+import { useActiveWalletConnectionStatus } from "thirdweb/react";
+import type { NebulaContext } from "../../api/chat";
+import type { NebulaUserMessage } from "../../api/types";
+import type { ExamplePrompt } from "../../data/examplePrompts";
+import { NebulaIcon } from "../../icons/NebulaIcon";
+import { ChatBar } from "../ChatBar";
+import { Chats } from "../Chats";
+import type { ChatMessage } from "../Chats";
+
+export default function CustomChatContent(props: {
+  authToken: string | undefined;
+  teamId: string | undefined;
+  clientId: string | undefined;
+  client: ThirdwebClient;
+  examplePrompts: ExamplePrompt[];
+  networks: NebulaContext["networks"];
+  requireLogin?: boolean;
+}) {
+  if (props.requireLogin !== false && !props.authToken) {
+    return <LoggedOutStateChatContent />;
+  }
+
+  return (
+    <CustomChatContentLoggedIn
+      networks={props.networks}
+      teamId={props.teamId}
+      clientId={props.clientId}
+      authToken={props.authToken || ""}
+      client={props.client}
+      examplePrompts={props.examplePrompts}
+    />
+  );
+}
+
+function CustomChatContentLoggedIn(props: {
+  authToken: string;
+  teamId: string | undefined;
+  clientId: string | undefined;
+  client: ThirdwebClient;
+  examplePrompts: ExamplePrompt[];
+  networks: NebulaContext["networks"];
+}) {
+  const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false);
+  const [messages, setMessages] = useState<Array<ChatMessage>>([]);
+  // sessionId is initially undefined, will be set to conversationId from API after first response
+  const [sessionId, setSessionId] = useState<string | undefined>(undefined);
+  const [chatAbortController, setChatAbortController] = useState<
+    AbortController | undefined
+  >();
+  const trackEvent = useTrack();
+  const [isChatStreaming, setIsChatStreaming] = useState(false);
+  const [enableAutoScroll, setEnableAutoScroll] = useState(false);
+  const connectionStatus = useActiveWalletConnectionStatus();
+
+  const handleSendMessage = useCallback(
+    async (userMessage: NebulaUserMessage) => {
+      const abortController = new AbortController();
+      setUserHasSubmittedMessage(true);
+      setIsChatStreaming(true);
+      setEnableAutoScroll(true);
+
+      const textMessage = userMessage.content.find((x) => x.type === "text");
+
+      trackEvent({
+        category: "floating_siwa",
+        action: "send",
+        label: "message",
+        message: textMessage?.text,
+        sessionId: sessionId,
+      });
+
+      setMessages((prev) => [
+        ...prev,
+        {
+          type: "user",
+          content: userMessage.content,
+        },
+        // instant loading indicator feedback to user
+        {
+          type: "presence",
+          texts: [],
+        },
+      ]);
+
+      // if this is first message, set the message prefix
+      // deep clone `userMessage` to avoid mutating the original message, its a pretty small object so JSON.parse is fine
+      const messageToSend = JSON.parse(
+        JSON.stringify(userMessage),
+      ) as NebulaUserMessage;
+
+      try {
+        setChatAbortController(abortController);
+        // --- Custom API call ---
+        const payload = {
+          message:
+            messageToSend.content.find((x) => x.type === "text")?.text ?? "",
+          conversationId: sessionId,
+        };
+        const apiUrl = process.env.NEXT_PUBLIC_SIWA_URL;
+        const response = await fetch(`${apiUrl}/v1/chat`, {
+          method: "POST",
+          headers: {
+            "Content-Type": "application/json",
+            Authorization: `Bearer ${props.authToken}`,
+            ...(props.teamId ? { "x-team-id": props.teamId } : {}),
+            ...(props.clientId ? { "x-client-id": props.clientId } : {}),
+          },
+          body: JSON.stringify(payload),
+          signal: abortController.signal,
+        });
+        const data = await response.json();
+        // If the response contains a conversationId, set it as the sessionId for future messages
+        if (data.conversationId && data.conversationId !== sessionId) {
+          setSessionId(data.conversationId);
+        }
+        setMessages((prev) => [
+          ...prev.slice(0, -1), // remove presence indicator
+          {
+            type: "assistant",
+            request_id: undefined,
+            text: data.data,
+          },
+        ]);
+      } catch (error) {
+        if (abortController.signal.aborted) {
+          return;
+        }
+        setMessages((prev) => [
+          ...prev.slice(0, -1),
+          {
+            type: "assistant",
+            request_id: undefined,
+            text: `Sorry, something went wrong. ${error instanceof Error ? error.message : "Unknown error"}`,
+          },
+        ]);
+      } finally {
+        setIsChatStreaming(false);
+        setEnableAutoScroll(false);
+      }
+    },
+    [props.authToken, props.clientId, props.teamId, sessionId, trackEvent],
+  );
+
+  const showEmptyState = !userHasSubmittedMessage && messages.length === 0;
+  return (
+    <div className="flex grow flex-col overflow-hidden">
+      {showEmptyState ? (
+        <EmptyStateChatPageContent
+          sendMessage={handleSendMessage}
+          examplePrompts={props.examplePrompts}
+        />
+      ) : (
+        <Chats
+          messages={messages}
+          isChatStreaming={isChatStreaming}
+          authToken={props.authToken}
+          sessionId={sessionId}
+          className="min-w-0 pb-10"
+          client={props.client}
+          enableAutoScroll={enableAutoScroll}
+          setEnableAutoScroll={setEnableAutoScroll}
+          useSmallText
+          sendMessage={handleSendMessage}
+        />
+      )}
+      <ChatBar
+        placeholder={"Ask AI"}
+        onLoginClick={undefined}
+        client={props.client}
+        isConnectingWallet={connectionStatus === "connecting"}
+        context={undefined}
+        setContext={() => {}}
+        showContextSelector={false}
+        connectedWallets={[]}
+        setActiveWallet={() => {}}
+        abortChatStream={() => {
+          chatAbortController?.abort();
+          setChatAbortController(undefined);
+          setIsChatStreaming(false);
+          // if last message is presence, remove it
+          if (messages[messages.length - 1]?.type === "presence") {
+            setMessages((prev) => prev.slice(0, -1));
+          }
+        }}
+        isChatStreaming={isChatStreaming}
+        prefillMessage={undefined}
+        sendMessage={handleSendMessage}
+        className="rounded-none border-x-0 border-b-0"
+        allowImageUpload={false}
+      />
path: apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Add wallet connection error handling.**

The component currently handles the "connecting" state but doesn't provide feedback for failed wallet connections.

Consider implementing error handling for wallet connection failures by adding a new prop to ChatBar (e.g., hasConnectionError) and providing a meaningful error message and retry option:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for any existing error handling in ChatBar component

# Find the ChatBar component file
echo "Locating ChatBar component..."
CHAT_BAR_FILE=$(find apps/dashboard/src/app -name "ChatBar.tsx" -type f | head -n 1)
echo "Found ChatBar at: $CHAT_BAR_FILE"

# Check for error handling related to wallet connection
echo -e "\nChecking for wallet connection error handling..."
grep -n -A 5 -B 5 "isConnectingWallet" "$CHAT_BAR_FILE" || echo "No isConnectingWallet found"

# Check if there's any existing error state rendering
echo -e "\nChecking for error state rendering in ChatBar..."
grep -n -A 5 -B 5 "error" "$CHAT_BAR_FILE" || echo "No explicit error handling found"
```

Length of output: 4530

---



</details>

**Add wallet connection error handling**

The `ChatBar` component today only covers the “connecting” spinner (`isConnectingWallet`) but never surfaces failures. Let’s extend it to surface and recover from wallet‐connection errors:

• In `apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx`  
  – Add new props:  
    • `connectionError?: string`  
    • `onRetryConnection?: () => void`  
  – Below the wallet selector (or spinner), render an error message when `connectionError` is set, plus a “Retry” button that calls `onRetryConnection`.

• In `apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx` (around lines 174–198)  
  – Wire `onLoginClick` to your wallet-connect function.  
  – Capture failures to set a local `connectionError` state and pass both `connectionError` and a retry handler into `<ChatBar … />`.  

This will give users clear feedback on failures and a simple way to retry.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx
around lines 174 to 198, the ChatBar component handles wallet connection loading
but lacks error feedback for connection failures. Fix this by adding a local
state variable to track connection errors and a retry handler function that
attempts to reconnect the wallet. Pass these as new props connectionError and
onRetryConnection to ChatBar. Also, wire onLoginClick to the wallet-connect
function to initiate connection attempts. This will enable ChatBar to display
error messages and provide a retry option for failed wallet connections.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-23T00:09:01Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-05-22T18:43:50Z
body: ###  ⚠️  No Changeset found

Latest commit: b32f10b7ab2864ec2b2e9e6ac3b5c4b228f281d4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/thirdweb-dev/js/new/yash/add-siwa?filename=.changeset/grumpy-rivers-destroy.md&value=---%0A%22thirdweb-dashboard%22%3A%20patch%0A---%0A%0A%5BDashboard%5D%20siwa%20chat%20widget%0A)


author_association: NONE
user: login: vercel[bot]
created_at: 2025-05-22T18:43:51Z
body: [vc]: #9tmc/Hybnlodile50HMqO5nbk0zNCAE4uoclb6A8Tsk=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ0aGlyZHdlYl9wbGF5Z3JvdW5kIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvcGxheWdyb3VuZC13ZWIiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS90aGlyZHdlYi90aGlyZHdlYl9wbGF5Z3JvdW5kLzJmWlZSdUdVTkdSUVRoNzhoQmEyQVFmSmQ0cE4iLCJwcmV2aWV3VXJsIjoiIiwibmV4dENvbW1pdFN0YXR1cyI6IlNLSVBQRUQifSx7Im5hbWUiOiJ0aGlyZHdlYi13d3ciLCJyb290RGlyZWN0b3J5IjoiYXBwcy9kYXNoYm9hcmQiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vdGhpcmR3ZWIvdGhpcmR3ZWItd3d3Lzc5UnlMdHJQVFZQNDJGY0hZeW50bXo2QUg3dEIiLCJwcmV2aWV3VXJsIjoidGhpcmR3ZWItd3d3LWdpdC15YXNoLWFkZC1zaXdhLnRoaXJkd2ViLXByZXZpZXcuY29tIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InRoaXJkd2ViLXd3dy1naXQteWFzaC1hZGQtc2l3YS50aGlyZHdlYi1wcmV2aWV3LmNvbSJ9fSx7Im5hbWUiOiJ3YWxsZXQtdWkiLCJyb290RGlyZWN0b3J5IjoiYXBwcy93YWxsZXQtdWkiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS90aGlyZHdlYi93YWxsZXQtdWkvOXNFZThuMUtDclhmUGJuYWlKMm01TWdEUWhFSCIsInByZXZpZXdVcmwiOiIiLCJuZXh0Q29tbWl0U3RhdHVzIjoiU0tJUFBFRCJ9LHsibmFtZSI6ImRvY3MtdjIiLCJyb290RGlyZWN0b3J5IjoiYXBwcy9wb3J0YWwiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS90aGlyZHdlYi9kb2NzLXYyLzRyZkFQV285MUVoV0w0ckIycFBqVDVoMzF3emIiLCJwcmV2aWV3VXJsIjoiIiwibmV4dENvbW1pdFN0YXR1cyI6IlNLSVBQRUQifSx7Im5hbWUiOiJsb2dpbiIsInJvb3REaXJlY3RvcnkiOiJhcHBzL2xvZ2luIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vdGhpcmR3ZWIvbG9naW4vNkpDNDl6RlBDelpQOVRNUkZOQm1qenAxa3p4NCIsInByZXZpZXdVcmwiOiIiLCJuZXh0Q29tbWl0U3RhdHVzIjoiU0tJUFBFRCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **thirdweb-www** | ✅ Ready ([Inspect](https://vercel.com/thirdweb/thirdweb-www/79RyLtrPTVP42FcHYyntmz6AH7tB)) | [Visit Preview](https://thirdweb-www-git-yash-add-siwa.thirdweb-preview.com) | 💬 [**Add feedback**](https://vercel.live/open-feedback/thirdweb-www-git-yash-add-siwa.thirdweb-preview.com?via=pr-comment-feedback-link) | May 23, 2025 4:19am |

<details><summary>4 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **docs-v2** | ⬜️ Skipped ([Inspect](https://vercel.com/thirdweb/docs-v2/4rfAPWo91EhWL4rB2pPjT5h31wzb)) |  |  | May 23, 2025 4:19am |
| **login** | ⬜️ Skipped ([Inspect](https://vercel.com/thirdweb/login/6JC49zFPCzZP9TMRFNBmjzp1kzx4)) |  |  | May 23, 2025 4:19am |
| **thirdweb_playground** | ⬜️ Skipped ([Inspect](https://vercel.com/thirdweb/thirdweb_playground/2fZVRuGUNGRQTh78hBa2AQfJd4pN)) |  |  | May 23, 2025 4:19am |
| **wallet-ui** | ⬜️ Skipped ([Inspect](https://vercel.com/thirdweb/wallet-ui/9sEe8n1KCrXfPbnaiJ2m5MgDQhEH)) |  |  | May 23, 2025 4:19am |
</details>






author_association: NONE
user: login: graphite-app[bot]
created_at: 2025-05-22T18:43:54Z
body: ## How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:
- <kbd>merge-queue</kbd> - adds this PR to the back of the merge queue
- <kbd>hotfix</kbd> - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/thirdweb-dev?ref=merge-queue-instructions-comment&prId=6258885316).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-05-22T18:43:54Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The changes introduce a new custom chat button and chat content components, replacing the previous Nebula chat button in the support page. The support chat now uses a different label, passes authentication tokens, and updates API parameter handling. Additionally, a feedback feature for assistant messages is added to the chat component, allowing users to rate responses.

## Changes

| File(s)                                                                                             | Change Summary                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
|-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx                                           | Replaced `NebulaChatButton` with `CustomChatButton`, removed thirdweb client creation, fetched teams asynchronously to get `teamId`, changed button label to "Ask AI for support", removed prompt prefix and old example prompts, added exported `siwaExamplePrompts`, passed `authToken`, `teamId`, and `clientId` props to chat button, and removed `nebulaParams`.                                                                                                                                                                                                                                                   |
| apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx                                        | Added new `FeedbackButtons` component for assistant messages to submit positive/negative feedback asynchronously to an API endpoint with session and team context; manages loading and thank-you states. Added `teamId` parameter propagation. Existing message feedback UI remains unchanged. Imported new icons, hooks, and button component.                                                                                                                                                                                                                                                              |
| apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx                  | Added new `CustomChatButton` React component managing modal open state, rendering a button and popup chat modal, passing props including `authToken`, `teamId`, and `clientId`. Uses a thirdweb client instance internally and conditionally renders `CustomChatContent` with example prompts mapped to objects.                                                                                                                                                                                                                                               |
| apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx                 | Added new `CustomChatContent` React component with authentication gating: renders sign-in prompt if not logged in, otherwise manages chat state with message streaming, session ID, abort controller, and analytics. Sends user messages to a proxied API with auth headers and team/client IDs. Renders empty state with example prompts or chat messages and input bar. Includes internal components for logged-out state and empty state with example prompts.                                                                                                                                   |
| apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx             | Replaced local example prompt strings with imported `examplePrompts` array for `NebulaChatButton` usage; simplified prompt mapping to direct array pass-through.                                                                                                                                                                                                                                                                                                                                                                                             |
| apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx                                     | Added a floating `CustomChatButton` at bottom-right of team layout page with props including logged-in status, all networks, floating state, label "Ask AI Assistant", team context, example prompts, and auth token.                                                                                                                                                                                                                                                                                                                                  |
| apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/layout.tsx                             | Added a floating `CustomChatButton` at bottom-right of project layout page with props including logged-in status, all networks, floating state, page type "support", label "Ask AI Assistant", example prompts `siwaExamplePrompts`, team ID, client ID from project publishable key, and auth token.                                                                                                                                                                                                                                                  |
| apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx                                           | Added authentication check in `RootTeamLayout` async function by importing and calling `getAuthToken`; redirects unauthenticated users to `/login` before existing logic.                                                                                                                                                                                                                                                                                                                                                                              |
| apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx                                     | Extended `ChatBar` component props to include required `placeholder` string; replaced hardcoded placeholder with prop value in the input textarea.                                                                                                                                                                                                                                                                                                                                                                                                        |
| apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx                             | Added `placeholder="Ask Nebula"` prop to `ChatBar` component usage.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| apps/dashboard/src/app/nebula-app/(app)/components/Chatbar.stories.tsx                            | Added static `placeholder="Ask Nebula"` prop to `ChatBar` component in storybook variant.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx                   | Added `placeholder="Ask Nebula"` prop to `ChatBar` component; simplified `ExamplePrompt` component props type annotation from multiline to single-line inline.                                                                                                                                                                                                                                                                                                                                                                                                      |
| apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx            | Added `placeholder="Ask Nebula"` prop to `ChatBar` component; simplified `ExamplePromptButton` props type annotation from multiline to single-line inline.                                                                                                                                                                                                                                                                                                                                                                                                        |
| apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx                              | Added explicit `teamId={undefined}` prop to `Chats` component invocation in storybook variant.                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant SupportPage
    participant CustomChatButton
    participant CustomChatContent
    participant API
    participant Auth

    User->>SupportPage: Visits support page
    SupportPage->>CustomChatButton: Renders chat button (with authToken)
    User->>CustomChatButton: Clicks chat button
    CustomChatButton->>CustomChatContent: Opens chat modal
    CustomChatContent->>Auth: Checks if user is authenticated
    alt Not Authenticated
        CustomChatContent->>User: Shows sign-in prompt
    else Authenticated
        CustomChatContent->>User: Shows chat UI
        User->>CustomChatContent: Submits message
        CustomChatContent->>API: Sends message with authToken and session info
        API-->>CustomChatContent: Streams assistant response
        CustomChatContent->>User: Displays assistant response
        User->>CustomChatContent: Provides feedback (good/bad)
        CustomChatContent->>API: Sends feedback with sessionId, message, rating
        API-->>CustomChatContent: Acknowledges feedback
        CustomChatContent->>User: Shows thank-you message
    end
```

<!-- walkthrough_end -->

<!-- This is an auto-generated comment: resource warnings by coderabbit.ai -->

> [!WARNING]
> ## Review ran into problems
> 
> <details>
> <summary>🔥 Problems</summary>
> 
> Errors were encountered while retrieving linked issues.
> 
> <details>
> <summary>Errors (1)</summary>
> 
> * TEAM-0000: Entity not found: Issue - Could not find referenced Issue.
> 
> </details>
> 
> </details>

<!-- end of auto-generated comment: resource warnings by coderabbit.ai -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3a50e8c74736e36bfddab54d2bb980f8a8b928cb and b32f10b7ab2864ec2b2e9e6ac3b5c4b228f281d4.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx` (2 hunks)
* `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/layout.tsx` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx
* apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/layout.tsx

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: E2E Tests (pnpm, webpack)
* GitHub Check: Unit Tests
* GitHub Check: E2E Tests (pnpm, vite)
* GitHub Check: Lint Packages
* GitHub Check: Size
* GitHub Check: Analyze (javascript)

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=thirdweb-dev/js&utm_content=7128):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-22T18:46:08Z
body: ## [Codecov](https://app.codecov.io/gh/thirdweb-dev/js/pull/7128?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thirdweb-dev) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 55.61%. Comparing base [(`526b6b5`)](https://app.codecov.io/gh/thirdweb-dev/js/commit/526b6b54cfb4a28f4dcc16e7e3ae39d73a6eff40?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thirdweb-dev) to head [(`b32f10b`)](https://app.codecov.io/gh/thirdweb-dev/js/commit/b32f10b7ab2864ec2b2e9e6ac3b5c4b228f281d4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thirdweb-dev).
> Report is 7 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #7128   +/-   ##
=======================================
  Coverage   55.61%   55.61%           
=======================================
  Files         902      902           
  Lines       58177    58177           
  Branches     4085     4085           
=======================================
  Hits        32356    32356           
  Misses      25716    25716           
  Partials      105      105           
```

| [Flag](https://app.codecov.io/gh/thirdweb-dev/js/pull/7128/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thirdweb-dev) | Coverage Δ | |
|---|---|---|
| [packages](https://app.codecov.io/gh/thirdweb-dev/js/pull/7128/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=thirdweb-dev) | `55.61% <ø> (ø)` | |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-22T18:47:48Z
body: ## size-limit report 📦 
| Path                                    | Size           | Loading time (3g) | Running time (snapdragon) | Total time |
| --------------------------------------- | -------------- | ----------------- | ------------------------- | ---------- |
| thirdweb (esm)                          | 56.62 KB (0%)  | 1.2 s (0%)        | 430 ms (+99.59% 🔺)       | 1.6 s      |
| thirdweb (cjs)                          | 309.13 KB (0%) | 6.2 s (0%)        | 2.1 s (+1.71% 🔺)         | 8.3 s      |
| thirdweb (minimal + tree-shaking)       | 5.69 KB (0%)   | 114 ms (0%)       | 148 ms (+877.68% 🔺)      | 262 ms     |
| thirdweb/chains (tree-shaking)          | 531 B (0%)     | 11 ms (0%)        | 66 ms (+1089.79% 🔺)      | 76 ms      |
| thirdweb/react (minimal + tree-shaking) | 19.5 KB (0%)   | 390 ms (0%)       | 228 ms (+483.7% 🔺)       | 618 ms     |
author_association: CONTRIBUTOR