1
1
import { PermissionAction } from '@supabase/shared-types/out/constants'
2
2
import { AnimatePresence , motion } from 'framer-motion'
3
3
import { last } from 'lodash'
4
- import { FileText , Info , X } from 'lucide-react'
5
- import { memo , useEffect , useMemo , useRef , useState } from 'react'
4
+ import { FileText , Info , X , ArrowDown } from 'lucide-react'
5
+ import { memo , useEffect , useMemo , useRef , useState , useCallback } from 'react'
6
6
import { toast } from 'sonner'
7
7
8
8
import type { Message as MessageType } from 'ai/react'
@@ -44,6 +44,7 @@ import DotGrid from '../DotGrid'
44
44
import AIOnboarding from './AIOnboarding'
45
45
import CollapsibleCodeBlock from './CollapsibleCodeBlock'
46
46
import { Message } from './Message'
47
+ import { useAutoScroll } from './hooks'
47
48
48
49
const MemoizedMessage = memo (
49
50
( { message, isLoading } : { message : MessageType ; isLoading : boolean } ) => {
@@ -89,8 +90,7 @@ export const AIAssistant = ({
89
90
const { open, initialInput, sqlSnippets, suggestions } = aiAssistantPanel
90
91
91
92
const inputRef = useRef < HTMLTextAreaElement > ( null )
92
- const bottomRef = useRef < HTMLDivElement > ( null )
93
- const scrollContainerRef = useRef < HTMLDivElement > ( null )
93
+ const { ref : scrollContainerRef , isSticky, scrollToEnd } = useAutoScroll ( )
94
94
95
95
const [ value , setValue ] = useState < string > ( initialInput )
96
96
const [ assistantError , setAssistantError ] = useState < string > ( )
@@ -229,37 +229,16 @@ export const AIAssistant = ({
229
229
)
230
230
}
231
231
232
- const handleScroll = ( ) => {
233
- const container = scrollContainerRef . current
234
- if ( container ) {
235
- const scrollPercentage =
236
- ( container . scrollTop / ( container . scrollHeight - container . clientHeight ) ) * 100
237
- const isScrollable = container . scrollHeight > container . clientHeight
238
- const isAtBottom = scrollPercentage >= 100
239
-
240
- setShowFade ( isScrollable && ! isAtBottom )
241
- }
242
- }
243
-
244
- // Add useEffect to set up scroll listener
232
+ // Update scroll behavior for new messages
245
233
useEffect ( ( ) => {
246
- // Use a small delay to ensure container is mounted and has content
247
- const timeoutId = setTimeout ( ( ) => {
248
- const container = scrollContainerRef . current
249
- if ( container ) {
250
- container . addEventListener ( 'scroll' , handleScroll )
251
- handleScroll ( )
252
- }
253
- } , 100 )
234
+ if ( ! isChatLoading ) {
235
+ if ( inputRef . current ) inputRef . current . focus ( )
236
+ }
254
237
255
- return ( ) => {
256
- clearTimeout ( timeoutId )
257
- const container = scrollContainerRef . current
258
- if ( container ) {
259
- container . removeEventListener ( 'scroll' , handleScroll )
260
- }
238
+ if ( isSticky ) {
239
+ setTimeout ( scrollToEnd , 0 )
261
240
}
262
- } , [ ] )
241
+ } , [ isChatLoading , isSticky , scrollToEnd , messages ] )
263
242
264
243
useEffect ( ( ) => {
265
244
setValue ( initialInput )
@@ -269,30 +248,6 @@ export const AIAssistant = ({
269
248
}
270
249
} , [ initialInput ] )
271
250
272
- useEffect ( ( ) => {
273
- if ( ! isChatLoading ) {
274
- if ( inputRef . current ) inputRef . current . focus ( )
275
- }
276
-
277
- setTimeout (
278
- ( ) => {
279
- if ( bottomRef . current ) bottomRef . current . scrollIntoView ( { behavior : 'smooth' } )
280
- } ,
281
- isChatLoading ? 100 : 500
282
- )
283
- } , [ isChatLoading ] )
284
-
285
- useEffect ( ( ) => {
286
- if ( bottomRef . current ) bottomRef . current . scrollIntoView ( { behavior : 'smooth' } )
287
- handleScroll ( )
288
- // Load messages into state
289
- if ( ! isChatLoading ) {
290
- setAiAssistantPanel ( {
291
- messages,
292
- } )
293
- }
294
- } , [ messages , isChatLoading , setAiAssistantPanel ] )
295
-
296
251
// Remove suggestions if sqlSnippets were removed
297
252
useEffect ( ( ) => {
298
253
if ( ! sqlSnippets || sqlSnippets . length === 0 ) {
@@ -310,11 +265,7 @@ export const AIAssistant = ({
310
265
return (
311
266
< >
312
267
< div className = { cn ( 'flex flex-col h-full' , className ) } >
313
- < div
314
- ref = { scrollContainerRef }
315
- className = { cn ( 'flex-grow overflow-auto flex flex-col' ) }
316
- onScroll = { handleScroll }
317
- >
268
+ < div ref = { scrollContainerRef } className = { cn ( 'flex-grow overflow-auto flex flex-col' ) } >
318
269
< div className = "z-30 sticky top-0" >
319
270
< div className = "border-b flex items-center bg gap-x-3 px-5 h-[46px]" >
320
271
< AiIconAnimation allowHoverEffect />
@@ -401,7 +352,7 @@ export const AIAssistant = ({
401
352
</ motion . div >
402
353
</ div >
403
354
) }
404
- < div ref = { bottomRef } className = "h-1" />
355
+ < div className = "h-1" />
405
356
</ motion . div >
406
357
) : suggestions ? (
407
358
< div className = "w-full h-full px-8 py-0 flex flex-col flex-1 justify-end" >
@@ -490,16 +441,41 @@ export const AIAssistant = ({
490
441
</ div >
491
442
) }
492
443
</ div >
444
+
493
445
< AnimatePresence >
494
- { showFade && (
495
- < motion . div
496
- initial = { { opacity : 0 } }
497
- animate = { { opacity : 1 } }
498
- exit = { { opacity : 0 } }
499
- className = "pointer-events-none z-10 -mt-24"
500
- >
501
- < div className = "h-24 w-full bg-gradient-to-t from-background muted to-transparent" />
502
- </ motion . div >
446
+ { ! isSticky && (
447
+ < >
448
+ < motion . div
449
+ initial = { { opacity : 0 } }
450
+ animate = { { opacity : 1 } }
451
+ exit = { { opacity : 0 } }
452
+ className = "pointer-events-none z-10 -mt-24"
453
+ >
454
+ < div className = "h-24 w-full bg-gradient-to-t from-background to-transparent" />
455
+ </ motion . div >
456
+ < motion . div
457
+ className = "absolute bottom-20 left-1/2 -translate-x-1/2"
458
+ variants = { {
459
+ hidden : { y : 5 , opacity : 0 } ,
460
+ show : { y : 0 , opacity : 1 } ,
461
+ } }
462
+ transition = { { duration : 0.1 } }
463
+ initial = "hidden"
464
+ animate = "show"
465
+ exit = "hidden"
466
+ >
467
+ < Button
468
+ type = "default"
469
+ className = "rounded-full w-8 h-8 p-1.5"
470
+ onClick = { ( ) => {
471
+ scrollToEnd ( )
472
+ if ( inputRef . current ) inputRef . current . focus ( )
473
+ } }
474
+ >
475
+ < ArrowDown size = { 16 } />
476
+ </ Button >
477
+ </ motion . div >
478
+ </ >
503
479
) }
504
480
</ AnimatePresence >
505
481
@@ -567,6 +543,7 @@ export const AIAssistant = ({
567
543
. join ( '\n' ) || ''
568
544
const valueWithSnippets = [ value , sqlSnippetsString ] . filter ( Boolean ) . join ( '\n\n' )
569
545
sendMessageToAssistant ( valueWithSnippets )
546
+ scrollToEnd ( )
570
547
} else {
571
548
sendMessageToAssistant ( value )
572
549
}
0 commit comments