From c33d255eba8ba9ff7c7a2a01a93dce80cb685708 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Thu, 10 Sep 2026 15:39:21 +1000 Subject: [PATCH] chore(studio): refine assistant empty states (#50191) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image image ## What kind of change does this PR introduce? UI refinement. ## What is the current behavior? The assistant sidebar and Explorer chat use different empty states. ## What is the new behavior? Share a centered empty state with chat-focused prompts, use-case icons, and a “Use your own agent” footer. Simplify prompt cards and align Explorer Home styling. ## Additional context Studio typecheck, targeted ESLint, and Prettier pass. E2E selectors updated; browser verification pending. Local build stopped after showing no progress during compilation. I have read CONTRIBUTING.md. ## Summary by CodeRabbit * **New Features** * Redesigned Explorer and AI Assistant onboarding with project-focused chat prompts. * Added personalized chat template icons and updated suggested actions. * Added an option to connect and use an external agent through Claude, OpenAI, or Cursor. * Improved chat composer placement and empty-chat guidance. * **Style** * Action cards now support layouts without descriptions and adjust alignment automatically. * **Tests** * Updated end-to-end coverage for the revised assistant interface. --- .../interfaces/Explorer/ExplorerHomeTab.tsx | 19 +- .../interfaces/Explorer/templates.ts | 24 -- .../components/layouts/Tabs/ActionCard.tsx | 8 +- .../AIAssistantPanel/AIAssistant.prompts.ts | 47 ++-- .../ui/AIAssistantPanel/AIOnboarding.tsx | 208 +++++------------- .../AssistantAgentHarnessFooter.tsx | 30 +++ .../ui/AIAssistantPanel/AssistantChat.tsx | 197 +++++++++-------- e2e/studio/features/assistant.spec.ts | 7 +- 8 files changed, 236 insertions(+), 304 deletions(-) create mode 100644 apps/studio/components/ui/AIAssistantPanel/AssistantAgentHarnessFooter.tsx diff --git a/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.tsx b/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.tsx index ef4bef56b26..80da4693f81 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.tsx +++ b/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.tsx @@ -1,9 +1,11 @@ -import { MessageSquarePlus, NotebookText, SquareCode } from 'lucide-react' +import { NotebookText, SquareCode } from 'lucide-react' import { useState } from 'react' import { useCreateChat, useCreateNotebook, useCreateQuery } from './hooks' -import { CHAT_TEMPLATES, NOTEBOOK_TEMPLATES } from './templates' +import { NOTEBOOK_TEMPLATES } from './templates' import { ActionCard } from '@/components/layouts/Tabs/ActionCard' +import { CHAT_TEMPLATES } from '@/components/ui/AIAssistantPanel/AIAssistant.prompts' +import { AssistantAgentHarnessFooter } from '@/components/ui/AIAssistantPanel/AssistantAgentHarnessFooter' import { AssistantChatForm } from '@/components/ui/AIAssistantPanel/AssistantChatForm' export const ExplorerHomeTab = () => { @@ -16,21 +18,19 @@ export const ExplorerHomeTab = () => { return (
-
-

Explore your project

-

- Ask the Assistant about your data, or begin with a new resource. -

+
+

Run SQL. Chat with your project. Create a Notebook

setValue(e.target.value)} onSubmit={(message) => createChat({ initialMessage: message })} /> +
@@ -70,9 +70,8 @@ export const ExplorerHomeTab = () => { {CHAT_TEMPLATES.map((template) => ( } + icon={} 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) => ( - - - - ))} -
- ) : ( - <> - {isLoadingLints ? ( -
- - {Array.from({ length: 6 }).map((_, index) => ( - - ))} -
- ) : ( - <> - {performanceErrorLints.length > 0 && ( -
-

- Improve Performance -

- {performanceErrorLints.map((lint, index) => { - return ( - - ) - })} -
- )} - - {securityErrorLints.length > 0 && ( -
-

- Improve Security -

- {securityErrorLints.map((lint, index) => { - return ( - - ) - })} -
- )} - -
-

Ideas

- {prompts.map((item, index) => ( - - ))} -
- - )} - - )} +
+
+
+

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(