Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
323 changes: 314 additions & 9 deletions ui/desktop/src/components/RecipesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ export default function RecipesView({ onLoadRecipe }: RecipesViewProps = {}) {
const [importGlobal, setImportGlobal] = useState(true);
const [importing, setImporting] = useState(false);

// Create Recipe state
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [createTitle, setCreateTitle] = useState('');
const [createDescription, setCreateDescription] = useState('');
const [createInstructions, setCreateInstructions] = useState('');
const [createPrompt, setCreatePrompt] = useState('');
const [createActivities, setCreateActivities] = useState('');
const [createRecipeName, setCreateRecipeName] = useState('');
const [createGlobal, setCreateGlobal] = useState(true);
const [creating, setCreating] = useState(false);

useEffect(() => {
loadSavedRecipes();
}, []);
Expand Down Expand Up @@ -223,6 +234,106 @@ export default function RecipesView({ onLoadRecipe }: RecipesViewProps = {}) {
}
};

// Create Recipe handlers
const handleCreateClick = () => {
// Reset form with example values
setCreateTitle('Python Development Assistant');
setCreateDescription(
'A helpful assistant for Python development tasks including coding, debugging, and code review.'
);
setCreateInstructions(`You are an expert Python developer assistant. Help users with:

1. Writing clean, efficient Python code
2. Debugging and troubleshooting issues
3. Code review and optimization suggestions
4. Best practices and design patterns
5. Testing and documentation

Always provide clear explanations and working code examples.

Parameters you can use:
- {{project_type}}: The type of Python project (web, data science, CLI, etc.)
- {{python_version}}: Target Python version`);
setCreatePrompt('What Python development task can I help you with today?');
setCreateActivities('coding, debugging, testing, documentation');
setCreateRecipeName('');
setCreateGlobal(true);
setShowCreateDialog(true);
};

const handleCreateRecipe = async () => {
if (
!createTitle.trim() ||
!createDescription.trim() ||
!createInstructions.trim() ||
!createRecipeName.trim()
) {
return;
}

setCreating(true);
try {
// Parse activities from comma-separated string
const activities = createActivities
.split(',')
.map((activity) => activity.trim())
.filter((activity) => activity.length > 0);

// Create the recipe object
const recipe: Recipe = {
title: createTitle.trim(),
description: createDescription.trim(),
instructions: createInstructions.trim(),
prompt: createPrompt.trim() || undefined,
activities: activities.length > 0 ? activities : undefined,
};

await saveRecipe(recipe, {
name: createRecipeName.trim(),
global: createGlobal,
});

// Reset dialog state
setShowCreateDialog(false);
setCreateTitle('');
setCreateDescription('');
setCreateInstructions('');
setCreatePrompt('');
setCreateActivities('');
setCreateRecipeName('');

await loadSavedRecipes();

toastSuccess({
title: createRecipeName.trim(),
msg: 'Recipe created successfully',
});
} catch (error) {
console.error('Failed to create recipe:', error);

toastError({
title: 'Create Failed',
msg: `Failed to create recipe: ${error instanceof Error ? error.message : 'Unknown error'}`,
traceback: error instanceof Error ? error.message : String(error),
});
} finally {
setCreating(false);
}
};

