fix: make schedule section visible in recipe modal

- Remove recipeTitle requirement from schedule section
- Handle empty titles with 'untitled-recipe' default
- Change modal height from h-[90vh] to max-h-[85vh]
This commit is contained in:
spencrmartin
2025-10-22 14:00:05 -04:00
parent 6f237f6452
commit 12cad771ff
4 changed files with 293 additions and 382 deletions
@@ -372,7 +372,7 @@ export default function CreateEditRecipeModal({
return (
<div className="fixed inset-0 z-[400] flex items-center justify-center bg-black/50">
<div className="bg-background-default border border-borderSubtle rounded-lg w-[90vw] max-w-4xl h-[90vh] flex flex-col">
<div className="bg-background-default border border-borderSubtle rounded-lg w-[90vw] max-w-4xl max-h-[85vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-borderSubtle">
<div className="flex items-center gap-3">
@@ -381,12 +381,12 @@ export default function CreateEditRecipeModal({
</div>
<div>
<h1 className="text-xl font-medium text-textProminent">
{isCreateMode ? 'Create Recipe' : 'View/edit recipe'}
{isCreateMode ? 'Create Command' : 'View/edit command'}
</h1>
<p className="text-textSubtle text-sm">
{isCreateMode
? 'Create a new recipe to define agent behavior and capabilities.'
: "You can edit the recipe below to change the agent's behavior in a new session."}
? 'Create a new command to define agent behavior and capabilities.'
: "You can edit the command below to change the agent's behavior in a new session."}
</p>
</div>
</div>
@@ -401,17 +401,15 @@ export default function CreateEditRecipeModal({
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto px-6 py-4">
<RecipeFormFields form={form} />
<div className="flex-1 overflow-y-auto px-6 py-4 pb-6">
<RecipeFormFields
form={form}
recipeTitle={title}
scheduleConfig={scheduleConfig || undefined}
onScheduleConfigChange={setScheduleConfig}
/>
{/* Schedule Configuration Section */}
<div className="mt-6">
<ScheduleConfigSection
recipeTitle={title}
value={scheduleConfig || undefined}
onChange={setScheduleConfig}
/>
</div>
{/* Deep Link Display */}
{requiredFieldsAreFilled() && (
@@ -480,7 +478,7 @@ export default function CreateEditRecipeModal({
className="inline-flex items-center justify-center gap-2 px-4 py-2"
>
<Save className="w-4 h-4" />
{isSaving ? 'Saving...' : 'Save Recipe'}
{isSaving ? 'Saving...' : 'Save Command'}
</Button>
<Button
onClick={handleSaveAndRunRecipeClick}
@@ -490,7 +488,7 @@ export default function CreateEditRecipeModal({
className="inline-flex items-center justify-center gap-2 px-4 py-2"
>
<Play className="w-4 h-4" />
{isSaving ? 'Saving...' : 'Save & Run Recipe'}
{isSaving ? 'Saving...' : 'Save & Run Command'}
</Button>
</div>
</div>
@@ -1,27 +1,29 @@
import React, { useState } from 'react';
import { Parameter } from '../../../recipe';
import { ChevronDown, ChevronUp } from 'lucide-react';
import ParameterInput from '../../parameter/ParameterInput';
import RecipeActivityEditor from '../RecipeActivityEditor';
import JsonSchemaEditor from './JsonSchemaEditor';
import InstructionsEditor from './InstructionsEditor';
import { Button } from '../../ui/button';
import { RecipeFormApi } from './recipeFormSchema';
import { ScheduleConfigSection, ScheduleConfig } from '../../shared/ScheduleConfigSection';
// Type for field API to avoid linting issues - use any to bypass complex type constraints
// Type for field API to avoid linting issues
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type FormFieldApi<_T = any> = any;
interface RecipeFormFieldsProps {
// Form instance from parent
form: RecipeFormApi;
// Event handlers
onTitleChange?: (value: string) => void;
onDescriptionChange?: (value: string) => void;
onInstructionsChange?: (value: string) => void;
onPromptChange?: (value: string) => void;
onJsonSchemaChange?: (value: string) => void;
// Schedule configuration props
recipeTitle?: string;
scheduleConfig?: ScheduleConfig;
onScheduleConfigChange?: (config: ScheduleConfig | null) => void;
}
export const extractTemplateVariables = (content: string): string[] => {
@@ -31,17 +33,13 @@ export const extractTemplateVariables = (content: string): string[] => {
while ((match = templateVarRegex.exec(content)) !== null) {
const variable = match[1].trim();
if (variable && !variables.includes(variable)) {
// Filter out complex variables that aren't valid parameter names
// This matches the backend logic in filter_complex_variables()
const validVarRegex = /^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*$/;
if (validVarRegex.test(variable)) {
variables.push(variable);
}
}
}
return variables;
};
@@ -52,32 +50,30 @@ export function RecipeFormFields({
onInstructionsChange,
onPromptChange,
onJsonSchemaChange,
recipeTitle,
scheduleConfig,
onScheduleConfigChange,
}: RecipeFormFieldsProps) {
const [showJsonSchemaEditor, setShowJsonSchemaEditor] = useState(false);
// Advanced configuration state
const [showAdvanced, setShowAdvanced] = useState(false);
// Other states
const [showInstructionsEditor, setShowInstructionsEditor] = useState(false);
const [newParameterName, setNewParameterName] = useState('');
const [expandedParameters, setExpandedParameters] = useState<Set<string>>(new Set());
// Force re-render when instructions, prompt, or activities change
const [_forceRender, setForceRender] = useState(0);
React.useEffect(() => {
return form.store.subscribe(() => {
// Force re-render when any form field changes to update parameter usage indicators
setForceRender((prev) => prev + 1);
});
}, [form.store]);
const parseParametersFromInstructions = React.useCallback(
(instructions: string, prompt?: string, activities?: string[]): Parameter[] => {
(instructions: string, prompt?: string): Parameter[] => {
const instructionVars = extractTemplateVariables(instructions);
const promptVars = prompt ? extractTemplateVariables(prompt) : [];
const activityVars = activities
? activities.flatMap((activity) => extractTemplateVariables(activity))
: [];
// Combine and deduplicate
const allVars = [...new Set([...instructionVars, ...promptVars, ...activityVars])];
const allVars = [...new Set([...instructionVars, ...promptVars])];
return allVars.map((key: string) => ({
key,
@@ -89,76 +85,36 @@ export function RecipeFormFields({
[]
);
// Function to update parameters based on current field values
const updateParametersFromFields = React.useCallback(() => {
const currentValues = form.state.values;
const { instructions, prompt, activities, parameters: currentParams } = currentValues;
const { instructions, prompt, parameters: currentParams } = currentValues;
const newParams = parseParametersFromInstructions(instructions, prompt, activities);
// Separate manually added parameters (those not found in instructions/prompt/activities)
const newParams = parseParametersFromInstructions(instructions, prompt);
const manualParams = currentParams.filter((param: Parameter) => {
// Only keep manual params that have a valid key and are not found in the parsed params
return (
param.key && param.key.trim() && !newParams.some((newParam) => newParam.key === param.key)
);
return !newParams.some((np) => np.key === param.key);
});
// Combine parsed parameters with manually added ones, filtering out empty ones
const combinedParams = [
...newParams.map((newParam) => {
const existing = currentParams.find((cp: Parameter) => cp.key === newParam.key);
return existing ? { ...existing } : newParam;
}),
...manualParams,
].filter((param: Parameter) => param.key && param.key.trim()) as Parameter[];
// Only update if parameters actually changed
const currentParamKeys = currentParams.map((p: Parameter) => p.key).sort();
const newParamKeys = combinedParams.map((p) => p.key).sort();
if (JSON.stringify(currentParamKeys) !== JSON.stringify(newParamKeys)) {
form.setFieldValue('parameters', combinedParams);
}
const allParams = [...newParams, ...manualParams];
form.setFieldValue('parameters', allParams);
}, [form, parseParametersFromInstructions]);
const isParameterUsed = (
paramKey: string,
instructions: string,
prompt?: string,
activities?: string[]
): boolean => {
const regex = new RegExp(
`\\{\\{\\s*${paramKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\}\\}`,
'g'
);
const usedInInstructions = regex.test(instructions);
const usedInPrompt = prompt ? regex.test(prompt) : false;
const usedInActivities = activities
? activities.some((activity) => {
// For activities, we need to check the full activity string, including message: prefixes
return regex.test(activity);
})
: false;
return usedInInstructions || usedInPrompt || usedInActivities;
};
React.useEffect(() => {
updateParametersFromFields();
}, [updateParametersFromFields]);
return (
<div className="space-y-4" data-testid="recipe-form">
{/* Title Field */}
<div className="space-y-4">
{/* REQUIRED: Title Field */}
<form.Field name="title">
{(field: FormFieldApi<string>) => (
<div>
<label
htmlFor="recipe-title"
className="block text-sm font-medium text-text-standard mb-2"
>
<label htmlFor="recipe-title" className="block text-sm font-medium text-text-standard mb-2">
Title <span className="text-red-500">*</span>
</label>
<input
id="recipe-title"
type="text"
value={field.state.value}
value={field.state.value || ''}
onChange={(e) => {
field.handleChange(e.target.value);
onTitleChange?.(e.target.value);
@@ -167,7 +123,7 @@ export function RecipeFormFields({
className={`w-full p-3 border rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 ${
field.state.meta.errors.length > 0 ? 'border-red-500' : 'border-border-subtle'
}`}
placeholder="Recipe title"
placeholder="Give your command a descriptive name"
data-testid="title-input"
/>
{field.state.meta.errors.length > 0 && (
@@ -177,314 +133,264 @@ export function RecipeFormFields({
)}
</form.Field>
{/* Description Field */}
<form.Field name="description">
{(field: FormFieldApi<string>) => (
<div>
<label
htmlFor="recipe-description"
className="block text-sm font-medium text-text-standard mb-2"
>
Description <span className="text-red-500">*</span>
</label>
<input
id="recipe-description"
type="text"
value={field.state.value}
onChange={(e) => {
field.handleChange(e.target.value);
onDescriptionChange?.(e.target.value);
}}
onBlur={field.handleBlur}
className={`w-full p-3 border rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 ${
field.state.meta.errors.length > 0 ? 'border-red-500' : 'border-border-subtle'
}`}
placeholder="Brief description of what this recipe does"
data-testid="description-input"
/>
{field.state.meta.errors.length > 0 && (
<p className="text-red-500 text-sm mt-1">{field.state.meta.errors[0]}</p>
)}
</div>
)}
</form.Field>
{/* Instructions Field */}
{/* REQUIRED: Instructions Field */}
<form.Field name="instructions">
{(field: FormFieldApi<string>) => (
<div>
<div className="flex items-center justify-between mb-2">
<label
htmlFor="recipe-instructions"
className="block text-sm font-medium text-text-standard"
>
<label htmlFor="recipe-instructions" className="block text-sm font-medium text-text-standard">
Instructions <span className="text-red-500">*</span>
</label>
<Button
type="button"
onClick={() => setShowInstructionsEditor(true)}
variant="outline"
onClick={() => setShowInstructionsEditor(!showInstructionsEditor)}
variant="ghost"
size="sm"
className="text-xs"
>
Open Editor
</Button>
</div>
<textarea
id="recipe-instructions"
value={field.state.value}
onChange={(e) => {
field.handleChange(e.target.value);
onInstructionsChange?.(e.target.value);
}}
onBlur={() => {
field.handleBlur();
updateParametersFromFields();
}}
className={`w-full p-3 border rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none font-mono text-sm ${
field.state.meta.errors.length > 0 ? 'border-red-500' : 'border-border-subtle'
}`}
placeholder="Detailed instructions for the AI, hidden from the user..."
rows={8}
data-testid="instructions-input"
/>
<p className="text-xs text-text-muted mt-1">
Use {`{{parameter_name}}`} to define parameters that users can fill in
</p>
{field.state.meta.errors.length > 0 && (
<p className="text-red-500 text-sm mt-1">{field.state.meta.errors[0]}</p>
)}
{/* Instructions Editor Modal */}
<InstructionsEditor
isOpen={showInstructionsEditor}
onClose={() => setShowInstructionsEditor(false)}
value={field.state.value}
onChange={(value) => {
field.handleChange(value);
onInstructionsChange?.(value);
updateParametersFromFields();
}}
error={field.state.meta.errors.length > 0 ? field.state.meta.errors[0] : undefined}
/>
</div>
)}
</form.Field>
{/* Initial Prompt Field */}
<form.Field name="prompt">
{(field: FormFieldApi<string | undefined>) => (
<div>
<label
htmlFor="recipe-prompt"
className="block text-sm font-medium text-text-standard mb-2"
>
Initial Prompt
</label>
<p className="text-xs text-text-muted mt-2 mb-2">
(Optional - Instructions or Prompt are required)
</p>
<textarea
id="recipe-prompt"
value={field.state.value || ''}
onChange={(e) => {
field.handleChange(e.target.value);
onPromptChange?.(e.target.value);
}}
onBlur={() => {
field.handleBlur();
updateParametersFromFields();
}}
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="Pre-filled prompt when the recipe starts..."
rows={3}
data-testid="prompt-input"
/>
</div>
)}
</form.Field>
{/* Activities Field */}
<form.Field name="activities">
{(field: FormFieldApi<string[]>) => (
<div>
<RecipeActivityEditor
activities={field.state.value}
setActivities={(activities) => field.handleChange(activities)}
onBlur={updateParametersFromFields}
/>
</div>
)}
</form.Field>
{/* Parameters Field */}
<form.Field name="parameters">
{(field: FormFieldApi<Parameter[]>) => {
const handleAddParameter = () => {
if (newParameterName.trim()) {
const newParam: Parameter = {
key: newParameterName.trim(),
description: `Enter value for ${newParameterName.trim()}`,
input_type: 'string',
requirement: 'required',
};
field.handleChange([...field.state.value, newParam]);
setNewParameterName('');
// Expand the newly added parameter by default
setExpandedParameters((prev) => {
const newSet = new Set(prev);
newSet.add(newParam.key);
return newSet;
});
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddParameter();
}
};
const handleDeleteParameter = (parameterKey: string) => {
const updatedParams = field.state.value.filter(
(param: Parameter) => param.key !== parameterKey
);
field.handleChange(updatedParams);
// Remove from expanded set if it was expanded
setExpandedParameters((prev) => {
const newSet = new Set(prev);
newSet.delete(parameterKey);
return newSet;
});
};
const handleToggleExpanded = (parameterKey: string) => {
setExpandedParameters((prev) => {
const newSet = new Set(prev);
if (newSet.has(parameterKey)) {
newSet.delete(parameterKey);
} else {
newSet.add(parameterKey);
}
return newSet;
});
};
return (
<div>
<label className="block text-md text-textProminent mb-2 font-bold">Parameters</label>
<p className="text-textSubtle text-sm space-y-2 pb-4">
Parameters will be automatically detected from {`{{parameter_name}}`} syntax in
instructions/prompt/activities or you can manually add them below.
</p>
{/* Add Parameter Input - Always Visible */}
<div className="flex gap-2 mb-4">
<input
type="text"
value={newParameterName}
onChange={(e) => setNewParameterName(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Enter parameter name..."
className="flex-1 px-3 py-2 border border-border-subtle rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
/>
<button
type="button"
onClick={handleAddParameter}
disabled={!newParameterName.trim()}
className="px-4 py-2 bg-blue-500 text-white rounded-lg text-sm hover:bg-blue-600 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"
>
Add parameter
</button>
</div>
{field.state.value.length > 0 &&
field.state.value
.filter((parameter: Parameter) => parameter.key && parameter.key.trim()) // Filter out empty parameters
.map((parameter: Parameter) => {
const currentValues = form.state.values;
const isUnused = !isParameterUsed(
parameter.key,
currentValues.instructions,
currentValues.prompt,
currentValues.activities
);
return (
<ParameterInput
key={parameter.key}
parameter={parameter}
isUnused={isUnused}
isExpanded={expandedParameters.has(parameter.key)}
onToggleExpanded={handleToggleExpanded}
onDelete={handleDeleteParameter}
onChange={(name, value) => {
const updatedParams = field.state.value.map((param: Parameter) =>
param.key === name ? { ...param, ...value } : param
);
field.handleChange(updatedParams);
}}
/>
);
})}
</div>
);
}}
</form.Field>
{/* JSON Schema Field */}
<form.Field name="jsonSchema">
{(field: FormFieldApi<string | undefined>) => (
<div>
<label className="block text-md text-textProminent mb-2 font-bold">
Response JSON Schema
</label>
<p className="text-textSubtle text-sm space-y-2 pb-4">
Define the expected structure of the AI's response using JSON Schema format
</p>
<div className="flex items-center justify-between mb-2">
<Button
type="button"
onClick={() => setShowJsonSchemaEditor(true)}
variant="outline"
size="sm"
className="text-xs"
>
Open Editor
{showInstructionsEditor ? 'Hide' : 'Show'} Editor
</Button>
</div>
{field.state.value && field.state.value.trim() && (
<div
className={`border rounded-lg p-3 bg-background-muted ${
{showInstructionsEditor ? (
<InstructionsEditor
value={field.state.value || ''}
onChange={(value) => {
field.handleChange(value);
onInstructionsChange?.(value);
}}
/>
) : (
<textarea
id="recipe-instructions"
value={field.state.value || ''}
onChange={(e) => {
field.handleChange(e.target.value);
onInstructionsChange?.(e.target.value);
}}
onBlur={field.handleBlur}
className={`w-full p-3 border rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none font-mono text-sm ${
field.state.meta.errors.length > 0 ? 'border-red-500' : 'border-border-subtle'
}`}
>
<pre className="text-xs font-mono text-text-standard whitespace-pre-wrap break-words max-h-32 overflow-y-auto">
{field.state.value}
</pre>
</div>
placeholder="Detailed instructions for the AI, hidden from the user..."
rows={8}
data-testid="instructions-input"
/>
)}
{field.state.meta.errors.length > 0 && (
<p className="text-red-500 text-sm mt-1">{field.state.meta.errors[0]}</p>
)}
{/* JSON Schema Editor Modal */}
<JsonSchemaEditor
isOpen={showJsonSchemaEditor}
onClose={() => setShowJsonSchemaEditor(false)}
value={field.state.value || ''}
onChange={(value) => {
field.handleChange(value);
onJsonSchemaChange?.(value);
}}
error={field.state.meta.errors.length > 0 ? field.state.meta.errors[0] : undefined}
/>
<p className="text-text-muted text-xs mt-1">
Use {`{{variable_name}}`} syntax to create parameters
</p>
</div>
)}
</form.Field>
{/* SCHEDULE CONFIGURATION - Moved above Advanced Configuration */}
{onScheduleConfigChange && (
<ScheduleConfigSection
recipeTitle={recipeTitle}
value={scheduleConfig}
onChange={onScheduleConfigChange}
/>
)}
{/* ADVANCED CONFIGURATION - Collapsible Section */}
<div className="border border-border-subtle rounded-lg p-4">
<button
type="button"
onClick={() => setShowAdvanced(!showAdvanced)}
className="w-full flex items-center justify-between"
>
<div className="flex items-center gap-2">
<h3 className="text-base font-semibold text-text-prominent">
Advanced Configuration
</h3>
<span className="text-xs text-text-muted">(Optional)</span>
</div>
{showAdvanced ? (
<ChevronUp className="w-5 h-5 text-text-muted" />
) : (
<ChevronDown className="w-5 h-5 text-text-muted" />
)}
</button>
{showAdvanced && (
<div className="mt-6 space-y-6 animate-in fade-in slide-in-from-top-2 duration-200">
{/* Description Field */}
<form.Field name="description">
{(field: FormFieldApi<string>) => (
<div>
<label htmlFor="recipe-description" className="block text-sm font-medium text-text-standard mb-2">
Description
</label>
<textarea
id="recipe-description"
value={field.state.value || ''}
onChange={(e) => {
field.handleChange(e.target.value);
onDescriptionChange?.(e.target.value);
}}
onBlur={field.handleBlur}
className={`w-full p-3 border rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none ${
field.state.meta.errors.length > 0 ? 'border-red-500' : 'border-border-subtle'
}`}
placeholder="Brief description of what this command does"
rows={3}
data-testid="description-input"
/>
{field.state.meta.errors.length > 0 && (
<p className="text-red-500 text-sm mt-1">{field.state.meta.errors[0]}</p>
)}
</div>
)}
</form.Field>
{/* Initial Prompt Field */}
<form.Field name="prompt">
{(field: FormFieldApi<string | undefined>) => (
<div>
<label htmlFor="recipe-prompt" className="block text-sm font-medium text-text-standard mb-2">
Initial Prompt
</label>
<textarea
id="recipe-prompt"
value={field.state.value || ''}
onChange={(e) => {
field.handleChange(e.target.value);
onPromptChange?.(e.target.value);
}}
onBlur={field.handleBlur}
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="Pre-filled prompt when the command starts..."
rows={3}
data-testid="prompt-input"
/>
<p className="text-text-muted text-xs mt-1">
This message will appear in the chat when the command starts
</p>
</div>
)}
</form.Field>
{/* Parameters Field */}
<form.Field name="parameters">
{(field: FormFieldApi<Parameter[]>) => {
const handleAddParameter = () => {
if (newParameterName.trim()) {
const newParam: Parameter = {
key: newParameterName.trim(),
description: `Enter value for ${newParameterName.trim()}`,
requirement: 'required',
input_type: 'string',
};
field.handleChange([...field.state.value, newParam]);
setNewParameterName('');
}
};
const handleRemoveParameter = (index: number) => {
const updated = field.state.value.filter((_: Parameter, i: number) => i !== index);
field.handleChange(updated);
};
const handleUpdateParameter = (index: number, updated: Parameter) => {
const newParams = [...field.state.value];
newParams[index] = updated;
field.handleChange(newParams);
};
const toggleExpanded = (key: string) => {
const newExpanded = new Set(expandedParameters);
if (newExpanded.has(key)) {
newExpanded.delete(key);
} else {
newExpanded.add(key);
}
setExpandedParameters(newExpanded);
};
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddParameter();
}
};
return (
<div>
<label className="block text-sm font-medium text-text-standard mb-2">
Parameters
</label>
<p className="text-text-muted text-sm mb-4">
Parameters will be automatically detected from {`{{parameter_name}}`} syntax in
instructions/prompt or you can manually add them below.
</p>
{/* Add parameter input */}
<div className="flex gap-2 mb-4">
<input
type="text"
value={newParameterName}
onChange={(e) => setNewParameterName(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Enter parameter name..."
className="flex-1 px-3 py-2 border border-border-subtle rounded-lg bg-background-default text-text-standard focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
/>
<button
type="button"
onClick={handleAddParameter}
disabled={!newParameterName.trim()}
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed text-sm font-medium transition-colors"
>
Add
</button>
</div>
{/* Parameters list */}
{field.state.value.length > 0 && (
<div className="space-y-2">
{field.state.value.map((param: Parameter, index: number) => (
<ParameterInput
key={param.key}
parameter={param}
isExpanded={expandedParameters.has(param.key)}
onToggleExpand={() => toggleExpanded(param.key)}
onUpdate={(updated) => handleUpdateParameter(index, updated)}
onRemove={() => handleRemoveParameter(index)}
/>
))}
</div>
)}
</div>
);
}}
</form.Field>
{/* JSON Schema Field */}
<form.Field name="jsonSchema">
{(field: FormFieldApi<string | undefined>) => (
<div>
<label className="block text-sm font-medium text-text-standard mb-2">
Response JSON Schema
</label>
<p className="text-text-muted text-sm mb-4">
Define a JSON schema to structure the AI's response format.
</p>
<JsonSchemaEditor
value={field.state.value || ''}
onChange={(value) => {
field.handleChange(value);
onJsonSchemaChange?.(value);
}}
/>
</div>
)}
</form.Field>
</div>
)}
</div>
</div>
);
}
@@ -15,26 +15,30 @@ export type RecipeParameter = z.infer<typeof parameterSchema>;
// Main recipe form schema
export const recipeFormSchema = z.object({
// REQUIRED FIELDS
title: z
.string()
.min(1, 'Title is required')
.min(3, 'Title must be at least 3 characters')
.max(100, 'Title must be 100 characters or less'),
description: z
.string()
.min(1, 'Description is required')
.min(10, 'Description must be at least 10 characters')
.max(500, 'Description must be 500 characters or less'),
instructions: z
.string()
.min(1, 'Instructions are required')
.min(20, 'Instructions must be at least 20 characters'),
prompt: z.string().optional(),
// OPTIONAL FIELDS
description: z
.string()
.optional()
.refine((val) => !val || val.length >= 10, {
message: 'Description must be at least 10 characters if provided',
})
.refine((val) => !val || val.length <= 500, {
message: 'Description must be 500 characters or less',
}),
activities: z.array(z.string()).default([]),
prompt: z.string().optional(),
parameters: z.array(parameterSchema).default([]),
@@ -34,6 +34,9 @@ export const ScheduleConfigSection: React.FC<ScheduleConfigSectionProps> = ({
}) => {
// Generate schedule ID from recipe title
const generateScheduleId = (title: string): string => {
if (!title || title.trim() === '') {
return 'untitled-recipe';
}
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')