fix(agents): properly initialize form with API data defaults
Root cause: The demo data's model_params was missing `top_p`, but the Zod schema required all three fields (temperature, max_tokens, top_p). This caused silent validation failures when editing agent types. Fixes: 1. Add getInitialValues() that ensures all required fields have defaults 2. Handle nested validation errors in handleFormError (e.g., model_params.top_p) 3. Add useEffect to reset form when agentType changes 4. Add console.error logging for debugging validation failures 5. Update demo data to include top_p in all agent types The form now properly initializes with safe defaults for any missing fields from the API response, preventing silent validation failures. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,8 @@
|
|||||||
"fallback_models": ["claude-haiku-3-5-20241022"],
|
"fallback_models": ["claude-haiku-3-5-20241022"],
|
||||||
"model_params": {
|
"model_params": {
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
"max_tokens": 4096
|
"max_tokens": 4096,
|
||||||
|
"top_p": 0.95
|
||||||
},
|
},
|
||||||
"mcp_servers": ["gitea", "knowledge-base"],
|
"mcp_servers": ["gitea", "knowledge-base"],
|
||||||
"tool_permissions": {
|
"tool_permissions": {
|
||||||
@@ -29,7 +30,8 @@
|
|||||||
"fallback_models": ["claude-haiku-3-5-20241022"],
|
"fallback_models": ["claude-haiku-3-5-20241022"],
|
||||||
"model_params": {
|
"model_params": {
|
||||||
"temperature": 0.5,
|
"temperature": 0.5,
|
||||||
"max_tokens": 8192
|
"max_tokens": 8192,
|
||||||
|
"top_p": 0.95
|
||||||
},
|
},
|
||||||
"mcp_servers": ["gitea", "knowledge-base"],
|
"mcp_servers": ["gitea", "knowledge-base"],
|
||||||
"tool_permissions": {
|
"tool_permissions": {
|
||||||
@@ -49,7 +51,8 @@
|
|||||||
"fallback_models": ["claude-haiku-3-5-20241022"],
|
"fallback_models": ["claude-haiku-3-5-20241022"],
|
||||||
"model_params": {
|
"model_params": {
|
||||||
"temperature": 0.6,
|
"temperature": 0.6,
|
||||||
"max_tokens": 8192
|
"max_tokens": 8192,
|
||||||
|
"top_p": 0.95
|
||||||
},
|
},
|
||||||
"mcp_servers": ["gitea", "knowledge-base", "filesystem"],
|
"mcp_servers": ["gitea", "knowledge-base", "filesystem"],
|
||||||
"tool_permissions": {
|
"tool_permissions": {
|
||||||
@@ -69,7 +72,8 @@
|
|||||||
"fallback_models": ["claude-haiku-3-5-20241022"],
|
"fallback_models": ["claude-haiku-3-5-20241022"],
|
||||||
"model_params": {
|
"model_params": {
|
||||||
"temperature": 0.3,
|
"temperature": 0.3,
|
||||||
"max_tokens": 16384
|
"max_tokens": 16384,
|
||||||
|
"top_p": 0.95
|
||||||
},
|
},
|
||||||
"mcp_servers": ["gitea", "knowledge-base", "filesystem"],
|
"mcp_servers": ["gitea", "knowledge-base", "filesystem"],
|
||||||
"tool_permissions": {
|
"tool_permissions": {
|
||||||
@@ -89,7 +93,8 @@
|
|||||||
"fallback_models": ["claude-haiku-3-5-20241022"],
|
"fallback_models": ["claude-haiku-3-5-20241022"],
|
||||||
"model_params": {
|
"model_params": {
|
||||||
"temperature": 0.4,
|
"temperature": 0.4,
|
||||||
"max_tokens": 8192
|
"max_tokens": 8192,
|
||||||
|
"top_p": 0.95
|
||||||
},
|
},
|
||||||
"mcp_servers": ["gitea", "knowledge-base", "filesystem"],
|
"mcp_servers": ["gitea", "knowledge-base", "filesystem"],
|
||||||
"tool_permissions": {
|
"tool_permissions": {
|
||||||
@@ -109,7 +114,8 @@
|
|||||||
"fallback_models": ["claude-haiku-3-5-20241022"],
|
"fallback_models": ["claude-haiku-3-5-20241022"],
|
||||||
"model_params": {
|
"model_params": {
|
||||||
"temperature": 0.4,
|
"temperature": 0.4,
|
||||||
"max_tokens": 8192
|
"max_tokens": 8192,
|
||||||
|
"top_p": 0.95
|
||||||
},
|
},
|
||||||
"mcp_servers": ["gitea", "knowledge-base", "filesystem"],
|
"mcp_servers": ["gitea", "knowledge-base", "filesystem"],
|
||||||
"tool_permissions": {
|
"tool_permissions": {
|
||||||
|
|||||||
@@ -57,28 +57,37 @@ export function AgentTypeForm({
|
|||||||
const [activeTab, setActiveTab] = useState('basic');
|
const [activeTab, setActiveTab] = useState('basic');
|
||||||
const [expertiseInput, setExpertiseInput] = useState('');
|
const [expertiseInput, setExpertiseInput] = useState('');
|
||||||
|
|
||||||
// Always use create schema for validation - editing requires all fields too
|
// Build initial values with proper defaults for missing fields
|
||||||
const form = useForm<AgentTypeCreateFormValues>({
|
const getInitialValues = useCallback((): AgentTypeCreateFormValues => {
|
||||||
resolver: zodResolver(agentTypeCreateSchema),
|
if (!agentType) return defaultAgentTypeValues;
|
||||||
defaultValues: agentType
|
|
||||||
? {
|
// Ensure model_params has all required fields with proper defaults
|
||||||
|
const modelParams = agentType.model_params ?? {};
|
||||||
|
const safeModelParams = {
|
||||||
|
temperature: typeof modelParams.temperature === 'number' ? modelParams.temperature : 0.7,
|
||||||
|
max_tokens: typeof modelParams.max_tokens === 'number' ? modelParams.max_tokens : 8192,
|
||||||
|
top_p: typeof modelParams.top_p === 'number' ? modelParams.top_p : 0.95,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
name: agentType.name,
|
name: agentType.name,
|
||||||
slug: agentType.slug,
|
slug: agentType.slug,
|
||||||
description: agentType.description,
|
description: agentType.description,
|
||||||
expertise: agentType.expertise,
|
expertise: agentType.expertise ?? [],
|
||||||
personality_prompt: agentType.personality_prompt,
|
personality_prompt: agentType.personality_prompt ?? '',
|
||||||
primary_model: agentType.primary_model,
|
primary_model: agentType.primary_model ?? 'claude-opus-4-5-20251101',
|
||||||
fallback_models: agentType.fallback_models,
|
fallback_models: agentType.fallback_models ?? [],
|
||||||
model_params: (agentType.model_params ?? {
|
model_params: safeModelParams,
|
||||||
temperature: 0.7,
|
mcp_servers: agentType.mcp_servers ?? [],
|
||||||
max_tokens: 8192,
|
tool_permissions: agentType.tool_permissions ?? {},
|
||||||
top_p: 0.95,
|
is_active: agentType.is_active ?? false,
|
||||||
}) as AgentTypeCreateFormValues['model_params'],
|
};
|
||||||
mcp_servers: agentType.mcp_servers,
|
}, [agentType]);
|
||||||
tool_permissions: agentType.tool_permissions,
|
|
||||||
is_active: agentType.is_active,
|
// Always use create schema for validation - editing requires all fields too
|
||||||
}
|
const form = useForm<AgentTypeCreateFormValues>({
|
||||||
: defaultAgentTypeValues,
|
resolver: zodResolver(agentTypeCreateSchema),
|
||||||
|
defaultValues: getInitialValues(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -90,29 +99,58 @@ export function AgentTypeForm({
|
|||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
|
// Helper to extract first error message from nested errors
|
||||||
|
const getFirstErrorMessage = useCallback(
|
||||||
|
(formErrors: FieldErrors<AgentTypeCreateFormValues>): { field: string; message: string } => {
|
||||||
|
for (const key of Object.keys(formErrors)) {
|
||||||
|
const error = formErrors[key as keyof typeof formErrors];
|
||||||
|
if (!error) continue;
|
||||||
|
|
||||||
|
// Handle nested errors (like model_params.temperature)
|
||||||
|
if (typeof error === 'object' && !('message' in error)) {
|
||||||
|
for (const nestedKey of Object.keys(error)) {
|
||||||
|
const nestedError = (error as Record<string, { message?: string }>)[nestedKey];
|
||||||
|
if (nestedError?.message) {
|
||||||
|
return { field: `${key}.${nestedKey}`, message: nestedError.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle direct error
|
||||||
|
if ('message' in error && error.message) {
|
||||||
|
return { field: key, message: error.message as string };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { field: 'unknown', message: 'Validation error' };
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
// Handle form validation errors - show toast with first error
|
// Handle form validation errors - show toast with first error
|
||||||
const handleFormError = useCallback((formErrors: FieldErrors<AgentTypeCreateFormValues>) => {
|
const handleFormError = useCallback(
|
||||||
// Find the first error and show it
|
(formErrors: FieldErrors<AgentTypeCreateFormValues>) => {
|
||||||
const firstErrorKey = Object.keys(formErrors)[0] as keyof AgentTypeCreateFormValues;
|
// Log for debugging
|
||||||
if (firstErrorKey) {
|
console.error('[AgentTypeForm] Validation errors:', formErrors);
|
||||||
const error = formErrors[firstErrorKey];
|
|
||||||
const message = error && 'message' in error ? error.message : 'Validation error';
|
const { field, message } = getFirstErrorMessage(formErrors);
|
||||||
toast.error('Please fix form errors', {
|
toast.error('Please fix form errors', {
|
||||||
description: `${String(firstErrorKey)}: ${message}`,
|
description: `${field}: ${message}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Navigate to the tab containing the error
|
// Navigate to the tab containing the error
|
||||||
if (['name', 'slug', 'description', 'expertise', 'is_active'].includes(firstErrorKey)) {
|
const topLevelField = field.split('.')[0];
|
||||||
|
if (['name', 'slug', 'description', 'expertise', 'is_active'].includes(topLevelField)) {
|
||||||
setActiveTab('basic');
|
setActiveTab('basic');
|
||||||
} else if (['primary_model', 'fallback_models', 'model_params'].includes(firstErrorKey)) {
|
} else if (['primary_model', 'fallback_models', 'model_params'].includes(topLevelField)) {
|
||||||
setActiveTab('model');
|
setActiveTab('model');
|
||||||
} else if (['mcp_servers', 'tool_permissions'].includes(firstErrorKey)) {
|
} else if (['mcp_servers', 'tool_permissions'].includes(topLevelField)) {
|
||||||
setActiveTab('permissions');
|
setActiveTab('permissions');
|
||||||
} else if (firstErrorKey === 'personality_prompt') {
|
} else if (topLevelField === 'personality_prompt') {
|
||||||
setActiveTab('personality');
|
setActiveTab('personality');
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}, []);
|
[getFirstErrorMessage]
|
||||||
|
);
|
||||||
|
|
||||||
const watchName = watch('name');
|
const watchName = watch('name');
|
||||||
/* istanbul ignore next -- defensive fallback, expertise always has default */
|
/* istanbul ignore next -- defensive fallback, expertise always has default */
|
||||||
@@ -120,6 +158,14 @@ export function AgentTypeForm({
|
|||||||
/* istanbul ignore next -- defensive fallback, mcp_servers always has default */
|
/* istanbul ignore next -- defensive fallback, mcp_servers always has default */
|
||||||
const watchMcpServers = watch('mcp_servers') || [];
|
const watchMcpServers = watch('mcp_servers') || [];
|
||||||
|
|
||||||
|
// Reset form when agentType changes (e.g., switching to edit mode)
|
||||||
|
useEffect(() => {
|
||||||
|
if (agentType) {
|
||||||
|
const values = getInitialValues();
|
||||||
|
form.reset(values);
|
||||||
|
}
|
||||||
|
}, [agentType?.id, form, getInitialValues]);
|
||||||
|
|
||||||
// Auto-generate slug from name for new agent types
|
// Auto-generate slug from name for new agent types
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isEditing && watchName) {
|
if (!isEditing && watchName) {
|
||||||
|
|||||||
Reference in New Issue
Block a user