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

@@ -3,3 +3,6 @@
// Authentication hooks
export * from './useAuth';
// Agent Types hooks
export * from './useAgentTypes';

View File

@@ -0,0 +1,220 @@
/**
* Agent Types Hooks
*
* TanStack Query hooks for managing agent type operations.
* Provides data fetching and mutations for the agent-types API.
*/
'use client';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/api/client';
import { useAuth } from '@/lib/auth/AuthContext';
import type {
AgentTypeCreate,
AgentTypeUpdate,
AgentTypeResponse,
AgentTypeListResponse,
AgentTypeListParams,
} from '@/lib/api/types/agentTypes';
/**
* Query keys for agent types
*/
export const agentTypeKeys = {
all: ['agent-types'] as const,
lists: () => [...agentTypeKeys.all, 'list'] as const,
list: (params: AgentTypeListParams) => [...agentTypeKeys.lists(), params] as const,
details: () => [...agentTypeKeys.all, 'detail'] as const,
detail: (id: string) => [...agentTypeKeys.details(), id] as const,
bySlug: (slug: string) => [...agentTypeKeys.all, 'slug', slug] as const,
};
/**
* Default page limit for listing agent types
*/
const DEFAULT_PAGE_LIMIT = 20;
/**
* Hook to fetch paginated list of agent types
*
* @param params - Query parameters for filtering and pagination
* @returns Query result with agent types list
*/
export function useAgentTypes(params: AgentTypeListParams = {}) {
const { user } = useAuth();
const {
page = 1,
limit = DEFAULT_PAGE_LIMIT,
is_active = true,
search,
} = params;
return useQuery({
queryKey: agentTypeKeys.list({ page, limit, is_active, search }),
queryFn: async (): Promise<AgentTypeListResponse> => {
const response = await apiClient.instance.get('/api/v1/agent-types', {
params: {
page,
limit,
is_active,
...(search ? { search } : {}),
},
});
return response.data;
},
enabled: !!user,
staleTime: 30000, // 30 seconds
});
}
/**
* Hook to fetch a single agent type by ID
*
* @param id - Agent type UUID
* @returns Query result with agent type details
*/
export function useAgentType(id: string | null) {
const { user } = useAuth();
return useQuery({
queryKey: agentTypeKeys.detail(id ?? ''),
queryFn: async (): Promise<AgentTypeResponse> => {
if (!id) throw new Error('Agent type ID is required');
const response = await apiClient.instance.get(`/api/v1/agent-types/${id}`);
return response.data;
},
enabled: !!user && !!id,
staleTime: 60000, // 1 minute
});
}
/**
* Hook to fetch an agent type by slug
*
* @param slug - Agent type slug
* @returns Query result with agent type details
*/
export function useAgentTypeBySlug(slug: string | null) {
const { user } = useAuth();
return useQuery({
queryKey: agentTypeKeys.bySlug(slug ?? ''),
queryFn: async (): Promise<AgentTypeResponse> => {
if (!slug) throw new Error('Agent type slug is required');
const response = await apiClient.instance.get(`/api/v1/agent-types/slug/${slug}`);
return response.data;
},
enabled: !!user && !!slug,
staleTime: 60000, // 1 minute
});
}
/**
* Hook to create a new agent type
*
* @returns Mutation for creating agent types
*/
export function useCreateAgentType() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: AgentTypeCreate): Promise<AgentTypeResponse> => {
const response = await apiClient.instance.post('/api/v1/agent-types', data);
return response.data;
},
onSuccess: () => {
// Invalidate all agent type lists to refetch
queryClient.invalidateQueries({ queryKey: agentTypeKeys.lists() });
},
});
}
/**
* Hook to update an existing agent type
*
* @returns Mutation for updating agent types
*/
export function useUpdateAgentType() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
id,
data,
}: {
id: string;
data: AgentTypeUpdate;
}): Promise<AgentTypeResponse> => {
const response = await apiClient.instance.patch(`/api/v1/agent-types/${id}`, data);
return response.data;
},
onSuccess: (updatedAgentType) => {
// Update the cache for this specific agent type
queryClient.setQueryData(
agentTypeKeys.detail(updatedAgentType.id),
updatedAgentType
);
// Invalidate lists to reflect changes
queryClient.invalidateQueries({ queryKey: agentTypeKeys.lists() });
},
});
}
/**
* Hook to deactivate (soft delete) an agent type
*
* @returns Mutation for deactivating agent types
*/
export function useDeactivateAgentType() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string): Promise<{ success: boolean; message: string }> => {
const response = await apiClient.instance.delete(`/api/v1/agent-types/${id}`);
return response.data;
},
onSuccess: (_data, id) => {
// Invalidate all agent type queries
queryClient.invalidateQueries({ queryKey: agentTypeKeys.all });
// Remove specific agent type from cache
queryClient.removeQueries({ queryKey: agentTypeKeys.detail(id) });
},
});
}
/**
* Hook to duplicate an agent type
*
* @returns Mutation for duplicating agent types
*/
export function useDuplicateAgentType() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (agentType: AgentTypeResponse): Promise<AgentTypeResponse> => {
// Create a new agent type based on the existing one
const newAgentType: AgentTypeCreate = {
name: `${agentType.name} (Copy)`,
slug: `${agentType.slug}-copy`,
description: agentType.description,
expertise: [...agentType.expertise],
personality_prompt: agentType.personality_prompt,
primary_model: agentType.primary_model,
fallback_models: [...agentType.fallback_models],
model_params: { ...agentType.model_params },
mcp_servers: [...agentType.mcp_servers],
tool_permissions: { ...agentType.tool_permissions },
is_active: false, // Start as inactive/draft
};
const response = await apiClient.instance.post('/api/v1/agent-types', newAgentType);
return response.data;
},
onSuccess: () => {
// Invalidate lists to show the new duplicate
queryClient.invalidateQueries({ queryKey: agentTypeKeys.lists() });
},
});
}