// Auto-generate recipe name when title changes
const handleCreateTitleChange = (value: string) => {
setCreateTitle(value);
if (value.trim() && !createRecipeName.trim()) {
const suggestedName = value
.toLowerCase()
.replace(/[^a-zA-Z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.trim();
setCreateRecipeName(suggestedName);
}
};

// Render a recipe item
const RecipeItem = ({ savedRecipe }: { savedRecipe: SavedRecipe }) => (
<Card className="py-2 px-4 mb-2 bg-background-default border-none hover:bg-background-muted cursor-pointer transition-all duration-150">
Expand Down Expand Up @@ -361,15 +472,26 @@ export default function RecipesView({ onLoadRecipe }: RecipesViewProps = {}) {
<div className="flex flex-col page-transition">
<div className="flex justify-between items-center mb-1">
<h1 className="text-4xl font-light">Recipes</h1>
<Button
onClick={handleImportClick}
variant="default"
size="sm"
className="flex items-center gap-2"
>
<Download className="w-4 h-4" />
Import Recipe
</Button>
<div className="flex gap-2">
<Button
onClick={handleCreateClick}
variant="outline"
size="sm"
className="flex items-center gap-2"
>
<FileText className="w-4 h-4" />
Create Recipe
</Button>
<Button
onClick={handleImportClick}
variant="default"
size="sm"
className="flex items-center gap-2"
>
<Download className="w-4 h-4" />
Import Recipe
</Button>
</div>
</div>
<p className="text-sm text-text-muted mb-1">
View and manage your saved recipes to quickly start new sessions with predefined
Expand Down Expand Up @@ -577,6 +699,189 @@ export default function RecipesView({ onLoadRecipe }: RecipesViewProps = {}) {
</div>
</div>
)}

{/* Create Recipe Dialog */}
{showCreateDialog && (
<div className="fixed inset-0 z-[300] flex items-center justify-center bg-black/50">
<div className="bg-background-default border border-border-subtle rounded-lg p-6 w-[700px] max-w-[90vw] max-h-[90vh] overflow-y-auto">
<h3 className="text-lg font-medium text-text-standard mb-4">Create New Recipe</h3>

<div className="space-y-4">
<div>
<label
htmlFor="create-title"
className="block text-sm font-medium text-text-standard mb-2"
>
Title <span className="text-red-500">*</span>
</label>
<input
id="create-title"
type="text"
value={createTitle}
onChange={(e) => handleCreateTitleChange(e.target.value)}
className="w-full p-3 border border-border-subtle rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Recipe title"
autoFocus
/>
</div>

<div>
<label
htmlFor="create-description"
className="block text-sm font-medium text-text-standard mb-2"
>
Description <span className="text-red-500">*</span>
</label>
<input
id="create-description"
type="text"
value={createDescription}
onChange={(e) => setCreateDescription(e.target.value)}
className="w-full p-3 border border-border-subtle rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Brief description of what this recipe does"
/>
</div>

<div>
<label
htmlFor="create-instructions"
className="block text-sm font-medium text-text-standard mb-2"
>
Instructions <span className="text-red-500">*</span>
</label>
<textarea
id="create-instructions"
value={createInstructions}
onChange={(e) => setCreateInstructions(e.target.value)}
className="w-full p-3 border border-border-subtle rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none font-mono text-sm"
placeholder="Detailed instructions for the AI agent..."
rows={8}
/>
<p className="text-xs text-text-muted mt-1">
Use {`{{parameter_name}}`} to define parameters that users can fill in
</p>
</div>

<div>
<label
htmlFor="create-prompt"
className="block text-sm font-medium text-text-standard mb-2"
>
Initial Prompt (Optional)
</label>
<textarea
id="create-prompt"
value={createPrompt}
onChange={(e) => setCreatePrompt(e.target.value)}
className="w-full p-3 border border-border-subtle rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
placeholder="First message to send when the recipe starts..."
rows={3}
/>
</div>

<div>
<label
htmlFor="create-activities"
className="block text-sm font-medium text-text-standard mb-2"
>
Activities (Optional)
</label>
<input
id="create-activities"
type="text"
value={createActivities}
onChange={(e) => setCreateActivities(e.target.value)}
className="w-full p-3 border border-border-subtle rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="coding, debugging, testing, documentation (comma-separated)"
/>
<p className="text-xs text-text-muted mt-1">
Comma-separated list of activities this recipe helps with
</p>
</div>

<div>
<label
htmlFor="create-recipe-name"
className="block text-sm font-medium text-text-standard mb-2"
>
Recipe Name <span className="text-red-500">*</span>
</label>
<input
id="create-recipe-name"
type="text"
value={createRecipeName}
onChange={(e) => setCreateRecipeName(e.target.value)}
className="w-full p-3 border border-border-subtle rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="File name for the recipe"
/>
</div>

<div>
<label className="block text-sm font-medium text-text-standard mb-2">
Save Location
</label>
<div className="space-y-2">
<label className="flex items-center">
<input
type="radio"
name="create-save-location"
checked={createGlobal}
onChange={() => setCreateGlobal(true)}
className="mr-2"
/>
<span className="text-sm text-text-standard">
Global - Available across all Goose sessions
</span>
</label>
<label className="flex items-center">
<input
type="radio"
name="create-save-location"
checked={!createGlobal}
onChange={() => setCreateGlobal(false)}
className="mr-2"
/>
<span className="text-sm text-text-standard">
Directory - Available in the working directory
</span>
</label>
</div>
</div>
</div>

<div className="flex justify-end space-x-3 mt-6">
<Button
onClick={() => {
setShowCreateDialog(false);
setCreateTitle('');
setCreateDescription('');
setCreateInstructions('');
setCreatePrompt('');
setCreateActivities('');
setCreateRecipeName('');
}}
variant="ghost"
disabled={creating}
>
Cancel
</Button>
<Button
onClick={handleCreateRecipe}
disabled={
!createTitle.trim() ||
!createDescription.trim() ||
!createInstructions.trim() ||
!createRecipeName.trim() ||
creating
}
variant="default"
>
{creating ? 'Creating...' : 'Create Recipe'}
</Button>
</div>
</div>
</div>
)}
</>
);
}