import { useState } from 'react'; import { useForm } from '@tanstack/react-form'; import { z } from 'zod'; import { Download } from 'lucide-react'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; import { Recipe, decodeRecipe } from '../../recipe'; import { toastSuccess, toastError } from '../../toasts'; import { useEscapeKey } from '../../hooks/useEscapeKey'; import { getRecipeJsonSchema } from '../../recipe/validation'; import { saveRecipe } from '../../recipe/recipe_management'; import { parseRecipe } from '../../api'; interface ImportRecipeFormProps { isOpen: boolean; onClose: () => void; onSuccess: () => void; } // Define Zod schema for the import form const importRecipeSchema = z .object({ deeplink: z .string() .refine( (value) => !value || value.trim().startsWith('goose://recipe?config='), 'Invalid deeplink format. Expected: goose://recipe?config=...' ), recipeUploadFile: z .instanceof(File) .nullable() .refine((file) => { if (!file) return true; return file.size <= 1024 * 1024; }, 'File is too large, max size is 1MB'), }) .refine((data) => (data.deeplink && data.deeplink.trim()) || data.recipeUploadFile, { message: 'Either of deeplink or recipe file are required', path: ['deeplink'], }); export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportRecipeFormProps) { const [importing, setImporting] = useState(false); const [showSchemaModal, setShowSchemaModal] = useState(false); useEscapeKey(isOpen, onClose); const parseDeeplink = async (deeplink: string): Promise => { try { const cleanLink = deeplink.trim(); if (!cleanLink.startsWith('goose://recipe?config=')) { throw new Error('Invalid deeplink format. Expected: goose://recipe?config=...'); } const recipeEncoded = cleanLink.replace('goose://recipe?config=', ''); if (!recipeEncoded) { throw new Error('No recipe configuration found in deeplink'); } const recipe = await decodeRecipe(recipeEncoded); if (!recipe.title || !recipe.description) { throw new Error('Recipe is missing required fields (title, description)'); } if (!recipe.instructions && !recipe.prompt) { throw new Error('Recipe must have either instructions or prompt'); } return recipe; } catch (error) { console.error('Failed to parse deeplink:', error); return null; } }; const parseRecipeFromFile = async (fileContent: string): Promise => { try { let response = await parseRecipe({ body: { content: fileContent, }, throwOnError: true, }); return response.data.recipe; } catch (error) { let error_message = 'unknown error'; if (typeof error === 'object' && error !== null && 'message' in error) { error_message = error.message as string; } throw new Error(error_message); } }; const importRecipeForm = useForm({ defaultValues: { deeplink: '', recipeUploadFile: null as File | null, }, validators: { onChange: importRecipeSchema, }, onSubmit: async ({ value }) => { setImporting(true); try { let recipe: Recipe; // Parse recipe from either deeplink or recipe file if (value.deeplink && value.deeplink.trim()) { const parsedRecipe = await parseDeeplink(value.deeplink.trim()); if (!parsedRecipe) { throw new Error('Invalid deeplink or recipe format'); } recipe = parsedRecipe; } else { const fileContent = await value.recipeUploadFile!.text(); recipe = await parseRecipeFromFile(fileContent); } await saveRecipe(recipe, null); // Reset dialog state importRecipeForm.reset({ deeplink: '', recipeUploadFile: null, }); onClose(); onSuccess(); toastSuccess({ title: recipe.title.trim(), msg: 'Recipe imported successfully', }); } catch (error) { console.error('Failed to import recipe:', error); toastError({ title: 'Import Failed', msg: `Failed to import recipe: ${error instanceof Error ? error.message : 'Unknown error'}`, traceback: error instanceof Error ? error.message : String(error), }); } finally { setImporting(false); } }, }); const handleClose = () => { importRecipeForm.reset({ deeplink: '', recipeUploadFile: null, }); onClose(); }; const handleDeeplinkChange = async ( value: string, field: { handleChange: (value: string) => void } ) => { field.handleChange(value); if (value.trim()) { try { await parseDeeplink(value.trim()); } catch (error) { toastError({ title: 'Invalid Deeplink', msg: `The deeplink format is invalid: ${error instanceof Error ? error.message : 'Unknown error'}`, }); } } }; const handleRecipeUploadChange = async (file: File | undefined) => { importRecipeForm.setFieldValue('recipeUploadFile', file || null); if (file) { try { const fileContent = await file.text(); await parseRecipeFromFile(fileContent); } catch (error) { toastError({ title: 'Invalid Recipe File', msg: error instanceof Error ? error.message : 'Unknown error', }); } } }; if (!isOpen) return null; return ( <>

Import Recipe

{ e.preventDefault(); e.stopPropagation(); importRecipeForm.handleSubmit(); }} >
state.values}> {(values) => ( <> {(field) => { const isDisabled = values.recipeUploadFile !== null; return (