Skip to content

thirdweb AI inside dashboard - #8003

Merged
joaquim-verges merged 1 commit into
mainfrom
09-08-thirdweb_ai_inside_dashboard
Sep 9, 2025
Merged

joaquim-verges merged 1 commit into
mainfrom
09-08-thirdweb_ai_inside_dashboard

Conversation

@joaquim-verges

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

Copy link
Copy Markdown
Contributor

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

changeset-bot Bot commented Sep 8, 2025 •

Copy link
Copy Markdown

⚠️ 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

vercel Bot commented Sep 8, 2025 •

Copy link
Copy Markdown

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

coderabbitai Bot commented Sep 8, 2025 •

Copy link
Copy Markdown
Contributor

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 fetch-event-stream@0.1.5.

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
Copy Markdown
Contributor 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

codecov Bot commented Sep 8, 2025 •

Copy link
Copy Markdown

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

github-actions Bot commented Sep 8, 2025 •

Copy link
Copy Markdown
Contributor

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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

Comment thread apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx Outdated
Comment thread apps/dashboard/src/middleware.ts Outdated
@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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (15)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatBar.tsx (6)

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

Keep branding consistent with PR notes.

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

176-199: Drag-over highlight can get stuck when uploads aren’t allowed; also fix contains typing.

Only set highlight when uploads are allowed; always clear on leave; cast contains arg as Node | null.

 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);
 }}

92-101: Strip data URL prefix before sending b64 to API.

Send raw base64, keep data URL only for preview.

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

If the backend actually expects a full data URL, ignore this change. Please confirm the schema in ../api/types and chat API.


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

Aligns with the product rename.

-  <p className="mb-3 text-muted-foreground text-sm">
-    Get access to image uploads by signing in to Nebula
-  </p>
+  <p className="mb-3 text-muted-foreground text-sm">
+    Get access to image uploads by signing in to thirdweb
+  </p>

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

Clone before sort to avoid side effects.

-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;
 });

227-236: Don’t send empty messages on Enter.

Mirror the Send button guard to avoid empty submissions.

 onKeyDown={(e) => {
   // ignore if shift key is pressed to allow entering new lines
   if (e.shiftKey) {
     return;
   }
   if (e.key === "Enter" && !props.isChatStreaming) {
-    e.preventDefault();
-    handleSubmit(message);
+    if (message.trim() === "" && images.length === 0) return;
+    e.preventDefault();
+    handleSubmit(message);
   }
 }}
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/TransactionsSection/TransactionsSection.tsx (7)

26-60: Expose className on UI component and merge with cn().

Matches apps/{dashboard}/**/*.tsx guideline and enables layout control.

-export function TransactionSectionUI(props: {
+export function TransactionSectionUI(props: {
   data: WalletTransaction[];
   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 Recent Activity</div>
       </div>
     );
   }
 
   return (
-    <div className="flex flex-col gap-1">
+    <div className={cn("flex flex-col gap-1", props.className)}>
       {!props.isPending &&
         props.data.map((asset) => (
           <TransactionInfo
             client={props.client}
             key={asset.hash}
             transaction={asset}
           />
         ))}
 
       {props.isPending &&
         new Array(10).fill(null).map((_, index) => (
           // biome-ignore lint/suspicious/noArrayIndexKey: TODO
           <SkeletonAssetItem key={index} />
         ))}
     </div>
   );
 }

95-101: Keep Link without rel; repo doesn’t require it.

Prior bot comment suggested rel for target="_blank", but maintainers confirmed it’s not required for external Links in this project. Current code is OK.


134-159: Simplify description with typed inputs + explicit return type.

Use the narrowed DecodedInputs type.

-function getTransactionDescription(transaction: WalletTransaction) {
+function getTransactionDescription(transaction: WalletTransaction): string {
   if (!transaction.decoded) {
     return `To: ${shortenAddress(transaction.to_address)}`;
   }
 
-  if (
-    typeof transaction.decoded.inputs === "object" &&
-    transaction.decoded.inputs !== null
-  ) {
-    if (
-      "to" in transaction.decoded.inputs &&
-      typeof transaction.decoded.inputs.to === "string"
-    ) {
-      return `To: ${shortenAddress(transaction.decoded.inputs.to)}`;
-    }
-
-    if (
-      "spender" in transaction.decoded.inputs &&
-      typeof transaction.decoded.inputs.spender === "string"
-    ) {
-      return `Spender: ${shortenAddress(transaction.decoded.inputs.spender)}`;
-    }
-  }
+  const inputs = transaction.decoded.inputs;
+  if (inputs?.to) {
+    return `To: ${shortenAddress(inputs.to)}`;
+  }
+  if (inputs?.spender) {
+    return `Spender: ${shortenAddress(inputs.spender)}`;
+  }
 
   return `To: ${shortenAddress(transaction.to_address)}`;
 }

203-210: Pass className through to UI.

Keeps styling extensible for embedding contexts.

   return (
     <TransactionSectionUI
       client={props.client}
       data={txQuery.data ?? []}
       isPending={txQuery.isPending}
+      className={props.className}
     />
   );

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

This file uses React Query and thirdweb hooks; it must be a Client Component.

+'use client';

12-24: Tighten decoded.inputs typing; avoid object.

Model the known shape so property access is type-safe.

-// Note: this is not the full object type returned from insight API, it only includes fields we care about
+// Note: this is not the full object type returned from insight API, it only includes fields we care about
+type DecodedInputs = {
+  to?: string;
+  spender?: string;
+} & Record<string, unknown>;
 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?: DecodedInputs | null;
   };
 };

