Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@ import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import type { FDW } from 'data/fdw/fdws-query'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { Badge, Sheet, SheetContent, TableCell, TableRow } from 'ui'
import {
Badge,
Sheet,
SheetContent,
TableCell,
TableRow,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { INTEGRATIONS } from '../Landing/Integrations.constants'
import DeleteWrapperModal from './DeleteWrapperModal'
import { EditWrapperSheet } from './EditWrapperSheet'
Expand Down Expand Up @@ -74,7 +83,12 @@ const WrapperRow = ({ wrapper }: WrapperRowProps) => {
<div className="relative w-3 h-3 flex items-center justify-center">
{integration.icon({ className: 'p-0' })}
</div>
{target}{' '}
<Tooltip>
<TooltipTrigger className="truncate max-w-28">{target}</TooltipTrigger>
<TooltipContent className="max-w-64 whitespace-pre-wrap break-words">
{target}
</TooltipContent>
</Tooltip>
<ChevronRight
size={12}
strokeWidth={1.5}
Expand All @@ -85,7 +99,14 @@ const WrapperRow = ({ wrapper }: WrapperRowProps) => {
<Link href={`/project/${ref}/editor/${table.id}`}>
<Badge className="transition hover:bg-surface-300 pl-5 rounded-l-none gap-2 h-6 font-mono text-[0.75rem] border-l-0">
<Table2 size={12} strokeWidth={1.5} className="text-foreground-lighter/50" />
{table.schema}.{table.table_name}
<Tooltip>
<TooltipTrigger className="truncate max-w-28">
{table.schema}.{table.table_name}
</TooltipTrigger>
<TooltipContent className="max-w-64 whitespace-pre-wrap break-words">
{table.schema}.{table.table_name}
</TooltipContent>
</Tooltip>
</Badge>
</Link>
</div>
Expand Down
72 changes: 59 additions & 13 deletions apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const MemoizedMessage = memo(
message,
isLoading,
onResults,
onDelete,
onEdit,
isAfterEditedMessage,
isBeingEdited,
Expand All @@ -58,6 +59,7 @@ const MemoizedMessage = memo(
resultId?: string
results: any[]
}) => void
onDelete: (id: string) => void
onEdit: (id: string) => void
isAfterEditedMessage: boolean
isBeingEdited: boolean
Expand All @@ -70,6 +72,7 @@ const MemoizedMessage = memo(
readOnly={message.role === 'user'}
isLoading={isLoading}
onResults={onResults}
onDelete={onDelete}
onEdit={onEdit}
isAfterEditedMessage={isAfterEditedMessage}
isBeingEdited={isBeingEdited}
Expand Down Expand Up @@ -105,7 +108,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
)

const inputRef = useRef<HTMLTextAreaElement>(null)
const { ref: scrollContainerRef, isSticky, scrollToEnd } = useAutoScroll()
const { ref: scrollContainerRef, isSticky, scrollToEnd, setIsSticky } = useAutoScroll()

const { aiOptInLevel, isHipaaProjectDisallowed } = useOrgAiOptInLevel()
const showMetadataWarning =
Expand All @@ -119,6 +122,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
const [value, setValue] = useState<string>(snap.initialInput || '')
const [isConfirmOptInModalOpen, setIsConfirmOptInModalOpen] = useState(false)
const [editingMessageId, setEditingMessageId] = useState<string | null>(null)
const [isResubmitting, setIsResubmitting] = useState(false)

const { data: check, isSuccess } = useCheckOpenAIKeyQuery()
const isApiKeySet = IS_PLATFORM || !!check?.hasKey
Expand Down Expand Up @@ -232,7 +236,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
onFinish: handleChatFinish,
})

const isChatLoading = chatStatus === 'submitted' || chatStatus === 'streaming'
const isChatLoading = chatStatus === 'submitted' || chatStatus === 'streaming' || isResubmitting

const updateMessage = useCallback(
({
Expand All @@ -249,6 +253,22 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
[snap]
)

const deleteMessageFromHere = useCallback(
(messageId: string) => {
// Find the message index in current chatMessages
const messageIndex = chatMessages.findIndex((msg) => msg.id === messageId)
if (messageIndex === -1) return

if (isChatLoading) stop()

snap.deleteMessagesAfter(messageId, { includeSelf: true })

const updatedMessages = chatMessages.slice(0, messageIndex)
setMessages(updatedMessages)
},
[snap, setMessages, chatMessages, isChatLoading, stop]
)

const editMessage = useCallback(
(messageId: string) => {
const messageIndex = chatMessages.findIndex((msg) => msg.id === messageId)
Expand All @@ -266,9 +286,16 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
.join('') ?? ''
setValue(textContent)

if (inputRef.current) {
inputRef.current.focus()
}
setTimeout(() => {
if (inputRef.current) {
inputRef?.current?.focus()

// [Joshen] This is just to make the cursor go to the end of the text when focusing
const val = inputRef.current.value
inputRef.current.value = ''
inputRef.current.value = val
}
}, 100)
},
[chatMessages, setValue]
)
Expand All @@ -292,14 +319,23 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
message={message}
isLoading={isChatLoading && message.id === chatMessages[chatMessages.length - 1].id}
onResults={updateMessage}
onDelete={deleteMessageFromHere}
onEdit={editMessage}
isAfterEditedMessage={isAfterEditedMessage}
isBeingEdited={isBeingEdited}
onCancelEdit={cancelEdit}
/>
)
}),
[chatMessages, isChatLoading, updateMessage, editMessage, editingMessageId, cancelEdit]
[
chatMessages,
isChatLoading,
updateMessage,
deleteMessageFromHere,
editMessage,
cancelEdit,
editingMessageId,
]
)

