// @ts-nocheck import type { UIMessage as MessageType } from '@ai-sdk/react' import { useChat } from '@ai-sdk/react' import { lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai' import { LOCAL_STORAGE_KEYS, useFlag } from 'common' import { useParams, useSearchParamsShallow } from 'common/hooks' import { AnimatePresence, motion } from 'framer-motion' import { Eraser, Pencil, X } from 'lucide-react' import { useRouter } from 'next/router' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Button, cn, KeyboardShortcut } from 'ui' import { Admonition } from 'ui-patterns' import AlertError from '../AlertError' import { ButtonTooltip } from '../ButtonTooltip' import { ErrorBoundary } from '../ErrorBoundary/ErrorBoundary' import { ASSISTANT_ERRORS } from './AiAssistant.constants' import type { SqlSnippet } from './AIAssistant.types' import { hasPendingToolApproval, onErrorChat, resolvePendingToolApprovalsAsDenied, } from './AIAssistant.utils' import { AIAssistantHeader } from './AIAssistantHeader' import { AIOnboarding } from './AIOnboarding' import { AssistantChatForm } from './AssistantChatForm' import { Conversation, ConversationContent, ConversationScrollButton, } from './elements/Conversation' import { Message } from './Message' import { Markdown } from '@/components/interfaces/Markdown' import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { useCheckOpenAIKeyQuery } from '@/data/ai/check-api-key-query' import { useRateMessageMutation } from '@/data/ai/rate-message-mutation' import { useTablesQuery } from '@/data/tables/tables-query' import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' import { useOrgAiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { DEFAULT_ASSISTANT_BASE_MODEL_ID, defaultAssistantModelId, isAssistantBaseModelId, isKnownAssistantModelId, } from '@/lib/ai/model.utils' import { IS_PLATFORM } from '@/lib/constants' import { uuidv4 } from '@/lib/helpers' import { useTrack } from '@/lib/telemetry/track' import type { AssistantModel } from '@/state/ai-assistant-state' import { useAiAssistantState, useAiAssistantStateSnapshot } from '@/state/ai-assistant-state' import { SHORTCUT_IDS } from '@/state/shortcuts/registry' import { useShortcut } from '@/state/shortcuts/useShortcut' import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state' import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2' interface AIAssistantProps { initialMessages?: MessageType[] | undefined className?: string } export const AIAssistant = ({ className }: AIAssistantProps) => { const router = useRouter() const { id: entityId } = useParams() const { data: project } = useSelectedProjectQuery() const searchParams = useSearchParamsShallow() const { data: selectedOrganization, isPending: isLoadingOrganization } = useSelectedOrganizationQuery() useShortcut(SHORTCUT_IDS.AI_ASSISTANT_CANCEL_EDIT, () => cancelEdit()) const disablePrompts = useFlag('disableAssistantPrompts') const { snippets } = useSqlEditorV2StateSnapshot() const snap = useAiAssistantStateSnapshot() const state = useAiAssistantState() const { activeSidebar, closeSidebar } = useSidebarManagerSnapshot() const { hasAccess: hasAccessToAdvanceModel, isLoading: isLoadingEntitlements } = useCheckEntitlements('assistant.advance_model') const selectedModel = useMemo(() => { // While entitlements are loading, use the stored model without enforcing access if (isLoadingEntitlements) { return snap.model ?? DEFAULT_ASSISTANT_BASE_MODEL_ID } const defaultModel = defaultAssistantModelId(hasAccessToAdvanceModel) const model = snap.model ?? defaultModel if (!isKnownAssistantModelId(model)) return defaultModel if (!hasAccessToAdvanceModel && !isAssistantBaseModelId(model)) { return DEFAULT_ASSISTANT_BASE_MODEL_ID } return model }, [isLoadingEntitlements, hasAccessToAdvanceModel, snap.model]) const [updatedOptInSinceMCP] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.AI_ASSISTANT_MCP_OPT_IN, false ) const inputRef = useRef(null) const { aiOptInLevel, isHipaaProjectDisallowed } = useOrgAiOptInLevel() const showMetadataWarning = IS_PLATFORM && !!selectedOrganization && (aiOptInLevel === 'disabled' || aiOptInLevel === 'schema') // Add a ref to store the last user message const lastUserMessageRef = useRef(null) // Keep latest selected organization to avoid stale values in useChat transport const selectedOrganizationRef = useRef(selectedOrganization) useEffect(() => { selectedOrganizationRef.current = selectedOrganization }, [selectedOrganization]) const [value, setValue] = useState(snap.initialInput || '') const [editingMessageId, setEditingMessageId] = useState(null) const [isResubmitting, setIsResubmitting] = useState(false) const [messageRatings, setMessageRatings] = useState>({}) const { data: check, isSuccess } = useCheckOpenAIKeyQuery() const isApiKeySet = !!check?.hasKey const { mutateAsync: rateMessage } = useRateMessageMutation() const isInSQLEditor = router.pathname.includes('/sql/[id]') const snippet = snippets[entityId ?? ''] const snippetContent = snippet?.snippet?.content?.unchecked_sql const { data: tables } = useTablesQuery( { projectRef: project?.ref, connectionString: project?.connectionString, schema: 'public', }, { enabled: isApiKeySet } ) const currentTable = tables?.find((t) => t.id.toString() === entityId) const currentSchema = searchParams?.get('schema') ?? 'public' // Update context in state useEffect(() => { state.setContext({ projectRef: project?.ref, orgSlug: selectedOrganizationRef.current?.slug, connectionString: project?.connectionString ?? '', }) }, [project?.ref, project?.connectionString, selectedOrganizationRef.current?.slug, state]) const track = useTrack() const { messages: chatMessages, status: chatStatus, error, sendMessage, setMessages, addToolApprovalResponse, stop, regenerate, } = useChat({ id: snap.activeChatId, ...(snap.activeChatId && snap.chatInstances[snap.activeChatId] ? { chat: snap.chatInstances[snap.activeChatId] } : {}), sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses, onError: onErrorChat, }) const isChatLoading = chatStatus === 'submitted' || chatStatus === 'streaming' const hasPendingApproval = hasPendingToolApproval(chatMessages) const isChatInputDisabled = !isApiKeySet || disablePrompts || isLoadingOrganization 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) if (messageIndex === -1) return // Target message const messageToEdit = chatMessages[messageIndex] // Activate editing mode setEditingMessageId(messageId) const textContent = messageToEdit.parts ?.filter((part) => part.type === 'text') .map((part) => part.text) .join('') ?? '' setValue(textContent) 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] ) const cancelEdit = useCallback(() => { setEditingMessageId(null) setValue('') }, [setValue]) const handleRateMessage = useCallback( async (messageId: string, rating: 'positive' | 'negative', reason?: string) => { if (!project?.ref || !selectedOrganization?.slug) return // Optimistically update UI setMessageRatings((prev) => ({ ...prev, [messageId]: rating })) try { const result = await rateMessage({ rating, messages: chatMessages, messageId, projectRef: project.ref, orgSlug: selectedOrganization.slug, reason, spanId: state.messageSpanIds[messageId], }) track('assistant_message_rating_submitted', { rating, category: result.category, ...(reason && { reason }), chatId: state.activeChatId, }) } catch (error) { console.error('Failed to rate message:', error) // Rollback on error setMessageRatings((prev) => { const { [messageId]: _, ...rest } = prev return rest }) } }, [chatMessages, project?.ref, selectedOrganization?.slug, rateMessage, track, state] ) const isContextExceededError = error && (error.message?.includes('context_length_exceeded') || error.message?.includes('exceeds the context window')) const renderedMessages = useMemo( () => chatMessages.map((message, index) => { const isBeingEdited = editingMessageId === message.id const isAfterEditedMessage = editingMessageId ? chatMessages.findIndex((m) => m.id === editingMessageId) < index : false const isLastMessage = index === chatMessages.length - 1 return ( ) }), [ chatMessages, deleteMessageFromHere, editMessage, cancelEdit, editingMessageId, chatStatus, addToolApprovalResponse, handleRateMessage, messageRatings, ] ) const hasMessages = chatMessages.length > 0 const sendMessageToAssistant = (finalContent: string) => { if (editingMessageId) { // Handling when the user is in edit mode // delete the message(s) from the chat just like the delete button setIsResubmitting(true) deleteMessageFromHere(editingMessageId) setEditingMessageId(null) } const payload = { role: 'user', createdAt: new Date(), parts: [{ type: 'text', text: finalContent }], id: uuidv4(), } as MessageType snap.clearSqlSnippets() lastUserMessageRef.current = payload if (hasPendingApproval && !editingMessageId) { setMessages(resolvePendingToolApprovalsAsDenied(chatMessages)) } sendMessage(payload, { body: { schema: currentSchema, table: currentTable?.name, }, }) setValue('') if (finalContent.includes('Help me to debug')) { track('assistant_debug_submitted', { chatId: snap.activeChatId }) } else { track('assistant_prompt_submitted', { chatId: snap.activeChatId }) } } const handleClearMessages = () => { if (isChatLoading) stop() snap.clearMessages() setMessages([]) lastUserMessageRef.current = null 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]) useEffect(() => { setValue(snap.initialInput || '') if (inputRef.current && snap.initialInput) { inputRef.current.focus() inputRef.current.setSelectionRange(snap.initialInput.length, snap.initialInput.length) } }, [snap.initialInput]) useEffect(() => { const isOpen = activeSidebar?.id === SIDEBAR_KEYS.AI_ASSISTANT if (isOpen && isInSQLEditor && !!snippetContent) { snap.setSqlSnippets([{ label: 'Current Query', content: snippetContent }]) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeSidebar?.id, isInSQLEditor, snippetContent]) return ( { handleClearMessages() window.location.reload() }, }, ]} >
closeSidebar(SIDEBAR_KEYS.AI_ASSISTANT)} showMetadataWarning={showMetadataWarning} updatedOptInSinceMCP={updatedOptInSinceMCP} isHipaaProjectDisallowed={isHipaaProjectDisallowed} aiOptInLevel={aiOptInLevel} /> {hasMessages ? ( {renderedMessages} {error && ( <> {isContextExceededError ? ( ) : ( <> } tooltip={{ content: { side: 'bottom', text: 'Clear messages' } }} /> )}
} /> )} {isChatLoading && ( )}

