mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 14:49:10 +03:00
Test.
This commit is contained in:
1 parent
a67fcb1809
commit
59c072c2dc
5 files changed
+305
-9
No files matched your search
@@ -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 (
|
||||
<>
|
||||
<div className="flex flex-col gap-8 w-full">
|
||||
@@ -28,6 +31,16 @@ const FunctionsEmptyState = () => {
|
||||
Examples
|
||||
</a>
|
||||
</Button>
|
||||
<Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button type="default" icon={<Plus />}>
|
||||
New AI Completion function
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent size="default" showClose={false} className="flex flex-col gap-0">
|
||||
<DeployAiCompletionFunctionSheet onClose={() => setIsSheetOpen(false)} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-8 bg-surface-100 px-5 py-4 border rounded-md">
|
||||
|
||||
+166
@@ -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<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
prompt: '',
|
||||
apiUrl: '',
|
||||
apiKey: '',
|
||||
name: '',
|
||||
},
|
||||
})
|
||||
|
||||
const { mutateAsync } = useEdgeFunctionDeployMutation()
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = 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 (
|
||||
<>
|
||||
<SheetHeader className="py-3 flex flex-row justify-between items-center border-b-0">
|
||||
<div className="flex flex-row gap-3 items-center">
|
||||
<SheetClose
|
||||
className={cn(
|
||||
'text-muted hover:text ring-offset-background transition-opacity hover:opacity-100',
|
||||
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
'disabled:pointer-events-none data-[state=open]:bg-secondary'
|
||||
)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetClose>
|
||||
<SheetTitle>Create AI Completion Function</SheetTitle>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
<Separator />
|
||||
<SheetSection className="overflow-auto flex-grow px-0">
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
id={FORM_ID}
|
||||
className="space-y-6 w-full py-5 flex-1"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="prompt"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label="Prompt"
|
||||
description="Enter your AI completion prompt"
|
||||
className="px-5"
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<ExpandingTextArea {...field} placeholder="Enter your prompt here..." />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="space-y-4 px-5">
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="apiUrl"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout label="API URL" description="Enter the API base URL">
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_ {...field} placeholder="https://api.example.com" />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="apiKey"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout label="API Key" description="Enter your API key">
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_ {...field} type="password" placeholder="Enter your API key" />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="endpoint"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label="API Endpoint"
|
||||
description="Enter the specific API endpoint"
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_ {...field} placeholder="/v1/completions" />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</SheetSection>
|
||||
<SheetFooter>
|
||||
<Button type="default" onClick={() => onClose()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button form={FORM_ID} htmlType="submit">
|
||||
Create function
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<ReturnType<typeof deployEdgeFunction>>
|
||||
|
||||
export const useEdgeFunctionDeployMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<EdgeFunctionsDeployData, ResponseError, EdgeFunctionsDeployVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<EdgeFunctionsDeployData, ResponseError, EdgeFunctionsDeployVariables>(
|
||||
(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,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,8 @@ export interface ExpandingTextAreaProps extends React.TextareaHTMLAttributes<HTM
|
||||
value: string
|
||||
}
|
||||
|
||||
const MIN_TEXTAREA_HEIGHT = 100
|
||||
|
||||
/**
|
||||
* This is a custom TextArea component that expands based on the content.
|
||||
*/
|
||||
@@ -24,11 +26,13 @@ const ExpandingTextArea = forwardRef<HTMLTextAreaElement, ExpandingTextAreaProps
|
||||
useEffect(() => {
|
||||
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<HTMLTextAreaElement, ExpandingTextAreaProps
|
||||
return (
|
||||
<TextArea
|
||||
ref={textAreaRef}
|
||||
rows={1}
|
||||
aria-expanded={false}
|
||||
className={cn('transition-all resize-none leading-6 box-border', className)}
|
||||
// rows={1}
|
||||
// aria-expanded={false}
|
||||
className={cn('transition-all resize-none box-border', className)}
|
||||
value={value}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user