mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Explorer home chat to present a Run SQL secondary action if value is detected to be a SQL query (#50560)
## Context We previously introduced a behaviour for the explorer home tab's chat form to run a SQL Query if the input is detected to be a SQL query. Adjusting it to shift that behaviour into a secondary action instead <img width="740" height="210" alt="image" src="https://github.com/user-attachments/assets/d4b1fec1-f38c-426f-8108-b50ead1a61ca" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * SQL statements entered in Explorer can be run directly with a dedicated “Run SQL” action. * Assistant forms support context-specific submit icons, labels, tooltips, and accessibility text. * **Bug Fixes** * Improved SQL detection for multi-statement queries. * Prevented mixed SQL and conversational text from being treated as executable SQL. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
85f19367ec
commit
501666e504
4 files changed
+76
-17
No files matched your search
@@ -55,6 +55,7 @@ const ExplorerHomeContent = () => {
|
||||
const { createChat } = useCreateChat()
|
||||
|
||||
const [value, setValue] = useState<string>('')
|
||||
const isSqlQuery = isSqlStatement(value)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
@@ -75,10 +76,14 @@ const ExplorerHomeContent = () => {
|
||||
placeholder="Explore your data, check project health, create a notebook..."
|
||||
value={value}
|
||||
onValueChange={(e) => setValue(e.target.value)}
|
||||
onSubmit={(message) =>
|
||||
isSqlStatement(message)
|
||||
? createQuery({ sql: message, autoRun: true })
|
||||
: createChat({ initialMessage: message })
|
||||
onSubmit={(message) => createChat({ initialMessage: message })}
|
||||
secondaryAction={
|
||||
isSqlQuery
|
||||
? {
|
||||
label: 'Run SQL',
|
||||
onClick: () => createQuery({ sql: value, autoRun: true }),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<AssistantAgentHarnessFooter />
|
||||
|
||||
@@ -19,6 +19,7 @@ describe('isSqlStatement', () => {
|
||||
'SHOW ALL;',
|
||||
'set search_path to public',
|
||||
"SET TIME ZONE 'UTC'",
|
||||
'select * from a;\n\nselect * from b;',
|
||||
])('returns true for %s', (message) => {
|
||||
expect(isSqlStatement(message)).toBe(true)
|
||||
})
|
||||
@@ -34,6 +35,7 @@ describe('isSqlStatement', () => {
|
||||
'Show me my tables',
|
||||
'Set up RLS on my users table',
|
||||
'With my current schema, what tables should I add?',
|
||||
'select * from colors;\n\nhelp me figure out what is wrong with this',
|
||||
])('returns false for %s', (message) => {
|
||||
expect(isSqlStatement(message)).toBe(false)
|
||||
})
|
||||
|
||||
@@ -17,17 +17,32 @@ const SET_STATEMENT_REGEX =
|
||||
/^\s*set\s+(session\s+|local\s+)?(time\s+zone\b|[a-zA-Z_][a-zA-Z0-9_.]*\s*(=|to)\s*\S)/i
|
||||
const WITH_STATEMENT_REGEX = /^\s*with\s+(recursive\s+)?[a-zA-Z_][a-zA-Z0-9_]*\s+as\s*\(/i
|
||||
|
||||
const isSqlLikeSegment = (segment: string): boolean =>
|
||||
UNAMBIGUOUS_SQL_STATEMENT_REGEX.test(segment) ||
|
||||
CREATE_STATEMENT_REGEX.test(segment) ||
|
||||
SHOW_STATEMENT_REGEX.test(segment) ||
|
||||
SET_STATEMENT_REGEX.test(segment) ||
|
||||
WITH_STATEMENT_REGEX.test(segment)
|
||||
|
||||
/**
|
||||
* Whether `message` looks like a SQL statement rather than a natural-language chat prompt,
|
||||
* so the Explorer home tab can route it to a query tab instead of creating an AI chat.
|
||||
*/
|
||||
export function isSqlStatement(message: string): boolean {
|
||||
const sql = removeCommentsFromSql(message)
|
||||
return (
|
||||
const sql = removeCommentsFromSql(message).trim()
|
||||
|
||||
const isSimpleStatement =
|
||||
UNAMBIGUOUS_SQL_STATEMENT_REGEX.test(sql) ||
|
||||
CREATE_STATEMENT_REGEX.test(sql) ||
|
||||
SHOW_STATEMENT_REGEX.test(sql) ||
|
||||
SET_STATEMENT_REGEX.test(sql) ||
|
||||
WITH_STATEMENT_REGEX.test(sql)
|
||||
)
|
||||
SET_STATEMENT_REGEX.test(sql)
|
||||
const isComplexStatement = CREATE_STATEMENT_REGEX.test(sql) || WITH_STATEMENT_REGEX.test(sql)
|
||||
|
||||
if (isComplexStatement) return true
|
||||
if (!isSimpleStatement) return false
|
||||
|
||||
// Simple statements (select/insert/update/.../show/set) are rarely legitimately
|
||||
// multi-paragraph, so a trailing paragraph that doesn't itself look like SQL means
|
||||
// this is a chat message that happens to include a query, not a query to run.
|
||||
const [, ...trailingParagraphs] = sql.split(/\n\s*\n/)
|
||||
return trailingParagraphs.every(isSqlLikeSegment)
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useBreakpoint } from 'common'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { ArrowUp, Loader2, Square } from 'lucide-react'
|
||||
import { ChangeEvent, FormEvent, forwardRef, KeyboardEvent, memo, useRef } from 'react'
|
||||
import { cn, ExpandingTextArea } from 'ui'
|
||||
import { ChangeEvent, FormEvent, forwardRef, KeyboardEvent, memo, ReactNode, useRef } from 'react'
|
||||
import { Button, cn, ExpandingTextArea } from 'ui'
|
||||
|
||||
import { ButtonTooltip } from '../ButtonTooltip'
|
||||
import { formatAttachedSnippets } from './AIAssistant.utils'
|
||||
@@ -49,6 +50,18 @@ export interface FormProps {
|
||||
selectedModel?: AssistantModelId
|
||||
/* Callback when a model is chosen */
|
||||
onSelectModel?: (model: AssistantModelId) => void
|
||||
/* Icon for the submit button when not loading, defaults to an up arrow */
|
||||
submitIcon?: ReactNode
|
||||
/* Label for the submit button when not loading, used for both aria-label and tooltip text, defaults to "Send message" */
|
||||
submitLabel?: string
|
||||
/**
|
||||
* An additional action shown beside the main submit button, transitioned in/out with
|
||||
* framer-motion. Pass undefined to hide it (e.g. when the input doesn't match its condition).
|
||||
*/
|
||||
secondaryAction?: {
|
||||
label: string
|
||||
onClick: () => void
|
||||
}
|
||||
}
|
||||
|
||||
const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(
|
||||
@@ -70,6 +83,9 @@ const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(
|
||||
isEditing = false,
|
||||
selectedModel,
|
||||
onSelectModel,
|
||||
submitIcon = <ArrowUp />,
|
||||
submitLabel = 'Send message',
|
||||
secondaryAction,
|
||||
...props
|
||||
},
|
||||
_ref
|
||||
@@ -134,7 +150,7 @@ const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-x-1.5 bottom-1.5 flex items-center pointer-events-none',
|
||||
'absolute inset-x-2 bottom-2 flex items-center pointer-events-none',
|
||||
showModelSelector ? 'justify-between' : 'justify-end'
|
||||
)}
|
||||
>
|
||||
@@ -144,7 +160,28 @@ const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 items-center pointer-events-auto">
|
||||
<div className="flex gap-x-1.5 items-center pointer-events-auto">
|
||||
<AnimatePresence>
|
||||
{!loading && secondaryAction && (
|
||||
<motion.div
|
||||
key="secondary-action"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
aria-label={secondaryAction.label}
|
||||
onClick={secondaryAction.onClick}
|
||||
className="h-7 rounded-full"
|
||||
>
|
||||
{secondaryAction.label}
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{loading ? (
|
||||
onStop ? (
|
||||
<ButtonTooltip
|
||||
@@ -162,14 +199,14 @@ const AssistantChatFormComponent = forwardRef<HTMLFormElement, FormProps>(
|
||||
<ButtonTooltip
|
||||
type="submit"
|
||||
variant={canSubmit ? 'primary' : 'default'}
|
||||
aria-label="Send message"
|
||||
icon={<ArrowUp />}
|
||||
aria-label={submitLabel}
|
||||
icon={submitIcon}
|
||||
disabled={!canSubmit}
|
||||
className={cn(
|
||||
'w-7 h-7 rounded-full p-0 text-center flex items-center justify-center',
|
||||
!canSubmit ? 'opacity-50' : 'opacity-100'
|
||||
)}
|
||||
tooltip={{ content: { side: 'top', text: 'Send message' } }}
|
||||
tooltip={{ content: { side: 'top', text: submitLabel } }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user