diff --git a/ui/desktop/src/components/recipes/ImportRecipeForm.tsx b/ui/desktop/src/components/recipes/ImportRecipeForm.tsx index e8a36e46eb7e..8934a741ada8 100644 --- a/ui/desktop/src/components/recipes/ImportRecipeForm.tsx +++ b/ui/desktop/src/components/recipes/ImportRecipeForm.tsx @@ -32,7 +32,7 @@ const importRecipeSchema = z (value) => !value || value.trim().startsWith('goose://recipe?config='), 'Invalid deeplink format. Expected: goose://recipe?config=...' ), - yamlFile: z + recipeUploadFile: z .instanceof(File) .nullable() .refine((file) => { @@ -42,8 +42,8 @@ const importRecipeSchema = z recipeName: recipeNameSchema, global: z.boolean(), }) - .refine((data) => (data.deeplink && data.deeplink.trim()) || data.yamlFile, { - message: 'Either of deeplink or YAML file are required', + .refine((data) => (data.deeplink && data.deeplink.trim()) || data.recipeUploadFile, { + message: 'Either of deeplink or recipe file are required', path: ['deeplink'], }); @@ -85,11 +85,24 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR } }; - const parseYamlFile = async (fileContent: string): Promise => { - const parsed = yaml.parse(fileContent); + const parseRecipeUploadFile = async (fileContent: string, fileName: string): Promise => { + const isJsonFile = fileName.toLowerCase().endsWith('.json'); + let parsed; + + try { + if (isJsonFile) { + parsed = JSON.parse(fileContent); + } else { + parsed = yaml.parse(fileContent); + } + } catch (error) { + throw new Error( + `Failed to parse ${isJsonFile ? 'JSON' : 'YAML'} file: ${error instanceof Error ? error.message : 'Invalid format'}` + ); + } if (!parsed) { - throw new Error('YAML file is empty or contains invalid content'); + throw new Error(`${isJsonFile ? 'JSON' : 'YAML'} file is empty or contains invalid content`); } // Handle both CLI format (flat structure) and Desktop format (nested under 'recipe' key) @@ -101,7 +114,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR const importRecipeForm = useForm({ defaultValues: { deeplink: '', - yamlFile: null as File | null, + recipeUploadFile: null as File | null, recipeName: '', global: true, }, @@ -113,7 +126,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR try { let recipe: Recipe; - // Parse recipe from either deeplink or YAML file + // Parse recipe from either deeplink or recipe file if (value.deeplink && value.deeplink.trim()) { const parsedRecipe = await parseDeeplink(value.deeplink.trim()); if (!parsedRecipe) { @@ -121,8 +134,8 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR } recipe = parsedRecipe; } else { - const fileContent = await value.yamlFile!.text(); - recipe = await parseYamlFile(fileContent); + const fileContent = await value.recipeUploadFile!.text(); + recipe = await parseRecipeUploadFile(fileContent, value.recipeUploadFile!.name); } const validationResult = validateRecipe(recipe); @@ -139,7 +152,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR // Reset dialog state importRecipeForm.reset({ deeplink: '', - yamlFile: null, + recipeUploadFile: null, recipeName: '', global: true, }); @@ -169,7 +182,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR // Reset form to default values importRecipeForm.reset({ deeplink: '', - yamlFile: null, + recipeUploadFile: null, recipeName: '', global: true, }); @@ -214,13 +227,13 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR } }; - const handleYamlFileChange = async (file: File | undefined) => { - importRecipeForm.setFieldValue('yamlFile', file || null); + const handleRecipeUploadChange = async (file: File | undefined) => { + importRecipeForm.setFieldValue('recipeUploadFile', file || null); if (file) { try { const fileContent = await file.text(); - const recipe = await parseYamlFile(fileContent); + const recipe = await parseRecipeUploadFile(fileContent, file.name); if (recipe.title) { const suggestedName = generateRecipeNameFromTitle(recipe.title); @@ -233,7 +246,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR } } catch (error) { // Silently handle parsing errors during auto-suggest - console.log('Could not parse YAML file for auto-suggest:', error); + console.log('Could not parse recipe file for auto-suggest:', error); } } else { // Clear the recipe name when file is removed @@ -266,7 +279,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR <> {(field) => { - const isDisabled = values.yamlFile !== null; + const isDisabled = values.recipeUploadFile !== null; return (
@@ -320,7 +333,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
- + {(field) => { const hasDeeplink = values.deeplink?.trim(); const isDisabled = !!hasDeeplink; @@ -328,22 +341,22 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR return (
{ - handleYamlFileChange(e.target.files?.[0]); + handleRecipeUploadChange(e.target.files?.[0]); }} onBlur={field.handleBlur} - className={`${field.state.meta.errors.length > 0 ? 'border-red-500' : ''} ${ + className={`file:pt-1 ${field.state.meta.errors.length > 0 ? 'border-red-500' : ''} ${ isDisabled ? 'cursor-not-allowed' : '' }`} /> @@ -352,7 +365,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR

- Upload a YAML file containing the recipe structure + Upload a YAML or JSON file containing the recipe structure