diff --git a/apps/studio/components/interfaces/Linter/LintDetail.tsx b/apps/studio/components/interfaces/Linter/LintDetail.tsx
index 334353bd703..a88d3bb6b45 100644
--- a/apps/studio/components/interfaces/Linter/LintDetail.tsx
+++ b/apps/studio/components/interfaces/Linter/LintDetail.tsx
@@ -2,15 +2,15 @@ import Link from 'next/link'
import ReactMarkdown from 'react-markdown'
import { createLintSummaryPrompt, lintInfoMap } from 'components/interfaces/Linter/Linter.utils'
+import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { Lint } from 'data/lint/lint-query'
import { DOCS_URL } from 'lib/constants'
import { useTrack } from 'lib/telemetry/track'
import { ExternalLink } from 'lucide-react'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
+import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
import { AiIconAnimation, Button } from 'ui'
import { EntityTypeIcon, LintCTA, LintEntity } from './Linter.utils'
-import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
-import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
interface LintDetailProps {
lint: Lint
@@ -35,7 +35,7 @@ const LintDetail = ({ lint, projectRef, onAskAssistant }: LintDetailProps) => {
openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
snap.newChat({
name: 'Summarize lint',
- initialInput: createLintSummaryPrompt(lint),
+ initialMessage: createLintSummaryPrompt(lint),
})
}
diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
index c075f57000b..3f353fb3712 100644
--- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
@@ -1,6 +1,6 @@
import type { UIMessage as MessageType } from '@ai-sdk/react'
import { useChat } from '@ai-sdk/react'
-import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai'
+import { lastAssistantMessageIsCompleteWithToolCalls } from 'ai'
import { AnimatePresence, motion } from 'framer-motion'
import { Eraser, Info, Pencil, X } from 'lucide-react'
import { useRouter } from 'next/router'
@@ -12,7 +12,6 @@ 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 { constructHeaders } from 'data/fetchers'
import { useTablesQuery } from 'data/tables/tables-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
@@ -20,11 +19,10 @@ import { useOrgAiOptInLevel } from 'hooks/misc/useOrgOptedIntoAi'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useHotKey } from 'hooks/ui/useHotKey'
-import { prepareMessagesForAPI } from 'lib/ai/message-utils'
-import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
+import { IS_PLATFORM } from 'lib/constants'
import { uuidv4 } from 'lib/helpers'
import type { AssistantModel } from 'state/ai-assistant-state'
-import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
+import { useAiAssistantState, useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import { Button, cn, KeyboardShortcut } from 'ui'
@@ -61,6 +59,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
const disablePrompts = useFlag('disableAssistantPrompts')
const { snippets } = useSqlEditorV2StateSnapshot()
const snap = useAiAssistantStateSnapshot()
+ const state = useAiAssistantState()
const { closeSidebar, activeSidebar } = useSidebarManagerSnapshot()
const isPaidPlan = selectedOrganization?.plan?.id !== 'free'
@@ -125,41 +124,16 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
const currentSchema = searchParams?.get('schema') ?? 'public'
const currentChat = snap.activeChat?.name
- const { mutate: sendEvent } = useSendEventMutation()
-
- const updateMessage = useCallback(
- (updatedMessage: MessageType) => {
- snap.updateMessage(updatedMessage)
- },
- [snap]
- )
-
- // Handle completion of the assistant's response
- const handleChatFinish = useCallback(
- ({ message }: { message: MessageType }) => {
- if (lastUserMessageRef.current) {
- snap.saveMessage([lastUserMessageRef.current, message])
- lastUserMessageRef.current = null
- } else {
- updateMessage(message)
- }
- },
- [snap, updateMessage]
- )
-
- // TODO(refactor): This useChat hook should be moved down into each chat session.
- // That way we won't have to disable switching chats while the chat is loading,
- // and don't run the risk of messages getting mixed up between chats.
- // Sanitize messages to remove Valtio proxy wrappers that can't be cloned
- const sanitizedMessages = useMemo(() => {
- if (!snap.activeChat?.messages) return undefined
-
- return snap.activeChat.messages.map((msg: any) => {
- // Convert proxy objects to plain objects
- const plainMessage = JSON.parse(JSON.stringify(msg))
- return plainMessage
+ // Update context in state
+ useEffect(() => {
+ state.setContext({
+ projectRef: project?.ref,
+ orgSlug: selectedOrganizationRef.current?.slug,
+ connectionString: project?.connectionString || undefined,
})
- }, [snap.activeChat?.messages])
+ }, [project?.ref, project?.connectionString, selectedOrganizationRef.current?.slug, state])
+
+ const { mutate: sendEvent } = useSendEventMutation()
const {
messages: chatMessages,
@@ -172,60 +146,11 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
regenerate,
} = useChat({
id: snap.activeChatId,
+ ...(snap.activeChatId && snap.chatInstances[snap.activeChatId]
+ ? { chat: snap.chatInstances[snap.activeChatId] }
+ : {}),
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
- messages: sanitizedMessages,
- async onToolCall({ toolCall }) {
- if (toolCall.dynamic) {
- return
- }
-
- if (toolCall.toolName === 'rename_chat') {
- const { newName } = toolCall.input as { newName: string }
-
- if (snap.activeChatId && newName?.trim()) {
- snap.renameChat(snap.activeChatId, newName.trim())
-
- addToolResult({
- tool: toolCall.toolName,
- toolCallId: toolCall.toolCallId,
- output: 'Chat renamed',
- })
- } else {
- addToolResult({
- tool: toolCall.toolName,
- toolCallId: toolCall.toolCallId,
- output: 'Failed to rename chat: Invalid chat or name',
- })
- }
- }
- },
- transport: new DefaultChatTransport({
- api: `${BASE_PATH}/api/ai/sql/generate-v4`,
- async prepareSendMessagesRequest({ messages, ...options }) {
- const cleanedMessages = prepareMessagesForAPI(messages)
-
- const headerData = await constructHeaders()
- const authorizationHeader = headerData.get('Authorization')
-
- return {
- ...options,
- body: {
- messages: cleanedMessages,
- aiOptInLevel,
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- schema: currentSchema,
- table: currentTable?.name,
- chatName: currentChat,
- orgSlug: selectedOrganizationRef.current?.slug,
- model: selectedModel,
- },
- ...(IS_PLATFORM ? { headers: { Authorization: authorizationHeader ?? '' } } : {}),
- }
- },
- }),
onError: onErrorChat,
- onFinish: handleChatFinish,
})
const isChatLoading = chatStatus === 'submitted' || chatStatus === 'streaming'
@@ -389,7 +314,12 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
snap.clearSqlSnippets()
lastUserMessageRef.current = payload
- sendMessage(payload)
+ sendMessage(payload, {
+ body: {
+ schema: currentSchema,
+ table: currentTable?.name,
+ },
+ })
setValue('')
if (finalContent.includes('Help me to debug')) {
@@ -412,6 +342,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
}
const handleClearMessages = () => {
+ if (isChatLoading) stop()
snap.clearMessages()
setMessages([])
lastUserMessageRef.current = null
@@ -649,7 +580,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
// to save partial responses from the AI
const lastMessage = chatMessages[chatMessages.length - 1]
if (lastMessage && lastMessage.role === 'assistant') {
- handleChatFinish({ message: lastMessage })
+ state.updateMessage(lastMessage)
}
}}
sqlSnippets={snap.sqlSnippets as SqlSnippet[] | undefined}
diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistantHeader.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistantHeader.tsx
index 6299e05b72f..76724362782 100644
--- a/apps/studio/components/ui/AIAssistantPanel/AIAssistantHeader.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistantHeader.tsx
@@ -47,7 +47,7 @@ export const AIAssistantHeader = ({