Skip to content

Commit eecfd78

Browse files
committed
feat(docs): DR-7571 Add floating Ask AI input
1 parent d06863c commit eecfd78

4 files changed

Lines changed: 151 additions & 106 deletions

File tree

apps/docs/src/app/(docs)/(default)/layout.tsx

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import { StatusIndicator } from "@/components/status-indicator";
99
import { SidebarBannerCarousel } from "@/components/sidebar-banner";
1010
import { fetchOgImage } from "@/lib/og-image";
1111
import { cn } from "@prisma-docs/ui/lib/cn";
12+
import { FloatingAsk } from "@/components/floating-ask";
1213

1314
// Sidebar announcement slides — set to [] to hide the banner
1415
const SIDEBAR_SLIDES = [
1516
{
1617
title: "The Next Evolution of Prisma ORM",
17-
description: "Prisma Next: a full TypeScript rewrite with a new query API, SQL builder, and extensible architecture.",
18+
description:
19+
"Prisma Next: a full TypeScript rewrite with a new query API, SQL builder, and extensible architecture.",
1820
href: "https://pris.ly/pn-anouncement",
1921
gradient: "orm" as const,
2022
badge: "New",
@@ -44,23 +46,26 @@ export default async function Layout({ children }: { children: React.ReactNode }
4446
);
4547

4648
return (
47-
<DocsLayout
48-
{...base}
49-
links={navbarLinks}
50-
nav={{ ...nav }}
51-
sidebar={{
52-
collapsible: false,
53-
footer: ({ className, ...props }: ComponentProps<"div">) => (
54-
<div className={cn("flex flex-col p-4 pt-2 gap-3", className)} {...props}>
55-
<SidebarBannerCarousel slides={slides} />
56-
<StatusIndicator />
57-
{props.children}
58-
</div>
59-
),
60-
}}
61-
tree={source.pageTree}
62-
>
63-
{children}
64-
</DocsLayout>
49+
<>
50+
<DocsLayout
51+
{...base}
52+
links={navbarLinks}
53+
nav={{ ...nav }}
54+
sidebar={{
55+
collapsible: false,
56+
footer: ({ className, ...props }: ComponentProps<"div">) => (
57+
<div className={cn("flex flex-col p-4 pt-2 gap-3", className)} {...props}>
58+
<SidebarBannerCarousel slides={slides} />
59+
<StatusIndicator />
60+
{props.children}
61+
</div>
62+
),
63+
}}
64+
tree={source.pageTree}
65+
>
66+
{children}
67+
</DocsLayout>
68+
<FloatingAsk />
69+
</>
6570
);
6671
}

apps/docs/src/components/ai-chat-sidebar.tsx

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

3-
import {
4-
useChat,
5-
useDeepThinking,
6-
type Reaction,
7-
type Source,
8-
} from "@kapaai/react-sdk";
3+
import { useChat, useDeepThinking, type Reaction, type Source } from "@kapaai/react-sdk";
94
import {
105
AlertCircleIcon,
116
ChevronRightIcon,
@@ -20,11 +15,7 @@ import { useEffect, useState } from "react";
2015
import { createPortal } from "react-dom";
2116
import { cn } from "@prisma-docs/ui/lib/cn";
2217
import { buttonVariants } from "@/components/ui/button";
23-
import {
24-
Tooltip,
25-
TooltipContent,
26-
TooltipTrigger,
27-
} from "@prisma-docs/ui/components/tooltip";
18+
import { Tooltip, TooltipContent, TooltipTrigger } from "@prisma-docs/ui/components/tooltip";
2819
import {
2920
Drawer,
3021
DrawerContent,
@@ -46,28 +37,17 @@ import {
4637
ConversationContent,
4738
ConversationScrollButton,
4839
} from "@/components/ai-elements/conversation";
49-
import {
50-
Message,
51-
MessageContent,
52-
MessageResponseMarkdown,
53-
} from "@/components/ai-elements/message";
40+
import { Message, MessageContent, MessageResponseMarkdown } from "@/components/ai-elements/message";
5441
import { Shimmer } from "@/components/ai-elements/shimmer";
5542
import { Spinner } from "@/components/ai-elements/spinner";
56-
import {
57-
PromptInput,
58-
PromptInputFooter,
59-
} from "@/components/ai-elements/prompt-input";
43+
import { PromptInput, PromptInputFooter } from "@/components/ai-elements/prompt-input";
6044
import { CopyChat } from "@/components/ai-elements/copy-chat";
6145

6246
interface AIChatSidebarProps {
6347
exampleQuestions?: string[];
6448
}
6549

66-
const SourcesDisplay = ({
67-
sources,
68-
}: {
69-
sources: (Source | StoredSource)[];
70-
}) => {
50+
const SourcesDisplay = ({ sources }: { sources: (Source | StoredSource)[] }) => {
7151
const [showAll, setShowAll] = useState(false);
7252
const displayedSources = showAll ? sources : sources.slice(0, 3);
7353
const hasMore = sources.length > 3;
@@ -132,14 +112,11 @@ const FeedbackButtons = ({
132112
"h-7 w-7",
133113
currentReaction === "upvote" &&
134114
"text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-950",
135-
disabled && "opacity-50 cursor-not-allowed"
115+
disabled && "opacity-50 cursor-not-allowed",
136116
)}
137117
>
138118
<ThumbsUpIcon
139-
className={cn(
140-
"size-3.5",
141-
currentReaction === "upvote" && "fill-current"
142-
)}
119+
className={cn("size-3.5", currentReaction === "upvote" && "fill-current")}
143120
/>
144121
</button>
145122
)}
@@ -159,14 +136,11 @@ const FeedbackButtons = ({
159136
"h-7 w-7",
160137
currentReaction === "downvote" &&
161138
"text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950",
162-
disabled && "opacity-50 cursor-not-allowed"
139+
disabled && "opacity-50 cursor-not-allowed",
163140
)}
164141
>
165142
<ThumbsDownIcon
166-
className={cn(
167-
"size-3.5",
168-
currentReaction === "downvote" && "fill-current"
169-
)}
143+
className={cn("size-3.5", currentReaction === "downvote" && "fill-current")}
170144
/>
171145
</button>
172146
)}
@@ -185,6 +159,7 @@ const ChatInner = ({
185159
onClose: () => void;
186160
}) => {
187161
const [inputValue, setInputValue] = useState("");
162+
const { pendingMessage, setPendingMessage } = useAIChatContext();
188163
const {
189164
initialMessages,
190165
isLoading: isPersistenceLoading,
@@ -205,6 +180,14 @@ const ChatInner = ({
205180

206181
const deepThinking = useDeepThinking();
207182

183+
// Auto-submit pending message from the floating input
184+
useEffect(() => {
185+
if (pendingMessage && !isGeneratingAnswer) {
186+
submitQuery(pendingMessage);
187+
setPendingMessage("");
188+
}
189+
}, [pendingMessage, isGeneratingAnswer, submitQuery, setPendingMessage]);
190+
208191
useEffect(() => {
209192
if (conversation.length === 0) return;
210193

@@ -258,9 +241,7 @@ const ChatInner = ({
258241
};
259242

260243
const messagesForCopy: ChatMessage[] = conversation.flatMap((qa) => {
261-
const msgs: ChatMessage[] = [
262-
{ id: `${qa.id}-user`, role: "user", content: qa.question },
263-
];
244+
const msgs: ChatMessage[] = [{ id: `${qa.id}-user`, role: "user", content: qa.question }];
264245
if (qa.answer) {
265246
msgs.push({
266247
id: `${qa.id}-assistant`,
@@ -273,30 +254,24 @@ const ChatInner = ({
273254

274255
const hasActiveConversation = conversation.length > 0;
275256
const hasPersistedMessages = initialMessages.length > 0;
276-
const showEmptyState =
277-
!hasActiveConversation && !hasPersistedMessages && !isPersistenceLoading;
257+
const showEmptyState = !hasActiveConversation && !hasPersistedMessages && !isPersistenceLoading;
278258

279259
return (
280260
<div className="flex size-full w-full flex-col overflow-hidden bg-fd-background">
281261
<div className="flex items-center justify-between px-4 py-2.5 border-b shrink-0">
282262
<h2 className="font-semibold text-sm">Chat</h2>
283263
<div className="flex items-center gap-1">
284-
<CopyChat
285-
messages={hasActiveConversation ? messagesForCopy : initialMessages}
286-
/>
264+
<CopyChat messages={hasActiveConversation ? messagesForCopy : initialMessages} />
287265
<Tooltip>
288266
<TooltipTrigger
289267
render={(props) => (
290268
<button
291269
{...props}
292270
onClick={handleClearChat}
293-
disabled={
294-
(!hasActiveConversation && !hasPersistedMessages) ||
295-
isGeneratingAnswer
296-
}
271+
disabled={(!hasActiveConversation && !hasPersistedMessages) || isGeneratingAnswer}
297272
className={cn(
298273
buttonVariants({ variant: "ghost", size: "icon-sm" }),
299-
"disabled:opacity-50"
274+
"disabled:opacity-50",
300275
)}
301276
>
302277
<Trash2 className="size-3.5" />
@@ -311,9 +286,7 @@ const ChatInner = ({
311286
<button
312287
{...props}
313288
onClick={onClose}
314-
className={cn(
315-
buttonVariants({ variant: "ghost", size: "icon-sm" })
316-
)}
289+
className={cn(buttonVariants({ variant: "ghost", size: "icon-sm" }))}
317290
>
318291
<ChevronRightIcon className="size-3.5" />
319292
</button>
@@ -333,9 +306,7 @@ const ChatInner = ({
333306
<MessagesSquareIcon className="size-6 text-fd-primary" />
334307
</div>
335308
<div>
336-
<h3 className="font-semibold text-fd-foreground">
337-
How can I help?
338-
</h3>
309+
<h3 className="font-semibold text-fd-foreground">How can I help?</h3>
339310
<p className="text-sm text-fd-muted-foreground mt-1">
340311
Ask me anything about Prisma
341312
</p>
@@ -373,8 +344,7 @@ const ChatInner = ({
373344
<>
374345
{conversation.map((qa, index) => {
375346
const isLastItem = index === conversation.length - 1;
376-
const isLoading =
377-
isLastItem && (isGeneratingAnswer || isPreparingAnswer);
347+
const isLoading = isLastItem && (isGeneratingAnswer || isPreparingAnswer);
378348
const hasAnswer = !!qa.answer;
379349
const isComplete = qa.id && !isLoading;
380350

@@ -387,9 +357,7 @@ const ChatInner = ({
387357
<Message from="assistant">
388358
<MessageContent>
389359
{hasAnswer ? (
390-
<MessageResponseMarkdown>
391-
{qa.answer}
392-
</MessageResponseMarkdown>
360+
<MessageResponseMarkdown>{qa.answer}</MessageResponseMarkdown>
393361
) : isLoading ? (
394362
<div className="flex items-center gap-2 overflow-hidden">
395363
<Spinner />
@@ -443,18 +411,14 @@ const ChatInner = ({
443411
<Message key={message.id} from={message.role}>
444412
<MessageContent>
445413
{message.role === "assistant" ? (
446-
<MessageResponseMarkdown>
447-
{message.content}
448-
</MessageResponseMarkdown>
414+
<MessageResponseMarkdown>{message.content}</MessageResponseMarkdown>
449415
) : (
450416
message.content
451417
)}
452418
</MessageContent>
453419
{message.role === "assistant" &&
454420
message.sources &&
455-
message.sources.length > 0 && (
456-
<SourcesDisplay sources={message.sources} />
457-
)}
421+
message.sources.length > 0 && <SourcesDisplay sources={message.sources} />}
458422
</Message>
459423
))}
460424
</>
@@ -523,14 +487,18 @@ export const AIChatSidebar = ({
523487
onClick={() => setIsOpen(!isOpen)}
524488
className={cn(
525489
buttonVariants({ variant: "outline" }),
526-
"hidden shrink-0 shadow-none md:inline-flex items-center gap-2 h-8 group cursor-pointer"
490+
"hidden shrink-0 shadow-none md:inline-flex items-center gap-2 h-8 group cursor-pointer",
527491
)}
528492
>
529493
<MessagesSquareIcon className="size-4 text-fd-muted-foreground group-hover:text-fd-accent-foreground" />
530494
<span>Ask AI</span>
531495
<div className="ms-auto inline-flex gap-0.5">
532-
<kbd className="rounded-md border bg-fd-background px-1.5 text-fd-muted-foreground group-hover:text-fd-accent-foreground">{isMac ? "⌘" : "Ctrl"}</kbd>
533-
<kbd className="rounded-md border bg-fd-background px-1.5 text-fd-muted-foreground group-hover:text-fd-accent-foreground">I</kbd>
496+
<kbd className="rounded-md border bg-fd-background px-1.5 text-fd-muted-foreground group-hover:text-fd-accent-foreground">
497+
{isMac ? "⌘" : "Ctrl"}
498+
</kbd>
499+
<kbd className="rounded-md border bg-fd-background px-1.5 text-fd-muted-foreground group-hover:text-fd-accent-foreground">
500+
I
501+
</kbd>
534502
</div>
535503
</button>
536504

@@ -542,28 +510,22 @@ export const AIChatSidebar = ({
542510
"inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-[500px]",
543511
"translate-x-full data-[state=open]:translate-x-0",
544512
"pointer-events-none data-[state=open]:pointer-events-auto",
545-
"hidden md:flex"
513+
"hidden md:flex",
546514
)}
547515
data-state={isOpen ? "open" : "closed"}
548516
>
549-
<ChatInner
550-
exampleQuestions={exampleQuestions}
551-
onClose={() => setIsOpen(false)}
552-
/>
517+
<ChatInner exampleQuestions={exampleQuestions} onClose={() => setIsOpen(false)} />
553518
</div>,
554-
document.body
519+
document.body,
555520
)}
556521

557522
<div className="md:hidden">
558-
<Drawer
559-
open={isMobile ? isOpen : false}
560-
onOpenChange={isMobile ? setIsOpen : undefined}
561-
>
523+
<Drawer open={isMobile ? isOpen : false} onOpenChange={isMobile ? setIsOpen : undefined}>
562524
<DrawerTrigger asChild>
563525
<button
564526
className={cn(
565527
buttonVariants({ variant: "outline", size: "sm" }),
566-
"shadow-none inline-flex items-center gap-1.5"
528+
"shadow-none inline-flex items-center gap-1.5",
567529
)}
568530
>
569531
<MessagesSquareIcon className="size-3.5 text-fd-muted-foreground" />
@@ -575,10 +537,7 @@ export const AIChatSidebar = ({
575537
<DrawerTitle>AI Chat</DrawerTitle>
576538
<DrawerDescription>Ask questions about Prisma</DrawerDescription>
577539
</DrawerHeader>
578-
<ChatInner
579-
exampleQuestions={exampleQuestions}
580-
onClose={() => setIsOpen(false)}
581-
/>
540+
<ChatInner exampleQuestions={exampleQuestions} onClose={() => setIsOpen(false)} />
582541
</DrawerContent>
583542
</Drawer>
584543
</div>

0 commit comments

Comments
 (0)