Skip to content

Commit 59a8dad

Browse files
[Playground] ai chat demo (#7833)
Co-authored-by: Cursor Agent <[email protected]>
1 parent f7aa3e6 commit 59a8dad

File tree

14 files changed

+1965
-6
lines changed

14 files changed

+1965
-6
lines changed

apps/playground-web/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"class-variance-authority": "^0.7.1",
1919
"clsx": "^2.1.1",
2020
"date-fns": "4.1.0",
21+
"fetch-event-stream": "0.1.5",
2122
"lucide-react": "0.525.0",
2223
"next": "15.3.5",
2324
"next-themes": "^0.4.6",
@@ -26,11 +27,15 @@
2627
"posthog-js": "1.256.1",
2728
"prettier": "3.6.2",
2829
"react": "19.1.0",
30+
"react-children-utilities": "^2.10.0",
2931
"react-dom": "19.1.0",
3032
"react-hook-form": "7.55.0",
33+
"react-markdown": "10.1.0",
3134
"react-pick-color": "^2.0.0",
35+
"remark-gfm": "4.0.1",
3236
"server-only": "^0.0.1",
3337
"shiki": "1.27.0",
38+
"sonner": "2.0.6",
3439
"tailwind-merge": "^2.6.0",
3540
"thirdweb": "workspace:*",
3641
"use-debounce": "^10.0.5",
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
import { stream } from "fetch-event-stream";
2+
import type { NebulaTxData, NebulaUserMessage } from "./types";
3+
4+
const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
5+
6+
export type NebulaContext = {
7+
chainIds: string[] | null;
8+
walletAddress: string | null;
9+
sessionId: string | null;
10+
};
11+
12+
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+
44+
export async function promptNebula(params: {
45+
message: NebulaUserMessage;
46+
handleStream: (res: ChatStreamedResponse) => void;
47+
abortController: AbortController;
48+
context: undefined | NebulaContext;
49+
}) {
50+
const body: Record<string, string | boolean | object> = {
51+
messages: [params.message],
52+
stream: true,
53+
};
54+
55+
if (params.context) {
56+
body.context = {
57+
chain_ids: params.context.chainIds?.map(Number) || [],
58+
session_id: params.context.sessionId ?? undefined,
59+
wallet_address: params.context.walletAddress,
60+
};
61+
}
62+
63+
const events = await stream(`${API_URL}/ai/chat`, {
64+
body: JSON.stringify(body),
65+
headers: {
66+
"x-client-id": process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID!,
67+
"Content-Type": "application/json",
68+
},
69+
method: "POST",
70+
signal: params.abortController.signal,
71+
});
72+
73+
for await (const _event of events) {
74+
if (!_event.data) {
75+
continue;
76+
}
77+
78+
const event = _event as ChatStreamedEvent;
79+
80+
switch (event.event) {
81+
case "delta": {
82+
params.handleStream({
83+
data: {
84+
v: JSON.parse(event.data).v,
85+
},
86+
event: "delta",
87+
});
88+
break;
89+
}
90+
91+
case "presence": {
92+
params.handleStream({
93+
data: JSON.parse(event.data),
94+
event: "presence",
95+
});
96+
break;
97+
}
98+
99+
case "image": {
100+
const data = JSON.parse(event.data) as {
101+
data: {
102+
width: number;
103+
height: number;
104+
url: string;
105+
};
106+
request_id: string;
107+
};
108+
109+
params.handleStream({
110+
data: data.data,
111+
event: "image",
112+
request_id: data.request_id,
113+
});
114+
break;
115+
}
116+
117+
case "action": {
118+
const data = JSON.parse(event.data);
119+
120+
if (data.type === "sign_transaction") {
121+
try {
122+
const parsedTxData = JSON.parse(data.data) as NebulaTxData;
123+
params.handleStream({
124+
data: parsedTxData,
125+
event: "action",
126+
request_id: data.request_id,
127+
type: "sign_transaction",
128+
});
129+
} catch (e) {
130+
console.error("failed to parse action data", e, { event });
131+
}
132+
}
133+
134+
if (data.type === "sign_swap") {
135+
try {
136+
const swapData = JSON.parse(data.data) as NebulaSwapData;
137+
params.handleStream({
138+
data: swapData,
139+
event: "action",
140+
request_id: data.request_id,
141+
type: "sign_swap",
142+
});
143+
} catch (e) {
144+
console.error("failed to parse action data", e, { event });
145+
}
146+
}
147+
148+
break;
149+
}
150+
151+
case "error": {
152+
const data = JSON.parse(event.data) as {
153+
code: number;
154+
error: {
155+
message: string;
156+
};
157+
};
158+
159+
params.handleStream({
160+
data: {
161+
code: data.code,
162+
errorMessage: data.error.message,
163+
},
164+
event: "error",
165+
});
166+
break;
167+
}
168+
169+
case "init": {
170+
const data = JSON.parse(event.data);
171+
params.handleStream({
172+
data: {
173+
request_id: data.request_id,
174+
session_id: data.session_id,
175+
},
176+
event: "init",
177+
});
178+
break;
179+
}
180+
181+
case "context": {
182+
const data = JSON.parse(event.data) as {
183+
data: string;
184+
request_id: string;
185+
session_id: string;
186+
};
187+
188+
const contextData = JSON.parse(data.data) as {
189+
wallet_address: string;
190+
chain_ids: number[];
191+
session_id: string;
192+
};
193+
194+
params.handleStream({
195+
data: contextData,
196+
event: "context",
197+
});
198+
break;
199+
}
200+
201+
case "ping": {
202+
break;
203+
}
204+
205+
default: {
206+
console.warn("unhandled event", event);
207+
}
208+
}
209+
}
210+
}
211+
212+
type ChatStreamedResponse =
213+
| {
214+
event: "init";
215+
data: {
216+
session_id: string;
217+
request_id: string;
218+
};
219+
}
220+
| {
221+
event: "presence";
222+
data: {
223+
session_id: string;
224+
request_id: string;
225+
source: "user" | "reviewer" | (string & {});
226+
data: string;
227+
};
228+
}
229+
| {
230+
event: "delta";
231+
data: {
232+
v: string;
233+
};
234+
}
235+
| {
236+
event: "action";
237+
type: "sign_transaction";
238+
data: NebulaTxData;
239+
request_id: string;
240+
}
241+
| {
242+
event: "action";
243+
type: "sign_swap";
244+
data: NebulaSwapData;
245+
request_id: string;
246+
}
247+
| {
248+
event: "image";
249+
data: {
250+
width: number;
251+
height: number;
252+
url: string;
253+
};
254+
request_id: string;
255+
}
256+
| {
257+
event: "context";
258+
data: {
259+
wallet_address: string;
260+
chain_ids: number[];
261+
session_id: string;
262+
};
263+
}
264+
| {
265+
event: "error";
266+
data: {
267+
code: number;
268+
errorMessage: string;
269+
};
270+
};
271+
272+
type ChatStreamedEvent =
273+
| {
274+
event: "init";
275+
data: string;
276+
}
277+
| {
278+
event: "presence";
279+
data: string;
280+
}
281+
| {
282+
event: "delta";
283+
data: string;
284+
}
285+
| {
286+
event: "image";
287+
data: string;
288+
}
289+
| {
290+
event: "action";
291+
type: "sign_transaction" | "sign_swap";
292+
data: string;
293+
}
294+
| {
295+
event: "context";
296+
data: string;
297+
}
298+
| {
299+
event: "error";
300+
data: string;
301+
}
302+
| {
303+
event: "ping";
304+
data: string;
305+
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
type SessionContextFilter = {
2+
chain_ids: string[] | null;
3+
wallet_address: string | null;
4+
};
5+
6+
type NebulaUserMessageContentItem =
7+
| {
8+
type: "image";
9+
image_url: string | null;
10+
b64: string | null;
11+
}
12+
| {
13+
type: "text";
14+
text: string;
15+
}
16+
| {
17+
type: "transaction";
18+
transaction_hash: string;
19+
chain_id: number;
20+
};
21+
22+
type NebulaUserMessageContent = NebulaUserMessageContentItem[];
23+
24+
export type NebulaUserMessage = {
25+
role: "user";
26+
content: NebulaUserMessageContent;
27+
};
28+
29+
export type NebulaSessionHistoryMessage =
30+
| {
31+
role: "assistant" | "action" | "image";
32+
content: string;
33+
timestamp: number;
34+
}
35+
| {
36+
role: "user";
37+
content: NebulaUserMessageContent | string;
38+
};
39+
40+
export type NebulaTxData = {
41+
chainId: number;
42+
data: `0x${string}`;
43+
to: string;
44+
value?: string;
45+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import ThirdwebProvider from "../../../components/thirdweb-provider";
2+
import { THIRDWEB_CLIENT } from "../../../lib/client";
3+
import { ChatPageContent } from "../components/ChatPageContent";
4+
5+
export default function ChatPage() {
6+
return (
7+
<div className="min-h-screen">
8+
<ThirdwebProvider>
9+
<ChatPageContent client={THIRDWEB_CLIENT} type="new-chat" />
10+
</ThirdwebProvider>
11+
</div>
12+
);
13+
}
14+
15+
export const metadata = {
16+
title: "AI Chat API - Playground",
17+
description: "Chat with thirdweb AI for blockchain interactions",
18+
};

0 commit comments

Comments
 (0)