import { AlertTriangle, User } from 'lucide-react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useEffectOnce } from 'react-use' import { IS_PLATFORM } from 'common' import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query' import { useOrgOptedIntoAi } from 'hooks/misc/useOrgOptedIntoAi' import { useSchemasForAi } from 'hooks/misc/useSchemasForAi' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { formatSql } from 'lib/formatSql' import { useAppStateSnapshot } from 'state/app-state' import { AiIconAnimation, Button, CodeBlock, CommandItem_Shadcn_, CommandList_Shadcn_, Command_Shadcn_, Input_Shadcn_, Modal, StatusIcon, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_, Tabs_Shadcn_, cn, } from 'ui' import { BadgeExperimental, generateCommandClassNames, useHistoryKeys, useQuery, useSetQuery, } from 'ui-patterns/CommandMenu' import type { UseAiChatOptions } from 'ui-patterns/CommandMenu/prepackaged/ai' import { AiWarning, MessageRole, MessageStatus, useAiChat, } from 'ui-patterns/CommandMenu/prepackaged/ai' import type { AiMetadataSkipReason } from './SqlGenerator.Alerts' import { ExcludeSchemaAlert, IncludeSchemaAlert } from './SqlGenerator.Alerts' import { SAMPLE_QUERIES, generatePrompt } from './SqlGenerator.utils' import { SQLOutputActions } from './SqlOutputActions' function useSchemaMetadataForAi() { const isOptedInToAI = useOrgOptedIntoAi() const project = useSelectedProject() const [schemas] = useSchemasForAi(project?.ref!) const includeMetadata = (isOptedInToAI || !IS_PLATFORM) && schemas.length > 0 && !!project const metadataSkipReason: AiMetadataSkipReason = !project ? 'no_project' : 'forbidden' const { data } = useEntityDefinitionsQuery( { schemas: schemas, projectRef: project?.ref, connectionString: project?.connectionString, }, { enabled: includeMetadata } ) const api = useMemo( () => includeMetadata ? { includeMetadata, data, } : { includeMetadata, metadataSkipReason }, [includeMetadata, data, metadataSkipReason] ) return api } function useAiSqlGeneration() { const { showGenerateSqlModal } = useAppStateSnapshot() const schemaMetadata = useSchemaMetadataForAi() const definitions = useMemo( () => (schemaMetadata.data ?? []).map((def) => def.sql.trim()).join('\n\n'), [schemaMetadata] ) const query = useQuery() const setQuery = useSetQuery() const [isLoading, setIsLoading] = useState(false) const messageTemplate = useCallback>( (message) => generatePrompt(message, schemaMetadata.includeMetadata ? definitions : undefined), [schemaMetadata, definitions] ) const { submit, reset, messages, isResponding, hasError } = useAiChat({ messageTemplate, setIsLoading, }) useHistoryKeys({ enable: showGenerateSqlModal && !isResponding, stack: messages.filter(({ role }) => role === MessageRole.User).map(({ content }) => content), }) const handleSubmit = useCallback( (message: string) => { setQuery('') submit(message) }, [setQuery, submit] ) const handleReset = useCallback(() => { setQuery('') reset() }, [setQuery, reset]) useEffectOnce(() => { if (query) handleSubmit(query) }) return { query, setQuery, isLoading, isResponding, hasError, messages, handleSubmit, handleReset, usesMetadata: schemaMetadata.includeMetadata, metadataSkipReason: schemaMetadata.metadataSkipReason, } } export default function SqlGeneratorImpl() { const { showGenerateSqlModal, setShowGenerateSqlModal } = useAppStateSnapshot() const inputRef = useRef(null) const timeoutHandle = useRef>() useEffect(() => () => clearTimeout(timeoutHandle.current), []) const { query, setQuery, isLoading, isResponding, hasError, messages, handleSubmit, handleReset, usesMetadata, metadataSkipReason, } = useAiSqlGeneration() return ( Generate SQL } hideClose hideFooter visible={showGenerateSqlModal} onOpenAutoFocus={() => { timeoutHandle.current = setTimeout(() => inputRef.current?.focus()) }} onCancel={() => { if (messages.length > 0) { handleReset() timeoutHandle.current = setTimeout(() => inputRef.current?.focus()) } else { setShowGenerateSqlModal(false) } }} className="w-11/12 !max-w-3xl h-4/5 max-h-[800px] grid-rows-[min-content,_1fr] bg-overlay" >
{messages.length > 0 && } {messages.length === 0 && !hasError && ( )} {hasError && }
{usesMetadata ? ( ) : ( )} { if (!isLoading || !isResponding) { setQuery(e.target.value) } }} onKeyDown={(e) => { switch (e.key) { case 'Enter': if (!query || isLoading || isResponding) { return } return handleSubmit(query) default: return } }} />
) } function Messages({ messages, handleReset, }: { messages: ReturnType['messages'] handleReset: () => void }) { const X_PADDING = 'px-4' const UserAvatar = useCallback(() => { return (
) }, []) return messages.map((message, i) => { switch (message.role) { case MessageRole.User: return (
{message.content}
) case MessageRole.Assistant: const unformattedAnswer = message.content .replace(/```sql/g, '') .replace(/```.*/gs, '') .replace(/-- End of SQL query\.*/g, '') .trim() const answer = message.status === MessageStatus.Complete ? formatSql(unformattedAnswer) : unformattedAnswer const cantHelp = answer.replace(/^-- /, '') === "Sorry, I don't know how to help with that." return (
{message.status === MessageStatus.Pending ? (
) : cantHelp ? (

Sorry, I don't know how to help with that.

) : (
{answer} {message.status === MessageStatus.Complete && ( )}
)}
) } }) } function EmptyState({ query, handleSubmit, }: { query: string handleSubmit: (query: string) => void }) { const [activeTab, setActiveTab] = useState(SAMPLE_QUERIES[0].category) useEffect(() => { function handleSwitchTab(event: KeyboardEvent) { if (query || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')) { return } const activeSampleIndex = SAMPLE_QUERIES.findIndex( (samples) => samples.category === activeTab )! const nextIndex = event.key === 'ArrowRight' ? Math.min(activeSampleIndex + 1, SAMPLE_QUERIES.length - 1) : Math.max(0, activeSampleIndex - 1) setActiveTab(SAMPLE_QUERIES[nextIndex].category) } document.addEventListener('keydown', handleSwitchTab) return () => document.removeEventListener('keydown', handleSwitchTab) }, [query, activeTab]) return ( <>

Describe what you need and Supabase AI will try to generate the relevant SQL statements

Here are some example prompts to try out:


setActiveTab(value)} className="focus-visible:ring-0" > {SAMPLE_QUERIES.map((samples) => ( {samples.category} ))} {SAMPLE_QUERIES.map((samples) => ( {samples.queries.map((sampleQuery) => ( { if (!query) { handleSubmit(sampleQuery) } }} > {sampleQuery} ))} ))} ) } function ErrorMessage({ handleReset }: { handleReset: () => void }) { return (

Sorry, looks like Supabase AI is having a hard time!

Please try again in a bit.

) }