Skip to content

Commit 1e918d0

Browse files
committed
siwa chat widget
1 parent 526b6b5 commit 1e918d0

File tree

4 files changed

+532
-8
lines changed

4 files changed

+532
-8
lines changed

apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import contractsIcon from "../../../../../public/assets/support/contracts.png";
1010
import engineIcon from "../../../../../public/assets/support/engine.png";
1111
import miscIcon from "../../../../../public/assets/support/misc.svg";
1212
import connectIcon from "../../../../../public/assets/support/wallets.png";
13-
import { NebulaChatButton } from "../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
13+
import { CustomChatButton } from "../../../nebula-app/(app)/components/CustomChat/CustomChatButton";
1414
import {
1515
getAuthToken,
1616
getAuthTokenWalletAddress,
@@ -129,8 +129,7 @@ export default async function SupportPage() {
129129
teamId: undefined,
130130
});
131131

132-
const supportPromptPrefix =
133-
"You are a Customer Success Agent at thirdweb, assisting customers with blockchain and Web3-related issues. Use the following details to craft a professional, empathetic response: ";
132+
const supportPromptPrefix ="";
134133
const examplePrompts = [
135134
"ERC20 - Transfer Amount Exceeds Allowance",
136135
"Replacement transaction underpriced / Replacement fee too low",
@@ -157,14 +156,14 @@ export default async function SupportPage() {
157156
team.
158157
</p>
159158
<div className="mt-6 flex w-full flex-col items-center gap-3">
160-
<NebulaChatButton
159+
<CustomChatButton
161160
isLoggedIn={!!accountAddress}
162161
networks="all"
163162
isFloating={false}
164163
pageType="support"
165-
label="Ask Nebula AI for support"
164+
label="Ask Siwa AI for support"
166165
client={client}
167-
nebulaParams={{
166+
customApiParams={{
168167
messagePrefix: supportPromptPrefix,
169168
chainIds: [],
170169
wallet: accountAddress ?? undefined,
@@ -173,6 +172,7 @@ export default async function SupportPage() {
173172
title: prompt,
174173
message: prompt,
175174
}))}
175+
authToken={authToken || undefined}
176176
/>
177177

178178
<Link

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

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
22
import { cn } from "@/lib/utils";
33
import { MarkdownRenderer } from "components/contract-components/published-contract/markdown-renderer";
4-
import { AlertCircleIcon } from "lucide-react";
5-
import { useEffect, useRef } from "react";
4+
import { AlertCircleIcon, ThumbsUpIcon, ThumbsDownIcon } from "lucide-react";
5+
import { useEffect, useRef, useState } from "react";
66
import type { ThirdwebClient } from "thirdweb";
77
import type { NebulaSwapData } from "../api/chat";
88
import type { NebulaUserMessage, NebulaUserMessageContent } from "../api/types";
@@ -224,6 +224,40 @@ function RenderMessage(props: {
224224
);
225225
}
226226

227+
// Feedback for assistant messages
228+
if (props.message.type === "assistant") {
229+
return (
230+
<div className="flex flex-col gap-2">
231+
<div className="flex gap-3">
232+
{/* Left Icon */}
233+
<div className="-translate-y-[2px] relative shrink-0">
234+
<div className="flex size-9 items-center justify-center rounded-full border bg-card">
235+
<NebulaIcon className="size-5 text-muted-foreground" />
236+
</div>
237+
</div>
238+
{/* Right Message */}
239+
<div className="min-w-0 grow">
240+
<ScrollShadow className="rounded-lg">
241+
<RenderResponse
242+
message={message}
243+
isMessagePending={props.isMessagePending}
244+
client={props.client}
245+
sendMessage={props.sendMessage}
246+
nextMessage={props.nextMessage}
247+
sessionId={props.sessionId}
248+
authToken={props.authToken}
249+
/>
250+
</ScrollShadow>
251+
<FeedbackButtons
252+
sessionId={props.sessionId}
253+
messageText={message.type === "assistant" ? message.text : ""}
254+
/>
255+
</div>
256+
</div>
257+
</div>
258+
);
259+
}
260+
227261
return (
228262
<div className="flex gap-3">
229263
{/* Left Icon */}
@@ -422,3 +456,55 @@ function StyledMarkdownRenderer(props: {
422456
/>
423457
);
424458
}
459+
460+
function FeedbackButtons({ sessionId, messageText }: { sessionId: string | undefined; messageText: string }) {
461+
const [feedback, setFeedback] = useState<"good" | "bad" | null>(null);
462+
const [loading, setLoading] = useState(false);
463+
const [thankYou, setThankYou] = useState(false);
464+
465+
async function sendFeedback(rating: "good" | "bad") {
466+
setLoading(true);
467+
try {
468+
await fetch("https://siwa-api.thirdweb-dev.com/v1/feedback", {
469+
method: "POST",
470+
headers: { "Content-Type": "application/json" },
471+
body: JSON.stringify({
472+
conversationId: sessionId,
473+
message: messageText,
474+
rating,
475+
}),
476+
});
477+
setFeedback(rating);
478+
setThankYou(true);
479+
} catch (e) {
480+
// handle error
481+
} finally {
482+
setLoading(false);
483+
}
484+
}
485+
486+
if (thankYou) {
487+
return <div className="mt-2 text-xs text-muted-foreground">Thank you for your feedback!</div>;
488+
}
489+
490+
return (
491+
<div className="flex gap-2 mt-2">
492+
<button
493+
className="p-1 rounded-full border hover:bg-muted-foreground/10"
494+
onClick={() => sendFeedback("good")}
495+
disabled={loading}
496+
aria-label="Thumbs up"
497+
>
498+
<ThumbsUpIcon className="size-4 text-green-500" />
499+
</button>
500+
<button
501+
className="p-1 rounded-full border hover:bg-muted-foreground/10"
502+
onClick={() => sendFeedback("bad")}
503+
disabled={loading}
504+
aria-label="Thumbs down"
505+
>
506+
<ThumbsDownIcon className="size-4 text-red-500" />
507+
</button>
508+
</div>
509+
);
510+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"use client";
2+
3+
import CustomChatContent from "./CustomChatContent";
4+
import type { ExamplePrompt } from "../../data/examplePrompts";
5+
import type { ThirdwebClient } from "thirdweb";
6+
import { useState, useCallback, useRef } from "react";
7+
import { Button } from "@/components/ui/button";
8+
import { XIcon, MessageCircleIcon } from "lucide-react";
9+
import { cn } from "@/lib/utils";
10+
11+
export function CustomChatButton(props: {
12+
isLoggedIn: boolean;
13+
networks: "mainnet" | "testnet" | "all" | null;
14+
isFloating: boolean;
15+
pageType: "chain" | "contract" | "support";
16+
label: string;
17+
client: ThirdwebClient;
18+
customApiParams: any;
19+
examplePrompts: ExamplePrompt[];
20+
authToken: string | undefined;
21+
requireLogin?: boolean;
22+
}) {
23+
const [isOpen, setIsOpen] = useState(false);
24+
const [hasBeenOpened, setHasBeenOpened] = useState(false);
25+
const [isDismissed, setIsDismissed] = useState(false);
26+
const closeModal = useCallback(() => setIsOpen(false), []);
27+
const ref = useRef<HTMLDivElement>(null);
28+
29+
// Close on outside click
30+
// (optional: can add if you want exact Nebula behavior)
31+
// useEffect(() => { ... }, [onOutsideClick]);
32+
33+
if (isDismissed) {
34+
return null;
35+
}
36+
37+
return (
38+
<>
39+
{/* Inline Button (not floating) */}
40+
<Button
41+
onClick={() => {
42+
setIsOpen(true);
43+
setHasBeenOpened(true);
44+
}}
45+
variant="default"
46+
className="gap-2 rounded-full shadow-lg"
47+
>
48+
<MessageCircleIcon className="size-4" />
49+
{props.label}
50+
</Button>
51+
52+
{/* Popup/Modal */}
53+
<div
54+
className={cn(
55+
"slide-in-from-bottom-20 zoom-in-95 fade-in-0 fixed bottom-0 left-0 z-50 flex h-[80vh] w-[100vw] animate-in flex-col overflow-hidden rounded-t-2xl border bg-background shadow-2xl duration-200 lg:right-6 lg:bottom-6 lg:left-auto lg:h-[80vh] lg:max-w-xl lg:rounded-xl",
56+
!isOpen && "hidden"
57+
)}
58+
ref={ref}
59+
>
60+
{/* Header with close button */}
61+
<div className="flex items-center justify-between border-b px-4 py-2">
62+
<div className="font-semibold text-lg flex items-center gap-2">
63+
<MessageCircleIcon className="size-5 text-muted-foreground" />
64+
{props.label}
65+
</div>
66+
<Button
67+
variant="ghost"
68+
size="icon"
69+
onClick={closeModal}
70+
className="h-auto w-auto p-1 text-muted-foreground"
71+
aria-label="Close chat"
72+
>
73+
<XIcon className="size-5" />
74+
</Button>
75+
</div>
76+
{/* Chat Content */}
77+
<div className="relative flex grow flex-col overflow-hidden">
78+
{hasBeenOpened && isOpen && (
79+
<CustomChatContent
80+
authToken={props.authToken}
81+
client={props.client}
82+
examplePrompts={props.examplePrompts}
83+
pageType={props.pageType}
84+
customApiParams={props.customApiParams}
85+
networks={props.networks}
86+
requireLogin={props.requireLogin}
87+
/>
88+
)}
89+
</div>
90+
</div>
91+
</>
92+
);
93+
}

0 commit comments

Comments
 (0)