diff --git a/apps/studio/components/interfaces/Functions/FunctionsEmptyState.tsx b/apps/studio/components/interfaces/Functions/FunctionsEmptyState.tsx
index 4165486a8d8..68805df6b68 100644
--- a/apps/studio/components/interfaces/Functions/FunctionsEmptyState.tsx
+++ b/apps/studio/components/interfaces/Functions/FunctionsEmptyState.tsx
@@ -1,10 +1,13 @@
-import { ExternalLink } from 'lucide-react'
-
import { DocsButton } from 'components/ui/DocsButton'
-import { Button } from 'ui'
+import { ExternalLink, Plus } from 'lucide-react'
+import { useState } from 'react'
+import { Button, Sheet, SheetContent, SheetTrigger } from 'ui'
+import { DeployAiCompletionFunctionSheet } from './deployAiCompletionFunctionSheet'
import TerminalInstructions from './TerminalInstructions'
const FunctionsEmptyState = () => {
+ const [isSheetOpen, setIsSheetOpen] = useState(false)
+
return (
<>
@@ -28,6 +31,16 @@ const FunctionsEmptyState = () => {
Examples
+
+
+ }>
+ New AI Completion function
+
+
+
+ setIsSheetOpen(false)} />
+
+
diff --git a/apps/studio/components/interfaces/Functions/deployAiCompletionFunctionSheet/index.tsx b/apps/studio/components/interfaces/Functions/deployAiCompletionFunctionSheet/index.tsx
new file mode 100644
index 00000000000..e7c8741ada1
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/deployAiCompletionFunctionSheet/index.tsx
@@ -0,0 +1,166 @@
+import { zodResolver } from '@hookform/resolvers/zod'
+import { X } from 'lucide-react'
+import { SubmitHandler, useForm } from 'react-hook-form'
+import { toast } from 'sonner'
+import * as z from 'zod'
+
+import { useEdgeFunctionDeployMutation } from 'data/edge-functions/edge-functions-deploy-mutation'
+import {
+ Button,
+ ExpandingTextArea,
+ FormControl_Shadcn_,
+ FormField_Shadcn_,
+ Form_Shadcn_,
+ Input_Shadcn_,
+ Separator,
+ SheetClose,
+ SheetFooter,
+ SheetHeader,
+ SheetSection,
+ SheetTitle,
+ cn,
+} from 'ui'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+
+const FORM_ID = 'create-ai-completion-function'
+
+const FormSchema = z.object({
+ prompt: z.string().min(1, 'Prompt is required'),
+ apiUrl: z.string().url('Must be a valid URL'),
+ apiKey: z.string().min(1, 'API key is required'),
+ name: z.string().min(1, 'Endpoint is required'),
+})
+
+interface NewAiCompletionFunctionDialogProps {
+ onClose: () => void
+}
+
+export const DeployAiCompletionFunctionSheet = ({
+ onClose,
+}: NewAiCompletionFunctionDialogProps) => {
+ const form = useForm
>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ prompt: '',
+ apiUrl: '',
+ apiKey: '',
+ name: '',
+ },
+ })
+
+ const { mutateAsync } = useEdgeFunctionDeployMutation()
+
+ const onSubmit: SubmitHandler> = async (values) => {
+ try {
+ await mutateAsync({
+ projectRef: 'projectRef',
+ metadata: {
+ entrypoint_path: 'index.ts',
+ import_map_path: 'import_map.json',
+ name: values.name,
+ verify_jwt: false,
+ },
+ files: [{ name: 'index.ts', content: values.prompt }],
+ })
+ toast.success('Successfully created AI completion function')
+ onClose()
+ } catch (error) {
+ toast.error('Failed to create AI completion function')
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+ Close
+
+ Create AI Completion Function
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/apps/studio/components/interfaces/Functions/deployAiCompletionFunctionSheet/utils.ts b/apps/studio/components/interfaces/Functions/deployAiCompletionFunctionSheet/utils.ts
new file mode 100644
index 00000000000..1e9c1532ab5
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/deployAiCompletionFunctionSheet/utils.ts
@@ -0,0 +1,29 @@
+const generateFunctionCode = async (prompt: string) => {
+ const code = `
+import OpenAI from 'https://deno.land/x/openai@v4.24.0/mod.ts'
+
+Deno.serve(async (req) => {
+ const { query } = await req.json()
+ const apiKey = Deno.env.get('OPENAI_API_KEY')
+ const openai = new OpenAI({
+ apiKey: apiKey,
+ })
+
+ // Documentation here: https://github.com/openai/openai-node
+ const chatCompletion = await openai.chat.completions.create({
+ messages: [{ role: 'user', content: query }],
+ // Choose model from here: https://platform.openai.com/docs/models
+ model: 'gpt-3.5-turbo',
+ stream: true,
+ })
+
+ const reply = chatCompletion.choices[0].message.content
+
+ return new Response(reply, {
+ headers: { 'Content-Type': 'text/plain' },
+ })
+})
+`
+
+ return code
+}
diff --git a/apps/studio/data/edge-functions/edge-functions-deploy-mutation.ts b/apps/studio/data/edge-functions/edge-functions-deploy-mutation.ts
new file mode 100644
index 00000000000..3bc6beb9a67
--- /dev/null
+++ b/apps/studio/data/edge-functions/edge-functions-deploy-mutation.ts
@@ -0,0 +1,84 @@
+import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
+import { toast } from 'sonner'
+
+import { handleError, post } from 'data/fetchers'
+import type { ResponseError } from 'types'
+import { edgeFunctionsKeys } from './keys'
+
+export type EdgeFunctionsDeployVariables = {
+ projectRef: string
+ metadata: {
+ entrypoint_path?: string
+ import_map_path?: string
+ name?: string
+ static_patterns?: string[]
+ verify_jwt?: boolean
+ }
+ files: { name: string; content: string }[]
+}
+
+export async function deployEdgeFunction({
+ projectRef,
+ metadata,
+ files,
+}: EdgeFunctionsDeployVariables) {
+ if (!projectRef) throw new Error('projectRef is required')
+
+ const { data, error } = await post(`/v1/projects/{ref}/functions/deploy`, {
+ params: {
+ path: { ref: projectRef },
+ },
+ body: {
+ file: files as any,
+ metadata: metadata,
+ },
+ bodySerializer(body) {
+ const formData = new FormData()
+
+ formData.append('metadata', JSON.stringify(body.metadata))
+
+ body.file.forEach((f: any) => {
+ const file = f as { name: string; content: string }
+ const blob = new Blob([file.content], { type: 'text/plain' })
+ formData.append('file', blob, file.name)
+ })
+
+ return formData
+ },
+ })
+
+ if (error) handleError(error)
+ return data
+}
+
+type EdgeFunctionsDeployData = Awaited>
+
+export const useEdgeFunctionDeployMutation = ({
+ onSuccess,
+ onError,
+ ...options
+}: Omit<
+ UseMutationOptions,
+ 'mutationFn'
+> = {}) => {
+ const queryClient = useQueryClient()
+
+ return useMutation(
+ (vars) => deployEdgeFunction(vars),
+ {
+ async onSuccess(data, variables, context) {
+ const { projectRef, slug } = variables
+ await Promise.all([queryClient.invalidateQueries(edgeFunctionsKeys.list(projectRef))])
+ await onSuccess?.(data, variables, context)
+ },
+ async onError(data, variables, context) {
+ if (onError === undefined) {
+ toast.error(`Failed to deploy edge function: ${data.message}`)
+ } else {
+ onError(data, variables, context)
+ }
+ },
+ ...options,
+ }
+ )
+}
diff --git a/packages/ui/src/components/ExpandingTextArea/index.tsx b/packages/ui/src/components/ExpandingTextArea/index.tsx
index 8184f243e2e..7f716b75a28 100644
--- a/packages/ui/src/components/ExpandingTextArea/index.tsx
+++ b/packages/ui/src/components/ExpandingTextArea/index.tsx
@@ -9,6 +9,8 @@ export interface ExpandingTextAreaProps extends React.TextareaHTMLAttributes {
if (textAreaRef) {
if (textAreaRef.current && !value) {
- textAreaRef.current.style.height = '40px'
+ textAreaRef.current.style.height = `${MIN_TEXTAREA_HEIGHT}px`
} else if (textAreaRef && textAreaRef.current) {
textAreaRef.current.style.height = 'auto'
- const newHeight = textAreaRef.current.scrollHeight + 'px'
- textAreaRef.current.style.height = newHeight
+ // the 1.001 is to prevent the textarea from randomly adding a scrollbar even though the calculation should
+ // prevent that.
+ const newHeight = Math.max(textAreaRef.current.scrollHeight * 1.001, MIN_TEXTAREA_HEIGHT)
+ textAreaRef.current.style.height = `${newHeight}px`
}
}
}, [value, textAreaRef])
@@ -36,9 +40,9 @@ const ExpandingTextArea = forwardRef