161-201: Type the query, hook into AbortSignal, add cache settings, and guard bad responses.

Prevents leaks and aligns with dashboard React Query guidance.

-export function TransactionsSection(props: { client: ThirdwebClient }) {
+export function TransactionsSection(props: { client: ThirdwebClient; className?: string }) {
   const account = useActiveAccount();
   const activeChain = useActiveWalletChain();
 
-  const txQuery = useQuery({
+  const txQuery = useQuery<WalletTransaction[]>({
     enabled: !!account && !!activeChain,
-    queryFn: async () => {
+    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/wallets/${account.address}/transactions`,
       );
       url.searchParams.set("limit", "20");
       url.searchParams.set("decode", "true");
       url.searchParams.set("clientId", props.client.clientId);
 
       const threeMonthsAgoUnixTime = Math.floor(
         (Date.now() - 3 * 30 * 24 * 60 * 60 * 1000) / 1000,
       );
 
       url.searchParams.set(
         "filter_block_timestamp_gte",
         `${threeMonthsAgoUnixTime}`,
       );
 
       for (const chain of chains) {
         url.searchParams.append("chain", chain.toString());
       }
 
-      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,
   });
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatSidebarLink.tsx (1)

17-18: Do not call tokened AI session APIs from a client component; proxy via an internal API route (duplicate).

Importing and calling deleteSession here sends an authenticated request directly from the browser. Per dashboard rules, attach JWT on the server via an internal Next.js route and call that route from this client component.

Apply locally (temporary shim until the server route exists):

-import { deleteSession } from "../api/session";
+// TODO: replace with typed internal route once added
+async function deleteSessionViaApi(sessionId: string): Promise<void> {
+  const res = await fetch(
+    `/api/thirdweb-ai/session/${encodeURIComponent(sessionId)}`,
+    { method: "DELETE" },
+  );
+  if (!res.ok) {
+    throw new Error("Failed to delete session");
+  }
+}
-  const deleteChat = useMutation({
-    mutationFn: () => {
-      return deleteSession({
-        project: props.project,
-        sessionId: props.sessionId,
-      });
-    },
+  const deleteChat = useMutation({
+    mutationFn: () => deleteSessionViaApi(props.sessionId),
     onSuccess: () => {
       const prev = deletedSessionsStore.getValue();
       deletedSessionsStore.setValue([...prev, props.sessionId]);
       if (isDeletingCurrentPage) {
         router.replace(newChatLink);
       }
     },
   });

Add the server route (outside this file):

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

export async function DELETE(
  _req: Request,
  { params }: { params: { sessionId: string } },
) {
  const token = await getAuthToken();
  if (!token) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const res = await fetch(`${NEXT_PUBLIC_THIRDWEB_AI_HOST}/session/${params.sessionId}`, {
    method: "DELETE",
    headers: { Authorization: `Bearer ${token}` },
  });

  const body = await res.json().catch(() => ({}));
  return NextResponse.json(body, { status: res.status });
}

Also applies to: 32-46

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

67-67: Fix prop type: sendTx must accept (tx, onTxSettled).

Current zero-arg handler violates ExecuteTransactionCardLayout’s callback type and will fail type-checking.

Apply:

-        sendTx={async () => {}}
+        sendTx={async (_tx, onTxSettled) => {
+          // simulate a happy-path flow in Storybook
+          setStatus({ type: "sending" });
+          await new Promise((r) => setTimeout(r, 500));
+          setStatus({ type: "confirming", txHash: exampleTxHash });
+          await new Promise((r) => setTimeout(r, 500));
+          setStatus({ type: "confirmed", txHash: exampleTxHash });
+          onTxSettled(exampleTxHash);
+        }}
🧹 Nitpick comments (37)
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatBar.tsx (6)

447-448: Use a stable unique key for previews.

File names can collide; include size/lastModified.

-  key={image.file.name}
+  key={`${image.file.name}-${image.file.size}-${image.file.lastModified}`}

15-15: Sync prefillMessage when the prop changes.

Keeps textarea in sync with parent updates.

-import { useState } from "react";
+import { useEffect, useState } from "react";

79-85: Add effect to update message when prefillMessage changes.

 const [message, setMessage] = useState(props.prefillMessage || "");
+useEffect(() => {
+  setMessage(props.prefillMessage || "");
+}, [props.prefillMessage]);

41-45: Reuse shared CopyButton instead of local duplicate.

Reduce duplication by importing the existing UI component.

 import { Skeleton } from "@/components/ui/skeleton";
 import { AutoResizeTextarea } from "@/components/ui/textarea";
 import { ToolTipLabel } from "@/components/ui/tooltip";
+import { CopyButton } from "@/components/ui/CopyButton";

653-655: Switch to shared CopyButton usage.

Match the shared component’s props.

-<CopyButton address={wallet.address} />
+<CopyButton
+  text={wallet.address}
+  tooltip={false}
+  className="p-1.5"
+  iconClassName="size-3"
/>

698-720: Remove local CopyButton duplicate.

Use the shared component imported above.

-function CopyButton(props: { address: string }) {
-  const [copied, setCopied] = useState(false);
-  return (
-    <Button
-      className="h-auto w-auto p-1.5 text-muted-foreground"
-      onClick={(e) => {
-        e.stopPropagation();
-        navigator.clipboard.writeText(props.address);
-        setCopied(true);
-        setTimeout(() => {
-          setCopied(false);
-        }, 1000);
-      }}
-      variant="ghost"
-    >
-      {copied ? (
-        <CheckIcon className="size-3 text-green-500" />
-      ) : (
-        <CopyIcon className="size-3" />
-      )}
-    </Button>
-  );
-}
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/TransactionsSection/TransactionsSection.tsx (2)

1-11: Import cn for className merging (design guideline).

You expose no className yet; import cn to enable it (see next comment).

 import { Skeleton } from "@/components/ui/skeleton";
 import { isProd } from "@/constants/env-utils";
 import { useAllChainsData } from "@/hooks/chains/allChains";
 import { ChainIconClient } from "@/icons/ChainIcon";
+import { cn } from "@/lib/utils";

111-133: Harden title logic; explicit utility return type and robust value check.

Avoid string-only zero check; handle hex/decimal safely.

-function getTransactionTitle(transaction: WalletTransaction) {
+function getTransactionTitle(transaction: WalletTransaction): string {
   if (!transaction.decoded) {
     return "Transaction Sent";
   }
 
   if (transaction.decoded.name) {
     return transaction.decoded.name;
   }
 
   if (transaction.decoded.signature) {
     const nameFromSignature = transaction.decoded.signature.split("(")[0];
     if (nameFromSignature) {
       return nameFromSignature;
     }
   }
 
-  if (transaction.value !== "0") {
-    return "Transfer";
-  }
+  try {
+    if (BigInt(transaction.value) !== 0n) return "Transfer";
+  } catch {
+    if (transaction.value !== "0" && transaction.value !== "0x0") return "Transfer";
+  }
 
   return "Transaction Sent";
 }
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatSidebarLink.tsx (5)

30-31: Tighten current-page detection to avoid false positives.

Using substring matching can misfire. Match the expected suffix:

-  const isDeletingCurrentPage = pathname.includes(props.sessionId);
+  const isDeletingCurrentPage = pathname.endsWith(`/ai/chat/${props.sessionId}`);

40-41: Avoid duplicate entries in deletedSessionsStore.

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

66-71: Replace raw color utilities with design tokens; add accessible label.

-          <Button
-            className="-translate-y-1/2 absolute top-1/2 right-1 z-10 h-auto w-auto bg-accent p-1 opacity-0 hover:bg-zinc-300 group-hover:opacity-100 dark:hover:bg-zinc-700"
+          <Button
+            className="-translate-y-1/2 absolute top-1/2 right-1 z-10 h-auto w-auto bg-accent p-1 opacity-0 hover:bg-muted group-hover:opacity-100"
+            aria-label="Open chat actions"
             variant="ghost"
           >

74-84: Disable the Delete button while the mutation is pending to prevent double-submits.

-          <Button
+          <Button
             className="!text-destructive-text flex w-full justify-start gap-2.5 px-2"
             onClick={() => {
               const promise = deleteChat.mutateAsync();
               toast.promise(promise, {
                 error: "Failed to delete chat",
                 success: "Chat deleted successfully",
               });
             }}
+            disabled={deleteChat.isPending}
             variant="ghost"
           >

20-20: Follow-up: add a confirm dialog before destructive delete.

I can wire an <AlertDialog> with “Delete/Cancel” and require explicit confirmation. Want a patch?

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

32-32: Add explicit return type to Story.

Conform to the repo TS guideline for explicit return types.

-function Story() {
+function Story(): JSX.Element {

58-58: Add explicit return type to Variant.

Match the explicit return type guidance.

-function Variant(props: { label: string; status: TxStatus }) {
+function Variant(props: { label: string; status: TxStatus }): JSX.Element {

12-21: Point Storybook at the component under test for better docs/controls.

Set meta.component to ExecuteTransactionCardLayout; use render/decorators for the wrapper.

-const meta = {
-  component: Story,
+const meta = {
+  component: ExecuteTransactionCardLayout,
   parameters: {
     nextjs: {
       appDirectory: true,
     },
   },
   title: "AI/actions/ExecuteTransactionCard",
-} satisfies Meta<typeof Story>;
+} satisfies Meta<typeof ExecuteTransactionCardLayout>;

23-27: Avoid value/type name collision and wire render to wrapper.

Rename the StoryObj alias to prevent confusion with the Story component, and render the wrapper if you adopt the meta change above.

-export default meta;
-type Story = StoryObj<typeof meta>;
+export default meta;
+type StoryDef = StoryObj<typeof meta>;
 
-export const Variants: Story = {
-  args: {},
-};
+export const Variants: StoryDef = {
+  args: {},
+  render: () => <Story />,
+};

72-74: Fix misleading inline comment.

“thirdweb.eth” describes the recipient, not the data field.

-          data: "0x", // thirdweb.eth
-          to: "0xEb0effdFB4dC5b3d5d3aC6ce29F3ED213E95d675",
+          data: "0x",
+          to: "0xEb0effdFB4dC5b3d5d3aC6ce29F3ED213E95d675", // thirdweb.eth
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/Chats.stories.tsx (4)

11-19: Pass client to ThirdwebProvider for consistent context

Some thirdweb React hooks/components expect the client on the provider. You’re already passing it to ConnectButton; pass it to the provider too to avoid context gaps.

Apply:

-      <ThirdwebProvider>
+      <ThirdwebProvider client={storybookThirdwebClient}>
         <div className="container flex max-w-[800px] flex-col gap-14 py-10">
           <div>
             <ConnectButton client={storybookThirdwebClient} />
           </div>
           <Story />
         </div>
-      </ThirdwebProvider>
+      </ThirdwebProvider>

105-123: Fix minor typos in markdown fixture

Keeps Storybook docs polished.

Apply:

-This a paragraph
+This is a paragraph
@@
-This a very long paragraph lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec pur us. Donec euismod, nunc nec vehicula.
-ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec pur us. Donec euismod, nunc nec vehicula.
+This is a very long paragraph lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec purus. Donec euismod, nunc nec vehicula.
+ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec purus. Donec euismod, nunc nec vehicula.
@@
-## Empasis
+## Emphasis
@@
-This a a paragraph with some `inlineCode()`
+This is a paragraph with some `inlineCode()`
@@
-This a `const longerCodeSnippet = "Example. This should be able to handle line breaks as well, it should not be overflowing the page";`
+This is `const longerCodeSnippet = "Example. This should be able to handle line breaks as well, it should not be overflowing the page";`

Also applies to: 155-158


198-203: Stabilize random content for visual regression tools

randomLorem() changes on every reload, causing flaky Chromatic/screenshot diffs. Prefer static strings or a seeded generator for stories.

If you use Chromatic, confirm whether these stories show diffs between runs without code changes.


225-239: Annotate return type for Variant

Follow repo TS guidance: explicit return types.

Apply:

-function Variant(props: { messages: ChatMessage[] }) {
+function Variant(props: { messages: ChatMessage[] }): JSX.Element {
   return (
     <Chats
       project={projectStub("xxxxx", "team-1")}
       authToken="xxxxx"
       client={storybookThirdwebClient}
       enableAutoScroll={false}
       isChatStreaming={false}
       messages={props.messages}
       sendMessage={() => {}}
       sessionId="xxxxx"
       setEnableAutoScroll={() => {}}
     />
   );
 }
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/Swap/SwapCards.stories.tsx (4)

1-1: Use @storybook/react for types (framework package doesn’t guarantee type exports).

Switch Meta/StoryObj import to @storybook/react to avoid type resolution issues across SB versions.

-import type { Meta, StoryObj } from "@storybook/nextjs";
+import type { Meta, StoryObj } from "@storybook/react";

12-21: Avoid “Story” name overload (value and type); add explicit return types.

Rename the component to SwapCardsStory and the StoryObj alias to StoryType; also make return types explicit per guidelines.

-const meta = {
-  component: Story,
+const meta = {
+  component: SwapCardsStory,
   parameters: {
     nextjs: {
       appDirectory: true,
     },
   },
   title: "AI/actions/Swap Cards",
-} satisfies Meta<typeof Story>;
+} satisfies Meta<typeof SwapCardsStory>;
 
 export default meta;
-type Story = StoryObj<typeof meta>;
+type StoryType = StoryObj<typeof meta>;
@@
-export const Swap: Story = {
+export const Swap: StoryType = {
@@
-export const Approve: Story = {
+export const Approve: StoryType = {
@@
-function Story(props: { swapData: NebulaSwapData }) {
+function SwapCardsStory(props: { swapData: NebulaSwapData }): JSX.Element {
@@
-function Variant(props: {
+function Variant(props: {
   label: string;
   status: TxStatus;
   swapData: NebulaSwapData;
-}) {
+}): JSX.Element {

Also applies to: 22-24, 77-112


121-139: Match sendTx signature and keep types explicit.

Accept the tx param and type it; keeps parity with component prop types.

-import {
+import {
   ApproveTransactionCardLayout,
   SwapTransactionCardLayout,
 } from "./SwapCards";
+import type { PreparedTransaction } from "thirdweb";
@@
         <ApproveTransactionCardLayout
           client={storybookThirdwebClient}
-          sendTx={async () => {}}
+          sendTx={async (_tx: PreparedTransaction) => {}}
           setStatus={setStatus}
           status={status}
           swapData={props.swapData}
         />
@@
         <SwapTransactionCardLayout
           client={storybookThirdwebClient}
-          sendTx={async () => {}}
+          sendTx={async (_tx: PreparedTransaction) => {}}
           setStatus={setStatus}
           status={status}
           swapData={props.swapData}
         />

Also applies to: 3-3


33-41: Gitleaks false positive (hex strings are token/tx data, not secrets).

Addresses and calldata here are public chain artifacts. Suggest allow-listing EVM addresses/calldata in gitleaks config to reduce noise.

If desired, I can draft a minimal gitleaks allowlist entry scoped to this path to avoid overbroad ignores.

Also applies to: 49-53

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

17-23: Use fullscreen layout to remove Storybook canvas padding

Helps the page-layout story render true-to-app.

   parameters: {
     nextjs: {
       appDirectory: true,
     },
+    layout: "fullscreen",
   },

46-53: Make fixtures deterministic and cover null titles

Stable timestamps improve visual regression; occasional null title exercises UI fallback.

-function generateRandomSessions(count: number): TruncatedSessionInfo[] {
-  return Array.from({ length: count }, (_, i) => ({
-    created_at: new Date().toISOString(),
-    id: i.toString(),
-    title: randomLorem(Math.floor(Math.random() * 10) + 1),
-    updated_at: new Date().toISOString(),
-  }));
-}
+function generateRandomSessions(count: number): TruncatedSessionInfo[] {
+  const baseMs = Date.parse("2025-01-01T00:00:00.000Z");
+  return Array.from({ length: count }, (_, i) => {
+    const ts = new Date(baseMs + i * 60_000).toISOString(); // +i min, deterministic
+    const title = i % 5 === 0 ? null : randomLorem(Math.floor(Math.random() * 10) + 1);
+    return {
+      created_at: ts,
+      id: i.toString(),
+      title,
+      updated_at: ts,
+    };
+  });
+}

28-44: Add a mixed/edge-case story

Quickly validates null titles and timestamp ordering.

 export const ThirtyChats: Story = {
   args: {
     sessions: generateRandomSessions(30),
   },
 };
+
+export const MixedTitles: Story = {
+  args: {
+    sessions: generateRandomSessions(5), // includes one null title with the generator above
+  },
+};

55-56: Annotate explicit return type

Aligns with repo TS guidelines for explicit returns.

-function Variant(props: { sessions: TruncatedSessionInfo[] }) {
+function Variant(props: { sessions: TruncatedSessionInfo[] }): JSX.Element {
apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/analytics/page.tsx (3)

15-23: Add explicit return type and widen searchParams types

Align with our TS guidelines and the helpers that accept string | string[].

-export default async function Page(props: {
-  params: Promise<{ team_slug: string; project_slug: string }>;
-  searchParams: Promise<{
-    from?: string;
-    to?: string;
-    type?: string;
-    interval?: string;
-  }>;
-}) {
+export default async function Page(props: {
+  params: Promise<{ team_slug: string; project_slug: string }>;
+  searchParams: Promise<{
+    from?: string | string[];
+    to?: string | string[];
+    type?: string | string[];
+    interval?: string | string[];
+  }>;
+}): Promise<JSX.Element> {

8-8: Importing from “.client.ts” in a Server Component—double-check module mode

This page is a Server Component. Importing from thirdweb-client.client.ts is risky if that module is client-marked or pulls browser-only deps.

  • If the constants file starts with 'use client' or imports browser-only libs, move getClientThirdwebClient to a server-safe module (e.g., thirdweb-client.server.ts) and import that here.

Also applies to: 44-48


9-9: Branding consistency: Nebula vs thirdweb

Confirm NebulaIcon is still the intended brand asset for AI pages. If branding migrated, switch to the corresponding thirdweb icon.

Also applies to: 61-61

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

5-16: Exclude non-serializable “client” from Controls/Docs to avoid args table warnings.
Storybook will try to serialize the client; disable its control.

Apply this diff inside the meta object:

   title: "AI/AssetsSection",
+  argTypes: {
+    client: { control: false },
+  },

8-12: Type the decorator for explicit return type.
Keeps us aligned with “explicit return types” guidance.

-import { type AssetBalance, AssetsSectionUI } from "./AssetsSection";
+import { type AssetBalance, AssetsSectionUI } from "./AssetsSection";
+import type { StoryFn } from "@storybook/react";
@@
-    (Story) => (
+    (Story: StoryFn): JSX.Element => (

18-19: Rename local Story alias to avoid confusion with decorator param name.
Small readability win.

-type Story = StoryObj<typeof meta>;
+type StoryDef = StoryObj<typeof meta>;
@@
-export const MultipleAssets: Story = {
+export const MultipleAssets: StoryDef = {
@@
-export const SingleAsset: Story = {
+export const SingleAsset: StoryDef = {
@@
-export const EmptyAssets: Story = {
+export const EmptyAssets: StoryDef = {
@@
-export const Loading: Story = {
+export const Loading: StoryDef = {

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


20-69: Broaden stub coverage (decimals/chains) and validate shape at compile-time.
Add a 6‑dec token and a second chain; also use satisfies for stronger type checking.

-const tokensStub: AssetBalance[] = [
+const tokensStub = [
@@
   {
     balance: "6237535850425",
     chain_id: 8453,
     decimals: 18,
     name: "Wrapped Ether",
     symbol: "WETH",
     token_address: "0x4200000000000000000000000000000000000006",
   },
+  {
+    balance: "123456", // 0.123456 in human units
+    chain_id: 1,
+    decimals: 6,
+    name: "USD Coin (stub)",
+    symbol: "USDC",
+    token_address: "0x0000000000000000000000000000000000000001",
+  },
-];
+] satisfies AssetBalance[];

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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_at ≥ created_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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

This branch was successfully deployed

4 active and 1 inactive deployments
Preview – thirdweb-www — 1f6c03e3 Deployed Sep 9, 2025 by vercel[bot]
Preview – thirdweb_playground — 1f6c03e3 Deployed Sep 9, 2025 by vercel[bot]
Preview – docs-v2 — 1f6c03e3 Deployed Sep 9, 2025 by vercel[bot]
Preview – nebula — 1f6c03e3 Deployed Sep 9, 2025 by vercel[bot]
Preview – wallet-ui — 1f6c03e3 Deployed Sep 9, 2025 by vercel[bot]
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.

1 participant