From 7fc619165d5632b162a2b588b36015478ae5e044 Mon Sep 17 00:00:00 2001 From: MananTank Date: Mon, 16 Dec 2024 20:18:27 +0000 Subject: [PATCH] [DASH-444] Nebula - Update login flow (#5750) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem solved Short description of the bug fixed or feature added --- ## PR-Codex overview This PR focuses on refactoring and updating the chat and login components in the `nebula-app`, enhancing the handling of chat sessions, prompts, and user interactions. ### Detailed summary - Added `initialPrompt` to `ChatPageContent` and `EmptyStateChatPageContent`. - Simplified `EmptyStateChatPageContent` props. - Introduced `LoginAndOnboardingPageContent` for better login handling. - Updated `NebulaLogin` to use `NebulaLoginPage`. - Refactored message handling and session management in `ChatPageContent`. - Removed unnecessary props in `Chatbar` and `EmptyStateChatPageContent`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- apps/dashboard/src/app/login/LoginPage.tsx | 14 + .../(app)/chat/[session_id]/page.tsx | 1 + .../src/app/nebula-app/(app)/chat/page.tsx | 1 + .../nebula-app/(app)/components/ChatBar.tsx | 3 - .../(app)/components/ChatPageContent.tsx | 395 +++++++++--------- .../(app)/components/Chatbar.stories.tsx | 29 +- .../EmptyStateChatPageContent.stories.tsx | 9 +- .../components/EmptyStateChatPageContent.tsx | 5 - .../src/app/nebula-app/(app)/page.tsx | 12 +- .../app/nebula-app/login/NebulaLoginPage.tsx | 68 +++ .../src/app/nebula-app/login/page.tsx | 17 +- 11 files changed, 290 insertions(+), 264 deletions(-) create mode 100644 apps/dashboard/src/app/nebula-app/login/NebulaLoginPage.tsx diff --git a/apps/dashboard/src/app/login/LoginPage.tsx b/apps/dashboard/src/app/login/LoginPage.tsx index e63f01e1c4d..d13d044cafd 100644 --- a/apps/dashboard/src/app/login/LoginPage.tsx +++ b/apps/dashboard/src/app/login/LoginPage.tsx @@ -77,6 +77,20 @@ export function LoginAndOnboardingPage(props: { + + + ); +} + +export function LoginAndOnboardingPageContent(props: { + account: Account | undefined; + redirectPath: string; +}) { + return ( +
); } 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 31300c2f23f..c7c6c1db170 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/chat/page.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/chat/page.tsx @@ -27,6 +27,7 @@ export default async function Page() { session={undefined} type="new-chat" account={account} + initialPrompt={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 33245b51901..aecfd0ce08c 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx @@ -5,11 +5,8 @@ import { AutoResizeTextarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; import { ArrowUpIcon, CircleStopIcon } from "lucide-react"; import { useState } from "react"; -import type { ExecuteConfig } from "../api/types"; export function Chatbar(props: { - updateConfig: (config: ExecuteConfig) => void; - config: ExecuteConfig; sendMessage: (message: string) => void; isChatStreaming: boolean; abortChatStream: () => void; 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 498c7eddb09..fa08800b51b 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx @@ -4,8 +4,7 @@ import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow"; import { useThirdwebClient } from "@/constants/thirdweb.client"; import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; -import { useMutation } from "@tanstack/react-query"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useActiveAccount } from "thirdweb/react"; import { type ContextFilters, promptNebula } from "../api/chat"; import { createSession, updateSession } from "../api/session"; @@ -22,6 +21,7 @@ export function ChatPageContent(props: { accountAddress: string; type: "landing" | "new-chat"; account: Account; + initialPrompt: string | undefined; }) { const address = useActiveAccount()?.address; const client = useThirdwebClient(); @@ -37,7 +37,6 @@ export function ChatPageContent(props: { return []; }); - const [_config, setConfig] = useState(); const [hasUserUpdatedContextFilters, setHasUserUpdatedContextFilters] = useState(false); @@ -80,10 +79,12 @@ export function ChatPageContent(props: { return _contextFilters; }, [_contextFilters, address, isNewSession, hasUserUpdatedContextFilters]); - const config = _config || { - mode: "client", - signer_wallet_address: props.accountAddress, - }; + const config: ExecuteConfig = useMemo(() => { + return { + mode: "client", + signer_wallet_address: props.accountAddress, + }; + }, [props.accountAddress]); const [sessionId, _setSessionId] = useState( props.session?.id, @@ -93,19 +94,22 @@ export function ChatPageContent(props: { AbortController | undefined >(); - function setSessionId(sessionId: string) { - _setSessionId(sessionId); - // update page URL without reloading - window.history.replaceState({}, "", `/chat/${sessionId}`); - - // if the current page is landing page, link to /chat - // if current page is new /chat page, link to landing page - if (props.type === "landing") { - newChatPageUrlStore.setValue("/chat"); - } else { - newChatPageUrlStore.setValue("/"); - } - } + const setSessionId = useCallback( + (sessionId: string) => { + _setSessionId(sessionId); + // update page URL without reloading + window.history.replaceState({}, "", `/chat/${sessionId}`); + + // if the current page is landing page, link to /chat + // if current page is new /chat page, link to landing page + if (props.type === "landing") { + newChatPageUrlStore.setValue("/chat"); + } else { + newChatPageUrlStore.setValue("/"); + } + }, + [props.type], + ); const messagesEndRef = useRef(null); const [isChatStreaming, setIsChatStreaming] = useState(false); @@ -120,7 +124,7 @@ export function ChatPageContent(props: { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, isUserSubmittedMessage]); - async function initSession() { + const initSession = useCallback(async () => { const session = await createSession({ authToken: props.authToken, config, @@ -128,198 +132,189 @@ export function ChatPageContent(props: { }); setSessionId(session.id); return session; - } + }, [config, contextFilters, props.authToken, setSessionId]); - async function handleSendMessage(message: string) { - setUserHasSubmittedMessage(true); - setMessages((prev) => [ - ...prev, - { text: message, type: "user" }, - // instant loading indicator feedback to user - { - type: "presence", - text: "Thinking...", - }, - ]); - - setIsChatStreaming(true); - setIsUserSubmittedMessage(true); - const abortController = new AbortController(); - - try { - // Ensure we have a session ID - let currentSessionId = sessionId; - if (!currentSessionId) { - const session = await initSession(); - currentSessionId = session.id; - } + const handleSendMessage = useCallback( + async (message: string) => { + setUserHasSubmittedMessage(true); + setMessages((prev) => [ + ...prev, + { text: message, type: "user" }, + // instant loading indicator feedback to user + { + type: "presence", + text: "Thinking...", + }, + ]); - let requestIdForMessage = ""; - - // add this session on sidebar - if (messages.length === 0) { - const prevValue = newSessionsStore.getValue(); - newSessionsStore.setValue([ - { - id: currentSessionId, - title: message, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - }, - ...prevValue, - ]); - } + setIsChatStreaming(true); + setIsUserSubmittedMessage(true); + const abortController = new AbortController(); + + try { + // Ensure we have a session ID + let currentSessionId = sessionId; + if (!currentSessionId) { + const session = await initSession(); + currentSessionId = session.id; + } + + let requestIdForMessage = ""; + + // add this session on sidebar + if (messages.length === 0) { + const prevValue = newSessionsStore.getValue(); + newSessionsStore.setValue([ + { + id: currentSessionId, + title: message, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, + ...prevValue, + ]); + } + + setChatAbortController(abortController); + + await promptNebula({ + abortController, + message: message, + sessionId: currentSessionId, + config: config, + authToken: props.authToken, + handleStream(res) { + if (abortController.signal.aborted) { + return; + } - setChatAbortController(abortController); - - await promptNebula({ - abortController, - message: message, - sessionId: currentSessionId, - config: config, - 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 (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") { + // 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.slice(0, -1), + ...prev, { - text: lastMessage.text + res.data.v, + 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") { + if (res.event === "presence") { setMessages((prev) => { - let prevMessages = prev; - // if last message is presence, remove it - if ( - prevMessages[prevMessages.length - 1]?.type === "presence" - ) { - prevMessages = prevMessages.slice(0, -1); + 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" }, + ]; } - - return [ - ...prevMessages, - { - type: "send_transaction", - data: res.data, - }, - ]; + // otherwise, add a new message + return [...prev, { text: res.data.data, type: "presence" }]; }); } - } - }, - 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; - }); - } finally { - setIsChatStreaming(false); - } - } - - async function handleUpdateConfig(newConfig: ExecuteConfig) { - setConfig(newConfig); - - try { - if (!sessionId) { - // If no session exists, create a new one - await initSession(); - } else { - await updateSession({ - authToken: props.authToken, - config: newConfig, - sessionId, - contextFilters, + 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, + }, + ]; + }); + } + } + }, + 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; }); + } finally { + setIsChatStreaming(false); } - } catch (error) { - console.error("Failed to update session", error); - setMessages((prev) => [ - ...prev, - { - text: `Error: Failed to ${sessionId ? "update" : "create"} session`, - type: "error", - }, - ]); - } - } + }, + [ + sessionId, + contextFilters, + config, + props.authToken, + messages.length, + initSession, + ], + ); - const updateConfig = useMutation({ - mutationFn: handleUpdateConfig, - }); + const hasDoneAutoPrompt = useRef(false); + useEffect(() => { + if ( + props.initialPrompt && + messages.length === 0 && + !hasDoneAutoPrompt.current + ) { + hasDoneAutoPrompt.current = true; + handleSendMessage(props.initialPrompt); + } + }, [props.initialPrompt, messages.length, handleSendMessage]); const showEmptyState = !userHasSubmittedMessage && messages.length === 0; @@ -345,13 +340,7 @@ export function ChatPageContent(props: {
{showEmptyState ? (
- { - updateConfig.mutate(config); - }} - /> +
) : (
@@ -376,11 +365,7 @@ export function ChatPageContent(props: { { - updateConfig.mutate(config); - }} abortChatStream={() => { chatAbortController?.abort(); setChatAbortController(undefined); 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 cb73d8486f1..e54cc308b11 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 @@ -29,44 +29,19 @@ export const Mobile: Story = { function Story() { return (
- + {}} - config={{ - mode: "client", - signer_wallet_address: "xxxxx", - }} isChatStreaming={false} sendMessage={() => {}} - updateConfig={() => {}} /> - + {}} - config={{ - mode: "client", - signer_wallet_address: "xxxxx", - }} isChatStreaming={true} sendMessage={() => {}} - updateConfig={() => {}} - /> - - - - {}} - config={{ - mode: "engine", - engine_authorization_token: "xxxxx", - engine_backend_wallet_address: "0x1234", - engine_url: "https://some-engine-url.com", - }} - isChatStreaming={false} - sendMessage={() => {}} - updateConfig={() => {}} />
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 4cb7d39c107..eee82fe6dc8 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 @@ -29,14 +29,7 @@ export const Mobile: Story = { function Story() { return (
- {}} - updateConfig={() => {}} - /> + {}} />
); } 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 21903104452..72626890f78 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx @@ -2,14 +2,11 @@ import { ArrowUpRightIcon } from "lucide-react"; import { Button } from "../../../../@/components/ui/button"; -import type { ExecuteConfig } from "../api/types"; import { NebulaIcon } from "../icons/NebulaIcon"; import { Chatbar } from "./ChatBar"; export function EmptyStateChatPageContent(props: { - updateConfig: (config: ExecuteConfig) => void; sendMessage: (message: string) => void; - config: ExecuteConfig; }) { return (
@@ -31,9 +28,7 @@ export function EmptyStateChatPageContent(props: {
{ // the page will switch so, no need to handle abort here }} diff --git a/apps/dashboard/src/app/nebula-app/(app)/page.tsx b/apps/dashboard/src/app/nebula-app/(app)/page.tsx index 136b38d5a2e..adde8c1b4fd 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/page.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/page.tsx @@ -6,8 +6,15 @@ import { import { loginRedirect } from "../../login/loginRedirect"; import { ChatPageContent } from "./components/ChatPageContent"; -export default async function Page() { - const authToken = await getAuthToken(); +export default async function Page(props: { + searchParams: Promise<{ + prompt?: string; + }>; +}) { + const [searchParams, authToken] = await Promise.all([ + props.searchParams, + getAuthToken(), + ]); if (!authToken) { loginRedirect(); @@ -27,6 +34,7 @@ export default async function Page() { session={undefined} type="landing" account={account} + initialPrompt={searchParams.prompt} /> ); } diff --git a/apps/dashboard/src/app/nebula-app/login/NebulaLoginPage.tsx b/apps/dashboard/src/app/nebula-app/login/NebulaLoginPage.tsx new file mode 100644 index 00000000000..ede0bfc11b2 --- /dev/null +++ b/apps/dashboard/src/app/nebula-app/login/NebulaLoginPage.tsx @@ -0,0 +1,68 @@ +"use client"; + +import Link from "next/link"; +import { useState } from "react"; +import { EmptyStateChatPageContent } from "../(app)/components/EmptyStateChatPageContent"; +import { NebulaIcon } from "../(app)/icons/NebulaIcon"; +import { Button } from "../../../@/components/ui/button"; +import type { Account } from "../../../@3rdweb-sdk/react/hooks/useApi"; +import { LoginAndOnboardingPageContent } from "../../login/LoginPage"; + +export function NebulaLoginPage(props: { + account: Account | undefined; +}) { + const [message, setMessage] = useState(undefined); + const [showLoginPage, setShowLoginPage] = useState(false); + + return ( +
+
+
+ + +
+ + Support + + + + Docs + + + {!showLoginPage && ( + + )} +
+
+
+ + {showLoginPage ? ( + + ) : ( +
+ { + setMessage(msg); + setShowLoginPage(true); + }} + /> +
+ )} +
+ ); +} diff --git a/apps/dashboard/src/app/nebula-app/login/page.tsx b/apps/dashboard/src/app/nebula-app/login/page.tsx index 05e92f2870b..2e9d930936c 100644 --- a/apps/dashboard/src/app/nebula-app/login/page.tsx +++ b/apps/dashboard/src/app/nebula-app/login/page.tsx @@ -1,19 +1,8 @@ import { getRawAccount } from "../../account/settings/getAccount"; -import { LoginAndOnboardingPage } from "../../login/LoginPage"; -import { isValidEncodedRedirectPath } from "../../login/isValidEncodedRedirectPath"; +import { NebulaLoginPage } from "./NebulaLoginPage"; -export default async function NebulaLogin(props: { - searchParams: Promise<{ - next?: string; - }>; -}) { - const nextPath = (await props.searchParams).next; +export default async function NebulaLogin() { const account = await getRawAccount(); - const redirectPath = - nextPath && isValidEncodedRedirectPath(nextPath) ? nextPath : "/"; - - return ( - - ); + return ; }