Skip to content

Commit 37cda56

Browse files
Handle already connected wallets in 1193 provider
1 parent 44e6e11 commit 37cda56

File tree

30 files changed

+3447
-171
lines changed

30 files changed

+3447
-171
lines changed

.changeset/many-pants-tease.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Handle already connected wallets in 1193 provider

apps/dashboard/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"remark-gfm": "4.0.1",
6161
"responsive-rsc": "0.0.7",
6262
"server-only": "^0.0.1",
63-
"shiki": "1.27.0",
63+
"shiki": "3.12.0",
6464
"sonner": "2.0.6",
6565
"spdx-correct": "^3.2.0",
6666
"stripe": "17.7.0",

apps/nebula/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"react-markdown": "10.1.0",
3333
"remark-gfm": "4.0.1",
3434
"server-only": "^0.0.1",
35-
"shiki": "1.27.0",
35+
"shiki": "3.12.0",
3636
"sonner": "2.0.6",
3737
"tailwind-merge": "^2.6.0",
3838
"tailwindcss-animate": "^1.0.7",

apps/playground-web/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
{
22
"dependencies": {
33
"@abstract-foundation/agw-react": "^1.6.4",
4+
"@ai-sdk/react": "^2.0.25",
45
"@hookform/resolvers": "^3.9.1",
6+
"@radix-ui/react-avatar": "^1.1.10",
57
"@radix-ui/react-checkbox": "^1.3.2",
68
"@radix-ui/react-collapsible": "^1.1.11",
79
"@radix-ui/react-dialog": "1.1.14",
@@ -14,8 +16,11 @@
1416
"@radix-ui/react-slot": "^1.2.3",
1517
"@radix-ui/react-switch": "^1.2.5",
1618
"@radix-ui/react-tooltip": "1.2.7",
19+
"@radix-ui/react-use-controllable-state": "^1.2.2",
1720
"@tanstack/react-query": "5.81.5",
21+
"@thirdweb-dev/ai-sdk-provider": "workspace:*",
1822
"@workspace/ui": "workspace:*",
23+
"ai": "^5.0.25",
1924
"class-variance-authority": "^0.7.1",
2025
"clsx": "^2.1.1",
2126
"date-fns": "4.1.0",
@@ -35,11 +40,13 @@
3540
"react-pick-color": "^2.0.0",
3641
"remark-gfm": "4.0.1",
3742
"server-only": "^0.0.1",
38-
"shiki": "1.27.0",
43+
"shiki": "3.12.0",
3944
"sonner": "2.0.6",
45+
"streamdown": "^1.1.4",
4046
"tailwind-merge": "^2.6.0",
4147
"thirdweb": "workspace:*",
4248
"use-debounce": "^10.0.5",
49+
"use-stick-to-bottom": "^1.1.1",
4350
"zod": "3.25.75"
4451
},
4552
"devDependencies": {
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
"use client";
2+
3+
import { useChat } from "@ai-sdk/react";
4+
import type { ThirdwebAiMessage } from "@thirdweb-dev/ai-sdk-provider";
5+
import { DefaultChatTransport } from "ai";
6+
import { useMemo, useState } from "react";
7+
import { defineChain, prepareTransaction } from "thirdweb";
8+
import {
9+
ConnectButton,
10+
TransactionButton,
11+
useActiveAccount,
12+
} from "thirdweb/react";
13+
import {
14+
Conversation,
15+
ConversationContent,
16+
ConversationScrollButton,
17+
} from "@/components/conversation";
18+
import { Message, MessageContent } from "@/components/message";
19+
import {
20+
PromptInput,
21+
PromptInputSubmit,
22+
PromptInputTextarea,
23+
} from "@/components/prompt-input";
24+
import {
25+
Reasoning,
26+
ReasoningContent,
27+
ReasoningTrigger,
28+
} from "@/components/reasoning";
29+
import { Response } from "@/components/response";
30+
import { Loader } from "../../../../components/loader";
31+
import { THIRDWEB_CLIENT } from "../../../../lib/client";
32+
33+
export function ChatContainer() {
34+
const [sessionId, setSessionId] = useState("");
35+
36+
const { messages, sendMessage, status, addToolResult } =
37+
useChat<ThirdwebAiMessage>({
38+
transport: new DefaultChatTransport({
39+
api: "/api/chat",
40+
}),
41+
onFinish: ({ message }) => {
42+
setSessionId(message.metadata?.session_id ?? "");
43+
},
44+
});
45+
const [input, setInput] = useState("");
46+
47+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
48+
e.preventDefault();
49+
if (input.trim()) {
50+
sendMessage(
51+
{ text: input },
52+
{
53+
body: {
54+
sessionId,
55+
},
56+
},
57+
);
58+
setInput("");
59+
}
60+
};
61+
62+
return (
63+
<div className="max-w-4xl mx-auto p-6 relative size-full">
64+
<div className="flex flex-col h-[600px] rounded-lg border">
65+
<Conversation>
66+
<ConversationContent>
67+
{messages.length === 0 && (
68+
<div className="h-[300px] flex items-center justify-center text-center text-muted-foreground">
69+
Type a message to start the conversation
70+
</div>
71+
)}
72+
{messages.map((message) => (
73+
<Message from={message.role} key={message.id}>
74+
<MessageContent>
75+
{message.parts.map((part, i) => {
76+
switch (part.type) {
77+
case "text":
78+
return (
79+
<Response key={`${message.id}-${i}`}>
80+
{part.text}
81+
</Response>
82+
);
83+
case "reasoning":
84+
return (
85+
<Reasoning
86+
key={`${message.id}-reasoning-${i}`}
87+
className="w-full"
88+
isStreaming={status === "streaming"}
89+
>
90+
<ReasoningTrigger />
91+
<ReasoningContent className="text-xs text-muted-foreground italic flex flex-col gap-2">
92+
{part.text}
93+
</ReasoningContent>
94+
</Reasoning>
95+
);
96+
case "tool-sign_transaction":
97+
return (
98+
<SignTransactionButton
99+
key={`${message.id}-transaction-${i}`}
100+
input={part.input}
101+
addToolResult={addToolResult}
102+
sendMessage={sendMessage}
103+
toolCallId={part.toolCallId}
104+
sessionId={sessionId}
105+
/>
106+
);
107+
case "tool-sign_swap":
108+
console.log("---sign_swap", part);
109+
return (
110+
<SignSwapButton
111+
key={`${message.id}-swap-${i}`}
112+
input={part.input}
113+
addToolResult={addToolResult}
114+
sendMessage={sendMessage}
115+
toolCallId={part.toolCallId}
116+
sessionId={sessionId}
117+
/>
118+
);
119+
default:
120+
return null;
121+
}
122+
})}
123+
</MessageContent>
124+
</Message>
125+
))}
126+
{status === "submitted" && <Loader />}
127+
</ConversationContent>
128+
<ConversationScrollButton />
129+
</Conversation>
130+
131+
<PromptInput
132+
onSubmit={handleSubmit}
133+
className="mt-4 w-full max-w-2xl mx-auto relative"
134+
>
135+
<PromptInputTextarea
136+
value={input}
137+
placeholder="Say something..."
138+
onChange={(e) => setInput(e.currentTarget.value)}
139+
className="pr-12"
140+
/>
141+
<PromptInputSubmit
142+
status={status === "streaming" ? "streaming" : "ready"}
143+
disabled={!input.trim()}
144+
className="absolute top-4 right-4"
145+
/>
146+
</PromptInput>
147+
</div>
148+
</div>
149+
);
150+
}
151+
152+
type SignTransactionButtonProps = {
153+
input:
154+
| Extract<
155+
ReturnType<
156+
typeof useChat<ThirdwebAiMessage>
157+
>["messages"][number]["parts"][number],
158+
{ type: "tool-sign_transaction" }
159+
>["input"]
160+
| undefined;
161+
addToolResult: ReturnType<typeof useChat<ThirdwebAiMessage>>["addToolResult"];
162+
toolCallId: string;
163+
sendMessage: ReturnType<typeof useChat<ThirdwebAiMessage>>["sendMessage"];
164+
sessionId: string;
165+
};
166+
167+
const SignTransactionButton = (props: SignTransactionButtonProps) => {
168+
const { input, addToolResult, toolCallId, sendMessage, sessionId } = props;
169+
const transactionData: {
170+
chain_id: number;
171+
to: string;
172+
data: `0x${string}`;
173+
value: bigint;
174+
} = useMemo(() => {
175+
return {
176+
chain_id: input?.chain_id || 8453,
177+
to: input?.to || "",
178+
data: (input?.data as `0x${string}`) || "0x",
179+
value: input?.value ? BigInt(input.value) : BigInt(0),
180+
};
181+
}, [input]);
182+
const account = useActiveAccount();
183+
184+
if (!account) {
185+
return <ConnectButton client={THIRDWEB_CLIENT} />;
186+
}
187+
188+
return (
189+
<div className="py-4">
190+
<TransactionButton
191+
style={{
192+
width: "100%",
193+
}}
194+
transaction={() =>
195+
prepareTransaction({
196+
client: THIRDWEB_CLIENT,
197+
chain: defineChain(transactionData.chain_id),
198+
to: transactionData.to,
199+
data: transactionData.data,
200+
value: transactionData.value,
201+
})
202+
}
203+
onTransactionSent={(transaction) => {
204+
addToolResult({
205+
tool: "sign_transaction",
206+
toolCallId,
207+
output: {
208+
transaction_hash: transaction.transactionHash,
209+
chain_id: transaction.chain.id,
210+
},
211+
});
212+
sendMessage(undefined, {
213+
body: {
214+
sessionId,
215+
},
216+
});
217+
}}
218+
onError={(error) => {
219+
sendMessage(
220+
{ text: `Transaction failed: ${error.message}` },
221+
{
222+
body: {
223+
sessionId,
224+
},
225+
},
226+
);
227+
}}
228+
>
229+
Sign Transaction
230+
</TransactionButton>
231+
</div>
232+
);
233+
};
234+
235+
type SignSwapButtonProps = {
236+
input:
237+
| Extract<
238+
ReturnType<
239+
typeof useChat<ThirdwebAiMessage>
240+
>["messages"][number]["parts"][number],
241+
{ type: "tool-sign_swap" }
242+
>["input"]
243+
| undefined;
244+
addToolResult: ReturnType<typeof useChat<ThirdwebAiMessage>>["addToolResult"];
245+
toolCallId: string;
246+
sendMessage: ReturnType<typeof useChat<ThirdwebAiMessage>>["sendMessage"];
247+
sessionId: string;
248+
};
249+
const SignSwapButton = (props: SignSwapButtonProps) => {
250+
const { input, addToolResult, toolCallId, sendMessage, sessionId } = props;
251+
const transactionData: {
252+
chain_id: number;
253+
to: string;
254+
data: `0x${string}`;
255+
value: bigint;
256+
} = useMemo(() => {
257+
return {
258+
chain_id: input?.transaction?.chain_id || 8453,
259+
to: input?.transaction?.to || "",
260+
data: (input?.transaction?.data as `0x${string}`) || "0x",
261+
value: input?.transaction?.value
262+
? BigInt(input.transaction.value)
263+
: BigInt(0),
264+
};
265+
}, [input]);
266+
const account = useActiveAccount();
267+
268+
if (!account) {
269+
return <ConnectButton client={THIRDWEB_CLIENT} />;
270+
}
271+
272+
return (
273+
<div className="py-4">
274+
<TransactionButton
275+
style={{
276+
width: "100%",
277+
}}
278+
transaction={() =>
279+
prepareTransaction({
280+
client: THIRDWEB_CLIENT,
281+
chain: defineChain(transactionData.chain_id),
282+
to: transactionData.to,
283+
data: transactionData.data,
284+
value: transactionData.value,
285+
})
286+
}
287+
onTransactionSent={(transaction) => {
288+
addToolResult({
289+
tool: "sign_swap",
290+
toolCallId,
291+
output: {
292+
transaction_hash: transaction.transactionHash,
293+
chain_id: transaction.chain.id,
294+
},
295+
});
296+
sendMessage(undefined, {
297+
body: {
298+
sessionId,
299+
},
300+
});
301+
}}
302+
onError={(error) => {
303+
sendMessage(
304+
{ text: `Transaction failed: ${error.message}` },
305+
{
306+
body: {
307+
sessionId,
308+
},
309+
},
310+
);
311+
}}
312+
>
313+
Sign swap
314+
</TransactionButton>
315+
</div>
316+
);
317+
};

0 commit comments

Comments
 (0)