View File

@@ -0,0 +1,137 @@
/**
* AgentType API Types
*
* These types mirror the backend Pydantic schemas for AgentType entities.
* Used for type-safe API communication with the agent-types endpoints.
*/
/**
* Base agent type fields shared across create, update, and response schemas
*/
export interface AgentTypeBase {
name: string;
slug: string;
description?: string | null;
expertise: string[];
personality_prompt: string;
primary_model: string;
fallback_models: string[];
model_params: Record<string, unknown>;
mcp_servers: string[];
tool_permissions: Record<string, unknown>;
is_active: boolean;
}
/**
* Schema for creating a new agent type
*/
export interface AgentTypeCreate {
name: string;
slug: string;
description?: string | null;
expertise?: string[];
personality_prompt: string;
primary_model: string;
fallback_models?: string[];
model_params?: Record<string, unknown>;
mcp_servers?: string[];
tool_permissions?: Record<string, unknown>;
is_active?: boolean;
}
/**
* Schema for updating an existing agent type
*/
export interface AgentTypeUpdate {
name?: string | null;
slug?: string | null;
description?: string | null;
expertise?: string[] | null;
personality_prompt?: string | null;
primary_model?: string | null;
fallback_models?: string[] | null;
model_params?: Record<string, unknown> | null;
mcp_servers?: string[] | null;
tool_permissions?: Record<string, unknown> | null;
is_active?: boolean | null;
}
/**
* Schema for agent type API responses
*/
export interface AgentTypeResponse {
id: string;
name: string;
slug: string;
description: string | null;
expertise: string[];
personality_prompt: string;
primary_model: string;
fallback_models: string[];
model_params: Record<string, unknown>;
mcp_servers: string[];
tool_permissions: Record<string, unknown>;
is_active: boolean;
created_at: string;
updated_at: string;
instance_count: number;
}
/**
* Pagination metadata for list responses
*/
export interface PaginationMeta {
total: number;
page: number;
page_size: number;
total_pages: number;
has_next: boolean;
has_prev: boolean;
}
/**
* Schema for paginated agent type list responses
*/
export interface AgentTypeListResponse {
data: AgentTypeResponse[];
pagination: PaginationMeta;
}
/**
* Query parameters for listing agent types
*/
export interface AgentTypeListParams {
page?: number;
limit?: number;
is_active?: boolean;
search?: string;
}
/**
* Model parameter configuration with typed fields
*/
export interface ModelParams {
temperature?: number;
max_tokens?: number;
top_p?: number;
[key: string]: unknown;
}
/**
* MCP permission scope configuration
*/
export interface McpPermission {
id: string;
name: string;
enabled: boolean;
scopes: string[];
}
/**
* Tool permission entry
*/
export interface ToolPermission {
tool_id: string;
enabled: boolean;
scopes?: string[];
}

View File

@@ -0,0 +1,8 @@
/**
* API Types
*
* Custom types for API entities that may not be in the generated client.
* These are typically used for Syndarix-specific features.
*/
export * from './agentTypes';