From 1e918d069fe4531cff32191cc6c0286573a4eb6e Mon Sep 17 00:00:00 2001 From: Yash <67926590+Yash094@users.noreply.github.com> Date: Fri, 23 May 2025 00:13:12 +0530 Subject: [PATCH 1/7] siwa chat widget --- .../app/(app)/(dashboard)/support/page.tsx | 12 +- .../app/nebula-app/(app)/components/Chats.tsx | 90 ++++- .../CustomChat/CustomChatButton.tsx | 93 +++++ .../CustomChat/CustomChatContent.tsx | 345 ++++++++++++++++++ 4 files changed, 532 insertions(+), 8 deletions(-) create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx index d95e5d2032e..2872c01835d 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx @@ -10,7 +10,7 @@ import contractsIcon from "../../../../../public/assets/support/contracts.png"; import engineIcon from "../../../../../public/assets/support/engine.png"; import miscIcon from "../../../../../public/assets/support/misc.svg"; import connectIcon from "../../../../../public/assets/support/wallets.png"; -import { NebulaChatButton } from "../../../nebula-app/(app)/components/FloatingChat/FloatingChat"; +import { CustomChatButton } from "../../../nebula-app/(app)/components/CustomChat/CustomChatButton"; import { getAuthToken, getAuthTokenWalletAddress, @@ -129,8 +129,7 @@ export default async function SupportPage() { teamId: undefined, }); - const supportPromptPrefix = - "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: "; + const supportPromptPrefix =""; const examplePrompts = [ "ERC20 - Transfer Amount Exceeds Allowance", "Replacement transaction underpriced / Replacement fee too low", @@ -157,14 +156,14 @@ export default async function SupportPage() { team.

- +
+ {/* Left Icon */} +
+
+ +
+
+ {/* Right Message */} +
+ + + + +
+
+
+ ); + } + return (
{/* Left Icon */} @@ -422,3 +456,55 @@ function StyledMarkdownRenderer(props: { /> ); } + +function FeedbackButtons({ sessionId, messageText }: { sessionId: string | undefined; messageText: string }) { + const [feedback, setFeedback] = useState<"good" | "bad" | null>(null); + const [loading, setLoading] = useState(false); + const [thankYou, setThankYou] = useState(false); + + async function sendFeedback(rating: "good" | "bad") { + setLoading(true); + try { + await fetch("https://siwa-api.thirdweb-dev.com/v1/feedback", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + conversationId: sessionId, + message: messageText, + rating, + }), + }); + setFeedback(rating); + setThankYou(true); + } catch (e) { + // handle error + } finally { + setLoading(false); + } + } + + if (thankYou) { + return
Thank you for your feedback!
; + } + + return ( +
+ + +
+ ); +} diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx new file mode 100644 index 00000000000..099cf430226 --- /dev/null +++ b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx @@ -0,0 +1,93 @@ +"use client"; + +import CustomChatContent from "./CustomChatContent"; +import type { ExamplePrompt } from "../../data/examplePrompts"; +import type { ThirdwebClient } from "thirdweb"; +import { useState, useCallback, useRef } from "react"; +import { Button } from "@/components/ui/button"; +import { XIcon, MessageCircleIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; + +export function CustomChatButton(props: { + isLoggedIn: boolean; + networks: "mainnet" | "testnet" | "all" | null; + isFloating: boolean; + pageType: "chain" | "contract" | "support"; + label: string; + client: ThirdwebClient; + customApiParams: any; + examplePrompts: ExamplePrompt[]; + authToken: string | undefined; + requireLogin?: boolean; +}) { + const [isOpen, setIsOpen] = useState(false); + const [hasBeenOpened, setHasBeenOpened] = useState(false); + const [isDismissed, setIsDismissed] = useState(false); + const closeModal = useCallback(() => setIsOpen(false), []); + const ref = useRef(null); + + // Close on outside click + // (optional: can add if you want exact Nebula behavior) + // useEffect(() => { ... }, [onOutsideClick]); + + if (isDismissed) { + return null; + } + + return ( + <> + {/* Inline Button (not floating) */} + + + {/* Popup/Modal */} +
+ {/* Header with close button */} +
+
+ + {props.label} +
+ +
+ {/* Chat Content */} +
+ {hasBeenOpened && isOpen && ( + + )} +
+
+ + ); +} \ No newline at end of file diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx new file mode 100644 index 00000000000..e302ec0b0ff --- /dev/null +++ b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx @@ -0,0 +1,345 @@ +"use client"; +import { Button } from "@/components/ui/button"; +import { useTrack } from "hooks/analytics/useTrack"; +import { ArrowRightIcon, ArrowUpRightIcon } from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useCallback, useState } from "react"; +import type { ThirdwebClient } from "thirdweb"; +import { + useActiveWallet, + useActiveWalletConnectionStatus, +} from "thirdweb/react"; +import type { NebulaContext } from "../../api/chat"; +// import { createSession } from "../../api/session"; // REMOVE +import type { NebulaUserMessage } from "../../api/types"; +import type { ExamplePrompt } from "../../data/examplePrompts"; +import { NebulaIcon } from "../../icons/NebulaIcon"; +import { ChatBar } from "../ChatBar"; +import { Chats } from "../Chats"; +import type { ChatMessage } from "../Chats"; + +export default function CustomChatContent(props: { + authToken: string | undefined; + client: ThirdwebClient; + examplePrompts: ExamplePrompt[]; + pageType: "chain" | "contract" | "support"; + networks: NebulaContext["networks"]; + customApiParams: + | { + messagePrefix: string; + chainIds: number[]; + wallet: string | undefined; + } + | undefined; + requireLogin?: boolean; +}) { + if (props.requireLogin !== false && !props.authToken) { + return ; + } + + return ( + + ); +} + +function CustomChatContentLoggedIn(props: { + authToken: string; + client: ThirdwebClient; + pageType: "chain" | "contract" | "support"; + examplePrompts: ExamplePrompt[]; + networks: NebulaContext["networks"]; + customApiParams: + | { + messagePrefix: string; + chainIds: number[]; + wallet: string | undefined; + } + | undefined; +}) { + const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false); + const [messages, setMessages] = useState>([]); + // sessionId is initially undefined, will be set to conversationId from API after first response + const [sessionId, setSessionId] = useState(undefined); + const [chatAbortController, setChatAbortController] = useState< + AbortController | undefined + >(); + const trackEvent = useTrack(); + const [isChatStreaming, setIsChatStreaming] = useState(false); + const [enableAutoScroll, setEnableAutoScroll] = useState(false); + const connectionStatus = useActiveWalletConnectionStatus(); + const activeWallet = useActiveWallet(); + + const [contextFilters, setContextFilters] = useState< + NebulaContext | undefined + >(() => { + return { + chainIds: + props.customApiParams?.chainIds.map((chainId) => chainId.toString()) || + null, + walletAddress: props.customApiParams?.wallet || null, + networks: props.networks, + }; + }); + + const handleSendMessage = useCallback( + async (userMessage: NebulaUserMessage) => { + const abortController = new AbortController(); + setUserHasSubmittedMessage(true); + setIsChatStreaming(true); + setEnableAutoScroll(true); + + const textMessage = userMessage.content.find((x) => x.type === "text"); + + trackEvent({ + category: "floating_siwa", + action: "send", + label: "message", + message: textMessage?.text, + page: props.pageType, + sessionId: sessionId, + }); + + setMessages((prev) => [ + ...prev, + { + type: "user", + content: userMessage.content, + }, + // instant loading indicator feedback to user + { + type: "presence", + texts: [], + }, + ]); + + const messagePrefix = props.customApiParams?.messagePrefix; + + // if this is first message, set the message prefix + // deep clone `userMessage` to avoid mutating the original message, its a pretty small object so JSON.parse is fine + const messageToSend = JSON.parse( + JSON.stringify(userMessage), + ) as NebulaUserMessage; + + // if this is first message, set the message prefix + if (messagePrefix && !userHasSubmittedMessage) { + const textMessage = messageToSend.content.find( + (x) => x.type === "text", + ); + if (textMessage) { + textMessage.text = `${messagePrefix}\n\n${textMessage.text}`; + } + } + + try { + setChatAbortController(abortController); + // --- Custom API call --- + const payload: any = { + message: messageToSend.content.find((x) => x.type === "text")?.text ?? "", + authToken: props.authToken, + conversationId: "25000000005", + }; + if (sessionId) { + payload.conversationId = sessionId; + } + const response = await fetch( + "https://proxy.cors.sh/https://siwa-slack-bot-u8ne.chainsaw-dev.zeet.app/siwa", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-cors-api-key": "temp_3f2b6d4409a86dc7f4b7c45840dbd8e9", // replace with your cors.sh API key + }, + body: JSON.stringify(payload), + signal: abortController.signal, + } + ); + const data = await response.json(); + // If the response contains a conversationId, set it as the sessionId for future messages + if (data.conversationId && data.conversationId !== sessionId) { + setSessionId(data.conversationId); + } + setMessages((prev) => [ + ...prev.slice(0, -1), // remove presence indicator + { + type: "assistant", + request_id: undefined, + text: data.data, + }, + ]); + } catch (error) { + if (abortController.signal.aborted) { + return; + } + setMessages((prev) => [ + ...prev.slice(0, -1), + { + type: "assistant", + request_id: undefined, + text: "Sorry, something went wrong.", + }, + ]); + } finally { + setIsChatStreaming(false); + setEnableAutoScroll(false); + } + }, + [ + props.authToken, + contextFilters, + sessionId, + props.customApiParams?.messagePrefix, + userHasSubmittedMessage, + trackEvent, + props.pageType, + ], + ); + + const showEmptyState = !userHasSubmittedMessage && messages.length === 0; + return ( +
+ {showEmptyState ? ( + + ) : ( + + )} + {}} + abortChatStream={() => { + chatAbortController?.abort(); + setChatAbortController(undefined); + setIsChatStreaming(false); + // if last message is presence, remove it + if (messages[messages.length - 1]?.type === "presence") { + setMessages((prev) => prev.slice(0, -1)); + } + }} + isChatStreaming={isChatStreaming} + prefillMessage={undefined} + sendMessage={handleSendMessage} + className="rounded-none border-x-0 border-b-0" + allowImageUpload={false} + /> +
+ ); +} + +function LoggedOutStateChatContent() { + const pathname = usePathname(); + return ( +
+
+
+
+ +
+
+
+ +

+ How can I help you
+ onchain today? +

+ +
+

+ Sign in to use Nebula AI +

+
+ + +
+ ); +} + +function EmptyStateChatPageContent(props: { + sendMessage: (message: any) => void; + examplePrompts: { title: string; message: string }[]; +}) { + return ( +
+
+
+
+ +
+
+
+ +

+ How can I help you
+ onchain today? +

+ +
+
+ {props.examplePrompts.map((prompt) => ( + + ))} +
+
+ ); +} \ No newline at end of file From 4994fbe156b8cd520e334fbb834472fd17cf7716 Mon Sep 17 00:00:00 2001 From: Yash <67926590+Yash094@users.noreply.github.com> Date: Fri, 23 May 2025 00:17:11 +0530 Subject: [PATCH 2/7] lint --- .../src/app/(app)/(dashboard)/support/page.tsx | 2 +- .../src/app/nebula-app/(app)/components/Chats.tsx | 13 ++++++++++--- .../components/CustomChat/CustomChatButton.tsx | 14 +++++++------- .../components/CustomChat/CustomChatContent.tsx | 9 +++++---- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx index 2872c01835d..235514c2e56 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx @@ -129,7 +129,7 @@ export default async function SupportPage() { teamId: undefined, }); - const supportPromptPrefix =""; + const supportPromptPrefix = ""; const examplePrompts = [ "ERC20 - Transfer Amount Exceeds Allowance", "Replacement transaction underpriced / Replacement fee too low", diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx index dfca99ba0df..ed8490c8f6c 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx @@ -1,7 +1,7 @@ import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow"; import { cn } from "@/lib/utils"; import { MarkdownRenderer } from "components/contract-components/published-contract/markdown-renderer"; -import { AlertCircleIcon, ThumbsUpIcon, ThumbsDownIcon } from "lucide-react"; +import { AlertCircleIcon, ThumbsDownIcon, ThumbsUpIcon } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import type { ThirdwebClient } from "thirdweb"; import type { NebulaSwapData } from "../api/chat"; @@ -457,7 +457,10 @@ function StyledMarkdownRenderer(props: { ); } -function FeedbackButtons({ sessionId, messageText }: { sessionId: string | undefined; messageText: string }) { +function FeedbackButtons({ + sessionId, + messageText, +}: { sessionId: string | undefined; messageText: string }) { const [feedback, setFeedback] = useState<"good" | "bad" | null>(null); const [loading, setLoading] = useState(false); const [thankYou, setThankYou] = useState(false); @@ -484,7 +487,11 @@ function FeedbackButtons({ sessionId, messageText }: { sessionId: string | undef } if (thankYou) { - return
Thank you for your feedback!
; + return ( +
+ Thank you for your feedback! +
+ ); } return ( diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx index 099cf430226..8c85bdaf0ce 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx @@ -1,12 +1,12 @@ "use client"; -import CustomChatContent from "./CustomChatContent"; -import type { ExamplePrompt } from "../../data/examplePrompts"; -import type { ThirdwebClient } from "thirdweb"; -import { useState, useCallback, useRef } from "react"; import { Button } from "@/components/ui/button"; -import { XIcon, MessageCircleIcon } from "lucide-react"; import { cn } from "@/lib/utils"; +import { MessageCircleIcon, XIcon } from "lucide-react"; +import { useCallback, useRef, useState } from "react"; +import type { ThirdwebClient } from "thirdweb"; +import type { ExamplePrompt } from "../../data/examplePrompts"; +import CustomChatContent from "./CustomChatContent"; export function CustomChatButton(props: { isLoggedIn: boolean; @@ -53,7 +53,7 @@ export function CustomChatButton(props: {
@@ -90,4 +90,4 @@ export function CustomChatButton(props: {
); -} \ No newline at end of file +} diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx index e302ec0b0ff..3ac709e3f7f 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx @@ -1,7 +1,7 @@ "use client"; import { Button } from "@/components/ui/button"; import { useTrack } from "hooks/analytics/useTrack"; -import { ArrowRightIcon, ArrowUpRightIcon } from "lucide-react"; +import { ArrowRightIcon } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useCallback, useState } from "react"; @@ -142,7 +142,8 @@ function CustomChatContentLoggedIn(props: { setChatAbortController(abortController); // --- Custom API call --- const payload: any = { - message: messageToSend.content.find((x) => x.type === "text")?.text ?? "", + message: + messageToSend.content.find((x) => x.type === "text")?.text ?? "", authToken: props.authToken, conversationId: "25000000005", }; @@ -159,7 +160,7 @@ function CustomChatContentLoggedIn(props: { }, body: JSON.stringify(payload), signal: abortController.signal, - } + }, ); const data = await response.json(); // If the response contains a conversationId, set it as the sessionId for future messages @@ -342,4 +343,4 @@ function EmptyStateChatPageContent(props: {
); -} \ No newline at end of file +} From c8843aedbce5f1e4bf824ad67bb8ae246f6a8204 Mon Sep 17 00:00:00 2001 From: Yash <67926590+Yash094@users.noreply.github.com> Date: Fri, 23 May 2025 00:19:45 +0530 Subject: [PATCH 3/7] replace api url --- .../(app)/components/CustomChat/CustomChatContent.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx index 3ac709e3f7f..6bfa1b0245f 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx @@ -151,12 +151,11 @@ function CustomChatContentLoggedIn(props: { payload.conversationId = sessionId; } const response = await fetch( - "https://proxy.cors.sh/https://siwa-slack-bot-u8ne.chainsaw-dev.zeet.app/siwa", + "https://siwa-api.thirdweb-dev.com/api/chat", { method: "POST", headers: { "Content-Type": "application/json", - "x-cors-api-key": "temp_3f2b6d4409a86dc7f4b7c45840dbd8e9", // replace with your cors.sh API key }, body: JSON.stringify(payload), signal: abortController.signal, From dca105cb8c4c8e52112cbcbbc0e57eac005ab2a5 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Thu, 22 May 2025 17:04:53 -0700 Subject: [PATCH 4/7] fixes --- .../[chain_id]/[contractAddress]/layout.tsx | 14 +- .../app/(app)/(dashboard)/support/page.tsx | 33 ++--- .../(app)/team/[team_slug]/(team)/layout.tsx | 19 +++ .../[team_slug]/[project_slug]/layout.tsx | 15 +++ .../src/app/(app)/team/[team_slug]/layout.tsx | 6 + .../nebula-app/(app)/components/ChatBar.tsx | 3 +- .../(app)/components/ChatPageContent.tsx | 1 + .../(app)/components/Chatbar.stories.tsx | 1 + .../app/nebula-app/(app)/components/Chats.tsx | 58 ++++++--- .../CustomChat/CustomChatButton.tsx | 37 +++--- .../CustomChat/CustomChatContent.tsx | 121 +++++------------- .../components/EmptyStateChatPageContent.tsx | 6 +- .../FloatingChat/FloatingChatContent.tsx | 6 +- 13 files changed, 150 insertions(+), 170 deletions(-) diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx index f0043de7388..04d5f3e9f01 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx @@ -20,6 +20,7 @@ import { supportedERCs } from "./_utils/detectedFeatures/supportedERCs"; import { getContractPageParamsInfo } from "./_utils/getContractFromParams"; import { getContractPageMetadata } from "./_utils/getContractPageMetadata"; import { getContractPageSidebarLinks } from "./_utils/getContractPageSidebarLinks"; +import { examplePrompts } from "../../../../../nebula-app/(app)/data/examplePrompts"; export default async function Layout(props: { params: Promise<{ @@ -94,14 +95,6 @@ Users may be considering integrating the contract into their applications. Discu The following is the user's message:`; - const examplePrompts: string[] = [ - "What does this contract do?", - "What permissions or roles exist in this contract?", - "Which functions are used the most?", - "Has this contract been used recently?", - "Who are the largest holders/users of this?", - ]; - return ( ({ - title: prompt, - message: prompt, - }))} + examplePrompts={examplePrompts} /> {props.children} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx index 235514c2e56..4b3ca3876f8 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx @@ -1,6 +1,5 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; import { BookOpenIcon, ChevronRightIcon } from "lucide-react"; import type { Metadata } from "next"; import Image from "next/image"; @@ -10,6 +9,7 @@ import contractsIcon from "../../../../../public/assets/support/contracts.png"; import engineIcon from "../../../../../public/assets/support/engine.png"; import miscIcon from "../../../../../public/assets/support/misc.svg"; import connectIcon from "../../../../../public/assets/support/wallets.png"; +import { getTeams } from "../../../../@/api/team"; import { CustomChatButton } from "../../../nebula-app/(app)/components/CustomChat/CustomChatButton"; import { getAuthToken, @@ -118,24 +118,21 @@ const HELP_PRODUCTS = [ }, ] as const; +export const siwaExamplePrompts = [ + "I see thirdweb support reference # in my console log please help me", + "What does this contract revert error mean", + "I want to add inapp wallet with sign in with X/twitter auth to my react app", + "Here is my code can you tell me why i'm seeing this error", +]; + export default async function SupportPage() { const [authToken, accountAddress] = await Promise.all([ getAuthToken(), getAuthTokenWalletAddress(), ]); - const client = getClientThirdwebClient({ - jwt: authToken, - teamId: undefined, - }); - - const supportPromptPrefix = ""; - const examplePrompts = [ - "ERC20 - Transfer Amount Exceeds Allowance", - "Replacement transaction underpriced / Replacement fee too low", - "Nonce too low: next nonce #, tx nonce #", - "Nonce too high", - ]; + const teams = await getTeams(); + const teamId = teams?.[0]?.id ?? undefined; return (
@@ -161,18 +158,14 @@ export default async function SupportPage() { networks="all" isFloating={false} pageType="support" - label="Ask Siwa AI for support" - client={client} - customApiParams={{ - messagePrefix: supportPromptPrefix, - chainIds: [], - wallet: accountAddress ?? undefined, - }} + label="Ask AI for support" examplePrompts={examplePrompts.map((prompt) => ({ title: prompt, message: prompt, }))} authToken={authToken || undefined} + teamId={teamId} + clientId={undefined} />
{props.children}
+
+ +
); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/layout.tsx index 524059cef0c..86d9ce36cd0 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/layout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/layout.tsx @@ -12,6 +12,8 @@ import { import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client"; import { ProjectSidebarLayout } from "./components/ProjectSidebarLayout"; import { SaveLastUsedProject } from "./components/SaveLastUsedProject"; +import { CustomChatButton } from "../../../../nebula-app/(app)/components/CustomChat/CustomChatButton"; +import { siwaExamplePrompts } from "../../../(dashboard)/support/page"; export default async function ProjectLayout(props: { children: React.ReactNode; @@ -78,6 +80,19 @@ export default async function ProjectLayout(props: { {props.children}
+
+ +
); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx index eba7709d06a..ed5f8e79cd5 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/layout.tsx @@ -5,6 +5,7 @@ import { ArrowRightIcon } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; import { Suspense } from "react"; +import { getAuthToken } from "../../api/lib/getAuthToken"; import { EnsureValidConnectedWalletLoginServer } from "../../components/EnsureValidConnectedWalletLogin/EnsureValidConnectedWalletLoginServer"; import { isTeamOnboardingComplete } from "../../login/onboarding/isOnboardingRequired"; import { SaveLastVisitedTeamPage } from "../components/last-visited-page/SaveLastVisitedPage"; @@ -18,12 +19,17 @@ export default async function RootTeamLayout(props: { params: Promise<{ team_slug: string }>; }) { const { team_slug } = await props.params; + const authToken = await getAuthToken(); const team = await getTeamBySlug(team_slug).catch(() => null); if (!team) { redirect("/team"); } + if (!authToken) { + redirect("/login"); + } + if (!isTeamOnboardingComplete(team)) { redirect(`/get-started/team/${team.slug}`); } diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx index 699b69f2325..bbf73828657 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx @@ -68,6 +68,7 @@ export function ChatBar(props: { isConnectingWallet: boolean; allowImageUpload: boolean; onLoginClick: undefined | (() => void); + placeholder: string; }) { const [message, setMessage] = useState(props.prefillMessage || ""); const selectedChainIds = props.context?.chainIds?.map((x) => Number(x)) || []; @@ -142,7 +143,7 @@ export function ChatBar(props: {
setMessage(e.target.value)} onKeyDown={(e) => { diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx index 4bca9787c17..0673aa4fa8c 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx @@ -353,6 +353,7 @@ export function ChatPageContent(props: {
{}} diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx index ed8490c8f6c..665f367aaf3 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx @@ -4,6 +4,7 @@ import { MarkdownRenderer } from "components/contract-components/published-contr import { AlertCircleIcon, ThumbsDownIcon, ThumbsUpIcon } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import type { ThirdwebClient } from "thirdweb"; +import { Button } from "../../../../@/components/ui/button"; import type { NebulaSwapData } from "../api/chat"; import type { NebulaUserMessage, NebulaUserMessageContent } from "../api/types"; import { NebulaIcon } from "../icons/NebulaIcon"; @@ -72,6 +73,7 @@ export function Chats(props: { enableAutoScroll: boolean; useSmallText?: boolean; sendMessage: (message: NebulaUserMessage) => void; + teamId: string | undefined; }) { const { messages, setEnableAutoScroll, enableAutoScroll } = props; const scrollAnchorRef = useRef(null); @@ -153,6 +155,7 @@ export function Chats(props: { nextMessage={props.messages[index + 1]} authToken={props.authToken} sessionId={props.sessionId} + teamId={props.teamId} />
); @@ -172,6 +175,7 @@ function RenderMessage(props: { sendMessage: (message: NebulaUserMessage) => void; nextMessage: ChatMessage | undefined; authToken: string; + teamId: string | undefined; sessionId: string | undefined; }) { const { message } = props; @@ -251,6 +255,8 @@ function RenderMessage(props: {
@@ -460,17 +466,29 @@ function StyledMarkdownRenderer(props: { function FeedbackButtons({ sessionId, messageText, -}: { sessionId: string | undefined; messageText: string }) { - const [feedback, setFeedback] = useState<"good" | "bad" | null>(null); + authToken, + teamId, +}: { + sessionId: string | undefined; + messageText: string; + authToken: string; + teamId: string | undefined; +}) { + const [, setFeedback] = useState<"good" | "bad" | null>(null); const [loading, setLoading] = useState(false); const [thankYou, setThankYou] = useState(false); async function sendFeedback(rating: "good" | "bad") { setLoading(true); try { - await fetch("https://siwa-api.thirdweb-dev.com/v1/feedback", { + const apiUrl = process.env.NEXT_PUBLIC_SIWA_URL; + await fetch(`${apiUrl}/v1/chat/feedback`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${authToken}`, + ...(teamId ? { "x-team-id": teamId } : {}), + }, body: JSON.stringify({ conversationId: sessionId, message: messageText, @@ -479,8 +497,8 @@ function FeedbackButtons({ }); setFeedback(rating); setThankYou(true); - } catch (e) { - // handle error + } catch { + // TODO handle error } finally { setLoading(false); } @@ -488,30 +506,32 @@ function FeedbackButtons({ if (thankYou) { return ( -
+
Thank you for your feedback!
); } return ( -
- - + +
); } diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx index 8c85bdaf0ce..a8b12a28ca5 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatButton.tsx @@ -4,36 +4,32 @@ import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { MessageCircleIcon, XIcon } from "lucide-react"; import { useCallback, useRef, useState } from "react"; -import type { ThirdwebClient } from "thirdweb"; -import type { ExamplePrompt } from "../../data/examplePrompts"; +import { createThirdwebClient } from "thirdweb"; +import { NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID } from "../../../../../@/constants/public-envs"; import CustomChatContent from "./CustomChatContent"; +// Create a thirdweb client for the chat functionality +const client = createThirdwebClient({ + clientId: NET_PUBLIC_DASHBOARD_THIRDWEB_CLIENT_ID, +}); + export function CustomChatButton(props: { isLoggedIn: boolean; networks: "mainnet" | "testnet" | "all" | null; isFloating: boolean; pageType: "chain" | "contract" | "support"; label: string; - client: ThirdwebClient; - customApiParams: any; - examplePrompts: ExamplePrompt[]; + examplePrompts: string[]; authToken: string | undefined; + teamId: string | undefined; + clientId: string | undefined; requireLogin?: boolean; }) { const [isOpen, setIsOpen] = useState(false); const [hasBeenOpened, setHasBeenOpened] = useState(false); - const [isDismissed, setIsDismissed] = useState(false); const closeModal = useCallback(() => setIsOpen(false), []); const ref = useRef(null); - // Close on outside click - // (optional: can add if you want exact Nebula behavior) - // useEffect(() => { ... }, [onOutsideClick]); - - if (isDismissed) { - return null; - } - return ( <> {/* Inline Button (not floating) */} @@ -59,7 +55,7 @@ export function CustomChatButton(props: { > {/* Header with close button */}
-
+
{props.label}
@@ -78,10 +74,13 @@ export function CustomChatButton(props: { {hasBeenOpened && isOpen && ( ({ + message: prompt, + title: prompt, + }))} networks={props.networks} requireLogin={props.requireLogin} /> diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx index 6bfa1b0245f..2246397106b 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/CustomChat/CustomChatContent.tsx @@ -6,12 +6,8 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { useCallback, useState } from "react"; import type { ThirdwebClient } from "thirdweb"; -import { - useActiveWallet, - useActiveWalletConnectionStatus, -} from "thirdweb/react"; +import { useActiveWalletConnectionStatus } from "thirdweb/react"; import type { NebulaContext } from "../../api/chat"; -// import { createSession } from "../../api/session"; // REMOVE import type { NebulaUserMessage } from "../../api/types"; import type { ExamplePrompt } from "../../data/examplePrompts"; import { NebulaIcon } from "../../icons/NebulaIcon"; @@ -21,17 +17,11 @@ import type { ChatMessage } from "../Chats"; export default function CustomChatContent(props: { authToken: string | undefined; + teamId: string | undefined; + clientId: string | undefined; client: ThirdwebClient; examplePrompts: ExamplePrompt[]; - pageType: "chain" | "contract" | "support"; networks: NebulaContext["networks"]; - customApiParams: - | { - messagePrefix: string; - chainIds: number[]; - wallet: string | undefined; - } - | undefined; requireLogin?: boolean; }) { if (props.requireLogin !== false && !props.authToken) { @@ -41,28 +31,22 @@ export default function CustomChatContent(props: { return ( ); } function CustomChatContentLoggedIn(props: { authToken: string; + teamId: string | undefined; + clientId: string | undefined; client: ThirdwebClient; - pageType: "chain" | "contract" | "support"; examplePrompts: ExamplePrompt[]; networks: NebulaContext["networks"]; - customApiParams: - | { - messagePrefix: string; - chainIds: number[]; - wallet: string | undefined; - } - | undefined; }) { const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false); const [messages, setMessages] = useState>([]); @@ -75,19 +59,6 @@ function CustomChatContentLoggedIn(props: { const [isChatStreaming, setIsChatStreaming] = useState(false); const [enableAutoScroll, setEnableAutoScroll] = useState(false); const connectionStatus = useActiveWalletConnectionStatus(); - const activeWallet = useActiveWallet(); - - const [contextFilters, setContextFilters] = useState< - NebulaContext | undefined - >(() => { - return { - chainIds: - props.customApiParams?.chainIds.map((chainId) => chainId.toString()) || - null, - walletAddress: props.customApiParams?.wallet || null, - networks: props.networks, - }; - }); const handleSendMessage = useCallback( async (userMessage: NebulaUserMessage) => { @@ -103,7 +74,6 @@ function CustomChatContentLoggedIn(props: { action: "send", label: "message", message: textMessage?.text, - page: props.pageType, sessionId: sessionId, }); @@ -120,47 +90,32 @@ function CustomChatContentLoggedIn(props: { }, ]); - const messagePrefix = props.customApiParams?.messagePrefix; - // if this is first message, set the message prefix // deep clone `userMessage` to avoid mutating the original message, its a pretty small object so JSON.parse is fine const messageToSend = JSON.parse( JSON.stringify(userMessage), ) as NebulaUserMessage; - // if this is first message, set the message prefix - if (messagePrefix && !userHasSubmittedMessage) { - const textMessage = messageToSend.content.find( - (x) => x.type === "text", - ); - if (textMessage) { - textMessage.text = `${messagePrefix}\n\n${textMessage.text}`; - } - } - try { setChatAbortController(abortController); // --- Custom API call --- - const payload: any = { + const payload = { message: messageToSend.content.find((x) => x.type === "text")?.text ?? "", - authToken: props.authToken, - conversationId: "25000000005", + conversationId: sessionId, }; - if (sessionId) { - payload.conversationId = sessionId; - } - const response = await fetch( - "https://siwa-api.thirdweb-dev.com/api/chat", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - signal: abortController.signal, + const apiUrl = process.env.NEXT_PUBLIC_SIWA_URL; + const response = await fetch(`${apiUrl}/v1/chat`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${props.authToken}`, + ...(props.teamId ? { "x-team-id": props.teamId } : {}), + ...(props.clientId ? { "x-client-id": props.clientId } : {}), }, - ); + body: JSON.stringify(payload), + signal: abortController.signal, + }); const data = await response.json(); // If the response contains a conversationId, set it as the sessionId for future messages if (data.conversationId && data.conversationId !== sessionId) { @@ -183,7 +138,7 @@ function CustomChatContentLoggedIn(props: { { type: "assistant", request_id: undefined, - text: "Sorry, something went wrong.", + text: `Sorry, something went wrong. ${error instanceof Error ? error.message : "Unknown error"}`, }, ]); } finally { @@ -191,15 +146,7 @@ function CustomChatContentLoggedIn(props: { setEnableAutoScroll(false); } }, - [ - props.authToken, - contextFilters, - sessionId, - props.customApiParams?.messagePrefix, - userHasSubmittedMessage, - trackEvent, - props.pageType, - ], + [props.authToken, props.clientId, props.teamId, sessionId, trackEvent], ); const showEmptyState = !userHasSubmittedMessage && messages.length === 0; @@ -225,22 +172,14 @@ function CustomChatContentLoggedIn(props: { /> )} {}} showContextSelector={false} - connectedWallets={ - props.customApiParams?.wallet && activeWallet - ? [ - { - address: props.customApiParams.wallet, - walletId: activeWallet.id, - }, - ] - : [] - } + connectedWallets={[]} setActiveWallet={() => {}} abortChatStream={() => { chatAbortController?.abort(); @@ -275,12 +214,12 @@ function LoggedOutStateChatContent() {

How can I help you
- onchain today? + today?

- Sign in to use Nebula AI + Sign in to use AI Assistant

@@ -298,7 +237,7 @@ function LoggedOutStateChatContent() { } function EmptyStateChatPageContent(props: { - sendMessage: (message: any) => void; + sendMessage: (message: NebulaUserMessage) => void; examplePrompts: { title: string; message: string }[]; }) { return ( @@ -313,7 +252,7 @@ function EmptyStateChatPageContent(props: {

How can I help you
- onchain today? + today?

diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx index 011c803c80f..d23f9e02ba3 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx @@ -43,6 +43,7 @@ export function EmptyStateChatPageContent(props: {
void; -}) { +function ExamplePrompt(props: { label: string; onClick: () => void }) { return (