Feat/assistant scroll (#31042)

This commit is contained in:
Saxon Fletcher authored and GitHub committed 2024-12-11 07:00:29 +10:00
1 parent 4b437c079c
commit 9f718e904f
4 files changed
+123 -74

No files matched your search

@@ -1,8 +1,8 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { AnimatePresence, motion } from 'framer-motion'
import { last } from 'lodash'
import { FileText, Info, X } from 'lucide-react'
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import { FileText, Info, X, ArrowDown } from 'lucide-react'
import { memo, useEffect, useMemo, useRef, useState, useCallback } from 'react'
import { toast } from 'sonner'
import type { Message as MessageType } from 'ai/react'
@@ -44,6 +44,7 @@ import DotGrid from '../DotGrid'
import AIOnboarding from './AIOnboarding'
import CollapsibleCodeBlock from './CollapsibleCodeBlock'
import { Message } from './Message'
import { useAutoScroll } from './hooks'
const MemoizedMessage = memo(
({ message, isLoading }: { message: MessageType; isLoading: boolean }) => {
@@ -89,8 +90,7 @@ export const AIAssistant = ({
const { open, initialInput, sqlSnippets, suggestions } = aiAssistantPanel
const inputRef = useRef<HTMLTextAreaElement>(null)
const bottomRef = useRef<HTMLDivElement>(null)
const scrollContainerRef = useRef<HTMLDivElement>(null)
const { ref: scrollContainerRef, isSticky, scrollToEnd } = useAutoScroll()
const [value, setValue] = useState<string>(initialInput)
const [assistantError, setAssistantError] = useState<string>()
@@ -229,37 +229,16 @@ export const AIAssistant = ({
)
}
const handleScroll = () => {
const container = scrollContainerRef.current
if (container) {
const scrollPercentage =
(container.scrollTop / (container.scrollHeight - container.clientHeight)) * 100
const isScrollable = container.scrollHeight > container.clientHeight
const isAtBottom = scrollPercentage >= 100
setShowFade(isScrollable && !isAtBottom)
}
}
// Add useEffect to set up scroll listener
// Update scroll behavior for new messages
useEffect(() => {
// Use a small delay to ensure container is mounted and has content
const timeoutId = setTimeout(() => {
const container = scrollContainerRef.current
if (container) {
container.addEventListener('scroll', handleScroll)
handleScroll()
}
}, 100)
return () => {
clearTimeout(timeoutId)
const container = scrollContainerRef.current
if (container) {
container.removeEventListener('scroll', handleScroll)
}
if (!isChatLoading) {
if (inputRef.current) inputRef.current.focus()
}
}, [])
if (isSticky) {
setTimeout(scrollToEnd, 0)
}
}, [isChatLoading, isSticky, scrollToEnd, messages])
useEffect(() => {
setValue(initialInput)
@@ -269,30 +248,6 @@ export const AIAssistant = ({
}
}, [initialInput])
useEffect(() => {
if (!isChatLoading) {
if (inputRef.current) inputRef.current.focus()
}
setTimeout(
() => {
if (bottomRef.current) bottomRef.current.scrollIntoView({ behavior: 'smooth' })
},
isChatLoading ? 100 : 500
)
}, [isChatLoading])
useEffect(() => {
if (bottomRef.current) bottomRef.current.scrollIntoView({ behavior: 'smooth' })
handleScroll()
// Load messages into state
if (!isChatLoading) {
setAiAssistantPanel({
messages,
})
}
}, [messages, isChatLoading, setAiAssistantPanel])
// Remove suggestions if sqlSnippets were removed
useEffect(() => {
if (!sqlSnippets || sqlSnippets.length === 0) {
@@ -310,11 +265,7 @@ export const AIAssistant = ({
return (
<>
<div className={cn('flex flex-col h-full', className)}>
<div
ref={scrollContainerRef}
className={cn('flex-grow overflow-auto flex flex-col')}
onScroll={handleScroll}
>
<div ref={scrollContainerRef} className={cn('flex-grow overflow-auto flex flex-col')}>
<div className="z-30 sticky top-0">
<div className="border-b flex items-center bg gap-x-3 px-5 h-[46px]">
<AiIconAnimation allowHoverEffect />
@@ -401,7 +352,7 @@ export const AIAssistant = ({
</motion.div>
</div>
)}
<div ref={bottomRef} className="h-1" />
<div className="h-1" />
</motion.div>
) : suggestions ? (
<div className="w-full h-full px-8 py-0 flex flex-col flex-1 justify-end">
@@ -490,16 +441,41 @@ export const AIAssistant = ({
</div>
)}
</div>
<AnimatePresence>
{showFade && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="pointer-events-none z-10 -mt-24"
>
<div className="h-24 w-full bg-gradient-to-t from-background muted to-transparent" />
</motion.div>
{!isSticky && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="pointer-events-none z-10 -mt-24"
>
<div className="h-24 w-full bg-gradient-to-t from-background to-transparent" />
</motion.div>
<motion.div
className="absolute bottom-20 left-1/2 -translate-x-1/2"
variants={{
hidden: { y: 5, opacity: 0 },
show: { y: 0, opacity: 1 },
}}
transition={{ duration: 0.1 }}
initial="hidden"
animate="show"
exit="hidden"
>
<Button
type="default"
className="rounded-full w-8 h-8 p-1.5"
onClick={() => {
scrollToEnd()
if (inputRef.current) inputRef.current.focus()
}}
>
<ArrowDown size={16} />
</Button>
</motion.div>
</>
)}
</AnimatePresence>
@@ -567,6 +543,7 @@ export const AIAssistant = ({
.join('\n') || ''
const valueWithSnippets = [value, sqlSnippetsString].filter(Boolean).join('\n\n')
sendMessageToAssistant(valueWithSnippets)
scrollToEnd()
} else {
sendMessageToAssistant(value)
}
@@ -183,6 +183,9 @@ export const SqlCard = ({
: 'This query involves running a function.'}{' '}
Are you sure you want to execute it?
</p>
<p className="text-foreground-light">
Make sure you are not accidentally removing something important.
</p>
<div className="flex justify-stretch mt-2 gap-2">
<Button
type="outline"
@@ -193,7 +196,7 @@ export const SqlCard = ({
Cancel
</Button>
<Button
type="outline"
type="danger"
size="tiny"
className="w-full flex-1"
onClick={() => {
@@ -0,0 +1,68 @@
import { useCallback, useEffect, useRef, useState } from 'react'
interface UseAutoScrollProps {
enabled?: boolean
}
export function useAutoScroll({ enabled = true }: UseAutoScrollProps = {}) {
const [container, setContainer] = useState<HTMLDivElement | null>(null)
const [isSticky, setIsSticky] = useState(true)
const isStickyRef = useRef(true)
const lastScrollHeightRef = useRef<number>()
const ref = useCallback((element: HTMLDivElement | null) => {
if (element) {
setContainer(element)
}
}, [])
const scrollToEnd = useCallback(() => {
if (container) {
isStickyRef.current = true
setIsSticky(true)
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth',
})
}
}, [container])
useEffect(() => {
if (!container || !enabled) return
const resizeObserver = new ResizeObserver(() => {
// Prevent duplicate scroll events from phantom height changes
if (
lastScrollHeightRef.current !== undefined &&
container.scrollHeight !== lastScrollHeightRef.current
) {
lastScrollHeightRef.current = container.scrollHeight
if (isStickyRef.current) {
scrollToEnd()
}
}
})
const handleScroll = () => {
const isAtBottom =
Math.abs(container.scrollHeight - container.scrollTop - container.clientHeight) < 10
isStickyRef.current = isAtBottom
setIsSticky(isAtBottom)
}
// Observe all children of the container
Array.from(container.children).forEach((child) => {
resizeObserver.observe(child)
})
container.addEventListener('scroll', handleScroll)
return () => {
resizeObserver.disconnect()
container.removeEventListener('scroll', handleScroll)
}
}, [container, enabled, scrollToEnd])
return { ref, isSticky, scrollToEnd }
}
+2 -1
View File
@@ -104,7 +104,8 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) {
# For all your abilities, follow these instructions:
- First look at the list of provided schemas and if needed, get more information about a schema. You will almost always need to retrieve information about the public schema before answering a question.
- If the question is about users or involves creating a users table, also retrieve the auth schema.
- If the question is about users or involves creating a users table, also retrieve the auth schema.
- If it a query is a destructive query e.g. table drop, ask for confirmation before writing the query. The user will still have to run the query once you create it
Here are the existing database schema names you can retrieve: ${schemas}