const hasMessages = chatMessages.length > 0
Expand All @@ -308,12 +344,9 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
const sendMessageToAssistant = (finalContent: string) => {
if (editingMessageId) {
// Handling when the user is in edit mode
const messageIndex = chatMessages.findIndex((msg) => msg.id === editingMessageId)
if (messageIndex === -1) return

snap.deleteMessagesAfter(editingMessageId, { includeSelf: true })
const updatedMessages = chatMessages.slice(0, messageIndex)
setMessages(updatedMessages)
// delete the message(s) from the chat just like the delete button
setIsResubmitting(true)
deleteMessageFromHere(editingMessageId)
setEditingMessageId(null)
}

Expand Down Expand Up @@ -355,6 +388,14 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
setEditingMessageId(null)
}

useEffect(() => {
// Keep "Thinking" visible while stopping and resubmitting during edit
// Only clear once the new response actually starts streaming (or errors)
if (isResubmitting && (chatStatus === 'streaming' || !!error)) {
setIsResubmitting(false)
}
}, [isResubmitting, chatStatus, error])

// Update scroll behavior for new messages
useEffect(() => {
if (!isChatLoading) {
Expand All @@ -381,6 +422,10 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [snap.open, isInSQLEditor, snippetContent])

useEffect(() => {
if (chatMessages.length === 0 && !isSticky) setIsSticky(true)
}, [chatMessages, isSticky, setIsSticky])

return (
<ErrorBoundary
message="Something went wrong with the AI Assistant"
Expand Down Expand Up @@ -699,7 +744,8 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
'z-20 [&>form>textarea]:text-base [&>form>textarea]:md:text-sm [&>form>textarea]:border-1 [&>form>textarea]:rounded-md [&>form>textarea]:!outline-none [&>form>textarea]:!ring-offset-0 [&>form>textarea]:!ring-0'
)}
loading={isChatLoading}
disabled={!isApiKeySet || disablePrompts || isChatLoading}
isEditing={!!editingMessageId}
disabled={!isApiKeySet || disablePrompts || (isChatLoading && !editingMessageId)}
placeholder={
hasMessages
? 'Ask a follow up question...'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export interface FormProps {
snippetsClassName?: string
/* Additional class name for the form wrapper */
className?: string
/* If currently editing an existing message */
isEditing?: boolean
}

const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(
Expand All @@ -59,6 +61,7 @@ const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(
snippetsClassName,
includeSnippetsInMessage = false,
className,
isEditing = false,
...props
},
ref
Expand All @@ -68,7 +71,7 @@ const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(

const handleSubmit = (event?: FormEvent<HTMLFormElement>) => {
if (event) event.preventDefault()
if (!value || loading) return
if (!value || (loading && !isEditing)) return

let finalMessage = value
if (includeSnippetsInMessage && sqlSnippets && sqlSnippets.length > 0) {
Expand Down Expand Up @@ -107,8 +110,8 @@ const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(
/>
)}
<ExpandingTextArea
autoFocus={!isMobile}
ref={textAreaRef}
autoFocus={isMobile}
disabled={disabled}
className={cn(
'text-sm pr-10 max-h-64',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
} from 'ui'

type DeleteMessageConfirmModalProps = {
visible: boolean
onConfirm: () => void
onCancel: () => void
}

export const DeleteMessageConfirmModal = ({
visible,
onConfirm,
onCancel,
}: DeleteMessageConfirmModalProps) => {
const onOpenChange = (open: boolean) => {
if (!open) onCancel()
}

return (
<Dialog open={visible} onOpenChange={onOpenChange}>
<DialogContent size="small">
<DialogHeader padding="small">
<DialogTitle>Delete Message</DialogTitle>
</DialogHeader>

<DialogSectionSeparator />

<DialogSection padding="small">
<p className="text-sm text-foreground-light">
Are you sure you want to delete this message and all subsequent messages? This action
cannot be undone.
</p>
</DialogSection>

<DialogFooter padding="small">
<Button type="default" onClick={onCancel}>
Cancel
</Button>
<Button type="danger" onClick={onConfirm}>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
Loading
Loading