Briven AI may not always produce correct answers. Double check responses.

) : ( setValue(val)} onFocusInput={() => inputRef.current?.focus()} /> )} {editingMessageId && (
Editing message
} onClick={cancelEdit} className="w-6 h-6 p-0" title="Cancel editing" aria-label="Cancel editing" tooltip={{ content: { side: 'top', text: }, }} />
)}
{disablePrompts && ( )} {isSuccess && !isApiKeySet && ( } /> )} form>textarea]:text-base [&>form>textarea]:md:text-sm [&>form>textarea]:border [&>form>textarea]:rounded-md [&>form>textarea]:outline-hidden! [&>form>textarea]:ring-offset-0! [&>form>textarea]:ring-0!' )} loading={isChatLoading} isEditing={!!editingMessageId} disabled={isChatInputDisabled} placeholder={ hasMessages ? 'Ask a follow up question...' : (snap.sqlSnippets ?? [])?.length > 0 ? 'Ask a question or make a change...' : 'Chat to Postgres...' } value={value} onValueChange={(e) => setValue(e.target.value)} onSubmit={(finalMessage) => { sendMessageToAssistant(finalMessage) }} onStop={() => { stop() // to save partial responses from the AI const lastMessage = chatMessages[chatMessages.length - 1] if (lastMessage && lastMessage.role === 'assistant') { state.updateMessage(lastMessage) } }} sqlSnippets={snap.sqlSnippets as SqlSnippet[] | undefined} onRemoveSnippet={(index) => { const newSnippets = [...(snap.sqlSnippets ?? [])] newSnippets.splice(index, 1) snap.setSqlSnippets(newSnippets) }} includeSnippetsInMessage={aiOptInLevel !== 'disabled'} selectedModel={selectedModel} onSelectModel={(model) => snap.setModel(model)} />
) }