Hide advanced recipe options under expandable content (#6021)
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { useForm } from '@tanstack/react-form';
|
import { useForm } from '@tanstack/react-form';
|
||||||
import { Recipe, generateDeepLink, Parameter } from '../../recipe';
|
import { Recipe, generateDeepLink, Parameter } from '../../recipe';
|
||||||
|
import { Check, ExternalLink, Play, Save, X } from 'lucide-react';
|
||||||
import { Geese } from '../icons/Geese';
|
import { Geese } from '../icons/Geese';
|
||||||
import Copy from '../icons/Copy';
|
import Copy from '../icons/Copy';
|
||||||
import { Check, Save, X, Play } from 'lucide-react';
|
|
||||||
import { ExtensionConfig } from '../ConfigContext';
|
import { ExtensionConfig } from '../ConfigContext';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
|
|
||||||
@@ -342,8 +342,17 @@ export default function CreateEditRecipeModal({
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="text-textSubtle text-sm">
|
<p className="text-textSubtle text-sm">
|
||||||
{isCreateMode
|
{isCreateMode
|
||||||
? 'Create a new recipe to define agent behavior and capabilities.'
|
? 'Create a new recipe to define agent behavior and capabilities for reusable chat sessions.'
|
||||||
: "You can edit the recipe below to change the agent's behavior in a new session."}
|
: "You can edit the recipe below to change the agent's behavior in a new session."}{' '}
|
||||||
|
<a
|
||||||
|
href="https://block.github.io/goose/docs/guides/recipes/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1 text-blue-500 hover:text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ export default function CreateRecipeFromSessionModal({
|
|||||||
className="text-lg font-medium text-textProminent"
|
className="text-lg font-medium text-textProminent"
|
||||||
data-testid="analyzing-title"
|
data-testid="analyzing-title"
|
||||||
>
|
>
|
||||||
Analyzing your conversation...
|
Analyzing your conversation
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-textSubtle text-center max-w-md" data-testid="analysis-stage">
|
<div className="text-textSubtle text-center max-w-md" data-testid="analysis-stage">
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ Use {{parameter_name}} syntax for any user-provided values.`;
|
|||||||
className={`w-full h-full min-h-[500px] 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 ${
|
className={`w-full h-full min-h-[500px] 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 ${
|
||||||
error ? 'border-red-500' : 'border-border-subtle'
|
error ? 'border-red-500' : 'border-border-subtle'
|
||||||
}`}
|
}`}
|
||||||
placeholder="Detailed instructions for the AI, hidden from the user..."
|
placeholder="Detailed instructions for the AI, hidden from the user"
|
||||||
/>
|
/>
|
||||||
{error && <p className="text-red-500 text-sm mt-2">{error}</p>}
|
{error && <p className="text-red-500 text-sm mt-2">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Parameter } from '../../../recipe';
|
import { Parameter } from '../../../recipe';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
import ParameterInput from '../../parameter/ParameterInput';
|
import ParameterInput from '../../parameter/ParameterInput';
|
||||||
import RecipeActivityEditor from '../RecipeActivityEditor';
|
import RecipeActivityEditor from '../RecipeActivityEditor';
|
||||||
import JsonSchemaEditor from './JsonSchemaEditor';
|
import JsonSchemaEditor from './JsonSchemaEditor';
|
||||||
import InstructionsEditor from './InstructionsEditor';
|
import InstructionsEditor from './InstructionsEditor';
|
||||||
import { Button } from '../../ui/button';
|
import { Button } from '../../ui/button';
|
||||||
import { RecipeFormApi } from './recipeFormSchema';
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../ui/collapsible';
|
||||||
|
import { RecipeFormApi, RecipeFormData } from './recipeFormSchema';
|
||||||
|
|
||||||
// Type for field API to avoid linting issues - use any to bypass complex type constraints
|
// Type for field API to avoid linting issues - use any to bypass complex type constraints
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -143,6 +145,15 @@ export function RecipeFormFields({
|
|||||||
return usedInInstructions || usedInPrompt || usedInActivities;
|
return usedInInstructions || usedInPrompt || usedInActivities;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkHasAdvancedData = React.useCallback((values: RecipeFormData) => {
|
||||||
|
const hasActivities = Boolean(values.activities && values.activities.length > 0);
|
||||||
|
const hasParameters = Boolean(values.parameters && values.parameters.length > 0);
|
||||||
|
const hasJsonSchema = Boolean(values.jsonSchema && values.jsonSchema.trim());
|
||||||
|
return hasActivities || hasParameters || hasJsonSchema;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [advancedOpen, setAdvancedOpen] = useState(() => checkHasAdvancedData(form.state.values));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4" data-testid="recipe-form">
|
<div className="space-y-4" data-testid="recipe-form">
|
||||||
{/* Title Field */}
|
{/* Title Field */}
|
||||||
@@ -244,12 +255,13 @@ 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 resize-none font-mono text-sm ${
|
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'
|
field.state.meta.errors.length > 0 ? 'border-red-500' : 'border-border-subtle'
|
||||||
}`}
|
}`}
|
||||||
placeholder="Detailed instructions for the AI, hidden from the user..."
|
placeholder="Detailed instructions for the AI, hidden from the user"
|
||||||
rows={8}
|
rows={8}
|
||||||
data-testid="instructions-input"
|
data-testid="instructions-input"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-text-muted mt-1">
|
<p className="text-xs text-text-muted mt-1">
|
||||||
Use {`{{parameter_name}}`} to define parameters that users can fill in
|
Use {`{{parameter_name}}`} to define parameters that can be filled in when running the
|
||||||
|
recipe.
|
||||||
</p>
|
</p>
|
||||||
{field.state.meta.errors.length > 0 && (
|
{field.state.meta.errors.length > 0 && (
|
||||||
<p className="text-red-500 text-sm mt-1">{field.state.meta.errors[0]}</p>
|
<p className="text-red-500 text-sm mt-1">{field.state.meta.errors[0]}</p>
|
||||||
@@ -296,7 +308,7 @@ export function RecipeFormFields({
|
|||||||
updateParametersFromFields();
|
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"
|
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..."
|
placeholder="Pre-filled prompt when the recipe starts"
|
||||||
rows={3}
|
rows={3}
|
||||||
data-testid="prompt-input"
|
data-testid="prompt-input"
|
||||||
/>
|
/>
|
||||||
@@ -304,187 +316,206 @@ export function RecipeFormFields({
|
|||||||
)}
|
)}
|
||||||
</form.Field>
|
</form.Field>
|
||||||
|
|
||||||
{/* Activities Field */}
|
{/* Advanced Section - Collapsible */}
|
||||||
<form.Field name="activities">
|
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen} className="mt-6">
|
||||||
{(field: FormFieldApi<string[]>) => (
|
<CollapsibleTrigger className="flex items-baseline gap-2 w-full py-3 px-4 bg-bgSubtle hover:bg-bgSubtle/80 rounded-lg transition-colors border border-borderSubtle">
|
||||||
<div>
|
<ChevronDown
|
||||||
<RecipeActivityEditor
|
className={`w-4 h-4 text-textSubtle transition-transform duration-200 flex-shrink-0 relative top-0.5 ${
|
||||||
activities={field.state.value}
|
advancedOpen ? 'rotate-0' : '-rotate-90'
|
||||||
setActivities={(activities) => field.handleChange(activities)}
|
}`}
|
||||||
onBlur={updateParametersFromFields}
|
/>
|
||||||
/>
|
<span className="text-sm font-medium text-textStandard">Advanced Options</span>
|
||||||
</div>
|
<span className="text-xs text-textSubtle">Activities, parameters, response schema</span>
|
||||||
)}
|
</CollapsibleTrigger>
|
||||||
</form.Field>
|
|
||||||
|
|
||||||
{/* Parameters Field */}
|
<CollapsibleContent className="mt-4 space-y-4 pl-6 border-l-2 border-borderSubtle ml-2">
|
||||||
<form.Field name="parameters">
|
{/* Activities Field */}
|
||||||
{(field: FormFieldApi<Parameter[]>) => {
|
<form.Field name="activities">
|
||||||
const handleAddParameter = () => {
|
{(field: FormFieldApi<string[]>) => (
|
||||||
if (newParameterName.trim()) {
|
<div>
|
||||||
const newParam: Parameter = {
|
<RecipeActivityEditor
|
||||||
key: newParameterName.trim(),
|
activities={field.state.value}
|
||||||
description: `Enter value for ${newParameterName.trim()}`,
|
setActivities={(activities) => field.handleChange(activities)}
|
||||||
input_type: 'string',
|
onBlur={updateParametersFromFields}
|
||||||
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
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{field.state.value && field.state.value.trim() && (
|
|
||||||
<div
|
|
||||||
className={`border rounded-lg p-3 bg-background-muted ${
|
|
||||||
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
{field.state.meta.errors.length > 0 && (
|
{/* Parameters Field */}
|
||||||
<p className="text-red-500 text-sm mt-1">{field.state.meta.errors[0]}</p>
|
<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
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{field.state.value && field.state.value.trim() && (
|
||||||
|
<div
|
||||||
|
className={`border rounded-lg p-3 bg-background-muted ${
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</form.Field>
|
||||||
{/* JSON Schema Editor Modal */}
|
</CollapsibleContent>
|
||||||
<JsonSchemaEditor
|
</Collapsible>
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ import { useForm } from '@tanstack/react-form';
|
|||||||
import { RecipeFormFields, extractTemplateVariables } from '../RecipeFormFields';
|
import { RecipeFormFields, extractTemplateVariables } from '../RecipeFormFields';
|
||||||
import { type RecipeFormData } from '../recipeFormSchema';
|
import { type RecipeFormData } from '../recipeFormSchema';
|
||||||
|
|
||||||
|
const expandAdvancedSection = async (user: ReturnType<typeof userEvent.setup>) => {
|
||||||
|
const advancedTrigger = screen.getByRole('button', { name: /advanced options/i });
|
||||||
|
const activitiesField = screen.queryByText('Activities');
|
||||||
|
if (!activitiesField) {
|
||||||
|
await user.click(advancedTrigger);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
describe('RecipeFormFields', () => {
|
describe('RecipeFormFields', () => {
|
||||||
const useTestForm = (initialValues?: Partial<RecipeFormData>) => {
|
const useTestForm = (initialValues?: Partial<RecipeFormData>) => {
|
||||||
const defaultValues: RecipeFormData = {
|
const defaultValues: RecipeFormData = {
|
||||||
@@ -48,13 +56,19 @@ describe('RecipeFormFields', () => {
|
|||||||
expect(screen.getByLabelText(/title/i)).toBeInTheDocument();
|
expect(screen.getByLabelText(/title/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders required form fields', () => {
|
it('renders required form fields', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
render(<TestWrapper />);
|
render(<TestWrapper />);
|
||||||
|
|
||||||
expect(screen.getByLabelText(/title/i)).toBeInTheDocument();
|
expect(screen.getByLabelText(/title/i)).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText(/description/i)).toBeInTheDocument();
|
expect(screen.getByLabelText(/description/i)).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText(/instructions/i)).toBeInTheDocument();
|
expect(screen.getByLabelText(/instructions/i)).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText(/prompt/i)).toBeInTheDocument();
|
expect(screen.getByLabelText(/prompt/i)).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByRole('button', { name: /advanced options/i })).toBeInTheDocument();
|
||||||
|
|
||||||
|
await expandAdvancedSection(user);
|
||||||
|
|
||||||
expect(screen.getAllByText(/activities/i)[0]).toBeInTheDocument();
|
expect(screen.getAllByText(/activities/i)[0]).toBeInTheDocument();
|
||||||
expect(screen.getAllByText(/parameters/i)[0]).toBeInTheDocument();
|
expect(screen.getAllByText(/parameters/i)[0]).toBeInTheDocument();
|
||||||
expect(screen.getByText(/response json schema/i)).toBeInTheDocument();
|
expect(screen.getByText(/response json schema/i)).toBeInTheDocument();
|
||||||
@@ -99,9 +113,12 @@ describe('RecipeFormFields', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Parameter Management', () => {
|
describe('Parameter Management', () => {
|
||||||
it('shows parameter input section', () => {
|
it('shows parameter input section', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
render(<TestWrapper />);
|
render(<TestWrapper />);
|
||||||
|
|
||||||
|
await expandAdvancedSection(user);
|
||||||
|
|
||||||
expect(screen.getByPlaceholderText('Enter parameter name...')).toBeInTheDocument();
|
expect(screen.getByPlaceholderText('Enter parameter name...')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: /add parameter/i })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /add parameter/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -110,6 +127,8 @@ describe('RecipeFormFields', () => {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<TestWrapper />);
|
render(<TestWrapper />);
|
||||||
|
|
||||||
|
await expandAdvancedSection(user);
|
||||||
|
|
||||||
const parameterInput = screen.getByPlaceholderText('Enter parameter name...');
|
const parameterInput = screen.getByPlaceholderText('Enter parameter name...');
|
||||||
const addButton = screen.getByRole('button', { name: /add parameter/i });
|
const addButton = screen.getByRole('button', { name: /add parameter/i });
|
||||||
|
|
||||||
@@ -160,7 +179,7 @@ describe('RecipeFormFields', () => {
|
|||||||
render(<TestWrapper />);
|
render(<TestWrapper />);
|
||||||
|
|
||||||
const instructionsInput = screen.getByPlaceholderText(
|
const instructionsInput = screen.getByPlaceholderText(
|
||||||
'Detailed instructions for the AI, hidden from the user...'
|
'Detailed instructions for the AI, hidden from the user'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Type instructions with template variables - use paste to avoid curly brace issues
|
// Type instructions with template variables - use paste to avoid curly brace issues
|
||||||
@@ -173,6 +192,8 @@ describe('RecipeFormFields', () => {
|
|||||||
// Just verify the component doesn't crash and the text is there
|
// Just verify the component doesn't crash and the text is there
|
||||||
expect(instructionsInput).toHaveValue('Hello {{name}}, please {{action}} the {{item}}');
|
expect(instructionsInput).toHaveValue('Hello {{name}}, please {{action}} the {{item}}');
|
||||||
|
|
||||||
|
await expandAdvancedSection(user);
|
||||||
|
|
||||||
// Check that the parameter section exists
|
// Check that the parameter section exists
|
||||||
expect(screen.getByText('Parameters')).toBeInTheDocument();
|
expect(screen.getByText('Parameters')).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
@@ -186,6 +207,8 @@ describe('RecipeFormFields', () => {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<TestWrapper />);
|
render(<TestWrapper />);
|
||||||
|
|
||||||
|
await expandAdvancedSection(user);
|
||||||
|
|
||||||
// Add a manual parameter
|
// Add a manual parameter
|
||||||
const parameterInput = screen.getByPlaceholderText('Enter parameter name...');
|
const parameterInput = screen.getByPlaceholderText('Enter parameter name...');
|
||||||
const addButton = screen.getByText('Add parameter');
|
const addButton = screen.getByText('Add parameter');
|
||||||
@@ -200,9 +223,12 @@ describe('RecipeFormFields', () => {
|
|||||||
expect(parameterInput).toHaveValue('');
|
expect(parameterInput).toHaveValue('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows parameter management UI', () => {
|
it('shows parameter management UI', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
render(<TestWrapper />);
|
render(<TestWrapper />);
|
||||||
|
|
||||||
|
await expandAdvancedSection(user);
|
||||||
|
|
||||||
// Check parameter section exists
|
// Check parameter section exists
|
||||||
expect(screen.getByText('Parameters')).toBeInTheDocument();
|
expect(screen.getByText('Parameters')).toBeInTheDocument();
|
||||||
expect(screen.getByPlaceholderText('Enter parameter name...')).toBeInTheDocument();
|
expect(screen.getByPlaceholderText('Enter parameter name...')).toBeInTheDocument();
|
||||||
@@ -216,6 +242,8 @@ describe('RecipeFormFields', () => {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<TestWrapper />);
|
render(<TestWrapper />);
|
||||||
|
|
||||||
|
await expandAdvancedSection(user);
|
||||||
|
|
||||||
// Check that activities section exists
|
// Check that activities section exists
|
||||||
expect(screen.getByText('Activities')).toBeInTheDocument();
|
expect(screen.getByText('Activities')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Message')).toBeInTheDocument();
|
expect(screen.getByText('Message')).toBeInTheDocument();
|
||||||
@@ -259,7 +287,7 @@ describe('RecipeFormFields', () => {
|
|||||||
render(<TestComponent />);
|
render(<TestComponent />);
|
||||||
|
|
||||||
const instructionsInput = screen.getByPlaceholderText(
|
const instructionsInput = screen.getByPlaceholderText(
|
||||||
'Detailed instructions for the AI, hidden from the user...'
|
'Detailed instructions for the AI, hidden from the user'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add instructions with template variables
|
// Add instructions with template variables
|
||||||
@@ -272,6 +300,8 @@ describe('RecipeFormFields', () => {
|
|||||||
// Wait a moment for the parameter detection to process
|
// Wait a moment for the parameter detection to process
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
await expandAdvancedSection(user);
|
||||||
|
|
||||||
// Check if parameters were detected and added
|
// Check if parameters were detected and added
|
||||||
// The parameters should appear as text in the parameter section
|
// The parameters should appear as text in the parameter section
|
||||||
const parameterSection = screen.getByText('Parameters').closest('div');
|
const parameterSection = screen.getByText('Parameters').closest('div');
|
||||||
@@ -408,6 +438,8 @@ describe('RecipeFormFields', () => {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<TestWrapper />);
|
render(<TestWrapper />);
|
||||||
|
|
||||||
|
await expandAdvancedSection(user);
|
||||||
|
|
||||||
// Add a manual parameter
|
// Add a manual parameter
|
||||||
const parameterInput = screen.getByPlaceholderText('Enter parameter name...');
|
const parameterInput = screen.getByPlaceholderText('Enter parameter name...');
|
||||||
const addButton = screen.getByText('Add parameter');
|
const addButton = screen.getByText('Add parameter');
|
||||||
@@ -617,6 +649,8 @@ describe('RecipeFormFields', () => {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<TestWrapper />);
|
render(<TestWrapper />);
|
||||||
|
|
||||||
|
await expandAdvancedSection(user);
|
||||||
|
|
||||||
// Add a manual parameter
|
// Add a manual parameter
|
||||||
const parameterInput = screen.getByPlaceholderText('Enter parameter name...');
|
const parameterInput = screen.getByPlaceholderText('Enter parameter name...');
|
||||||
const addButton = screen.getByText('Add parameter');
|
const addButton = screen.getByText('Add parameter');
|
||||||
@@ -648,6 +682,8 @@ describe('RecipeFormFields', () => {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<TestWrapper />);
|
render(<TestWrapper />);
|
||||||
|
|
||||||
|
await expandAdvancedSection(user);
|
||||||
|
|
||||||
// Add a parameter and test changing its type
|
// Add a parameter and test changing its type
|
||||||
const parameterInput = screen.getByPlaceholderText('Enter parameter name...');
|
const parameterInput = screen.getByPlaceholderText('Enter parameter name...');
|
||||||
const addButton = screen.getByText('Add parameter');
|
const addButton = screen.getByText('Add parameter');
|
||||||
|
|||||||
Reference in New Issue
Block a user