From 59c072c2dc2b56eb8a299131e974b06752a86116 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Tue, 4 Feb 2025 15:25:13 +0100 Subject: [PATCH] Test. --- .../Functions/FunctionsEmptyState.tsx | 19 +- .../deployAiCompletionFunctionSheet/index.tsx | 166 ++++++++++++++++++ .../deployAiCompletionFunctionSheet/utils.ts | 29 +++ .../edge-functions-deploy-mutation.ts | 84 +++++++++ .../components/ExpandingTextArea/index.tsx | 16 +- 5 files changed, 305 insertions(+), 9 deletions(-) create mode 100644 apps/studio/components/interfaces/Functions/deployAiCompletionFunctionSheet/index.tsx create mode 100644 apps/studio/components/interfaces/Functions/deployAiCompletionFunctionSheet/utils.ts create mode 100644 apps/studio/data/edge-functions/edge-functions-deploy-mutation.ts 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 + + + + + + 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