mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 04:15:04 +03:00
Feat/assistant scroll (#31042)
This commit is contained in:
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 }
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user