Files
supabase/apps/studio/components/ui/EditorPanel/EditorPanel.tsx
Saxon Fletcher d60aceb562 Prompt and tool refactoring (#37500)
* try a really long context window to maximize caching

* update examples

* attempt to update packages and useChat

* update endpoints

* update zod

* zod

* update to v5

* message update

* Revert "zod"

This reverts commit ec39bac6b6.

* revert zod

* zod i

* fix complete endpoints

* remove async

* change to content

* type cleanup

* Revert the package bumps to rebuild them.

* Bump zod to 2.25.76 in all packages.

* Bump openai in all packages.

* Bump ai and ai-related packages.

* Remove unneeded files.

* Fix the rest of the migration stuff.

* Prettier fixes.

* add policy list tool

* refactor

* ai sdk 5 fixes

* refactor complete endpoint

* edge function prompt

* remove example

* slight prompt change

* Minor clean up

* More clean up

---------

Co-authored-by: Jordi Enric <jordi.err@gmail.com>
Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2025-08-08 15:25:57 +07:00

455 lines
16 KiB
TypeScript

import { Book, Save, X } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { useParams } from 'common'
import {
createSqlSnippetSkeletonV2,
suffixWithLimit,
} from 'components/interfaces/SQLEditor/SQLEditor.utils'
import Results from 'components/interfaces/SQLEditor/UtilityPanel/Results'
import { SqlRunButton } from 'components/interfaces/SQLEditor/UtilityPanel/RunButton'
import { useSqlTitleGenerateMutation } from 'data/ai/sql-title-mutation'
import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { BASE_PATH } from 'lib/constants'
import { uuidv4 } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import {
Button,
cn,
CodeBlock,
Command_Shadcn_,
CommandEmpty_Shadcn_,
CommandGroup_Shadcn_,
CommandInput_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
Form_Shadcn_,
FormField_Shadcn_,
HoverCard_Shadcn_,
HoverCardContent_Shadcn_,
HoverCardTrigger_Shadcn_,
Input_Shadcn_ as Input,
KeyboardShortcut,
Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SQL_ICON,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { containsUnknownFunction, isReadOnlySelect } from '../AIAssistantPanel/AIAssistant.utils'
import AIEditor from '../AIEditor'
import { ButtonTooltip } from '../ButtonTooltip'
import { InlineLink } from '../InlineLink'
import SqlWarningAdmonition from '../SqlWarningAdmonition'
type Template = {
name: string
description: string
content: string
}
interface EditorPanelProps {
open: boolean
onClose: () => void
initialValue?: string
label?: string
saveLabel?: string
saveValue?: string
onSave?: (value: string, saveValue: string) => void
onRunSuccess?: (value: any[]) => void
onRunError?: (value: any) => void
functionName?: string
templates?: Template[]
initialPrompt?: string
onChange?: (value: string) => void
}
export const EditorPanel = ({
open,
onClose,
initialValue = '',
label = '',
saveLabel = 'Save',
saveValue = '',
onSave,
onRunSuccess,
onRunError,
templates = [],
initialPrompt = '',
onChange,
}: EditorPanelProps) => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const { profile } = useProfile()
const snapV2 = useSqlEditorV2StateSnapshot()
const { mutateAsync: generateSqlTitle } = useSqlTitleGenerateMutation()
const { data: org } = useSelectedOrganizationQuery()
const [isSaving, setIsSaving] = useState(false)
const [error, setError] = useState<QueryResponseError>()
const [results, setResults] = useState<undefined | any[]>(undefined)
const [showWarning, setShowWarning] = useState<'hasWriteOperation' | 'hasUnknownFunctions'>()
const [currentValue, setCurrentValue] = useState(initialValue)
const [showResults, setShowResults] = useState(true)
const [isTemplatesOpen, setIsTemplatesOpen] = useState(false)
const saveForm = useForm({
defaultValues: {
saveValue: saveValue || '',
},
})
const errorHeader = error?.formattedError?.split('\n')?.filter((x: string) => x.length > 0)?.[0]
const errorContent =
'formattedError' in (error || {})
? error?.formattedError
?.split('\n')
?.filter((x: string) => x.length > 0)
?.slice(1) ?? []
: [error?.message ?? '']
const { mutate: executeSql, isLoading: isExecuting } = useExecuteSqlMutation({
onSuccess: async (res) => {
setResults(res.result)
if (onRunSuccess) {
onRunSuccess(res.result)
}
},
onError: (error) => {
setError(error)
setResults([])
if (onRunError) {
onRunError(error)
}
},
})
const onExecuteSql = (skipValidation = false) => {
setError(undefined)
setShowWarning(undefined)
if (currentValue.length === 0) return
if (!skipValidation) {
const isReadOnlySelectSQL = isReadOnlySelect(currentValue)
if (!isReadOnlySelectSQL) {
const hasUnknownFunctions = containsUnknownFunction(currentValue)
setShowWarning(hasUnknownFunctions ? 'hasUnknownFunctions' : 'hasWriteOperation')
return
}
}
executeSql({
sql: suffixWithLimit(currentValue, 100),
projectRef: project?.ref,
connectionString: project?.connectionString,
handleError: (error) => {
throw error
},
contextualInvalidation: true,
})
}
const handleChange = (value: string) => {
setCurrentValue(value)
onChange?.(value)
}
const onSelectTemplate = (content: string) => {
handleChange(content)
setIsTemplatesOpen(false)
}
useEffect(() => {
if (initialValue !== undefined && initialValue !== currentValue) {
setCurrentValue(initialValue)
setResults(undefined)
setError(undefined)
setShowWarning(undefined)
}
}, [initialValue])
useEffect(() => {
saveForm.reset({
saveValue: saveValue || '',
})
}, [saveValue, saveForm])
return (
<Sheet open={open} onOpenChange={(open) => !open && onClose()}>
<SheetContent
showClose={false}
className="w-full sm:max-w-3xl flex flex-col h-full p-0 space-y-0 gap-0"
>
<SheetHeader className="flex shrink-0 items-center gap-x-3">
<div className="flex-1">
<SheetTitle className="text-sm">SQL Editor</SheetTitle>
{label && <SheetDescription className="text-sm">{label}</SheetDescription>}
</div>
<div className="flex gap-2 items-center">
{templates.length > 0 && (
<Popover_Shadcn_ open={isTemplatesOpen} onOpenChange={setIsTemplatesOpen}>
<PopoverTrigger_Shadcn_ asChild>
<Button
size="tiny"
type="default"
role="combobox"
aria-expanded={isTemplatesOpen}
icon={<Book size={14} />}
>
Templates
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ align="end" className="w-[300px] p-0">
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Search templates..." />
<CommandList_Shadcn_>
<CommandEmpty_Shadcn_>No templates found.</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
{templates.map((template) => (
<HoverCard_Shadcn_ key={template.name}>
<HoverCardTrigger_Shadcn_ asChild>
<CommandItem_Shadcn_
value={template.name}
onSelect={() => onSelectTemplate(template.content)}
className="cursor-pointer"
>
<div className="flex items-center gap-3">
<SQL_ICON
size={16}
className={cn(
'w-5 h-5 flex-0 mr-2 transition-colors fill-foreground-muted'
)}
/>
<div className="flex-1">
<h4 className="text-foreground flex-1">{template.name}</h4>
<p className="text-xs text-foreground-light">
{template.description}
</p>
</div>
</div>
</CommandItem_Shadcn_>
</HoverCardTrigger_Shadcn_>
<HoverCardContent_Shadcn_ side="left" className="w-[500px] p-0">
<CodeBlock
language="sql"
className="language-sql border-none"
hideLineNumbers
value={template.content}
/>
</HoverCardContent_Shadcn_>
</HoverCard_Shadcn_>
))}
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)}
<ButtonTooltip
tooltip={{
content: {
side: 'bottom',
text: 'Save as snippet',
},
}}
size="tiny"
type="default"
className="w-7 h-7"
loading={isSaving}
icon={<Save size={16} />}
onClick={async () => {
if (!ref) return console.error('Project ref is required')
if (!project) return console.error('Project is required')
if (!profile) return console.error('Profile is required')
try {
setIsSaving(true)
const { title: name } = await generateSqlTitle({
sql: currentValue,
})
const snippet = createSqlSnippetSkeletonV2({
id: uuidv4(),
name,
sql: currentValue,
owner_id: profile.id,
project_id: project.id,
})
snapV2.addSnippet({ projectRef: ref, snippet })
snapV2.addNeedsSaving(snippet.id)
toast.success(
<div>
Saved snippet! View it{' '}
<InlineLink href={`/project/${ref}/sql/${snippet.id}`}>here</InlineLink>
</div>
)
} catch (error: any) {
toast.error(`Failed to create new query: ${error.message}`)
} finally {
setIsSaving(false)
}
}}
/>
<ButtonTooltip
size="tiny"
type="default"
className="w-7 h-7"
onClick={onClose}
icon={<X size={16} />}
tooltip={{
content: {
side: 'bottom',
text: (
<div className="flex items-center gap-4">
<span>Close Editor</span>
<KeyboardShortcut keys={['Meta', 'e']} />
</div>
),
},
}}
/>
</div>
</SheetHeader>
<div className="flex-1 overflow-hidden flex flex-col h-full">
<div className="flex-1 min-h-0 relative">
<AIEditor
autoFocus
language="pgsql"
value={currentValue}
onChange={handleChange}
aiEndpoint={`${BASE_PATH}/api/ai/code/complete`}
aiMetadata={{
projectRef: project?.ref,
connectionString: project?.connectionString,
orgSlug: org?.slug,
}}
initialPrompt={initialPrompt}
options={{
tabSize: 2,
fontSize: 13,
minimap: { enabled: false },
wordWrap: 'on',
lineNumbers: 'on',
folding: false,
padding: { top: 16 },
lineNumbersMinChars: 3,
}}
executeQuery={onExecuteSql}
onClose={() => onClose()}
/>
</div>
{error !== undefined && (
<div className="shrink-0">
<Admonition
type="warning"
className="m-0 rounded-none border-x-0 border-b-0 [&>div>div>pre]:text-sm [&>div]:flex [&>div]:flex-col [&>div]:gap-y-2"
title={errorHeader || 'Error running SQL query'}
description={
<div>
{errorContent.length > 0 ? (
errorContent.map((errorText: string, i: number) => (
<pre key={`err-${i}`} className="font-mono text-xs whitespace-pre-wrap">
{errorText}
</pre>
))
) : (
<p className="font-mono text-xs">{error.error}</p>
)}
</div>
}
/>
</div>
)}
{showWarning && (
<SqlWarningAdmonition
className="border-t"
warningType={showWarning}
onCancel={() => setShowWarning(undefined)}
onConfirm={() => {
setShowWarning(undefined)
onExecuteSql(true)
}}
/>
)}
{results !== undefined && results.length > 0 && (
<div className={`max-h-72 shrink-0 flex flex-col ${showResults && 'h-full'}`}>
{showResults && (
<div className="border-t flex-1 overflow-auto">
<Results rows={results} />
</div>
)}
<p className="text-xs text-foreground-light border-t py-2 px-5 flex items-center justify-between">
<span className="font-mono">
{results.length} rows{results.length >= 100 && ` (Limited to only 100 rows)`}
</span>
<Button
size="tiny"
type="default"
className="ml-2"
onClick={() => setShowResults((prev) => !prev)}
>
{showResults ? 'Hide Results' : 'Show Results'}
</Button>
</p>
</div>
)}
{results !== undefined && results.length === 0 && !error && (
<div className="shrink-0">
<p className="text-xs text-foreground-light font-mono py-2 px-5">
Success. No rows returned.
</p>
</div>
)}
<div className="flex items-center gap-2 justify-end px-5 py-4 w-full border-t shrink-0">
{onSave && (
<Form_Shadcn_ {...saveForm}>
<form
onSubmit={saveForm.handleSubmit((values) => {
onSave(currentValue, values.saveValue)
})}
className="flex items-center gap-2"
>
{saveValue && (
<FormField_Shadcn_
control={saveForm.control}
name="saveValue"
render={({ field }) => (
<Input size="tiny" placeholder="Enter save value..." {...field} />
)}
/>
)}
<Button size="tiny" type="default" htmlType="submit">
{saveLabel}
</Button>
</form>
</Form_Shadcn_>
)}
<SqlRunButton
isDisabled={isExecuting}
isExecuting={isExecuting}
onClick={onExecuteSql}
/>
</div>
</div>
</SheetContent>
</Sheet>
)
}
export default EditorPanel