This commit is contained in:
Ivan Vasilov committed 2025-02-04 15:25:13 +01:00
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">
@@ -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}
/>