Skip to content

Conversation

@joaquim-verges
Copy link
Member

@joaquim-verges joaquim-verges commented Sep 8, 2025


PR-Codex overview

This PR focuses on updating the Nebula application to thirdweb AI, enhancing functionality, and improving UI components related to AI interactions and transactions.

Detailed summary

  • Updated references from Nebula to thirdweb AI.
  • Added AbortSignal to request options in proxies.ts.
  • Introduced new AI-related components and stores.
  • Improved chat functionalities and session management.
  • Enhanced transaction handling and feedback mechanisms.
  • Updated package dependencies in package.json.

The following files were skipped due to too many changes: apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/Swap/common.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/Chatbar.stories.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/Chats.stories.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/EmptyStateChatPageContent.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/api/chat.ts, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/TransactionsSection/TransactionsSection.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/chat/history/ChatHistoryPage.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/Swap/SwapCards.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatSidebar.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/AssetsSection/AssetsSection.tsx, pnpm-lock.yaml, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/Chats.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatBar.tsx, apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatPageContent.tsx

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

Summary by CodeRabbit

  • New Features

    • thirdweb AI chat: streaming responses, image attachments (upload/paste/drag), wallet & chain context, session creation/management, chat page, history with fuzzy search & deletion, sidebar & mobile nav.
    • In-chat executable flows: transaction & swap cards with status tracking; image viewer with fullscreen, download, and feedback.
    • Assets and Transactions panels showing multi-chain balances and recent activity.
  • Improvements

    • Rebranded Nebula → thirdweb AI and updated sign-in copy.
  • Chores

    • Added AI host env var, fetch-event-stream dependency, Storybook demos, image upload button, and local session stores.

@changeset-bot
Copy link

changeset-bot bot commented Sep 8, 2025

⚠️ No Changeset found

Latest commit: 1f6c03e

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.

This PR includes no changesets

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

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Sep 8, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
docs-v2 Ready Ready Preview Comment Sep 9, 2025 0:09am
nebula Ready Ready Preview Comment Sep 9, 2025 0:09am
thirdweb_playground Ready Ready Preview Comment Sep 9, 2025 0:09am
thirdweb-www Ready Ready Preview Comment Sep 9, 2025 0:09am
1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
wallet-ui Skipped Skipped Sep 9, 2025 0:09am

@github-actions github-actions bot added the Dashboard Involves changes to the Dashboard. label Sep 8, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 8, 2025

Walkthrough

Adds a thirdweb AI chat integration: streaming client, session CRUD and feedback endpoints, authenticated fetch helper, many UI components (chat UI, sidebar, mobile nav, assets/transactions, transaction/swap cards), image upload/viewer, local session stores, Storybook stories, an env constant, branding text updates, and a package dependency.

Changes

Cohort / File(s) Summary
AI API & Types
apps/dashboard/src/app/.../(sidebar)/ai/api/types.ts, .../chat.ts, .../session.ts, .../fetchWithAuthToken.ts, .../feedback.ts, apps/dashboard/src/@/constants/public-envs.ts
Adds Nebula/thirdweb AI TypeScript types, streaming client promptNebula, session CRUD client functions, fetchWithAuthToken helper with timeout and auth, submitFeedback server action, and NEXT_PUBLIC_THIRDWEB_AI_HOST env constant.
Chat Pages & Routing
apps/dashboard/src/app/.../(sidebar)/ai/page.tsx, .../chat/[session_id]/page.tsx, .../chat/history/page.tsx, .../analytics/page.tsx
Adds chat-focused pages (landing, session, history) and an analytics page; pages fetch auth/project/sessions, initialize clients, and render new layouts or notFound/redirect as appropriate.
Core Chat UI & Composer
apps/dashboard/src/app/.../(sidebar)/ai/components/ChatPageContent.tsx, ChatPageLayout.tsx, Chats.tsx, ChatBar.tsx, EmptyStateChatPageContent.tsx
Implements chat orchestration, streaming event handling, message parsing/rendering, ChatBar with image upload/paste/drag, wallet/context selection, session management, and top-level layout/content components.
Image Upload & Viewer
apps/dashboard/src/@/components/ui/image-upload-button.tsx, .../NebulaImage.tsx
Adds ImageUploadButton component and NebulaImage viewer with IPFS resolution, fullscreen dialog, client-side download flow, and optional message actions.
Message Actions & Feedback UI
.../MessageActions.tsx, .../api/feedback.ts
Adds copy and thumbs-up/down feedback UI and server call to submit feedback to AI host.
Sidebar & Mobile Nav
.../ChatSidebar.tsx, .../ChatSidebarLink.tsx, .../NebulaMobileNav.tsx, ChatPageLayout.stories.tsx
Adds responsive chat sidebar, recent chats list with delete handling, New Chat routing, docs/analytics links, and mobile drawer navigation.
Wallet Panels: Assets & Transactions
.../AssetsSection/AssetsSection.tsx, .../TransactionsSection/TransactionsSection.tsx, stories...
Adds multi-chain assets and transactions sections with Insight API fetching, skeletons, chain icons, explorer links, and Storybook stories.
Swap & Tx Execution
.../ExecuteTransactionCard.tsx, .../Swap/SwapCards.tsx, .../Swap/common.tsx, swap stories...
Adds ExecuteTransactionCard, Swap/Approve transaction cards, TxStatus lifecycle UI, TxHash/Status rows, and useTxSetup orchestration hook.
Supporting Components & Hooks
.../NebulaConnectButton.tsx, .../Reasoning/Reasoning.tsx, .../hooks/useSessionsWithLocalOverrides.ts, .../stores.ts, many small components/stories...
Adds NebulaConnectWallet, Reasoning panel, local session stores and merge hook, and numerous helper components and Storybook stories.
Example Data & Storybook Stubs
apps/dashboard/src/app/.../data/examplePrompts.ts, apps/dashboard/src/@/storybook/stubs.ts
Adds examplePrompts dataset and randomLorem helper for stories.
Branding & Visibility Tweaks
apps/dashboard/src/app/(app)/(dashboard)/(chain)/components/server/products.ts, apps/dashboard/src/@/components/chat/CustomChatButton.tsx, apps/dashboard/src/@/components/chat/ChatBar.tsx, .../(sidebar)/layout.tsx, packages/service-utils/src/core/services.ts
Renames Nebula display to “thirdweb AI”, updates external links, broadens CustomChatButton visibility gating to hide on "ai" routes, updates a ChatBar sign-in copy to reference “thirdweb”, minor formatting change, and updates service title constant.
Type / Action Adjustments
apps/dashboard/src/@/actions/proxies.ts
Adds optional signal?: AbortSignal to ProxyActionParams type (type-only).
Layout Removal
apps/dashboard/src/@/components/blocks/full-width-sidebar-layout.tsx
Removes AppFooter rendering.
Package Dependency
apps/dashboard/package.json
Adds runtime dependency [email protected].

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant ChatBar
  participant ChatContent
  participant promptNebula
  participant AIHost as "AI Host (/chat)"

  User->>ChatBar: Type message / attach images
  ChatBar->>ChatContent: sendMessage(NebulaUserMessage)
  ChatContent->>promptNebula: promptNebula({message, sessionId, authToken, context, abortController})
  promptNebula->>AIHost: POST /chat (stream: true)
  loop server-sent events
    AIHost-->>promptNebula: event (init|delta|presence|image|action|context|error)
    promptNebula->>ChatContent: handleStream(event)
    alt delta/image/action
      ChatContent->>ChatContent: append/merge messages
    else presence/context/error
      ChatContent->>ChatContent: update presence/context or append error
    end
  end
  ChatContent-->>User: incremental UI updates
Loading
sequenceDiagram
  autonumber
  actor User
  participant UI
  participant SessionAPI
  participant AIHost

  User->>UI: Create/Open session
  UI->>SessionAPI: createSession/updateSession
  SessionAPI->>AIHost: POST/PUT /session
  AIHost-->>SessionAPI: SessionInfo/UpdatedSessionInfo
  SessionAPI-->>UI: SessionInfo
  User->>UI: submit feedback
  UI->>SessionAPI: submitFeedback({sessionId, requestId, rating})
  SessionAPI->>AIHost: POST /feedback
  AIHost-->>SessionAPI: 200 / error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Pre-merge checks (1 passed, 2 warnings)

❌ Failed Checks (2 warnings)
Check Name Status Explanation Resolution
Description Check ⚠️ Warning The PR description includes only the commented-out template and an automated summary, but omits the required title header, notes for the reviewer, and how-to-test sections as specified by the repository template. Update the PR description to explicitly include the formatted title line, "Notes for the reviewer" section, and "How to test" section per the repository’s template.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.15% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed Checks (1 passed)
Check Name Status Explanation
Title Check ✅ Passed Though the PR includes numerous feature additions, the overarching theme is embedding thirdweb AI in the dashboard, and the title reflects this central goal in a concise manner.

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • TEAM-0000: Entity not found: Issue - Could not find referenced Issue.
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 09-08-thirdweb_ai_inside_dashboard

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Member Author


How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • merge-queue - adds this PR to the back of the merge queue
  • hotfix - 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.

An organization admin has enabled the Graphite Merge Queue in this repository.

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

This stack of pull requests is managed by Graphite. Learn more about stacking.

@codecov
Copy link

codecov bot commented Sep 8, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 56.65%. Comparing base (e37bd8e) to head (1f6c03e).
⚠️ Report is 7 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #8003   +/-   ##
=======================================
  Coverage   56.64%   56.65%           
=======================================
  Files         904      904           
  Lines       58677    58677           
  Branches     4164     4165    +1     
=======================================
+ Hits        33236    33241    +5     
+ Misses      25335    25330    -5     
  Partials      106      106           
Flag Coverage Δ
packages 56.65% <ø> (+<0.01%) ⬆️
see 1 file with indirect coverage changes
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@joaquim-verges joaquim-verges marked this pull request as ready for review September 8, 2025 03:30
@joaquim-verges joaquim-verges requested review from a team as code owners September 8, 2025 03:30
@github-actions
Copy link
Contributor

github-actions bot commented Sep 8, 2025

size-limit report 📦

Path Size Loading time (3g) Running time (snapdragon) Total time
thirdweb (esm) 63.96 KB (0%) 1.3 s (0%) 316 ms (+141.35% 🔺) 1.6 s
thirdweb (cjs) 356.86 KB (0%) 7.2 s (0%) 1.1 s (+13.05% 🔺) 8.2 s
thirdweb (minimal + tree-shaking) 5.73 KB (0%) 115 ms (0%) 98 ms (+1269.94% 🔺) 213 ms
thirdweb/chains (tree-shaking) 526 B (0%) 11 ms (0%) 88 ms (+2755.85% 🔺) 99 ms
thirdweb/react (minimal + tree-shaking) 19.15 KB (0%) 383 ms (0%) 92 ms (+499.55% 🔺) 475 ms

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 33

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/dashboard/src/@/components/chat/ChatBar.tsx (2)

59-63: Block Enter-submit when message is empty, wallet is connecting, or IME composing

Pressing Enter currently submits even if the message is whitespace and even while a wallet is connecting; it also ignores IME composition. Align behavior with the Send button and avoid accidental sends.

Apply:

