Skip to content

Commit ed65f4c

Browse files
committed
Portal: Move Chat UI in floating widget (#7765)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR primarily focuses on enhancing the chat functionality in the application by introducing a new `ChatButton` component, improving code structure, and refining the chat interface with better loading states and animations. ### Detailed summary - Removed the `Button` component linking to `/chat` in favor of `ChatButton`. - Introduced `ChatButton` component with modal functionality for the chat interface. - Added a `QueryClientProvider` to manage chat state. - Improved conditional rendering and loading states in chat messages. - Enhanced CSS animations for chat components. - Updated key usage in `CodeClient` mapping to ensure unique keys. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a new "Ask AI" button that opens a modal chat interface, accessible across the portal. * The chat modal includes options to reset the conversation and close the modal, with a loading spinner for improved user experience. * **Improvements** * Enhanced chat interface with animated message appearance and a more precise scroll-to-latest-message effect. * Updated code snippet rendering in authentication tabs to ensure unique keys for better performance and reliability. * **Bug Fixes** * Resolved potential issues with rendering multiple code snippets for the same platform by ensuring unique keys. * **Refactor** * Replaced previous "Ask AI" button implementations with the new unified ChatButton component for consistency. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 0483776 commit ed65f4c

File tree

6 files changed

+182
-112
lines changed

6 files changed

+182
-112
lines changed

apps/portal/src/app/Header.tsx

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
"use client";
22

33
import clsx from "clsx";
4-
import {
5-
ChevronDownIcon,
6-
MenuIcon,
7-
MessageCircleIcon,
8-
TableOfContentsIcon,
9-
} from "lucide-react";
4+
import { ChevronDownIcon, MenuIcon, TableOfContentsIcon } from "lucide-react";
105
import Link from "next/link";
116
import { usePathname } from "next/navigation";
127
import { useState } from "react";
@@ -18,6 +13,7 @@ import {
1813
DropdownMenuItem,
1914
DropdownMenuTrigger,
2015
} from "@/components/ui/dropdown-menu";
16+
import { ChatButton } from "../components/AI/chat-button";
2117
import { GithubIcon } from "../components/Document/GithubButtonLink";
2218
import { CustomAccordion } from "../components/others/CustomAccordion";
2319
import { ThemeSwitcher } from "../components/others/theme/ThemeSwitcher";
@@ -242,22 +238,13 @@ export function Header() {
242238
</div>
243239

244240
<div className="hidden xl:block">
245-
<Button asChild>
246-
<Link href="/chat">
247-
<MessageCircleIcon className="mr-2 size-4" />
248-
Ask AI
249-
</Link>
250-
</Button>
241+
<ChatButton />
251242
</div>
252243

253244
<div className="flex items-center gap-1 xl:hidden">
254245
<ThemeSwitcher className="border-none bg-transparent" />
255246
<DocSearch variant="icon" />
256-
<Button className="p-2" asChild variant="ghost">
257-
<Link href="/chat">
258-
<MessageCircleIcon className="size-6" />
259-
</Link>
260-
</Button>
247+
<ChatButton />
261248
<Button
262249
className="p-2"
263250
onClick={() => setShowBurgerMenu(!showBurgerMenu)}

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

Lines changed: 0 additions & 16 deletions
This file was deleted.

apps/portal/src/app/page.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { BotIcon, MessageCircleIcon, WebhookIcon, ZapIcon } from "lucide-react";
1+
import { BotIcon, WebhookIcon, ZapIcon } from "lucide-react";
22
import Image from "next/image";
33
import Link from "next/link";
44
import { Heading } from "@/components/Document";
5-
import { Button } from "@/components/ui/button";
5+
import { ChatButton } from "../components/AI/chat-button";
66
import {
77
DotNetIcon,
88
ExternalLinkIcon,
@@ -18,6 +18,7 @@ import { InsightIcon } from "../icons/products/InsightIcon";
1818
import { PlaygroundIcon } from "../icons/products/PlaygroundIcon";
1919
import DocsHeroDark from "./_images/docs-hero-dark.png";
2020
import DocsHeroLight from "./_images/docs-hero-light.png";
21+
2122
export default function Page() {
2223
return (
2324
<main className="container max-w-5xl grow pb-20" data-noindex>
@@ -44,12 +45,7 @@ function Hero() {
4445
Development framework for building onchain apps, games, and agents.
4546
</p>
4647
<div className="flex">
47-
<Button className="flex items-center gap-2" asChild>
48-
<Link href="/chat">
49-
<MessageCircleIcon className="size-4" />
50-
Ask AI
51-
</Link>
52-
</Button>
48+
<ChatButton />
5349
</div>
5450
</div>
5551
</div>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"use client";
2+
3+
import { MessageCircleIcon, RefreshCcwIcon, XIcon } from "lucide-react";
4+
import { lazy, Suspense, useCallback, useState } from "react";
5+
import { Button } from "@/components/ui/button";
6+
import { cn } from "@/lib/utils";
7+
import { Spinner } from "../ui/Spinner/Spinner";
8+
9+
const Chat = lazy(() =>
10+
import("./chat").then((mod) => ({ default: mod.Chat })),
11+
);
12+
13+
export function ChatButton() {
14+
const [isOpen, setIsOpen] = useState(false);
15+
const [hasBeenOpened, setHasBeenOpened] = useState(false);
16+
const closeModal = useCallback(() => setIsOpen(false), []);
17+
const [id, setId] = useState(0);
18+
19+
return (
20+
<>
21+
{/* Inline Button (not floating) */}
22+
<Button
23+
className="gap-2 rounded-full shadow-lg"
24+
onClick={() => {
25+
setIsOpen(true);
26+
setHasBeenOpened(true);
27+
}}
28+
variant="default"
29+
>
30+
<MessageCircleIcon className="size-4" />
31+
Ask AI
32+
</Button>
33+
34+
{/* Popup/Modal */}
35+
<div
36+
className={cn(
37+
"slide-in-from-bottom-20 zoom-in-95 fade-in-0 fixed bottom-0 left-0 z-modal flex h-[80vh] w-[100vw] animate-in flex-col overflow-hidden rounded-t-2xl border bg-background shadow-2xl duration-200 lg:right-6 lg:bottom-6 lg:left-auto lg:h-[80vh] lg:max-w-xl lg:rounded-xl",
38+
!isOpen && "hidden",
39+
)}
40+
>
41+
{/* Header with close button */}
42+
<div className="flex items-center justify-between border-b p-4">
43+
<div className="flex items-center gap-2 font-medium text-lg pl-0.5">
44+
Ask AI
45+
</div>
46+
47+
<div className="flex items-center gap-2">
48+
<Button
49+
className="size-auto p-1 text-muted-foreground rounded-full"
50+
onClick={() => setId((x) => x + 1)}
51+
size="icon"
52+
aria-label="Reset chat"
53+
variant="ghost"
54+
>
55+
<RefreshCcwIcon className="size-5" />
56+
</Button>
57+
58+
<Button
59+
aria-label="Close chat"
60+
className="size-auto p-1 text-muted-foreground rounded-full"
61+
onClick={closeModal}
62+
size="icon"
63+
variant="ghost"
64+
>
65+
<XIcon className="size-5" />
66+
</Button>
67+
</div>
68+
</div>
69+
{/* Chat Content */}
70+
<div className="flex grow flex-col overflow-hidden relative">
71+
{hasBeenOpened && (
72+
<Suspense fallback={<ChatLoading />}>
73+
<Chat key={id} />
74+
</Suspense>
75+
)}
76+
</div>
77+
</div>
78+
</>
79+
);
80+
}
81+
82+
function ChatLoading() {
83+
return (
84+
<div className="flex items-center justify-center p-8 absolute inset-0">
85+
<Spinner className="size-10" />
86+
</div>
87+
);
88+
}

apps/portal/src/components/AI/chat.tsx

Lines changed: 68 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
"use client";
22

3-
import { useMutation } from "@tanstack/react-query";
3+
import {
4+
QueryClient,
5+
QueryClientProvider,
6+
useMutation,
7+
} from "@tanstack/react-query";
48
import {
59
ArrowUpIcon,
610
ThumbsDownIcon,
@@ -41,6 +45,8 @@ const predefinedPrompts = [
4145
"How do I send a transaction in Unity?",
4246
];
4347

48+
const queryClient = new QueryClient();
49+
4450
// Empty State Component
4551
function ChatEmptyState({
4652
onPromptClick,
@@ -156,13 +162,16 @@ export function Chat() {
156162
[conversationId, posthog],
157163
);
158164

165+
const lastMessageLength = messages[messages.length - 1]?.content.length ?? 0;
166+
167+
// biome-ignore lint/correctness/useExhaustiveDependencies: need both the number of messages and the last message length to trigger the scroll
159168
useEffect(() => {
160169
if (scrollAnchorRef.current && messages.length > 0) {
161170
scrollAnchorRef.current.scrollIntoView({
162171
behavior: "smooth",
163172
});
164173
}
165-
}, [messages.length]);
174+
}, [messages.length, lastMessageLength]);
166175

167176
const handleInputChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
168177
setInput(e.target.value);
@@ -178,57 +187,59 @@ export function Chat() {
178187
};
179188

180189
return (
181-
<div className="container flex max-h-full flex-col grow overflow-hidden lg:max-w-4xl pb-6">
182-
<Toaster richColors />
183-
<div className="relative flex max-h-full flex-1 flex-col overflow-hidden">
184-
{messages.length === 0 ? (
185-
<ChatEmptyState onPromptClick={handleSendMessage} />
186-
) : (
187-
<ScrollShadow
188-
className="flex-1"
189-
scrollableClassName="max-h-full overscroll-contain"
190-
shadowColor="hsl(var(--background))"
191-
shadowClassName="z-[1]"
192-
>
193-
<div className="space-y-8 pt-10 pb-16">
194-
{messages.map((message) => (
195-
<RenderMessage
196-
conversationId={conversationId}
197-
message={message}
198-
key={message.id}
199-
/>
200-
))}
201-
</div>
202-
<div ref={scrollAnchorRef} />
203-
</ScrollShadow>
204-
)}
205-
</div>
190+
<QueryClientProvider client={queryClient}>
191+
<div className="flex max-h-full flex-col grow overflow-hidden">
192+
<Toaster richColors />
193+
<div className="relative flex max-h-full flex-1 flex-col overflow-hidden px-4">
194+
{messages.length === 0 ? (
195+
<ChatEmptyState onPromptClick={handleSendMessage} />
196+
) : (
197+
<ScrollShadow
198+
className="flex-1"
199+
scrollableClassName="max-h-full overscroll-contain"
200+
shadowColor="hsl(var(--background))"
201+
shadowClassName="z-[1]"
202+
>
203+
<div className="space-y-8 pt-6 pb-16">
204+
{messages.map((message) => (
205+
<RenderMessage
206+
conversationId={conversationId}
207+
message={message}
208+
key={message.id}
209+
/>
210+
))}
211+
</div>
212+
<div ref={scrollAnchorRef} />
213+
</ScrollShadow>
214+
)}
215+
</div>
206216

207-
<div className="relative z-stickyTop">
208-
<AutoResizeTextarea
209-
className="min-h-[120px] rounded-xl bg-card"
210-
onChange={handleInputChange}
211-
onKeyDown={handleKeyDown}
212-
placeholder="Ask AI Assistant..."
213-
rows={2}
214-
value={input}
215-
/>
216-
<Button
217-
className="absolute bottom-3 right-3 disabled:opacity-100 !h-auto w-auto shrink-0 gap-2 p-2"
218-
disabled={!input.trim()}
219-
onClick={() => {
220-
const currentInput = input;
221-
setInput("");
222-
handleSendMessage(currentInput);
223-
}}
224-
type="submit"
225-
size="sm"
226-
variant="default"
227-
>
228-
<ArrowUpIcon className="size-4" />
229-
</Button>
217+
<div className="relative z-stickyTop">
218+
<AutoResizeTextarea
219+
className="min-h-[120px] rounded-xl border-x-0 border-b-0 rounded-t-none bg-card focus-visible:ring-0 focus-visible:ring-offset-0"
220+
onChange={handleInputChange}
221+
onKeyDown={handleKeyDown}
222+
placeholder="Ask AI Assistant..."
223+
rows={2}
224+
value={input}
225+
/>
226+
<Button
227+
className="absolute bottom-3 right-3 disabled:opacity-100 !h-auto w-auto shrink-0 gap-2 p-2"
228+
disabled={!input.trim()}
229+
onClick={() => {
230+
const currentInput = input;
231+
setInput("");
232+
handleSendMessage(currentInput);
233+
}}
234+
type="submit"
235+
size="sm"
236+
variant="default"
237+
>
238+
<ArrowUpIcon className="size-4" />
239+
</Button>
240+
</div>
230241
</div>
231-
</div>
242+
</QueryClientProvider>
232243
);
233244
}
234245

@@ -272,7 +283,7 @@ function RenderAIResponse(props: {
272283
return (
273284
<div className="flex items-start gap-3.5">
274285
{aiIcon}
275-
<div className="flex-1 min-w-0 overflow-hidden">
286+
<div className="flex-1 min-w-0 overflow-hidden fade-in-0 duration-300 animate-in">
276287
<StyledMarkdownRenderer
277288
text={props.message.content}
278289
type="assistant"
@@ -334,7 +345,7 @@ function RenderMessage(props: {
334345
return (
335346
<div className="flex items-start gap-3.5">
336347
{userIcon}
337-
<div className="px-3.5 py-2 rounded-xl border bg-card relative">
348+
<div className="px-3.5 py-2 rounded-xl border bg-card relative fade-in-0 duration-300 animate-in">
338349
<StyledMarkdownRenderer
339350
text={props.message.content}
340351
type="user"
@@ -349,7 +360,9 @@ function RenderMessage(props: {
349360
return (
350361
<div className="flex items-center gap-3.5">
351362
{aiIcon}
352-
<TextShimmer text="Thinking..." className="text-sm md:text-base" />
363+
<div className="fade-in-0 duration-300 animate-in">
364+
<TextShimmer text="Thinking..." className="text-sm" />
365+
</div>
353366
</div>
354367
);
355368
}
@@ -371,7 +384,7 @@ function StyledMarkdownRenderer(props: {
371384
}) {
372385
return (
373386
<MarkdownRenderer
374-
className="text-sm md:text-base text-foreground [&>*:first-child]:mt-0 [&>*:first-child]:border-none [&>*:first-child]:pb-0 [&>*:last-child]:mb-0 leading-relaxed"
387+
className="text-sm text-foreground [&>*:first-child]:mt-0 [&>*:first-child]:border-none [&>*:first-child]:pb-0 [&>*:last-child]:mb-0 leading-relaxed"
375388
code={{
376389
className: "bg-card",
377390
ignoreFormattingErrors: true,

0 commit comments

Comments
 (0)