DisplayBlockRenderer.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import { acceptUntrustedSql, type UntrustedSqlFragment } from '@supabase/pg-meta'
  2. import { PermissionAction } from '@supabase/shared-types/out/constants'
  3. import { useQueryClient } from '@tanstack/react-query'
  4. import type { ToolUIPart } from 'ai'
  5. import { useParams } from 'common'
  6. import { useRouter } from 'next/router'
  7. import { useRef, useState, type DragEvent, type PropsWithChildren } from 'react'
  8. import { DEFAULT_CHART_CONFIG, QueryBlock } from '../QueryBlock/QueryBlock'
  9. import { identifyQueryType } from './AIAssistant.utils'
  10. import { ConfirmFooter } from './ConfirmFooter'
  11. import { ChartConfig } from '@/components/interfaces/SQLEditor/UtilityPanel/ChartConfig'
  12. import { entityTypeKeys } from '@/data/entity-types/keys'
  13. import { lintKeys } from '@/data/lint/keys'
  14. import { usePrimaryDatabase } from '@/data/read-replicas/replicas-query'
  15. import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
  16. import { useChangedSync } from '@/hooks/misc/useChanged'
  17. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  18. import { useProfile } from '@/lib/profile'
  19. import { useTrack } from '@/lib/telemetry/track'
  20. interface DisplayBlockRendererProps {
  21. messageId: string
  22. toolCallId: string
  23. initialArgs: {
  24. sql: UntrustedSqlFragment
  25. label?: string
  26. isWriteQuery?: boolean
  27. view?: 'table' | 'chart'
  28. xAxis?: string
  29. yAxis?: string
  30. }
  31. initialResults?: unknown
  32. /** Called when locally running SQL fails before or during client-side execution. */
  33. onError?: (args: { messageId: string; errorText: string }) => void
  34. /** Responds affirmatively to an AI SDK tool approval request; does not run SQL directly. */
  35. onApprove?: () => void
  36. /** Responds negatively to an AI SDK tool approval request; does not run SQL directly. */
  37. onDeny?: () => void
  38. /** AI SDK tool state used to show approval UI for pending tool calls. */
  39. toolState?: ToolUIPart['state']
  40. toolApprovalRespondedApproved?: boolean
  41. isLastPart?: boolean
  42. isLastMessage?: boolean
  43. showConfirmFooter?: boolean
  44. onChartConfigChange?: (chartConfig: ChartConfig) => void
  45. /** Called when the user clicks the query block play button to run SQL locally. */
  46. onQueryRun?: (queryType: 'select' | 'mutation') => void
  47. }
  48. export const DisplayBlockRenderer = ({
  49. messageId,
  50. toolCallId,
  51. initialArgs,
  52. initialResults,
  53. onError,
  54. onApprove,
  55. onDeny,
  56. toolState,
  57. toolApprovalRespondedApproved,
  58. isLastPart = false,
  59. isLastMessage = false,
  60. showConfirmFooter = true,
  61. onChartConfigChange,
  62. onQueryRun,
  63. }: PropsWithChildren<DisplayBlockRendererProps>) => {
  64. const queryClient = useQueryClient()
  65. const savedInitialArgs = useRef(initialArgs)
  66. const savedInitialResults = useRef(initialResults)
  67. const savedInitialConfig = useRef<ChartConfig>({
  68. ...DEFAULT_CHART_CONFIG,
  69. view: initialArgs.view === 'chart' ? 'chart' : 'table',
  70. xKey: initialArgs.xAxis ?? '',
  71. yKey: initialArgs.yAxis ?? '',
  72. })
  73. const router = useRouter()
  74. const { ref } = useParams()
  75. const { profile } = useProfile()
  76. const track = useTrack()
  77. const { can: canCreateSQLSnippet } = useAsyncCheckPermissions(
  78. PermissionAction.CREATE,
  79. 'user_content',
  80. {
  81. resource: { type: 'sql', owner_id: profile?.id },
  82. subject: { id: profile?.id },
  83. }
  84. )
  85. const [chartConfig, setChartConfig] = useState<ChartConfig>(() => ({
  86. ...DEFAULT_CHART_CONFIG,
  87. view: initialArgs.view === 'chart' ? 'chart' : 'table',
  88. xKey: initialArgs.xAxis ?? '',
  89. yKey: initialArgs.yAxis ?? '',
  90. }))
  91. const [rows, setRows] = useState<any[] | undefined>(
  92. Array.isArray(initialResults) ? initialResults : undefined
  93. )
  94. const isReportsPage = router.pathname.endsWith('/reports/[id]')
  95. const isHomePage = router.pathname === '/project/[ref]'
  96. const isDraggableToReports = canCreateSQLSnippet && (isReportsPage || isHomePage)
  97. const label = initialArgs.label || 'SQL Results'
  98. const [isWriteQuery, setIsWriteQuery] = useState<boolean>(initialArgs.isWriteQuery || false)
  99. const sqlQuery = initialArgs.sql
  100. const { database: primaryDatabase } = usePrimaryDatabase({ projectRef: ref })
  101. const readOnlyConnectionString = primaryDatabase?.connection_string_read_only
  102. const postgresConnectionString = primaryDatabase?.connectionString
  103. const {
  104. mutate: executeSql,
  105. error: executeSqlError,
  106. isPending: executeSqlLoading,
  107. } = useExecuteSqlMutation({
  108. onError: () => {
  109. // Suppress toast because error message is displayed inline
  110. },
  111. })
  112. const toolCallIdChanged = useChangedSync(toolCallId)
  113. if (toolCallIdChanged) {
  114. setChartConfig(savedInitialConfig.current)
  115. onChartConfigChange?.(savedInitialConfig.current)
  116. setIsWriteQuery(savedInitialArgs.current.isWriteQuery || false)
  117. setRows(Array.isArray(savedInitialResults.current) ? savedInitialResults.current : undefined)
  118. }
  119. const initialResultsChanged = useChangedSync(initialResults)
  120. if (initialResultsChanged) {
  121. const normalized = Array.isArray(initialResults) ? initialResults : undefined
  122. if (!normalized || normalized === rows) return
  123. setRows(normalized)
  124. }
  125. const handleRunQuery = (queryType: 'select' | 'mutation') => {
  126. if (!sqlQuery) return
  127. onQueryRun?.(queryType)
  128. track('assistant_suggestion_run_query_clicked', {
  129. queryType,
  130. ...(queryType === 'mutation'
  131. ? { mutationType: identifyQueryType(sqlQuery) ?? 'unknown' }
  132. : {}),
  133. })
  134. }
  135. const runQuery = (queryType: 'select' | 'mutation') => {
  136. if (!ref || !sqlQuery) return
  137. const connectionString =
  138. queryType === 'mutation'
  139. ? postgresConnectionString
  140. : (readOnlyConnectionString ?? postgresConnectionString)
  141. if (!connectionString) {
  142. const fallbackMessage = 'Unable to find a database connection to execute this query.'
  143. onError?.({ messageId, errorText: fallbackMessage })
  144. return
  145. }
  146. if (queryType === 'mutation') {
  147. setIsWriteQuery(true)
  148. }
  149. executeSql(
  150. { projectRef: ref, connectionString, sql: acceptUntrustedSql(sqlQuery) },
  151. {
  152. onSuccess: (data) => {
  153. setRows(Array.isArray(data.result) ? data.result : undefined)
  154. setIsWriteQuery(queryType === 'mutation' || initialArgs.isWriteQuery || false)
  155. if (queryType === 'mutation') {
  156. queryClient.invalidateQueries({ queryKey: lintKeys.lint(ref) })
  157. queryClient.invalidateQueries({ queryKey: entityTypeKeys.list(ref) })
  158. }
  159. },
  160. onError: (error) => {
  161. const lowerMessage = error.message.toLowerCase()
  162. const isReadOnlyError =
  163. lowerMessage.includes('read-only transaction') ||
  164. lowerMessage.includes('permission denied') ||
  165. lowerMessage.includes('must be owner')
  166. if (queryType === 'select' && isReadOnlyError) {
  167. setIsWriteQuery(true)
  168. }
  169. onError?.({ messageId, errorText: error.message })
  170. },
  171. }
  172. )
  173. }
  174. const handleExecute = (queryType: 'select' | 'mutation') => {
  175. handleRunQuery(queryType)
  176. runQuery(queryType)
  177. }
  178. const handleUpdateChartConfig = ({
  179. chartConfig: updatedValues,
  180. }: {
  181. chartConfig: Partial<ChartConfig>
  182. }) => {
  183. setChartConfig((prev) => {
  184. const next = { ...prev, ...updatedValues }
  185. onChartConfigChange?.(next)
  186. return next
  187. })
  188. }
  189. const handleDragStart = (e: DragEvent<Element>) => {
  190. e.dataTransfer.setData(
  191. 'application/json',
  192. JSON.stringify({ label, sql: sqlQuery, config: chartConfig })
  193. )
  194. }
  195. const isApprovalRequested = toolState === 'approval-requested'
  196. const isApprovalResponded = toolState === 'approval-responded'
  197. const isApprovalDenied = isApprovalResponded && toolApprovalRespondedApproved === false
  198. const shouldShowConfirmFooter =
  199. showConfirmFooter &&
  200. (isApprovalRequested || (isApprovalResponded && !isApprovalDenied)) &&
  201. isLastPart &&
  202. isLastMessage &&
  203. (isApprovalResponded || (!!onApprove && !!onDeny))
  204. const isRunningApprovedTool = (isApprovalResponded && !isApprovalDenied) || executeSqlLoading
  205. return (
  206. <div className="display-block w-auto overflow-x-hidden">
  207. <div className="relative z-10">
  208. <QueryBlock
  209. label={label}
  210. isWriteQuery={isWriteQuery}
  211. sql={sqlQuery}
  212. results={rows}
  213. errorText={executeSqlError?.message}
  214. chartConfig={chartConfig}
  215. onExecute={handleExecute}
  216. onUpdateChartConfig={handleUpdateChartConfig}
  217. draggable={isDraggableToReports}
  218. onDragStart={handleDragStart}
  219. disabled={shouldShowConfirmFooter}
  220. isExecuting={isRunningApprovedTool}
  221. />
  222. </div>
  223. {shouldShowConfirmFooter && (
  224. <div className="mx-4">
  225. <ConfirmFooter
  226. message="Assistant wants to run this query"
  227. cancelLabel="Skip"
  228. confirmLabel="Run Query"
  229. confirmLabelLoading="Running..."
  230. isLoading={isApprovalResponded || executeSqlLoading}
  231. onCancel={isApprovalRequested ? onDeny : undefined}
  232. onConfirm={isApprovalRequested ? onApprove : undefined}
  233. />
  234. </div>
  235. )}
  236. </div>
  237. )
  238. }