Skip to content

Commit 7548fd1

Browse files
ivasilovjoshenlimsaltcod
authored
feat: Conversational AI for the SQL Editor (supabase#21388)
* Add a API endpoint for generating queries for the SQL editor. * Merge all multiline props. * Add a new component for diff actions. * Copy components for the AI panel. * Add useChat hook. * Add a feature preview for the this feature. The preview is dependent on the feature flag. * Reorder the nesting in the SQL editor to accomodate the AI assistant. * Try to fit both AI assistants in the SQL editor, available via a feature preview. * Refactor the SQL editor to make the diff work correctly in all cases. * Minor fixes for the old AI feature. * Fix the debug functionality to work with both assistants. * Fix some copy-paste leftovers. * Remove unneeded code. * Make the icons softer. * Fix the name of the panel component. * Fix console.logs. * Add overflow to the AI assistant. * surface opt in config in ai settings button * Skip diffing if editor is empty * Add selected state when selecting a message to insert/replace code * Add sample prompts * Add SQL ai dislaimer when replacing code * Light mode action bar nudges * Add text for the feature preview. * lang nudges * Hide the command suggestions for now. * Set the discussion url to undefined. * Don't add the disclaimer twice. --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com> Co-authored-by: Terry Sutton <saltcod@gmail.com>
1 parent 010aa4b commit 7548fd1

File tree

19 files changed

+1374
-521
lines changed

19 files changed

+1374
-521
lines changed

apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const FeaturePreviewContextProvider = ({ children }: PropsWithChildren<{}
2222
[LOCAL_STORAGE_KEYS.UI_PREVIEW_API_SIDE_PANEL]: false,
2323
[LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_AI_ASSISTANT]: false,
2424
[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]: false,
25+
[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_AI_ASSISTANT]: false,
2526
})
2627

2728
useEffect(() => {
@@ -35,6 +36,8 @@ export const FeaturePreviewContextProvider = ({ children }: PropsWithChildren<{}
3536
localStorage.getItem(LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_AI_ASSISTANT) === 'true',
3637
[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]:
3738
localStorage.getItem(LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS) === 'true',
39+
[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_AI_ASSISTANT]:
40+
localStorage.getItem(LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_AI_ASSISTANT) === 'true',
3841
})
3942
}
4043
}, [])
@@ -69,3 +72,8 @@ export const useIsColumnLevelPrivilegesEnabled = () => {
6972
const { flags } = useFeaturePreviewContext()
7073
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]
7174
}
75+
76+
export const useIsSQLEditorAiAssistantEnabled = () => {
77+
const { flags } = useFeaturePreviewContext()
78+
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_AI_ASSISTANT]
79+
}

apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@ import { useRouter } from 'next/router'
55
import { useEffect, useState } from 'react'
66
import { Button, IconExternalLink, IconEye, IconEyeOff, Modal, ScrollArea, cn } from 'ui'
77

8+
import { useFlag } from 'hooks'
89
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
910
import Telemetry from 'lib/telemetry'
1011
import { useAppStateSnapshot } from 'state/app-state'
1112
import APISidePanelPreview from './APISidePanelPreview'
1213
import CLSPreview from './CLSPreview'
1314
import { useFeaturePreviewContext } from './FeaturePreviewContext'
1415
import RLSAIAssistantPreview from './RLSAIAssistantPreview'
16+
import { SQLEditorAIAssistantPreview } from './SQLEditorAIAssistantPreview'
1517

