Skip to content

Commit c3bbf17

Browse files
[Portal] Add AI chat functionality with markdown support
1 parent 41971d7 commit c3bbf17

27 files changed

+1633
-168
lines changed

apps/portal/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@radix-ui/react-select": "^2.2.2",
2929
"@radix-ui/react-slot": "^1.2.0",
3030
"@radix-ui/react-tabs": "^1.1.8",
31+
"@radix-ui/react-tooltip": "1.2.3",
3132
"@tanstack/react-query": "5.74.4",
3233
"@tryghost/content-api": "^1.11.22",
3334
"class-variance-authority": "^0.7.1",
@@ -45,8 +46,10 @@
4546
"posthog-js": "1.67.1",
4647
"prettier": "3.5.3",
4748
"react": "19.1.0",
49+
"react-children-utilities": "^2.10.0",
4850
"react-dom": "19.1.0",
4951
"react-html-parser": "2.0.2",
52+
"react-markdown": "^9.0.1",
5053
"remark-gfm": "3.0.1",
5154
"server-only": "^0.0.1",
5255
"shiki": "1.27.0",

apps/portal/src/app/Header.tsx

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ import {
99
DropdownMenuTrigger,
1010
} from "@/components/ui/dropdown-menu";
1111
import clsx from "clsx";
12-
import { ChevronDownIcon, MenuIcon, TableOfContentsIcon } from "lucide-react";
12+
import {
13+
BotIcon,
14+
ChevronDownIcon,
15+
MenuIcon,
16+
TableOfContentsIcon,
17+
} from "lucide-react";
1318
import Link from "next/link";
14-
import { usePathname } from "next/navigation";
19+
import { usePathname, useRouter } from "next/navigation";
1520
import { useState } from "react";
1621
import { GithubIcon } from "../components/Document/GithubButtonLink";
1722
import { CustomAccordion } from "../components/others/CustomAccordion";
@@ -29,7 +34,6 @@ const links = [
2934
{
3035
name: "Connect",
3136
href: "/connect",
32-
icon: TableOfContentsIcon,
3337
},
3438
{
3539
name: "Bridge",
@@ -191,6 +195,7 @@ const supportLinks = [
191195

192196
export function Header() {
193197
const [showBurgerMenu, setShowBurgerMenu] = useState(false);
198+
const router = useRouter();
194199

195200
return (
196201
<header className="flex w-full flex-col gap-2 border-b bg-background p-2 lg:px-4">
@@ -211,24 +216,46 @@ export function Header() {
211216
</div>
212217

213218
<div className="flex items-center gap-3">
214-
<div className="hidden xl:flex">
215-
<ThemeSwitcher />
219+
<div className="hidden xl:block">
220+
<DocSearch variant="search" />
216221
</div>
217222

218223
<div className="hidden xl:block">
219-
<DocSearch variant="search" />
224+
<Button
225+
variant="primary"
226+
onClick={() => {
227+
router.push("/chat");
228+
}}
229+
>
230+
<BotIcon className="mr-2 size-4" />
231+
Ask AI
232+
</Button>
220233
</div>
221234

222-
<div className="flex items-center gap-1 xl:hidden">
235+
<div className="hidden xl:block">
223236
<ThemeSwitcher className="border-none bg-transparent" />
224-
<DocSearch variant="icon" />
237+
</div>
238+
239+
<div className="hidden xl:block">
225240
<Link
226241
href="https://github.com/thirdweb-dev"
227242
target="_blank"
228243
className="text-foreground"
229244
>
230245
<GithubIcon className="mx-3 size-6" />
231246
</Link>
247+
</div>
248+
249+
<div className="flex items-center gap-1 xl:hidden">
250+
<ThemeSwitcher className="border-none bg-transparent" />
251+
<DocSearch variant="icon" />
252+
<Button
253+
variant="ghost"
254+
className="p-2"
255+
onClick={() => router.push("/chat")}
256+
>
257+
<BotIcon className="size-7" />
258+
</Button>
232259
<Button
233260
variant="ghost"
234261
className="p-2"
@@ -260,12 +287,6 @@ export function Header() {
260287
</li>
261288
);
262289
})}
263-
264-
<DropdownLinks
265-
links={toolLinks}
266-
onLinkClick={() => setShowBurgerMenu(false)}
267-
category="Tools"
268-
/>
269290
</ul>
270291
</nav>
271292

@@ -285,6 +306,14 @@ export function Header() {
285306
/>
286307
</div>
287308

309+
<div className="px-1">
310+
<DropdownLinks
311+
links={toolLinks}
312+
onLinkClick={() => setShowBurgerMenu(false)}
313+
category="Tools"
314+
/>
315+
</div>
316+
288317
<div className="px-1">
289318
<DropdownLinks
290319
links={supportLinks}
@@ -300,14 +329,6 @@ export function Header() {
300329
setShowBurgerMenu(false);
301330
}}
302331
/>
303-
304-
<Link
305-
href="https://github.com/thirdweb-dev"
306-
target="_blank"
307-
className="text-muted-foreground transition-colors hover:text-foreground"
308-
>
309-
<GithubIcon className="mx-2 size-6" />
310-
</Link>
311332
</div>
312333
</div>
313334

apps/portal/src/app/chat/page.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"use client";
2+
3+
import { Chat } from "@/components/AI/chat";
4+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5+
import { useSearchParams } from "next/navigation";
6+
7+
const queryClient = new QueryClient();
8+
9+
export default function ChatPage() {
10+
const searchParams = useSearchParams();
11+
const query = searchParams.get("query");
12+
13+
return (
14+
<QueryClientProvider client={queryClient}>
15+
<div className="m-auto flex h-[calc(100vh-4rem)] w-full flex-col overflow-hidden lg:size-[calc(100vh-8rem)]">
16+
<Chat prefilledMessage={query || undefined} />
17+
</div>
18+
</QueryClientProvider>
19+
);
20+
}

apps/portal/src/app/globals.css

Lines changed: 66 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,7 @@
22
@tailwind components;
33
@tailwind utilities;
44

5-
html {
6-
/* scroll-behavior: smooth; */
7-
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
8-
font-feature-settings: "rlig" 1, "calt" 0;
9-
text-rendering: optimizeLegibility;
10-
-webkit-font-smoothing: antialiased;
11-
font-variation-settings: normal;
12-
-webkit-text-size-adjust: 100%;
13-
-webkit-tap-highlight-color: transparent;
14-
}
15-
165
@layer base {
17-
:root {
18-
--radius: 0.5rem;
19-
--sticky-top-height: 70px;
20-
}
21-
226
:root {
237
/* bg - neutral */
248
--background: 0 0% 98%;
@@ -27,7 +11,7 @@ html {
2711
--secondary: 0 0% 90%;
2812
--muted: 0 0% 93%;
2913
--accent: 0 0% 93%;
30-
--inverted: 0 0 0%;
14+
--inverted: 0 0% 4%;
3115

3216
/* bg - colorful */
3317
--primary: 221 83% 54%;
@@ -46,13 +30,26 @@ html {
4630
--link-foreground: 221.21deg 83.19% 53.33%;
4731
--success-text: 142.09 70.56% 35.29%;
4832
--warning-text: 38 92% 40%;
49-
--destructive-text: 357.15deg 100% 68.72%;
33+
--destructive-text: 360 72% 60%;
5034

5135
/* Borders */
5236
--border: 0 0% 85%;
5337
--active-border: 0 0% 70%;
5438
--input: 0 0% 85%;
5539
--ring: 0 0% 80%;
40+
41+
/* Others */
42+
--radius: 0.5rem;
43+
44+
/* Sidebar */
45+
--sidebar-background: 0 0% 98%;
46+
--sidebar-foreground: 0 0% 4%;
47+
--sidebar-primary: 221 83% 54%;
48+
--sidebar-primary-foreground: 0 0% 100%;
49+
--sidebar-accent: 0 0% 93%;
50+
--sidebar-accent-foreground: 0 0% 9%;
51+
--sidebar-border: 0 0% 85%;
52+
--sidebar-ring: 0 0% 80%;
5653
}
5754

5855
.dark {
@@ -89,23 +86,17 @@ html {
8986
--active-border: 0 0% 22%;
9087
--ring: 0 0% 30%;
9188
--input: 0 0% 15%;
92-
}
93-
}
94-
95-
.dark .light-only {
96-
display: none;
97-
}
98-
99-
html:not(.dark) .dark-only {
100-
display: none;
101-
}
10289

103-
code span {
104-
color: var(--code-light-color);
105-
}
106-
107-
.dark code span {
108-
color: var(--code-dark-color);
90+
/* sidebar */
91+
--sidebar-background: 0 0% 0%;
92+
--sidebar-foreground: 0 0% 98%;
93+
--sidebar-primary: 221 83% 54%;
94+
--sidebar-primary-foreground: 0 0% 100%;
95+
--sidebar-accent: 0 0% 11%;
96+
--sidebar-accent-foreground: 0 0% 98%;
97+
--sidebar-border: 0 0% 15%;
98+
--sidebar-ring: 0 0% 30%;
99+
}
109100
}
110101

111102
@layer base {
@@ -117,45 +108,60 @@ code span {
117108
}
118109
}
119110

120-
.styled-scrollbar::-webkit-scrollbar {
121-
width: 0.5rem;
122-
height: 0.5rem;
111+
.dark .shiki,
112+
.dark .shiki span {
113+
color: var(--shiki-dark) !important;
114+
/* Optional, if you also want font styles */
115+
font-style: var(--shiki-dark-font-style) !important;
116+
font-weight: var(--shiki-dark-font-weight) !important;
117+
text-decoration: var(--shiki-dark-text-decoration) !important;
123118
}
124119

125-
@media (max-width: 640px) {
126-
.styled-scrollbar::-webkit-scrollbar {
127-
width: 0;
128-
height: 0;
129-
}
120+
.shiki,
121+
.shiki span {
122+
background-color: transparent !important;
130123
}
131124

132-
.styled-scrollbar::-webkit-scrollbar-thumb {
133-
border-radius: 0.5rem;
134-
transition: color 200ms ease;
135-
background: var(--border);
136-
}
125+
/* Fix colors on auto-filled inputs */
126+
input:-webkit-autofill,
127+
input:-webkit-autofill:hover,
128+
input:-webkit-autofill:focus,
129+
input:-webkit-autofill:active {
130+
/* Revert text color */
131+
-webkit-text-fill-color: hsl(var(--foreground)) !important;
132+
color: hsl(var(--foreground)) !important;
133+
caret-color: hsl(var(--foreground)) !important;
137134

138-
.styled-scrollbar::-webkit-scrollbar-thumb:hover {
139-
background: hsl(var(--foreground));
135+
/* Revert background color */
136+
transition: background-color 5000s ease-in-out 0s;
140137
}
141138

142-
.styled-scrollbar::-webkit-scrollbar-track {
143-
background-color: transparent;
139+
@layer utilities {
140+
/* Hide scrollbar for Chrome, Safari and Opera */
141+
.no-scrollbar::-webkit-scrollbar,
142+
.no-scrollbar *::-webkit-scrollbar {
143+
display: none;
144+
}
145+
/* Hide scrollbar for IE, Edge and Firefox */
146+
.no-scrollbar,
147+
.no-scrollbar * {
148+
-ms-overflow-style: none; /* IE and Edge */
149+
scrollbar-width: none; /* Firefox */
150+
}
144151
}
145152

146-
button {
147-
-webkit-tap-highlight-color: transparent;
153+
.dark .light-only {
154+
display: none;
148155
}
149156

150-
::selection {
151-
background: hsl(var(--foreground));
152-
color: hsl(var(--background));
157+
html:not(.dark) .dark-only {
158+
display: none;
153159
}
154160

155-
.hide-scrollbar {
156-
scrollbar-width: none; /* Firefox */
161+
code span {
162+
color: var(--code-light-color);
157163
}
158164

159-
.hide-scrollbar::-webkit-scrollbar {
160-
display: none; /* Safari and Chrome */
165+
.dark code span {
166+
color: var(--code-dark-color);
161167
}

apps/portal/src/app/layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import "./globals.css";
22
import { createMetadata } from "@/components/Document";
33
import { PosthogHeadSetup } from "@/lib/posthog/PosthogHeadSetup";
4+
import {} from "@tanstack/react-query";
45
import { ThemeProvider } from "next-themes";
56
import { Fira_Code, Inter } from "next/font/google";
67
import Script from "next/script";

apps/portal/src/components/AI/api.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"use server";
2+
3+
const serviceKey = process.env.SIWA_SERVICE_KEY as string;
4+
const apiUrl = process.env.SIWA_URL;
5+
6+
export const getChatResponse = async (
7+
userMessage: string,
8+
sessionId: string | undefined,
9+
) => {
10+
try {
11+
const payload = {
12+
message: userMessage,
13+
conversationId: sessionId,
14+
};
15+
const response = await fetch(`${apiUrl}/v1/chat`, {
16+
method: "POST",
17+
headers: {
18+
"Content-Type": "application/json",
19+
"x-service-api-key": serviceKey,
20+
},
21+
body: JSON.stringify(payload),
22+
});
23+
24+
if (!response.ok) {
25+
const error = await response.text();
26+
throw new Error(
27+
`Failed to get chat response: ${response.status} - ${error}`,
28+
);
29+
}
30+
31+
const data = (await response.json()) as {
32+
data: string;
33+
conversationId: string;
34+
};
35+
return data;
36+
} catch (error) {
37+
console.error(error);
38+
return null;
39+
}
40+
};

0 commit comments

Comments
 (0)