mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 09:59:03 +08:00
## Summary * Query blocks embedded inside an active assistant conversation (`AssistantQueryCell`) reused the same "Debug with Assistant" handler as standalone query blocks (Explorer Query tab, notebook cells), which always opens a brand-new chat and navigates away. * Clicking Debug on a block that's already part of the open conversation silently abandoned it for an unrelated new chat, which read as the button doing nothing. * Added an optional `onDebug` override threaded through `QueryEditor` → `QueryResultRenderer` → `QueryResultError`; `AssistantQueryCell` now uses it to write the debug prompt into the currently active chat's composer (`ai-assistant-state`'s new `setInitialInput`) instead of creating a new chat. Standalone query blocks keep the existing "open a new chat" behavior since no `onDebug` override is passed there. * `ExplorerChatTab` now wires `composerContext` into `AssistantChat` (it wasn't before), so the pre-filled prompt actually reaches the visible textarea on the Explorer chat route. Fixes [FE-4319](https://linear.app/supabase/issue/FE-4319/debug-with-ai-assistant-does-seemingly-nothing-if-query-is-already). ## Test plan - [X] `pnpm vitest run` on `QueryResultError.test.tsx` / `QueryResultError.selfhosted.test.tsx` / `ExplorerChatTab.test.tsx` / `AssistantQueryCell.utils.test.ts` — all pass, including new test asserting `onDebug` is called instead of `createChat`. - [X] `pnpm exec eslint` on touched files — clean (only pre-existing unrelated warnings). - [X] Manual check: run a query inside an assistant chat that errors, click "Debug with Assistant" on that block, confirm the debug prompt appears in the current chat's composer rather than opening a new chat. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a “Debug with Assistant” workflow that sends SQL error details to the AI Assistant as its initial input. * Preserved the existing behavior of opening a new debug chat when the Assistant panel is unavailable. * **Tests** * Added coverage confirming that debugging invokes the Assistant callback without creating an additional chat. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
190 lines
5.8 KiB
TypeScript
190 lines
5.8 KiB
TypeScript
import { useRef, useState } from 'react'
|
|
|
|
import { identifyQueryType } from './AIAssistant.utils'
|
|
import {
|
|
changeAssistantQuerySource,
|
|
createAssistantQueryModel,
|
|
DEFAULT_ASSISTANT_LOGS_QUERY_TITLE,
|
|
DEFAULT_ASSISTANT_QUERY_TITLE,
|
|
getAssistantQueryDisplay,
|
|
setAssistantQuerySql,
|
|
shouldClearAssistantQueryResult,
|
|
} from './AssistantQueryCell.utils'
|
|
import { Confirm } from './Confirm'
|
|
import { type ConfirmFooterApprovalState } from './Confirm.utils'
|
|
import { QueryEditor } from '@/components/interfaces/Explorer/QueryEditor'
|
|
import { type QueryDisplay, type QueryResult } from '@/components/interfaces/Explorer/types'
|
|
import {
|
|
type QuerySourceBinding,
|
|
type QuerySourceTag,
|
|
} from '@/data/query-sources/query-source-registry'
|
|
import { useTrack } from '@/lib/telemetry/track'
|
|
import { useAiAssistantState } from '@/state/ai-assistant-state'
|
|
import { useLocalRoleImpersonationState } from '@/state/role-impersonation-state'
|
|
|
|
interface AssistantQueryCellProps {
|
|
id: string
|
|
sql: string
|
|
title?: string
|
|
initialResult?: QueryResult
|
|
source?: QuerySourceBinding
|
|
view?: 'table' | 'chart'
|
|
xAxis?: string
|
|
yAxis?: string
|
|
/** Follow incoming SQL while the assistant is still streaming the query text. */
|
|
isStreaming?: boolean
|
|
confirmState?: ConfirmFooterApprovalState
|
|
onApprove?: () => void
|
|
onDeny?: () => void
|
|
}
|
|
|
|
const DEFAULT_SOURCE: QuerySourceBinding = { _tag: 'database' }
|
|
|
|
const QUERY_OUTCOME_MESSAGES: Record<
|
|
QuerySourceTag,
|
|
{ success: string; error: string; denied: string }
|
|
> = {
|
|
database: {
|
|
success: 'Query executed',
|
|
error: 'Failed to execute SQL',
|
|
denied: 'Skipped query',
|
|
},
|
|
logs: {
|
|
success: 'Query executed',
|
|
error: 'Failed to query logs',
|
|
denied: 'Skipped query',
|
|
},
|
|
}
|
|
|
|
/** Assistant adapter around the shared QueryEditor. Local state only — nothing is persisted. */
|
|
export const AssistantQueryCell = ({
|
|
id,
|
|
sql: initialSql,
|
|
title: initialTitle,
|
|
initialResult,
|
|
source = DEFAULT_SOURCE,
|
|
view,
|
|
xAxis,
|
|
yAxis,
|
|
isStreaming = false,
|
|
confirmState,
|
|
onApprove,
|
|
onDeny,
|
|
}: AssistantQueryCellProps) => {
|
|
const track = useTrack()
|
|
const roleImpersonationState = useLocalRoleImpersonationState()
|
|
const aiAssistantState = useAiAssistantState()
|
|
|
|
const fallbackTitle =
|
|
initialTitle?.trim() ||
|
|
(source._tag === 'logs' ? DEFAULT_ASSISTANT_LOGS_QUERY_TITLE : DEFAULT_ASSISTANT_QUERY_TITLE)
|
|
|
|
const hasExplicitAxes = Boolean(xAxis || yAxis)
|
|
|
|
const [title, setTitle] = useState(fallbackTitle)
|
|
const [query, setQuery] = useState(() => createAssistantQueryModel(initialSql, source))
|
|
const [showQuery, setShowQuery] = useState(true)
|
|
// undefined uses the tool output; null intentionally clears it after changing source.
|
|
const [resultOverride, setResultOverride] = useState<QueryResult | null>()
|
|
const [localDisplay, setLocalDisplay] = useState<QueryDisplay | undefined>(undefined)
|
|
const previousId = useRef(id)
|
|
|
|
if (previousId.current !== id) {
|
|
previousId.current = id
|
|
setTitle(fallbackTitle)
|
|
setQuery(createAssistantQueryModel(initialSql, source))
|
|
setShowQuery(true)
|
|
setResultOverride(undefined)
|
|
setLocalDisplay(undefined)
|
|
}
|
|
|
|
if (isStreaming && query.uncheckedSql !== initialSql) {
|
|
setQuery((current) => setAssistantQuerySql(current, initialSql))
|
|
}
|
|
|
|
const result = resultOverride === undefined ? initialResult : (resultOverride ?? undefined)
|
|
const display =
|
|
localDisplay ??
|
|
getAssistantQueryDisplay({
|
|
view,
|
|
xAxis,
|
|
yAxis,
|
|
sql: query.uncheckedSql,
|
|
rows: result?.rows,
|
|
})
|
|
|
|
const handleTitleChange = (value: string) => {
|
|
const nextTitle = value.trim()
|
|
if (!nextTitle) return
|
|
setTitle(nextTitle)
|
|
}
|
|
|
|
const handleSourceChange = (nextSource: QuerySourceBinding) => {
|
|
const isBackendChange = nextSource._tag !== query._tag
|
|
if (shouldClearAssistantQueryResult(query, nextSource)) setResultOverride(null)
|
|
if (isBackendChange && !hasExplicitAxes) setLocalDisplay(undefined)
|
|
setQuery((current) => changeAssistantQuerySource(current, nextSource))
|
|
}
|
|
|
|
const handleDisplayChange = (nextDisplay: QueryDisplay) => {
|
|
setLocalDisplay(nextDisplay)
|
|
}
|
|
|
|
const handleResultChange = (nextResult: QueryResult) => {
|
|
setResultOverride(nextResult)
|
|
}
|
|
|
|
const handleRun = () => {
|
|
const sql = query.uncheckedSql
|
|
const mutationType = identifyQueryType(sql)
|
|
track('assistant_suggestion_run_query_clicked', {
|
|
queryType: mutationType ? 'mutation' : 'select',
|
|
...(mutationType ? { mutationType } : {}),
|
|
})
|
|
}
|
|
|
|
const isConfirming = confirmState !== undefined
|
|
const outcomeMessages = QUERY_OUTCOME_MESSAGES[source._tag]
|
|
|
|
return (
|
|
<Confirm
|
|
fill
|
|
className="h-96 w-full max-w-6xl mx-auto"
|
|
state={confirmState}
|
|
message="Assistant wants to run this query"
|
|
cancelLabel="Skip"
|
|
confirmLabel="Run query"
|
|
confirmLabelLoading="Running..."
|
|
successMessage={outcomeMessages.success}
|
|
errorMessage={outcomeMessages.error}
|
|
deniedMessage={outcomeMessages.denied}
|
|
onCancel={onDeny}
|
|
onConfirm={onApprove}
|
|
>
|
|
<QueryEditor
|
|
isReadOnly
|
|
id={id}
|
|
variant="viewport"
|
|
title={title}
|
|
query={query}
|
|
result={result}
|
|
showQuery={showQuery}
|
|
onShowQueryChange={setShowQuery}
|
|
roleImpersonationState={roleImpersonationState}
|
|
display={display}
|
|
isRunDisabled={isConfirming}
|
|
onTitleChange={handleTitleChange}
|
|
onSqlChange={(sql) => setQuery((current) => setAssistantQuerySql(current, sql))}
|
|
onSourceChange={handleSourceChange}
|
|
onResultChange={handleResultChange}
|
|
onRowLimitChange={(rowLimit) =>
|
|
setQuery((current) => (current._tag === 'database' ? { ...current, rowLimit } : current))
|
|
}
|
|
onDisplayChange={handleDisplayChange}
|
|
onRun={handleRun}
|
|
onDebug={aiAssistantState.setInitialInput}
|
|
/>
|
|
</Confirm>
|
|
)
|
|
}
|