1618
const FeaturePreviewModal = () => {
19+
const isAIConversational = useFlag('sqlEditorConversationalAi')
20+
1721
// [Ivan] We should probably move this to a separate file, together with LOCAL_STORAGE_KEYS. We should make adding new feature previews as simple as possible.
1822
const FEATURE_PREVIEWS: { key: string; name: string; content: any; discussionsUrl?: string }[] = [
1923
{
@@ -34,6 +38,17 @@ const FeaturePreviewModal = () => {
3438
content: <CLSPreview />,
3539
discussionsUrl: 'https://github.com/orgs/supabase/discussions/20295',
3640
},
41+
// the user should only be able to see the panel for the AI assistant if the feature flag is true
42+
...(isAIConversational
43+
? [
44+
{
45+
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_AI_ASSISTANT,
46+
name: 'Supabase Assistant for SQL editor',
47+
content: <SQLEditorAIAssistantPreview />,
48+
discussionsUrl: undefined,
49+
},
50+
]
51+
: []),
3752
]
3853

3954
const router = useRouter()
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useParams } from 'common'
2+
import { Markdown } from 'components/interfaces/Markdown'
3+
import { BASE_PATH } from 'lib/constants'
4+
import Image from 'next/image'
5+
6+
export const SQLEditorAIAssistantPreview = () => {
7+
const { ref } = useParams()
8+
9+
return (
10+
<div className="space-y-2">
11+
<div className="mb-4 flex flex-col gap-y-2">
12+
<Markdown
13+
className="text-foreground-light max-w-full"
14+
content="When using the [SQL Editor](http://supabase.com/project/_/sql) you'll have access to a new and improved AI Assistant which you can use to help you write your queries."
15+
/>
16+
<Markdown
17+
className="text-foreground-light max-w-full"
18+
content={`Let our AI Assistant handle the SQL while you focus on building your app.`}
19+
/>
20+
</div>
21+
<Image
22+
src={`${BASE_PATH}/img/previews/rls-ai-assistant-preview.png`}
23+
width={1860}
24+
height={970}
25+
alt="api-docs-side-panel-preview"
26+
className="rounded border"
27+
/>
28+
<div className="space-y-2 !mt-4">
29+
<p className="text-sm">Enabling this preview will:</p>
30+
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
31+
<li>
32+
<Markdown
33+
className="text-foreground-light"
34+
content={`Replace the existing single-input UI with a side panel where you can have a full conversation.`}
35+
/>
36+
</li>
37+
<li>
38+
<Markdown
39+
className="text-foreground-light"
40+
content={`Supabase Assistant will iteratively generate SQL from your natural language prompts.`}
41+
/>
42+
</li>
43+
</ul>
44+
</div>
45+
</div>
46+
)
47+
}

apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyChat.tsx

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { zodResolver } from '@hookform/resolvers/zod'
2+
import { useTelemetryProps } from 'common'
3+
import Telemetry from 'lib/telemetry'
24
import { compact, last } from 'lodash'
35
import { Loader2 } from 'lucide-react'
6+
import { useRouter } from 'next/router'
47
import { useEffect, useRef } from 'react'
58
import { useForm } from 'react-hook-form'
69
import {
@@ -10,14 +13,13 @@ import {
1013
FormField_Shadcn_,
1114
FormItem_Shadcn_,
1215
Form_Shadcn_,
13-
IconSettings,
1416
Input_Shadcn_,
17+
cn,
1518
} from 'ui'
1619
import * as z from 'zod'
17-
import Telemetry from 'lib/telemetry'
18-
import { useTelemetryProps } from 'common'
19-
import { useRouter } from 'next/router'
2020

21+
import { useLocalStorageQuery, useSelectedOrganization } from 'hooks'
22+
import { IS_PLATFORM, LOCAL_STORAGE_KEYS, OPT_IN_TAGS } from 'lib/constants'
2123
import { useProfile } from 'lib/profile'
2224
import { useAppStateSnapshot } from 'state/app-state'
2325
import { MessageWithDebug } from './AIPolicyEditorPanel.utils'
@@ -40,12 +42,17 @@ export const AIPolicyChat = ({
4042
onDiff,
4143
onChange,
4244
}: AIPolicyChatProps) => {
45+
const router = useRouter()
4346
const { profile } = useProfile()
4447
const snap = useAppStateSnapshot()
48+
const organization = useSelectedOrganization()
4549
const bottomRef = useRef<HTMLDivElement>(null)
46-
const router = useRouter()
4750
const telemetryProps = useTelemetryProps()
4851

52+
const isOptedInToAI = organization?.opt_in_tags?.includes(OPT_IN_TAGS.AI_SQL) ?? false
53+
const [hasEnabledAISchema] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.SQL_EDITOR_AI_SCHEMA, true)
54+
const includeSchemaMetadata = (isOptedInToAI || !IS_PLATFORM) && hasEnabledAISchema
55+
4956
const name = compact([profile?.first_name, profile?.last_name]).join(' ')
5057

5158
const FormSchema = z.object({ chat: z.string() })
@@ -90,15 +97,21 @@ export const AIPolicyChat = ({
9097
Make sure to verify any generated code or suggestions, and share feedback so that we can
9198
learn and improve.`}
9299
>
93-
<div>
94-
<Button
95-
type="default"
96-
icon={<IconSettings strokeWidth={1.5} />}
97-
onClick={() => snap.setShowAiSettingsModal(true)}
98-
>
99-
AI Settings
100-
</Button>
101-
</div>
100+
<Button
101+
type="default"
102+
className="w-min"
103+
icon={
104+
<div
105+
className={cn(
106+
'w-2 h-2 rounded-full',
107+
includeSchemaMetadata ? 'bg-brand' : 'border border-stronger'
108+
)}
109+
/>
110+
}
111+
onClick={() => snap.setShowAiSettingsModal(true)}
112+
>
113+
{includeSchemaMetadata ? 'Include' : 'Exclude'} database metadata in queries
114+
</Button>
102115
</Message>
103116

104117
{messages.map((m) => (

apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/Message.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,10 @@ const Message = memo(function Message({
8282
return (
8383
<AIPolicyPre
8484
onDiff={onDiff}
85-
className={
85+
className={cn(
86+
'transition',
8687
isSelected ? '[&>div>pre]:!border-stronger [&>div>pre]:!bg-surface-200' : ''
87-
}
88+
)}
8889
>
8990
{props.children[0].props.children}
9091
</AIPolicyPre>
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { useTelemetryProps } from 'common'
2+
import { InsertCode, ReplaceCode } from 'icons'
3+
import { Check, Copy } from 'lucide-react'
4+
import { useRouter } from 'next/router'
5+
import { useEffect, useState } from 'react'
6+
import { format } from 'sql-formatter'
7+
import {
8+
Button,
9+
CodeBlock,
10+
TooltipContent_Shadcn_,
11+
TooltipTrigger_Shadcn_,
12+
Tooltip_Shadcn_,
13+
cn,
14+
} from 'ui'
15+
16+
import Telemetry from 'lib/telemetry'
17+
import { DiffType } from '../SQLEditor.types'
18+
19+
interface AiMessagePreProps {
20+
onDiff: (type: DiffType, s: string) => void
21+
children: string[]
22+
className?: string
23+
}
24+
25+
export const AiMessagePre = ({ onDiff, children, className }: AiMessagePreProps) => {
26+
const [copied, setCopied] = useState(false)
27+
const router = useRouter()
28+
const telemetryProps = useTelemetryProps()
29+
30+
useEffect(() => {
31+
if (!copied) return
32+
const timer = setTimeout(() => setCopied(false), 2000)
33+
return () => clearTimeout(timer)
34+
}, [copied])
35+
36+
let formatted = (children || [''])[0]
37+
try {
38+
formatted = format(formatted, { language: 'postgresql', keywordCase: 'upper' })
39+
} catch {}
40+
41+
if (formatted.length === 0) {
42+
return null
43+
}
44+
45+
function handleCopy(formatted: string) {
46+
navigator.clipboard.writeText(formatted).then()
47+
setCopied(true)
48+
}
49+
50+
return (
51+
<pre className={cn('rounded-md relative group', className)}>
52+
<CodeBlock
53+
value={formatted}
54+
language="sql"
55+
className={cn(
56+
'!bg-transparent !py-3 !px-3.5 prose dark:prose-dark',
57+
// change the look of the code block. The flex hack is so that the code is wrapping since
58+
// every word is a separate span
59+
'[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap'
60+
)}
61+
hideCopy
62+
hideLineNumbers
63+
/>
64+
<div className="absolute top-5 right-2 bg-surface-100 border-muted border rounded-lg h-[28px] hidden group-hover:block">
65+
<Tooltip_Shadcn_>
66+
<TooltipTrigger_Shadcn_ asChild>
67+
<Button
68+
type="text"
69+
size="tiny"
70+
onClick={() => {
71+
onDiff(DiffType.Addition, formatted)
72+
Telemetry.sendEvent(
73+
{
74+
category: 'sql_editor_ai_assistant',
75+
action: 'ai_suggestion_inserted',
76+
label: 'sql-editor-ai-assistant',
77+
},
78+
telemetryProps,
79+
router
80+
)
81+
}}
82+
>
83+
<InsertCode className="h-4 w-4 text-foreground-light" strokeWidth={1.5} />
84+
</Button>
85+
</TooltipTrigger_Shadcn_>
86+
<TooltipContent_Shadcn_ side="bottom" className="font-sans">
87+
Insert code
88+
</TooltipContent_Shadcn_>
89+
</Tooltip_Shadcn_>
90+
91+
<Tooltip_Shadcn_>
92+
<TooltipTrigger_Shadcn_ asChild>
93+
<Button
94+
type="text"
95+
size="tiny"
96+
onClick={() => {
97+
onDiff(DiffType.Modification, formatted)
98+
Telemetry.sendEvent(
99+
{
100+
category: 'sql_editor_ai_assistant',
101+
action: 'ai_suggestion_replaced',
102+
label: 'sql-editor-ai-assistant',
103+
},
104+
telemetryProps,
105+
router
106+
)
107+
}}
108+
>
109+
<ReplaceCode className="h-4 w-4 text-foreground-light" strokeWidth={1.5} />
110+
</Button>
111+
</TooltipTrigger_Shadcn_>
112+
<TooltipContent_Shadcn_ side="bottom" className="font-sans">
113+
Replace code
114+
</TooltipContent_Shadcn_>
115+
</Tooltip_Shadcn_>
116+
117+
<Tooltip_Shadcn_>
118+
<TooltipTrigger_Shadcn_ asChild>
119+
<Button
120+
type="text"
121+
size="tiny"
122+
onClick={() => {
123+
handleCopy(formatted)
124+
Telemetry.sendEvent(
125+
{
126+
category: 'sql_editor_ai_assistant',
127+
action: 'ai_suggestion_copied',
128+
label: 'sql-editor-ai-assistant',
129+
},
130+
telemetryProps,
131+
router
132+
)
133+
}}
134+
>
135+
{copied ? (
136+
<Check size={16} className="text-brand-600" />
137+
) : (
138+
<Copy size={16} className="text-foreground-light" strokeWidth={1.5} />
139+
)}
140+
</Button>
141+
</TooltipTrigger_Shadcn_>
142+
<TooltipContent_Shadcn_ side="bottom" className="font-sans">
143+
Copy code
144+
</TooltipContent_Shadcn_>
145+
</Tooltip_Shadcn_>
146+
</div>
147+
</pre>
148+
)
149+
}

0 commit comments

Comments
 (0)