Updated{" "}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/chat/page.tsx b/apps/dashboard/src/app/nebula-app/(app)/chat/page.tsx
index 31300c2f23f..c7c6c1db170 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/chat/page.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/chat/page.tsx
@@ -27,6 +27,7 @@ export default async function Page() {
session={undefined}
type="new-chat"
account={account}
+ initialPrompt={undefined}
/>
);
}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx
index 33245b51901..aecfd0ce08c 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx
@@ -5,11 +5,8 @@ import { AutoResizeTextarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { ArrowUpIcon, CircleStopIcon } from "lucide-react";
import { useState } from "react";
-import type { ExecuteConfig } from "../api/types";
export function Chatbar(props: {
- updateConfig: (config: ExecuteConfig) => void;
- config: ExecuteConfig;
sendMessage: (message: string) => void;
isChatStreaming: boolean;
abortChatStream: () => void;
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx
index 92dc3e64aef..2a4e8d7f0b4 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx
@@ -1,11 +1,8 @@
"use client";
-
-/* eslint-disable no-restricted-syntax */
-import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
-import { useMutation } from "@tanstack/react-query";
-import { useEffect, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
import { type ContextFilters, promptNebula } from "../api/chat";
import { createSession, updateSession } from "../api/session";
import type { ExecuteConfig, SessionInfo } from "../api/types";
@@ -21,7 +18,10 @@ export function ChatPageContent(props: {
accountAddress: string;
type: "landing" | "new-chat";
account: Account;
+ initialPrompt: string | undefined;
}) {
+ const address = useActiveAccount()?.address;
+ const activeChain = useActiveWalletChain();
const client = useThirdwebClient();
const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false);
const [messages, setMessages] = useState>(() => {
@@ -35,23 +35,60 @@ export function ChatPageContent(props: {
return [];
});
- const [_config, setConfig] = useState();
- const [contextFilters, setContextFilters] = useState<
+ const [hasUserUpdatedContextFilters, setHasUserUpdatedContextFilters] =
+ useState(false);
+
+ const [contextFilters, _setContextFilters] = useState<
ContextFilters | undefined
>(() => {
const contextFilterRes = props.session?.context_filter;
- if (contextFilterRes) {
- return {
- chainIds: contextFilterRes.chain_ids,
- contractAddresses: contextFilterRes.contract_addresses,
- };
- }
+ const value: ContextFilters = {
+ chainIds: contextFilterRes?.chain_ids || undefined,
+ contractAddresses: contextFilterRes?.contract_addresses || undefined,
+ walletAddresses: contextFilterRes?.wallet_addresses || undefined,
+ };
+
+ return value;
});
- const config = _config || {
- mode: "client",
- signer_wallet_address: props.accountAddress,
- };
+ const setContextFilters = useCallback((v: ContextFilters | undefined) => {
+ _setContextFilters(v);
+ setHasUserUpdatedContextFilters(true);
+ }, []);
+
+ const isNewSession = !props.session;
+
+ // if this is a new session, user has not manually updated context filters
+ // update the context filters to the current user's wallet address and chain id
+ // eslint-disable-next-line no-restricted-syntax
+ useEffect(() => {
+ if (
+ !isNewSession ||
+ hasUserUpdatedContextFilters ||
+ !address ||
+ !activeChain
+ ) {
+ return;
+ }
+
+ _setContextFilters((_contextFilters) => {
+ const updatedContextFilters: ContextFilters = _contextFilters
+ ? { ..._contextFilters }
+ : {};
+
+ updatedContextFilters.walletAddresses = [address];
+ updatedContextFilters.chainIds = [activeChain.id.toString()];
+
+ return updatedContextFilters;
+ });
+ }, [address, isNewSession, hasUserUpdatedContextFilters, activeChain]);
+
+ const config: ExecuteConfig = useMemo(() => {
+ return {
+ mode: "client",
+ signer_wallet_address: props.accountAddress,
+ };
+ }, [props.accountAddress]);
const [sessionId, _setSessionId] = useState(
props.session?.id,
@@ -61,34 +98,27 @@ export function ChatPageContent(props: {
AbortController | undefined
>();
- function setSessionId(sessionId: string) {
- _setSessionId(sessionId);
- // update page URL without reloading
- window.history.replaceState({}, "", `/chat/${sessionId}`);
-
- // if the current page is landing page, link to /chat
- // if current page is new /chat page, link to landing page
- if (props.type === "landing") {
- newChatPageUrlStore.setValue("/chat");
- } else {
- newChatPageUrlStore.setValue("/");
- }
- }
-
- const messagesEndRef = useRef(null);
- const [isChatStreaming, setIsChatStreaming] = useState(false);
- const [isUserSubmittedMessage, setIsUserSubmittedMessage] = useState(false);
+ const setSessionId = useCallback(
+ (sessionId: string) => {
+ _setSessionId(sessionId);
+ // update page URL without reloading
+ window.history.replaceState({}, "", `/chat/${sessionId}`);
- // biome-ignore lint/correctness/useExhaustiveDependencies:
- useEffect(() => {
- if (!isUserSubmittedMessage) {
- return;
- }
+ // if the current page is landing page, link to /chat
+ // if current page is new /chat page, link to landing page
+ if (props.type === "landing") {
+ newChatPageUrlStore.setValue("/chat");
+ } else {
+ newChatPageUrlStore.setValue("/");
+ }
+ },
+ [props.type],
+ );
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
- }, [messages, isUserSubmittedMessage]);
+ const [isChatStreaming, setIsChatStreaming] = useState(false);
+ const [enableAutoScroll, setEnableAutoScroll] = useState(false);
- async function initSession() {
+ const initSession = useCallback(async () => {
const session = await createSession({
authToken: props.authToken,
config,
@@ -96,204 +126,198 @@ export function ChatPageContent(props: {
});
setSessionId(session.id);
return session;
- }
-
- async function handleSendMessage(message: string) {
- setUserHasSubmittedMessage(true);
- setMessages((prev) => [
- ...prev,
- { text: message, type: "user" },
- // instant loading indicator feedback to user
- {
- type: "presence",
- text: "Thinking...",
- },
- ]);
-
- setIsChatStreaming(true);
- setIsUserSubmittedMessage(true);
- const abortController = new AbortController();
-
- try {
- // Ensure we have a session ID
- let currentSessionId = sessionId;
- if (!currentSessionId) {
- const session = await initSession();
- currentSessionId = session.id;
- }
-
- let requestIdForMessage = "";
-
- // add this session on sidebar
- if (messages.length === 0) {
- const prevValue = newSessionsStore.getValue();
- newSessionsStore.setValue([
- {
- id: currentSessionId,
- title: message,
- created_at: new Date().toISOString(),
- updated_at: new Date().toISOString(),
- },
- ...prevValue,
- ]);
- }
+ }, [config, contextFilters, props.authToken, setSessionId]);
- setChatAbortController(abortController);
-
- await promptNebula({
- abortController,
- message: message,
- sessionId: currentSessionId,
- config: config,
- authToken: props.authToken,
- handleStream(res) {
- if (abortController.signal.aborted) {
- return;
- }
-
- if (res.event === "init") {
- requestIdForMessage = res.data.request_id;
- }
-
- if (res.event === "delta") {
- setMessages((prev) => {
- const lastMessage = prev[prev.length - 1];
- // if last message is presence, overwrite it
- if (lastMessage?.type === "presence") {
- return [
- ...prev.slice(0, -1),
- {
- text: res.data.v,
- type: "assistant",
- request_id: requestIdForMessage,
- },
- ];
- }
+ const handleSendMessage = useCallback(
+ async (message: string) => {
+ setUserHasSubmittedMessage(true);
+ setMessages((prev) => [
+ ...prev,
+ { text: message, type: "user" },
+ // instant loading indicator feedback to user
+ {
+ type: "presence",
+ text: "Thinking...",
+ },
+ ]);
- // if last message is from chat, append to it
- if (lastMessage?.type === "assistant") {
- return [
- ...prev.slice(0, -1),
- {
- text: lastMessage.text + res.data.v,
- type: "assistant",
- request_id: requestIdForMessage,
- },
- ];
- }
+ setIsChatStreaming(true);
+ setEnableAutoScroll(true);
+ const abortController = new AbortController();
+
+ try {
+ // Ensure we have a session ID
+ let currentSessionId = sessionId;
+ if (!currentSessionId) {
+ const session = await initSession();
+ currentSessionId = session.id;
+ }
+
+ let requestIdForMessage = "";
+
+ // add this session on sidebar
+ if (messages.length === 0) {
+ const prevValue = newSessionsStore.getValue();
+ newSessionsStore.setValue([
+ {
+ id: currentSessionId,
+ title: message,
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ },
+ ...prevValue,
+ ]);
+ }
+
+ setChatAbortController(abortController);
+
+ await promptNebula({
+ abortController,
+ message: message,
+ sessionId: currentSessionId,
+ config: config,
+ authToken: props.authToken,
+ handleStream(res) {
+ if (abortController.signal.aborted) {
+ return;
+ }
- // otherwise, add a new message
- return [
- ...prev,
- {
- text: res.data.v,
- type: "assistant",
- request_id: requestIdForMessage,
- },
- ];
- });
- }
-
- if (res.event === "presence") {
- setMessages((prev) => {
- const lastMessage = prev[prev.length - 1];
- // if last message is presence, overwrite it
- if (lastMessage?.type === "presence") {
- return [
- ...prev.slice(0, -1),
- { text: res.data.data, type: "presence" },
- ];
- }
- // otherwise, add a new message
- return [...prev, { text: res.data.data, type: "presence" }];
- });
- }
+ if (res.event === "init") {
+ requestIdForMessage = res.data.request_id;
+ }
- if (res.event === "action") {
- if (res.type === "sign_transaction") {
+ if (res.event === "delta") {
setMessages((prev) => {
- let prevMessages = prev;
- // if last message is presence, remove it
- if (
- prevMessages[prevMessages.length - 1]?.type === "presence"
- ) {
- prevMessages = prevMessages.slice(0, -1);
+ const lastMessage = prev[prev.length - 1];
+ // if last message is presence, overwrite it
+ if (lastMessage?.type === "presence") {
+ return [
+ ...prev.slice(0, -1),
+ {
+ text: res.data.v,
+ type: "assistant",
+ request_id: requestIdForMessage,
+ },
+ ];
}
+ // if last message is from chat, append to it
+ if (lastMessage?.type === "assistant") {
+ return [
+ ...prev.slice(0, -1),
+ {
+ text: lastMessage.text + res.data.v,
+ type: "assistant",
+ request_id: requestIdForMessage,
+ },
+ ];
+ }
+
+ // otherwise, add a new message
return [
- ...prevMessages,
+ ...prev,
{
- type: "send_transaction",
- data: res.data,
+ text: res.data.v,
+ type: "assistant",
+ request_id: requestIdForMessage,
},
];
});
}
- }
- },
- contextFilters: contextFilters,
- });
- } catch (error) {
- if (abortController.signal.aborted) {
- return;
- }
- console.error(error);
-
- setMessages((prev) => {
- const newMessages = prev.slice(
- 0,
- prev[prev.length - 1]?.type === "presence" ? -1 : undefined,
- );
-
- // add error message
- newMessages.push({
- text: `Error: ${error instanceof Error ? error.message : "Failed to execute command"}`,
- type: "error",
- });
-
- return newMessages;
- });
- } finally {
- setIsChatStreaming(false);
- }
- }
- async function handleUpdateConfig(newConfig: ExecuteConfig) {
- setConfig(newConfig);
+ if (res.event === "presence") {
+ setMessages((prev) => {
+ const lastMessage = prev[prev.length - 1];
+ // if last message is presence, overwrite it
+ if (lastMessage?.type === "presence") {
+ return [
+ ...prev.slice(0, -1),
+ { text: res.data.data, type: "presence" },
+ ];
+ }
+ // otherwise, add a new message
+ return [...prev, { text: res.data.data, type: "presence" }];
+ });
+ }
- try {
- if (!sessionId) {
- // If no session exists, create a new one
- await initSession();
- } else {
- await updateSession({
- authToken: props.authToken,
- config: newConfig,
- sessionId,
- contextFilters,
+ if (res.event === "action") {
+ if (res.type === "sign_transaction") {
+ setMessages((prev) => {
+ let prevMessages = prev;
+ // if last message is presence, remove it
+ if (
+ prevMessages[prevMessages.length - 1]?.type === "presence"
+ ) {
+ prevMessages = prevMessages.slice(0, -1);
+ }
+
+ return [
+ ...prevMessages,
+ {
+ type: "send_transaction",
+ data: res.data,
+ },
+ ];
+ });
+ }
+ }
+ },
+ contextFilters: contextFilters,
+ });
+ } catch (error) {
+ if (abortController.signal.aborted) {
+ return;
+ }
+ console.error(error);
+
+ setMessages((prev) => {
+ const newMessages = prev.slice(
+ 0,
+ prev[prev.length - 1]?.type === "presence" ? -1 : undefined,
+ );
+
+ // add error message
+ newMessages.push({
+ text: `Error: ${error instanceof Error ? error.message : "Failed to execute command"}`,
+ type: "error",
+ });
+
+ return newMessages;
});
+ } finally {
+ setIsChatStreaming(false);
+ setEnableAutoScroll(false);
}
- } catch (error) {
- console.error("Failed to update session", error);
- setMessages((prev) => [
- ...prev,
- {
- text: `Error: Failed to ${sessionId ? "update" : "create"} session`,
- type: "error",
- },
- ]);
- }
- }
+ },
+ [
+ sessionId,
+ contextFilters,
+ config,
+ props.authToken,
+ messages.length,
+ initSession,
+ ],
+ );
- const updateConfig = useMutation({
- mutationFn: handleUpdateConfig,
- });
+ const hasDoneAutoPrompt = useRef(false);
+
+ // eslint-disable-next-line no-restricted-syntax
+ useEffect(() => {
+ if (
+ props.initialPrompt &&
+ messages.length === 0 &&
+ !hasDoneAutoPrompt.current
+ ) {
+ hasDoneAutoPrompt.current = true;
+ handleSendMessage(props.initialPrompt);
+ }
+ }, [props.initialPrompt, messages.length, handleSendMessage]);
const showEmptyState = !userHasSubmittedMessage && messages.length === 0;
return (
-
+
-
+
{showEmptyState ? (
-
-
{
- updateConfig.mutate(config);
- }}
- />
+
+
) : (
-
-
- {/* Scroll anchor */}
-
-
-
-
{
- updateConfig.mutate(config);
- }}
- abortChatStream={() => {
- chatAbortController?.abort();
- setChatAbortController(undefined);
- setIsChatStreaming(false);
- // if last message is presence, remove it
- if (messages[messages.length - 1]?.type === "presence") {
- setMessages((prev) => prev.slice(0, -1));
- }
- }}
+ authToken={props.authToken}
+ sessionId={sessionId}
+ className="min-w-0 pt-10 pb-32"
+ twAccount={props.account}
+ client={client}
+ enableAutoScroll={enableAutoScroll}
+ setEnableAutoScroll={setEnableAutoScroll}
/>
+
+
+ {
+ chatAbortController?.abort();
+ setChatAbortController(undefined);
+ setIsChatStreaming(false);
+ // if last message is presence, remove it
+ if (messages[messages.length - 1]?.type === "presence") {
+ setMessages((prev) => prev.slice(0, -1));
+ }
+ }}
+ />
+
)}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/ChatSidebar.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/ChatSidebar.tsx
index 76224a4b52f..3fc35bf5add 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatSidebar.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatSidebar.tsx
@@ -1,8 +1,13 @@
"use client";
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
+import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
-import { ChevronRightIcon, MessageSquareDashedIcon } from "lucide-react";
+import {
+ ChevronRightIcon,
+ FlaskConicalIcon,
+ MessageSquareDashedIcon,
+} from "lucide-react";
import Link from "next/link";
import type { TruncatedSessionInfo } from "../api/types";
import { useNewChatPageLink } from "../hooks/useNewChatPageLink";
@@ -23,10 +28,15 @@ export function ChatSidebar(props: {
return (
-
+
+
+
+
+ Alpha
+
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/Chatbar.stories.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/Chatbar.stories.tsx
index cb73d8486f1..e54cc308b11 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/Chatbar.stories.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/Chatbar.stories.tsx
@@ -29,44 +29,19 @@ export const Mobile: Story = {
function Story() {
return (
-
+
{}}
- config={{
- mode: "client",
- signer_wallet_address: "xxxxx",
- }}
isChatStreaming={false}
sendMessage={() => {}}
- updateConfig={() => {}}
/>
-
+
{}}
- config={{
- mode: "client",
- signer_wallet_address: "xxxxx",
- }}
isChatStreaming={true}
sendMessage={() => {}}
- updateConfig={() => {}}
- />
-
-
-
- {}}
- config={{
- mode: "engine",
- engine_authorization_token: "xxxxx",
- engine_backend_wallet_address: "0x1234",
- engine_url: "https://some-engine-url.com",
- }}
- isChatStreaming={false}
- sendMessage={() => {}}
- updateConfig={() => {}}
/>
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx
index a9065a397e4..72c7ae61d55 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx
@@ -157,6 +157,8 @@ function Story() {
},
]}
client={getThirdwebClient()}
+ enableAutoScroll={true}
+ setEnableAutoScroll={() => {}}
/>
@@ -167,6 +169,8 @@ function Story() {
isChatStreaming={false}
sessionId="xxxxx"
twAccount={accountStub()}
+ enableAutoScroll={true}
+ setEnableAutoScroll={() => {}}
messages={[
{
text: randomLorem(10),
@@ -182,6 +186,8 @@ function Story() {
{}}
client={getThirdwebClient()}
authToken="xxxxx"
isChatStreaming={false}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx
index a5355bfe06d..2d42c17f3a5 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx
@@ -1,7 +1,9 @@
import { GradientAvatar } from "@/components/blocks/Avatars/GradientAvatar";
+import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Alert, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
+import { getThirdwebClient } from "@/constants/thirdweb.server";
import { cn } from "@/lib/utils";
import type { Account as TWAccount } from "@3rdweb-sdk/react/hooks/useApi";
import { useMutation } from "@tanstack/react-query";
@@ -12,13 +14,15 @@ import {
ThumbsDownIcon,
ThumbsUpIcon,
} from "lucide-react";
-import { useState } from "react";
+import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import type { ThirdwebClient } from "thirdweb";
+import { sendTransaction } from "thirdweb";
import { useActiveAccount } from "thirdweb/react";
import type { Account } from "thirdweb/wallets";
import { TransactionButton } from "../../../../components/buttons/TransactionButton";
import { MarkdownRenderer } from "../../../../components/contract-components/published-contract/markdown-renderer";
+import { useV5DashboardChain } from "../../../../lib/v5-adapter";
import { submitFeedback } from "../api/feedback";
import { NebulaIcon } from "../icons/NebulaIcon";
@@ -48,97 +52,153 @@ export function Chats(props: {
className?: string;
twAccount: TWAccount;
client: ThirdwebClient;
+ setEnableAutoScroll: (enable: boolean) => void;
+ enableAutoScroll: boolean;
}) {
+ const { messages, setEnableAutoScroll, enableAutoScroll } = props;
+ const scrollAnchorRef = useRef(null);
+ const chatContainerRef = useRef(null);
+
+ // auto scroll to bottom when messages change
+ // eslint-disable-next-line no-restricted-syntax
+ useEffect(() => {
+ if (!enableAutoScroll || messages.length === 0) {
+ return;
+ }
+
+ scrollAnchorRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, [messages, enableAutoScroll]);
+
+ // stop auto scrolling when user interacts with chat
+ // eslint-disable-next-line no-restricted-syntax
+ useEffect(() => {
+ if (!enableAutoScroll) {
+ return;
+ }
+
+ const chatScrollContainer =
+ chatContainerRef.current?.querySelector("[data-scrollable]");
+
+ if (!chatScrollContainer) {
+ return;
+ }
+
+ const disableScroll = () => {
+ setEnableAutoScroll(false);
+ chatScrollContainer.removeEventListener("mousedown", disableScroll);
+ chatScrollContainer.removeEventListener("wheel", disableScroll);
+ };
+
+ chatScrollContainer.addEventListener("mousedown", disableScroll);
+ chatScrollContainer.addEventListener("wheel", disableScroll);
+ }, [setEnableAutoScroll, enableAutoScroll]);
+
return (
-
- {props.messages.map((message, index) => {
- const isMessagePending =
- props.isChatStreaming && index === props.messages.length - 1;
- return (
-
-
-
- {message.type === "user" ? (
-
- ) : (
+
+
+
+
+ {props.messages.map((message, index) => {
+ const isMessagePending =
+ props.isChatStreaming && index === props.messages.length - 1;
+ return (
+
- {message.type === "presence" && (
-
- )}
+
+ {message.type === "user" ? (
+
+ ) : (
+
+ {message.type === "presence" && (
+
+ )}
- {message.type === "assistant" && (
-
- )}
+ {message.type === "assistant" && (
+
+ )}
- {message.type === "error" && (
-
- )}
-
- )}
-
-
- {message.type === "assistant" ? (
-
- ) : message.type === "error" ? (
-
- {message.text}
-
- ) : message.type === "send_transaction" ? (
-
- ) : (
-
{message.text}
- )}
+ {message.type === "error" && (
+
+ )}
+
+ )}
+
+
+ {message.type === "assistant" ? (
+
+ ) : message.type === "error" ? (
+
+ {message.text}
+
+ ) : message.type === "send_transaction" ? (
+
+ ) : (
+ {message.text}
+ )}
- {message.type === "assistant" &&
- !props.isChatStreaming &&
- props.sessionId &&
- message.request_id && (
-
- )}
-
-
+ {message.type === "assistant" &&
+ !props.isChatStreaming &&
+ props.sessionId &&
+ message.request_id && (
+
+ )}
+
+
+
+ );
+ })}
+
- );
- })}
+
+
);
}
@@ -241,16 +301,28 @@ function SendTransactionButton(props: {
twAccount: TWAccount;
}) {
const account = useActiveAccount();
+ const chain = useV5DashboardChain(props.txData?.chainId);
+
const sendTxMutation = useMutation({
mutationFn: () => {
if (!account) {
throw new Error("No active account");
}
- if (!props.txData) {
+ if (!props.txData || !chain) {
throw new Error("Invalid transaction");
}
- return account.sendTransaction(props.txData);
+
+ return sendTransaction({
+ account,
+ transaction: {
+ ...props.txData,
+ nonce: Number(props.txData.nonce),
+ to: props.txData.to || undefined, // Get rid of the potential null value
+ chain,
+ client: getThirdwebClient(),
+ },
+ });
},
});
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.stories.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.stories.tsx
index 28b82f25688..f14a055342a 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.stories.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.stories.tsx
@@ -31,20 +31,70 @@ export const Mobile: Story = {
function Story() {
return (
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.tsx
index 29af4ac1184..d2f9852fbfe 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.tsx
@@ -1,6 +1,7 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
+import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -42,14 +43,46 @@ export default function ContextFiltersButton(props: {
mutationFn: props.updateContextFilters,
});
+ const chainIds = props.contextFilters?.chainIds;
+ const contractAddresses = props.contextFilters?.contractAddresses;
+ const walletAddresses = props.contextFilters?.walletAddresses;
+
return (
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.stories.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.stories.tsx
index 4cb7d39c107..eee82fe6dc8 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.stories.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.stories.tsx
@@ -29,14 +29,7 @@ export const Mobile: Story = {
function Story() {
return (
- {}}
- updateConfig={() => {}}
- />
+ {}} />
);
}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx
index 21903104452..72626890f78 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx
@@ -2,14 +2,11 @@
import { ArrowUpRightIcon } from "lucide-react";
import { Button } from "../../../../@/components/ui/button";
-import type { ExecuteConfig } from "../api/types";
import { NebulaIcon } from "../icons/NebulaIcon";
import { Chatbar } from "./ChatBar";
export function EmptyStateChatPageContent(props: {
- updateConfig: (config: ExecuteConfig) => void;
sendMessage: (message: string) => void;
- config: ExecuteConfig;
}) {
return (
@@ -31,9 +28,7 @@ export function EmptyStateChatPageContent(props: {
{
// the page will switch so, no need to handle abort here
}}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/NebulaMobileNav.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/NebulaMobileNav.tsx
index fd0bd585522..1ecbdaa02cf 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/NebulaMobileNav.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/NebulaMobileNav.tsx
@@ -32,7 +32,7 @@ export function MobileNav(props: {
onClick={() => setIsOpen(!isOpen)}
className="h-auto w-auto p-0.5"
>
-
+
;
+}) {
+ const [searchParams, authToken] = await Promise.all([
+ props.searchParams,
+ getAuthToken(),
+ ]);
if (!authToken) {
loginRedirect();
@@ -27,6 +34,7 @@ export default async function Page() {
session={undefined}
type="landing"
account={account}
+ initialPrompt={searchParams.prompt}
/>
);
}
diff --git a/apps/dashboard/src/app/nebula-app/[...not-found]/page.tsx b/apps/dashboard/src/app/nebula-app/[...not-found]/page.tsx
new file mode 100644
index 00000000000..ff297366998
--- /dev/null
+++ b/apps/dashboard/src/app/nebula-app/[...not-found]/page.tsx
@@ -0,0 +1,67 @@
+import { TrackedLinkTW } from "@/components/ui/tracked-link";
+
+export default function NebulaNotFound() {
+ return (
+
+
+
+
+ 404
+
+
+
+
+
+ Uh oh.
+ Looks like Nebula
+ can't be found here.
+
+
+
+
+
+
+ Go to{" "}
+
+ homepage
+
+
+
+
+
+
+
+
+ );
+}
+
+type AuroraProps = {
+ size: { width: string; height: string };
+ pos: { top: string; left: string };
+ color: string;
+};
+
+const Aurora: React.FC = ({ color, pos, size }) => {
+ return (
+
+ );
+};
diff --git a/apps/dashboard/src/app/nebula-app/layout.tsx b/apps/dashboard/src/app/nebula-app/layout.tsx
new file mode 100644
index 00000000000..77298a40e8a
--- /dev/null
+++ b/apps/dashboard/src/app/nebula-app/layout.tsx
@@ -0,0 +1,21 @@
+import type { Metadata } from "next";
+
+const title =
+ "thirdweb Nebula: The Most powerful AI for interacting with the blockchain";
+const description =
+ "The most powerful AI for interacting with the blockchain, with real-time access to EVM chains and their data";
+
+export const metadata: Metadata = {
+ title,
+ description,
+ openGraph: {
+ title,
+ description,
+ },
+};
+
+export default function Layout(props: {
+ children: React.ReactNode;
+}) {
+ return props.children;
+}
diff --git a/apps/dashboard/src/app/nebula-app/login/NebulaLoginPage.tsx b/apps/dashboard/src/app/nebula-app/login/NebulaLoginPage.tsx
new file mode 100644
index 00000000000..ede0bfc11b2
--- /dev/null
+++ b/apps/dashboard/src/app/nebula-app/login/NebulaLoginPage.tsx
@@ -0,0 +1,68 @@
+"use client";
+
+import Link from "next/link";
+import { useState } from "react";
+import { EmptyStateChatPageContent } from "../(app)/components/EmptyStateChatPageContent";
+import { NebulaIcon } from "../(app)/icons/NebulaIcon";
+import { Button } from "../../../@/components/ui/button";
+import type { Account } from "../../../@3rdweb-sdk/react/hooks/useApi";
+import { LoginAndOnboardingPageContent } from "../../login/LoginPage";
+
+export function NebulaLoginPage(props: {
+ account: Account | undefined;
+}) {
+ const [message, setMessage] = useState(undefined);
+ const [showLoginPage, setShowLoginPage] = useState(false);
+
+ return (
+
+
+
+ {showLoginPage ? (
+
+ ) : (
+
+ {
+ setMessage(msg);
+ setShowLoginPage(true);
+ }}
+ />
+
+ )}
+
+ );
+}
diff --git a/apps/dashboard/src/app/nebula-app/login/page.tsx b/apps/dashboard/src/app/nebula-app/login/page.tsx
index 05e92f2870b..2e9d930936c 100644
--- a/apps/dashboard/src/app/nebula-app/login/page.tsx
+++ b/apps/dashboard/src/app/nebula-app/login/page.tsx
@@ -1,19 +1,8 @@
import { getRawAccount } from "../../account/settings/getAccount";
-import { LoginAndOnboardingPage } from "../../login/LoginPage";
-import { isValidEncodedRedirectPath } from "../../login/isValidEncodedRedirectPath";
+import { NebulaLoginPage } from "./NebulaLoginPage";
-export default async function NebulaLogin(props: {
- searchParams: Promise<{
- next?: string;
- }>;
-}) {
- const nextPath = (await props.searchParams).next;
+export default async function NebulaLogin() {
const account = await getRawAccount();
- const redirectPath =
- nextPath && isValidEncodedRedirectPath(nextPath) ? nextPath : "/";
-
- return (
-
- );
+ return ;
}
diff --git a/apps/dashboard/src/app/nebula-app/opengraph-image.png b/apps/dashboard/src/app/nebula-app/opengraph-image.png
new file mode 100644
index 00000000000..6c632879f47
Binary files /dev/null and b/apps/dashboard/src/app/nebula-app/opengraph-image.png differ
diff --git a/apps/dashboard/src/app/providers.tsx b/apps/dashboard/src/app/providers.tsx
index 39680b3d838..8a5e1cf908a 100644
--- a/apps/dashboard/src/app/providers.tsx
+++ b/apps/dashboard/src/app/providers.tsx
@@ -2,11 +2,16 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider } from "next-themes";
-import { useMemo } from "react";
-import { ThirdwebProvider, useActiveAccount } from "thirdweb/react";
+import { useEffect, useMemo } from "react";
+import {
+ ThirdwebProvider,
+ useActiveAccount,
+ useConnectionManager,
+} from "thirdweb/react";
import { CustomConnectWallet } from "../@3rdweb-sdk/react/components/connect-wallet";
import { PosthogIdentifier } from "../components/wallets/PosthogIdentifier";
import { isSanctionedAddress } from "../data/eth-sanctioned-addresses";
+import { useAllChainsData } from "../hooks/chains/allChains";
import { SyncChainStores } from "../stores/chainStores";
import { TWAutoConnect } from "./components/autoconnect";
@@ -17,6 +22,7 @@ export function AppRouterProviders(props: { children: React.ReactNode }) {
+
{
+ if (allChainsV5.length > 0) {
+ connectionManager.defineChains(allChainsV5);
+ }
+ }, [allChainsV5, connectionManager]);
+
+ return null;
+}
+
const SanctionedAddressesChecker = ({
children,
}: {
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/AccountAbstractionPage.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/AccountAbstractionPage.tsx
index fcb3fd01e5d..00328f55525 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/AccountAbstractionPage.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/AccountAbstractionPage.tsx
@@ -2,7 +2,6 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import {
- type Account,
type ApiKeyService,
accountStatus,
} from "@3rdweb-sdk/react/hooks/useApi";
@@ -25,7 +24,6 @@ export function AccountAbstractionPage(props: {
apiKeyServices: ApiKeyService[];
billingStatus: "validPayment" | (string & {}) | null;
tab?: string;
- twAccount: Account;
}) {
const { apiKeyServices } = props;
@@ -81,11 +79,9 @@ export function AccountAbstractionPage(props: {
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/page.tsx
index 0550876f974..86f3c8fe4ed 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/page.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/account-abstraction/page.tsx
@@ -4,7 +4,6 @@ import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import type { Metadata } from "next";
import { notFound, redirect } from "next/navigation";
import { getAbsoluteUrl } from "../../../../../../lib/vercel-utils";
-import { getValidAccount } from "../../../../../account/settings/getAccount";
import { getAPIKeyForProjectId } from "../../../../../api/lib/getAPIKeys";
import { AccountAbstractionPage } from "./AccountAbstractionPage";
@@ -14,10 +13,7 @@ export default async function Page(props: {
}) {
const { team_slug, project_slug } = await props.params;
- const [account, team, project] = await Promise.all([
- getValidAccount(
- `/${team_slug}/${project_slug}/connect/account-abstraction`,
- ),
+ const [team, project] = await Promise.all([
getTeamBySlug(team_slug),
getProject(team_slug, project_slug),
]);
@@ -45,7 +41,6 @@ export default async function Page(props: {
projectKey={project.publishableKey}
apiKeyServices={apiKey.services || []}
tab={(await props.searchParams).tab}
- twAccount={account}
/>
);
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/config/loading.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/config/loading.tsx
deleted file mode 100644
index 6c54ef15def..00000000000
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/config/loading.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-"use client";
-
-export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/config/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/config/page.tsx
deleted file mode 100644
index 1eaf77e9e90..00000000000
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/config/page.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { getProject } from "@/api/projects";
-import { getAPIKeyForProjectId } from "app/api/lib/getAPIKeys";
-import { notFound, redirect } from "next/navigation";
-import { InAppWalletSettingsPage } from "../../../../../../../components/embedded-wallets/Configure";
-import { getValidAccount } from "../../../../../../account/settings/getAccount";
-import { TRACKING_CATEGORY } from "../_constants";
-
-export default async function Page(props: {
- params: Promise<{ team_slug: string; project_slug: string }>;
-}) {
- const params = await props.params;
- const [account, project] = await Promise.all([
- getValidAccount(
- `/${params.team_slug}/${params.project_slug}/connect/in-app-wallets/config`,
- ),
- getProject(params.team_slug, params.project_slug),
- ]);
-
- if (!project) {
- redirect("/team");
- }
-
- const apiKey = await getAPIKeyForProjectId(project.id);
-
- if (!apiKey) {
- notFound();
- }
-
- return (
- <>
-
- >
- );
-}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/layout.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/layout.tsx
index 3dc051d0fcb..f3b8564326d 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/layout.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/in-app-wallets/layout.tsx
@@ -45,11 +45,6 @@ export default async function Layout(props: {
path: `/team/${team_slug}/${project_slug}/connect/in-app-wallets/users`,
exactMatch: true,
},
- {
- name: "Configuration",
- path: `/team/${team_slug}/${project_slug}/connect/in-app-wallets/config`,
- exactMatch: true,
- },
]}
/>
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/page.tsx
index e8c8a734c52..1b134327574 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/page.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/page.tsx
@@ -1,5 +1,4 @@
-import { getProject } from "@/api/projects";
-import { notFound, redirect } from "next/navigation";
+import { redirect } from "next/navigation";
export default async function Page(props: {
params: Promise<{
@@ -8,11 +7,6 @@ export default async function Page(props: {
}>;
}) {
const params = await props.params;
- const project = await getProject(params.team_slug, params.project_slug);
-
- if (!project) {
- notFound();
- }
redirect(
`/team/${params.team_slug}/${params.project_slug}/connect/in-app-wallets`,
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/insight/[blueprint_slug]/blueprint-playground.client.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/insight/[blueprint_slug]/blueprint-playground.client.tsx
index aecd1cc031e..bc9fe21f76d 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/insight/[blueprint_slug]/blueprint-playground.client.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/insight/[blueprint_slug]/blueprint-playground.client.tsx
@@ -1,7 +1,7 @@
"use client";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
-import { useHeightObserver } from "@/components/ui/DynamicHeight";
+import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
@@ -28,9 +28,12 @@ import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { type UseFormReturn, useForm } from "react-hook-form";
import { z } from "zod";
+import { useTrack } from "../../../../../../hooks/analytics/useTrack";
import { getVercelEnv } from "../../../../../../lib/vercel-utils";
import type { BlueprintParameter, BlueprintPathMetadata } from "../utils";
+const trackingCategory = "insightBlueprint";
+
export function BlueprintPlayground(props: {
metadata: BlueprintPathMetadata;
backLink: string;
@@ -151,6 +154,7 @@ export function BlueprintPlaygroundUI(props: {
projectSettingsLink: string;
supportedChainIds: number[];
}) {
+ const trackEvent = useTrack();
const parameters = useMemo(() => {
return modifyParametersForPlayground(props.metadata.parameters);
}, [props.metadata.parameters]);
@@ -182,14 +186,15 @@ export function BlueprintPlaygroundUI(props: {
intent: "run",
});
+ trackEvent({
+ category: trackingCategory,
+ action: "click",
+ label: "run",
+ url: url,
+ });
props.onRun(url);
}
- // This allows us to always limit the grid height to whatever is the height of left section on desktop
- // so that entire left section is always visible, but the right section has a scrollbar if it exceeds the height of left section
- const { height, elementRef: leftSectionRef } = useHeightObserver();
- const isMobile = useIsMobileViewport();
-
return (