Skip to content

Commit ea1ec6b

Browse files
committed
[NEB-69] Nebula: Add UI for Swap and Approve transactions (#6898)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on refactoring transaction handling in the `Nebula` application, improving message types, and enhancing UI components for transaction approval and execution. ### Detailed summary - Updated `TransactionButton` spacing from `gap-3` to `gap-2`. - Changed message types from `"send_transaction"` to `"action"` with a `subtype`. - Removed `InvalidTxData` story. - Introduced `NebulaSwapData` type for better structure. - Added `ApproveTransactionCard` and `SwapTransactionCard` components. - Enhanced error handling in transaction processing. - Updated `ExecuteTransactionCard` to use `useTxSetup` for transaction status management. - Improved UI components for displaying transaction status and hashes. - Refined `Chats` component to render messages based on new action types. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 293b246 commit ea1ec6b

File tree

10 files changed

+810
-296
lines changed

10 files changed

+810
-296
lines changed

apps/dashboard/src/app/nebula-app/(app)/api/chat.ts

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,38 @@ export type NebulaContext = {
99
networks: "mainnet" | "testnet" | "all" | null;
1010
};
1111

12+
export type NebulaSwapData = {
13+
action: string;
14+
transaction: {
15+
chainId: number;
16+
to: `0x${string}`;
17+
data: `0x${string}`;
18+
};
19+
to: {
20+
address: `0x${string}`;
21+
amount: string;
22+
chain_id: number;
23+
decimals: number;
24+
symbol: string;
25+
};
26+
from: {
27+
address: `0x${string}`;
28+
amount: string;
29+
chain_id: number;
30+
decimals: number;
31+
symbol: string;
32+
};
33+
intent: {
34+
amount: string;
35+
destinationChainId: number;
36+
destinationTokenAddress: `0x${string}`;
37+
originChainId: number;
38+
originTokenAddress: `0x${string}`;
39+
receiver: `0x${string}`;
40+
sender: `0x${string}`;
41+
};
42+
};
43+
1244
export async function promptNebula(params: {
1345
message: string;
1446
sessionId: string;
@@ -71,26 +103,29 @@ export async function promptNebula(params: {
71103
const data = JSON.parse(event.data);
72104

73105
if (data.type === "sign_transaction") {
74-
let txData = null;
75-
76106
try {
77-
const parsedTxData = JSON.parse(data.data);
78-
if (
79-
parsedTxData !== null &&
80-
typeof parsedTxData === "object" &&
81-
parsedTxData.chainId
82-
) {
83-
txData = parsedTxData;
84-
}
107+
const parsedTxData = JSON.parse(data.data) as NebulaTxData;
108+
params.handleStream({
109+
event: "action",
110+
type: "sign_transaction",
111+
data: parsedTxData,
112+
});
85113
} catch (e) {
86-
console.error("failed to parse action data", e);
114+
console.error("failed to parse action data", e, { event });
87115
}
116+
}
88117

89-
params.handleStream({
90-
event: "action",
91-
type: "sign_transaction",
92-
data: txData,
93-
});
118+
if (data.type === "sign_swap") {
119+
try {
120+
const swapData = JSON.parse(data.data) as NebulaSwapData;
121+
params.handleStream({
122+
event: "action",
123+
type: "sign_swap",
124+
data: swapData,
125+
});
126+
} catch (e) {
127+
console.error("failed to parse action data", e, { event });
128+
}
94129
}
95130

96131
break;
@@ -160,9 +195,14 @@ type ChatStreamedResponse =
160195
}
161196
| {
162197
event: "action";
163-
type: "sign_transaction" & (string & {});
198+
type: "sign_transaction";
164199
data: NebulaTxData;
165200
}
201+
| {
202+
event: "action";
203+
type: "sign_swap";
204+
data: NebulaSwapData;
205+
}
166206
| {
167207
event: "context";
168208
data: {
@@ -187,7 +227,7 @@ type ChatStreamedEvent =
187227
}
188228
| {
189229
event: "action";
190-
type: "sign_transaction" & (string & {});
230+
type: "sign_transaction" | "sign_swap";
191231
data: string;
192232
}
193233
| {

apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,21 @@ export function ChatPageContent(props: {
6060

6161
if (content.type === "sign_transaction") {
6262
const txData = JSON.parse(content.data);
63-
if (
64-
typeof txData === "object" &&
65-
txData !== null &&
66-
txData.chainId
67-
) {
68-
_messages.push({
69-
type: "send_transaction",
70-
data: txData,
71-
});
72-
}
63+
_messages.push({
64+
type: "action",
65+
subtype: "sign_transaction",
66+
data: txData,
67+
});
68+
} else if (content.type === "sign_swap") {
69+
const swapData = JSON.parse(content.data);
70+
_messages.push({
71+
type: "action",
72+
subtype: "sign_swap",
73+
data: swapData,
74+
});
7375
}
74-
} catch {
75-
// ignore
76+
} catch (e) {
77+
console.error("error processing message", e, { message });
7678
}
7779
} else {
7880
_messages.push({
@@ -548,13 +550,25 @@ export async function handleNebulaPrompt(params: {
548550
}
549551

550552
if (res.event === "action") {
553+
hasReceivedResponse = true;
551554
if (res.type === "sign_transaction") {
552-
hasReceivedResponse = true;
553555
setMessages((prevMessages) => {
554556
return [
555557
...prevMessages,
556558
{
557-
type: "send_transaction",
559+
type: "action",
560+
subtype: res.type,
561+
data: res.data,
562+
},
563+
];
564+
});
565+
} else if (res.type === "sign_swap") {
566+
setMessages((prevMessages) => {
567+
return [
568+
...prevMessages,
569+
{
570+
type: "action",
571+
subtype: res.type,
558572
data: res.data,
559573
},
560574
];

apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ export const SendTransaction: Story = {
5757
request_id: undefined,
5858
},
5959
{
60-
type: "send_transaction",
60+
type: "action",
61+
subtype: "sign_transaction",
6162
data: {
6263
chainId: 1,
6364
to: "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
@@ -69,22 +70,6 @@ export const SendTransaction: Story = {
6970
},
7071
};
7172

72-
export const InvalidTxData: Story = {
73-
args: {
74-
messages: [
75-
{
76-
text: randomLorem(40),
77-
type: "assistant",
78-
request_id: undefined,
79-
},
80-
{
81-
type: "send_transaction",
82-
data: null,
83-
},
84-
],
85-
},
86-
};
87-
8873
export const WithAndWithoutRequestId: Story = {
8974
args: {
9075
messages: [

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

Lines changed: 81 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
22
import { Spinner } from "@/components/ui/Spinner/Spinner";
3-
import { Alert, AlertTitle } from "@/components/ui/alert";
43
import { Button } from "@/components/ui/button";
54
import { cn } from "@/lib/utils";
65
import { useMutation } from "@tanstack/react-query";
@@ -15,16 +14,18 @@ import {
1514
import { useEffect, useRef, useState } from "react";
1615
import { toast } from "sonner";
1716
import type { ThirdwebClient } from "thirdweb";
17+
import type { NebulaSwapData } from "../api/chat";
1818
import { submitFeedback } from "../api/feedback";
1919
import { NebulaIcon } from "../icons/NebulaIcon";
2020
import { ExecuteTransactionCard } from "./ExecuteTransactionCard";
2121
import { Reasoning } from "./Reasoning/Reasoning";
22+
import { ApproveTransactionCard, SwapTransactionCard } from "./Swap/SwapCards";
2223

2324
export type NebulaTxData = {
2425
chainId: number;
2526
data: `0x${string}`;
2627
to: string;
27-
value: string;
28+
value?: string;
2829
};
2930

3031
export type ChatMessage =
@@ -43,8 +44,14 @@ export type ChatMessage =
4344
type: "assistant";
4445
}
4546
| {
46-
type: "send_transaction";
47-
data: NebulaTxData | null;
47+
type: "action";
48+
subtype: "sign_transaction";
49+
data: NebulaTxData;
50+
}
51+
| {
52+
type: "action";
53+
subtype: "sign_swap";
54+
data: NebulaSwapData;
4855
};
4956

5057
export function Chats(props: {
@@ -161,32 +168,12 @@ export function Chats(props: {
161168
{/* Right Message */}
162169
<div className="min-w-0 grow">
163170
<ScrollShadow className="rounded-lg">
164-
{message.type === "assistant" ? (
165-
<StyledMarkdownRenderer
166-
text={message.text}
167-
isMessagePending={isMessagePending}
168-
type="assistant"
169-
/>
170-
) : message.type === "error" ? (
171-
<div className="rounded-xl border bg-card px-4 py-2 text-destructive-text leading-normal">
172-
{message.text}
173-
</div>
174-
) : message.type === "send_transaction" ? (
175-
<ExecuteTransactionCardWithFallback
176-
txData={message.data}
177-
client={props.client}
178-
onTxSettled={(txHash) => {
179-
props.sendMessage(
180-
getTransactionSettledPrompt(txHash),
181-
);
182-
}}
183-
/>
184-
) : message.type === "presence" ? (
185-
<Reasoning
186-
isPending={isMessagePending}
187-
texts={message.texts}
188-
/>
189-
) : null}
171+
<RenderMessage
172+
message={message}
173+
isMessagePending={isMessagePending}
174+
client={props.client}
175+
sendMessage={props.sendMessage}
176+
/>
190177
</ScrollShadow>
191178

192179
{message.type === "assistant" &&
@@ -215,36 +202,77 @@ export function Chats(props: {
215202
);
216203
}
217204

205+
function RenderMessage(props: {
206+
message: ChatMessage;
207+
isMessagePending: boolean;
208+
client: ThirdwebClient;
209+
sendMessage: (message: string) => void;
210+
}) {
211+
const { message, isMessagePending, client, sendMessage } = props;
212+
213+
switch (message.type) {
214+
case "assistant":
215+
return (
216+
<StyledMarkdownRenderer
217+
text={message.text}
218+
isMessagePending={isMessagePending}
219+
type="assistant"
220+
/>
221+
);
222+
223+
case "presence":
224+
return <Reasoning isPending={isMessagePending} texts={message.texts} />;
225+
226+
case "error":
227+
return (
228+
<div className="rounded-xl border bg-card px-4 py-2 text-destructive-text leading-normal">
229+
{message.text}
230+
</div>
231+
);
232+
233+
case "action": {
234+
if (message.subtype === "sign_transaction") {
235+
return (
236+
<ExecuteTransactionCard
237+
txData={message.data}
238+
client={client}
239+
onTxSettled={(txHash) => {
240+
sendMessage(getTransactionSettledPrompt(txHash));
241+
}}
242+
/>
243+
);
244+
}
245+
246+
if (message.subtype === "sign_swap") {
247+
if (message.data.action === "approval") {
248+
return (
249+
<ApproveTransactionCard swapData={message.data} client={client} />
250+
);
251+
}
252+
253+
return (
254+
<SwapTransactionCard
255+
swapData={message.data}
256+
client={client}
257+
onTxSettled={() => {
258+
// no op
259+
}}
260+
/>
261+
);
262+
}
263+
}
264+
}
265+
266+
return null;
267+
}
268+
218269
function getTransactionSettledPrompt(txHash: string) {
219270
return `\
220271
I've executed the following transaction successfully with hash: ${txHash}.
221272
222273
If our conversation calls for it, continue on to the next transaction or suggest next steps`;
223274
}
224275

225-
function ExecuteTransactionCardWithFallback(props: {
226-
txData: NebulaTxData | null;
227-
client: ThirdwebClient;
228-
onTxSettled: (txHash: string) => void;
229-
}) {
230-
if (!props.txData) {
231-
return (
232-
<Alert variant="destructive">
233-
<AlertCircleIcon className="size-5" />
234-
<AlertTitle>Failed to parse transaction data</AlertTitle>
235-
</Alert>
236-
);
237-
}
238-
239-
return (
240-
<ExecuteTransactionCard
241-
txData={props.txData}
242-
client={props.client}
243-
onTxSettled={props.onTxSettled}
244-
/>
245-
);
246-
}
247-
248276
function MessageActions(props: {
249277
authToken: string;
250278
requestId: string;

0 commit comments

Comments
 (0)