forked from cardosofelipe/fast-next-template
Compare commits
3 Commits
8e16e2645e
...
4ad3d20cf2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ad3d20cf2 | ||
|
|
8623eb56f5 | ||
|
|
3cb6c8d13b |
@@ -109,7 +109,7 @@
|
|||||||
"category": "development",
|
"category": "development",
|
||||||
"icon": "file-text",
|
"icon": "file-text",
|
||||||
"color": "#3B82F6",
|
"color": "#3B82F6",
|
||||||
"sort_order": 30,
|
"sort_order": 20,
|
||||||
"typical_tasks": ["Requirements analysis", "Process modeling", "Gap analysis", "Functional specifications"],
|
"typical_tasks": ["Requirements analysis", "Process modeling", "Gap analysis", "Functional specifications"],
|
||||||
"collaboration_hints": ["product-owner", "solutions-architect", "qa-engineer"]
|
"collaboration_hints": ["product-owner", "solutions-architect", "qa-engineer"]
|
||||||
},
|
},
|
||||||
@@ -151,7 +151,7 @@
|
|||||||
"category": "development",
|
"category": "development",
|
||||||
"icon": "git-branch",
|
"icon": "git-branch",
|
||||||
"color": "#3B82F6",
|
"color": "#3B82F6",
|
||||||
"sort_order": 40,
|
"sort_order": 20,
|
||||||
"typical_tasks": ["System design", "ADR creation", "Technology selection", "Integration patterns"],
|
"typical_tasks": ["System design", "ADR creation", "Technology selection", "Integration patterns"],
|
||||||
"collaboration_hints": ["backend-engineer", "frontend-engineer", "security-engineer"]
|
"collaboration_hints": ["backend-engineer", "frontend-engineer", "security-engineer"]
|
||||||
},
|
},
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
"category": "development",
|
"category": "development",
|
||||||
"icon": "code",
|
"icon": "code",
|
||||||
"color": "#3B82F6",
|
"color": "#3B82F6",
|
||||||
"sort_order": 50,
|
"sort_order": 30,
|
||||||
"typical_tasks": ["End-to-end feature development", "API design", "UI implementation", "Database operations"],
|
"typical_tasks": ["End-to-end feature development", "API design", "UI implementation", "Database operations"],
|
||||||
"collaboration_hints": ["solutions-architect", "qa-engineer", "devops-engineer"]
|
"collaboration_hints": ["solutions-architect", "qa-engineer", "devops-engineer"]
|
||||||
},
|
},
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
"category": "development",
|
"category": "development",
|
||||||
"icon": "server",
|
"icon": "server",
|
||||||
"color": "#3B82F6",
|
"color": "#3B82F6",
|
||||||
"sort_order": 60,
|
"sort_order": 30,
|
||||||
"typical_tasks": ["API development", "Database optimization", "System integration", "Performance tuning"],
|
"typical_tasks": ["API development", "Database optimization", "System integration", "Performance tuning"],
|
||||||
"collaboration_hints": ["solutions-architect", "frontend-engineer", "data-engineer"]
|
"collaboration_hints": ["solutions-architect", "frontend-engineer", "data-engineer"]
|
||||||
},
|
},
|
||||||
@@ -289,7 +289,7 @@
|
|||||||
"category": "development",
|
"category": "development",
|
||||||
"icon": "layout",
|
"icon": "layout",
|
||||||
"color": "#3B82F6",
|
"color": "#3B82F6",
|
||||||
"sort_order": 70,
|
"sort_order": 30,
|
||||||
"typical_tasks": ["UI component development", "State management", "API integration", "Responsive design"],
|
"typical_tasks": ["UI component development", "State management", "API integration", "Responsive design"],
|
||||||
"collaboration_hints": ["ui-ux-designer", "backend-engineer", "qa-engineer"]
|
"collaboration_hints": ["ui-ux-designer", "backend-engineer", "qa-engineer"]
|
||||||
},
|
},
|
||||||
@@ -332,7 +332,7 @@
|
|||||||
"category": "development",
|
"category": "development",
|
||||||
"icon": "smartphone",
|
"icon": "smartphone",
|
||||||
"color": "#3B82F6",
|
"color": "#3B82F6",
|
||||||
"sort_order": 80,
|
"sort_order": 30,
|
||||||
"typical_tasks": ["Native app development", "Cross-platform solutions", "Mobile optimization", "App store deployment"],
|
"typical_tasks": ["Native app development", "Cross-platform solutions", "Mobile optimization", "App store deployment"],
|
||||||
"collaboration_hints": ["backend-engineer", "ui-ux-designer", "qa-engineer"]
|
"collaboration_hints": ["backend-engineer", "ui-ux-designer", "qa-engineer"]
|
||||||
},
|
},
|
||||||
@@ -373,7 +373,7 @@
|
|||||||
"category": "design",
|
"category": "design",
|
||||||
"icon": "palette",
|
"icon": "palette",
|
||||||
"color": "#EC4899",
|
"color": "#EC4899",
|
||||||
"sort_order": 10,
|
"sort_order": 20,
|
||||||
"typical_tasks": ["Interface design", "User flow creation", "Design system maintenance", "Prototyping"],
|
"typical_tasks": ["Interface design", "User flow creation", "Design system maintenance", "Prototyping"],
|
||||||
"collaboration_hints": ["frontend-engineer", "ux-researcher", "product-owner"]
|
"collaboration_hints": ["frontend-engineer", "ux-researcher", "product-owner"]
|
||||||
},
|
},
|
||||||
@@ -455,7 +455,7 @@
|
|||||||
"category": "quality",
|
"category": "quality",
|
||||||
"icon": "shield",
|
"icon": "shield",
|
||||||
"color": "#10B981",
|
"color": "#10B981",
|
||||||
"sort_order": 10,
|
"sort_order": 20,
|
||||||
"typical_tasks": ["Test strategy development", "Test automation", "Bug verification", "Quality metrics"],
|
"typical_tasks": ["Test strategy development", "Test automation", "Bug verification", "Quality metrics"],
|
||||||
"collaboration_hints": ["backend-engineer", "frontend-engineer", "devops-engineer"]
|
"collaboration_hints": ["backend-engineer", "frontend-engineer", "devops-engineer"]
|
||||||
},
|
},
|
||||||
@@ -543,7 +543,7 @@
|
|||||||
"category": "quality",
|
"category": "quality",
|
||||||
"icon": "shield-check",
|
"icon": "shield-check",
|
||||||
"color": "#10B981",
|
"color": "#10B981",
|
||||||
"sort_order": 20,
|
"sort_order": 30,
|
||||||
"typical_tasks": ["Security architecture", "Vulnerability assessment", "Compliance validation", "Threat modeling"],
|
"typical_tasks": ["Security architecture", "Vulnerability assessment", "Compliance validation", "Threat modeling"],
|
||||||
"collaboration_hints": ["solutions-architect", "devops-engineer", "backend-engineer"]
|
"collaboration_hints": ["solutions-architect", "devops-engineer", "backend-engineer"]
|
||||||
},
|
},
|
||||||
@@ -585,7 +585,7 @@
|
|||||||
"category": "ai_ml",
|
"category": "ai_ml",
|
||||||
"icon": "brain",
|
"icon": "brain",
|
||||||
"color": "#8B5CF6",
|
"color": "#8B5CF6",
|
||||||
"sort_order": 10,
|
"sort_order": 30,
|
||||||
"typical_tasks": ["Model development", "Algorithm selection", "Feature engineering", "Model optimization"],
|
"typical_tasks": ["Model development", "Algorithm selection", "Feature engineering", "Model optimization"],
|
||||||
"collaboration_hints": ["data-scientist", "mlops-engineer", "backend-engineer"]
|
"collaboration_hints": ["data-scientist", "mlops-engineer", "backend-engineer"]
|
||||||
},
|
},
|
||||||
@@ -625,7 +625,7 @@
|
|||||||
"category": "ai_ml",
|
"category": "ai_ml",
|
||||||
"icon": "microscope",
|
"icon": "microscope",
|
||||||
"color": "#8B5CF6",
|
"color": "#8B5CF6",
|
||||||
"sort_order": 20,
|
"sort_order": 30,
|
||||||
"typical_tasks": ["Research paper analysis", "Novel algorithm design", "Experiment design", "Benchmark evaluation"],
|
"typical_tasks": ["Research paper analysis", "Novel algorithm design", "Experiment design", "Benchmark evaluation"],
|
||||||
"collaboration_hints": ["ai-ml-engineer", "data-scientist", "scientific-computing-expert"]
|
"collaboration_hints": ["ai-ml-engineer", "data-scientist", "scientific-computing-expert"]
|
||||||
},
|
},
|
||||||
@@ -709,7 +709,7 @@
|
|||||||
"category": "ai_ml",
|
"category": "ai_ml",
|
||||||
"icon": "message-square",
|
"icon": "message-square",
|
||||||
"color": "#8B5CF6",
|
"color": "#8B5CF6",
|
||||||
"sort_order": 40,
|
"sort_order": 30,
|
||||||
"typical_tasks": ["Text processing pipelines", "Language model fine-tuning", "Named entity recognition", "Sentiment analysis"],
|
"typical_tasks": ["Text processing pipelines", "Language model fine-tuning", "Named entity recognition", "Sentiment analysis"],
|
||||||
"collaboration_hints": ["ai-ml-engineer", "data-scientist", "backend-engineer"]
|
"collaboration_hints": ["ai-ml-engineer", "data-scientist", "backend-engineer"]
|
||||||
},
|
},
|
||||||
@@ -751,7 +751,7 @@
|
|||||||
"category": "operations",
|
"category": "operations",
|
||||||
"icon": "settings-2",
|
"icon": "settings-2",
|
||||||
"color": "#F59E0B",
|
"color": "#F59E0B",
|
||||||
"sort_order": 20,
|
"sort_order": 30,
|
||||||
"typical_tasks": ["ML pipeline development", "Model deployment", "Feature store management", "Model monitoring"],
|
"typical_tasks": ["ML pipeline development", "Model deployment", "Feature store management", "Model monitoring"],
|
||||||
"collaboration_hints": ["ai-ml-engineer", "devops-engineer", "data-engineer"]
|
"collaboration_hints": ["ai-ml-engineer", "devops-engineer", "data-engineer"]
|
||||||
},
|
},
|
||||||
@@ -793,7 +793,7 @@
|
|||||||
"category": "data",
|
"category": "data",
|
||||||
"icon": "chart-bar",
|
"icon": "chart-bar",
|
||||||
"color": "#06B6D4",
|
"color": "#06B6D4",
|
||||||
"sort_order": 10,
|
"sort_order": 30,
|
||||||
"typical_tasks": ["Statistical analysis", "Predictive modeling", "Data visualization", "Insight generation"],
|
"typical_tasks": ["Statistical analysis", "Predictive modeling", "Data visualization", "Insight generation"],
|
||||||
"collaboration_hints": ["data-engineer", "ai-ml-engineer", "business-analyst"]
|
"collaboration_hints": ["data-engineer", "ai-ml-engineer", "business-analyst"]
|
||||||
},
|
},
|
||||||
@@ -835,7 +835,7 @@
|
|||||||
"category": "data",
|
"category": "data",
|
||||||
"icon": "database",
|
"icon": "database",
|
||||||
"color": "#06B6D4",
|
"color": "#06B6D4",
|
||||||
"sort_order": 20,
|
"sort_order": 30,
|
||||||
"typical_tasks": ["Data pipeline development", "ETL optimization", "Data warehouse design", "Data quality management"],
|
"typical_tasks": ["Data pipeline development", "ETL optimization", "Data warehouse design", "Data quality management"],
|
||||||
"collaboration_hints": ["data-scientist", "backend-engineer", "mlops-engineer"]
|
"collaboration_hints": ["data-scientist", "backend-engineer", "mlops-engineer"]
|
||||||
},
|
},
|
||||||
@@ -911,7 +911,7 @@
|
|||||||
"category": "leadership",
|
"category": "leadership",
|
||||||
"icon": "target",
|
"icon": "target",
|
||||||
"color": "#F97316",
|
"color": "#F97316",
|
||||||
"sort_order": 20,
|
"sort_order": 10,
|
||||||
"typical_tasks": ["Sprint facilitation", "Impediment removal", "Process improvement", "Team coaching"],
|
"typical_tasks": ["Sprint facilitation", "Impediment removal", "Process improvement", "Team coaching"],
|
||||||
"collaboration_hints": ["project-manager", "product-owner", "technical-lead"]
|
"collaboration_hints": ["project-manager", "product-owner", "technical-lead"]
|
||||||
},
|
},
|
||||||
@@ -992,7 +992,7 @@
|
|||||||
"category": "domain_expert",
|
"category": "domain_expert",
|
||||||
"icon": "heart-pulse",
|
"icon": "heart-pulse",
|
||||||
"color": "#84CC16",
|
"color": "#84CC16",
|
||||||
"sort_order": 20,
|
"sort_order": 50,
|
||||||
"typical_tasks": ["Healthcare system design", "HIPAA compliance", "HL7/FHIR integration", "Clinical workflow optimization"],
|
"typical_tasks": ["Healthcare system design", "HIPAA compliance", "HL7/FHIR integration", "Clinical workflow optimization"],
|
||||||
"collaboration_hints": ["solutions-architect", "security-engineer", "data-engineer"]
|
"collaboration_hints": ["solutions-architect", "security-engineer", "data-engineer"]
|
||||||
},
|
},
|
||||||
@@ -1034,7 +1034,7 @@
|
|||||||
"category": "domain_expert",
|
"category": "domain_expert",
|
||||||
"icon": "flask",
|
"icon": "flask",
|
||||||
"color": "#84CC16",
|
"color": "#84CC16",
|
||||||
"sort_order": 30,
|
"sort_order": 50,
|
||||||
"typical_tasks": ["HPC architecture", "Scientific algorithm implementation", "Data pipeline optimization", "Numerical computing"],
|
"typical_tasks": ["HPC architecture", "Scientific algorithm implementation", "Data pipeline optimization", "Numerical computing"],
|
||||||
"collaboration_hints": ["ai-researcher", "data-scientist", "backend-engineer"]
|
"collaboration_hints": ["ai-researcher", "data-scientist", "backend-engineer"]
|
||||||
},
|
},
|
||||||
@@ -1073,7 +1073,7 @@
|
|||||||
"category": "domain_expert",
|
"category": "domain_expert",
|
||||||
"icon": "lightbulb",
|
"icon": "lightbulb",
|
||||||
"color": "#84CC16",
|
"color": "#84CC16",
|
||||||
"sort_order": 40,
|
"sort_order": 50,
|
||||||
"typical_tasks": ["Behavioral design", "Engagement optimization", "User motivation analysis", "Ethical AI guidelines"],
|
"typical_tasks": ["Behavioral design", "Engagement optimization", "User motivation analysis", "Ethical AI guidelines"],
|
||||||
"collaboration_hints": ["ux-researcher", "ui-ux-designer", "product-owner"]
|
"collaboration_hints": ["ux-researcher", "ui-ux-designer", "product-owner"]
|
||||||
},
|
},
|
||||||
|
|||||||
55
frontend/package-lock.json
generated
55
frontend/package-lock.json
generated
@@ -21,6 +21,7 @@
|
|||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@tanstack/react-query": "^5.90.5",
|
"@tanstack/react-query": "^5.90.5",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"axios": "^1.13.1",
|
"axios": "^1.13.1",
|
||||||
@@ -4688,6 +4689,60 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-toggle": {
|
||||||
|
"version": "1.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
|
||||||
|
"integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-toggle-group": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-roving-focus": "1.1.11",
|
||||||
|
"@radix-ui/react-toggle": "1.1.10",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@tanstack/react-query": "^5.90.5",
|
"@tanstack/react-query": "^5.90.5",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"axios": "^1.13.1",
|
"axios": "^1.13.1",
|
||||||
|
|||||||
@@ -73,6 +73,13 @@ export default function AgentTypeDetailPage() {
|
|||||||
mcp_servers: data.mcp_servers,
|
mcp_servers: data.mcp_servers,
|
||||||
tool_permissions: data.tool_permissions,
|
tool_permissions: data.tool_permissions,
|
||||||
is_active: data.is_active,
|
is_active: data.is_active,
|
||||||
|
// Category and display fields
|
||||||
|
category: data.category,
|
||||||
|
icon: data.icon,
|
||||||
|
color: data.color,
|
||||||
|
sort_order: data.sort_order,
|
||||||
|
typical_tasks: data.typical_tasks,
|
||||||
|
collaboration_hints: data.collaboration_hints,
|
||||||
});
|
});
|
||||||
toast.success('Agent type created', {
|
toast.success('Agent type created', {
|
||||||
description: `${result.name} has been created successfully`,
|
description: `${result.name} has been created successfully`,
|
||||||
@@ -94,6 +101,13 @@ export default function AgentTypeDetailPage() {
|
|||||||
mcp_servers: data.mcp_servers,
|
mcp_servers: data.mcp_servers,
|
||||||
tool_permissions: data.tool_permissions,
|
tool_permissions: data.tool_permissions,
|
||||||
is_active: data.is_active,
|
is_active: data.is_active,
|
||||||
|
// Category and display fields
|
||||||
|
category: data.category,
|
||||||
|
icon: data.icon,
|
||||||
|
color: data.color,
|
||||||
|
sort_order: data.sort_order,
|
||||||
|
typical_tasks: data.typical_tasks,
|
||||||
|
collaboration_hints: data.collaboration_hints,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
toast.success('Agent type updated', {
|
toast.success('Agent type updated', {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Agent Types List Page
|
* Agent Types List Page
|
||||||
*
|
*
|
||||||
* Displays a list of agent types with search and filter functionality.
|
* Displays a list of agent types with search, status, and category filters.
|
||||||
* Allows navigation to agent type detail and creation pages.
|
* Supports grid and list view modes with user preference persistence.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
@@ -10,9 +10,10 @@
|
|||||||
import { useState, useCallback, useMemo } from 'react';
|
import { useState, useCallback, useMemo } from 'react';
|
||||||
import { useRouter } from '@/lib/i18n/routing';
|
import { useRouter } from '@/lib/i18n/routing';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { AgentTypeList } from '@/components/agents';
|
import { AgentTypeList, type ViewMode } from '@/components/agents';
|
||||||
import { useAgentTypes } from '@/lib/api/hooks/useAgentTypes';
|
import { useAgentTypes } from '@/lib/api/hooks/useAgentTypes';
|
||||||
import { useDebounce } from '@/lib/hooks/useDebounce';
|
import { useDebounce } from '@/lib/hooks/useDebounce';
|
||||||
|
import type { AgentTypeCategory } from '@/lib/api/types/agentTypes';
|
||||||
|
|
||||||
export default function AgentTypesPage() {
|
export default function AgentTypesPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -20,6 +21,8 @@ export default function AgentTypesPage() {
|
|||||||
// Filter state
|
// Filter state
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState('all');
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
|
const [categoryFilter, setCategoryFilter] = useState('all');
|
||||||
|
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
||||||
|
|
||||||
// Debounce search for API calls
|
// Debounce search for API calls
|
||||||
const debouncedSearch = useDebounce(searchQuery, 300);
|
const debouncedSearch = useDebounce(searchQuery, 300);
|
||||||
@@ -31,21 +34,25 @@ export default function AgentTypesPage() {
|
|||||||
return undefined; // 'all' returns undefined to not filter
|
return undefined; // 'all' returns undefined to not filter
|
||||||
}, [statusFilter]);
|
}, [statusFilter]);
|
||||||
|
|
||||||
|
// Determine category filter value
|
||||||
|
const categoryFilterValue = useMemo(() => {
|
||||||
|
if (categoryFilter === 'all') return undefined;
|
||||||
|
return categoryFilter as AgentTypeCategory;
|
||||||
|
}, [categoryFilter]);
|
||||||
|
|
||||||
// Fetch agent types
|
// Fetch agent types
|
||||||
const { data, isLoading, error } = useAgentTypes({
|
const { data, isLoading, error } = useAgentTypes({
|
||||||
search: debouncedSearch || undefined,
|
search: debouncedSearch || undefined,
|
||||||
is_active: isActiveFilter,
|
is_active: isActiveFilter,
|
||||||
|
category: categoryFilterValue,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter results client-side for 'all' status
|
// Get filtered and sorted agent types (sort by sort_order ascending - smaller first)
|
||||||
const filteredAgentTypes = useMemo(() => {
|
const filteredAgentTypes = useMemo(() => {
|
||||||
if (!data?.data) return [];
|
if (!data?.data) return [];
|
||||||
|
return [...data.data].sort((a, b) => a.sort_order - b.sort_order);
|
||||||
// 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]);
|
}, [data?.data]);
|
||||||
|
|
||||||
// Handle navigation to agent type detail
|
// Handle navigation to agent type detail
|
||||||
@@ -71,6 +78,16 @@ export default function AgentTypesPage() {
|
|||||||
setStatusFilter(status);
|
setStatusFilter(status);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Handle category filter change
|
||||||
|
const handleCategoryFilterChange = useCallback((category: string) => {
|
||||||
|
setCategoryFilter(category);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handle view mode change
|
||||||
|
const handleViewModeChange = useCallback((mode: ViewMode) => {
|
||||||
|
setViewMode(mode);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Show error toast if fetch fails
|
// Show error toast if fetch fails
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error('Failed to load agent types', {
|
toast.error('Failed to load agent types', {
|
||||||
@@ -87,6 +104,10 @@ export default function AgentTypesPage() {
|
|||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
statusFilter={statusFilter}
|
statusFilter={statusFilter}
|
||||||
onStatusFilterChange={handleStatusFilterChange}
|
onStatusFilterChange={handleStatusFilterChange}
|
||||||
|
categoryFilter={categoryFilter}
|
||||||
|
onCategoryFilterChange={handleCategoryFilterChange}
|
||||||
|
viewMode={viewMode}
|
||||||
|
onViewModeChange={handleViewModeChange}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
onCreate={handleCreate}
|
onCreate={handleCreate}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
* AgentTypeDetail Component
|
* AgentTypeDetail Component
|
||||||
*
|
*
|
||||||
* Displays detailed information about a single agent type.
|
* Displays detailed information about a single agent type.
|
||||||
* Shows model configuration, permissions, personality, and instance stats.
|
* Features a hero header with icon/color, category, typical tasks,
|
||||||
|
* collaboration hints, model configuration, and instance stats.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
@@ -36,8 +37,13 @@ import {
|
|||||||
Cpu,
|
Cpu,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
Sparkles,
|
||||||
|
Users,
|
||||||
|
Check,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { AgentTypeResponse } from '@/lib/api/types/agentTypes';
|
import { DynamicIcon } from '@/components/ui/dynamic-icon';
|
||||||
|
import type { AgentTypeResponse, AgentTypeCategory } from '@/lib/api/types/agentTypes';
|
||||||
|
import { CATEGORY_METADATA } from '@/lib/api/types/agentTypes';
|
||||||
import { AVAILABLE_MCP_SERVERS } from '@/lib/validations/agentType';
|
import { AVAILABLE_MCP_SERVERS } from '@/lib/validations/agentType';
|
||||||
|
|
||||||
interface AgentTypeDetailProps {
|
interface AgentTypeDetailProps {
|
||||||
@@ -51,6 +57,30 @@ interface AgentTypeDetailProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Category badge with color
|
||||||
|
*/
|
||||||
|
function CategoryBadge({ category }: { category: AgentTypeCategory | null }) {
|
||||||
|
if (!category) return null;
|
||||||
|
|
||||||
|
const meta = CATEGORY_METADATA[category];
|
||||||
|
if (!meta) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="font-medium"
|
||||||
|
style={{
|
||||||
|
borderColor: meta.color,
|
||||||
|
color: meta.color,
|
||||||
|
backgroundColor: `${meta.color}10`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{meta.label}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status badge component for agent types
|
* Status badge component for agent types
|
||||||
*/
|
*/
|
||||||
@@ -81,11 +111,22 @@ function AgentTypeStatusBadge({ isActive }: { isActive: boolean }) {
|
|||||||
function AgentTypeDetailSkeleton() {
|
function AgentTypeDetailSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-4">
|
{/* Hero skeleton */}
|
||||||
<Skeleton className="h-10 w-10" />
|
<div className="rounded-xl border p-6">
|
||||||
<div className="flex-1">
|
<div className="flex items-start gap-6">
|
||||||
<Skeleton className="h-8 w-64" />
|
<Skeleton className="h-20 w-20 rounded-xl" />
|
||||||
<Skeleton className="mt-2 h-4 w-48" />
|
<div className="flex-1 space-y-3">
|
||||||
|
<Skeleton className="h-8 w-64" />
|
||||||
|
<Skeleton className="h-4 w-96" />
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Skeleton className="h-6 w-20" />
|
||||||
|
<Skeleton className="h-6 w-24" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Skeleton className="h-9 w-24" />
|
||||||
|
<Skeleton className="h-9 w-20" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-6 lg:grid-cols-3">
|
<div className="grid gap-6 lg:grid-cols-3">
|
||||||
@@ -161,57 +202,134 @@ export function AgentTypeDetail({
|
|||||||
top_p?: number;
|
top_p?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const agentColor = agentType.color || '#3B82F6';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{/* Header */}
|
{/* Back button */}
|
||||||
<div className="mb-6 flex items-center gap-4">
|
<Button variant="ghost" size="sm" onClick={onBack} className="mb-4">
|
||||||
<Button variant="ghost" size="icon" onClick={onBack}>
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
<ArrowLeft className="h-4 w-4" />
|
Back to Agent Types
|
||||||
<span className="sr-only">Go back</span>
|
</Button>
|
||||||
</Button>
|
|
||||||
<div className="flex-1">
|
{/* Hero Header */}
|
||||||
<div className="flex items-center gap-3">
|
<div
|
||||||
<h1 className="text-3xl font-bold">{agentType.name}</h1>
|
className="mb-6 overflow-hidden rounded-xl border"
|
||||||
<AgentTypeStatusBadge isActive={agentType.is_active} />
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${agentColor}08 0%, transparent 60%)`,
|
||||||
|
borderColor: `${agentColor}30`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-1.5 w-full"
|
||||||
|
style={{ background: `linear-gradient(90deg, ${agentColor}, ${agentColor}60)` }}
|
||||||
|
/>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex flex-col gap-6 md:flex-row md:items-start">
|
||||||
|
{/* Icon */}
|
||||||
|
<div
|
||||||
|
className="flex h-20 w-20 shrink-0 items-center justify-center rounded-xl"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${agentColor}15`,
|
||||||
|
boxShadow: `0 8px 32px ${agentColor}20`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DynamicIcon
|
||||||
|
name={agentType.icon}
|
||||||
|
className="h-10 w-10"
|
||||||
|
style={{ color: agentColor }}
|
||||||
|
fallback="bot"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div className="flex-1 space-y-3">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">{agentType.name}</h1>
|
||||||
|
<p className="mt-1 text-muted-foreground">
|
||||||
|
{agentType.description || 'No description provided'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<AgentTypeStatusBadge isActive={agentType.is_active} />
|
||||||
|
<CategoryBadge category={agentType.category} />
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Last updated:{' '}
|
||||||
|
{new Date(agentType.updated_at).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex shrink-0 gap-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={onDuplicate}>
|
||||||
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
|
Duplicate
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onClick={onEdit}>
|
||||||
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Last modified:{' '}
|
|
||||||
{new Date(agentType.updated_at).toLocaleDateString('en-US', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="outline" size="sm" onClick={onDuplicate}>
|
|
||||||
<Copy className="mr-2 h-4 w-4" />
|
|
||||||
Duplicate
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" onClick={onEdit}>
|
|
||||||
<Edit className="mr-2 h-4 w-4" />
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 lg:grid-cols-3">
|
<div className="grid gap-6 lg:grid-cols-3">
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="space-y-6 lg:col-span-2">
|
<div className="space-y-6 lg:col-span-2">
|
||||||
{/* Description Card */}
|
{/* What This Agent Does Best */}
|
||||||
<Card>
|
{agentType.typical_tasks.length > 0 && (
|
||||||
<CardHeader>
|
<Card className="border-primary/20 bg-gradient-to-br from-primary/5 to-transparent">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardHeader className="pb-3">
|
||||||
<FileText className="h-5 w-5" />
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
Description
|
<Sparkles className="h-5 w-5 text-primary" />
|
||||||
</CardTitle>
|
What This Agent Does Best
|
||||||
</CardHeader>
|
</CardTitle>
|
||||||
<CardContent>
|
</CardHeader>
|
||||||
<p className="text-muted-foreground">
|
<CardContent>
|
||||||
{agentType.description || 'No description provided'}
|
<ul className="space-y-2">
|
||||||
</p>
|
{agentType.typical_tasks.map((task, index) => (
|
||||||
</CardContent>
|
<li key={index} className="flex items-start gap-2">
|
||||||
</Card>
|
<Check
|
||||||
|
className="mt-0.5 h-4 w-4 shrink-0 text-primary"
|
||||||
|
style={{ color: agentColor }}
|
||||||
|
/>
|
||||||
|
<span className="text-sm">{task}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Works Well With */}
|
||||||
|
{agentType.collaboration_hints.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<Users className="h-5 w-5" />
|
||||||
|
Works Well With
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Agents that complement this type for effective collaboration
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{agentType.collaboration_hints.map((hint, index) => (
|
||||||
|
<Badge key={index} variant="secondary" className="text-sm">
|
||||||
|
{hint}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Expertise Card */}
|
{/* Expertise Card */}
|
||||||
<Card>
|
<Card>
|
||||||
@@ -355,7 +473,9 @@ export function AgentTypeDetail({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-4xl font-bold text-primary">{agentType.instance_count}</p>
|
<p className="text-4xl font-bold" style={{ color: agentColor }}>
|
||||||
|
{agentType.instance_count}
|
||||||
|
</p>
|
||||||
<p className="text-sm text-muted-foreground">Active instances</p>
|
<p className="text-sm text-muted-foreground">Active instances</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" className="mt-4 w-full" size="sm" disabled>
|
<Button variant="outline" className="mt-4 w-full" size="sm" disabled>
|
||||||
@@ -364,6 +484,36 @@ export function AgentTypeDetail({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Agent Info */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<FileText className="h-5 w-5" />
|
||||||
|
Details
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Slug</span>
|
||||||
|
<code className="rounded bg-muted px-1.5 py-0.5 text-xs">{agentType.slug}</code>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Sort Order</span>
|
||||||
|
<span>{agentType.sort_order}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Created</span>
|
||||||
|
<span>
|
||||||
|
{new Date(agentType.created_at).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Danger Zone */}
|
{/* Danger Zone */}
|
||||||
<Card className="border-destructive/50">
|
<Card className="border-destructive/50">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* AgentTypeList Component
|
* AgentTypeList Component
|
||||||
*
|
*
|
||||||
* Displays a grid of agent type cards with search and filter functionality.
|
* Displays agent types in grid or list view with search, status, and category filters.
|
||||||
* Used on the main agent types page for browsing and selecting agent types.
|
* Shows icon, color accent, and category for each agent type.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
@@ -20,8 +20,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Bot, Plus, Search, Cpu } from 'lucide-react';
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||||
import type { AgentTypeResponse } from '@/lib/api/types/agentTypes';
|
import { Bot, Plus, Search, Cpu, LayoutGrid, List } from 'lucide-react';
|
||||||
|
import { DynamicIcon } from '@/components/ui/dynamic-icon';
|
||||||
|
import type { AgentTypeResponse, AgentTypeCategory } from '@/lib/api/types/agentTypes';
|
||||||
|
import { CATEGORY_METADATA } from '@/lib/api/types/agentTypes';
|
||||||
|
import { AGENT_TYPE_CATEGORIES } from '@/lib/validations/agentType';
|
||||||
|
|
||||||
|
export type ViewMode = 'grid' | 'list';
|
||||||
|
|
||||||
interface AgentTypeListProps {
|
interface AgentTypeListProps {
|
||||||
agentTypes: AgentTypeResponse[];
|
agentTypes: AgentTypeResponse[];
|
||||||
@@ -30,6 +36,10 @@ interface AgentTypeListProps {
|
|||||||
onSearchChange: (query: string) => void;
|
onSearchChange: (query: string) => void;
|
||||||
statusFilter: string;
|
statusFilter: string;
|
||||||
onStatusFilterChange: (status: string) => void;
|
onStatusFilterChange: (status: string) => void;
|
||||||
|
categoryFilter: string;
|
||||||
|
onCategoryFilterChange: (category: string) => void;
|
||||||
|
viewMode: ViewMode;
|
||||||
|
onViewModeChange: (mode: ViewMode) => void;
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
onCreate: () => void;
|
onCreate: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -60,11 +70,36 @@ function AgentTypeStatusBadge({ isActive }: { isActive: boolean }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loading skeleton for agent type cards
|
* Category badge with color
|
||||||
|
*/
|
||||||
|
function CategoryBadge({ category }: { category: AgentTypeCategory | null }) {
|
||||||
|
if (!category) return null;
|
||||||
|
|
||||||
|
const meta = CATEGORY_METADATA[category];
|
||||||
|
if (!meta) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs font-medium"
|
||||||
|
style={{
|
||||||
|
borderColor: meta.color,
|
||||||
|
color: meta.color,
|
||||||
|
backgroundColor: `${meta.color}10`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{meta.label}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading skeleton for agent type cards (grid view)
|
||||||
*/
|
*/
|
||||||
function AgentTypeCardSkeleton() {
|
function AgentTypeCardSkeleton() {
|
||||||
return (
|
return (
|
||||||
<Card className="h-[200px]">
|
<Card className="h-[220px] overflow-hidden">
|
||||||
|
<div className="h-1 w-full bg-muted" />
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<Skeleton className="h-10 w-10 rounded-lg" />
|
<Skeleton className="h-10 w-10 rounded-lg" />
|
||||||
@@ -91,6 +126,23 @@ function AgentTypeCardSkeleton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading skeleton for list view
|
||||||
|
*/
|
||||||
|
function AgentTypeListSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-4 rounded-lg border p-4">
|
||||||
|
<Skeleton className="h-12 w-12 rounded-lg" />
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<Skeleton className="h-5 w-48" />
|
||||||
|
<Skeleton className="h-4 w-96" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="h-5 w-20" />
|
||||||
|
<Skeleton className="h-5 w-16" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract model display name from model ID
|
* Extract model display name from model ID
|
||||||
*/
|
*/
|
||||||
@@ -103,6 +155,169 @@ function getModelDisplayName(modelId: string): string {
|
|||||||
return modelId;
|
return modelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid card view for agent type
|
||||||
|
*/
|
||||||
|
function AgentTypeGridCard({
|
||||||
|
type,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
type: AgentTypeResponse;
|
||||||
|
onSelect: (id: string) => void;
|
||||||
|
}) {
|
||||||
|
const agentColor = type.color || '#3B82F6';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className="cursor-pointer overflow-hidden transition-all hover:shadow-lg"
|
||||||
|
onClick={() => onSelect(type.id)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
onSelect(type.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label={`View ${type.name} agent type`}
|
||||||
|
style={{
|
||||||
|
borderTopColor: agentColor,
|
||||||
|
borderTopWidth: '3px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div
|
||||||
|
className="flex h-11 w-11 items-center justify-center rounded-lg"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${agentColor}15`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DynamicIcon
|
||||||
|
name={type.icon}
|
||||||
|
className="h-5 w-5"
|
||||||
|
style={{ color: agentColor }}
|
||||||
|
fallback="bot"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-end gap-1">
|
||||||
|
<AgentTypeStatusBadge isActive={type.is_active} />
|
||||||
|
<CategoryBadge category={type.category} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CardTitle className="mt-3 line-clamp-1">{type.name}</CardTitle>
|
||||||
|
<CardDescription className="line-clamp-2">
|
||||||
|
{type.description || 'No description provided'}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Expertise tags */}
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{type.expertise.slice(0, 3).map((skill) => (
|
||||||
|
<Badge key={skill} variant="secondary" className="text-xs">
|
||||||
|
{skill}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{type.expertise.length > 3 && (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
+{type.expertise.length - 3}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{type.expertise.length === 0 && (
|
||||||
|
<span className="text-xs text-muted-foreground">No expertise defined</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Metadata */}
|
||||||
|
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Cpu className="h-3.5 w-3.5" />
|
||||||
|
<span className="text-xs">{getModelDisplayName(type.primary_model)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Bot className="h-3.5 w-3.5" />
|
||||||
|
<span className="text-xs">{type.instance_count} instances</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List row view for agent type
|
||||||
|
*/
|
||||||
|
function AgentTypeListRow({
|
||||||
|
type,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
type: AgentTypeResponse;
|
||||||
|
onSelect: (id: string) => void;
|
||||||
|
}) {
|
||||||
|
const agentColor = type.color || '#3B82F6';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center gap-4 rounded-lg border p-4 transition-all hover:border-primary hover:shadow-md"
|
||||||
|
onClick={() => onSelect(type.id)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
onSelect(type.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-label={`View ${type.name} agent type`}
|
||||||
|
style={{
|
||||||
|
borderLeftColor: agentColor,
|
||||||
|
borderLeftWidth: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Icon */}
|
||||||
|
<div
|
||||||
|
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-lg"
|
||||||
|
style={{ backgroundColor: `${agentColor}15` }}
|
||||||
|
>
|
||||||
|
<DynamicIcon
|
||||||
|
name={type.icon}
|
||||||
|
className="h-6 w-6"
|
||||||
|
style={{ color: agentColor }}
|
||||||
|
fallback="bot"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold">{type.name}</h3>
|
||||||
|
<CategoryBadge category={type.category} />
|
||||||
|
</div>
|
||||||
|
<p className="line-clamp-1 text-sm text-muted-foreground">
|
||||||
|
{type.description || 'No description'}
|
||||||
|
</p>
|
||||||
|
<div className="mt-1 flex items-center gap-3 text-xs text-muted-foreground">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Cpu className="h-3 w-3" />
|
||||||
|
{getModelDisplayName(type.primary_model)}
|
||||||
|
</span>
|
||||||
|
<span>{type.expertise.length} expertise areas</span>
|
||||||
|
<span>{type.instance_count} instances</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status */}
|
||||||
|
<div className="shrink-0">
|
||||||
|
<AgentTypeStatusBadge isActive={type.is_active} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function AgentTypeList({
|
export function AgentTypeList({
|
||||||
agentTypes,
|
agentTypes,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
@@ -110,6 +325,10 @@ export function AgentTypeList({
|
|||||||
onSearchChange,
|
onSearchChange,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
onStatusFilterChange,
|
onStatusFilterChange,
|
||||||
|
categoryFilter,
|
||||||
|
onCategoryFilterChange,
|
||||||
|
viewMode,
|
||||||
|
onViewModeChange,
|
||||||
onSelect,
|
onSelect,
|
||||||
onCreate,
|
onCreate,
|
||||||
className,
|
className,
|
||||||
@@ -131,7 +350,7 @@ export function AgentTypeList({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row">
|
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
@@ -142,8 +361,25 @@ export function AgentTypeList({
|
|||||||
aria-label="Search agent types"
|
aria-label="Search agent types"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Category Filter */}
|
||||||
|
<Select value={categoryFilter} onValueChange={onCategoryFilterChange}>
|
||||||
|
<SelectTrigger className="w-full sm:w-44" aria-label="Filter by category">
|
||||||
|
<SelectValue placeholder="All Categories" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Categories</SelectItem>
|
||||||
|
{AGENT_TYPE_CATEGORIES.map((cat) => (
|
||||||
|
<SelectItem key={cat.value} value={cat.value}>
|
||||||
|
{cat.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{/* Status Filter */}
|
||||||
<Select value={statusFilter} onValueChange={onStatusFilterChange}>
|
<Select value={statusFilter} onValueChange={onStatusFilterChange}>
|
||||||
<SelectTrigger className="w-full sm:w-40" aria-label="Filter by status">
|
<SelectTrigger className="w-full sm:w-36" aria-label="Filter by status">
|
||||||
<SelectValue placeholder="Status" />
|
<SelectValue placeholder="Status" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -152,10 +388,25 @@ export function AgentTypeList({
|
|||||||
<SelectItem value="inactive">Inactive</SelectItem>
|
<SelectItem value="inactive">Inactive</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
{/* View Mode Toggle */}
|
||||||
|
<ToggleGroup
|
||||||
|
type="single"
|
||||||
|
value={viewMode}
|
||||||
|
onValueChange={(value: string) => value && onViewModeChange(value as ViewMode)}
|
||||||
|
className="hidden sm:flex"
|
||||||
|
>
|
||||||
|
<ToggleGroupItem value="grid" aria-label="Grid view" size="sm">
|
||||||
|
<LayoutGrid className="h-4 w-4" />
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="list" aria-label="List view" size="sm">
|
||||||
|
<List className="h-4 w-4" />
|
||||||
|
</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Loading State */}
|
{/* Loading State - Grid */}
|
||||||
{isLoading && (
|
{isLoading && viewMode === 'grid' && (
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||||
<AgentTypeCardSkeleton key={i} />
|
<AgentTypeCardSkeleton key={i} />
|
||||||
@@ -163,71 +414,29 @@ export function AgentTypeList({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Agent Type Grid */}
|
{/* Loading State - List */}
|
||||||
{!isLoading && agentTypes.length > 0 && (
|
{isLoading && viewMode === 'list' && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||||
|
<AgentTypeListSkeleton key={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Agent Type Grid View */}
|
||||||
|
{!isLoading && agentTypes.length > 0 && viewMode === 'grid' && (
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{agentTypes.map((type) => (
|
{agentTypes.map((type) => (
|
||||||
<Card
|
<AgentTypeGridCard key={type.id} type={type} onSelect={onSelect} />
|
||||||
key={type.id}
|
))}
|
||||||
className="cursor-pointer transition-all hover:border-primary hover:shadow-md"
|
</div>
|
||||||
onClick={() => onSelect(type.id)}
|
)}
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
onSelect(type.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
aria-label={`View ${type.name} agent type`}
|
|
||||||
>
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
|
|
||||||
<Bot className="h-5 w-5 text-primary" />
|
|
||||||
</div>
|
|
||||||
<AgentTypeStatusBadge isActive={type.is_active} />
|
|
||||||
</div>
|
|
||||||
<CardTitle className="mt-3">{type.name}</CardTitle>
|
|
||||||
<CardDescription className="line-clamp-2">
|
|
||||||
{type.description || 'No description provided'}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{/* Expertise tags */}
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{type.expertise.slice(0, 3).map((skill) => (
|
|
||||||
<Badge key={skill} variant="secondary" className="text-xs">
|
|
||||||
{skill}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
{type.expertise.length > 3 && (
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
+{type.expertise.length - 3}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{type.expertise.length === 0 && (
|
|
||||||
<span className="text-xs text-muted-foreground">No expertise defined</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
{/* Agent Type List View */}
|
||||||
|
{!isLoading && agentTypes.length > 0 && viewMode === 'list' && (
|
||||||
{/* Metadata */}
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
{agentTypes.map((type) => (
|
||||||
<div className="flex items-center gap-1">
|
<AgentTypeListRow key={type.id} type={type} onSelect={onSelect} />
|
||||||
<Cpu className="h-3.5 w-3.5" />
|
|
||||||
<span className="text-xs">{getModelDisplayName(type.primary_model)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Bot className="h-3.5 w-3.5" />
|
|
||||||
<span className="text-xs">{type.instance_count} instances</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -238,11 +447,11 @@ export function AgentTypeList({
|
|||||||
<Bot className="mx-auto h-12 w-12 text-muted-foreground" />
|
<Bot className="mx-auto h-12 w-12 text-muted-foreground" />
|
||||||
<h3 className="mt-4 font-semibold">No agent types found</h3>
|
<h3 className="mt-4 font-semibold">No agent types found</h3>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{searchQuery || statusFilter !== 'all'
|
{searchQuery || statusFilter !== 'all' || categoryFilter !== 'all'
|
||||||
? 'Try adjusting your search or filters'
|
? 'Try adjusting your search or filters'
|
||||||
: 'Create your first agent type to get started'}
|
: 'Create your first agent type to get started'}
|
||||||
</p>
|
</p>
|
||||||
{!searchQuery && statusFilter === 'all' && (
|
{!searchQuery && statusFilter === 'all' && categoryFilter === 'all' && (
|
||||||
<Button onClick={onCreate} className="mt-4">
|
<Button onClick={onCreate} className="mt-4">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Create Agent Type
|
Create Agent Type
|
||||||
|
|||||||
@@ -5,5 +5,5 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export { AgentTypeForm } from './AgentTypeForm';
|
export { AgentTypeForm } from './AgentTypeForm';
|
||||||
export { AgentTypeList } from './AgentTypeList';
|
export { AgentTypeList, type ViewMode } from './AgentTypeList';
|
||||||
export { AgentTypeDetail } from './AgentTypeDetail';
|
export { AgentTypeDetail } from './AgentTypeDetail';
|
||||||
|
|||||||
84
frontend/src/components/ui/dynamic-icon.tsx
Normal file
84
frontend/src/components/ui/dynamic-icon.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* DynamicIcon Component
|
||||||
|
*
|
||||||
|
* Renders Lucide icons dynamically by name string.
|
||||||
|
* Useful when icon names come from data (e.g., database).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as LucideIcons from 'lucide-react';
|
||||||
|
import type { LucideProps } from 'lucide-react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of icon names to their components.
|
||||||
|
* Uses kebab-case names (e.g., 'clipboard-check') as keys.
|
||||||
|
*/
|
||||||
|
const iconMap: Record<string, React.ComponentType<LucideProps>> = {
|
||||||
|
// Development
|
||||||
|
'clipboard-check': LucideIcons.ClipboardCheck,
|
||||||
|
briefcase: LucideIcons.Briefcase,
|
||||||
|
'file-text': LucideIcons.FileText,
|
||||||
|
'git-branch': LucideIcons.GitBranch,
|
||||||
|
code: LucideIcons.Code,
|
||||||
|
server: LucideIcons.Server,
|
||||||
|
layout: LucideIcons.Layout,
|
||||||
|
smartphone: LucideIcons.Smartphone,
|
||||||
|
// Design
|
||||||
|
palette: LucideIcons.Palette,
|
||||||
|
search: LucideIcons.Search,
|
||||||
|
// Quality
|
||||||
|
shield: LucideIcons.Shield,
|
||||||
|
'shield-check': LucideIcons.ShieldCheck,
|
||||||
|
// Operations
|
||||||
|
settings: LucideIcons.Settings,
|
||||||
|
'settings-2': LucideIcons.Settings2,
|
||||||
|
// AI/ML
|
||||||
|
brain: LucideIcons.Brain,
|
||||||
|
microscope: LucideIcons.Microscope,
|
||||||
|
eye: LucideIcons.Eye,
|
||||||
|
'message-square': LucideIcons.MessageSquare,
|
||||||
|
// Data
|
||||||
|
'bar-chart': LucideIcons.BarChart,
|
||||||
|
database: LucideIcons.Database,
|
||||||
|
// Leadership
|
||||||
|
users: LucideIcons.Users,
|
||||||
|
target: LucideIcons.Target,
|
||||||
|
// Domain Expert
|
||||||
|
calculator: LucideIcons.Calculator,
|
||||||
|
'heart-pulse': LucideIcons.HeartPulse,
|
||||||
|
'flask-conical': LucideIcons.FlaskConical,
|
||||||
|
lightbulb: LucideIcons.Lightbulb,
|
||||||
|
'book-open': LucideIcons.BookOpen,
|
||||||
|
// Generic
|
||||||
|
bot: LucideIcons.Bot,
|
||||||
|
cpu: LucideIcons.Cpu,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DynamicIconProps extends Omit<LucideProps, 'name'> {
|
||||||
|
/** Icon name in kebab-case (e.g., 'clipboard-check', 'bot') */
|
||||||
|
name: string | null | undefined;
|
||||||
|
/** Fallback icon name if the specified icon is not found */
|
||||||
|
fallback?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a Lucide icon dynamically by name.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <DynamicIcon name="clipboard-check" className="h-5 w-5" />
|
||||||
|
* <DynamicIcon name={agent.icon} fallback="bot" />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function DynamicIcon({ name, fallback = 'bot', ...props }: DynamicIconProps) {
|
||||||
|
const iconName = name || fallback;
|
||||||
|
const IconComponent = iconMap[iconName] || iconMap[fallback] || LucideIcons.Bot;
|
||||||
|
|
||||||
|
return <IconComponent {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available icon names for validation or display
|
||||||
|
*/
|
||||||
|
export function getAvailableIconNames(): string[] {
|
||||||
|
return Object.keys(iconMap);
|
||||||
|
}
|
||||||
93
frontend/src/components/ui/toggle-group.tsx
Normal file
93
frontend/src/components/ui/toggle-group.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
|
||||||
|
import { type VariantProps, cva } from 'class-variance-authority';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const toggleGroupVariants = cva(
|
||||||
|
'inline-flex items-center justify-center rounded-md border bg-transparent',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-transparent',
|
||||||
|
outline: 'border border-input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'outline',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleGroupItemVariants = cva(
|
||||||
|
'inline-flex items-center justify-center whitespace-nowrap text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-transparent hover:bg-muted hover:text-muted-foreground',
|
||||||
|
outline: 'bg-transparent hover:bg-accent hover:text-accent-foreground',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: 'h-10 px-3',
|
||||||
|
sm: 'h-9 px-2.5',
|
||||||
|
lg: 'h-11 px-5',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
size: 'default',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const ToggleGroupContext = React.createContext<VariantProps<typeof toggleGroupItemVariants>>({
|
||||||
|
size: 'default',
|
||||||
|
variant: 'default',
|
||||||
|
});
|
||||||
|
|
||||||
|
const ToggleGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleGroupVariants> &
|
||||||
|
VariantProps<typeof toggleGroupItemVariants>
|
||||||
|
>(({ className, variant, size, children, ...props }, ref) => (
|
||||||
|
<ToggleGroupPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(toggleGroupVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ToggleGroupContext.Provider value={{ variant, size }}>{children}</ToggleGroupContext.Provider>
|
||||||
|
</ToggleGroupPrimitive.Root>
|
||||||
|
));
|
||||||
|
|
||||||
|
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const ToggleGroupItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
|
||||||
|
VariantProps<typeof toggleGroupItemVariants>
|
||||||
|
>(({ className, children, variant, size, ...props }, ref) => {
|
||||||
|
const context = React.useContext(ToggleGroupContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleGroupPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
toggleGroupItemVariants({
|
||||||
|
variant: context.variant || variant,
|
||||||
|
size: context.size || size,
|
||||||
|
}),
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupPrimitive.Item>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
export { ToggleGroup, ToggleGroupItem };
|
||||||
@@ -44,10 +44,10 @@ const DEFAULT_PAGE_LIMIT = 20;
|
|||||||
export function useAgentTypes(params: AgentTypeListParams = {}) {
|
export function useAgentTypes(params: AgentTypeListParams = {}) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
const { page = 1, limit = DEFAULT_PAGE_LIMIT, is_active = true, search } = params;
|
const { page = 1, limit = DEFAULT_PAGE_LIMIT, is_active = true, search, category } = params;
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: agentTypeKeys.list({ page, limit, is_active, search }),
|
queryKey: agentTypeKeys.list({ page, limit, is_active, search, category }),
|
||||||
queryFn: async (): Promise<AgentTypeListResponse> => {
|
queryFn: async (): Promise<AgentTypeListResponse> => {
|
||||||
const response = await apiClient.instance.get('/api/v1/agent-types', {
|
const response = await apiClient.instance.get('/api/v1/agent-types', {
|
||||||
params: {
|
params: {
|
||||||
@@ -55,6 +55,7 @@ export function useAgentTypes(params: AgentTypeListParams = {}) {
|
|||||||
limit,
|
limit,
|
||||||
is_active,
|
is_active,
|
||||||
...(search ? { search } : {}),
|
...(search ? { search } : {}),
|
||||||
|
...(category ? { category } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -65,9 +65,8 @@ describe('AgentTypeDetail', () => {
|
|||||||
expect(screen.getByText('Inactive')).toBeInTheDocument();
|
expect(screen.getByText('Inactive')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders description card', () => {
|
it('renders description in hero header', () => {
|
||||||
render(<AgentTypeDetail {...defaultProps} />);
|
render(<AgentTypeDetail {...defaultProps} />);
|
||||||
expect(screen.getByText('Description')).toBeInTheDocument();
|
|
||||||
expect(
|
expect(
|
||||||
screen.getByText('Designs system architecture and makes technology decisions')
|
screen.getByText('Designs system architecture and makes technology decisions')
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
@@ -137,7 +136,7 @@ describe('AgentTypeDetail', () => {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<AgentTypeDetail {...defaultProps} />);
|
render(<AgentTypeDetail {...defaultProps} />);
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: /go back/i }));
|
await user.click(screen.getByRole('button', { name: /back to agent types/i }));
|
||||||
expect(defaultProps.onBack).toHaveBeenCalledTimes(1);
|
expect(defaultProps.onBack).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -218,4 +217,146 @@ describe('AgentTypeDetail', () => {
|
|||||||
);
|
);
|
||||||
expect(screen.getByText('None configured')).toBeInTheDocument();
|
expect(screen.getByText('None configured')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Hero Header', () => {
|
||||||
|
it('renders hero header with agent name', () => {
|
||||||
|
render(<AgentTypeDetail {...defaultProps} />);
|
||||||
|
expect(
|
||||||
|
screen.getByRole('heading', { level: 1, name: 'Software Architect' })
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders dynamic icon in hero header', () => {
|
||||||
|
const { container } = render(<AgentTypeDetail {...defaultProps} />);
|
||||||
|
expect(container.querySelector('svg.lucide-git-branch')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies agent color to hero header gradient', () => {
|
||||||
|
const { container } = render(<AgentTypeDetail {...defaultProps} />);
|
||||||
|
const heroHeader = container.querySelector('[style*="linear-gradient"]');
|
||||||
|
expect(heroHeader).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders category badge in hero header', () => {
|
||||||
|
render(<AgentTypeDetail {...defaultProps} />);
|
||||||
|
expect(screen.getByText('Development')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows last updated date in hero header', () => {
|
||||||
|
render(<AgentTypeDetail {...defaultProps} />);
|
||||||
|
expect(screen.getByText(/Last updated:/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Jan 18, 2025/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Typical Tasks Card', () => {
|
||||||
|
it('renders "What This Agent Does Best" card', () => {
|
||||||
|
render(<AgentTypeDetail {...defaultProps} />);
|
||||||
|
expect(screen.getByText('What This Agent Does Best')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays all typical tasks', () => {
|
||||||
|
render(<AgentTypeDetail {...defaultProps} />);
|
||||||
|
expect(screen.getByText('Design system architecture')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Create ADRs')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render typical tasks card when empty', () => {
|
||||||
|
render(
|
||||||
|
<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, typical_tasks: [] }} />
|
||||||
|
);
|
||||||
|
expect(screen.queryByText('What This Agent Does Best')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Collaboration Hints Card', () => {
|
||||||
|
it('renders "Works Well With" card', () => {
|
||||||
|
render(<AgentTypeDetail {...defaultProps} />);
|
||||||
|
expect(screen.getByText('Works Well With')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays collaboration hints as badges', () => {
|
||||||
|
render(<AgentTypeDetail {...defaultProps} />);
|
||||||
|
expect(screen.getByText('backend-engineer')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('frontend-engineer')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render collaboration hints card when empty', () => {
|
||||||
|
render(
|
||||||
|
<AgentTypeDetail
|
||||||
|
{...defaultProps}
|
||||||
|
agentType={{ ...mockAgentType, collaboration_hints: [] }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.queryByText('Works Well With')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Category Badge', () => {
|
||||||
|
it('renders category badge with correct label', () => {
|
||||||
|
render(<AgentTypeDetail {...defaultProps} />);
|
||||||
|
expect(screen.getByText('Development')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render category badge when category is null', () => {
|
||||||
|
render(
|
||||||
|
<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, category: null }} />
|
||||||
|
);
|
||||||
|
// Should not have a Development badge in the hero header area
|
||||||
|
// The word "Development" should not appear
|
||||||
|
expect(screen.queryByText('Development')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Details Card', () => {
|
||||||
|
it('renders details card with slug', () => {
|
||||||
|
render(<AgentTypeDetail {...defaultProps} />);
|
||||||
|
expect(screen.getByText('Slug')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('software-architect')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders details card with sort order', () => {
|
||||||
|
render(<AgentTypeDetail {...defaultProps} />);
|
||||||
|
expect(screen.getByText('Sort Order')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('40')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders details card with creation date', () => {
|
||||||
|
render(<AgentTypeDetail {...defaultProps} />);
|
||||||
|
expect(screen.getByText('Created')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Jan 10, 2025/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Dynamic Icon', () => {
|
||||||
|
it('renders fallback icon when icon is null', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, icon: null }} />
|
||||||
|
);
|
||||||
|
// Should fall back to 'bot' icon
|
||||||
|
expect(container.querySelector('svg.lucide-bot')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correct icon based on agent type', () => {
|
||||||
|
const agentWithBrainIcon = { ...mockAgentType, icon: 'brain' };
|
||||||
|
const { container } = render(
|
||||||
|
<AgentTypeDetail {...defaultProps} agentType={agentWithBrainIcon} />
|
||||||
|
);
|
||||||
|
expect(container.querySelector('svg.lucide-brain')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Color Styling', () => {
|
||||||
|
it('applies custom color to instance count', () => {
|
||||||
|
render(<AgentTypeDetail {...defaultProps} />);
|
||||||
|
const instanceCount = screen.getByText('2');
|
||||||
|
expect(instanceCount).toHaveStyle({ color: 'rgb(59, 130, 246)' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses default color when color is null', () => {
|
||||||
|
render(<AgentTypeDetail {...defaultProps} agentType={{ ...mockAgentType, color: null }} />);
|
||||||
|
// Should still render without errors
|
||||||
|
expect(screen.getByText('Software Architect')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ describe('AgentTypeList', () => {
|
|||||||
onSearchChange: jest.fn(),
|
onSearchChange: jest.fn(),
|
||||||
statusFilter: 'all',
|
statusFilter: 'all',
|
||||||
onStatusFilterChange: jest.fn(),
|
onStatusFilterChange: jest.fn(),
|
||||||
|
categoryFilter: 'all',
|
||||||
|
onCategoryFilterChange: jest.fn(),
|
||||||
|
viewMode: 'grid' as const,
|
||||||
|
onViewModeChange: jest.fn(),
|
||||||
onSelect: jest.fn(),
|
onSelect: jest.fn(),
|
||||||
onCreate: jest.fn(),
|
onCreate: jest.fn(),
|
||||||
};
|
};
|
||||||
@@ -208,4 +212,158 @@ describe('AgentTypeList', () => {
|
|||||||
const { container } = render(<AgentTypeList {...defaultProps} className="custom-class" />);
|
const { container } = render(<AgentTypeList {...defaultProps} className="custom-class" />);
|
||||||
expect(container.firstChild).toHaveClass('custom-class');
|
expect(container.firstChild).toHaveClass('custom-class');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Category Filter', () => {
|
||||||
|
it('renders category filter dropdown', () => {
|
||||||
|
render(<AgentTypeList {...defaultProps} />);
|
||||||
|
expect(screen.getByRole('combobox', { name: /filter by category/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows "All Categories" as default option', () => {
|
||||||
|
render(<AgentTypeList {...defaultProps} categoryFilter="all" />);
|
||||||
|
expect(screen.getByText('All Categories')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays category badge on agent cards', () => {
|
||||||
|
render(<AgentTypeList {...defaultProps} />);
|
||||||
|
// Both agents have 'development' category
|
||||||
|
const developmentBadges = screen.getAllByText('Development');
|
||||||
|
expect(developmentBadges.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows filter hint in empty state when category filter is applied', () => {
|
||||||
|
render(<AgentTypeList {...defaultProps} agentTypes={[]} categoryFilter="design" />);
|
||||||
|
expect(screen.getByText('Try adjusting your search or filters')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('View Mode Toggle', () => {
|
||||||
|
it('renders view mode toggle buttons', () => {
|
||||||
|
render(<AgentTypeList {...defaultProps} />);
|
||||||
|
expect(screen.getByRole('radio', { name: /grid view/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('radio', { name: /list view/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders grid view by default', () => {
|
||||||
|
const { container } = render(<AgentTypeList {...defaultProps} viewMode="grid" />);
|
||||||
|
// Grid view uses CSS grid
|
||||||
|
expect(container.querySelector('.grid')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders list view when viewMode is list', () => {
|
||||||
|
const { container } = render(<AgentTypeList {...defaultProps} viewMode="list" />);
|
||||||
|
// List view uses space-y-3 for vertical stacking
|
||||||
|
expect(container.querySelector('.space-y-3')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onViewModeChange when grid toggle is clicked', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onViewModeChange = jest.fn();
|
||||||
|
render(
|
||||||
|
<AgentTypeList {...defaultProps} viewMode="list" onViewModeChange={onViewModeChange} />
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('radio', { name: /grid view/i }));
|
||||||
|
expect(onViewModeChange).toHaveBeenCalledWith('grid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onViewModeChange when list toggle is clicked', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onViewModeChange = jest.fn();
|
||||||
|
render(
|
||||||
|
<AgentTypeList {...defaultProps} viewMode="grid" onViewModeChange={onViewModeChange} />
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('radio', { name: /list view/i }));
|
||||||
|
expect(onViewModeChange).toHaveBeenCalledWith('list');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows list-specific loading skeletons when viewMode is list', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<AgentTypeList {...defaultProps} agentTypes={[]} isLoading={true} viewMode="list" />
|
||||||
|
);
|
||||||
|
expect(container.querySelectorAll('.animate-pulse').length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('List View', () => {
|
||||||
|
it('shows agent info in list rows', () => {
|
||||||
|
render(<AgentTypeList {...defaultProps} viewMode="list" />);
|
||||||
|
expect(screen.getByText('Product Owner')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Software Architect')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows category badge in list view', () => {
|
||||||
|
render(<AgentTypeList {...defaultProps} viewMode="list" />);
|
||||||
|
const developmentBadges = screen.getAllByText('Development');
|
||||||
|
expect(developmentBadges.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows expertise count in list view', () => {
|
||||||
|
render(<AgentTypeList {...defaultProps} viewMode="list" />);
|
||||||
|
// Both agents have 3 expertise areas
|
||||||
|
const expertiseTexts = screen.getAllByText('3 expertise areas');
|
||||||
|
expect(expertiseTexts.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onSelect when list row is clicked', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onSelect = jest.fn();
|
||||||
|
render(<AgentTypeList {...defaultProps} viewMode="list" onSelect={onSelect} />);
|
||||||
|
|
||||||
|
await user.click(screen.getByText('Product Owner'));
|
||||||
|
expect(onSelect).toHaveBeenCalledWith('type-001');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports keyboard navigation on list rows', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onSelect = jest.fn();
|
||||||
|
render(<AgentTypeList {...defaultProps} viewMode="list" onSelect={onSelect} />);
|
||||||
|
|
||||||
|
const rows = screen.getAllByRole('button', { name: /view .* agent type/i });
|
||||||
|
rows[0].focus();
|
||||||
|
await user.keyboard('{Enter}');
|
||||||
|
expect(onSelect).toHaveBeenCalledWith('type-001');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Dynamic Icons', () => {
|
||||||
|
it('renders agent icon in grid view', () => {
|
||||||
|
const { container } = render(<AgentTypeList {...defaultProps} viewMode="grid" />);
|
||||||
|
// Check for svg icons with lucide classes
|
||||||
|
const icons = container.querySelectorAll('svg.lucide-clipboard-check, svg.lucide-git-branch');
|
||||||
|
expect(icons.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders agent icon in list view', () => {
|
||||||
|
const { container } = render(<AgentTypeList {...defaultProps} viewMode="list" />);
|
||||||
|
const icons = container.querySelectorAll('svg.lucide-clipboard-check, svg.lucide-git-branch');
|
||||||
|
expect(icons.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Color Accent', () => {
|
||||||
|
it('applies color to card border in grid view', () => {
|
||||||
|
const { container } = render(<AgentTypeList {...defaultProps} viewMode="grid" />);
|
||||||
|
const card = container.querySelector('[style*="border-top-color"]');
|
||||||
|
expect(card).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies color to row border in list view', () => {
|
||||||
|
const { container } = render(<AgentTypeList {...defaultProps} viewMode="list" />);
|
||||||
|
const row = container.querySelector('[style*="border-left-color"]');
|
||||||
|
expect(row).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Category Badge Component', () => {
|
||||||
|
it('does not render category badge when category is null', () => {
|
||||||
|
const agentWithNoCategory: AgentTypeResponse = {
|
||||||
|
...mockAgentTypes[0],
|
||||||
|
category: null,
|
||||||
|
};
|
||||||
|
render(<AgentTypeList {...defaultProps} agentTypes={[agentWithNoCategory]} />);
|
||||||
|
expect(screen.queryByText('Development')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
158
frontend/tests/components/ui/DynamicIcon.test.tsx
Normal file
158
frontend/tests/components/ui/DynamicIcon.test.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
/**
|
||||||
|
* Tests for DynamicIcon Component
|
||||||
|
* Verifies dynamic icon rendering by name string
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { DynamicIcon, getAvailableIconNames } from '@/components/ui/dynamic-icon';
|
||||||
|
|
||||||
|
describe('DynamicIcon', () => {
|
||||||
|
describe('Basic Rendering', () => {
|
||||||
|
it('renders an icon by name', () => {
|
||||||
|
render(<DynamicIcon name="bot" data-testid="icon" />);
|
||||||
|
const icon = screen.getByTestId('icon');
|
||||||
|
expect(icon).toBeInTheDocument();
|
||||||
|
expect(icon.tagName).toBe('svg');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders different icons by name', () => {
|
||||||
|
const { rerender } = render(<DynamicIcon name="code" data-testid="icon" />);
|
||||||
|
expect(screen.getByTestId('icon')).toHaveClass('lucide-code');
|
||||||
|
|
||||||
|
rerender(<DynamicIcon name="brain" data-testid="icon" />);
|
||||||
|
expect(screen.getByTestId('icon')).toHaveClass('lucide-brain');
|
||||||
|
|
||||||
|
rerender(<DynamicIcon name="shield" data-testid="icon" />);
|
||||||
|
expect(screen.getByTestId('icon')).toHaveClass('lucide-shield');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders kebab-case icon names correctly', () => {
|
||||||
|
render(<DynamicIcon name="clipboard-check" data-testid="icon" />);
|
||||||
|
expect(screen.getByTestId('icon')).toHaveClass('lucide-clipboard-check');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Fallback Behavior', () => {
|
||||||
|
it('renders fallback icon when name is null', () => {
|
||||||
|
render(<DynamicIcon name={null} data-testid="icon" />);
|
||||||
|
expect(screen.getByTestId('icon')).toHaveClass('lucide-bot');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders fallback icon when name is undefined', () => {
|
||||||
|
render(<DynamicIcon name={undefined} data-testid="icon" />);
|
||||||
|
expect(screen.getByTestId('icon')).toHaveClass('lucide-bot');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders fallback icon when name is not found', () => {
|
||||||
|
render(<DynamicIcon name="nonexistent-icon" data-testid="icon" />);
|
||||||
|
expect(screen.getByTestId('icon')).toHaveClass('lucide-bot');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses custom fallback when specified', () => {
|
||||||
|
render(<DynamicIcon name={null} fallback="code" data-testid="icon" />);
|
||||||
|
expect(screen.getByTestId('icon')).toHaveClass('lucide-code');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to bot when custom fallback is also invalid', () => {
|
||||||
|
render(<DynamicIcon name="invalid" fallback="also-invalid" data-testid="icon" />);
|
||||||
|
expect(screen.getByTestId('icon')).toHaveClass('lucide-bot');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Props Forwarding', () => {
|
||||||
|
it('forwards className to icon', () => {
|
||||||
|
render(<DynamicIcon name="bot" className="h-5 w-5 text-primary" data-testid="icon" />);
|
||||||
|
const icon = screen.getByTestId('icon');
|
||||||
|
expect(icon).toHaveClass('h-5');
|
||||||
|
expect(icon).toHaveClass('w-5');
|
||||||
|
expect(icon).toHaveClass('text-primary');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forwards style to icon', () => {
|
||||||
|
render(<DynamicIcon name="bot" style={{ color: 'red' }} data-testid="icon" />);
|
||||||
|
const icon = screen.getByTestId('icon');
|
||||||
|
expect(icon).toHaveStyle({ color: 'rgb(255, 0, 0)' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forwards aria-hidden to icon', () => {
|
||||||
|
render(<DynamicIcon name="bot" aria-hidden="true" data-testid="icon" />);
|
||||||
|
const icon = screen.getByTestId('icon');
|
||||||
|
expect(icon).toHaveAttribute('aria-hidden', 'true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Available Icons', () => {
|
||||||
|
it('includes development icons', () => {
|
||||||
|
const icons = getAvailableIconNames();
|
||||||
|
expect(icons).toContain('clipboard-check');
|
||||||
|
expect(icons).toContain('briefcase');
|
||||||
|
expect(icons).toContain('code');
|
||||||
|
expect(icons).toContain('server');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes design icons', () => {
|
||||||
|
const icons = getAvailableIconNames();
|
||||||
|
expect(icons).toContain('palette');
|
||||||
|
expect(icons).toContain('search');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes quality icons', () => {
|
||||||
|
const icons = getAvailableIconNames();
|
||||||
|
expect(icons).toContain('shield');
|
||||||
|
expect(icons).toContain('shield-check');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes ai_ml icons', () => {
|
||||||
|
const icons = getAvailableIconNames();
|
||||||
|
expect(icons).toContain('brain');
|
||||||
|
expect(icons).toContain('microscope');
|
||||||
|
expect(icons).toContain('eye');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes data icons', () => {
|
||||||
|
const icons = getAvailableIconNames();
|
||||||
|
expect(icons).toContain('bar-chart');
|
||||||
|
expect(icons).toContain('database');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes domain expert icons', () => {
|
||||||
|
const icons = getAvailableIconNames();
|
||||||
|
expect(icons).toContain('calculator');
|
||||||
|
expect(icons).toContain('heart-pulse');
|
||||||
|
expect(icons).toContain('flask-conical');
|
||||||
|
expect(icons).toContain('lightbulb');
|
||||||
|
expect(icons).toContain('book-open');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes generic icons', () => {
|
||||||
|
const icons = getAvailableIconNames();
|
||||||
|
expect(icons).toContain('bot');
|
||||||
|
expect(icons).toContain('cpu');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Icon Categories Coverage', () => {
|
||||||
|
const iconTestCases = [
|
||||||
|
// Development
|
||||||
|
{ name: 'clipboard-check', expectedClass: 'lucide-clipboard-check' },
|
||||||
|
{ name: 'briefcase', expectedClass: 'lucide-briefcase' },
|
||||||
|
{ name: 'file-text', expectedClass: 'lucide-file-text' },
|
||||||
|
{ name: 'git-branch', expectedClass: 'lucide-git-branch' },
|
||||||
|
{ name: 'layout', expectedClass: 'lucide-panels-top-left' },
|
||||||
|
{ name: 'smartphone', expectedClass: 'lucide-smartphone' },
|
||||||
|
// Operations
|
||||||
|
{ name: 'settings', expectedClass: 'lucide-settings' },
|
||||||
|
{ name: 'settings-2', expectedClass: 'lucide-settings-2' },
|
||||||
|
// AI/ML
|
||||||
|
{ name: 'message-square', expectedClass: 'lucide-message-square' },
|
||||||
|
// Leadership
|
||||||
|
{ name: 'users', expectedClass: 'lucide-users' },
|
||||||
|
{ name: 'target', expectedClass: 'lucide-target' },
|
||||||
|
];
|
||||||
|
|
||||||
|
it.each(iconTestCases)('renders $name icon correctly', ({ name, expectedClass }) => {
|
||||||
|
render(<DynamicIcon name={name} data-testid="icon" />);
|
||||||
|
expect(screen.getByTestId('icon')).toHaveClass(expectedClass);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user