@@ -10,7 +10,7 @@ import { Presence } from "@zayne-labs/ui-react/common/presence";
1010import { DefaultChatTransport } from "ai" ;
1111import Link from "fumadocs-core/link" ;
1212import { Loader2 , MessageCircleIcon , RefreshCw , Send , X } from "lucide-react" ;
13- import { useEffect , useMemo , useRef , useState } from "react" ;
13+ import { useEffect , useInsertionEffect , useMemo , useRef , useState } from "react" ;
1414import { toast } from "sonner" ;
1515import { z } from "zod" ;
1616import { Button } from "../ui/button" ;
@@ -41,7 +41,12 @@ function Header() {
4141 < div className = "sticky top-0 flex items-start gap-2" >
4242 < div className = "flex-1 rounded-xl border bg-fd-card p-3 text-fd-card-foreground" >
4343 < p className = "mb-2 text-sm font-medium" > Ask AI</ p >
44- < p className = "text-xs text-fd-muted-foreground" > Powered by Gemma</ p >
44+ < p className = "text-xs text-fd-muted-foreground" >
45+ Powered by{ " " }
46+ < a href = "https://gemini.google.com" target = "_blank" rel = "noreferrer noopener" >
47+ Gemma
48+ </ a >
49+ </ p >
4550 </ div >
4651
4752 < Button
@@ -97,11 +102,13 @@ function SearchAIActions() {
97102 ) ;
98103}
99104
105+ const StorageKeyInput = "__ai_search_input" ;
106+
100107function SearchAIInput ( props : InferProps < "form" > ) {
101108 const { className, ...restOfProps } = props ;
102109 const { sendMessage, status, stop } = useChatContext ( ) ;
103110
104- const [ input , setInput ] = useState ( "" ) ;
111+ const [ input , setInput ] = useState ( ( ) => localStorage . getItem ( StorageKeyInput ) ?? "" ) ;
105112
106113 const isLoading = status === "streaming" || status === "submitted" ;
107114
@@ -111,6 +118,10 @@ function SearchAIInput(props: InferProps<"form">) {
111118 setInput ( "" ) ;
112119 } ;
113120
121+ useInsertionEffect ( ( ) => {
122+ localStorage . setItem ( StorageKeyInput , input ) ;
123+ } , [ input ] ) ;
124+
114125 useEffect ( ( ) => {
115126 if ( isLoading ) {
116127 document . querySelector < HTMLElement > ( "#nd-ai-input" ) ?. focus ( ) ;
@@ -286,7 +297,9 @@ const roleName = {
286297 user : "you" ,
287298} satisfies Record < Exclude < UIMessage [ "role" ] , "system" > , string > as Record < string , string > ;
288299
289- export function AISearchTrigger ( ) {
300+ export function AISearchRoot ( props : { children : React . ReactNode } ) {
301+ const { children } = props ;
302+
290303 const [ open , setOpen ] = useState ( false ) ;
291304
292305 const chat = useChat ( {
@@ -296,6 +309,36 @@ export function AISearchTrigger() {
296309 } ) ,
297310 } ) ;
298311
312+ const contextValue = useMemo < GeneralContextType > (
313+ ( ) => ( { chat, open, setOpen } ) satisfies GeneralContextType ,
314+ [ chat , open ]
315+ ) ;
316+
317+ return < GeneralContextProvider value = { contextValue } > { children } </ GeneralContextProvider > ;
318+ }
319+
320+ export function AISearchTrigger ( ) {
321+ const { open, setOpen } = useGeneralContext ( ) ;
322+
323+ return (
324+ < Button
325+ theme = "secondary"
326+ className = { cnMerge (
327+ `fixed end-[calc(--spacing(4)+var(--removed-body-scroll-bar-size,0px))] bottom-4 z-20 w-24
328+ gap-3 rounded-2xl text-fd-muted-foreground shadow-lg transition-[translate,opacity]` ,
329+ open && "translate-y-10 opacity-0"
330+ ) }
331+ onClick = { ( ) => setOpen ( true ) }
332+ >
333+ < MessageCircleIcon className = "size-4.5" />
334+ Ask AI
335+ </ Button >
336+ ) ;
337+ }
338+
339+ export function AISearchPanel ( ) {
340+ const { chat, open, setOpen } = useGeneralContext ( ) ;
341+
299342 const onKeyPress = useCallbackRef ( ( event : KeyboardEvent ) => {
300343 if ( event . key === "Escape" && open ) {
301344 setOpen ( false ) ;
@@ -314,80 +357,87 @@ export function AISearchTrigger() {
314357 return ( ) => cleanup ( ) ;
315358 } , [ onKeyPress ] ) ;
316359
317- const contextValue = useMemo < GeneralContextType > (
318- ( ) => ( { chat, open, setOpen } ) satisfies GeneralContextType ,
319- [ chat , open ]
320- ) ;
321-
322360 return (
323- < GeneralContextProvider value = { contextValue } >
361+ < >
324362 < style >
325363 { css `
326364 @keyframes ask-ai-open {
327365 from {
328- translate : 100% 0 ;
366+ width : 0px ;
367+ }
368+ to {
369+ width : var (--ai-chat-width );
329370 }
330371 }
331-
332372 @keyframes ask-ai-close {
373+ from {
374+ width : var (--ai-chat-width );
375+ }
333376 to {
334- translate : 100% 0 ;
335- opacity : 0 ;
377+ width : 0px ;
336378 }
337379 }
338380 ` }
339381 </ style >
340382
383+ < Presence present = { open } >
384+ < div
385+ data-state = { open ? "open" : "closed" }
386+ className = "fixed inset-0 z-30 bg-fd-overlay backdrop-blur-xs
387+ data-[state=closed]:animate-fd-fade-out data-[state=open]:animate-fd-fade-in lg:hidden"
388+ onClick = { ( ) => setOpen ( false ) }
389+ />
390+ </ Presence >
391+
341392 < Presence present = { open } >
342393 < div
343394 className = { cnMerge (
344- `fixed inset-y-2 z-30 flex flex-col rounded-2xl border bg-fd-popover p-2
345- text-fd-popover-foreground shadow-lg max-sm:inset-x-2 sm:end-2 sm:w-[460px]` ,
346- open ? "animate-[ask-ai-open_300ms]" : "animate-[ask-ai-close_300ms]"
395+ `z-30 overflow-hidden bg-fd-popover text-fd-popover-foreground [--ai-chat-width:400px]
396+ xl:[--ai-chat-width:460px]` ,
397+ `max-lg:fixed max-lg:inset-x-2 max-lg:top-4 max-lg:rounded-2xl max-lg:border
398+ max-lg:shadow-xl` ,
399+ `lg:sticky lg:top-0 lg:ms-auto lg:h-dvh lg:border-s
400+ lg:in-[#nd-docs-layout]:[grid-area:toc] lg:in-[#nd-notebook-layout]:col-start-5
401+ lg:in-[#nd-notebook-layout]:row-span-full` ,
402+ open ?
403+ "animate-fd-dialog-in lg:animate-[ask-ai-open_200ms]"
404+ : "animate-fd-dialog-out lg:animate-[ask-ai-close_200ms]"
347405 ) }
348406 >
349- < Header />
350- < MessageList
351- className = "flex-1 overscroll-contain px-3 py-4"
352- style = { {
353- maskImage :
354- "linear-gradient(to bottom, transparent, white 1rem, white calc(100% - 1rem), transparent 100%)" ,
355- } }
356- >
357- < div className = "flex flex-col gap-4" >
358- { chat . messages
359- . filter ( ( msg ) => msg . role !== "system" )
360- . map ( ( item ) => (
361- < Message key = { item . id } message = { item } />
362- ) ) }
363- </ div >
364- </ MessageList >
365-
366407 < div
367- className = "rounded-xl border bg-fd-card text-fd-card-foreground has-focus-visible:ring-2
368- has-focus-visible:ring-fd-ring "
408+ className = "flex size-full flex-col p-2 max-lg:max-h-[80dvh] lg:w-(--ai-chat-width)
409+ xl:p-4 "
369410 >
370- < SearchAIInput />
371- < div className = "flex items-center gap-1.5 p-1 empty:hidden" >
372- < SearchAIActions />
411+ < Header />
412+
413+ < MessageList
414+ className = "flex-1 overscroll-contain px-3 py-4"
415+ style = { {
416+ maskImage :
417+ "linear-gradient(to bottom, transparent, white 1rem, white calc(100% - 1rem), transparent 100%)" ,
418+ } }
419+ >
420+ < div className = "flex flex-col gap-4" >
421+ { chat . messages
422+ . filter ( ( msg ) => msg . role !== "system" )
423+ . map ( ( item ) => (
424+ < Message key = { item . id } message = { item } />
425+ ) ) }
426+ </ div >
427+ </ MessageList >
428+
429+ < div
430+ className = "rounded-xl border bg-fd-card text-fd-card-foreground
431+ has-focus-visible:ring-2 has-focus-visible:ring-fd-ring"
432+ >
433+ < SearchAIInput />
434+ < div className = "flex items-center gap-1.5 p-1 empty:hidden" >
435+ < SearchAIActions />
436+ </ div >
373437 </ div >
374438 </ div >
375439 </ div >
376440 </ Presence >
377-
378- < Button
379- unstyled = { true }
380- className = { cnMerge (
381- `fixed bottom-4 z-20 flex h-10 w-24 items-center gap-3 rounded-2xl border bg-fd-secondary
382- px-2 text-sm font-medium text-fd-muted-foreground shadow-lg transition-[translate,opacity]` ,
383- "end-[calc(var(--removed-body-scroll-bar-size,0px)+var(--fd-layout-offset)+1rem)]" ,
384- open && "translate-y-10 opacity-0"
385- ) }
386- onClick = { ( ) => setOpen ( true ) }
387- >
388- < MessageCircleIcon className = "size-4.5" />
389- Ask AI
390- </ Button >
391- </ GeneralContextProvider >
441+ </ >
392442 ) ;
393443}
0 commit comments