feat(frontend): implement agent configuration pages (#41)

- Add agent types list page with search and filter functionality
- Add agent type detail/edit page with tabbed interface
- Create AgentTypeForm component with React Hook Form + Zod validation
- Implement model configuration (temperature, max tokens, top_p)
- Add MCP permission management with checkboxes
- Include personality prompt editor textarea
- Create TanStack Query hooks for agent-types API
- Add useDebounce hook for search optimization
- Comprehensive unit tests for all components (68 tests)

Components:
- AgentTypeList: Grid view with status badges, expertise tags
- AgentTypeDetail: Full detail view with model config, MCP permissions
- AgentTypeForm: Create/edit with 4 tabs (Basic, Model, Permissions, Personality)

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 23:48:49 +01:00
parent e85788f79f
commit 68f1865a1e
17 changed files with 2888 additions and 0 deletions

View File

@@ -0,0 +1,194 @@
/**
* Agent Type Detail/Edit Page
*
* Displays agent type details with options to edit, duplicate, or deactivate.
* Handles 'new' as a special ID to show the create form.
*/
'use client';
import { useCallback, useState } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { toast } from 'sonner';
import { AgentTypeDetail, AgentTypeForm } from '@/components/agents';
import {
useAgentType,
useCreateAgentType,
useUpdateAgentType,
useDeactivateAgentType,
useDuplicateAgentType,
} from '@/lib/api/hooks/useAgentTypes';
import type { AgentTypeCreateFormValues } from '@/lib/validations/agentType';
type ViewMode = 'detail' | 'edit' | 'create';
export default function AgentTypeDetailPage() {
const router = useRouter();
const params = useParams();
const id = params.id as string;
// Determine initial view mode
const isNew = id === 'new';
const [viewMode, setViewMode] = useState<ViewMode>(isNew ? 'create' : 'detail');
// Fetch agent type data (skip if creating new)
const {
data: agentType,
isLoading,
error,
} = useAgentType(isNew ? null : id);
// Mutations
const createMutation = useCreateAgentType();
const updateMutation = useUpdateAgentType();
const deactivateMutation = useDeactivateAgentType();
const duplicateMutation = useDuplicateAgentType();
// Handle navigation back to list
const handleBack = useCallback(() => {
if (viewMode === 'edit') {
setViewMode('detail');
} else {
router.push('/agents');
}
}, [router, viewMode]);
// Handle edit button click
const handleEdit = useCallback(() => {
setViewMode('edit');
}, []);
// Handle form submission for create/update
const handleSubmit = useCallback(
async (data: AgentTypeCreateFormValues) => {
try {
if (isNew || viewMode === 'create') {
// Create new agent type
const result = await createMutation.mutateAsync({
name: data.name,
slug: data.slug,
description: data.description,
expertise: data.expertise,
personality_prompt: data.personality_prompt,
primary_model: data.primary_model,
fallback_models: data.fallback_models,
model_params: data.model_params,
mcp_servers: data.mcp_servers,
tool_permissions: data.tool_permissions,
is_active: data.is_active,
});
toast.success('Agent type created', {
description: `${result.name} has been created successfully`,
});
router.push(`/agents/${result.id}`);
} else {
// Update existing agent type
const result = await updateMutation.mutateAsync({
id,
data: {
name: data.name,
slug: data.slug,
description: data.description,
expertise: data.expertise,
personality_prompt: data.personality_prompt,
primary_model: data.primary_model,
fallback_models: data.fallback_models,
model_params: data.model_params,
mcp_servers: data.mcp_servers,
tool_permissions: data.tool_permissions,
is_active: data.is_active,
},
});
toast.success('Agent type updated', {
description: `${result.name} has been updated successfully`,
});
setViewMode('detail');
}
} catch (err) {
const message = err instanceof Error ? err.message : 'An error occurred';
toast.error('Failed to save agent type', { description: message });
}
},
[id, isNew, viewMode, createMutation, updateMutation, router]
);
// Handle duplicate
const handleDuplicate = useCallback(async () => {
if (!agentType) return;
try {
const result = await duplicateMutation.mutateAsync(agentType);
toast.success('Agent type duplicated', {
description: `${result.name} has been created`,
});
router.push(`/agents/${result.id}`);
} catch (err) {
const message = err instanceof Error ? err.message : 'An error occurred';
toast.error('Failed to duplicate agent type', { description: message });
}
}, [agentType, duplicateMutation, router]);
// Handle deactivate
const handleDeactivate = useCallback(async () => {
try {
await deactivateMutation.mutateAsync(id);
toast.success('Agent type deactivated', {
description: 'The agent type has been deactivated',
});
router.push('/agents');
} catch (err) {
const message = err instanceof Error ? err.message : 'An error occurred';
toast.error('Failed to deactivate agent type', { description: message });
}
}, [id, deactivateMutation, router]);
// Handle cancel from form
const handleCancel = useCallback(() => {
if (isNew) {
router.push('/agents');
} else {
setViewMode('detail');
}
}, [isNew, router]);
// Show error state
if (error && !isNew) {
return (
<div className="container mx-auto px-4 py-6">
<AgentTypeDetail
agentType={null}
onBack={handleBack}
onEdit={handleEdit}
onDuplicate={handleDuplicate}
onDeactivate={handleDeactivate}
/>
</div>
);
}
// Render based on view mode
return (
<div className="container mx-auto px-4 py-6">
{(viewMode === 'create' || viewMode === 'edit') && (
<AgentTypeForm
agentType={viewMode === 'edit' ? agentType ?? undefined : undefined}
onSubmit={handleSubmit}
onCancel={handleCancel}
isSubmitting={createMutation.isPending || updateMutation.isPending}
/>
)}
{viewMode === 'detail' && (
<AgentTypeDetail
agentType={agentType ?? null}
isLoading={isLoading}
onBack={handleBack}
onEdit={handleEdit}
onDuplicate={handleDuplicate}
onDeactivate={handleDeactivate}
isDeactivating={deactivateMutation.isPending}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,95 @@
/**
* Agent Types List Page
*
* Displays a list of agent types with search and filter functionality.
* Allows navigation to agent type detail and creation pages.
*/
'use client';
import { useState, useCallback, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import { AgentTypeList } from '@/components/agents';
import { useAgentTypes } from '@/lib/api/hooks/useAgentTypes';
import { useDebounce } from '@/lib/hooks/useDebounce';
export default function AgentTypesPage() {
const router = useRouter();
// Filter state
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
// Debounce search for API calls
const debouncedSearch = useDebounce(searchQuery, 300);
// Determine is_active filter value
const isActiveFilter = useMemo(() => {
if (statusFilter === 'active') return true;
if (statusFilter === 'inactive') return false;
return undefined; // 'all' returns undefined to not filter
}, [statusFilter]);
// Fetch agent types
const { data, isLoading, error } = useAgentTypes({
search: debouncedSearch || undefined,
is_active: isActiveFilter,
page: 1,
limit: 50,
});
// Filter results client-side for 'all' status
const filteredAgentTypes = useMemo(() => {
if (!data?.data) return [];
// When status is 'all', we need to fetch both and combine
// For now, the API returns based on is_active filter
return data.data;
}, [data?.data]);
// Handle navigation to agent type detail
const handleSelect = useCallback(
(id: string) => {
router.push(`/agents/${id}`);
},
[router]
);
// Handle navigation to create page
const handleCreate = useCallback(() => {
router.push('/agents/new');
}, [router]);
// Handle search change
const handleSearchChange = useCallback((query: string) => {
setSearchQuery(query);
}, []);
// Handle status filter change
const handleStatusFilterChange = useCallback((status: string) => {
setStatusFilter(status);
}, []);
// Show error toast if fetch fails
if (error) {
toast.error('Failed to load agent types', {
description: 'Please try again later',
});
}
return (
<div className="container mx-auto px-4 py-6">
<AgentTypeList
agentTypes={filteredAgentTypes}
isLoading={isLoading}
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
statusFilter={statusFilter}
onStatusFilterChange={handleStatusFilterChange}
onSelect={handleSelect}
onCreate={handleCreate}
/>
</div>
);
}