diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx index a414618597e..4dcb7b16e07 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx @@ -13,12 +13,18 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { getThirdwebClient } from "@/constants/thirdweb.server"; import { ChevronDownIcon, TicketCheckIcon } from "lucide-react"; import type { Metadata } from "next"; import Link from "next/link"; import { redirect } from "next/navigation"; import { mapV4ChainToV5Chain } from "../../../../../contexts/map-chains"; -import { getAuthToken } from "../../../../api/lib/getAuthToken"; +import { getRawAccount } from "../../../../account/settings/getAccount"; +import { + getAuthToken, + getAuthTokenWalletAddress, +} from "../../../../api/lib/getAuthToken"; +import { NebulaFloatingChatButton } from "../../../../nebula-app/(app)/components/FloatingChat/FloatingChat"; import { StarButton } from "../../components/client/star-button"; import { getChain, getChainMetadata } from "../../utils"; import { AddChainToWallet } from "./components/client/add-chain-to-wallet"; @@ -55,17 +61,58 @@ export default async function ChainPageLayout(props: { }) { const params = await props.params; const { children } = props; - const chain = await getChain(params.chain_id); - const authToken = await getAuthToken(); + const [chain, authToken, account, accountAddress] = await Promise.all([ + getChain(params.chain_id), + getAuthToken(), + getRawAccount(), + getAuthTokenWalletAddress(), + ]); if (params.chain_id !== chain.slug) { redirect(chain.slug); } const chainMetadata = await getChainMetadata(chain.chainId); + const client = getThirdwebClient(authToken ?? undefined); + + const chainPromptPrefix = `\ +You are assisting users exploring the chain ${chain.name} (Chain ID: ${chain.chainId}). Provide concise insights into the types of applications and activities prevalent on this chain, such as DeFi protocols, NFT marketplaces, or gaming platforms. Highlight notable projects or trends without delving into technical details like consensus mechanisms or gas fees. +Users may seek comparisons between ${chain.name} and other chains. Provide objective, succinct comparisons focusing on performance, fees, and ecosystem support. Refrain from transaction-specific advice unless requested. +Provide users with an understanding of the unique use cases and functionalities that ${chain.name} supports. Discuss how developers leverage this chain for specific applications, such as scalable dApps, low-cost transactions, or specialized token standards, focusing on practical implementations. +Users may be interested in utilizing thirdweb tools on ${chain.name}. Offer clear guidance on how thirdweb's SDKs, smart contract templates, and deployment tools integrate with this chain. Emphasize the functionalities enabled by thirdweb without discussing transaction execution unless prompted. +Avoid transaction-related actions to be executed by the user unless inquired about. + +The following is the user's message: + `; + + const examplePrompts: string[] = [ + "What are users doing on this chain?", + "What are the most active contracts?", + "Why would I use this chain over others?", + "Can I deploy thirdweb contracts to this chain?", + ]; + + if (chain.chainId !== 1) { + examplePrompts.push("Can I bridge assets from Ethereum to this chain?"); + } return ( <> + ({ + title: prompt, + message: prompt, + }))} + />
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx index 2663ce8370a..90847460123 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx @@ -9,6 +9,12 @@ import { isAddress, isContractDeployed } from "thirdweb/utils"; import type { MinimalTeamsAndProjects } from "../../../../../components/contract-components/contract-deploy-form/add-to-project-card"; import { resolveFunctionSelectors } from "../../../../../lib/selectors"; import { shortenIfAddress } from "../../../../../utils/usedapp-external"; +import { getRawAccount } from "../../../../account/settings/getAccount"; +import { + getAuthToken, + getAuthTokenWalletAddress, +} from "../../../../api/lib/getAuthToken"; +import { NebulaFloatingChatButton } from "../../../../nebula-app/(app)/components/FloatingChat/FloatingChat"; import { ConfigureCustomChain } from "./_layout/ConfigureCustomChain"; import { getContractMetadataHeaderData } from "./_layout/contract-metadata"; import { ContractPageLayout } from "./_layout/contract-page-layout"; @@ -42,6 +48,12 @@ export default async function Layout(props: { notFound(); } + const [authToken, account, accountAddress] = await Promise.all([ + getAuthToken(), + getRawAccount(), + getAuthTokenWalletAddress(), + ]); + const client = getThirdwebClient(); const teamsAndProjects = await getTeamsAndProjectsIfLoggedIn(); @@ -75,6 +87,25 @@ export default async function Layout(props: { const { contractMetadata, externalLinks } = await getContractMetadataHeaderData(contract); + const contractAddress = info.contract.address; + const chainName = info.chainMetadata.name; + const chainId = info.contract.chain.id; + + const contractPromptPrefix = `A user is viewing the contract address ${contractAddress} on ${chainName} (Chain ID: ${chainId}). Provide a concise summary of this contract's functionalities, such as token minting, staking, or governance mechanisms. Focus on what the contract enables users to do, avoiding transaction execution details unless requested. +Users may be interested in how to interact with the contract. Outline common interaction patterns, such as claiming rewards, participating in governance, or transferring assets. Emphasize the contract's capabilities without guiding through transaction processes unless asked. +Provide insights into how the contract is being used. Share information on user engagement, transaction volumes, or integration with other dApps, focusing on the contract's role within the broader ecosystem. +Users may be considering integrating the contract into their applications. Discuss how this contract's functionalities can be leveraged within different types of dApps, highlighting potential use cases and benefits. + +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, + }))} + /> {props.children} ); 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 e65f25c66c2..018e862936f 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx @@ -11,11 +11,17 @@ export function ChatBar(props: { isChatStreaming: boolean; abortChatStream: () => void; prefillMessage: string | undefined; + className?: string; }) { const [message, setMessage] = useState(props.prefillMessage || ""); return ( -
+
prompt.message.toLowerCase() === lowerCaseMessage, )?.interceptedReply; - if (interceptedReply) { // slight delay to match other response times await new Promise((resolve) => setTimeout(resolve, 1000)); @@ -231,8 +230,6 @@ export function ChatPageContent(props: { currentSessionId = session.id; } - let requestIdForMessage = ""; - // add this session on sidebar if (messages.length === 0) { const prevValue = newSessionsStore.getValue(); @@ -249,117 +246,22 @@ export function ChatPageContent(props: { setChatAbortController(abortController); - await promptNebula({ + await handleNebulaPrompt({ abortController, - message: message, + message, sessionId: currentSessionId, authToken: props.authToken, - handleStream(res) { - if (abortController.signal.aborted) { - return; - } - - if (res.event === "init") { - requestIdForMessage = res.data.request_id; - } - - if (res.event === "delta") { - setMessages((prev) => { - const lastMessage = prev[prev.length - 1]; - // if last message is presence, overwrite it - if (lastMessage?.type === "presence") { - return [ - ...prev.slice(0, -1), - { - text: res.data.v, - type: "assistant", - request_id: requestIdForMessage, - }, - ]; - } - - // if last message is from chat, append to it - if (lastMessage?.type === "assistant") { - return [ - ...prev.slice(0, -1), - { - text: lastMessage.text + res.data.v, - type: "assistant", - request_id: requestIdForMessage, - }, - ]; - } - - // otherwise, add a new message - return [ - ...prev, - { - text: res.data.v, - type: "assistant", - request_id: requestIdForMessage, - }, - ]; - }); - } - - if (res.event === "presence") { - setMessages((prev) => { - const lastMessage = prev[prev.length - 1]; - // if last message is presence, overwrite it - if (lastMessage?.type === "presence") { - return [ - ...prev.slice(0, -1), - { text: res.data.data, type: "presence" }, - ]; - } - // otherwise, add a new message - return [...prev, { text: res.data.data, type: "presence" }]; - }); - } - - if (res.event === "action") { - if (res.type === "sign_transaction") { - setMessages((prev) => { - let prevMessages = prev; - // if last message is presence, remove it - if ( - prevMessages[prevMessages.length - 1]?.type === "presence" - ) { - prevMessages = prevMessages.slice(0, -1); - } - - return [ - ...prevMessages, - { - type: "send_transaction", - data: res.data, - }, - ]; - }); - } - } - }, - context: contextFilters, + setMessages, + contextFilters: contextFilters, }); } catch (error) { if (abortController.signal.aborted) { return; } - console.error(error); - setMessages((prev) => { - const newMessages = prev.slice( - 0, - prev[prev.length - 1]?.type === "presence" ? -1 : undefined, - ); - - // add error message - newMessages.push({ - text: `Error: ${error instanceof Error ? error.message : "Failed to execute command"}`, - type: "error", - }); - - return newMessages; + handleNebulaPromptError({ + error, + setMessages, }); } finally { setIsChatStreaming(false); @@ -543,3 +445,136 @@ function getLastUsedChainIds(): string[] | null { return null; } } + +export async function handleNebulaPrompt(params: { + abortController: AbortController; + message: string; + sessionId: string; + authToken: string; + setMessages: React.Dispatch>; + contextFilters: NebulaContext | undefined; +}) { + const { + abortController, + message, + sessionId, + authToken, + setMessages, + contextFilters, + } = params; + let requestIdForMessage = ""; + + await promptNebula({ + abortController, + message, + sessionId, + authToken, + handleStream(res) { + if (abortController.signal.aborted) { + return; + } + + if (res.event === "init") { + requestIdForMessage = res.data.request_id; + } + + if (res.event === "delta") { + setMessages((prev) => { + const lastMessage = prev[prev.length - 1]; + // if last message is presence, overwrite it + if (lastMessage?.type === "presence") { + return [ + ...prev.slice(0, -1), + { + text: res.data.v, + type: "assistant", + request_id: requestIdForMessage, + }, + ]; + } + + // if last message is from chat, append to it + if (lastMessage?.type === "assistant") { + return [ + ...prev.slice(0, -1), + { + text: lastMessage.text + res.data.v, + type: "assistant", + request_id: requestIdForMessage, + }, + ]; + } + + // otherwise, add a new message + return [ + ...prev, + { + text: res.data.v, + type: "assistant", + request_id: requestIdForMessage, + }, + ]; + }); + } + + if (res.event === "presence") { + setMessages((prev) => { + const lastMessage = prev[prev.length - 1]; + // if last message is presence, overwrite it + if (lastMessage?.type === "presence") { + return [ + ...prev.slice(0, -1), + { text: res.data.data, type: "presence" }, + ]; + } + // otherwise, add a new message + return [...prev, { text: res.data.data, type: "presence" }]; + }); + } + + if (res.event === "action") { + if (res.type === "sign_transaction") { + setMessages((prev) => { + let prevMessages = prev; + // if last message is presence, remove it + if (prevMessages[prevMessages.length - 1]?.type === "presence") { + prevMessages = prevMessages.slice(0, -1); + } + + return [ + ...prevMessages, + { + type: "send_transaction", + data: res.data, + }, + ]; + }); + } + } + }, + context: contextFilters, + }); +} + +export function handleNebulaPromptError(params: { + error: unknown; + setMessages: React.Dispatch>; +}) { + const { error, setMessages } = params; + console.error(error); + + setMessages((prev) => { + const newMessages = prev.slice( + 0, + prev[prev.length - 1]?.type === "presence" ? -1 : undefined, + ); + + // add error message + newMessages.push({ + text: `Error: ${error instanceof Error ? error.message : "Failed to execute command"}`, + type: "error", + }); + + return newMessages; + }); +} 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 bbc4f3ec6ec..e5bcd4a7e64 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx @@ -53,6 +53,7 @@ export function Chats(props: { client: ThirdwebClient; setEnableAutoScroll: (enable: boolean) => void; enableAutoScroll: boolean; + useSmallText?: boolean; }) { const { messages, setEnableAutoScroll, enableAutoScroll } = props; const scrollAnchorRef = useRef(null); @@ -99,7 +100,7 @@ export function Chats(props: { > @@ -110,7 +111,10 @@ export function Chats(props: { props.isChatStreaming && index === props.messages.length - 1; return (
@@ -120,6 +124,7 @@ export function Chats(props: {
@@ -156,6 +161,7 @@ export function Chats(props: { ) : message.type === "error" ? (
@@ -252,10 +258,14 @@ function MessageActions(props: { const sendBadRating = useMutation({ mutationFn: () => sendRating("bad"), onSuccess() { - toast.info("Thanks for the feedback!"); + toast.info("Thanks for the feedback!", { + position: "top-right", + }); }, onError() { - toast.error("Failed to send feedback"); + toast.error("Failed to send feedback", { + position: "top-right", + }); }, }); @@ -319,6 +329,7 @@ function MessageActions(props: { function StyledMarkdownRenderer(props: { text: string; isMessagePending: boolean; + type: "assistant" | "user"; }) { return ( 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 64bba7d3bfc..0fe662ea14b 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx @@ -15,8 +15,8 @@ export function EmptyStateChatPageContent(props: {
-
-
+
+
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChat.stories.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChat.stories.tsx new file mode 100644 index 00000000000..022ccc63b88 --- /dev/null +++ b/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChat.stories.tsx @@ -0,0 +1,45 @@ +import { getThirdwebClient } from "@/constants/thirdweb.server"; +import type { Meta, StoryObj } from "@storybook/react"; +import { ThirdwebProvider } from "thirdweb/react"; +import { accountStub } from "../../../../../stories/stubs"; +import { examplePrompts } from "../../data/examplePrompts"; +import { NebulaFloatingChatButton } from "./FloatingChat"; + +const meta = { + title: "Nebula/FloatingChat", + component: NebulaFloatingChatButton, + decorators: [ + (Story) => ( + + + + ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const client = getThirdwebClient(); + +export const LoggedIn: Story = { + args: { + account: accountStub(), + authToken: "foo", + nebulaParams: undefined, + label: "Ask AI about this contract", + examplePrompts: examplePrompts, + client, + }, +}; + +export const LoggedOut: Story = { + args: { + account: undefined, + authToken: undefined, + nebulaParams: undefined, + label: "Ask AI about this contract", + examplePrompts: examplePrompts, + client, + }, +}; diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChat.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChat.tsx new file mode 100644 index 00000000000..10d3178b1f4 --- /dev/null +++ b/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChat.tsx @@ -0,0 +1,201 @@ +"use client"; + +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Button } from "@/components/ui/button"; +import { ToolTipLabel } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; +import { ExternalLinkIcon, RefreshCcwIcon, XIcon } from "lucide-react"; +import Link from "next/link"; +import { + Suspense, + lazy, + useCallback, + useEffect, + useRef, + useState, +} from "react"; +import type { ThirdwebClient } from "thirdweb"; +import type { ExamplePrompt } from "../../data/examplePrompts"; +import { NebulaIcon } from "../../icons/NebulaIcon"; + +const LazyFloatingChatContent = lazy(() => import("./FloatingChatContent")); + +export function NebulaFloatingChatButton(props: { + authToken: string | undefined; + examplePrompts: ExamplePrompt[]; + account: Account | undefined; + label: string; + client: ThirdwebClient; + nebulaParams: + | { + messagePrefix: string; + chainIds: number[]; + wallet: string | undefined; + } + | undefined; +}) { + const [isOpen, setIsOpen] = useState(false); + const [hasBeenOpened, setHasBeenOpened] = useState(false); + const closeModal = useCallback(() => setIsOpen(false), []); + const [isDismissed, setIsDismissed] = useState(false); + + if (isDismissed) { + return null; + } + + return ( + <> + {!isOpen && ( +
+ + +
+ )} + + + + ); +} + +function NebulaChatUIContainer(props: { + onClose: () => void; + isOpen: boolean; + hasBeenOpened: boolean; + authToken: string | undefined; + account: Account | undefined; + examplePrompts: ExamplePrompt[]; + client: ThirdwebClient; + nebulaParams: + | { + messagePrefix: string; + chainIds: number[]; + wallet: string | undefined; + } + | undefined; +}) { + const ref = useOutsideClick(props.onClose); + const shouldRenderChat = props.isOpen || props.hasBeenOpened; + const [nebulaSessionKey, setNebulaSessionKey] = useState(0); + + return ( +
+
+ +

Nebula

+ + + +
+ + + + + + + +
+
+ + {/* once opened keep the component mounted to preserve the states */} +
+ {shouldRenderChat && ( + + +
+ } + > + + + )} +
+
+ ); +} + +function useOutsideClick(onOutsideClick: () => void) { + const ref = useRef(null); + + // clicking outside the chat window should close it + // eslint-disable-next-line no-restricted-syntax + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + // if clicked on a dialog or popover - ignore + if ( + (event.target as HTMLElement).closest( + "[data-radix-popper-content-wrapper], [role='dialog'], [data-state='open']", + ) + ) { + return; + } + onOutsideClick(); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [onOutsideClick]); + + return ref; +} diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx new file mode 100644 index 00000000000..4c9600d18c9 --- /dev/null +++ b/apps/dashboard/src/app/nebula-app/(app)/components/FloatingChat/FloatingChatContent.tsx @@ -0,0 +1,278 @@ +import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; +import { ArrowRightIcon, ArrowUpRightIcon } from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useCallback, useMemo, useState } from "react"; +import type { ThirdwebClient } from "thirdweb"; +import { Button } from "../../../../../@/components/ui/button"; +import type { NebulaContext } from "../../api/chat"; +import { createSession } from "../../api/session"; +import type { ExamplePrompt } from "../../data/examplePrompts"; +import { NebulaIcon } from "../../icons/NebulaIcon"; +import { ChatBar } from "../ChatBar"; +import { + handleNebulaPrompt, + handleNebulaPromptError, +} from "../ChatPageContent"; +import { Chats } from "../Chats"; +import type { ChatMessage } from "../Chats"; + +export default function FloatingChatContent(props: { + authToken: string | undefined; + account: Account | undefined; + client: ThirdwebClient; + examplePrompts: ExamplePrompt[]; + nebulaParams: + | { + messagePrefix: string; + chainIds: number[]; + wallet: string | undefined; + } + | undefined; +}) { + if (!props.account || !props.authToken) { + return ; + } + + return ( + + ); +} + +function FloatingChatContentLoggedIn(props: { + authToken: string; + account: Account; + client: ThirdwebClient; + examplePrompts: ExamplePrompt[]; + nebulaParams: + | { + messagePrefix: string; + chainIds: number[]; + wallet: string | undefined; + } + | undefined; +}) { + const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false); + const [messages, setMessages] = useState>([]); + const [sessionId, setSessionId] = useState(undefined); + const [chatAbortController, setChatAbortController] = useState< + AbortController | undefined + >(); + const [isChatStreaming, setIsChatStreaming] = useState(false); + const [enableAutoScroll, setEnableAutoScroll] = useState(false); + + const contextFilters: NebulaContext = useMemo(() => { + return { + chainIds: + props.nebulaParams?.chainIds.map((chainId) => chainId.toString()) || + null, + walletAddress: props.nebulaParams?.wallet || null, + }; + }, [props.nebulaParams]); + + const initSession = useCallback(async () => { + const session = await createSession({ + authToken: props.authToken, + context: contextFilters, + }); + setSessionId(session.id); + return session; + }, [props.authToken, contextFilters]); + + const handleSendMessage = useCallback( + async (userMessage: string) => { + const abortController = new AbortController(); + setUserHasSubmittedMessage(true); + setIsChatStreaming(true); + setEnableAutoScroll(true); + + // if this is first message, set the message prefix + const messageToSend = + props.nebulaParams?.messagePrefix && !userHasSubmittedMessage + ? `${props.nebulaParams.messagePrefix}\n\n${userMessage}` + : userMessage; + + setMessages((prev) => [ + ...prev, + { text: userMessage, type: "user" }, + // instant loading indicator feedback to user + { + type: "presence", + text: "Thinking...", + }, + ]); + + try { + // Ensure we have a session ID + let currentSessionId = sessionId; + if (!currentSessionId) { + const session = await initSession(); + currentSessionId = session.id; + } + + setChatAbortController(abortController); + await handleNebulaPrompt({ + abortController, + message: messageToSend, + sessionId: currentSessionId, + authToken: props.authToken, + setMessages, + contextFilters: contextFilters, + }); + } catch (error) { + if (abortController.signal.aborted) { + return; + } + + handleNebulaPromptError({ + error, + setMessages, + }); + } finally { + setIsChatStreaming(false); + setEnableAutoScroll(false); + } + }, + [ + props.authToken, + contextFilters, + initSession, + sessionId, + props.nebulaParams?.messagePrefix, + userHasSubmittedMessage, + ], + ); + + const showEmptyState = !userHasSubmittedMessage && messages.length === 0; + + return ( +
+ {showEmptyState ? ( + + ) : ( + + )} + { + 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="" + sendMessage={handleSendMessage} + className="rounded-none border-x-0 border-b-0" + /> +
+ ); +} + +function LoggedOutStateChatContent() { + const pathname = usePathname(); + return ( +
+
+
+
+ +
+
+
+ +

+ How can I help you
+ onchain today? +

+ +
+

+ Sign in to use Nebula AI +

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

+ How can I help you
+ onchain today? +

+ +
+
+ {props.examplePrompts.map((prompt) => { + return ( + props.sendMessage(prompt.message)} + /> + ); + })} +
+
+ ); +} + +function ExamplePromptButton(props: { + label: string; + onClick: () => void; +}) { + return ( + + ); +} diff --git a/apps/dashboard/src/app/nebula-app/(app)/data/examplePrompts.ts b/apps/dashboard/src/app/nebula-app/(app)/data/examplePrompts.ts index b690752da83..443c7097564 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/data/examplePrompts.ts +++ b/apps/dashboard/src/app/nebula-app/(app)/data/examplePrompts.ts @@ -1,4 +1,4 @@ -type ExamplePrompt = { +export type ExamplePrompt = { title: string; message: string; interceptedReply?: string;