From 60a5b7c3e785c9a99ce83819c589ac12f7107a26 Mon Sep 17 00:00:00 2001 From: MananTank Date: Tue, 15 Apr 2025 21:44:25 +0000 Subject: [PATCH] [NEB-123] Nebula: Allow setting context with search params (#6736) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on enhancing the chat functionality by introducing `prefillMessage` support across various components and updating the way parameters are handled in the chat interface. ### Detailed summary - Replaced `initialPrompt` with `initialParams` in chat components. - Added `prefillMessage` prop to `ChatBar`, `EmptyStateChatPageContent`, and related story files. - Updated `NebulaLogin` to handle search parameters. - Modified `ChatPageContent` to utilize `initialParams` for message handling. - Introduced a new function `getChainIds` for fetching chain IDs based on user input. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- .npmrc | 3 +- .../(app)/chat/[session_id]/page.tsx | 2 +- .../src/app/nebula-app/(app)/chat/page.tsx | 2 +- .../nebula-app/(app)/components/ChatBar.tsx | 3 +- .../(app)/components/ChatPageContent.tsx | 51 +++++++++-------- .../(app)/components/Chatbar.stories.tsx | 11 ++++ .../EmptyStateChatPageContent.stories.tsx | 19 ++++++- .../components/EmptyStateChatPageContent.tsx | 2 + .../src/app/nebula-app/(app)/page.tsx | 55 +++++++++++++++++-- .../app/nebula-app/login/NebulaLoginPage.tsx | 34 ++++++++++-- .../src/app/nebula-app/login/page.tsx | 23 +++++++- apps/dashboard/src/middleware.ts | 21 +++++-- 12 files changed, 180 insertions(+), 46 deletions(-) diff --git a/.npmrc b/.npmrc index 7d8c50db0f7..c63239aaee1 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,3 @@ public-hoist-pattern[]=*import-in-the-middle* -public-hoist-pattern[]=*require-in-the-middle* \ No newline at end of file +public-hoist-pattern[]=*require-in-the-middle* +public-hoist-pattern[]=*pino-pretty* diff --git a/apps/dashboard/src/app/nebula-app/(app)/chat/[session_id]/page.tsx b/apps/dashboard/src/app/nebula-app/(app)/chat/[session_id]/page.tsx index 75d0c4f5de3..cf321b43ef3 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/chat/[session_id]/page.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/chat/[session_id]/page.tsx @@ -34,7 +34,7 @@ export default async function Page(props: { session={session} type="new-chat" account={account} - initialPrompt={undefined} + initialParams={undefined} /> ); } diff --git a/apps/dashboard/src/app/nebula-app/(app)/chat/page.tsx b/apps/dashboard/src/app/nebula-app/(app)/chat/page.tsx index 7132b6d949e..a1e2c71f40b 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/chat/page.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/chat/page.tsx @@ -17,7 +17,7 @@ export default async function Page() { session={undefined} type="new-chat" account={account} - initialPrompt={undefined} + initialParams={undefined} /> ); } 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 c122d5d24e0..e65f25c66c2 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx @@ -10,8 +10,9 @@ export function ChatBar(props: { sendMessage: (message: string) => void; isChatStreaming: boolean; abortChatStream: () => void; + prefillMessage: string | undefined; }) { - const [message, setMessage] = useState(""); + const [message, setMessage] = useState(props.prefillMessage || ""); return (
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 5f9b74316aa..e34db3c2e40 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx @@ -29,7 +29,13 @@ export function ChatPageContent(props: { authToken: string; type: "landing" | "new-chat"; account: Account; - initialPrompt: string | undefined; + initialParams: + | { + q: string | undefined; + chainIds: number[]; + wallet: string | undefined; + } + | undefined; }) { const address = useActiveAccount()?.address; const client = useThirdwebClient(props.authToken); @@ -85,8 +91,12 @@ export function ChatPageContent(props: { >(() => { const contextRes = props.session?.context; const value: NebulaContext = { - chainIds: contextRes?.chain_ids || null, - walletAddress: contextRes?.wallet_address || null, + chainIds: + contextRes?.chain_ids || + props.initialParams?.chainIds.map((x) => x.toString()) || + [], + walletAddress: + contextRes?.wallet_address || props.initialParams?.wallet || null, }; return value; @@ -118,8 +128,9 @@ export function ChatPageContent(props: { walletAddress: null, }; - // Only set wallet address from connected wallet - updatedContextFilters.walletAddress = address || null; + if (!updatedContextFilters.walletAddress && address) { + updatedContextFilters.walletAddress = address; + } // if we have last used chains in storage, continue using them try { @@ -176,10 +187,6 @@ export function ChatPageContent(props: { const handleSendMessage = useCallback( async (message: string) => { - if (!address) { - setShowConnectModal(true); - return; - } setUserHasSubmittedMessage(true); setMessages((prev) => [ ...prev, @@ -355,14 +362,7 @@ export function ChatPageContent(props: { setEnableAutoScroll(false); } }, - [ - sessionId, - contextFilters, - props.authToken, - messages.length, - initSession, - address, - ], + [sessionId, contextFilters, props.authToken, messages.length, initSession], ); const hasDoneAutoPrompt = useRef(false); @@ -370,16 +370,19 @@ export function ChatPageContent(props: { // eslint-disable-next-line no-restricted-syntax useEffect(() => { if ( - props.initialPrompt && + props.initialParams?.q && messages.length === 0 && !hasDoneAutoPrompt.current ) { hasDoneAutoPrompt.current = true; - handleSendMessage(props.initialPrompt); + handleSendMessage(props.initialParams.q); } - }, [props.initialPrompt, messages.length, handleSendMessage]); + }, [props.initialParams?.q, messages.length, handleSendMessage]); - const showEmptyState = !userHasSubmittedMessage && messages.length === 0; + const showEmptyState = + !userHasSubmittedMessage && + messages.length === 0 && + !props.initialParams?.q; const handleUpdateContextFilters = async ( values: NebulaContext | undefined, @@ -412,7 +415,10 @@ export function ChatPageContent(props: {
{showEmptyState ? (
- +
) : (
@@ -430,6 +436,7 @@ export function ChatPageContent(props: {
{ diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/Chatbar.stories.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/Chatbar.stories.tsx index 08fd722963e..de76c613283 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/Chatbar.stories.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/Chatbar.stories.tsx @@ -27,6 +27,7 @@ function Story() { abortChatStream={() => {}} isChatStreaming={false} sendMessage={() => {}} + prefillMessage={undefined} /> @@ -35,6 +36,16 @@ function Story() { abortChatStream={() => {}} isChatStreaming={true} sendMessage={() => {}} + prefillMessage={undefined} + /> + + + + {}} + isChatStreaming={false} + sendMessage={() => {}} + prefillMessage="This is a prefilled message" />
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.stories.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.stories.tsx index fe85df5b0c5..d6ab43e2429 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.stories.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.stories.tsx @@ -15,13 +15,26 @@ export default meta; type Story = StoryObj; export const Default: Story = { - args: {}, + args: { + prefillMessage: undefined, + }, +}; + +export const PrefilledMessage: Story = { + args: { + prefillMessage: "This is a prefilled message", + }, }; -function Story() { +function Story(props: { + prefillMessage: string | undefined; +}) { return (
- {}} /> + {}} + prefillMessage={props.prefillMessage} + />
); } 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 9dd7dd2d192..763a2b0e42f 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx @@ -8,6 +8,7 @@ import { ChatBar } from "./ChatBar"; export function EmptyStateChatPageContent(props: { sendMessage: (message: string) => void; + prefillMessage: string | undefined; }) { return (
@@ -33,6 +34,7 @@ export function EmptyStateChatPageContent(props: { abortChatStream={() => { // the page will switch so, no need to handle abort here }} + prefillMessage={props.prefillMessage} />
diff --git a/apps/dashboard/src/app/nebula-app/(app)/page.tsx b/apps/dashboard/src/app/nebula-app/(app)/page.tsx index e86bfda456e..ab7064e4ac2 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/page.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/page.tsx @@ -1,3 +1,6 @@ +import { unstable_cache } from "next/cache"; +import { isAddress } from "thirdweb"; +import { fetchChain } from "../../../utils/fetchChain"; import { getValidAccount } from "../../account/settings/getAccount"; import { getAuthToken } from "../../api/lib/getAuthToken"; import { loginRedirect } from "../../login/loginRedirect"; @@ -5,27 +8,67 @@ import { ChatPageContent } from "./components/ChatPageContent"; export default async function Page(props: { searchParams: Promise<{ - prompt?: string; + q?: string | string[]; + chain?: string | string[]; + wallet?: string | string[]; }>; }) { - const [searchParams, authToken] = await Promise.all([ - props.searchParams, + const searchParams = await props.searchParams; + + const [chainIds, authToken, account] = await Promise.all([ + getChainIds(searchParams.chain), getAuthToken(), + getValidAccount(), ]); if (!authToken) { loginRedirect(); } - const account = await getValidAccount(); - return ( ); } + +const getChainIds = unstable_cache( + async (_chainNames: string[] | string | undefined) => { + if (!_chainNames) { + return []; + } + + const chainIds: number[] = []; + + const chainNames = + typeof _chainNames === "string" ? [_chainNames] : _chainNames; + + const chainResults = await Promise.allSettled( + chainNames.map((x) => fetchChain(x)), + ); + + for (const chainResult of chainResults) { + if (chainResult.status === "fulfilled" && chainResult.value) { + chainIds.push(chainResult.value.chainId); + } + } + + return chainIds; + }, + ["nebula_getChainIds"], + { + revalidate: 60 * 60 * 24, // 24 hours + }, +); diff --git a/apps/dashboard/src/app/nebula-app/login/NebulaLoginPage.tsx b/apps/dashboard/src/app/nebula-app/login/NebulaLoginPage.tsx index b2d39305553..f27cc5bfa69 100644 --- a/apps/dashboard/src/app/nebula-app/login/NebulaLoginPage.tsx +++ b/apps/dashboard/src/app/nebula-app/login/NebulaLoginPage.tsx @@ -11,11 +11,38 @@ import { LoginAndOnboardingPageContent } from "../../login/LoginPage"; export function NebulaLoginPage(props: { account: Account | undefined; + params: { + chain: string | string[] | undefined; + q: string | undefined; + wallet: string | undefined; + }; }) { - const [message, setMessage] = useState(undefined); + const [message, setMessage] = useState(props.params.q); const [showPage, setShowPage] = useState<"connect" | "welcome">( props.account ? "connect" : "welcome", ); + + const redirectPathObj = { + chain: props.params.chain, + q: message, // don't use props.params.q, because message may be updated by user + wallet: props.params.wallet, + }; + + const redirectPathParams = Object.entries(redirectPathObj) + .map(([key, value]) => { + if (!value) { + return ""; + } + + if (Array.isArray(value)) { + return value.map((v) => `${key}=${encodeURIComponent(v)}`).join("&"); + } + + return `${key}=${encodeURIComponent(value)}`; + }) + .filter((v) => v !== "") + .join("&"); + return (
{/* nav */} @@ -55,15 +82,14 @@ export function NebulaLoginPage(props: { )} {showPage === "welcome" && (
{ setMessage(msg); setShowPage("connect"); diff --git a/apps/dashboard/src/app/nebula-app/login/page.tsx b/apps/dashboard/src/app/nebula-app/login/page.tsx index 2e9d930936c..4a31ccfd4d5 100644 --- a/apps/dashboard/src/app/nebula-app/login/page.tsx +++ b/apps/dashboard/src/app/nebula-app/login/page.tsx @@ -1,8 +1,27 @@ import { getRawAccount } from "../../account/settings/getAccount"; import { NebulaLoginPage } from "./NebulaLoginPage"; -export default async function NebulaLogin() { +export default async function NebulaLogin(props: { + searchParams: Promise<{ + chain?: string | string[]; + q?: string | string[]; + wallet?: string | string[]; + }>; +}) { + const searchParams = await props.searchParams; const account = await getRawAccount(); - return ; + return ( + + ); } diff --git a/apps/dashboard/src/middleware.ts b/apps/dashboard/src/middleware.ts index ce8f8923fc2..a6b6d08579a 100644 --- a/apps/dashboard/src/middleware.ts +++ b/apps/dashboard/src/middleware.ts @@ -32,14 +32,29 @@ export async function middleware(request: NextRequest) { const subdomain = host?.split(".")[0]; const paths = pathname.slice(1).split("/"); + const activeAccount = request.cookies.get(COOKIE_ACTIVE_ACCOUNT)?.value; + const authCookie = activeAccount + ? request.cookies.get(COOKIE_PREFIX_TOKEN + getAddress(activeAccount)) + : null; + // nebula.thirdweb.com -> render page at app/nebula-app // on vercel preview, the format is nebula---thirdweb-www-git-.thirdweb-preview.com if ( subdomain && (subdomain === "nebula" || subdomain.startsWith("nebula---")) ) { + // preserve search params when redirecting to /login page + if (!authCookie && paths[0] !== "login") { + return redirect(request, "/login", { + searchParams: request.nextUrl.searchParams.toString(), + }); + } + const newPaths = ["nebula-app", ...paths]; - return rewrite(request, `/${newPaths.join("/")}`, undefined); + + return rewrite(request, `/${newPaths.join("/")}`, { + searchParams: request.nextUrl.searchParams.toString(), + }); } // requesting page at app/nebula-app on thirdweb.com -> redirect to nebula.thirdweb.com @@ -53,10 +68,6 @@ export async function middleware(request: NextRequest) { } let cookiesToSet: Record | undefined = undefined; - const activeAccount = request.cookies.get(COOKIE_ACTIVE_ACCOUNT)?.value; - const authCookie = activeAccount - ? request.cookies.get(COOKIE_PREFIX_TOKEN + getAddress(activeAccount)) - : null; // utm collection // if user is already signed in - don't bother capturing utm params