mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
chore(studio): refine assistant empty states (#50191)
<img width="1062" height="712" alt="image" src="https://github.com/user-attachments/assets/44c82e00-94b4-405a-b2ef-cbc08401c4db" /> <img width="1583" height="962" alt="image" src="https://github.com/user-attachments/assets/b19b9fd8-24b1-48d6-8b31-11fba9911b9a" /> ## 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. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
1131e3e2ce
commit
c33d255eba
8 files changed
+236
-304
No files matched your search
@@ -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 (
|
||||
<div className="bg-surface-100 h-full flex flex-col items-center justify-center px-10">
|
||||
<div className="w-full max-w-2xl">
|
||||
<div className="flex items-center justify-center flex-col gap-y-1 mb-12">
|
||||
<h1 className="heading-section">Explore your project</h1>
|
||||
<p className="text-foreground-lighter text-sm">
|
||||
Ask the Assistant about your data, or begin with a new resource.
|
||||
</p>
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="heading-section">Run SQL. Chat with your project. Create a Notebook</h1>
|
||||
</div>
|
||||
|
||||
<AssistantChatForm
|
||||
loading={false}
|
||||
className="bg"
|
||||
placeholder="Ask anything about your project"
|
||||
placeholder="Explore your data, check project health, create a notebook..."
|
||||
value={value}
|
||||
onValueChange={(e) => setValue(e.target.value)}
|
||||
onSubmit={(message) => createChat({ initialMessage: message })}
|
||||
/>
|
||||
<AssistantAgentHarnessFooter />
|
||||
|
||||
<section className="mt-6">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
@@ -70,9 +70,8 @@ export const ExplorerHomeTab = () => {
|
||||
{CHAT_TEMPLATES.map((template) => (
|
||||
<ActionCard
|
||||
key={template.title}
|
||||
icon={<MessageSquarePlus className="h-4 w-4 text-foreground" strokeWidth={1.5} />}
|
||||
icon={<template.icon className="h-4 w-4 text-foreground" strokeWidth={1.5} />}
|
||||
title={template.title}
|
||||
description={template.description}
|
||||
bgColor="bg-blue-500"
|
||||
onClick={() =>
|
||||
createChat({ name: template.title, initialMessage: template.initialMessage })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<div className={`relative flex items-start gap-3`}>
|
||||
<div className={cn('relative flex gap-3', card.description ? 'items-start' : 'items-center')}>
|
||||
<div
|
||||
className={`rounded-full ${card.bgColor} w-8 h-8 flex items-center justify-center shrink-0`}
|
||||
>
|
||||
@@ -21,7 +21,7 @@ export const ActionCard = (card: {
|
||||
</div>
|
||||
<div className="flex flex-col gap-0">
|
||||
<h3 className="text-sm text-foreground mb-0">{card.title}</h3>
|
||||
<p className="text-xs text-foreground-light">{card.description}</p>
|
||||
{card.description && <p className="text-xs text-foreground-light">{card.description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
@@ -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: <FileText strokeWidth={1.25} size={14} className="w-4! h-4!" />,
|
||||
}))
|
||||
: 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 (
|
||||
<div className="flex-1 overflow-y-auto max-w-3xl mx-auto w-full">
|
||||
<div className="w-full flex-1 max-h-full min-h-full px-4 flex flex-col gap-0">
|
||||
<div className="mt-auto w-full space-y-6 py-8 ">
|
||||
<motion.h2
|
||||
className="heading-section text-foreground mx-4 flex items-center gap-x-3"
|
||||
initial={{ y: 5, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
How can I assist you?
|
||||
<AiIconAnimation size={18} allowHoverEffect={false} />
|
||||
</motion.h2>
|
||||
{suggestions?.prompts?.length ? (
|
||||
<div>
|
||||
<h3 className="heading-meta text-foreground-light mb-3 mx-4">Suggestions</h3>
|
||||
{prompts.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.title}
|
||||
initial={{ y: 5, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
className="w-full justify-start border-b hover:border-b-0 hover:rounded-md rounded-none"
|
||||
icon={
|
||||
<FileText strokeWidth={1.5} size={14} className="text-foreground-light" />
|
||||
}
|
||||
onClick={() => {
|
||||
onValueChange(item.prompt)
|
||||
onFocusInput?.()
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Button>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{isLoadingLints ? (
|
||||
<div className="px-4 flex flex-col gap-2">
|
||||
<Skeleton className="h-5 w-20" />
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<Skeleton key={`loader-${index}`} className="h-7 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{performanceErrorLints.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="heading-meta text-foreground-light mb-3 mx-4">
|
||||
Improve Performance
|
||||
</h3>
|
||||
{performanceErrorLints.map((lint, index) => {
|
||||
return (
|
||||
<Button
|
||||
key={`${lint.name}-${index}`}
|
||||
size="small"
|
||||
variant="text"
|
||||
className="w-full justify-start"
|
||||
icon={
|
||||
<BarChart
|
||||
strokeWidth={1.5}
|
||||
size={14}
|
||||
className="text-foreground-light"
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
onValueChange(createLintSummaryPrompt(lint))
|
||||
onFocusInput?.()
|
||||
}}
|
||||
>
|
||||
{lint.detail ? lint.detail.replace('\\`', '') : lint.title}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{securityErrorLints.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="heading-meta text-foreground-light mb-3 mx-4">
|
||||
Improve Security
|
||||
</h3>
|
||||
{securityErrorLints.map((lint, index) => {
|
||||
return (
|
||||
<Button
|
||||
key={`${lint.name}-${index}`}
|
||||
size="small"
|
||||
variant="text"
|
||||
className="w-full justify-start"
|
||||
icon={<Shield strokeWidth={1.5} size={14} className="text-warning" />}
|
||||
onClick={() => {
|
||||
onValueChange(createLintSummaryPrompt(lint))
|
||||
onFocusInput?.()
|
||||
}}
|
||||
>
|
||||
{lint.detail ? lint.detail.replace(/\\`/g, '') : lint.title}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h3 className="heading-meta text-foreground-light mb-3 mx-4">Ideas</h3>
|
||||
{prompts.map((item, index) => (
|
||||
<Button
|
||||
key={`${item.title}-${index}`}
|
||||
size="small"
|
||||
variant="text"
|
||||
className="w-full justify-start"
|
||||
icon={
|
||||
<FileText strokeWidth={1.5} size={14} className="text-foreground-light" />
|
||||
}
|
||||
onClick={() => {
|
||||
onValueChange(item.prompt)
|
||||
onFocusInput?.()
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="@container flex min-h-0 flex-1 flex-col overflow-y-auto px-6 py-8">
|
||||
<div className="m-auto w-full max-w-2xl">
|
||||
<div className="mb-6 text-center">
|
||||
<h2 className="heading-section">Chat with your project</h2>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
|
||||
<section className="mt-8 flex flex-col gap-y-3">
|
||||
{hasSuggestions && suggestions?.title && (
|
||||
<h3 className="text-sm font-medium text-foreground">{suggestions.title}</h3>
|
||||
)}
|
||||
<div className="grid grid-cols-1 gap-3 @2xl:grid-cols-3">
|
||||
{templates.map((template) => (
|
||||
<button
|
||||
key={template.title}
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
className="rounded-md text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
|
||||
onClick={() => {
|
||||
onValueChange(template.initialMessage)
|
||||
onFocusInput?.()
|
||||
}}
|
||||
>
|
||||
<ActionCard
|
||||
icon={<template.icon className="h-4 w-4 text-foreground" strokeWidth={1.5} />}
|
||||
title={template.title}
|
||||
bgColor="bg-brand-400"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
<div className="mx-3">
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
className="flex w-full shrink-0 cursor-pointer items-center justify-between gap-3 overflow-hidden rounded-b-lg border-x border-b bg-surface-100 py-2 pr-2 pl-3 text-left text-xs text-foreground-light shadow-xs transition-colors hover:bg-surface-200 hover:border-default"
|
||||
onClick={() => setConnectParams({ showConnect: true, connectTab: 'mcp' })}
|
||||
>
|
||||
<span className="min-w-0 flex-1">Use your own agent</span>
|
||||
<span className="flex shrink-0 items-center gap-3" aria-hidden="true">
|
||||
<ConnectionIcon connection="claude" theme={theme} />
|
||||
<ConnectionIcon connection="openai" theme={theme} hasDistinctDarkIcon />
|
||||
<ConnectionIcon connection="cursor" theme={theme} hasDistinctDarkIcon />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 = (
|
||||
<div className={cn('relative z-20 w-full', hasMessages && 'px-7 pb-3')}>
|
||||
<div className="mx-auto flex w-full max-w-3xl flex-col gap-y-3">
|
||||
{isSupportChat && !isSupportChatClosed && (
|
||||
<div>
|
||||
<div className="mb-3 border-t" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="tiny"
|
||||
disabled={!supportConversationId}
|
||||
onClick={() => state.setSupportLifecycleStatus(chatId, 'escalated')}
|
||||
>
|
||||
Escalate to human
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="tiny"
|
||||
disabled={!supportConversationId}
|
||||
onClick={() => state.setSupportLifecycleStatus(chatId, 'user_resolved')}
|
||||
>
|
||||
Resolve
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{disablePrompts && (
|
||||
<Admonition
|
||||
showIcon={false}
|
||||
type="default"
|
||||
title="Assistant has been temporarily disabled"
|
||||
description="We're currently looking into getting it back online"
|
||||
/>
|
||||
)}
|
||||
|
||||
{isSuccess && !isApiKeySet && (
|
||||
<Admonition
|
||||
type="default"
|
||||
title="OpenAI API key not set"
|
||||
description={
|
||||
<Markdown
|
||||
content={
|
||||
'Add your `OPENAI_API_KEY` to your environment variables to use the AI Assistant.'
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<AssistantChatForm
|
||||
textAreaRef={inputRef}
|
||||
className={cn('z-20', !hasMessages && 'bg')}
|
||||
loading={isChatLoading}
|
||||
isEditing={!!editingMessageId}
|
||||
disabled={isChatInputDisabled}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onValueChange={(e) => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
message="Something went wrong with the AI Assistant"
|
||||
@@ -546,15 +633,7 @@ export const AssistantChat = ({
|
||||
</ConversationContent>
|
||||
<ConversationScrollButton />
|
||||
</Conversation>
|
||||
) : (
|
||||
<AIOnboarding
|
||||
key={chatId}
|
||||
sqlSnippets={composerContext?.sqlSnippets}
|
||||
suggestions={composerContext?.suggestions}
|
||||
onValueChange={(val) => setValue(val)}
|
||||
onFocusInput={() => inputRef.current?.focus()}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<AnimatePresence>
|
||||
{editingMessageId && (
|
||||
@@ -600,87 +679,23 @@ export const AssistantChat = ({
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="relative z-20 w-full px-7 pb-3">
|
||||
<div className="mx-auto flex w-full max-w-3xl flex-col gap-y-3">
|
||||
{isSupportChat && !isSupportChatClosed && (
|
||||
<div>
|
||||
<div className="mb-3 border-t" />
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="tiny"
|
||||
disabled={!supportConversationId}
|
||||
onClick={() => state.setSupportLifecycleStatus(chatId, 'escalated')}
|
||||
>
|
||||
Escalate to human
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="tiny"
|
||||
disabled={!supportConversationId}
|
||||
onClick={() => state.setSupportLifecycleStatus(chatId, 'user_resolved')}
|
||||
>
|
||||
Resolve
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{disablePrompts && (
|
||||
<Admonition
|
||||
showIcon={false}
|
||||
type="default"
|
||||
title="Assistant has been temporarily disabled"
|
||||
description="We're currently looking into getting it back online"
|
||||
/>
|
||||
)}
|
||||
|
||||
{isSuccess && !isApiKeySet && (
|
||||
<Admonition
|
||||
type="default"
|
||||
title="OpenAI API key not set"
|
||||
description={
|
||||
<Markdown
|
||||
content={
|
||||
'Add your `OPENAI_API_KEY` to your environment variables to use the AI Assistant.'
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AssistantChatForm
|
||||
textAreaRef={inputRef}
|
||||
className="z-20"
|
||||
loading={isChatLoading}
|
||||
isEditing={!!editingMessageId}
|
||||
disabled={isChatInputDisabled}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onValueChange={(e) => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{hasMessages ? (
|
||||
composer
|
||||
) : (
|
||||
<AIOnboarding
|
||||
key={chatId}
|
||||
sqlSnippets={composerContext?.sqlSnippets}
|
||||
suggestions={composerContext?.suggestions}
|
||||
onValueChange={(prompt) => {
|
||||
setValue(prompt)
|
||||
onInputChange?.(prompt)
|
||||
}}
|
||||
onFocusInput={() => inputRef.current?.focus()}
|
||||
>
|
||||
{composer}
|
||||
{!isSupportChat && <AssistantAgentHarnessFooter />}
|
||||
</AIOnboarding>
|
||||
)}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user