Desktop json recipes upload (#4629)
This commit is contained in:
@@ -32,7 +32,7 @@ const importRecipeSchema = z
|
|||||||
(value) => !value || value.trim().startsWith('goose://recipe?config='),
|
(value) => !value || value.trim().startsWith('goose://recipe?config='),
|
||||||
'Invalid deeplink format. Expected: goose://recipe?config=...'
|
'Invalid deeplink format. Expected: goose://recipe?config=...'
|
||||||
),
|
),
|
||||||
yamlFile: z
|
recipeUploadFile: z
|
||||||
.instanceof(File)
|
.instanceof(File)
|
||||||
.nullable()
|
.nullable()
|
||||||
.refine((file) => {
|
.refine((file) => {
|
||||||
@@ -42,8 +42,8 @@ const importRecipeSchema = z
|
|||||||
recipeName: recipeNameSchema,
|
recipeName: recipeNameSchema,
|
||||||
global: z.boolean(),
|
global: z.boolean(),
|
||||||
})
|
})
|
||||||
.refine((data) => (data.deeplink && data.deeplink.trim()) || data.yamlFile, {
|
.refine((data) => (data.deeplink && data.deeplink.trim()) || data.recipeUploadFile, {
|
||||||
message: 'Either of deeplink or YAML file are required',
|
message: 'Either of deeplink or recipe file are required',
|
||||||
path: ['deeplink'],
|
path: ['deeplink'],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,11 +85,24 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseYamlFile = async (fileContent: string): Promise<Recipe> => {
|
const parseRecipeUploadFile = async (fileContent: string, fileName: string): Promise<Recipe> => {
|
||||||
const parsed = yaml.parse(fileContent);
|
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) {
|
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)
|
// 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({
|
const importRecipeForm = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
deeplink: '',
|
deeplink: '',
|
||||||
yamlFile: null as File | null,
|
recipeUploadFile: null as File | null,
|
||||||
recipeName: '',
|
recipeName: '',
|
||||||
global: true,
|
global: true,
|
||||||
},
|
},
|
||||||
@@ -113,7 +126,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
|||||||
try {
|
try {
|
||||||
let recipe: Recipe;
|
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()) {
|
if (value.deeplink && value.deeplink.trim()) {
|
||||||
const parsedRecipe = await parseDeeplink(value.deeplink.trim());
|
const parsedRecipe = await parseDeeplink(value.deeplink.trim());
|
||||||
if (!parsedRecipe) {
|
if (!parsedRecipe) {
|
||||||
@@ -121,8 +134,8 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
|||||||
}
|
}
|
||||||
recipe = parsedRecipe;
|
recipe = parsedRecipe;
|
||||||
} else {
|
} else {
|
||||||
const fileContent = await value.yamlFile!.text();
|
const fileContent = await value.recipeUploadFile!.text();
|
||||||
recipe = await parseYamlFile(fileContent);
|
recipe = await parseRecipeUploadFile(fileContent, value.recipeUploadFile!.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationResult = validateRecipe(recipe);
|
const validationResult = validateRecipe(recipe);
|
||||||
@@ -139,7 +152,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
|||||||
// Reset dialog state
|
// Reset dialog state
|
||||||
importRecipeForm.reset({
|
importRecipeForm.reset({
|
||||||
deeplink: '',
|
deeplink: '',
|
||||||
yamlFile: null,
|
recipeUploadFile: null,
|
||||||
recipeName: '',
|
recipeName: '',
|
||||||
global: true,
|
global: true,
|
||||||
});
|
});
|
||||||
@@ -169,7 +182,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
|||||||
// Reset form to default values
|
// Reset form to default values
|
||||||
importRecipeForm.reset({
|
importRecipeForm.reset({
|
||||||
deeplink: '',
|
deeplink: '',
|
||||||
yamlFile: null,
|
recipeUploadFile: null,
|
||||||
recipeName: '',
|
recipeName: '',
|
||||||
global: true,
|
global: true,
|
||||||
});
|
});
|
||||||
@@ -214,13 +227,13 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleYamlFileChange = async (file: File | undefined) => {
|
const handleRecipeUploadChange = async (file: File | undefined) => {
|
||||||
importRecipeForm.setFieldValue('yamlFile', file || null);
|
importRecipeForm.setFieldValue('recipeUploadFile', file || null);
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
try {
|
try {
|
||||||
const fileContent = await file.text();
|
const fileContent = await file.text();
|
||||||
const recipe = await parseYamlFile(fileContent);
|
const recipe = await parseRecipeUploadFile(fileContent, file.name);
|
||||||
if (recipe.title) {
|
if (recipe.title) {
|
||||||
const suggestedName = generateRecipeNameFromTitle(recipe.title);
|
const suggestedName = generateRecipeNameFromTitle(recipe.title);
|
||||||
|
|
||||||
@@ -233,7 +246,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Silently handle parsing errors during auto-suggest
|
// 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 {
|
} else {
|
||||||
// Clear the recipe name when file is removed
|
// Clear the recipe name when file is removed
|
||||||
@@ -266,7 +279,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
|||||||
<>
|
<>
|
||||||
<importRecipeForm.Field name="deeplink">
|
<importRecipeForm.Field name="deeplink">
|
||||||
{(field) => {
|
{(field) => {
|
||||||
const isDisabled = values.yamlFile !== null;
|
const isDisabled = values.recipeUploadFile !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={isDisabled ? 'opacity-50' : ''}>
|
<div className={isDisabled ? 'opacity-50' : ''}>
|
||||||
@@ -320,7 +333,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<importRecipeForm.Field name="yamlFile">
|
<importRecipeForm.Field name="recipeUploadFile">
|
||||||
{(field) => {
|
{(field) => {
|
||||||
const hasDeeplink = values.deeplink?.trim();
|
const hasDeeplink = values.deeplink?.trim();
|
||||||
const isDisabled = !!hasDeeplink;
|
const isDisabled = !!hasDeeplink;
|
||||||
@@ -328,22 +341,22 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
|||||||
return (
|
return (
|
||||||
<div className={isDisabled ? 'opacity-50' : ''}>
|
<div className={isDisabled ? 'opacity-50' : ''}>
|
||||||
<label
|
<label
|
||||||
htmlFor="import-yaml-file"
|
htmlFor="import-recipe-file"
|
||||||
className="block text-sm font-medium text-text-standard mb-3"
|
className="block text-sm font-medium text-text-standard mb-3"
|
||||||
>
|
>
|
||||||
Recipe YAML File
|
Recipe File
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
id="import-yaml-file"
|
id="import-recipe-file"
|
||||||
type="file"
|
type="file"
|
||||||
accept=".yaml,.yml"
|
accept=".yaml,.yml,.json"
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
handleYamlFileChange(e.target.files?.[0]);
|
handleRecipeUploadChange(e.target.files?.[0]);
|
||||||
}}
|
}}
|
||||||
onBlur={field.handleBlur}
|
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' : ''
|
isDisabled ? 'cursor-not-allowed' : ''
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
@@ -352,7 +365,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
|||||||
<p
|
<p
|
||||||
className={`text-xs mt-1 ${isDisabled ? 'text-gray-300' : 'text-text-muted'}`}
|
className={`text-xs mt-1 ${isDisabled ? 'text-gray-300' : 'text-text-muted'}`}
|
||||||
>
|
>
|
||||||
Upload a YAML file containing the recipe structure
|
Upload a YAML or JSON file containing the recipe structure
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -380,7 +393,8 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
|||||||
</importRecipeForm.Subscribe>
|
</importRecipeForm.Subscribe>
|
||||||
|
|
||||||
<p className="text-xs text-text-muted">
|
<p className="text-xs text-text-muted">
|
||||||
Ensure you review contents of YAML files before adding them to your goose interface.
|
Ensure you review contents of recipe files before adding them to your goose
|
||||||
|
interface.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<importRecipeForm.Field name="recipeName">
|
<importRecipeForm.Field name="recipeName">
|
||||||
@@ -481,7 +495,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
|||||||
{JSON.stringify(getRecipeJsonSchema(), null, 2)}
|
{JSON.stringify(getRecipeJsonSchema(), null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
<p className="mt-4 text-blue-700 text-sm">
|
<p className="mt-4 text-blue-700 text-sm">
|
||||||
Your YAML file should follow this structure. Required fields are: title,
|
Your YAML or JSON file should follow this structure. Required fields are: title,
|
||||||
description, and either instructions or prompt.
|
description, and either instructions or prompt.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user