From 462138dfae9ac880ea2fe3a078f0f604f3c0abb3 Mon Sep 17 00:00:00 2001 From: MananTank Date: Tue, 10 Dec 2024 21:58:32 +0000 Subject: [PATCH] Add Nebula Chat UI (#5483) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DASH-444 --- ## PR-Codex overview This PR primarily focuses on adding new features and enhancements to the `Nebula` application, including updates to environment variables, improved session handling, and new UI components. ### Detailed summary - Added `fetch-event-stream` dependency. - Introduced `NEXT_PUBLIC_NEBULA_URL` in environment variables. - Enhanced `loginRedirect` to handle optional paths. - Updated `getValidAccount` to accept optional `pagePath`. - Implemented new stores for session management. - Improved UI components like `Chatbar`, `ChatPageLayout`, and `EmptyStateChatPageContent`. - Added `isValidEncodedRedirectPath` function for redirect validation. - Modified `NebulaWaitListPage` to directly use team data. - Enhanced session handling functions in `api/session.ts`. - Created new types for session management and context filters. - Updated styles and class names for better UI consistency. > The following files were skipped due to too many changes: `apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx`, `apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx`, `apps/dashboard/src/app/nebula-app/(app)/components/NebulaAccountButton.tsx`, `apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.tsx`, `apps/dashboard/src/app/nebula-app/(app)/chat/history/ChatHistoryPage.tsx`, `apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx`, `apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx`, `pnpm-lock.yaml` > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- apps/dashboard/.env.example | 5 +- apps/dashboard/package.json | 1 + apps/dashboard/src/@/api/team.ts | 12 + .../blocks/DangerSettingCard.stories.tsx | 56 ++- .../@/components/blocks/NetworkSelectors.tsx | 5 +- .../src/@/components/blocks/multi-select.tsx | 13 +- .../ui/ScrollShadow/ScrollShadow.tsx | 25 +- .../src/@/components/ui/code/code.client.tsx | 7 +- .../src/@/components/ui/code/getCodeHtml.tsx | 19 +- .../src/@/components/ui/inline-code.tsx | 2 +- apps/dashboard/src/@/components/ui/tabs.tsx | 12 + .../src/@/components/ui/textarea.tsx | 24 ++ apps/dashboard/src/@/constants/env.ts | 2 + .../src/app/account/settings/getAccount.ts | 2 +- .../Header/SecondaryNav/SecondaryNav.tsx | 3 +- apps/dashboard/src/app/login/LoginPage.tsx | 30 +- .../app/login/isValidEncodedRedirectPath.ts | 12 + apps/dashboard/src/app/login/loginRedirect.ts | 6 +- apps/dashboard/src/app/login/page.tsx | 8 +- .../src/app/nebula-app/(app)/api/chat.ts | 162 ++++++++ .../src/app/nebula-app/(app)/api/feedback.ts | 25 ++ .../src/app/nebula-app/(app)/api/session.ts | 131 +++++++ .../src/app/nebula-app/(app)/api/types.ts | 49 +++ .../(app)/chat/[session_id]/page.tsx | 49 +++ .../chat/history/ChatHistoryPage.stories.tsx | 99 +++++ .../(app)/chat/history/ChatHistoryPage.tsx | 218 +++++++++++ .../nebula-app/(app)/chat/history/page.tsx | 15 + .../src/app/nebula-app/(app)/chat/page.tsx | 32 ++ .../nebula-app/(app)/components/ChatBar.tsx | 70 ++++ .../(app)/components/ChatPageContent.tsx | 367 ++++++++++++++++++ .../components/ChatPageLayout.stories.tsx | 86 ++++ .../(app)/components/ChatPageLayout.tsx | 40 ++ .../(app)/components/ChatSidebar.tsx | 87 +++++ .../(app)/components/ChatSidebarLink.tsx | 91 +++++ .../(app)/components/Chatbar.stories.tsx | 74 ++++ .../(app)/components/Chats.stories.tsx | 216 +++++++++++ .../app/nebula-app/(app)/components/Chats.tsx | 279 +++++++++++++ .../components/ContextFilters.stories.tsx | 73 ++++ .../(app)/components/ContextFilters.tsx | 212 ++++++++++ .../EmptyStateChatPageContent.stories.tsx | 42 ++ .../components/EmptyStateChatPageContent.tsx | 150 +++++++ .../(app)/components/NebulaAccountButton.tsx | 181 +++++++++ .../(app)/components/NebulaMobileNav.tsx | 73 ++++ .../hooks/useSessionsWithLocalOverrides.ts | 13 + .../app/nebula-app/(app)/icons/NebulaIcon.tsx | 20 + .../src/app/nebula-app/(app)/layout.tsx | 103 +++++ .../src/app/nebula-app/(app)/page.tsx | 32 ++ .../src/app/nebula-app/(app)/stores.ts | 9 + .../src/app/nebula-app/login/page.tsx | 19 + apps/dashboard/src/app/nebula-app/readme.md | 10 + .../team/[team_slug]/(team)/~/nebula/page.tsx | 14 +- .../nebula-waitlist-page-ui.client.tsx | 35 +- .../components/nebula-waitlist-page.tsx | 28 +- .../[project_slug]/nebula/page.tsx | 14 +- .../src/components/ClientOnly/ClientOnly.tsx | 5 +- .../markdown-renderer.stories.tsx | 14 +- .../published-contract/markdown-renderer.tsx | 38 +- .../components/notices/AnnouncementBanner.tsx | 22 +- apps/dashboard/src/middleware.ts | 25 +- apps/dashboard/src/stories/stubs.ts | 57 ++- .../dashboard/src/utils/fetchWithAuthToken.ts | 50 +++ pnpm-lock.yaml | 278 ++++++------- turbo.json | 3 +- 63 files changed, 3553 insertions(+), 301 deletions(-) create mode 100644 apps/dashboard/src/app/login/isValidEncodedRedirectPath.ts create mode 100644 apps/dashboard/src/app/nebula-app/(app)/api/chat.ts create mode 100644 apps/dashboard/src/app/nebula-app/(app)/api/feedback.ts create mode 100644 apps/dashboard/src/app/nebula-app/(app)/api/session.ts create mode 100644 apps/dashboard/src/app/nebula-app/(app)/api/types.ts create mode 100644 apps/dashboard/src/app/nebula-app/(app)/chat/[session_id]/page.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/chat/history/ChatHistoryPage.stories.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/chat/history/ChatHistoryPage.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/chat/history/page.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/chat/page.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/ChatBar.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/ChatPageLayout.stories.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/ChatPageLayout.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/ChatSidebar.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/ChatSidebarLink.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/Chatbar.stories.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/Chats.stories.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/Chats.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.stories.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.stories.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/EmptyStateChatPageContent.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/NebulaAccountButton.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/components/NebulaMobileNav.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/hooks/useSessionsWithLocalOverrides.ts create mode 100644 apps/dashboard/src/app/nebula-app/(app)/icons/NebulaIcon.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/layout.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/page.tsx create mode 100644 apps/dashboard/src/app/nebula-app/(app)/stores.ts create mode 100644 apps/dashboard/src/app/nebula-app/login/page.tsx create mode 100644 apps/dashboard/src/app/nebula-app/readme.md create mode 100644 apps/dashboard/src/utils/fetchWithAuthToken.ts diff --git a/apps/dashboard/.env.example b/apps/dashboard/.env.example index d0bd739a0f6..27bf2a4453f 100644 --- a/apps/dashboard/.env.example +++ b/apps/dashboard/.env.example @@ -94,4 +94,7 @@ NEXT_PUBLIC_TURNSTILE_SITE_KEY="" TURNSTILE_SECRET_KEY="" REDIS_URL="" -ANALYTICS_SERVICE_URL="" \ No newline at end of file +ANALYTICS_SERVICE_URL="" + +# Required for Nebula Chat +NEXT_PUBLIC_NEBULA_URL="" \ No newline at end of file diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index c2377dc61f8..71b75059aee 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -60,6 +60,7 @@ "color": "^4.2.3", "compare-versions": "^6.1.0", "date-fns": "4.1.0", + "fetch-event-stream": "0.1.5", "flat": "^6.0.1", "framer-motion": "11.13.3", "fuse.js": "7.0.0", diff --git a/apps/dashboard/src/@/api/team.ts b/apps/dashboard/src/@/api/team.ts index a7ac5fa0ed5..6569df0fa2c 100644 --- a/apps/dashboard/src/@/api/team.ts +++ b/apps/dashboard/src/@/api/team.ts @@ -2,6 +2,17 @@ import "server-only"; import { API_SERVER_URL } from "@/constants/env"; import { getAuthToken } from "../../app/api/lib/getAuthToken"; +type EnabledTeamScope = + | "pay" + | "storage" + | "rpc" + | "bundler" + | "insight" + | "embeddedWallets" + | "relayer" + | "chainsaw" + | "nebula"; + export type Team = { id: string; name: string; @@ -15,6 +26,7 @@ export type Team = { billingStatus: "validPayment" | (string & {}) | null; billingEmail: string | null; growthTrialEligible: boolean | null; + enabledScopes: EnabledTeamScope[]; }; export async function getTeamBySlug(slug: string) { diff --git a/apps/dashboard/src/@/components/blocks/DangerSettingCard.stories.tsx b/apps/dashboard/src/@/components/blocks/DangerSettingCard.stories.tsx index e7124337ae7..5db6a5eb906 100644 --- a/apps/dashboard/src/@/components/blocks/DangerSettingCard.stories.tsx +++ b/apps/dashboard/src/@/components/blocks/DangerSettingCard.stories.tsx @@ -28,36 +28,34 @@ export const Mobile: Story = { function Story() { return ( -
-
- - {}} - isPending={false} - confirmationDialog={{ - title: "This is confirmation title", - description: "This is confirmation description", - }} - /> - +
+ + {}} + isPending={false} + confirmationDialog={{ + title: "This is confirmation title", + description: "This is confirmation description", + }} + /> + - - {}} - isPending={true} - confirmationDialog={{ - title: "This is confirmation title", - description: "This is confirmation description", - }} - /> - -
+ + {}} + isPending={true} + confirmationDialog={{ + title: "This is confirmation title", + description: "This is confirmation description", + }} + /> +
); } diff --git a/apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx b/apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx index d8740d69d23..40cf87b008b 100644 --- a/apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx +++ b/apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx @@ -72,7 +72,10 @@ export function MultiNetworkSelector(props: { onSelectedValuesChange={(chainIds) => { props.onChange(chainIds.map(Number)); }} - placeholder="Select Chains" + placeholder={ + allChains.length === 0 ? "Loading Chains..." : "Select Chains" + } + disabled={allChains.length === 0} overrideSearchFn={searchFn} renderOption={renderOption} /> diff --git a/apps/dashboard/src/@/components/blocks/multi-select.tsx b/apps/dashboard/src/@/components/blocks/multi-select.tsx index 4ca98cb78e3..ae025280626 100644 --- a/apps/dashboard/src/@/components/blocks/multi-select.tsx +++ b/apps/dashboard/src/@/components/blocks/multi-select.tsx @@ -59,6 +59,9 @@ export const MultiSelect = forwardRef( maxCount = Number.POSITIVE_INFINITY, className, selectedValues, + overrideSearchFn, + renderOption, + searchPlaceholder, ...props }, ref, @@ -105,8 +108,6 @@ export const MultiSelect = forwardRef( // show 50 initially and then 20 more when reaching the end const { itemsToShow, lastItemRef } = useShowMore(50, 20); - const { overrideSearchFn } = props; - const optionsToShow = useMemo(() => { const filteredOptions: { label: string; @@ -152,7 +153,7 @@ export const MultiSelect = forwardRef( }, [searchValue]); return ( - + ); diff --git a/apps/dashboard/src/@/components/ui/ScrollShadow/ScrollShadow.tsx b/apps/dashboard/src/@/components/ui/ScrollShadow/ScrollShadow.tsx index a1e08d27cfa..85f605ce09a 100644 --- a/apps/dashboard/src/@/components/ui/ScrollShadow/ScrollShadow.tsx +++ b/apps/dashboard/src/@/components/ui/ScrollShadow/ScrollShadow.tsx @@ -11,6 +11,7 @@ export function ScrollShadow(props: { scrollableClassName?: string; disableTopShadow?: boolean; shadowColor?: string; + shadowClassName?: string; }) { const scrollableEl = useRef(null); const shadowTopEl = useRef(null); @@ -94,7 +95,11 @@ export function ScrollShadow(props: { } >
= ({ @@ -21,10 +22,14 @@ export const CodeClient: React.FC = ({ scrollableClassName, keepPreviousDataOnCodeChange = false, copyButtonClassName, + ignoreFormattingErrors, }) => { const codeQuery = useQuery({ queryKey: ["html", code], - queryFn: () => getCodeHtml(code, lang), + queryFn: () => + getCodeHtml(code, lang, { + ignoreFormattingErrors: ignoreFormattingErrors, + }), placeholderData: keepPreviousDataOnCodeChange ? keepPreviousData : undefined, diff --git a/apps/dashboard/src/@/components/ui/code/getCodeHtml.tsx b/apps/dashboard/src/@/components/ui/code/getCodeHtml.tsx index 43ea0dcd758..bcc6bf5780a 100644 --- a/apps/dashboard/src/@/components/ui/code/getCodeHtml.tsx +++ b/apps/dashboard/src/@/components/ui/code/getCodeHtml.tsx @@ -16,15 +16,28 @@ function isPrettierSupportedLang(lang: BundledLanguage) { ); } -export async function getCodeHtml(code: string, lang: BundledLanguage) { +export async function getCodeHtml( + code: string, + lang: BundledLanguage, + options?: { + ignoreFormattingErrors?: boolean; + }, +) { const formattedCode = isPrettierSupportedLang(lang) ? await format(code, { parser: "babel-ts", plugins: [parserBabel, estree], printWidth: 60, }).catch((e) => { - console.error(e); - console.error("Failed to format code"); + if (!options?.ignoreFormattingErrors) { + console.error(e); + console.error("Failed to format code"); + console.log({ + code, + lang, + }); + } + return code; }) : code; diff --git a/apps/dashboard/src/@/components/ui/inline-code.tsx b/apps/dashboard/src/@/components/ui/inline-code.tsx index 1426ede4d7f..e7e9765242e 100644 --- a/apps/dashboard/src/@/components/ui/inline-code.tsx +++ b/apps/dashboard/src/@/components/ui/inline-code.tsx @@ -7,7 +7,7 @@ export function InlineCode({ return ( diff --git a/apps/dashboard/src/@/components/ui/tabs.tsx b/apps/dashboard/src/@/components/ui/tabs.tsx index 58cde11e601..f74a9e74e6c 100644 --- a/apps/dashboard/src/@/components/ui/tabs.tsx +++ b/apps/dashboard/src/@/components/ui/tabs.tsx @@ -171,10 +171,22 @@ function useUnderline() { } update(); + let resizeObserver: ResizeObserver | undefined = undefined; + + if (containerRef.current) { + resizeObserver = new ResizeObserver(() => { + setTimeout(() => { + update(); + }, 100); + }); + resizeObserver.observe(containerRef.current); + } + // add event listener for resize window.addEventListener("resize", update); return () => { window.removeEventListener("resize", update); + resizeObserver?.disconnect(); }; }, [activeTabEl]); diff --git a/apps/dashboard/src/@/components/ui/textarea.tsx b/apps/dashboard/src/@/components/ui/textarea.tsx index 67ff936bcf6..4144b3d35d1 100644 --- a/apps/dashboard/src/@/components/ui/textarea.tsx +++ b/apps/dashboard/src/@/components/ui/textarea.tsx @@ -22,3 +22,27 @@ const Textarea = React.forwardRef( Textarea.displayName = "Textarea"; export { Textarea }; + +export function AutoResizeTextarea(props: TextareaProps) { + const textareaRef = React.useRef(null); + + // biome-ignore lint/correctness/useExhaustiveDependencies: value is needed in deps array + React.useEffect(() => { + const textarea = textareaRef.current; + if (textarea) { + textarea.style.height = "auto"; + textarea.style.height = `${textarea.scrollHeight}px`; + } + }, [props.value]); + + return ( +