mirror of
https://github.com/supabase/supabase.git
synced 2026-09-07 02:20:52 +08:00
## Context Adds the inline AI completion functionality into Explorer QueryEditor, similar to what we've got for the existing SQL editor - Shifts the `ResizableAIWidget` and `InlineWidget` components out of the SQL Editor folder into `components/ui/AiEditor` to be used by both SQL Editor and Query Editor - Consolidates the "proposal" logic that was initially set up for the Clickhouse Migration functionality with this Inline AI stuff - Also added the prompt into the proposal header (Refer to the screenshots below) - SQL Editor didn't have this - but figured this is useful as context for the user <img width="935" height="352" alt="image" src="https://github.com/user-attachments/assets/3f71539a-dda1-4763-be08-a850bdc8aec6" /> Source selected: Database <img width="922" height="357" alt="image" src="https://github.com/user-attachments/assets/81e772e6-dcc9-440d-83db-49d0d487dd13" /> Source selected: Logs <img width="920" height="345" alt="image" src="https://github.com/user-attachments/assets/83f1dae3-cf62-4424-be42-b06e70cb366d" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added AI-assisted SQL generation with contextual prompts and OS-specific guidance. - Review generated SQL changes in a diff, then accept, reject, or cancel suggestions. - Added inline, resizable AI prompt controls with loading and submission states. - Added the Ctrl/Cmd+Shift+K shortcut to run AI SQL generation. - **Improvements** - Added options to disable query execution and run custom actions. - Renamed “Recent” to “Recently updated.” - Improved editor widget positioning and display behavior. - Added clearer error notifications when AI generation fails. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
128 lines
3.7 KiB
TypeScript
128 lines
3.7 KiB
TypeScript
import type { editor } from 'monaco-editor'
|
|
import { PropsWithChildren, useEffect, useMemo, useRef } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
|
|
export interface InlineWidgetProps {
|
|
/**
|
|
* The Monaco editor instance.
|
|
*/
|
|
editor: editor.IStandaloneCodeEditor | editor.IStandaloneDiffEditor
|
|
|
|
/**
|
|
* ID used by Monaco to reference this widget
|
|
*/
|
|
id: string
|
|
|
|
/**
|
|
* The line number after or before which this zone should appear.
|
|
* Use 0 to place a view zone before the first line number.
|
|
*/
|
|
afterLineNumber: number
|
|
beforeLineNumber?: number
|
|
|
|
/**
|
|
* The height in lines of the view zone.
|
|
* @default 1
|
|
*/
|
|
heightInLines?: number
|
|
}
|
|
|
|
/**
|
|
* Adds an inline widget to a Monaco editor instance. Acts as a
|
|
* container for custom widgets that are rendered inline.
|
|
*
|
|
* Implemented using the same techniques VS Code uses for their inline
|
|
* widgets (such as "Go to References"), but built for React.
|
|
* Uses a combination of a view zone and overlay widget.
|
|
*/
|
|
export const InlineWidget = ({
|
|
children,
|
|
editor,
|
|
id,
|
|
beforeLineNumber,
|
|
afterLineNumber = 0,
|
|
heightInLines = 1,
|
|
}: PropsWithChildren<InlineWidgetProps>) => {
|
|
const lineNumber = beforeLineNumber ?? afterLineNumber
|
|
const key = `${id}-${lineNumber.toString()}`
|
|
const containerElement = useMemo(() => document.createElement('div'), [])
|
|
const zoneIdRef = useRef<string>(null)
|
|
const viewZoneRef = useRef<{
|
|
top: number
|
|
height: number
|
|
heightInLines: number
|
|
}>({ top: 0, height: 0, heightInLines: heightInLines })
|
|
|
|
// Get the appropriate editor instance for diff editor
|
|
const targetEditor = 'getModifiedEditor' in editor ? editor.getModifiedEditor() : editor
|
|
|
|
const recalculateLayout = () => {
|
|
const layoutInfo = targetEditor.getLayoutInfo()
|
|
|
|
if (!layoutInfo) {
|
|
return
|
|
}
|
|
|
|
containerElement.style.left = `${layoutInfo.contentLeft}px`
|
|
containerElement.style.top = `${viewZoneRef.current.top}px`
|
|
containerElement.style.width = `${layoutInfo.width - layoutInfo.contentLeft - 20}px`
|
|
containerElement.style.height = `${viewZoneRef.current.height}px`
|
|
}
|
|
|
|
const createViewZone = () => {
|
|
targetEditor.changeViewZones((accessor) => {
|
|
// Remove existing zone if it exists
|
|
if (zoneIdRef.current) {
|
|
accessor.removeZone(zoneIdRef.current)
|
|
}
|
|
|
|
// Create new zone with current height
|
|
zoneIdRef.current = accessor.addZone({
|
|
afterLineNumber: beforeLineNumber ?? afterLineNumber,
|
|
heightInLines: viewZoneRef.current.heightInLines,
|
|
domNode: document.createElement('div'),
|
|
onDomNodeTop: (top) => {
|
|
viewZoneRef.current.top = top
|
|
recalculateLayout()
|
|
},
|
|
onComputedHeight: (height) => {
|
|
viewZoneRef.current.height = height
|
|
recalculateLayout()
|
|
},
|
|
})
|
|
})
|
|
}
|
|
|
|
// Initial setup of view zone and overlay widget
|
|
useEffect(() => {
|
|
const overlayWidget: editor.IOverlayWidget = {
|
|
getId: () => id,
|
|
getDomNode: () => containerElement,
|
|
getPosition: () => null,
|
|
}
|
|
|
|
createViewZone()
|
|
targetEditor.addOverlayWidget(overlayWidget)
|
|
|
|
// Remove the view zone & overlay widget on unmount
|
|
return () => {
|
|
targetEditor.changeViewZones((accessor) => {
|
|
if (zoneIdRef.current) {
|
|
accessor.removeZone(zoneIdRef.current)
|
|
}
|
|
targetEditor.removeOverlayWidget(overlayWidget)
|
|
})
|
|
}
|
|
}, [targetEditor, id, beforeLineNumber, afterLineNumber]) // Note: heightInLines removed from deps
|
|
|
|
// Update view zone height when heightInLines changes
|
|
useEffect(() => {
|
|
if (heightInLines !== viewZoneRef.current.heightInLines) {
|
|
viewZoneRef.current.heightInLines = heightInLines
|
|
createViewZone()
|
|
}
|
|
}, [heightInLines])
|
|
|
|
return createPortal(children, containerElement, key)
|
|
}
|