Skip to content

Commit 87aa725

Browse files
thirdweb AI inside dashboard
1 parent e37bd8e commit 87aa725

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+6387
-1
lines changed

apps/dashboard/src/@/components/chat/ChatBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export function ChatBar(props: {
8787
<PopoverContent className="w-72">
8888
<div>
8989
<p className="mb-3 text-muted-foreground text-sm">
90-
Get access to image uploads by signing in to Nebula
90+
Get access to image uploads by signing in to thirdweb
9191
</p>
9292
<Button
9393
className="w-full"
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"use client";
2+
3+
import type React from "react";
4+
import { useRef } from "react";
5+
import { Button } from "@/components/ui/button";
6+
7+
interface ImageUploadProps {
8+
value: File | undefined;
9+
onChange?: (files: File[]) => void;
10+
children?: React.ReactNode;
11+
variant?: React.ComponentProps<typeof Button>["variant"];
12+
className?: string;
13+
multiple?: boolean;
14+
accept: string;
15+
}
16+
17+
export function ImageUploadButton(props: ImageUploadProps) {
18+
const fileInputRef = useRef<HTMLInputElement>(null);
19+
20+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
21+
const files = Array.from(e.target.files || []);
22+
props.onChange?.(files);
23+
};
24+
25+
return (
26+
<div>
27+
<Button
28+
className={props.className}
29+
onClick={() => fileInputRef.current?.click()}
30+
variant={props.variant}
31+
>
32+
{props.children}
33+
</Button>
34+
<input
35+
accept={props.accept}
36+
aria-label="Upload image"
37+
className="hidden"
38+
multiple={props.multiple}
39+
onChange={handleFileChange}
40+
ref={fileInputRef}
41+
type="file"
42+
/>
43+
</div>
44+
);
45+
}

apps/dashboard/src/@/constants/public-envs.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,6 @@ export const NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET =
2929

3030
export const NEXT_PUBLIC_DEMO_ENGINE_URL =
3131
process.env.NEXT_PUBLIC_DEMO_ENGINE_URL || "";
32+
33+
export const NEXT_PUBLIC_THIRDWEB_AI_HOST =
34+
process.env.NEXT_PUBLIC_THIRDWEB_AI_HOST || "https://nebula-api.thirdweb.com";

apps/dashboard/src/@/storybook/stubs.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,32 @@ export function newAccountStub(overrides?: Partial<Account>): Account {
279279
...overrides,
280280
};
281281
}
282+
283+
export function randomLorem(length: number) {
284+
const loremWords = [
285+
"lorem",
286+
"ipsum",
287+
"dolor",
288+
"sit",
289+
"amet",
290+
"consectetur",
291+
"adipiscing",
292+
"elit",
293+
"sed",
294+
"do",
295+
"eiusmod",
296+
"tempor",
297+
"incididunt",
298+
"ut",
299+
"labore",
300+
"et",
301+
"dolore",
302+
"magna",
303+
"aliqua",
304+
];
305+
306+
return Array.from({ length }, () => {
307+
const randomIndex = Math.floor(Math.random() * loremWords.length);
308+
return loremWords[randomIndex];
309+
}).join(" ");
310+
}
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import { stream } from "fetch-event-stream";
2+
import type { Project } from "@/api/project/projects";
3+
import { NEXT_PUBLIC_THIRDWEB_AI_HOST } from "@/constants/public-envs";
4+
import type {
5+
NebulaContext,
6+
NebulaSwapData,
7+
NebulaTxData,
8+
NebulaUserMessage,
9+
} from "./types";
10+
11+
export async function promptNebula(params: {
12+
message: NebulaUserMessage;
13+
sessionId: string;
14+
authToken: string;
15+
handleStream: (res: ChatStreamedResponse) => void;
16+
abortController: AbortController;
17+
context: undefined | NebulaContext;
18+
project: Project;
19+
}) {
20+
const body: Record<string, string | boolean | object> = {
21+
messages: [params.message],
22+
session_id: params.sessionId,
23+
stream: true,
24+
};
25+
26+
if (params.context) {
27+
body.context = {
28+
chain_ids: params.context.chainIds || [],
29+
networks: params.context.networks,
30+
wallet_address: params.context.walletAddress,
31+
};
32+
}
33+
34+
const events = await stream(`${NEXT_PUBLIC_THIRDWEB_AI_HOST}/chat`, {
35+
body: JSON.stringify(body),
36+
headers: {
37+
Authorization: `Bearer ${params.authToken}`,
38+
"x-team-id": params.project.teamId,
39+
"x-client-id": params.project.publishableKey,
40+
"Content-Type": "application/json",
41+
},
42+
method: "POST",
43+
signal: params.abortController.signal,
44+
});
45+
46+
for await (const _event of events) {
47+
if (!_event.data) {
48+
continue;
49+
}
50+
51+
const event = _event as ChatStreamedEvent;
52+
53+
switch (event.event) {
54+
case "delta": {
55+
params.handleStream({
56+
data: {
57+
v: JSON.parse(event.data).v,
58+
},
59+
event: "delta",
60+
});
61+
break;
62+
}
63+
64+
case "presence": {
65+
params.handleStream({
66+
data: JSON.parse(event.data),
67+
event: "presence",
68+
});
69+
break;
70+
}
71+
72+
case "image": {
73+
const data = JSON.parse(event.data) as {
74+
data: {
75+
width: number;
76+
height: number;
77+
url: string;
78+
};
79+
request_id: string;
80+
};
81+
82+
params.handleStream({
83+
data: data.data,
84+
event: "image",
85+
request_id: data.request_id,
86+
});
87+
break;
88+
}
89+
90+
case "action": {
91+
const data = JSON.parse(event.data);
92+
93+
if (data.type === "sign_transaction") {
94+
try {
95+
const parsedTxData = data.data as NebulaTxData;
96+
params.handleStream({
97+
data: parsedTxData,
98+
event: "action",
99+
request_id: data.request_id,
100+
type: "sign_transaction",
101+
});
102+
} catch (e) {
103+
console.error("failed to parse action data", e, { event });
104+
}
105+
}
106+
107+
if (data.type === "sign_swap") {
108+
try {
109+
const swapData = data.data as NebulaSwapData;
110+
params.handleStream({
111+
data: swapData,
112+
event: "action",
113+
request_id: data.request_id,
114+
type: "sign_swap",
115+
});
116+
} catch (e) {
117+
console.error("failed to parse action data", e, { event });
118+
}
119+
}
120+
121+
break;
122+
}
123+
124+
case "error": {
125+
const data = JSON.parse(event.data) as {
126+
code: number;
127+
error: {
128+
message: string;
129+
};
130+
};
131+
132+
params.handleStream({
133+
data: {
134+
code: data.code,
135+
errorMessage: data.error.message,
136+
},
137+
event: "error",
138+
});
139+
break;
140+
}
141+
142+
case "init": {
143+
const data = JSON.parse(event.data);
144+
params.handleStream({
145+
data: {
146+
request_id: data.request_id,
147+
session_id: data.session_id,
148+
},
149+
event: "init",
150+
});
151+
break;
152+
}
153+
154+
case "context": {
155+
const data = JSON.parse(event.data) as {
156+
data: string;
157+
request_id: string;
158+
session_id: string;
159+
};
160+
161+
const contextData = JSON.parse(data.data) as {
162+
wallet_address: string;
163+
chain_ids: number[];
164+
networks: NebulaContext["networks"];
165+
};
166+
167+
params.handleStream({
168+
data: contextData,
169+
event: "context",
170+
});
171+
break;
172+
}
173+
174+
case "ping": {
175+
break;
176+
}
177+
178+
default: {
179+
console.warn("unhandled event", event);
180+
}
181+
}
182+
}
183+
}
184+
185+
type ChatStreamedResponse =
186+
| {
187+
event: "init";
188+
data: {
189+
session_id: string;
190+
request_id: string;
191+
};
192+
}
193+
| {
194+
event: "presence";
195+
data: {
196+
session_id: string;
197+
request_id: string;
198+
source: "user" | "reviewer" | (string & {});
199+
data: string;
200+
};
201+
}
202+
| {
203+
event: "delta";
204+
data: {
205+
v: string;
206+
};
207+
}
208+
| {
209+
event: "action";
210+
type: "sign_transaction";
211+
data: NebulaTxData;
212+
request_id: string;
213+
}
214+
| {
215+
event: "action";
216+
type: "sign_swap";
217+
data: NebulaSwapData;
218+
request_id: string;
219+
}
220+
| {
221+
event: "image";
222+
data: {
223+
width: number;
224+
height: number;
225+
url: string;
226+
};
227+
request_id: string;
228+
}
229+
| {
230+
event: "context";
231+
data: {
232+
wallet_address: string;
233+
chain_ids: number[];
234+
networks: NebulaContext["networks"];
235+
};
236+
}
237+
| {
238+
event: "error";
239+
data: {
240+
code: number;
241+
errorMessage: string;
242+
};
243+
};
244+
245+
type ChatStreamedEvent =
246+
| {
247+
event: "init";
248+
data: string;
249+
}
250+
| {
251+
event: "presence";
252+
data: string;
253+
}
254+
| {
255+
event: "delta";
256+
data: string;
257+
}
258+
| {
259+
event: "image";
260+
data: string;
261+
}
262+
| {
263+
event: "action";
264+
type: "sign_transaction" | "sign_swap";
265+
data: string;
266+
}
267+
| {
268+
event: "context";
269+
data: string;
270+
}
271+
| {
272+
event: "error";
273+
data: string;
274+
}
275+
| {
276+
event: "ping";
277+
data: string;
278+
};

0 commit comments

Comments
 (0)