| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- 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<DisplayBlockRendererProps>) => {
- const queryClient = useQueryClient()
- const savedInitialArgs = useRef(initialArgs)
- const savedInitialResults = useRef(initialResults)
- const savedInitialConfig = useRef<ChartConfig>({
- ...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<ChartConfig>(() => ({
- ...DEFAULT_CHART_CONFIG,
- view: initialArgs.view === 'chart' ? 'chart' : 'table',
- xKey: initialArgs.xAxis ?? '',
- yKey: initialArgs.yAxis ?? '',
- }))
- const [rows, setRows] = useState<any[] | undefined>(
- 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<boolean>(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<ChartConfig>
- }) => {
- setChartConfig((prev) => {
- const next = { ...prev, ...updatedValues }
- onChartConfigChange?.(next)
- return next
- })
- }
- const handleDragStart = (e: DragEvent<Element>) => {
- 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 (
- <div className="display-block w-auto overflow-x-hidden">
- <div className="relative z-10">
- <QueryBlock
- label={label}
- isWriteQuery={isWriteQuery}
- sql={sqlQuery}
- results={rows}
- errorText={executeSqlError?.message}
- chartConfig={chartConfig}
- onExecute={handleExecute}
- onUpdateChartConfig={handleUpdateChartConfig}
- draggable={isDraggableToReports}
- onDragStart={handleDragStart}
- disabled={shouldShowConfirmFooter}
- isExecuting={isRunningApprovedTool}
- />
- </div>
- {shouldShowConfirmFooter && (
- <div className="mx-4">
- <ConfirmFooter
- message="Assistant wants to run this query"
- cancelLabel="Skip"
- confirmLabel="Run Query"
- confirmLabelLoading="Running..."
- isLoading={isApprovalResponded || executeSqlLoading}
- onCancel={isApprovalRequested ? onDeny : undefined}
- onConfirm={isApprovalRequested ? onApprove : undefined}
- />
- </div>
- )}
- </div>
- )
- }
|