@@ -70,9 +70,8 @@ export const ExplorerHomeTab = () => {
{CHAT_TEMPLATES.map((template) => (
}
title={template.title}
- description={template.description}
bgColor="bg-blue-500"
onClick={() =>
createChat({ name: template.title, initialMessage: template.initialMessage })
diff --git a/apps/studio/components/interfaces/Explorer/templates.ts b/apps/studio/components/interfaces/Explorer/templates.ts
index 498f958b824..31e33055af8 100644
--- a/apps/studio/components/interfaces/Explorer/templates.ts
+++ b/apps/studio/components/interfaces/Explorer/templates.ts
@@ -1,30 +1,6 @@
import { createLogCellSkeleton, createMarkdownCellSkeleton, createQueryCellSkeleton } from './utils'
import type { Notebooks } from '@/types'
-export type ChatTemplate = {
- title: string
- description: string
- initialMessage: string
-}
-
-export const CHAT_TEMPLATES: ChatTemplate[] = [
- {
- title: 'Generate sample data',
- description: 'Chat template',
- initialMessage: 'Generate sample data for a blog with users, posts, and comments tables.',
- },
- {
- title: 'Set up RLS policies',
- description: 'Chat template',
- initialMessage: 'Create RLS policies to ensure users can only access their own data.',
- },
- {
- title: 'Build a notebook',
- description: 'Chat template',
- initialMessage: 'Build me a notebook that tracks weekly signups and active users.',
- },
-]
-
export type NotebookTemplate = {
title: string
description: string
diff --git a/apps/studio/components/layouts/Tabs/ActionCard.tsx b/apps/studio/components/layouts/Tabs/ActionCard.tsx
index 24f4057997b..9586bb2599a 100644
--- a/apps/studio/components/layouts/Tabs/ActionCard.tsx
+++ b/apps/studio/components/layouts/Tabs/ActionCard.tsx
@@ -1,10 +1,10 @@
import type { ReactNode } from 'react'
-import { Card } from 'ui'
+import { Card, cn } from 'ui'
export const ActionCard = (card: {
icon: ReactNode
title: string
- description: string
+ description?: string
bgColor: string
onClick?: () => void
}) => {
@@ -13,7 +13,7 @@ export const ActionCard = (card: {
className="grow bg-surface-100 p-3 transition-colors hover:bg-surface-200 border hover:border-default cursor-pointer"
onClick={card.onClick}
>
-
+
@@ -21,7 +21,7 @@ export const ActionCard = (card: {
{card.title}
-
{card.description}
+ {card.description &&
{card.description}
}
diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.prompts.ts b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.prompts.ts
index 8274e777d8e..b2711d99aa8 100644
--- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.prompts.ts
+++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.prompts.ts
@@ -1,42 +1,51 @@
-export const defaultPrompts = [
+import {
+ BookOpen,
+ Bug,
+ Database,
+ Gauge,
+ NotebookText,
+ ShieldCheck,
+ type LucideIcon,
+} from 'lucide-react'
+
+export type ChatTemplate = {
+ title: string
+ icon: LucideIcon
+ initialMessage: string
+}
+
+export const CHAT_TEMPLATES: ChatTemplate[] = [
{
- title: 'Create a back-end',
- prompt:
- 'Create a messaging app with users, messages, and an edge function that uses OpenAI to summarize message threads.',
- },
- {
- title: 'Health check',
- prompt: 'Can you check if my database and edge functions are healthy?',
- },
- {
- title: 'Query your data',
- prompt: 'Give me a list of new users from the auth.users table who signed up in the past week',
+ title: 'Generate sample data',
+ icon: Database,
+ initialMessage: 'Generate sample data for a blog with users, posts, and comments tables.',
},
{
title: 'Set up RLS policies',
- prompt: 'Create RLS policies to ensure users can only access their own data',
+ icon: ShieldCheck,
+ initialMessage: 'Create RLS policies to ensure users can only access their own data.',
},
{
- title: 'Create a function',
- prompt: 'Create an edge function that summarises the contents of a table row using OpenAI',
- },
- {
- title: 'Generate sample data',
- prompt: 'Generate sample data for a blog with users, posts, and comments tables',
+ title: 'Build a notebook',
+ icon: NotebookText,
+ initialMessage: 'Build me a notebook that tracks weekly signups and active users.',
},
]
export const codeSnippetPrompts = [
{
title: 'Explain code',
+ icon: BookOpen,
prompt: 'Explain what this code does and how it works',
},
{
title: 'Improve code',
+ icon: Gauge,
prompt: 'How can I improve this code for better performance and readability?',
},
{
title: 'Debug issues',
+ icon: Bug,
prompt: 'Help me debug any potential issues with this code',
},
]
diff --git a/apps/studio/components/ui/AIAssistantPanel/AIOnboarding.tsx b/apps/studio/components/ui/AIAssistantPanel/AIOnboarding.tsx
index 83df5deb1a1..74771eb8ecb 100644
--- a/apps/studio/components/ui/AIAssistantPanel/AIOnboarding.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/AIOnboarding.tsx
@@ -1,15 +1,12 @@
-import { useParams } from 'common'
-import { motion } from 'framer-motion'
-import { BarChart, FileText, Shield } from 'lucide-react'
-import { AiIconAnimation, Button, Skeleton } from 'ui'
+import { MessageSquarePlus } from 'lucide-react'
+import type { ReactNode } from 'react'
-import { codeSnippetPrompts, defaultPrompts } from './AIAssistant.prompts'
-import { LINTER_LEVELS } from '@/components/interfaces/Linter/Linter.constants'
-import { createLintSummaryPrompt } from '@/components/interfaces/Linter/Linter.utils'
-import { useProjectLintsQuery, type Lint } from '@/data/lint/lint-query'
+import { CHAT_TEMPLATES, codeSnippetPrompts } from './AIAssistant.prompts'
+import { ActionCard } from '@/components/layouts/Tabs/ActionCard'
import type { SqlSnippet } from '@/state/ai-assistant-state'
interface AIOnboardingProps {
+ children: ReactNode
sqlSnippets?: SqlSnippet[]
suggestions?: {
title?: string
@@ -20,161 +17,64 @@ interface AIOnboardingProps {
}
export const AIOnboarding = ({
+ children,
sqlSnippets,
suggestions,
onValueChange,
onFocusInput,
}: AIOnboardingProps) => {
- const prompts = suggestions?.prompts
- ? suggestions.prompts.map((suggestion) => ({
- title: suggestion.label,
- prompt: suggestion.description,
- icon:
,
- }))
- : sqlSnippets && sqlSnippets.length > 0
- ? codeSnippetPrompts
- : defaultPrompts
+ const suggestionPrompts = suggestions?.prompts ?? []
+ const hasSuggestions = suggestionPrompts.length > 0
+ let templates = CHAT_TEMPLATES
- const { ref: projectRef } = useParams()
- const { data: lints, isLoading: isLoadingLints } = useProjectLintsQuery({ projectRef })
-
- const errorLints: Lint[] = (lints?.filter((lint) => lint.level === LINTER_LEVELS.ERROR) ??
- []) as Lint[]
- const securityErrorLints = errorLints.filter((lint) => lint.categories?.[0] === 'SECURITY')
- const performanceErrorLints = errorLints.filter((lint) => lint.categories?.[0] !== 'SECURITY')
+ if (hasSuggestions) {
+ templates = suggestionPrompts.map((suggestion) => ({
+ title: suggestion.label,
+ icon: MessageSquarePlus,
+ initialMessage: suggestion.description,
+ }))
+ } else if (sqlSnippets && sqlSnippets.length > 0) {
+ templates = codeSnippetPrompts.map((prompt) => ({
+ title: prompt.title,
+ icon: prompt.icon,
+ initialMessage: prompt.prompt,
+ }))
+ }
return (
-
-
-
-
- How can I assist you?
-
-
- {suggestions?.prompts?.length ? (
-
-
Suggestions
- {prompts.map((item, index) => (
-
-
- }
- onClick={() => {
- onValueChange(item.prompt)
- onFocusInput?.()
- }}
- >
- {item.title}
-
-
- ))}
-
- ) : (
- <>
- {isLoadingLints ? (
-
-
- {Array.from({ length: 6 }).map((_, index) => (
-
- ))}
-
- ) : (
- <>
- {performanceErrorLints.length > 0 && (
-
-
- Improve Performance
-
- {performanceErrorLints.map((lint, index) => {
- return (
-
- }
- onClick={() => {
- onValueChange(createLintSummaryPrompt(lint))
- onFocusInput?.()
- }}
- >
- {lint.detail ? lint.detail.replace('\\`', '') : lint.title}
-
- )
- })}
-
- )}
-
- {securityErrorLints.length > 0 && (
-
-
- Improve Security
-
- {securityErrorLints.map((lint, index) => {
- return (
- }
- onClick={() => {
- onValueChange(createLintSummaryPrompt(lint))
- onFocusInput?.()
- }}
- >
- {lint.detail ? lint.detail.replace(/\\`/g, '') : lint.title}
-
- )
- })}
-
- )}
-
-
-
Ideas
- {prompts.map((item, index) => (
-
- }
- onClick={() => {
- onValueChange(item.prompt)
- onFocusInput?.()
- }}
- >
- {item.title}
-
- ))}
-
- >
- )}
- >
- )}
+
+
+
+
Chat with your project
+
+ {children}
+
+
+ {hasSuggestions && suggestions?.title && (
+ {suggestions.title}
+ )}
+
+ {templates.map((template) => (
+
+ ))}
+
+
)
diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantAgentHarnessFooter.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantAgentHarnessFooter.tsx
new file mode 100644
index 00000000000..59efa660839
--- /dev/null
+++ b/apps/studio/components/ui/AIAssistantPanel/AssistantAgentHarnessFooter.tsx
@@ -0,0 +1,30 @@
+import { useTheme } from 'next-themes'
+import { parseAsBoolean, parseAsString, useQueryStates } from 'nuqs'
+import { ConnectionIcon } from 'ui-patterns/McpUrlBuilder/components/ConnectionIcon'
+
+export const AssistantAgentHarnessFooter = () => {
+ const { resolvedTheme } = useTheme()
+ const theme = resolvedTheme === 'dark' ? 'dark' : 'light'
+ const [, setConnectParams] = useQueryStates({
+ showConnect: parseAsBoolean.withDefault(false),
+ connectTab: parseAsString,
+ })
+
+ return (
+
+
+
+ )
+}
diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantChat.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantChat.tsx
index 20a61842684..afd612b6c0f 100644
--- a/apps/studio/components/ui/AIAssistantPanel/AssistantChat.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/AssistantChat.tsx
@@ -21,6 +21,7 @@ import {
resolvePendingToolApprovalsAsDenied,
} from './AIAssistant.utils'
import { AIOnboarding } from './AIOnboarding'
+import { AssistantAgentHarnessFooter } from './AssistantAgentHarnessFooter'
import { AssistantChatForm } from './AssistantChatForm'
import {
Conversation,
@@ -445,9 +446,95 @@ export const AssistantChat = ({
} else if (isSupportChat) {
placeholder = 'Describe your support issue...'
} else {
- placeholder = 'Chat to Postgres...'
+ placeholder = 'Ask about your data, troubleshoot an issue, or explore your project...'
}
+ const composer = (
+
+
+ {isSupportChat && !isSupportChatClosed && (
+
+
+
+
+
+
+
+ )}
+ {disablePrompts && (
+
+ )}
+
+ {isSuccess && !isApiKeySet && (
+
+ }
+ />
+ )}
+
+
+
{
+ setValue(e.target.value)
+ onInputChange?.(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, chatId)
+ }
+ }}
+ sqlSnippets={composerContext?.sqlSnippets}
+ onRemoveSnippet={(index) => {
+ const newSnippets = [...(composerContext?.sqlSnippets ?? [])]
+ newSnippets.splice(index, 1)
+ composerContext?.onSetSqlSnippets?.(newSnippets)
+ }}
+ includeSnippetsInMessage={includeSnippetsInMessage}
+ />
+
+
+
+ )
+
return (
- ) : (
- setValue(val)}
- onFocusInput={() => inputRef.current?.focus()}
- />
- )}
+ ) : null}
{editingMessageId && (
@@ -600,87 +679,23 @@ export const AssistantChat = ({
)}
-
-
- {isSupportChat && !isSupportChatClosed && (
-
-
-
-
-
-
-
- )}
- {disablePrompts && (
-
- )}
-
- {isSuccess && !isApiKeySet && (
-
- }
- />
- )}
-
-
{
- setValue(e.target.value)
- onInputChange?.(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, chatId)
- }
- }}
- sqlSnippets={composerContext?.sqlSnippets}
- onRemoveSnippet={(index) => {
- const newSnippets = [...(composerContext?.sqlSnippets ?? [])]
- newSnippets.splice(index, 1)
- composerContext?.onSetSqlSnippets?.(newSnippets)
- }}
- includeSnippetsInMessage={includeSnippetsInMessage}
- />
-
-
+ {hasMessages ? (
+ composer
+ ) : (
+ {
+ setValue(prompt)
+ onInputChange?.(prompt)
+ }}
+ onFocusInput={() => inputRef.current?.focus()}
+ >
+ {composer}
+ {!isSupportChat && }
+
+ )}
)
diff --git a/e2e/studio/features/assistant.spec.ts b/e2e/studio/features/assistant.spec.ts
index c549040e531..d510ca77c12 100644
--- a/e2e/studio/features/assistant.spec.ts
+++ b/e2e/studio/features/assistant.spec.ts
@@ -1,4 +1,5 @@
import { expect } from '@playwright/test'
+
import { test } from '../utils/test.js'
import { toUrl } from '../utils/to-url.js'
@@ -16,10 +17,12 @@ test.describe('AI Assistant', async () => {
await page.locator('#assistant-trigger').click()
// Wait for the assistant panel to be visible
- await expect(page.getByRole('heading', { name: 'How can I assist you?' })).toBeVisible()
+ await expect(page.getByRole('heading', { name: 'Chat with your project' })).toBeVisible()
// Type "hello" in the chat input
- const chatInput = page.getByRole('textbox', { name: 'Chat to Postgres...' })
+ const chatInput = page.getByRole('textbox', {
+ name: 'Ask about your data, troubleshoot an issue, or explore your project...',
+ })
await chatInput.fill('hello')
const responsePromise = page.waitForResponse(