Skip to content

Commit 1f2bb78

Browse files
[Portal] Add AI chat functionality with markdown support
1 parent 6b73f43 commit 1f2bb78

26 files changed

+1561
-155
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/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="flex min-h-screen w-full items-start justify-center pt-10">
16+
<Chat prefilledMessage={query || undefined} />
17+
</div>
18+
</QueryClientProvider>
19+
);
20+
}

apps/portal/src/app/globals.css

Lines changed: 58 additions & 68 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,44 @@ code span {
117108
}
118109
}
119110

120-
.styled-scrollbar::-webkit-scrollbar {
121-
width: 0.5rem;
122-
height: 0.5rem;
123-
}
124-
125-
@media (max-width: 640px) {
126-
.styled-scrollbar::-webkit-scrollbar {
127-
width: 0;
128-
height: 0;
129-
}
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;
130118
}
131119

132-
.styled-scrollbar::-webkit-scrollbar-thumb {
133-
border-radius: 0.5rem;
134-
transition: color 200ms ease;
135-
background: var(--border);
120+
.shiki,
121+
.shiki span {
122+
background-color: transparent !important;
136123
}
137124

138-
.styled-scrollbar::-webkit-scrollbar-thumb:hover {
139-
background: hsl(var(--foreground));
140-
}
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;
141134

142-
.styled-scrollbar::-webkit-scrollbar-track {
143-
background-color: transparent;
135+
/* Revert background color */
136+
transition: background-color 5000s ease-in-out 0s;
144137
}
145138

146-
button {
147-
-webkit-tap-highlight-color: transparent;
148-
}
149-
150-
::selection {
151-
background: hsl(var(--foreground));
152-
color: hsl(var(--background));
153-
}
154-
155-
.hide-scrollbar {
156-
scrollbar-width: none; /* Firefox */
157-
}
158-
159-
.hide-scrollbar::-webkit-scrollbar {
160-
display: none; /* Safari and Chrome */
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+
}
161151
}

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)