import { acceptUntrustedSql, type UntrustedSqlFragment } from '@supabase/pg-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' import type { ToolUIPart } from 'ai' import { useParams } from 'common' import { useRouter } from 'next/router' import { useRef, useState, type DragEvent, type PropsWithChildren } from 'react' import { DEFAULT_CHART_CONFIG, QueryBlock } from '../QueryBlock/QueryBlock' import { identifyQueryType } from './AIAssistant.utils' import { ConfirmFooter } from './ConfirmFooter' import { ChartConfig } from '@/components/interfaces/SQLEditor/UtilityPanel/ChartConfig' import { entityTypeKeys } from '@/data/entity-types/keys' import { lintKeys } from '@/data/lint/keys' import { usePrimaryDatabase } from '@/data/read-replicas/replicas-query' import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation' import { useChangedSync } from '@/hooks/misc/useChanged' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useProfile } from '@/lib/profile' import { useTrack } from '@/lib/telemetry/track' interface DisplayBlockRendererProps { messageId: string toolCallId: string initialArgs: { sql: UntrustedSqlFragment label?: string isWriteQuery?: boolean view?: 'table' | 'chart' xAxis?: string yAxis?: string } initialResults?: unknown /** Called when locally running SQL fails before or during client-side execution. */ onError?: (args: { messageId: string; errorText: string }) => void /** Responds affirmatively to an AI SDK tool approval request; does not run SQL directly. */ onApprove?: () => void /** Responds negatively to an AI SDK tool approval request; does not run SQL directly. */ onDeny?: () => void /** AI SDK tool state used to show approval UI for pending tool calls. */ toolState?: ToolUIPart['state'] toolApprovalRespondedApproved?: boolean isLastPart?: boolean isLastMessage?: boolean showConfirmFooter?: boolean onChartConfigChange?: (chartConfig: ChartConfig) => void /** Called when the user clicks the query block play button to run SQL locally. */ onQueryRun?: (queryType: 'select' | 'mutation') => void } export const DisplayBlockRenderer = ({ messageId, toolCallId, initialArgs, initialResults, onError, onApprove, onDeny, toolState, toolApprovalRespondedApproved, isLastPart = false, isLastMessage = false, showConfirmFooter = true, onChartConfigChange, onQueryRun, }: PropsWithChildren) => { const queryClient = useQueryClient() const savedInitialArgs = useRef(initialArgs) const savedInitialResults = useRef(initialResults) const savedInitialConfig = useRef({ ...DEFAULT_CHART_CONFIG, view: initialArgs.view === 'chart' ? 'chart' : 'table', xKey: initialArgs.xAxis ?? '', yKey: initialArgs.yAxis ?? '', }) const router = useRouter() const { ref } = useParams() const { profile } = useProfile() const track = useTrack() const { can: canCreateSQLSnippet } = useAsyncCheckPermissions( PermissionAction.CREATE, 'user_content', { resource: { type: 'sql', owner_id: profile?.id }, subject: { id: profile?.id }, } ) const [chartConfig, setChartConfig] = useState(() => ({ ...DEFAULT_CHART_CONFIG, view: initialArgs.view === 'chart' ? 'chart' : 'table', xKey: initialArgs.xAxis ?? '', yKey: initialArgs.yAxis ?? '', })) const [rows, setRows] = useState( Array.isArray(initialResults) ? initialResults : undefined ) const isReportsPage = router.pathname.endsWith('/reports/[id]') const isHomePage = router.pathname === '/project/[ref]' const isDraggableToReports = canCreateSQLSnippet && (isReportsPage || isHomePage) const label = initialArgs.label || 'SQL Results' const [isWriteQuery, setIsWriteQuery] = useState(initialArgs.isWriteQuery || false) const sqlQuery = initialArgs.sql const { database: primaryDatabase } = usePrimaryDatabase({ projectRef: ref }) const readOnlyConnectionString = primaryDatabase?.connection_string_read_only const postgresConnectionString = primaryDatabase?.connectionString const { mutate: executeSql, error: executeSqlError, isPending: executeSqlLoading, } = useExecuteSqlMutation({ onError: () => { // Suppress toast because error message is displayed inline }, }) const toolCallIdChanged = useChangedSync(toolCallId) if (toolCallIdChanged) { setChartConfig(savedInitialConfig.current) onChartConfigChange?.(savedInitialConfig.current) setIsWriteQuery(savedInitialArgs.current.isWriteQuery || false) setRows(Array.isArray(savedInitialResults.current) ? savedInitialResults.current : undefined) } const initialResultsChanged = useChangedSync(initialResults) if (initialResultsChanged) { const normalized = Array.isArray(initialResults) ? initialResults : undefined if (!normalized || normalized === rows) return setRows(normalized) } const handleRunQuery = (queryType: 'select' | 'mutation') => { if (!sqlQuery) return onQueryRun?.(queryType) track('assistant_suggestion_run_query_clicked', { queryType, ...(queryType === 'mutation' ? { mutationType: identifyQueryType(sqlQuery) ?? 'unknown' } : {}), }) } const runQuery = (queryType: 'select' | 'mutation') => { if (!ref || !sqlQuery) return const connectionString = queryType === 'mutation' ? postgresConnectionString : (readOnlyConnectionString ?? postgresConnectionString) if (!connectionString) { const fallbackMessage = 'Unable to find a database connection to execute this query.' onError?.({ messageId, errorText: fallbackMessage }) return } if (queryType === 'mutation') { setIsWriteQuery(true) } executeSql( { projectRef: ref, connectionString, sql: acceptUntrustedSql(sqlQuery) }, { onSuccess: (data) => { setRows(Array.isArray(data.result) ? data.result : undefined) setIsWriteQuery(queryType === 'mutation' || initialArgs.isWriteQuery || false) if (queryType === 'mutation') { queryClient.invalidateQueries({ queryKey: lintKeys.lint(ref) }) queryClient.invalidateQueries({ queryKey: entityTypeKeys.list(ref) }) } }, onError: (error) => { const lowerMessage = error.message.toLowerCase() const isReadOnlyError = lowerMessage.includes('read-only transaction') || lowerMessage.includes('permission denied') || lowerMessage.includes('must be owner') if (queryType === 'select' && isReadOnlyError) { setIsWriteQuery(true) } onError?.({ messageId, errorText: error.message }) }, } ) } const handleExecute = (queryType: 'select' | 'mutation') => { handleRunQuery(queryType) runQuery(queryType) } const handleUpdateChartConfig = ({ chartConfig: updatedValues, }: { chartConfig: Partial }) => { setChartConfig((prev) => { const next = { ...prev, ...updatedValues } onChartConfigChange?.(next) return next }) } const handleDragStart = (e: DragEvent) => { e.dataTransfer.setData( 'application/json', JSON.stringify({ label, sql: sqlQuery, config: chartConfig }) ) } const isApprovalRequested = toolState === 'approval-requested' const isApprovalResponded = toolState === 'approval-responded' const isApprovalDenied = isApprovalResponded && toolApprovalRespondedApproved === false const shouldShowConfirmFooter = showConfirmFooter && (isApprovalRequested || (isApprovalResponded && !isApprovalDenied)) && isLastPart && isLastMessage && (isApprovalResponded || (!!onApprove && !!onDeny)) const isRunningApprovedTool = (isApprovalResponded && !isApprovalDenied) || executeSqlLoading return (
{shouldShowConfirmFooter && (
)}
) }