diff --git a/apps/dashboard/src/@/styles/globals.css b/apps/dashboard/src/@/styles/globals.css
index 84017105617..724a3a1cf44 100644
--- a/apps/dashboard/src/@/styles/globals.css
+++ b/apps/dashboard/src/@/styles/globals.css
@@ -157,6 +157,11 @@ input:-webkit-autofill:active {
transition: background-color 5000s ease-in-out 0s;
}
+.shiki,
+.shiki span {
+ background-color: var(--background) !important;
+}
+
.dark .shiki,
.dark .shiki span {
color: var(--shiki-dark) !important;
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 8b530d0ba32..9bf5ef79650 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx
@@ -43,7 +43,6 @@ export function ChatPageContent(props: {
try {
const content = JSON.parse(message.content) as {
session_id: string;
- request_id: string;
data: string;
type: "sign_transaction" | (string & {});
};
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 aea98d73417..91e15e365c5 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatSidebar.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatSidebar.tsx
@@ -79,7 +79,7 @@ export function ChatSidebar(props: {
)}
-
+
-
-
+
+
+
+
+
+
{}}
- />
-
-
-
- {}}
- messages={[
- {
- text: randomLorem(10),
- type: "user",
- },
{
text: randomLorem(20),
type: "error",
},
]}
/>
-
-
-
- {}}
- client={getThirdwebClient()}
- authToken="xxxxx"
- isChatStreaming={false}
- sessionId="xxxxx"
- twAccount={accountStub()}
+
+
-
-
-
- {}}
- client={getThirdwebClient()}
- authToken="xxxxx"
- isChatStreaming={false}
- sessionId="xxxxx"
- twAccount={accountStub()}
+
+
-
-
-
+
+
+ {}}
+ client={getThirdwebClient()}
+ authToken="xxxxx"
+ isChatStreaming={false}
+ sessionId="xxxxx"
+ twAccount={accountStub()}
+ messages={[
+ {
+ text: randomLorem(40),
+ type: "assistant",
+ request_id: "xxxxx",
+ },
+ {
+ text: randomLorem(50),
+ type: "assistant",
+ request_id: undefined,
+ },
+ ]}
+ />
+
+
+
+ {}}
+ client={getThirdwebClient()}
+ authToken="xxxxx"
+ isChatStreaming={false}
+ sessionId="xxxxx"
+ twAccount={accountStub()}
+ messages={[
+ {
+ text: responseWithCodeMarkdown,
+ type: "assistant",
+ request_id: undefined,
+ },
+ {
+ text: responseWithCodeMarkdown,
+ type: "user",
+ },
+ ]}
+ />
+
+
+
+
+
+ );
+}
+
+function Variant(props: {
+ label: string;
+ messages: ChatMessage[];
+}) {
+ return (
+
+ {}}
+ client={getThirdwebClient()}
+ authToken="xxxxx"
+ isChatStreaming={false}
+ sessionId="xxxxx"
+ twAccount={accountStub()}
+ messages={props.messages}
+ />
+
);
}
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 c59616cdcfe..c2073a44816 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx
@@ -12,17 +12,13 @@ import {
ThumbsDownIcon,
ThumbsUpIcon,
} from "lucide-react";
-import { useTheme } from "next-themes";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
-import { type ThirdwebClient, prepareTransaction } from "thirdweb";
-import { useSendTransaction } from "thirdweb/react";
-import { TransactionButton } from "../../../../components/buttons/TransactionButton";
+import type { ThirdwebClient } from "thirdweb";
import { MarkdownRenderer } from "../../../../components/contract-components/published-contract/markdown-renderer";
-import { useV5DashboardChain } from "../../../../lib/v5-adapter";
-import { getSDKTheme } from "../../../components/sdk-component-theme";
import { submitFeedback } from "../api/feedback";
import { NebulaIcon } from "../icons/NebulaIcon";
+import { ExecuteTransactionCard } from "./ExecuteTransactionCard";
export type NebulaTxData = {
chainId: number;
@@ -119,7 +115,7 @@ export function Chats(props: {
key={index}
>
{message.type === "user" ? (
-
+
) : message.type === "error" ? (
-
+
{message.text}
-
+
) : message.type === "send_transaction" ? (
-
{message.type === "assistant" &&
- !props.isChatStreaming &&
+ !isMessagePending &&
props.sessionId &&
message.request_id && (
);
}
-
-function SendTransactionButton(props: {
- txData: NebulaTxData;
- twAccount: TWAccount;
- client: ThirdwebClient;
-}) {
- const { theme } = useTheme();
- const { txData } = props;
- const sendTransaction = useSendTransaction({
- payModal: {
- theme: getSDKTheme(theme === "light" ? "light" : "dark"),
- },
- });
- const chain = useV5DashboardChain(txData.chainId);
-
- return (
- {
- const tx = prepareTransaction({
- chain: chain,
- client: props.client,
- data: txData.data,
- to: txData.to,
- value: BigInt(txData.value),
- });
-
- const promise = sendTransaction.mutateAsync(tx);
-
- toast.promise(promise, {
- success: "Transaction sent successfully",
- error: "Failed to send transaction",
- });
- }}
- className="gap-2"
- twAccount={props.twAccount}
- >
- Execute Transaction
-
- );
-}
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 17a26b30a3c..00935d5a348 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx
@@ -20,7 +20,7 @@ export function EmptyStateChatPageContent(props: {
-
+
How can I help you
with the blockchain today?
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/ExecuteTransactionCard.stories.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/ExecuteTransactionCard.stories.tsx
new file mode 100644
index 00000000000..c0dc838bb61
--- /dev/null
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/ExecuteTransactionCard.stories.tsx
@@ -0,0 +1,88 @@
+import { getThirdwebClient } from "@/constants/thirdweb.server";
+import type { Meta, StoryObj } from "@storybook/react";
+import { useState } from "react";
+import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
+import { accountStub } from "../../../../stories/stubs";
+import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
+import {
+ ExecuteTransactionCardLayout,
+ type TxStatus,
+} from "./ExecuteTransactionCard";
+
+const meta = {
+ title: "Nebula/ExecuteTransactionCard",
+ component: Story,
+ parameters: {
+ nextjs: {
+ appDirectory: true,
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Desktop: Story = {
+ args: {},
+};
+
+export const Mobile: Story = {
+ args: {},
+ parameters: {
+ viewport: mobileViewport("iphone14"),
+ },
+};
+
+const client = getThirdwebClient();
+
+const exampleTxHash =
+ "0xbe81f5a6421625052214b41bb79d1d82751b29aa5639b54d120f00531bb8bcf";
+
+function Story() {
+ return (
+
+
+
+ );
+}
+
+function Variant(props: {
+ label: string;
+ status: TxStatus;
+}) {
+ const [status, setStatus] = useState(props.status);
+ return (
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/ExecuteTransactionCard.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/ExecuteTransactionCard.tsx
new file mode 100644
index 00000000000..5c32384ea72
--- /dev/null
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/ExecuteTransactionCard.tsx
@@ -0,0 +1,213 @@
+import { CopyTextButton } from "@/components/ui/CopyTextButton";
+import { Spinner } from "@/components/ui/Spinner/Spinner";
+import {} from "@/components/ui/accordion";
+import { Button } from "@/components/ui/button";
+import { CodeClient } from "@/components/ui/code/code.client";
+import type { Account as TWAccount } from "@3rdweb-sdk/react/hooks/useApi";
+import {
+ ArrowRightLeftIcon,
+ CircleCheckIcon,
+ CircleXIcon,
+ ExternalLinkIcon,
+} from "lucide-react";
+import { useTheme } from "next-themes";
+import Link from "next/link";
+import { useState } from "react";
+import {
+ type ThirdwebClient,
+ prepareTransaction,
+ waitForReceipt,
+} from "thirdweb";
+import { useSendTransaction } from "thirdweb/react";
+import { TransactionButton } from "../../../../components/buttons/TransactionButton";
+import { useV5DashboardChain } from "../../../../lib/v5-adapter";
+import { getSDKTheme } from "../../../components/sdk-component-theme";
+import type { NebulaTxData } from "./Chats";
+
+export type TxStatus =
+ | {
+ type: "idle";
+ }
+ | {
+ type: "sending";
+ }
+ | {
+ type: "confirming";
+ txHash: string;
+ }
+ | {
+ type: "confirmed";
+ txHash: string;
+ }
+ | {
+ type: "failed";
+ txHash: string | undefined;
+ };
+
+export function ExecuteTransactionCard(props: {
+ txData: NebulaTxData;
+ twAccount: TWAccount;
+ client: ThirdwebClient;
+}) {
+ const [status, setStatus] = useState({ type: "idle" });
+ return (
+
+ );
+}
+
+export function ExecuteTransactionCardLayout(props: {
+ txData: NebulaTxData;
+ twAccount: TWAccount;
+ client: ThirdwebClient;
+ status: TxStatus;
+ setStatus: (status: TxStatus) => void;
+}) {
+ const { theme } = useTheme();
+ const { txData } = props;
+ const sendTransaction = useSendTransaction({
+ payModal: {
+ theme: getSDKTheme(theme === "light" ? "light" : "dark"),
+ },
+ });
+ const chain = useV5DashboardChain(txData.chainId);
+ const isTransactionSent =
+ props.status.type === "confirming" || props.status.type === "confirmed";
+
+ const explorer = chain.blockExplorers?.[0]?.url;
+
+ return (
+
+
+
+
+ Transaction
+
+
+
+
+
+
+
{
+ const tx = prepareTransaction({
+ chain: chain,
+ client: props.client,
+ data: txData.data,
+ to: txData.to,
+ value: BigInt(txData.value),
+ });
+
+ let txHash: string | undefined;
+
+ try {
+ // submit transaction
+ props.setStatus({ type: "sending" });
+ const submittedReceipt = await sendTransaction.mutateAsync(tx);
+ txHash = submittedReceipt.transactionHash;
+
+ // wait for receipt
+ props.setStatus({
+ type: "confirming",
+ txHash: submittedReceipt.transactionHash,
+ });
+
+ const confirmReceipt = await waitForReceipt(submittedReceipt);
+ txHash = confirmReceipt.transactionHash;
+ props.setStatus({
+ type: "confirmed",
+ txHash: confirmReceipt.transactionHash,
+ });
+ } catch {
+ props.setStatus({
+ type: "failed",
+ txHash: txHash,
+ });
+ }
+ }}
+ className="gap-2"
+ twAccount={props.twAccount}
+ >
+
+ Execute Transaction
+
+
+
+
+ {/* Tx Status */}
+ {props.status.type !== "idle" && (
+
+
+ {props.status.type === "sending" && (
+
+
+
Sending Transaction
+
+ )}
+
+ {isTransactionSent && (
+
+ )}
+
+ {props.status.type === "confirming" && (
+
+
+
Confirming Transaction
+
+ )}
+
+ {props.status.type === "confirmed" && (
+
+
+
Transaction Confirmed
+
+ )}
+
+ {props.status.type === "failed" && (
+
+ )}
+
+
+ {"txHash" in props.status && props.status.txHash && (
+
+ {explorer && (
+
+ )}
+
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/apps/dashboard/src/components/buttons/TransactionButton.tsx b/apps/dashboard/src/components/buttons/TransactionButton.tsx
index fffcaab57cb..7dc6588d5b8 100644
--- a/apps/dashboard/src/components/buttons/TransactionButton.tsx
+++ b/apps/dashboard/src/components/buttons/TransactionButton.tsx
@@ -29,7 +29,7 @@ type TransactionButtonProps = Omit & {
isPending: boolean;
isGasless?: boolean;
txChainID: number;
- variant?: "destructive" | "primary";
+ variant?: "destructive" | "primary" | "default";
twAccount: Account | undefined;
};