Skip to content

Commit 462138d

Browse files
committed
Add Nebula Chat UI (#5483)
DASH-444 <!-- start pr-codex --> --- ## 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}` <!-- end pr-codex -->
1 parent 67305d3 commit 462138d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+3553
-301
lines changed

apps/dashboard/.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,7 @@ NEXT_PUBLIC_TURNSTILE_SITE_KEY=""
9494
TURNSTILE_SECRET_KEY=""
9595
REDIS_URL=""
9696

97-
ANALYTICS_SERVICE_URL=""
97+
ANALYTICS_SERVICE_URL=""
98+
99+
# Required for Nebula Chat
100+
NEXT_PUBLIC_NEBULA_URL=""

apps/dashboard/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"color": "^4.2.3",
6161
"compare-versions": "^6.1.0",
6262
"date-fns": "4.1.0",
63+
"fetch-event-stream": "0.1.5",
6364
"flat": "^6.0.1",
6465
"framer-motion": "11.13.3",
6566
"fuse.js": "7.0.0",

apps/dashboard/src/@/api/team.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@ import "server-only";
22
import { API_SERVER_URL } from "@/constants/env";
33
import { getAuthToken } from "../../app/api/lib/getAuthToken";
44

5+
type EnabledTeamScope =
6+
| "pay"
7+
| "storage"
8+
| "rpc"
9+
| "bundler"
10+
| "insight"
11+
| "embeddedWallets"
12+
| "relayer"
13+
| "chainsaw"
14+
| "nebula";
15+
516
export type Team = {
617
id: string;
718
name: string;
@@ -15,6 +26,7 @@ export type Team = {
1526
billingStatus: "validPayment" | (string & {}) | null;
1627
billingEmail: string | null;
1728
growthTrialEligible: boolean | null;
29+
enabledScopes: EnabledTeamScope[];
1830
};
1931

2032
export async function getTeamBySlug(slug: string) {

apps/dashboard/src/@/components/blocks/DangerSettingCard.stories.tsx

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,36 +28,34 @@ export const Mobile: Story = {
2828

2929
function Story() {
3030
return (
31-
<div className="min-h-screen bg-background py-4 text-foreground">
32-
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
33-
<BadgeContainer label="Base">
34-
<DangerSettingCard
35-
title="This is a title"
36-
description="This is a description"
37-
buttonLabel="Some Action"
38-
buttonOnClick={() => {}}
39-
isPending={false}
40-
confirmationDialog={{
41-
title: "This is confirmation title",
42-
description: "This is confirmation description",
43-
}}
44-
/>
45-
</BadgeContainer>
31+
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
32+
<BadgeContainer label="Base">
33+
<DangerSettingCard
34+
title="This is a title"
35+
description="This is a description"
36+
buttonLabel="Some Action"
37+
buttonOnClick={() => {}}
38+
isPending={false}
39+
confirmationDialog={{
40+
title: "This is confirmation title",
41+
description: "This is confirmation description",
42+
}}
43+
/>
44+
</BadgeContainer>
4645

47-
<BadgeContainer label="Loading">
48-
<DangerSettingCard
49-
title="This is a title"
50-
description="This is a description"
51-
buttonLabel="Some Action"
52-
buttonOnClick={() => {}}
53-
isPending={true}
54-
confirmationDialog={{
55-
title: "This is confirmation title",
56-
description: "This is confirmation description",
57-
}}
58-
/>
59-
</BadgeContainer>
60-
</div>
46+
<BadgeContainer label="Loading">
47+
<DangerSettingCard
48+
title="This is a title"
49+
description="This is a description"
50+
buttonLabel="Some Action"
51+
buttonOnClick={() => {}}
52+
isPending={true}
53+
confirmationDialog={{
54+
title: "This is confirmation title",
55+
description: "This is confirmation description",
56+
}}
57+
/>
58+
</BadgeContainer>
6159
</div>
6260
);
6361
}

apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ export function MultiNetworkSelector(props: {
7272
onSelectedValuesChange={(chainIds) => {
7373
props.onChange(chainIds.map(Number));
7474
}}
75-
placeholder="Select Chains"
75+
placeholder={
76+
allChains.length === 0 ? "Loading Chains..." : "Select Chains"
77+
}
78+
disabled={allChains.length === 0}
7679
overrideSearchFn={searchFn}
7780
renderOption={renderOption}
7881
/>

apps/dashboard/src/@/components/blocks/multi-select.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
5959
maxCount = Number.POSITIVE_INFINITY,
6060
className,
6161
selectedValues,
62+
overrideSearchFn,
63+
renderOption,
64+
searchPlaceholder,
6265
...props
6366
},
6467
ref,
@@ -105,8 +108,6 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
105108
// show 50 initially and then 20 more when reaching the end
106109
const { itemsToShow, lastItemRef } = useShowMore<HTMLButtonElement>(50, 20);
107110

108-
const { overrideSearchFn } = props;
109-
110111
const optionsToShow = useMemo(() => {
111112
const filteredOptions: {
112113
label: string;
@@ -152,7 +153,7 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
152153
}, [searchValue]);
153154

154155
return (
155-
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
156+
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal>
156157
<PopoverTrigger asChild>
157158
<Button
158159
ref={ref}
@@ -238,7 +239,7 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
238239
{/* Search */}
239240
<div className="relative">
240241
<Input
241-
placeholder={props.searchPlaceholder || "Search"}
242+
placeholder={searchPlaceholder || "Search"}
242243
value={searchValue}
243244
onChange={(e) => setSearchValue(e.target.value)}
244245
className="!h-auto rounded-b-none border-0 border-border border-b py-4 pl-10 focus-visible:ring-0 focus-visible:ring-offset-0"
@@ -285,9 +286,7 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
285286
</div>
286287

287288
<div className="min-w-0 grow">
288-
{props.renderOption
289-
? props.renderOption(option)
290-
: option.label}
289+
{renderOption ? renderOption(option) : option.label}
291290
</div>
292291
</Button>
293292
);

apps/dashboard/src/@/components/ui/ScrollShadow/ScrollShadow.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export function ScrollShadow(props: {
1111
scrollableClassName?: string;
1212
disableTopShadow?: boolean;
1313
shadowColor?: string;
14+
shadowClassName?: string;
1415
}) {
1516
const scrollableEl = useRef<HTMLDivElement>(null);
1617
const shadowTopEl = useRef<HTMLDivElement>(null);
@@ -94,29 +95,45 @@ export function ScrollShadow(props: {
9495
}
9596
>
9697
<div
97-
className={cn(styles.scrollShadowTop, styles.scrollShadowY)}
98+
className={cn(
99+
styles.scrollShadowTop,
100+
styles.scrollShadowY,
101+
props.shadowClassName,
102+
)}
98103
ref={shadowTopEl}
99104
style={{
100105
opacity: "0",
101106
display: props.disableTopShadow ? "none" : "block",
102107
}}
103108
/>
104109
<div
105-
className={cn(styles.scrollShadowBottom, styles.scrollShadowY)}
110+
className={cn(
111+
styles.scrollShadowBottom,
112+
styles.scrollShadowY,
113+
props.shadowClassName,
114+
)}
106115
ref={shadowBottomEl}
107116
style={{
108117
opacity: "0",
109118
}}
110119
/>
111120
<div
112-
className={cn(styles.scrollShadowLeft, styles.scrollShadowX)}
121+
className={cn(
122+
styles.scrollShadowLeft,
123+
styles.scrollShadowX,
124+
props.shadowClassName,
125+
)}
113126
ref={shadowLeftEl}
114127
style={{
115128
opacity: "0",
116129
}}
117130
/>
118131
<div
119-
className={cn(styles.scrollShadowRight, styles.scrollShadowX)}
132+
className={cn(
133+
styles.scrollShadowRight,
134+
styles.scrollShadowX,
135+
props.shadowClassName,
136+
)}
120137
ref={shadowRightEl}
121138
style={{
122139
opacity: "0",

apps/dashboard/src/@/components/ui/code/code.client.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type CodeProps = {
1212
scrollableClassName?: string;
1313
keepPreviousDataOnCodeChange?: boolean;
1414
copyButtonClassName?: string;
15+
ignoreFormattingErrors?: boolean;
1516
};
1617

1718
export const CodeClient: React.FC<CodeProps> = ({
@@ -21,10 +22,14 @@ export const CodeClient: React.FC<CodeProps> = ({
2122
scrollableClassName,
2223
keepPreviousDataOnCodeChange = false,
2324
copyButtonClassName,
25+
ignoreFormattingErrors,
2426
}) => {
2527
const codeQuery = useQuery({
2628
queryKey: ["html", code],
27-
queryFn: () => getCodeHtml(code, lang),
29+
queryFn: () =>
30+
getCodeHtml(code, lang, {
31+
ignoreFormattingErrors: ignoreFormattingErrors,
32+
}),
2833
placeholderData: keepPreviousDataOnCodeChange
2934
? keepPreviousData
3035
: undefined,

apps/dashboard/src/@/components/ui/code/getCodeHtml.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,28 @@ function isPrettierSupportedLang(lang: BundledLanguage) {
1616
);
1717
}
1818

19-
export async function getCodeHtml(code: string, lang: BundledLanguage) {
19+
export async function getCodeHtml(
20+
code: string,
21+
lang: BundledLanguage,
22+
options?: {
23+
ignoreFormattingErrors?: boolean;
24+
},
25+
) {
2026
const formattedCode = isPrettierSupportedLang(lang)
2127
? await format(code, {
2228
parser: "babel-ts",
2329
plugins: [parserBabel, estree],
2430
printWidth: 60,
2531
}).catch((e) => {
26-
console.error(e);
27-
console.error("Failed to format code");
32+
if (!options?.ignoreFormattingErrors) {
33+
console.error(e);
34+
console.error("Failed to format code");
35+
console.log({
36+
code,
37+
lang,
38+
});
39+
}
40+
2841
return code;
2942
})
3043
: code;

apps/dashboard/src/@/components/ui/inline-code.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export function InlineCode({
77
return (
88
<code
99
className={cn(
10-
"mx-0.5 inline rounded-lg border border-border px-1.5 py-[3px] font-mono text-[0.85em] text-foreground",
10+
"mx-0.5 inline rounded-lg border border-border bg-muted px-[0.4em] py-[0.25em] font-mono text-[0.85em] text-foreground",
1111
className,
1212
)}
1313
>

apps/dashboard/src/@/components/ui/tabs.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,22 @@ function useUnderline<El extends HTMLElement>() {
171171
}
172172

173173
update();
174+
let resizeObserver: ResizeObserver | undefined = undefined;
175+
176+
if (containerRef.current) {
177+
resizeObserver = new ResizeObserver(() => {
178+
setTimeout(() => {
179+
update();
180+
}, 100);
181+
});
182+
resizeObserver.observe(containerRef.current);
183+
}
184+
174185
// add event listener for resize
175186
window.addEventListener("resize", update);
176187
return () => {
177188
window.removeEventListener("resize", update);
189+
resizeObserver?.disconnect();
178190
};
179191
}, [activeTabEl]);
180192

apps/dashboard/src/@/components/ui/textarea.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,27 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
2222
Textarea.displayName = "Textarea";
2323

2424
export { Textarea };
25+
26+
export function AutoResizeTextarea(props: TextareaProps) {
27+
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
28+
29+
// biome-ignore lint/correctness/useExhaustiveDependencies: value is needed in deps array
30+
React.useEffect(() => {
31+
const textarea = textareaRef.current;
32+
if (textarea) {
33+
textarea.style.height = "auto";
34+
textarea.style.height = `${textarea.scrollHeight}px`;
35+
}
36+
}, [props.value]);
37+
38+
return (
39+
<Textarea
40+
ref={textareaRef}
41+
{...props}
42+
style={{
43+
...props.style,
44+
overflowY: "hidden",
45+
}}
46+
/>
47+
);
48+
}

apps/dashboard/src/@/constants/env.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,5 @@ export const BASE_URL = isProd
3939
: (process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL
4040
? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}`
4141
: "http://localhost:3000") || "https://thirdweb-dev.com";
42+
43+
export const NEXT_PUBLIC_NEBULA_URL = process.env.NEXT_PUBLIC_NEBULA_URL;

apps/dashboard/src/app/account/settings/getAccount.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export async function getRawAccount() {
3636
* If there's no account or account onboarding not complete, redirect to login page
3737
* @param pagePath - the path of the current page to redirect back to after login/onboarding
3838
*/
39-
export async function getValidAccount(pagePath: string) {
39+
export async function getValidAccount(pagePath?: string) {
4040
const account = await getRawAccount();
4141

4242
// enforce login & onboarding

apps/dashboard/src/app/components/Header/SecondaryNav/SecondaryNav.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export function SecondaryNavLinks() {
3030
<ResourcesDropdownButton />
3131

3232
<Link
33-
href="/support"
33+
target="_blank"
34+
href="https://thirdweb.com/support"
3435
className="text-muted-foreground text-sm hover:text-foreground"
3536
>
3637
Support

0 commit comments

Comments
 (0)