-              onKeyDown={(e) => {
+              onKeyDown={(e) => {
                 // ignore if shift key is pressed to allow entering new lines
                 if (e.shiftKey) {
                   return;
                 }
-                if (e.key === "Enter" && !props.isChatStreaming) {
+                // avoid sending while composing (IME), while streaming, or while connecting wallet
+                // and block empty/whitespace messages
+                // @ts-expect-error - nativeEvent available on KeyboardEvent
+                const isComposing = e.nativeEvent?.isComposing === true;
+                if (
+                  e.key === "Enter" &&
+                  !props.isChatStreaming &&
+                  !props.isConnectingWallet &&
+                  !isComposing &&
+                  message.trim() !== ""
+                ) {
                   e.preventDefault();
                   handleSubmit(message);
                 }
               }}

28-38: Trim input and no-op on empty submits inside handleSubmit

Guarantees consistent behavior regardless of call site.

-  function handleSubmit(message: string) {
+  function handleSubmit(message: string) {
+    const trimmed = message.trim();
+    if (!trimmed) {
+      return;
+    }
     const userMessage: NebulaUserMessage = {
-      content: [{ text: message, type: "text" }],
+      content: [{ text: trimmed, type: "text" }],
       role: "user",
     };
 
     props.sendMessage(userMessage);
     setMessage("");
   }

@joaquim-verges joaquim-verges force-pushed the 09-08-thirdweb_ai_inside_dashboard branch from 87aa725 to ee25ba7 Compare September 8, 2025 08:50
@vercel vercel bot temporarily deployed to Preview – nebula September 8, 2025 08:50 Inactive
@vercel vercel bot temporarily deployed to Preview – docs-v2 September 8, 2025 08:50 Inactive
@vercel vercel bot temporarily deployed to Preview – thirdweb_playground September 8, 2025 08:50 Inactive
@vercel vercel bot temporarily deployed to Preview – wallet-ui September 8, 2025 08:50 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/dashboard/src/app/(app)/(dashboard)/(chain)/components/server/products.ts (1)

1-1: Add server-only guard

This lives under a server path; add the guard to prevent accidental client import and keep bundles clean.

import "server-only";
♻️ Duplicate comments (11)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/Swap/SwapCards.tsx (4)

1-1: Add 'use client' (hooks used in this file).

Without it, Next will treat this as a Server Component and crash at runtime/build.

Apply:

+'use client';
+
 import { ArrowRightLeftIcon, CheckIcon } from "lucide-react";

42-43: Guard prepareTransaction when chain is undefined; disable button until ready (Swap).

txChain can be undefined; calling prepareTransaction or enabling send will throw/allow invalid interaction.

Apply:

   const txChain = useV5DashboardChain(swapData.transaction.chain_id);
@@
               disabled={
-                props.status.type === "sending" ||
-                props.status.type === "confirming"
+                !txChain ||
+                props.status.type === "sending" ||
+                props.status.type === "confirming"
               }
@@
-              onClick={async () => {
+              onClick={async () => {
+                if (!txChain) {
+                  return;
+                }
                 const tx = prepareTransaction({
                   chain: txChain,
                   client: props.client,
                   data: swapData.transaction.data,
                   value: swapData.transaction.value
                     ? BigInt(swapData.transaction.value)
                     : undefined,
                   extraGas: 50000n,
                   erc20Value: isSellingNativeToken
                     ? undefined
                     : {
                         amountWei: BigInt(swapData.from_token.amount),
                         tokenAddress: swapData.from_token.address,
                       },
                   to: swapData.transaction.to,
                 });
 
-                props.sendTx(tx);
+                await props.sendTx(tx);
               }}
@@
-              txChainID={swapData.transaction.chain_id}
+              txChainID={txChain?.id ?? swapData.transaction.chain_id}

Also applies to: 98-101, 104-123, 126-127


164-168: Mirror chain-ready guards for Approve; await send.

Prevent prepareTransaction with undefined chain; keep button disabled until ready.

Apply:

   const txChain = useV5DashboardChain(swapData.transaction.chain_id);
@@
-              disabled={isTransactionPending}
+              disabled={!txChain || isTransactionPending}
@@
-              onClick={async () => {
+              onClick={async () => {
+                if (!txChain) {
+                  return;
+                }
                 const tx = prepareTransaction({
                   chain: txChain,
                   client: props.client,
                   data: swapData.transaction.data,
                   to: swapData.transaction.to,
                 });
 
-                props.sendTx(tx);
+                await props.sendTx(tx);
               }}
@@
-              txChainID={swapData.transaction.chain_id}
+              txChainID={txChain?.id ?? swapData.transaction.chain_id}

Also applies to: 217-224, 225-234, 237-238


258-276: Safely handle undefined chain in TokenRow.

chain may be undefined until chains load; direct property access will throw.

Apply:

   const chain = useV5DashboardChain(props.chainId);
@@
           <ChainIconClient
             className="size-4"
             client={props.client}
-            src={chain.icon?.url || ""}
+            src={chain?.icon?.url}
           />
-          <div>{chain.name}</div>
+          <div>{chain?.name ?? `Chain ${props.chainId}`}</div>
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatBar.tsx (4)

56-60: Brand text: “Nebula” → “thirdweb” (toast copy)

Align copy with the rename.

 function showSigninToUploadImagesToast() {
-  toast.error("Sign in to upload images to Nebula", {
+  toast.error("Sign in to upload images to thirdweb", {
     position: "top-right",
   });
 }

372-376: Brand text: “Nebula” → “thirdweb” (popover copy)

Update popover message to reflect the rename.

-  Get access to image uploads by signing in to Nebula
+  Get access to image uploads by signing in to thirdweb

176-199: Drag-over highlight can get stuck when uploads aren’t allowed

Only set the highlight when uploads are allowed; always clear it correctly; and accept Node | null in contains.

 onDragEnter={(e) => {
   e.preventDefault();
-  setIsDragOver(true);
-  if (!props.allowImageUpload) {
-    return;
-  }
+  if (!props.allowImageUpload) return;
+  setIsDragOver(true);
 }}
 onDragLeave={(e) => {
   e.preventDefault();
-  if (!props.allowImageUpload) {
-    return;
-  }
-  // Only set drag over to false if we're leaving the container entirely
-  if (!e.currentTarget.contains(e.relatedTarget as Node)) {
+  // Only set drag over to false if we're leaving the container entirely
+  if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
     setIsDragOver(false);
   }
 }}
 onDragOver={(e) => {
   e.preventDefault();
-  setIsDragOver(true);
-  if (!props.allowImageUpload) {
-    return;
-  }
+  if (!props.allowImageUpload) return;
+  setIsDragOver(true);
 }}

227-236: Prevent sending empty messages on Enter

Match the Send-button guard so Enter doesn’t submit empty text with no images.

 if (e.key === "Enter" && !props.isChatStreaming) {
-  e.preventDefault();
-  handleSubmit(message);
+  if (message.trim() !== "" || images.length > 0) {
+    e.preventDefault();
+    handleSubmit(message);
+  }
 }
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatPageLayout.stories.tsx (1)

55-69: Story now supplies required props; renders as expected.

Fix addresses the earlier missing-props issue.

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/api/chat.ts (1)

34-44: Wrap stream setup in try/catch and surface connection/abort errors to UI (duplicate).

Network errors before first event currently throw and bypass handleStream. Reuse the earlier suggested guard.

-  const events = await stream(`${NEXT_PUBLIC_THIRDWEB_AI_HOST}/chat`, {
+  try {
+    const events = await stream(`${NEXT_PUBLIC_THIRDWEB_AI_HOST}/chat`, {
       body: JSON.stringify(body),
       headers: {
         Authorization: `Bearer ${params.authToken}`,
         "x-team-id": params.project.teamId,
         "x-client-id": params.project.publishableKey,
         "Content-Type": "application/json",
       },
       method: "POST",
       signal: params.abortController.signal,
-  });
+    });
+    for await (const _event of events) {
+      // existing switch…
+    }
+  } catch (e) {
+    if (params.abortController.signal.aborted) return;
+    params.handleStream({
+      event: "error",
+      data: {
+        code: 0,
+        errorMessage: e instanceof Error ? e.message : "Failed to connect to chat stream",
+      },
+    });
+  }
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatPageContent.tsx (1)

160-167: Fix possible undefined .toLowerCase() (duplicate).

Guard when no text segment exists.

-      const lowerCaseMessage = message.content
-        .find((x) => x.type === "text")
-        ?.text.toLowerCase();
+      const lowerCaseMessage = message.content
+        .find((x) => x.type === "text")
+        ?.text
+        ?.toLowerCase();
🧹 Nitpick comments (29)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/AssetsSection/AssetsSection.stories.tsx (7)

5-15: Set default args and tidy controls in meta (optional).

Avoid repeating client in every story, hide it from Controls, and enable autodocs/fullscreen.

Apply:

 const meta = {
   component: AssetsSectionUI,
   decorators: [
     (Story) => (
       <div className="mx-auto h-dvh w-full max-w-[300px] bg-card p-2">
         <Story />
       </div>
     ),
   ],
   title: "AI/AssetsSection",
+  args: { client: storybookThirdwebClient },
+  parameters: {
+    layout: "fullscreen",
+    controls: { exclude: ["client"] },
+  },
+  tags: ["autodocs"],
 } satisfies Meta<typeof AssetsSectionUI>;

20-69: Gitleaks false-positives on EVM addresses; add a scoped allowlist.

The token_address values are public contract addresses (not secrets) but may trip Gitleaks (see hints). Add a path-scoped allowlist to prevent CI noise.

Option A (repo-level): create/append gitleaks config:

+# gitleaks.toml
+[allowlist]
+paths = [
+  '''apps/dashboard/src/app/(app)/team/.*/AssetsSection/AssetsSection\.stories\.tsx'''
+]

Option B (inline, local): if you prefer inline suppression, add // gitleaks:allow after each token_address line.


71-77: Remove redundant client arg if meta.args is set.

 export const MultipleAssets: Story = {
   args: {
-    client: storybookThirdwebClient,
     data: tokensStub,
     isPending: false,
   },
 };

79-85: Same: dedupe client arg.

 export const SingleAsset: Story = {
   args: {
-    client: storybookThirdwebClient,
     data: tokensStub.slice(0, 1),
     isPending: false,
   },
 };

87-93: Same: dedupe client arg.

 export const EmptyAssets: Story = {
   args: {
-    client: storybookThirdwebClient,
     data: [],
     isPending: false,
   },
 };

95-101: Same: dedupe client arg.

 export const Loading: Story = {
   args: {
-    client: storybookThirdwebClient,
     data: [],
     isPending: true,
   },
 };

5-15: Expose className on AssetsSectionUI to match app guidelines.

Per apps/{dashboard,playground}/**/*.tsx, components should accept className on the root. AssetsSectionUI currently doesn’t. Minor refactor:

Outside this file (AssetsSection.tsx):

@@
-import { XIcon } from "lucide-react";
+import { XIcon } from "lucide-react";
+import { cn } from "@/lib/utils";
@@
-export function AssetsSectionUI(props: {
-  data: AssetBalance[];
-  isPending: boolean;
-  client: ThirdwebClient;
-}) {
-  if (props.data.length === 0 && !props.isPending) {
+export function AssetsSectionUI({
+  data,
+  isPending,
+  client,
+  className,
+}: {
+  data: AssetBalance[];
+  isPending: boolean;
+  client: ThirdwebClient;
+  className?: string;
+}) {
+  if (data.length === 0 && !isPending) {
     return (
-      <div className="flex h-full flex-col items-center justify-center gap-3 px-2 py-1">
+      <div className={cn("flex h-full flex-col items-center justify-center gap-3 px-2 py-1", className)}>
         <div className="rounded-full border p-1">
           <XIcon className="size-4" />
         </div>
         <div className="text-muted-foreground text-sm">No Assets </div>
       </div>
     );
   }
 
   return (
-    <div className="flex flex-col gap-1">
-      {!props.isPending &&
-        props.data.map((asset) => (
+    <div className={cn("flex flex-col gap-1", className)}>
+      {!isPending &&
+        data.map((asset) => (
           <AssetItem
             asset={asset}
-            client={props.client}
+            client={client}
             key={`${asset.chain_id}-${asset.token_address}`}
           />
         ))}
 
-      {props.isPending &&
+      {isPending &&
         new Array(10).fill(null).map((_, index) => (
           // biome-ignore lint/suspicious/noArrayIndexKey: for the placeholder this is explicitly the key
           <SkeletonAssetItem key={index} />
         ))}
     </div>
   );
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/Swap/SwapCards.tsx (3)

1-15: Expose className on exported components and add explicit return types.

Matches dashboard conventions and improves composition; also aligns with “explicit return types” guideline.

Apply:

+'use client';
+
+import type { JSX } from "react";
 import { ArrowRightLeftIcon, CheckIcon } from "lucide-react";
@@
+import { cn } from "@/lib/utils";
@@
-export function SwapTransactionCard(props: {
+export function SwapTransactionCard(props: {
   swapData: NebulaSwapData;
   client: ThirdwebClient;
   onTxSettled: (txHash: string) => void;
-}) {
+  className?: string;
+}): JSX.Element {
@@
     <SwapTransactionCardLayout
       client={props.client}
       sendTx={(tx) => sendTx(tx, props.onTxSettled)}
       setStatus={setStatus}
       status={status}
       swapData={props.swapData}
+      className={props.className}
     />
@@
-export function SwapTransactionCardLayout(props: {
+export function SwapTransactionCardLayout(props: {
   swapData: NebulaSwapData;
   client: ThirdwebClient;
   status: TxStatus;
   setStatus: (status: TxStatus) => void;
   sendTx: (tx: PreparedTransaction) => Promise<void>;
-}) {
+  className?: string;
+}): JSX.Element {
@@
-    <div className="max-w-lg">
+    <div className={cn("max-w-lg", props.className)}>
@@
-export function ApproveTransactionCard(props: {
+export function ApproveTransactionCard(props: {
   swapData: NebulaSwapData;
   client: ThirdwebClient;
-}) {
+  className?: string;
+}): JSX.Element {
@@
     <ApproveTransactionCardLayout
       client={props.client}
       sendTx={(tx) => sendTx(tx, undefined)}
       setStatus={setStatus}
       status={status}
       swapData={props.swapData}
+      className={props.className}
     />
@@
-export function ApproveTransactionCardLayout(props: {
+export function ApproveTransactionCardLayout(props: {
   swapData: NebulaSwapData;
   client: ThirdwebClient;
   status: TxStatus;
   setStatus: (status: TxStatus) => void;
   sendTx: (tx: PreparedTransaction) => Promise<void>;
-}) {
+  className?: string;
+}): JSX.Element {
@@
-    <div className="max-w-lg">
+    <div className={cn("max-w-lg", props.className)}>
@@
-function TokenRow(props: {
+function TokenRow(props: {
   amount: string;
   symbol: string;
   chainId: number;
   client: ThirdwebClient;
   title: string;
   decimals: number;
-}) {
+}): JSX.Element {

Also applies to: 16-20, 34-40, 49-50, 139-144, 156-162, 170-171, 250-257


112-113: Consider estimating or parameterizing extraGas.

Hard-coding 50k may be insufficient/excessive across routes; consider deriving from estimation with a safety multiplier or move to a prop.


102-103: isLoggedIn hard-coded to true.

If this button should honor actual auth/wallet state, wire the real flag; otherwise add a comment explaining why always-true is correct.

Also applies to: 223-224

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatBar.tsx (3)

624-682: Remove duplicate WalletProvider nesting

WalletProvider is wrapped twice with the same id, adding unnecessary providers.

-                  <WalletProvider id={wallet.walletId}>
-                    <div className="flex items-center gap-2.5">
+                  <div className="flex items-center gap-2.5">
                       <AccountAvatar
                         className="size-8 rounded-full"
                         fallbackComponent={accountAvatarFallback}
                         loadingComponent={accountAvatarFallback}
                       />
@@
-                    </div>
-                  </WalletProvider>
+                  </div>

114-165: Large inline b64 uploads can inflate request size; consider pre-upload + URL

Up to 4×5MB images become ~26MB+ with base64 overhead. Prefer pre-uploading to storage and sending image_url references instead of inlining b64 in the chat message.


353-354: Drop no-op prop

value={undefined} on ImageUploadButton has no effect and can be removed.

-                  value={undefined}
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx (1)

83-93: Avoid making the layout dynamic just to hide the chat button

Using headers() forces dynamic rendering. Consider moving the “/ai” guard into a tiny client wrapper using useSelectedLayoutSegments() (as done in CustomChatButton for “support”), or pass the current route via props from an existing client boundary.

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatPageLayout.stories.tsx (1)

12-15: Pass the Storybook client to ThirdwebProvider to avoid context warnings.

Prevents potential runtime warnings and enables hooks inside stories if added later.

-      <ThirdwebProvider>
+      <ThirdwebProvider client={storybookThirdwebClient}>
         <Story />
       </ThirdwebProvider>
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/chat/history/ChatHistoryPage.tsx (3)

100-107: Use a stable key to minimize unnecessary re-renders.

key includes timestamps; it will change when updated_at changes.

-                  key={session.id + session.updated_at + session.created_at}
+                  key={session.id}

185-188: Avoid duplicate IDs in deletedSessionsStore.

Prevents store bloat if users retry deletions.

-      const prev = deletedSessionsStore.getValue();
-      deletedSessionsStore.setValue([...prev, props.session.id]);
+      const prev = deletedSessionsStore.getValue();
+      if (!prev.includes(props.session.id)) {
+        deletedSessionsStore.setValue([...prev, props.session.id]);
+      }

7-7: Optional: lazy-load Fuse to trim initial bundle.

If bundle size for this route matters, consider dynamically importing fuse.js only when search is used (e.g., on first non-empty query) and caching the instance.

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/api/chat.ts (2)

46-182: Guard all JSON.parse calls to prevent a single bad event from killing the stream.

Add a tiny safe-parse helper and use it per case.

+function safeParse<T>(s: string): T | null {
+  try { return JSON.parse(s) as T; } catch { return null; }
+}
...
-      case "delta": {
-        params.handleStream({
-          data: {
-            v: JSON.parse(event.data).v,
-          },
-          event: "delta",
-        });
+      case "delta": {
+        const d = safeParse<{ v: string }>(event.data);
+        if (!d) break;
+        params.handleStream({ data: { v: d.v }, event: "delta" });
         break;
       }
...
-      case "presence": {
-        params.handleStream({
-          data: JSON.parse(event.data),
-          event: "presence",
-        });
+      case "presence": {
+        const d = safeParse<{ session_id: string; request_id: string; source: string; data: string }>(event.data);
+        if (!d) break;
+        params.handleStream({ data: d, event: "presence" });
         break;
       }
...
-      case "image": {
-        const data = JSON.parse(event.data) as {
+      case "image": {
+        const data = safeParse<{
           data: { width: number; height: number; url: string };
           request_id: string;
-        };
+        }>(event.data);
+        if (!data) break;
...
-      case "action": {
-        const data = JSON.parse(event.data);
+      case "action": {
+        const data = safeParse<{ type: "sign_transaction" | "sign_swap"; data: unknown; request_id: string }>(event.data);
+        if (!data) break;
...
-      case "error": {
-        const data = JSON.parse(event.data) as {
+      case "error": {
+        const data = safeParse<{ code: number; error: { message: string } }>(event.data);
+        if (!data) break;
...
-      case "init": {
-        const data = JSON.parse(event.data);
+      case "init": {
+        const data = safeParse<{ request_id: string; session_id: string }>(event.data);
+        if (!data) break;
...
-      case "context": {
-        const data = JSON.parse(event.data) as {
+      case "context": {
+        const data = safeParse<{ data: string; request_id: string; session_id: string }>(event.data);
+        if (!data) break;
-        const contextData = JSON.parse(data.data) as {
+        const contextData = safeParse<{ wallet_address: string; chain_ids: number[]; networks: NebulaContext["networks"] }>(data.data);
+        if (!contextData) break;

20-33: Type the request payload and dedupe context mapping.

Define a ChatRequestPayload and a toApiContext() helper to reduce casts and keep chain_ids mapping consistent across modules.

-  const body: Record<string, string | boolean | object> = {
+  type ChatRequestPayload = {
+    messages: [NebulaUserMessage];
+    session_id: string;
+    stream: true;
+    context?: { chain_ids: string[]; networks: NebulaContext["networks"]; wallet_address: string | null };
+  };
+  const body: ChatRequestPayload = {
     messages: [params.message],
     session_id: params.sessionId,
     stream: true,
   };
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatPageContent.tsx (5)

36-49: Expose className on root and merge via cn().

Matches dashboard .tsx guideline.

 import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { cn } from "@/lib/utils";
...
 export function ChatPageContent(props: {
   project: Project;
   session: SessionInfo | undefined;
   accountAddress: string;
   authToken: string;
   client: ThirdwebClient;
   type: "landing" | "new-chat";
+  className?: string;
   initialParams:
...
-  return (
-    <div className="flex grow flex-col overflow-hidden">
+  return (
+    <div className={cn("flex grow flex-col overflow-hidden", props.className)}>

Also applies to: 305-312


365-369: On abort, remove the trailing “presence” placeholder.

Prevents a stuck “presence” row after user cancels.

                   abortChatStream={() => {
                     chatAbortController?.abort();
                     setChatAbortController(undefined);
                     setIsChatStreaming(false);
+                    setMessages((prev) =>
+                      prev[prev.length - 1]?.type === "presence"
+                        ? prev.slice(0, -1)
+                        : prev,
+                    );
                   }}

710-726: Wrap image history JSON.parse in try/catch like actions.

A malformed row will currently throw and drop the render.

-      case "image": {
-        const content = JSON.parse(message.content) as {
+      case "image": {
+        try {
+          const content = JSON.parse(message.content) as {
             type: "image";
             request_id: string;
             data: { width: number; height: number; url: string };
-        };
-
-        messages.push({
-          data: content.data,
-          request_id: content.request_id,
-          type: "image",
-        });
+          };
+          messages.push({
+            data: content.data,
+            request_id: content.request_id,
+            type: "image",
+          });
+        } catch (e) {
+          console.error("error processing image message", e, { message });
+        }
         break;
       }

745-751: Add break to avoid future fallthrough.

Safer if new cases are appended later.

       case "assistant": {
         messages.push({
           request_id: undefined,
           text: message.content,
           type: message.role,
         });
+        break;
       }

42-48: Unused prop “type”.

If unused, remove to reduce API surface; otherwise, add usage or a comment.

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/api/session.ts (4)

27-42: Redundant res.ok checks; rely on fetchWithAuthToken throwing on non-OK.

Simplify and let the wrapper handle errors.

   const res = await fetchWithAuthToken({
     project: params.project,
     authToken: params.authToken,
     body: body,
     endpoint: `${NEXT_PUBLIC_THIRDWEB_AI_HOST}/session`,
     method: "POST",
   });
-
-  if (!res.ok) {
-    const error = await res.text();
-    throw new Error(`Failed to create session: ${error}`);
-  }
   const data = await res.json();
   return data.result as SessionInfo;

60-74: Apply the same simplification to update/delete/list/get; fix delete error message.

The wrapper already throws; also “Failed to update session” in delete/list/get is misleading.

-  if (!res.ok) {
-    throw new Error("Failed to update session");
-  }
+  // fetchWithAuthToken throws on non-OK
   const data = await res.json();
   return data.result as UpdatedSessionInfo;
-  if (!res.ok) {
-    throw new Error("Failed to update session");
-  }
+  // fetchWithAuthToken throws on non-OK
   const data = await res.json();
   return data.result as DeletedSessionInfo;
-  if (!res.ok) {
-    throw new Error("Failed to update session");
-  }
+  // fetchWithAuthToken throws on non-OK
   const data = await res.json();
   return data.result as TruncatedSessionInfo[];
-  if (!res.ok) {
-    throw new Error("Failed to update session");
-  }
+  // fetchWithAuthToken throws on non-OK
   const data = await res.json();
   return data.result as SessionInfo;

Also applies to: 80-94, 100-113, 120-133


12-16: Add explicit return types per TS guideline.

Clarifies API surfaces.

-export async function createSession(params: { ... }) {
+export async function createSession(params: { ... }): Promise<SessionInfo> {
...
-export async function updateSession(params: { ... }) {
+export async function updateSession(params: { ... }): Promise<UpdatedSessionInfo> {
...
-export async function deleteSession(params: { ... }) {
+export async function deleteSession(params: { ... }): Promise<DeletedSessionInfo> {
...
-export async function getSessions(params: { ... }) {
+export async function getSessions(params: { ... }): Promise<TruncatedSessionInfo[]> {
...
-export async function getSessionById(params: { ... }) {
+export async function getSessionById(params: { ... }): Promise<SessionInfo> {

Also applies to: 44-49, 76-80, 96-99, 115-119


19-25: DRY context mapping with a helper.

Avoid repeating chain_ids/networks/wallet_address mapping.

function toApiContext(ctx: NebulaContext) {
  return {
    chain_ids: ctx.chainIds || [],
    networks: ctx.networks,
    wallet_address: ctx.walletAddress,
  };
}
-  if (params.context) {
-    body.context = {
-      chain_ids: params.context.chainIds || [],
-      networks: params.context.networks,
-      wallet_address: params.context.walletAddress,
-    };
-  }
+  if (params.context) body.context = toApiContext(params.context);

Also applies to: 52-58

@joaquim-verges joaquim-verges force-pushed the 09-08-thirdweb_ai_inside_dashboard branch from ee25ba7 to 9138778 Compare September 8, 2025 09:09
@vercel vercel bot temporarily deployed to Preview – thirdweb_playground September 8, 2025 09:10 Inactive
@vercel vercel bot temporarily deployed to Preview – docs-v2 September 8, 2025 09:10 Inactive
@vercel vercel bot temporarily deployed to Preview – nebula September 8, 2025 09:10 Inactive
@vercel vercel bot temporarily deployed to Preview – wallet-ui September 8, 2025 09:10 Inactive
@joaquim-verges joaquim-verges force-pushed the 09-08-thirdweb_ai_inside_dashboard branch from 9138778 to b4c89eb Compare September 8, 2025 09:23
@vercel vercel bot temporarily deployed to Preview – nebula September 8, 2025 09:23 Inactive
@vercel vercel bot temporarily deployed to Preview – docs-v2 September 8, 2025 09:23 Inactive
@vercel vercel bot temporarily deployed to Preview – wallet-ui September 8, 2025 09:23 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (19)
apps/dashboard/src/@/actions/proxies.ts (1)

18-19: Wire AbortSignal through and handle network/abort errors; disable caching for proxied calls.

signal is added to params but not passed to fetch, and network/abort errors will throw and bypass your typed result. Also consider cache: "no-store" for true proxy semantics.

-  const res = await fetch(url, {
-    body: params.body,
-    headers: {
-      ...params.headers,
-      ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
-    },
-    method: params.method,
-  });
+  let res: Response;
+  try {
+    res = await fetch(url, {
+      body: params.body,
+      headers: {
+        ...params.headers,
+        ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
+      },
+      method: params.method,
+      signal: params.signal,
+      cache: "no-store",
+      // next: { revalidate: 0 }, // optional: Next.js caching control
+    });
+  } catch (err) {
+    const message = (err as Error)?.message || "Network error";
+    const aborted =
+      (err as any)?.name === "AbortError" || message.toLowerCase().includes("abort");
+    return {
+      ok: false,
+      status: aborted ? 499 : 502,
+      error: aborted ? "Request aborted" : message,
+    };
+  }

Also applies to: 51-58

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/TransactionsSection/TransactionsSection.tsx (3)

12-24: Tighten decoded.inputs typing (avoid object).

Model known fields to get type-safety where you access inputs.to / inputs.spender.

 export type WalletTransaction = {
   chain_id: string;
   value: string;
   hash: string;
   from_address: string;
   to_address: string;
   decoded?: {
-    name: string;
-    signature: string;
-    inputs: null | object;
+    name?: string | null;
+    signature?: string | null;
+    inputs?: null | {
+      to?: string;
+      spender?: string;
+    };
   };
 };

165-201: Harden React Query: types, abort signal, ok-check, cache.

Add explicit generics, wire AbortSignal, check response.ok, and configure stale/gc time.

-  const txQuery = useQuery({
+  const txQuery = useQuery<WalletTransaction[]>({
     enabled: !!account && !!activeChain,
-    queryFn: async () => {
+    queryFn: async ({ signal }) => {
       if (!account || !activeChain) {
         return [];
       }
@@
-      const response = await fetch(url.toString());
+      const response = await fetch(url.toString(), { signal });
+      if (!response.ok) {
+        return [];
+      }
       const json = (await response.json()) as {
         data?: WalletTransaction[];
       };
 
       return json.data ?? [];
     },
     queryKey: ["v1/wallets/transactions", account?.address, activeChain?.id],
-    retry: false,
+    staleTime: 60_000,
+    gcTime: 5 * 60_000,
+    retry: false,
   });

1-1: Add 'use client' (hooks used in this file).

This component uses React Query and thirdweb hooks and must be a Client Component.

Apply:

+ 'use client';
+
 import { useQuery } from "@tanstack/react-query";
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatBar.tsx (6)

56-60: Brand string: “Nebula” → “thirdweb”.

Align with PR rename.

 function showSigninToUploadImagesToast() {
-  toast.error("Sign in to upload images to Nebula", {
+  toast.error("Sign in to upload images to thirdweb", {
     position: "top-right",
   });
 }

372-376: Update popover copy to “thirdweb”.

-                        Get access to image uploads by signing in to Nebula
+                        Get access to image uploads by signing in to thirdweb

176-199: Fix drag-over highlight getting stuck; gate on allowImageUpload and handle null relatedTarget.

         onDragEnter={(e) => {
           e.preventDefault();
-          setIsDragOver(true);
-          if (!props.allowImageUpload) {
-            return;
-          }
+          if (!props.allowImageUpload) return;
+          setIsDragOver(true);
         }}
         onDragLeave={(e) => {
           e.preventDefault();
-          if (!props.allowImageUpload) {
-            return;
-          }
-          // Only set drag over to false if we're leaving the container entirely
-          if (!e.currentTarget.contains(e.relatedTarget as Node)) {
+          // Only set drag over to false if we're leaving the container entirely
+          if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
             setIsDragOver(false);
           }
         }}
         onDragOver={(e) => {
           e.preventDefault();
-          setIsDragOver(true);
-          if (!props.allowImageUpload) {
-            return;
-          }
+          if (!props.allowImageUpload) return;
+          setIsDragOver(true);
         }}

232-235: Prevent sending empty messages on Enter (no text and no images).

-                if (e.key === "Enter" && !props.isChatStreaming) {
-                  e.preventDefault();
-                  handleSubmit(message);
-                }
+                if (e.key === "Enter" && !props.isChatStreaming) {
+                  if (message.trim() === "" && images.length === 0) return;
+                  e.preventDefault();
+                  handleSubmit(message);
+                }

93-101: Send raw base64 (strip data URL prefix) to reduce payloads.

-        if (image.b64) {
-          userMessage.content.push({
-            b64: image.b64,
-            image_url: null,
-            type: "image",
-          });
-        }
+        if (image.b64) {
+          const dataUrl = image.b64;
+          const base64 = dataUrl.includes(",")
+            ? dataUrl.split(",")[1]
+            : dataUrl;
+          userMessage.content.push({
+            b64: base64,
+            image_url: null,
+            type: "image",
+          });
+        }

536-541: Don’t mutate props when sorting wallets.

-  const sortedWallets = props.wallets.sort((a, b) => {
+  const sortedWallets = [...props.wallets].sort((a, b) => {
     if (a.walletId === "smart") return -1;
     if (b.walletId === "smart") return 1;
     return 0;
   });
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/Chats.tsx (3)

88-110: Add cleanup for event listeners to prevent leaks.

   useEffect(() => {
     if (!enableAutoScroll) {
       return;
     }
@@
     chatScrollContainer.addEventListener("mousedown", disableScroll);
     chatScrollContainer.addEventListener("wheel", disableScroll);
+    return () => {
+      chatScrollContainer.removeEventListener("mousedown", disableScroll);
+      chatScrollContainer.removeEventListener("wheel", disableScroll);
+    };
   }, [setEnableAutoScroll, enableAutoScroll]);

1-1: Add 'use client' (hooks used).

+'use client';
 import { MarkdownRenderer } from "@workspace/ui/components/markdown-renderer";

62-74: Remove unused authToken prop; avoid exposing tokens in client.

It’s unused and should not be part of client props.

 export function Chats(props: {
   project: Project;
   messages: Array<ChatMessage>;
   isChatStreaming: boolean;
-  authToken: string;
   sessionId: string | undefined;
@@
                 <div
@@
                   <RenderMessage
                     project={props.project}
-                    authToken={props.authToken}
                     client={props.client}
@@
 function RenderMessage(props: {
   message: ChatMessage;
   isMessagePending: boolean;
   client: ThirdwebClient;
   sendMessage: (message: NebulaUserMessage) => void;
   nextMessage: ChatMessage | undefined;
-  authToken: string;
   sessionId: string | undefined;
   project: Project;
 }) {
@@
         <ScrollShadow className="rounded-lg">
           <RenderResponse
             project={props.project}
-            authToken={props.authToken}
             client={props.client}
             isMessagePending={props.isMessagePending}
             message={message}
             nextMessage={props.nextMessage}
             sendMessage={props.sendMessage}
             sessionId={props.sessionId}
           />
         </ScrollShadow>

And drop the authToken field from RenderResponse props:

 function RenderResponse(props: {
   message: ChatMessage;
   isMessagePending: boolean;
   client: ThirdwebClient;
   sendMessage: (message: NebulaUserMessage) => void;
   nextMessage: ChatMessage | undefined;
   sessionId: string | undefined;
-  authToken: string;
   project: Project;
 }) {

Also applies to: 146-155, 167-176, 254-266, 287-296

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/Swap/SwapCards.tsx (4)

1-1: Add 'use client' (hooks used).

+'use client';
 import { ArrowRightLeftIcon, CheckIcon } from "lucide-react";

92-127: Guard undefined chain and await send (Swap).

Prevent crashes when chain metadata isn’t ready; disable button until ready and early-return in handler.

         {props.status.type !== "confirmed" && (
           <div className="flex items-center justify-end border-t px-4 py-4 lg:px-5">
             <TransactionButton
               className="gap-2"
               client={props.client}
-              disabled={
-                props.status.type === "sending" ||
-                props.status.type === "confirming"
-              }
+              disabled={
+                !txChain ||
+                props.status.type === "sending" ||
+                props.status.type === "confirming"
+              }
               isLoggedIn={true}
               isPending={props.status.type === "sending"}
               onClick={async () => {
+                if (!txChain) return;
                 const tx = prepareTransaction({
                   chain: txChain,
                   client: props.client,
                   data: swapData.transaction.data,
                   value: swapData.transaction.value
                     ? BigInt(swapData.transaction.value)
                     : undefined,
                   extraGas: 50000n,
                   erc20Value: isSellingNativeToken
                     ? undefined
                     : {
                         amountWei: BigInt(swapData.from_token.amount),
                         tokenAddress: swapData.from_token.address,
                       },
                   to: swapData.transaction.to,
                 });
 
-                props.sendTx(tx);
+                await props.sendTx(tx);
               }}
               size="sm"
               transactionCount={undefined}
-              txChainID={swapData.transaction.chain_id}
+              txChainID={txChain?.id ?? swapData.transaction.chain_id}
               variant="default"
             >

216-239: Mirror chain-ready guards for Approve.

         {props.status.type !== "confirmed" && (
           <div className="flex items-center justify-end border-t px-4 py-4 lg:px-5">
             <TransactionButton
               className="gap-2"
               client={props.client}
-              disabled={isTransactionPending}
+              disabled={!txChain || isTransactionPending}
               isLoggedIn={true}
               isPending={isTransactionPending}
               onClick={async () => {
+                if (!txChain) return;
                 const tx = prepareTransaction({
                   chain: txChain,
                   client: props.client,
                   data: swapData.transaction.data,
                   to: swapData.transaction.to,
                 });
 
-                props.sendTx(tx);
+                await props.sendTx(tx);
               }}
               size="sm"
               transactionCount={undefined}
-              txChainID={swapData.transaction.chain_id}
+              txChainID={txChain?.id ?? swapData.transaction.chain_id}
               variant="default"
             >

258-276: Avoid NPEs: chain can be undefined while loading.

   const chain = useV5DashboardChain(props.chainId);
   const tokenDisplayValue = toTokens(BigInt(props.amount), props.decimals);
@@
         <div className="flex items-center gap-1.5">
           <ChainIconClient
             className="size-4"
             client={props.client}
-            src={chain.icon?.url || ""}
+            src={chain?.icon?.url}
           />
-          <div>{chain.name}</div>
+          <div>{chain?.name ?? `Chain ${props.chainId}`}</div>
         </div>
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/api/types.ts (1)

6-15: Make “image” a true one‑of (exactly one of b64 or image_url).

 type NebulaUserMessageContentItem =
   | {
       type: "image";
-      image_url: string | null;
-      b64: string | null;
+      image_url: string;
+      b64: null;
     }
+  | {
+      type: "image";
+      image_url: null;
+      b64: string;
+    }
   | {
       type: "text";
       text: string;
     }
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/page.tsx (1)

31-38: Do not pass auth tokens into client-rendered children; keep tokens server-side

Passing authToken to ChatPageLayout/ChatPageContent (both render client trees) exposes the JWT to the browser. Route client calls through server actions or route handlers that read the token via getAuthToken() and attach Authorization: Bearer <token> server-side. Minimal change in this file:

   <ChatPageLayout
     team_slug={params.team_slug}
     project={project}
-    authToken={authToken}
     client={client}
     accountAddress={""}
     sessions={sessions}
   >
     <ChatPageContent
       project={project}
       accountAddress={""}
-      authToken={authToken}
       client={client}
       initialParams={undefined}
       session={undefined}
       type="new-chat"
     />
   </ChatPageLayout>

Follow-up (outside this file): remove authToken from those components’ props and have their network calls hit internal /api/ai/* handlers or server actions that inject the header.

Also applies to: 39-47

🧹 Nitpick comments (11)
apps/dashboard/src/@/actions/proxies.ts (3)

6-8: Remove unused import or add an AI proxy helper.

NEXT_PUBLIC_THIRDWEB_AI_HOST is imported but never used. Either remove it or add a dedicated aiServerProxy to keep parity with the other helpers.

+export async function aiServerProxy<T>(params: ProxyActionParams) {
+  return proxy<T>(NEXT_PUBLIC_THIRDWEB_AI_HOST, params);
+}

42-49: Use set instead of append for query params to avoid accidental duplicates.

-  if (params.searchParams) {
-    for (const key in params.searchParams) {
-      const value = params.searchParams[key];
-      if (value) {
-        url.searchParams.append(key, value);
-      }
-    }
-  }
+  if (params.searchParams) {
+    for (const key in params.searchParams) {
+      const value = params.searchParams[key];
+      if (value !== undefined) {
+        url.searchParams.set(key, value);
+      }
+    }
+  }

77-81: Guard JSON parsing for 204/empty or non-JSON responses.

res.json() will throw on 204 or non-JSON payloads.

-  return {
-    data: params.parseAsText ? await res.text() : await res.json(),
-    ok: true,
-    status: res.status,
-  };
+  const ct = res.headers.get("content-type") || "";
+  const isJson = ct.includes("application/json") || ct.includes("+json");
+  const data = params.parseAsText
+    ? await res.text()
+    : res.status === 204
+      ? (null as unknown as T)
+      : isJson
+        ? await res.json()
+        : ((await res.text()) as unknown as T);
+  return { data, ok: true, status: res.status };
apps/dashboard/src/@/components/chat/CustomChatButton.tsx (2)

18-24: Add explicit return type to match TS guidelines

Declare the component’s return type.

Use:

export function CustomChatButton(props: { /* … */ }): JSX.Element {

14-16: Lazy‑init thirdweb client to keep initial bundle lean

Creating the client at module scope pulls thirdweb into the initial chunk. Initialize when the chat first opens.

Example:

// remove module-scope client

export function CustomChatButton(/* … */): JSX.Element {
  const [isOpen, setIsOpen] = useState(false);
  const [hasBeenOpened, setHasBeenOpened] = useState(false);
  const clientRef = useRef<ReturnType<typeof createThirdwebClient> | null>(null);

  if (hasBeenOpened && !clientRef.current) {
    clientRef.current = createThirdwebClient({ clientId: NEXT_PUBLIC_DASHBOARD_CLIENT_ID });
  }

  // …
  {hasBeenOpened && isOpen && clientRef.current && (
    <CustomChatContent client={clientRef.current} /* … */ />
  )}
}
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatSidebar.tsx (1)

85-96: Use NavLink for internal route to get active state styling.

Optional: swap Link → NavLink for “Analytics”.

-import Link from "next/link";
+import Link from "next/link";
+import { NavLink } from "@/components/ui/NavLink";
@@
-        <Link
+        <NavLink
           className="flex items-center gap-1 rounded-full text-foreground text-sm hover:underline justify-between"
           href={`/team/${props.team_slug}/${props.project.slug}/ai/analytics`}
         >
@@
-        </Link>
+        </NavLink>
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/api/types.ts (1)

77-82: Narrow NebulaTxData.to to an address literal type.

 export type NebulaTxData = {
   chain_id: number;
   data: `0x${string}`;
-  to: string;
+  to: `0x${string}`;
   value?: string;
 };
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/page.tsx (4)

1-2: Add "server-only" to enforce Server Component boundary

Prevents accidental client bundling/imports.

+import "server-only";
 import { notFound } from "next/navigation";
 import { getAuthToken, getUserThirdwebClient } from "@/api/auth-token";

8-10: Declare an explicit return type for Page

Matches repo guidelines for TS explicit return types.

-export default async function Page(props: {
+export default async function Page(props: {
   params: Promise<{ team_slug: string; project_slug: string }>;
-}) {
+}): Promise<JSX.Element> {

11-11: Simplify awaiting params

No need for Promise.all on a single promise.

-  const [params] = await Promise.all([props.params]);
+  const params = await props.params;

28-28: Swallowing session fetch errors hides issues; at least log

Keeps UX resilient but adds visibility when the API fails.

-  const sessions = await getSessions({ project }).catch(() => []);
+  const sessions = await getSessions({ project }).catch((err) => {
+    console.error("getSessions failed", err);
+    return [];
+  });

jwt: authToken,
teamId: project.teamId,
});
const client = await getUserThirdwebClient({ teamId: project.teamId });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify ThirdwebClient is safe/serializable across the RSC → client boundary

If any downstream is a Client Component, passing a ThirdwebClient instance may fail serialization or leak server-only config. Prefer constructing a browser-safe client in the client layer (using public config) or pass minimal primitives.

Run to check which components are client-rendered and receive client:

Also applies to: 31-38


🏁 Script executed:

#!/bin/bash
# Locate ChatPage* files and detect 'use client' + 'client: ThirdwebClient' usage
fd -t f 'ChatPage*.tsx' apps | xargs -I{} sh -c 'echo "== {} =="; head -n 3 {}; rg -n "^'\''use client'\''" {}; rg -nP -C1 "\bclient\s*:\s*ThirdwebClient" {}; echo'
# Find downstream props that also accept `client`
rg -nP -C1 --type tsx "\bclient\s*:\s*ThirdwebClient\b" apps

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Locate the ai/page.tsx file under the team project path
FILE=$(find apps/dashboard/src/app -type f -path "*/team/*/*/ai/page.tsx")
echo "Inspecting file: $FILE"
# Show top of file for directives and imports
sed -n '1,30p' "$FILE"
# Check for client component directive
rg -n "'use client'" "$FILE"
# Search for any prop or invocation passing `client =`
rg -nP "client\s*=" -C1 "$FILE"

Length of output: 1248


🏁 Script executed:

#!/bin/bash
set -e
# Locate the ai page component
PAGE=$(find apps/dashboard/src/app -type f -path "*/team/*/*/ai/page.tsx")
COMP_DIR=$(dirname "$PAGE")/components

for comp in ChatPageContent.tsx ChatPageLayout.tsx; do
  FILE="$COMP_DIR/$comp"
  echo "== $FILE =="
  # Show top lines for 'use client' directive and imports
  sed -n '1,20p' "$FILE"
  # Check for client component
  rg -n "'use client'" "$FILE" || echo "no 'use client' directive"
  echo
done

Length of output: 1424


Avoid passing server-side ThirdwebClient into a client component
In page.tsx you fetch a server-only ThirdwebClient and pass it via ChatPageLayout into ChatPageContent.tsx, which is a "use client" component. Class instances aren’t JSON-serializable and may expose server config. Instead, construct a browser-safe client inside the client component (e.g. use new ThirdwebClient({…publicConfig}) or the useClient() hook from thirdweb/react), or pass only serializable primitives (tokens, chain IDs).

🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/page.tsx
around line 26, you are creating a server-only ThirdwebClient and passing that
instance into a client component; instead, stop passing the class instance
across the server->client boundary and only pass serializable primitives (e.g.,
ownerAddress, accessToken, chainId) or construct the Thirdweb client inside the
client component. Remove the server-side client from ChatPageLayout props,
change the layout to accept only serializable values, and in ChatPageContent (or
its wrapper) create a browser-safe client (either via new
ThirdwebClient({…publicConfig}) or the thirdweb/react useClient() hook) using
the passed primitives/tokens.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (16)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/data/examplePrompts.ts (1)

1-5: Import the shared ExamplePrompt type to avoid drift

Reuse the canonical type from chat/types instead of redefining it locally.

-type ExamplePrompt = {
-  title: string;
-  message: string;
-  interceptedReply?: string;
-};
+import type { ExamplePrompt } from "@/components/chat/types";
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/page.tsx (2)

34-34: Do not forward JWTs to client-rendered children.

Passing authToken into ChatPageLayout/ChatPageContent leaks the token to the browser; route client calls via internal API routes/server actions that read the token server-side.

       project={project}
-      authToken={authToken}
       client={client}
       accountAddress={""}
       sessions={sessions}
     >
       <ChatPageContent
         project={project}
         accountAddress={""}
-        authToken={authToken}
         client={client}
         initialParams={undefined}
         session={undefined}
         type="new-chat"
       />

Would you like a follow-up patch adding /api/ai/* route handlers that attach Authorization: Bearer <token> using getAuthToken()?

#!/bin/bash
# Verify any 'use client' components accept/receive authToken
rg -nP -C2 "^'use client'|authToken\s*:" apps/dashboard/src/app/(app)/team/.*/ai/components

Also applies to: 42-42


35-35: Avoid passing a server-created ThirdwebClient into client components.

Class instances aren’t RSC-serializable and can expose server-only config. Construct a browser-safe client inside the client layer (e.g. useClient() or new ThirdwebClient({...public})) or pass only serializable primitives.

-      client={client}
+      /* construct client in client component instead */
#!/bin/bash
# Find client components receiving a ThirdwebClient instance
rg -nP -C2 "^'use client'|client\s*:\s*ThirdwebClient|client=\{client\}" apps/dashboard/src/app/(app)/team/.*/ai

Also applies to: 43-43

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/AssetsSection/AssetsSection.stories.tsx (2)

9-11: Replace nonstandard h-dvh with a supported utility.

Use h-[100dvh] (or a configured DVH token) to ensure height applies in Storybook.

-      <div className="mx-auto h-dvh w-full max-w-[300px] bg-card p-2">
+      <div className="mx-auto h-[100dvh] w-full max-w-[300px] bg-card p-2">

2-2: Use a stubbed client in Storybook and de-duplicate client via meta.args.

Avoid network calls in stories. Import the stubbed client (not the real SDK) and set it once in meta.args, then drop per-story client props.

-import { storybookThirdwebClient } from "@/storybook/utils";
+import { storybookThirdwebClient } from "@/storybook/stubs";

 const meta = {
   component: AssetsSectionUI,
   decorators: [
     (Story) => (
-      <div className="mx-auto h-dvh w-full max-w-[300px] bg-card p-2">
+      <div className="mx-auto h-dvh w-full max-w-[300px] bg-card p-2">
         <Story />
       </div>
     ),
   ],
   title: "AI/AssetsSection",
+  args: { client: storybookThirdwebClient },
 } satisfies Meta<typeof AssetsSectionUI>;

 export const MultipleAssets: Story = {
   args: {
-    client: storybookThirdwebClient,
     data: tokensStub,
     isPending: false,
   },
 };

 export const SingleAsset: Story = {
   args: {
-    client: storybookThirdwebClient,
     data: tokensStub.slice(0, 1),
     isPending: false,
   },
 };

 export const EmptyAssets: Story = {
   args: {
-    client: storybookThirdwebClient,
     data: [],
     isPending: false,
   },
 };

 export const Loading: Story = {
   args: {
-    client: storybookThirdwebClient,
     data: [],
     isPending: true,
   },
 };

Also applies to: 5-15, 71-77, 79-85, 87-93, 95-101

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ExecuteTransactionCard.tsx (4)

95-100: Guard BigInt conversion and undefined chain to prevent runtime crashes

BigInt(txData.value) can throw and chain may be undefined. Render safely.

-          {txData.value !== undefined && (
+          {txData.value !== undefined && (
             <div className="flex items-center justify-between gap-2">
               <span className="font-medium text-muted-foreground">Value</span>
-              {toEther(BigInt(txData.value))} {chain.nativeCurrency?.symbol}
+              <span>
+                {(() => {
+                  try {
+                    return `${toEther(BigInt(txData.value))} ${chain?.nativeCurrency?.symbol ?? ""}`;
+                  } catch {
+                    return "—";
+                  }
+                })()}
+              </span>
             </div>
           )}

106-114: Optional chain for icon/name; add safe fallbacks

Avoid property access on possibly undefined chain.

               <ChainIconClient
                 className="size-5 rounded-full"
                 client={props.client}
-                src={chain.icon?.url}
+                src={chain?.icon?.url}
               />
               <span className="text-foreground">
-                {chain.name || `Chain ID: ${txData.chain_id}`}
+                {chain?.name ?? `Chain ID: ${txData.chain_id}`}
               </span>

134-139: Disable send without wallet/chain; set isLoggedIn from state

Prevents attempts to send without prerequisites.

               disabled={
-                props.status.type === "sending" ||
-                props.status.type === "confirming"
+                props.status.type === "sending" ||
+                props.status.type === "confirming" ||
+                !account ||
+                !chain
               }
-              isLoggedIn={true}
+              isLoggedIn={!!account}

141-149: Guard preconditions and wrap value parsing in try/catch

Avoids crashes and sending invalid txs.

-              onClick={async () => {
-                const tx = prepareTransaction({
-                  chain: chain,
-                  client: props.client,
-                  data: txData.data,
-                  to: txData.to,
-                  value: txData.value ? BigInt(txData.value) : undefined,
-                });
-
-                props.sendTx(tx, props.onTxSettled);
-              }}
+              onClick={async () => {
+                if (!account || !chain) return;
+                let valueWei: bigint | undefined = undefined;
+                try {
+                  valueWei = txData.value ? BigInt(txData.value) : undefined;
+                } catch {
+                  return;
+                }
+                const tx = prepareTransaction({
+                  chain,
+                  client: props.client,
+                  data: txData.data,
+                  to: txData.to,
+                  value: valueWei,
+                });
+                props.sendTx(tx, props.onTxSettled);
+              }}
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/api/types.ts (2)

6-15: Make “image” content a true one-of to avoid invalid states

Prevents { image_url: null, b64: null }.

 type NebulaUserMessageContentItem =
   | {
       type: "image";
-      image_url: string | null;
-      b64: string | null;
+      image_url: string;
+      b64: null;
     }
+  | {
+      type: "image";
+      image_url: null;
+      b64: string;
+    }
   | {
       type: "text";
       text: string;
     }

40-56: Typo: modal_name → model_name (twice)

Matches upstream naming and avoids dropped fields.

 export type SessionInfo = {
   id: string;
   account_id: string;
-  modal_name: string;
+  model_name: string;
@@
 export type UpdatedSessionInfo = {
   title: string;
-  modal_name: string;
+  model_name: string;
   account_id: string;
   context: SessionContextFilter | null;
 };
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ExecuteTransactionCard.stories.tsx (1)

67-67: Fix sendTx prop signature to match component contract

Current zero-arg handler fails type checks.

-        sendTx={async () => {}}
+        sendTx={async (_tx, _onTxSettled) => {
+          // no-op in story
+        }}
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatSidebarLink.tsx (1)

17-19: Don’t call tokened session API from client; proxy via internal route

Keep JWT server-side by hitting an internal DELETE route.

-import { deleteSession } from "../api/session";
+import { cn } from "@/lib/utils";
@@
-// TODO - add delete chat confirmation dialog
+// TODO - add delete chat confirmation dialog
+// Temporary client shim; replace with typed internal route when available.
+async function deleteSessionViaApi(sessionId: string) {
+  const res = await fetch(
+    `/api/thirdweb-ai/session/${encodeURIComponent(sessionId)}`,
+    { method: "DELETE" },
+  );
+  if (!res.ok) {
+    throw new Error("Failed to delete session");
+  }
+  return res.json();
+}
@@
-export function ChatSidebarLink(props: {
+export function ChatSidebarLink(props: {
   sessionId: string;
   title: string;
   project: Project;
   team_slug: string;
-}) {
+  className?: string;
+}) {
@@
-  const deleteChat = useMutation({
-    mutationFn: () => {
-      return deleteSession({
-        project: props.project,
-        sessionId: props.sessionId,
-      });
-    },
+  const deleteChat = useMutation({
+    mutationFn: () => deleteSessionViaApi(props.sessionId),
@@
-  return (
-    <div
-      className="group relative rounded-lg hover:bg-accent"
+  return (
+    <div
+      className={cn("group relative rounded-lg hover:bg-accent", props.className)}
       key={props.sessionId}
     >

Also applies to: 32-46, 74-91

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/chat/history/ChatHistoryPage.stories.tsx (3)

12-17: Provide Thirdweb client to Provider

Use the shared storybook client.

   decorators: [
     (Story) => (
-      <ThirdwebProvider>
+      <ThirdwebProvider client={storybookThirdwebClient}>
         <Story />
       </ThirdwebProvider>
     ),
   ],

54-69: Stable IDs and consistent timestamps for mock sessions

Make updated_atcreated_at and use UUIDs when available.

-function createRandomSessions(length: number) {
+function createRandomSessions(length: number) {
   const sessions = [];
   for (let i = 0; i < length; i++) {
-    sessions.push({
-      created_at: new Date().toISOString(),
-      id: Math.random().toString(),
-      title: randomLorem(Math.floor(5 + Math.random() * 15)),
-      updated_at: subDays(
-        new Date(),
-        Math.floor(Math.random() * 10),
-      ).toISOString(),
-    });
+    const daysAgo = Math.floor(Math.random() * 10) + 1;
+    sessions.push({
+      created_at: subDays(new Date(), daysAgo).toISOString(),
+      id:
+        typeof crypto !== "undefined" && crypto.randomUUID
+          ? crypto.randomUUID()
+          : Math.random().toString(36).slice(2),
+      title: randomLorem(Math.floor(5 + Math.random() * 15)),
+      updated_at: subDays(new Date(), daysAgo - 1).toISOString(),
+    });
   }
 
   return sessions;
 }

71-90: Generate sessions once and reuse for layout + UI to keep them in sync

Avoid two different lists on the same screen.

-function Variant(props: { length: number; prefillSearch?: string }) {
-  return (
+function Variant(props: { length: number; prefillSearch?: string }) {
+  const sessions = createRandomSessions(props.length);
+  return (
     <ChatPageLayout
       accountAddress="0x1234567890"
       authToken="xxxxxxxx"
-      sessions={createRandomSessions(props.length)}
+      sessions={sessions}
       project={projectStub("xxxxx", "team-1")}
       client={storybookThirdwebClient}
       team_slug="team-1"
     >
       <ChatHistoryPageUI
         deleteSession={async () => {
           await new Promise((resolve) => setTimeout(resolve, 1000));
         }}
         prefillSearch={props.prefillSearch}
-        sessions={createRandomSessions(props.length)}
+        sessions={sessions}
         team_slug="team-1"
         project={projectStub("xxxxx", "team-1")}
       />
     </ChatPageLayout>
   );
 }
🧹 Nitpick comments (20)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/data/examplePrompts.ts (3)

7-7: Rename variable to match the “thirdweb AI” rebrand

Keep identifiers consistent with the copy; drop “Nebula”.

-const whatCanNebulaDoReply = `
+const whatCanThirdwebAIDoReply = `
@@
-    interceptedReply: whatCanNebulaDoReply,
+    interceptedReply: whatCanThirdwebAIDoReply,

Also applies to: 85-85


7-82: Consider lazy-loading these large reply strings to trim the client bundle

These multiline Markdown blobs will ship to the client wherever this module is imported. Split them into a separate chunk and dynamically import in the empty‑state component.

Example in the consumer:

// inside EmptyStateChatPageContent
const { examplePrompts } = await import("../data/examplePrompts"); // on demand

9-9: Centralize the docs URL

Move the portal URL to a shared constants/config to avoid future link drift and ease updates.

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/page.tsx (3)

1-2: Add the server-only guard.

Mark this page as a Server Component explicitly to prevent accidental client import.

+import "server-only";
 import { notFound } from "next/navigation";
 import { getAuthToken, getUserThirdwebClient } from "@/api/auth-token";

8-10: Annotate the return type.

Match our TS guideline for explicit return types on exported functions.

 export default async function Page(props: {
   params: Promise<{ team_slug: string; project_slug: string }>;
-}) {
+}): Promise<JSX.Element> {

26-29: Fetch client and sessions concurrently to cut TTFB.

Both depend only on project; parallelize:

-  const client = await getUserThirdwebClient({ teamId: project.teamId });
-
-  const sessions = await getSessions({ project }).catch(() => []);
+  const [client, sessions] = await Promise.all([
+    getUserThirdwebClient({ teamId: project.teamId }),
+    getSessions({ project }).catch(() => []),
+  ]);
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/AssetsSection/AssetsSection.stories.tsx (2)

27-27: Gitleaks false positives: these are public Ethereum token addresses, not secrets.

Silence CI noise by allow-listing the address pattern in Gitleaks config.

Add to .gitleaks.toml:

[allowlist]
description = "Allow public Ethereum addresses in stubs/stories"
regexes = [
  '''(?i)\b0x[a-f0-9]{40}\b'''
]

Alternatively, scope it by path (stories/fixtures) if you prefer stricter matching.

Also applies to: 35-35, 43-43, 51-51, 59-59


9-11: Nit: prefer design-token widths over arbitrary max-w-[300px].

Use a tokenized width like max-w-xs (20rem) or w-80 for consistency with the design system.

-      <div className="mx-auto h-[100dvh] w-full max-w-[300px] bg-card p-2">
+      <div className="mx-auto h-[100dvh] w-full max-w-xs bg-card p-2">
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/TransactionsSection/TransactionsSection.stories.tsx (4)

8-18: Hoist default args (client) into meta to DRY the stories.

Removes duplication and reduces drift risk.

 const meta = {
   component: TransactionSectionUI,
   decorators: [
     (Story) => (
       <div className="mx-auto h-dvh w-full max-w-[300px] bg-card p-2">
         <Story />
       </div>
     ),
   ],
+  args: {
+    client: storybookThirdwebClient,
+  },
   title: "AI/TransactionsSection",
 } satisfies Meta<typeof TransactionSectionUI>;

23-23: Use satisfies for tighter type conformance without widening.

Prevents accidental extra keys while keeping literal types for better DX.

-const transactionsStub: WalletTransaction[] = [
+const transactionsStub = [
@@
-];
+] satisfies WalletTransaction[];

Also applies to: 121-121


40-50: Prefer omitting decoded entirely when unknown instead of empty strings.

Empty strings are ambiguous; decoded being absent better reflects “no decoding available” and exercises that UI path.

   {
     chain_id: "8453",
-    decoded: {
-      inputs: null,
-      name: "",
-      signature: "",
-    },
     from_address: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
     hash: "0xc521bfa0ba3e68fa1a52c67f93a8e215d3ade0b45956ba215390bcc0576202f1",
     to_address: "0x83dd93fa5d8343094f850f90b3fb90088c1bb425",
     value: "1000000000000000",
   },

123-129: Drop repeated client args in individual stories after hoisting to meta.

Keeps stories concise and consistent.

 export const MultipleAssets: Story = {
   args: {
-    client: storybookThirdwebClient,
     data: transactionsStub,
     isPending: false,
   },
 };
 
 export const SingleAsset: Story = {
   args: {
-    client: storybookThirdwebClient,
     data: transactionsStub.slice(0, 1),
     isPending: false,
   },
 };
 
 export const EmptyAssets: Story = {
   args: {
-    client: storybookThirdwebClient,
     data: [],
     isPending: false,
   },
 };
 
 export const Loading: Story = {
   args: {
-    client: storybookThirdwebClient,
     data: [],
     isPending: true,
   },
 };

Also applies to: 131-137, 139-145, 147-153

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ExecuteTransactionCard.tsx (1)

1-20: Expose className on root and merge with cn() per dashboard TSX rules

Lets parents control outer spacing/layout.

+import { cn } from "@/lib/utils";
@@
-export function ExecuteTransactionCard(props: {
+export function ExecuteTransactionCard(props: {
   txData: NebulaTxData;
   client: ThirdwebClient;
   onTxSettled: (txHash: string) => void;
-}) {
+  className?: string;
+}) {
@@
     <ExecuteTransactionCardLayout
       client={props.client}
       onTxSettled={props.onTxSettled}
+      className={props.className}
       sendTx={sendTx}
       setStatus={setStatus}
       status={status}
       txData={props.txData}
     />
@@
-export function ExecuteTransactionCardLayout(props: {
+export function ExecuteTransactionCardLayout(props: {
   txData: NebulaTxData;
   client: ThirdwebClient;
   status: TxStatus;
   setStatus: (status: TxStatus) => void;
   onTxSettled: (txHash: string) => void;
   sendTx: (
     tx: PreparedTransaction,
     onTxSettled: (txHash: string) => void,
   ) => Promise<void>;
-}) {
+  className?: string;
+}) {
@@
-  return (
-    <div>
+  return (
+    <div className={cn(props.className)}>

Also applies to: 21-38, 40-51, 55-63

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ExecuteTransactionCard.stories.tsx (1)

12-16: Pass client to ThirdwebProvider in stories

Prevents context-dependent issues.

-    (Story) => (
-      <ThirdwebProvider>
+    (Story) => (
+      <ThirdwebProvider client={storybookThirdwebClient}>
         <Story />
       </ThirdwebProvider>
     ),
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/chat/history/ChatHistoryPage.tsx (1)

7-13: Consider lazy-loading Fuse.js to reduce initial bundle

Fuse is relatively heavy; defer loading until needed (when there are sessions or a query).

Example pattern:

// replace static import
// import Fuse from "fuse.js";
const [FuseCtor, setFuseCtor] = useState<typeof import("fuse.js")["default"] | null>(null);
useEffect(() => {
  import("fuse.js").then((m) => setFuseCtor(m.default));
}, []);

const fuse = useMemo(() => {
  if (!FuseCtor) return null;
  return new FuseCtor(allSessions, { keys: [{ name: "title", weight: 1 }], threshold: 0.5 });
}, [allSessions, FuseCtor]);

const filtered = useMemo(() => (searchVal && fuse ? fuse.search(searchVal).map((e) => e.item) : allSessions), [searchVal, fuse, allSessions]);

Also applies to: 59-69

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatSidebar.tsx (5)

2-9: Use NavLink for internal navigation to get active states

Per dashboard rules, prefer NavLink for internal routes. Keep Link for the external docs.

+import { NavLink } from "@/components/ui/NavLink";
 import Link from "next/link";
-        <Link
+        <NavLink
+          activeClassName="underline"
           className="flex items-center gap-1 rounded-full text-foreground text-sm hover:underline justify-between"
           href={`/team/${props.team_slug}/${props.project.slug}/ai/analytics`}
-        >
+        >
           <div className="flex items-center gap-2">
             <ChartLineIcon className="size-3.5 text-muted-foreground" />
             Analytics
           </div>
           <ChevronRightIcon className="size-3.5 text-muted-foreground" />
-        </Link>
+        </NavLink>
-              <Link
+              <NavLink
+                activeClassName="underline"
                 className="flex items-center gap-1 rounded-full text-foreground text-xs hover:underline"
                 href={`/team/${props.team_slug}/${props.project.slug}/ai/chat/history`}
-              >
+              >
                 View All
                 <ChevronRightIcon className="size-3.5 text-muted-foreground" />
-              </Link>
+              </NavLink>

Also applies to: 86-95, 114-120


38-44: Add explicit return types for TSX components

Follow TS guideline: explicit return types help with inference and future refactors.

-export function ChatSidebar(props: { ... }) {
+export function ChatSidebar(props: { ... }): JSX.Element {
-function WalletDetails(props: { client: ThirdwebClient }) {
+function WalletDetails(props: { client: ThirdwebClient }): JSX.Element {
-function CustomConnectButton(props: { client: ThirdwebClient }) {
+function CustomConnectButton(props: { client: ThirdwebClient }): JSX.Element {

Also applies to: 147-151, 201-205


170-196: Remove redundant isExpanded check inside isExpanded block

The inner {isExpanded && ...} is nested within another isExpanded, so it’s dead code. Also add an id to hook up aria-controls.

-          {isExpanded && (
-            <div className="h-[230px] overflow-y-auto py-4 pr-1 pl-2">
-              {tab === "assets" && <AssetsSection client={props.client} />}
-
-              {tab === "transactions" && (
-                <TransactionsSection client={props.client} />
-              )}
-            </div>
-          )}
+          <div
+            className="h-[230px] overflow-y-auto py-4 pr-1 pl-2"
+            id="wallet-details-content"
+          >
+            {tab === "assets" && <AssetsSection client={props.client} />}
+            {tab === "transactions" && (
+              <TransactionsSection client={props.client} />
+            )}
+          </div>

67-76: Avoid full page reload; force re-mount via search param

Prevent a jarring reload and preserve app state by replacing with a same-path navigation that changes search params.

           onClick={() => {
             if (pathname === newChatPage) {
-              // TODO - make it not reload the whole page, but just the React session state of the chat component
-              window.location.reload();
+              // Force a re-mount without a full reload
+              router.replace(`${newChatPage}?r=${Date.now()}`);
             } else {
               router.push(newChatPage);
             }
           }}

154-164: Improve a11y for collapse toggle

Expose expanded state and control relationship.

-        <Button
+        <Button
           className={cn(
             "h-auto w-auto p-1.5 transition-transform duration-300",
             isExpanded ? "rotate-180" : "",
           )}
           onClick={() => setIsExpanded(!isExpanded)}
+          aria-expanded={isExpanded}
+          aria-controls="wallet-details-content"
           size="sm"
           variant="ghost"
         >
           <ChevronDownIcon className="size-4" />
         </Button>

Also applies to: 186-193

Comment on lines +25 to +46
import { deleteSession } from "../../api/session";
import type { TruncatedSessionInfo } from "../../api/types";
import { useSessionsWithLocalOverrides } from "../../hooks/useSessionsWithLocalOverrides";
import { deletedSessionsStore } from "../../stores";

export function ChatHistoryPage(props: {
sessions: TruncatedSessionInfo[];
prefillSearch?: string;
team_slug: string;
project: Project;
}) {
return (
<ChatHistoryPageUI
{...props}
deleteSession={async (s) => {
await deleteSession({
project: props.project,
sessionId: s,
});
}}
/>
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Move session deletion off the client; avoid importing tokened API here

This is a client module. Proxy via an internal route to keep JWT server-side.

-import { deleteSession } from "../../api/session";
+// do not import tokened server API in client components
@@
 export function ChatHistoryPage(props: {
@@
   return (
     <ChatHistoryPageUI
       {...props}
-      deleteSession={async (s) => {
-        await deleteSession({
-          project: props.project,
-          sessionId: s,
-        });
-      }}
+      deleteSession={async (s) => {
+        const res = await fetch(
+          `/api/thirdweb-ai/session/${encodeURIComponent(s)}`,
+          { method: "DELETE" },
+        );
+        if (!res.ok) throw new Error("Failed to delete session");
+        await res.json();
+      }}
     />
   );
📝 Committable suggestion

‼️ 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.

Suggested change
import { deleteSession } from "../../api/session";
import type { TruncatedSessionInfo } from "../../api/types";
import { useSessionsWithLocalOverrides } from "../../hooks/useSessionsWithLocalOverrides";
import { deletedSessionsStore } from "../../stores";
export function ChatHistoryPage(props: {
sessions: TruncatedSessionInfo[];
prefillSearch?: string;
team_slug: string;
project: Project;
}) {
return (
<ChatHistoryPageUI
{...props}
deleteSession={async (s) => {
await deleteSession({
project: props.project,
sessionId: s,
});
}}
/>
);
// do not import tokened server API in client components
import type { TruncatedSessionInfo } from "../../api/types";
import { useSessionsWithLocalOverrides } from "../../hooks/useSessionsWithLocalOverrides";
import { deletedSessionsStore } from "../../stores";
export function ChatHistoryPage(props: {
sessions: TruncatedSessionInfo[];
prefillSearch?: string;
team_slug: string;
project: Project;
}) {
return (
<ChatHistoryPageUI
{...props}
deleteSession={async (s) => {
const res = await fetch(
`/api/thirdweb-ai/session/${encodeURIComponent(s)}`,
{ method: "DELETE" },
);
if (!res.ok) throw new Error("Failed to delete session");
await res.json();
}}
/>
);
}

Comment on lines +38 to +44
export function ChatSidebar(props: {
sessions: TruncatedSessionInfo[];
team_slug: string;
project: Project;
client: ThirdwebClient;
type: "desktop" | "mobile";
}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Expose className on root and merge with cn()

Dashboard components should accept a className and apply it on the root. Add the prop and merge classes.

-export function ChatSidebar(props: {
+export function ChatSidebar(props: {
   sessions: TruncatedSessionInfo[];
   team_slug: string;
   project: Project;
   client: ThirdwebClient;
   type: "desktop" | "mobile";
-}) {
+  className?: string;
+}): JSX.Element {
   const sessions = useSessionsWithLocalOverrides(props.sessions);
-    <div className="flex h-full flex-col">
+    <div className={cn("flex h-full flex-col", props.className)}>

Also applies to: 51-52

🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatSidebar.tsx
around lines 38-44 (and also update the prop types at lines 51-52), add an
optional className?: string to the component props, then merge it with the
component's existing classes using the cn(...) utility and apply the result to
the root/outermost JSX element; ensure the prop is optional, forwarded/used when
present, and that any existing hardcoded class strings are combined with the
incoming className via cn so external styling can be applied.

Comment on lines +43 to +44
type: "desktop" | "mobile";
}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Unused prop ‘type’ ("desktop" | "mobile")

It isn’t used in this component. If not consumed by callers, drop it; otherwise, consider using it (e.g., default collapsed on mobile).

Run to confirm external usage before removal:


🏁 Script executed:

#!/bin/bash
# Find ChatSidebar call sites and look for a `type` prop usage.
rg -nP --type tsx -C2 '\b<ChatSidebar\b' | sed -n '1,200p'
rg -nP --type tsx '\btype\s*=\s*"(desktop|mobile)"'

Length of output: 55


🏁 Script executed:

#!/bin/bash
# Search for any ChatSidebar usage in TSX/JSX files
rg -n '<ChatSidebar' -g '*.{tsx,jsx}' -C3

# Search for explicit type="desktop" or type="mobile" assignments
rg -n 'type\s*=\s*"(desktop|mobile)"' -g '*.{tsx,jsx}'

Length of output: 5392


Remove unused type prop from ChatSidebar component signature
The type prop isn’t used inside this component and no callers in apps/dashboard pass it; remove it from the props definition.

🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatSidebar.tsx
around lines 43-44, the component signature includes an unused prop `type:
"desktop" | "mobile"`; remove `type` from the props parameter and any related
type annotations in this file so the component only accepts the props it
actually uses, and update the component’s prop type/interface definition
accordingly (also run a quick search to confirm no internal references to `type`
remain).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (23)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/Swap/SwapCards.tsx (4)

1-1: Add 'use client' (hooks are used in this file).

Without it, Next will treat this as a Server Component and crash at runtime/build.

+'use client';
+
 import { ArrowRightLeftIcon, CheckIcon } from "lucide-react";

42-47: Guard prepareTransaction and disable the Swap button until chain is ready.

txChain can be undefined; calling prepareTransaction with an undefined chain will throw. Also await sendTx and pass txChain?.id to TransactionButton.

   const txChain = useV5DashboardChain(swapData.transaction.chain_id);
@@
             <TransactionButton
               className="gap-2"
               client={props.client}
-              disabled={
-                props.status.type === "sending" ||
-                props.status.type === "confirming"
-              }
+              disabled={
+                !txChain ||
+                props.status.type === "sending" ||
+                props.status.type === "confirming"
+              }
               isLoggedIn={true}
               isPending={props.status.type === "sending"}
               onClick={async () => {
+                if (!txChain) {
+                  return;
+                }
                 const tx = prepareTransaction({
                   chain: txChain,
                   client: props.client,
                   data: swapData.transaction.data,
                   value: swapData.transaction.value
                     ? BigInt(swapData.transaction.value)
                     : undefined,
                   extraGas: 50000n,
                   erc20Value: isSellingNativeToken
                     ? undefined
                     : {
                         amountWei: BigInt(swapData.from_token.amount),
                         tokenAddress: swapData.from_token.address,
                       },
                   to: swapData.transaction.to,
                 });
 
-                props.sendTx(tx);
+                await props.sendTx(tx);
               }}
               size="sm"
               transactionCount={undefined}
-              txChainID={swapData.transaction.chain_id}
+              txChainID={txChain?.id ?? swapData.transaction.chain_id}
               variant="default"
             >

Also applies to: 92-103, 104-123, 126-127


164-168: Mirror the same chain-ready guards for Approve.

Avoid prepareTransaction with an undefined chain; await sendTx; pass txChain?.id.

   const isTransactionPending =
     props.status.type === "sending" || props.status.type === "confirming";
@@
             <TransactionButton
               className="gap-2"
               client={props.client}
-              disabled={isTransactionPending}
+              disabled={!txChain || isTransactionPending}
               isLoggedIn={true}
               isPending={isTransactionPending}
               onClick={async () => {
+                if (!txChain) {
+                  return;
+                }
                 const tx = prepareTransaction({
                   chain: txChain,
                   client: props.client,
                   data: swapData.transaction.data,
                   to: swapData.transaction.to,
                 });
 
-                props.sendTx(tx);
+                await props.sendTx(tx);
               }}
               size="sm"
               transactionCount={undefined}
-              txChainID={swapData.transaction.chain_id}
+              txChainID={txChain?.id ?? swapData.transaction.chain_id}
               variant="default"
             >

Also applies to: 217-224, 225-234, 237-238


258-276: Avoid undefined access: guard chain metadata in TokenRow.

chain may be undefined while chains load; chain.icon/name access will throw.

   const chain = useV5DashboardChain(props.chainId);
@@
           <ChainIconClient
             className="size-4"
             client={props.client}
-            src={chain.icon?.url || ""}
+            src={chain?.icon?.url}
           />
-          <div>{chain.name}</div>
+          <div>{chain?.name ?? `Chain ${props.chainId}`}</div>
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/AssetsSection/AssetsSection.stories.tsx (2)

9-11: Replace non-standard h-dvh with supported utility

Tailwind may not have h-dvh configured. Use h-[100dvh] (or your DVH token).

-      <div className="mx-auto h-dvh w-full max-w-[300px] bg-card p-2">
+      <div className="mx-auto h-[100dvh] w-full max-w-[300px] bg-card p-2">

2-2: Use a stubbed Storybook client (no network in stories)

Make sure storybookThirdwebClient is a stub/mocked client so stories never hit live APIs. If the version in @/storybook/utils uses the real SDK, swap to a stub.

Example change (adjust path to your existing stub):

-import { storybookThirdwebClient } from "@/storybook/utils";
+import { storybookThirdwebClient } from "@/storybook/stubs";
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/AssetsSection/AssetsSection.tsx (1)

213-245: Type queries, gate execution, and set staleTime

Aligns with guidelines and reduces refetch churn.

-  const nativeBalances = useQuery({
-    queryFn: async () => {
+  const nativeBalances = useQuery<AssetBalance[]>({
+    enabled: !!account && !!activeChain,
+    staleTime: 60_000,
+    queryFn: async () => {
       if (!account || !activeChain) {
         return [];
       }
@@
-    queryKey: ["getWalletBalance", account?.address, activeChain?.id],
+    queryKey: ["getWalletBalance", account?.address, activeChain?.id],
   });
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/data/examplePrompts.ts (1)

1-5: Reuse the shared ExamplePrompt type from the chat types barrel

Avoid duplicate shapes; import the canonical type.

-type ExamplePrompt = {
-  title: string;
-  message: string;
-  interceptedReply?: string;
-};
+import type { ExamplePrompt } from "@/components/chat/types";
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/api/chat.ts (1)

11-19: Critical: JWT is exposed to the browser; proxy the stream via an internal route

This function is used by client code; passing authToken and setting Authorization here leaks the JWT. Move the upstream call behind a Next.js route that reads the token server-side, then call that route from the client.

-export async function promptNebula(params: {
+export async function promptNebula(params: {
   message: NebulaUserMessage;
   sessionId: string;
-  authToken: string;
   handleStream: (res: ChatStreamedResponse) => void;
   abortController: AbortController;
   context: undefined | NebulaContext;
   project: Project;
-}) {
+}): Promise<void> {
@@
-    const events = await stream(`${NEXT_PUBLIC_THIRDWEB_AI_HOST}/chat`, {
+    const events = await stream(`/api/thirdweb-ai/chat`, {
       body: JSON.stringify(body),
       headers: {
-        Authorization: `Bearer ${params.authToken}`,
         "x-team-id": params.project.teamId,
         "x-client-id": params.project.publishableKey,
         "Content-Type": "application/json",
       },

Example route (new file) to add server-side:

// apps/dashboard/src/app/api/thirdweb-ai/chat/route.ts
import "server-only";
import { NextRequest } from "next/server";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_AI_HOST } from "@/constants/public-envs";

export async function POST(req: NextRequest) {
  const token = await getAuthToken();
  if (!token) return new Response("unauthorized", { status: 401 });

  const upstream = await fetch(`${NEXT_PUBLIC_THIRDWEB_AI_HOST}/chat`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${token}`,
      "content-type": "application/json",
      "x-team-id": req.headers.get("x-team-id") ?? "",
      "x-client-id": req.headers.get("x-client-id") ?? "",
    },
    body: await req.text(),
  });

  return new Response(upstream.body, {
    status: upstream.status,
    headers: {
      "content-type":
        upstream.headers.get("content-type") ?? "text/event-stream",
    },
  });
}

Also applies to: 38-41

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/chat/history/ChatHistoryPage.tsx (1)

25-29: Do not call tokened server utilities from the client; delete session via an internal API route

Importing ../../api/session here risks leaking auth details and won’t work in the browser. Proxy the DELETE through /api/thirdweb-ai/session/[id].

-import { deleteSession } from "../../api/session";
@@
   return (
     <ChatHistoryPageUI
       {...props}
       deleteSession={async (s) => {
-        await deleteSession({
-          project: props.project,
-          sessionId: s,
-        });
+        const res = await fetch(
+          `/api/thirdweb-ai/session/${encodeURIComponent(s)}`,
+          { method: "DELETE" },
+        );
+        if (!res.ok) {
+          throw new Error("Failed to delete session");
+        }
       }}
     />
   );

Example API route to add:

// apps/dashboard/src/app/api/thirdweb-ai/session/[id]/route.ts
import "server-only";
import { NEXT_PUBLIC_THIRDWEB_AI_HOST } from "@/constants/public-envs";
import { getAuthToken } from "@/api/auth-token";
import { NextRequest } from "next/server";

export async function DELETE(
  _req: NextRequest,
  { params }: { params: { id: string } },
) {
  const token = await getAuthToken();
  if (!token) return new Response("unauthorized", { status: 401 });

  const upstream = await fetch(
    `${NEXT_PUBLIC_THIRDWEB_AI_HOST}/session/${encodeURIComponent(params.id)}`,
    { method: "DELETE", headers: { Authorization: `Bearer ${token}` } },
  );
  return new Response(await upstream.text(), { status: upstream.status });
}

Also applies to: 39-45

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/NebulaImage.tsx (1)

1-3: Missing 'use client' directive

This component uses hooks; add the client directive at the top.

+'use client';
+
 import { useMutation } from "@tanstack/react-query";
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/chat/history/ChatHistoryPage.stories.tsx (2)

71-90: Create sessions once and pass to both layout and UI; add explicit return type.

Prevents sidebar/content desync and satisfies the “explicit return types” guideline.

-function Variant(props: { length: number; prefillSearch?: string }) {
+function Variant(props: { length: number; prefillSearch?: string }): JSX.Element {
-  return (
+  const sessions = createRandomSessions(props.length);
+  return (
     <ChatPageLayout
       accountAddress="0x1234567890"
       authToken="xxxxxxxx"
-      sessions={createRandomSessions(props.length)}
+      sessions={sessions}
       project={projectStub("xxxxx", "team-1")}
       client={storybookThirdwebClient}
       team_slug="team-1"
     >
       <ChatHistoryPageUI
         deleteSession={async () => {
           await new Promise((resolve) => setTimeout(resolve, 1000));
         }}
         prefillSearch={props.prefillSearch}
-        sessions={createRandomSessions(props.length)}
+        sessions={sessions}
         team_slug="team-1"
         project={projectStub("xxxxx", "team-1")}
       />
     </ChatPageLayout>
   );
 }

13-15: Pass a client to ThirdwebProvider (story otherwise breaks runtime).

Provider requires a client; use the story’s stubbed client.

-      <ThirdwebProvider>
+      <ThirdwebProvider client={storybookThirdwebClient}>
         <Story />
       </ThirdwebProvider>
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatPageContent.tsx (1)

159-166: Guard optional message text to prevent runtime crash on image-only user messages.

.toLowerCase() can be invoked on undefined.

-      const lowerCaseMessage = message.content
-        .find((x) => x.type === "text")
-        ?.text.toLowerCase();
+      const lowerCaseMessage = message.content
+        .find((x) => x.type === "text")
+        ?.text
+        ?.toLowerCase();
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/Chats.tsx (3)

90-110: Add effect cleanup for event listeners (prevent leaks).

   useEffect(() => {
     if (!enableAutoScroll) {
       return;
     }
@@
     chatScrollContainer.addEventListener("mousedown", disableScroll);
     chatScrollContainer.addEventListener("wheel", disableScroll);
+    return () => {
+      chatScrollContainer.removeEventListener("mousedown", disableScroll);
+      chatScrollContainer.removeEventListener("wheel", disableScroll);
+    };
   }, [setEnableAutoScroll, enableAutoScroll]);

1-4: Mark as Client Component.

This file uses React hooks; add the directive.

+ 'use client';
 import { MarkdownRenderer } from "@workspace/ui/components/markdown-renderer";

66-74: Remove unused authToken prop (avoid exposing secrets to UI).

Not used here and should not flow through UI components.

   isChatStreaming: boolean;
-  authToken: string;
   sessionId: string | undefined;
@@
-                  <RenderMessage
+                  <RenderMessage
                     project={props.project}
-                    authToken={props.authToken}
                     client={props.client}
@@
-function RenderMessage(props: {
+function RenderMessage(props: {
   message: ChatMessage;
   isMessagePending: boolean;
   client: ThirdwebClient;
   sendMessage: (message: NebulaUserMessage) => void;
   nextMessage: ChatMessage | undefined;
-  authToken: string;
   sessionId: string | undefined;
   project: Project;
 }) {
@@
-          <RenderResponse
+          <RenderResponse
             project={props.project}
-            authToken={props.authToken}
             client={props.client}
@@
-function RenderResponse(props: {
+function RenderResponse(props: {
   message: ChatMessage;
   isMessagePending: boolean;
   client: ThirdwebClient;
   sendMessage: (message: NebulaUserMessage) => void;
   nextMessage: ChatMessage | undefined;
   sessionId: string | undefined;
-  authToken: string;
   project: Project;
 }) {

Also applies to: 146-155, 259-266, 173-176, 287-296

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatBar.tsx (6)

56-60: Rename brand in toast: “Nebula” → “thirdweb”.

 function showSigninToUploadImagesToast() {
-  toast.error("Sign in to upload images to Nebula", {
+  toast.error("Sign in to upload images to thirdweb", {
     position: "top-right",
   });
 }

374-376: Rename brand in popover copy: “Nebula” → “thirdweb”.

-                        Get access to image uploads by signing in to Nebula
+                        Get access to image uploads by signing in to thirdweb

537-541: Avoid mutating props when sorting wallets.

Clone before sorting.

-  const sortedWallets = props.wallets.sort((a, b) => {
+  const sortedWallets = [...props.wallets].sort((a, b) => {
     if (a.walletId === "smart") return -1;
     if (b.walletId === "smart") return 1;
     return 0;
   });

176-199: Fix drag-over highlight getting stuck and adjust contains() typing.

         onDragEnter={(e) => {
           e.preventDefault();
-          setIsDragOver(true);
-          if (!props.allowImageUpload) {
-            return;
-          }
+          if (!props.allowImageUpload) return;
+          setIsDragOver(true);
         }}
         onDragLeave={(e) => {
           e.preventDefault();
-          if (!props.allowImageUpload) {
-            return;
-          }
           // Only set drag over to false if we're leaving the container entirely
-          if (!e.currentTarget.contains(e.relatedTarget as Node)) {
+          if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
             setIsDragOver(false);
           }
         }}
         onDragOver={(e) => {
           e.preventDefault();
-          setIsDragOver(true);
-          if (!props.allowImageUpload) {
-            return;
-          }
+          if (!props.allowImageUpload) return;
+          setIsDragOver(true);
         }}

232-236: Prevent Enter from sending empty messages (no text and no images).

-                if (e.key === "Enter" && !props.isChatStreaming) {
+                if (e.key === "Enter" && !props.isChatStreaming) {
                   e.preventDefault();
-                  handleSubmit(message);
+                  if (message.trim() !== "" || images.length > 0) {
+                    handleSubmit(message);
+                  }
                 }

95-101: Send raw base64, not a full data URL, in image content.

Shrinks payloads and matches typical API schemas for b64 fields.

-          userMessage.content.push({
-            b64: image.b64,
+          const dataUrl = image.b64;
+          const base64 = dataUrl.includes(",") ? dataUrl.split(",")[1] : dataUrl;
+          userMessage.content.push({
+            b64: base64,
             image_url: null,
             type: "image",
           });
🧹 Nitpick comments (22)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/Swap/SwapCards.tsx (5)

16-20: Add explicit return types to exported components.

Matches our TS guidelines and improves API clarity.

-export function SwapTransactionCard(props: {
+export function SwapTransactionCard(props: {
   swapData: NebulaSwapData;
   client: ThirdwebClient;
   onTxSettled: (txHash: string) => void;
-}) {
+}): JSX.Element {
@@
-export function SwapTransactionCardLayout(props: {
+export function SwapTransactionCardLayout(props: {
   swapData: NebulaSwapData;
   client: ThirdwebClient;
   status: TxStatus;
   setStatus: (status: TxStatus) => void;
   sendTx: (tx: PreparedTransaction) => Promise<void>;
-}) {
+}): JSX.Element {
@@
-export function ApproveTransactionCard(props: {
+export function ApproveTransactionCard(props: {
   swapData: NebulaSwapData;
   client: ThirdwebClient;
-}) {
+}): JSX.Element {
@@
-export function ApproveTransactionCardLayout(props: {
+export function ApproveTransactionCardLayout(props: {
   swapData: NebulaSwapData;
   client: ThirdwebClient;
   status: TxStatus;
   setStatus: (status: TxStatus) => void;
   sendTx: (tx: PreparedTransaction) => Promise<void>;
-}) {
+}): JSX.Element {
@@
-function TokenRow(props: {
+function TokenRow(props: {
   amount: string;
   symbol: string;
   chainId: number;
   client: ThirdwebClient;
   title: string;
   decimals: number;
-}) {
+}): JSX.Element {

Also applies to: 34-41, 139-144, 156-163, 250-257


16-20: Expose className on root and merge with cn().

Required by our dashboard .tsx guideline; keeps components composable.

+import { cn } from "@/lib/utils";
@@
-export function SwapTransactionCard(props: {
+export function SwapTransactionCard(props: {
   swapData: NebulaSwapData;
   client: ThirdwebClient;
   onTxSettled: (txHash: string) => void;
-}): JSX.Element {
+  className?: string;
+}): JSX.Element {
@@
-    <SwapTransactionCardLayout
+    <SwapTransactionCardLayout
       client={props.client}
       sendTx={(tx) => sendTx(tx, props.onTxSettled)}
-      setStatus={setStatus}
+      setStatus={setStatus}
       status={status}
       swapData={props.swapData}
+      className={props.className}
     />
@@
-export function SwapTransactionCardLayout(props: {
+export function SwapTransactionCardLayout(props: {
   swapData: NebulaSwapData;
   client: ThirdwebClient;
   status: TxStatus;
   setStatus: (status: TxStatus) => void;
   sendTx: (tx: PreparedTransaction) => Promise<void>;
-}): JSX.Element {
+  className?: string;
+}): JSX.Element {
@@
-  return (
-    <div className="max-w-lg">
+  return (
+    <div className={cn("max-w-lg", props.className)}>
@@
-export function ApproveTransactionCard(props: {
+export function ApproveTransactionCard(props: {
   swapData: NebulaSwapData;
   client: ThirdwebClient;
-}): JSX.Element {
+  className?: string;
+}): JSX.Element {
@@
-    <ApproveTransactionCardLayout
+    <ApproveTransactionCardLayout
       client={props.client}
       sendTx={(tx) => sendTx(tx, undefined)}
-      setStatus={setStatus}
+      setStatus={setStatus}
       status={status}
       swapData={props.swapData}
+      className={props.className}
     />
@@
-export function ApproveTransactionCardLayout(props: {
+export function ApproveTransactionCardLayout(props: {
   swapData: NebulaSwapData;
   client: ThirdwebClient;
   status: TxStatus;
   setStatus: (status: TxStatus) => void;
   sendTx: (tx: PreparedTransaction) => Promise<void>;
-}): JSX.Element {
+  className?: string;
+}): JSX.Element {
@@
-  return (
-    <div className="max-w-lg">
+  return (
+    <div className={cn("max-w-lg", props.className)}>

Also applies to: 34-41, 49-50, 139-144, 156-163, 170-171


34-41: Remove unused setStatus prop from Layouts.

It’s not referenced inside either Layout; simplify the API.

-export function SwapTransactionCardLayout(props: {
+export function SwapTransactionCardLayout(props: {
   swapData: NebulaSwapData;
   client: ThirdwebClient;
   status: TxStatus;
-  setStatus: (status: TxStatus) => void;
   sendTx: (tx: PreparedTransaction) => Promise<void>;
   className?: string;
 }): JSX.Element {
@@
-      setStatus={setStatus}
       status={status}
@@
-export function ApproveTransactionCardLayout(props: {
+export function ApproveTransactionCardLayout(props: {
   swapData: NebulaSwapData;
   client: ThirdwebClient;
   status: TxStatus;
-  setStatus: (status: TxStatus) => void;
   sendTx: (tx: PreparedTransaction) => Promise<void>;
   className?: string;
 }): JSX.Element {
@@
-      setStatus={setStatus}
       status={status}

Also applies to: 146-153, 156-163


259-266: Verify amount semantics; BigInt will throw for decimal strings.

Confirm from_token.amount/to_token.amount are atomic-unit integers. If they may contain decimals, parse units first.

Example fallback:

-  const tokenDisplayValue = toTokens(BigInt(props.amount), props.decimals);
+  const amountWei =
+    props.amount.includes(".")
+      ? /* parse to wei */ BigInt(
+          (Number.parseFloat(props.amount) * 10 ** props.decimals).toFixed(0),
+        )
+      : BigInt(props.amount);
+  const tokenDisplayValue = toTokens(amountWei, props.decimals);

44-47: Broaden native-token detection (optional).

Some sources use zero-address for native tokens; consider treating both sentinel and zero-address as native.

-  const isSellingNativeToken =
-    getAddress(swapData.from_token.address) ===
-    getAddress(NATIVE_TOKEN_ADDRESS);
+  const fromAddr = getAddress(swapData.from_token.address);
+  const isSellingNativeToken =
+    fromAddr === getAddress(NATIVE_TOKEN_ADDRESS) ||
+    fromAddr === getAddress("0x0000000000000000000000000000000000000000");
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/AssetsSection/AssetsSection.stories.tsx (1)

20-69: Gitleaks “Generic API Key” alerts are false positives (token addresses)

These 0x... values are public contract addresses in stub data, not secrets. Add an allowlist to silence noise in CI.

Add to your gitleaks config (example):

# .gitleaks.toml
[[rules.allowlist.regexes]]
description = "Allow Ethereum-like addresses in story stubs"
regex = '''0x[a-fA-F0-9]{40}'''

Or exclude Storybook stubs path in allowlist.paths.

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/AssetsSection/AssetsSection.tsx (2)

146-154: Pass through className from container to UI

Expose className on AssetsSection and forward it to AssetsSectionUI.

-export function AssetsSection(props: { client: ThirdwebClient }) {
+export function AssetsSection(props: { client: ThirdwebClient; className?: string }) {
@@
   return (
-    <AssetsSectionUI client={props.client} data={data} isPending={isPending} />
+    <AssetsSectionUI
+      client={props.client}
+      data={data}
+      isPending={isPending}
+      className={props.className}
+    />
   );
 }

Also applies to: 249-253


200-209: Optional: move sorting to select for memoized derivation

Using React Query’s select avoids re-sorting on re-renders when the fetched array identity is stable.

Example:

const assetsQuery = useQuery<AssetBalance[]>({
  // ...
  select: (data) => data.slice().sort(sortByTopTokens),
});
apps/dashboard/src/@/constants/public-envs.ts (1)

33-34: Normalize trailing slash to avoid accidental double slashes in requests

If env includes a trailing slash, ${host}/chat becomes //chat. Normalize once at definition.

-export const NEXT_PUBLIC_THIRDWEB_AI_HOST =
-  process.env.NEXT_PUBLIC_THIRDWEB_AI_HOST || "https://nebula-api.thirdweb.com";
+const _AI_HOST =
+  process.env.NEXT_PUBLIC_THIRDWEB_AI_HOST || "https://nebula-api.thirdweb.com";
+export const NEXT_PUBLIC_THIRDWEB_AI_HOST = _AI_HOST.replace(/\/+$/, "");
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/data/examplePrompts.ts (1)

7-8: Consistency: rename Nebula-era identifier

Variable name still says “Nebula”. Optional rename for clarity with the new branding.

-const whatCanNebulaDoReply = `
+const whatCanThirdwebAIDoReply = `
@@
-    interceptedReply: whatCanNebulaDoReply,
+    interceptedReply: whatCanThirdwebAIDoReply,

Also applies to: 85-88

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/api/chat.ts (3)

20-33: Strongly type the request body and coerce chain IDs to numbers

Prevents schema drift and aligns with codebase TS guidelines.

-  const body: Record<string, string | boolean | object> = {
+  type ChatRequestBody = {
+    messages: NebulaUserMessage[];
+    session_id: string;
+    stream: true;
+    context?: {
+      chain_ids: number[];
+      networks: NebulaContext["networks"];
+      wallet_address: string | null;
+    };
+  };
+
+  const body: ChatRequestBody = {
     messages: [params.message],
     session_id: params.sessionId,
     stream: true,
   };
 
   if (params.context) {
-    body.context = {
-      chain_ids: params.context.chainIds || [],
-      networks: params.context.networks,
-      wallet_address: params.context.walletAddress,
-    };
+    body.context = {
+      chain_ids: (params.context.chainIds ?? [])
+        .map((x) => Number(x))
+        .filter((n) => Number.isFinite(n)),
+      networks: params.context.networks,
+      wallet_address: params.context.walletAddress ?? null,
+    };
   }

184-194: Abort semantics: don’t surface an error when the user cancels

If the request was intentionally aborted, skip emitting an error event.

-  } catch (error) {
-    console.error("failed to stream events", error);
-    params.handleStream({
+  } catch (error) {
+    if (params.abortController.signal.aborted) {
+      return;
+    }
+    console.error("failed to stream events", error);
+    params.handleStream({
       data: {
         code: 500,
         errorMessage: `Failed to stream events: ${error instanceof Error ? error.message : "Unknown error"}`,
       },
       event: "error",
     });
     params.abortController.abort();
   }

55-62: Harden per-event parsing to avoid stream teardown on bad JSON

Wrap JSON.parse per branch; a single malformed event currently aborts the entire stream.

If you want, I can push a patch that adds small try/catch blocks around each parse and logs the offending event without breaking the loop.

Also applies to: 65-71, 73-89, 91-121, 125-141, 143-153, 155-173

apps/dashboard/src/@/components/ui/image-upload-button.tsx (2)

7-15: Expose wrapper className and allow same-file reselect

  • Add wrapperClassName for styling the root element (per dashboard rules), keep existing className for the Button.
  • Clear input value after change so selecting the same file twice triggers onChange.
 import { useRef } from "react";
 import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
 
 interface ImageUploadProps {
   value: File | undefined;
   onChange?: (files: File[]) => void;
   children?: React.ReactNode;
   variant?: React.ComponentProps<typeof Button>["variant"];
   className?: string;
+  wrapperClassName?: string;
   multiple?: boolean;
   accept: string;
 }
@@
   const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     const files = Array.from(e.target.files || []);
     props.onChange?.(files);
+    // allow selecting the same file again
+    e.target.value = "";
   };
 
   return (
-    <div>
+    <div className={cn(props.wrapperClassName)}>
       <Button
         className={props.className}
         onClick={() => fileInputRef.current?.click()}
         variant={props.variant}
       >
         {props.children}
       </Button>

Also applies to: 25-43


7-12: Dead prop: value is unused

If not needed by consumers, drop it to keep the API minimal. Otherwise, document its behavior.

Want me to scan usage to confirm it’s safe to remove?

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatPageLayout.tsx (1)

13-14: Unused prop: accountAddress

Not read in this component. Consider removing to keep the surface lean.

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/chat/history/ChatHistoryPage.tsx (1)

191-201: Key stability

Keying by id + updated_at + created_at causes unnecessary re-mounts. Prefer session.id.

-                  key={session.id + session.updated_at + session.created_at}
+                  key={session.id}
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/NebulaImage.tsx (2)

17-35: Expose className on the root element and wire via cn()

Improves composability and aligns with dashboard component conventions.

+import { cn } from "@/lib/utils";
@@
 export function NebulaImage(
   props:
     | {
         project: Project;
         type: "response";
         url: string;
         width: number;
         height: number;
         client: ThirdwebClient;
         requestId: string;
         sessionId: string | undefined;
+        className?: string;
       }
     | {
         project: Project;
         type: "submitted";
         url: string;
         client: ThirdwebClient;
+        className?: string;
       },
 ) {
@@
-  return (
-    <div className="group relative w-full max-w-[80%] lg:max-w-[50%]">
+  return (
+    <div
+      className={cn(
+        "group relative w-full max-w-[80%] lg:max-w-[50%]",
+        props.className,
+      )}
+    >

Also applies to: 51-53


111-126: Download fallback for strict CORS

If CORS blocks fetching the image, fall back to opening the src in a new tab.

   } catch (error) {
-    console.error("Download failed:", error);
+    console.error("Download failed:", error);
+    try {
+      window.open(src, "_blank", "noopener,noreferrer");
+    } catch {}
   }
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/chat/history/ChatHistoryPage.stories.tsx (2)

23-24: Align Storybook title with branding: “thirdweb/history”.

Matches the PR-wide rename.

-  title: "AI/history",
+  title: "thirdweb/history",

54-69: Type the factory and ensure monotonic timestamps + stable IDs.

Return type + make updated_at ≥ created_at and use uuid when available.

-function createRandomSessions(length: number) {
+import type { TruncatedSessionInfo } from "../../api/types";
+
+function createRandomSessions(length: number): TruncatedSessionInfo[] {
   const sessions = [];
   for (let i = 0; i < length; i++) {
+    const daysAgo = Math.floor(Math.random() * 10) + 1;
     sessions.push({
-      created_at: new Date().toISOString(),
-      id: Math.random().toString(),
+      created_at: subDays(new Date(), daysAgo).toISOString(),
+      id:
+        typeof crypto !== "undefined" && crypto.randomUUID
+          ? crypto.randomUUID()
+          : Math.random().toString(36).slice(2),
       title: randomLorem(Math.floor(5 + Math.random() * 15)),
-      updated_at: subDays(
-        new Date(),
-        Math.floor(Math.random() * 10),
-      ).toISOString(),
+      updated_at: subDays(new Date(), daysAgo - 1).toISOString(),
     });
   }
 
   return sessions;
 }
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/Chats.tsx (1)

143-145: Prefer stable keys for message list.

Index keys can cause incorrect re-use on edits; consider request_id || index for assistant/action/image, and a hash of content for user/presence.

Comment on lines +35 to +39
export function AssetsSectionUI(props: {
data: AssetBalance[];
isPending: boolean;
client: ThirdwebClient;
}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Expose className on exported components and merge with cn()

Guideline: apps//.tsx components should accept a className on the root element.

+import { cn } from "@/lib/utils";
@@
-export function AssetsSectionUI(props: {
+export function AssetsSectionUI(props: {
   data: AssetBalance[];
   isPending: boolean;
   client: ThirdwebClient;
+  className?: string;
 }) {
   if (props.data.length === 0 && !props.isPending) {
     return (
-      <div className="flex h-full flex-col items-center justify-center gap-3 px-2 py-1">
+      <div
+        className={cn(
+          "flex h-full flex-col items-center justify-center gap-3 px-2 py-1",
+          props.className,
+        )}
+      >
         <div className="rounded-full border p-1">
           <XIcon className="size-4" />
         </div>
-        <div className="text-muted-foreground text-sm">No Assets </div>
+        <div className="text-muted-foreground text-sm">No Assets</div>
       </div>
     );
   }
 
   return (
-    <div className="flex flex-col gap-1">
+    <div className={cn("flex flex-col gap-1", props.className)}>

Removed trailing space in “No Assets”.

Also applies to: 40-49, 51-68

Comment on lines +150 to +171
const assetsQuery = useQuery({
enabled: !!account && !!activeChain,
queryFn: async () => {
if (!account || !activeChain) {
return [];
}
const chains = [...new Set([1, 8453, 10, 137, activeChain.id])];
const url = new URL(
`https://insight.${isProd ? "thirdweb" : "thirdweb-dev"}.com/v1/tokens/erc20/${account?.address}`,
);
url.searchParams.set("limit", "50");
url.searchParams.set("metadata", "true");
url.searchParams.set("include_spam", "false");
url.searchParams.set("clientId", props.client.clientId);
for (const chain of chains) {
url.searchParams.append("chain", chain.toString());
}

const response = await fetch(url.toString());
const json = (await response.json()) as {
data: AssetBalance[];
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

React Query: add generics, abort signal, and handle non-2xx responses

Prevents stale requests from updating state and ensures failures bubble to React Query.

-  const assetsQuery = useQuery({
-    enabled: !!account && !!activeChain,
-    queryFn: async () => {
+  const assetsQuery = useQuery<AssetBalance[]>({
+    enabled: !!account && !!activeChain,
+    staleTime: 60_000,
+    queryFn: async ({ signal }) => {
       if (!account || !activeChain) {
         return [];
       }
@@
-      const response = await fetch(url.toString());
-      const json = (await response.json()) as {
-        data: AssetBalance[];
-      };
+      const response = await fetch(url.toString(), { signal });
+      if (!response.ok) {
+        throw new Error(`insight API ${response.status}: ${await response.text()}`);
+      }
+      const json = (await response.json()) as { data: AssetBalance[] };
📝 Committable suggestion

‼️ 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.

Suggested change
const assetsQuery = useQuery({
enabled: !!account && !!activeChain,
queryFn: async () => {
if (!account || !activeChain) {
return [];
}
const chains = [...new Set([1, 8453, 10, 137, activeChain.id])];
const url = new URL(
`https://insight.${isProd ? "thirdweb" : "thirdweb-dev"}.com/v1/tokens/erc20/${account?.address}`,
);
url.searchParams.set("limit", "50");
url.searchParams.set("metadata", "true");
url.searchParams.set("include_spam", "false");
url.searchParams.set("clientId", props.client.clientId);
for (const chain of chains) {
url.searchParams.append("chain", chain.toString());
}
const response = await fetch(url.toString());
const json = (await response.json()) as {
data: AssetBalance[];
};
const assetsQuery = useQuery<AssetBalance[]>({
enabled: !!account && !!activeChain,
staleTime: 60_000,
queryFn: async ({ signal }) => {
if (!account || !activeChain) {
return [];
}
const chains = [...new Set([1, 8453, 10, 137, activeChain.id])];
const url = new URL(
`https://insight.${isProd ? "thirdweb" : "thirdweb-dev"}.com/v1/tokens/erc20/${account?.address}`,
);
url.searchParams.set("limit", "50");
url.searchParams.set("metadata", "true");
url.searchParams.set("include_spam", "false");
url.searchParams.set("clientId", props.client.clientId);
for (const chain of chains) {
url.searchParams.append("chain", chain.toString());
}
const response = await fetch(url.toString(), { signal });
if (!response.ok) {
throw new Error(`insight API ${response.status}: ${await response.text()}`);
}
const json = (await response.json()) as { data: AssetBalance[] };
// …rest of your logic, e.g.:
// return json.data;
},
});
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/AssetsSection/AssetsSection.tsx
around lines 150-171, the queryFn should be typed with React Query generics,
accept the query function context to get the AbortSignal, pass that signal into
fetch to abort stale requests, and check the fetch response status — if non-2xx
throw an Error (or include response text) so failures bubble to React Query;
finally ensure the function returns the typed AssetBalance[] (or empty array)
and not undefined.

Comment on lines +36 to +49
export function ChatPageContent(props: {
project: Project;
session: SessionInfo | undefined;
accountAddress: string;
authToken: string;
client: ThirdwebClient;
type: "landing" | "new-chat";
initialParams:
| {
q: string | undefined;
chainIds: number[];
}
| undefined;
}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not expose authToken to the browser; move streaming behind an internal API route.

Passing JWT to a Client Component violates the repo’s “keep tokens secret via internal routes or server actions” rule and risks leakage. Proxy promptNebula via a Next.js route that reads the token server-side, then stream to the client.

Minimal local diffs (removing the token from props and calls):

 export function ChatPageContent(props: {
   project: Project;
   session: SessionInfo | undefined;
   accountAddress: string;
-  authToken: string;
   client: ThirdwebClient;
   type: "landing" | "new-chat";
@@
         await handleNebulaPrompt({
           abortController,
           project: props.project,
-          authToken: props.authToken,
           contextFilters: contextFilters,
           message: message,
           sessionId: currentSessionId,
           setContextFilters,
           setMessages,
         });
@@
               <Chats
                 project={props.project}
-                authToken={props.authToken}
                 className="min-w-0 pt-6 pb-32"
                 client={props.client}

Follow-up (outside this diff): introduce /api/ai/stream (server route) that calls promptNebula with Authorization; swap handleNebulaPrompt to call that route without passing tokens client-side. I can draft that patch if you want.

Also applies to: 211-221, 346-358

🤖 Prompt for AI Agents
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatPageContent.tsx
lines 36-49: the component is accepting and exposing authToken to a Client
Component; remove authToken from the component props and all client-side usages,
and instead implement an internal Next.js API route (e.g., /api/ai/stream) that
performs promptNebula calls server-side using the JWT from the server
environment or a server action; update handleNebulaPrompt and any other client
functions to call this internal route (fetch to /api/ai/stream) and consume the
streamed response without ever sending the JWT to the browser; also apply the
same removal and routing changes to the other occurrences referenced (lines
~211-221 and ~346-358) so no client code carries the token.

Comment on lines +8 to +17
export function ChatPageLayout(props: {
team_slug: string;
authToken: string;
project: Project;
client: ThirdwebClient;
accountAddress: string;
sessions: TruncatedSessionInfo[];
children: React.ReactNode;
className?: string;
}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Critical: JWT is passed to a client component via props

authToken flows into MobileNav (client) which exposes it to the browser. Remove the prop entirely; MobileNav does not use it.

 export function ChatPageLayout(props: {
   team_slug: string;
-  authToken: string;
   project: Project;
   client: ThirdwebClient;
-  accountAddress: string;
   sessions: TruncatedSessionInfo[];
   children: React.ReactNode;
   className?: string;
 }) {
@@
       <MobileNav
         project={props.project}
-        authToken={props.authToken}
         sessions={props.sessions}
         client={props.client}
         team_slug={props.team_slug}
       />

Also remove authToken from NebulaMobileNav.tsx props signature.

I can provide a follow-up patch for NebulaMobileNav.tsx if helpful.

Also applies to: 25-31


🏁 Script executed:

#!/bin/bash
# Locate and display the ChatPageLayout component
fd ChatPageLayout.tsx --exec sed -n '1,200p' {}

# Locate and display the NebulaMobileNav (MobileNav) component
fd NebulaMobileNav.tsx --exec sed -n '1,200p' {}

Length of output: 6502


Remove unused authToken and accountAddress props from ChatPageLayout

In apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatPageLayout.tsx:

 export function ChatPageLayout(props: {
   team_slug: string;
-  authToken: string;
   project: Project;
   client: ThirdwebClient;
-  accountAddress: string;
   sessions: TruncatedSessionInfo[];
   children: React.ReactNode;
   className?: string;
 }) {
@@
       <MobileNav
         project={props.project}
-        authToken={props.authToken}
         sessions={props.sessions}
         client={props.client}
         team_slug={props.team_slug}
       />

Also remove the unused authToken parameter from the MobileNav signature in apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/NebulaMobileNav.tsx. I can provide a follow-up patch if helpful.

📝 Committable suggestion

‼️ 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.

Suggested change
export function ChatPageLayout(props: {
team_slug: string;
authToken: string;
project: Project;
client: ThirdwebClient;
accountAddress: string;
sessions: TruncatedSessionInfo[];
children: React.ReactNode;
className?: string;
}) {
export function ChatPageLayout(props: {
team_slug: string;
project: Project;
client: ThirdwebClient;
sessions: TruncatedSessionInfo[];
children: React.ReactNode;
className?: string;
}) {
return (
<div className={cn("flex h-full flex-col", props.className)}>
<MobileNav
project={props.project}
sessions={props.sessions}
client={props.client}
team_slug={props.team_slug}
/>
{/* ...rest of layout... */}
{props.children}
</div>
);
}
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatPageLayout.tsx
around lines 8-17, remove the unused props authToken and accountAddress from the
ChatPageLayout props interface and any references to them in the component
signature/usage; then open
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/NebulaMobileNav.tsx
and remove the unused authToken parameter from the MobileNav function signature
and any callers passing it (update call sites to stop passing authToken), run
TypeScript to ensure no remaining references, and adjust imports/exports if
necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Dashboard Involves changes to the Dashboard. packages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants