From fe2104822e96bae1b1e96793b0c929fc3c488b36 Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Sat, 3 Jan 2026 02:12:26 +0100 Subject: [PATCH] feat(frontend): add Projects, Agents, and Settings pages for enhanced project management - Added routing and localization for "Projects" and "Agents" in `Header.tsx`. - Introduced `ProjectAgentsPage` to manage and display agent details per project. - Added `ProjectActivityPage` for real-time event tracking and approval workflows. - Implemented `ProjectSettingsPage` for project configuration, including autonomy levels and repository integration. - Updated language files (`en.json`, `it.json`) with new translations for "Projects" and "Agents". --- frontend/messages/en.json | 4 +- frontend/messages/it.json | 4 +- .../projects/[id]/activity/page.tsx | 249 +++++++++++++++ .../projects/[id]/agents/page.tsx | 245 +++++++++++++++ .../projects/[id]/settings/page.tsx | 296 ++++++++++++++++++ frontend/src/components/layout/Header.tsx | 2 + 6 files changed, 798 insertions(+), 2 deletions(-) create mode 100644 frontend/src/app/[locale]/(authenticated)/projects/[id]/activity/page.tsx create mode 100644 frontend/src/app/[locale]/(authenticated)/projects/[id]/agents/page.tsx create mode 100644 frontend/src/app/[locale]/(authenticated)/projects/[id]/settings/page.tsx diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 888bce2..aa37ff0 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -32,7 +32,9 @@ "demos": "Demos", "design": "Design System", "admin": "Admin", - "adminPanel": "Admin Panel" + "adminPanel": "Admin Panel", + "projects": "Projects", + "agents": "Agents" }, "auth": { "login": { diff --git a/frontend/messages/it.json b/frontend/messages/it.json index 4c65efc..d5baf24 100644 --- a/frontend/messages/it.json +++ b/frontend/messages/it.json @@ -32,7 +32,9 @@ "demos": "Demo", "design": "Design System", "admin": "Admin", - "adminPanel": "Pannello Admin" + "adminPanel": "Pannello Admin", + "projects": "Progetti", + "agents": "Agenti" }, "auth": { "login": { diff --git a/frontend/src/app/[locale]/(authenticated)/projects/[id]/activity/page.tsx b/frontend/src/app/[locale]/(authenticated)/projects/[id]/activity/page.tsx new file mode 100644 index 0000000..d6f6ed7 --- /dev/null +++ b/frontend/src/app/[locale]/(authenticated)/projects/[id]/activity/page.tsx @@ -0,0 +1,249 @@ +'use client'; + +/** + * Project Activity Page + * + * Full-page view of real-time project activity with: + * - Real-time SSE connection for project events + * - Event filtering and search + * - Time-based grouping + * - Approval handling + * + * @module app/[locale]/(authenticated)/projects/[id]/activity/page + */ + +import { use, useState, useCallback } from 'react'; +import { useRouter } from '@/lib/i18n/routing'; +import { ArrowLeft, RefreshCw, Bell, BellOff, AlertTriangle } from 'lucide-react'; +import { useProjectEvents } from '@/lib/hooks/useProjectEvents'; +import { ActivityFeed } from '@/components/activity'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { EventType, type ProjectEvent } from '@/lib/types/events'; +import { toast } from 'sonner'; + +interface ProjectActivityPageProps { + params: Promise<{ + locale: string; + id: string; + }>; +} + +// Demo events for when SSE is not connected +function getDemoEvents(projectId: string): ProjectEvent[] { + return [ + { + id: 'demo-001', + type: EventType.APPROVAL_REQUESTED, + timestamp: new Date(Date.now() - 1000 * 60 * 5).toISOString(), + project_id: projectId, + actor_id: 'agent-001', + actor_type: 'agent', + payload: { + approval_id: 'apr-001', + approval_type: 'architecture_decision', + description: 'Approval required for API design document for the checkout flow.', + requested_by: 'Architect', + timeout_minutes: 60, + }, + }, + { + id: 'demo-002', + type: EventType.AGENT_MESSAGE, + timestamp: new Date(Date.now() - 1000 * 60 * 10).toISOString(), + project_id: projectId, + actor_id: 'agent-002', + actor_type: 'agent', + payload: { + agent_instance_id: 'agent-002', + message: 'Completed JWT token generation and validation. Moving on to session management.', + message_type: 'info', + metadata: { progress: 65 }, + }, + }, + { + id: 'demo-003', + type: EventType.AGENT_STATUS_CHANGED, + timestamp: new Date(Date.now() - 1000 * 60 * 20).toISOString(), + project_id: projectId, + actor_id: 'agent-003', + actor_type: 'agent', + payload: { + agent_instance_id: 'agent-003', + previous_status: 'idle', + new_status: 'active', + reason: 'Started working on product catalog component', + }, + }, + { + id: 'demo-004', + type: EventType.ISSUE_UPDATED, + timestamp: new Date(Date.now() - 1000 * 60 * 30).toISOString(), + project_id: projectId, + actor_id: 'agent-002', + actor_type: 'agent', + payload: { + issue_id: 'issue-038', + changes: { status: { from: 'in_progress', to: 'in_review' } }, + }, + }, + { + id: 'demo-005', + type: EventType.SPRINT_STARTED, + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString(), + project_id: projectId, + actor_id: null, + actor_type: 'system', + payload: { + sprint_id: 'sprint-003', + sprint_name: 'Sprint 3 - Checkout Flow', + goal: 'Complete checkout and payment integration', + issue_count: 15, + }, + }, + { + id: 'demo-006', + type: EventType.ISSUE_CREATED, + timestamp: new Date(Date.now() - 1000 * 60 * 60 * 4).toISOString(), + project_id: projectId, + actor_id: 'agent-001', + actor_type: 'agent', + payload: { + issue_id: 'issue-050', + title: 'Implement shopping cart persistence', + priority: 'high', + labels: ['feature', 'sprint-3'], + }, + }, + ]; +} + +export default function ProjectActivityPage({ params }: ProjectActivityPageProps) { + const { id: projectId } = use(params); + const router = useRouter(); + const [notificationsEnabled, setNotificationsEnabled] = useState(true); + + // SSE hook for real-time events + const { + events: sseEvents, + connectionState, + reconnect, + isConnected, + } = useProjectEvents(projectId, { + autoConnect: true, + onEvent: (event) => { + // Show notification for new events if enabled + if (notificationsEnabled && event.type === EventType.APPROVAL_REQUESTED) { + toast.info('New approval request', { + description: 'An agent is requesting your approval.', + }); + } + }, + }); + + // Use demo events when not connected or no events received + const demoEvents = getDemoEvents(projectId); + const events = isConnected && sseEvents.length > 0 ? sseEvents : demoEvents; + + const handleBack = useCallback(() => { + router.push(`/projects/${projectId}`); + }, [router, projectId]); + + // Approval handlers + const handleApprove = useCallback((event: ProjectEvent) => { + // TODO: Call API to approve the request + toast.success('Approval granted', { + description: `Approved request ${event.id}`, + }); + }, []); + + const handleReject = useCallback((event: ProjectEvent) => { + // TODO: Call API to reject the request + toast.info('Approval rejected', { + description: `Rejected request ${event.id}`, + }); + }, []); + + const handleEventClick = useCallback( + (event: ProjectEvent) => { + // Navigate to relevant detail page based on event type + if (event.type.startsWith('issue.')) { + const payload = event.payload as Record; + if (payload.issue_id) { + router.push(`/projects/${projectId}/issues/${payload.issue_id}`); + } + } + }, + [router, projectId] + ); + + const pendingCount = events.filter((e) => e.type === EventType.APPROVAL_REQUESTED).length; + + return ( +
+
+ {/* Page Header */} +
+
+ +
+

Project Activity

+

Real-time updates from this project

+
+
+
+ {pendingCount > 0 && ( + + + {pendingCount} pending + + )} + + +
+
+ + {/* Demo Mode Banner */} + {(!isConnected || sseEvents.length === 0) && ( +
+

+ Demo Mode: Showing sample events. Events will appear here when agents + are active. +

+
+ )} + + {/* Activity Feed Component */} + +
+
+ ); +} diff --git a/frontend/src/app/[locale]/(authenticated)/projects/[id]/agents/page.tsx b/frontend/src/app/[locale]/(authenticated)/projects/[id]/agents/page.tsx new file mode 100644 index 0000000..29a56d6 --- /dev/null +++ b/frontend/src/app/[locale]/(authenticated)/projects/[id]/agents/page.tsx @@ -0,0 +1,245 @@ +'use client'; + +/** + * Project Agents Page + * + * Displays and manages agents assigned to a specific project. + * Shows agent status, current tasks, and provides management controls. + * + * @module app/[locale]/(authenticated)/projects/[id]/agents/page + */ + +import { use, useCallback } from 'react'; +import { useRouter } from '@/lib/i18n/routing'; +import { ArrowLeft, Bot, Plus, Settings2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { cn } from '@/lib/utils'; + +interface ProjectAgentsPageProps { + params: Promise<{ + locale: string; + id: string; + }>; +} + +type AgentStatus = 'working' | 'idle' | 'waiting' | 'error' | 'terminated'; + +interface MockAgent { + id: string; + name: string; + role: string; + status: AgentStatus; + current_task: string; + last_activity_at: string; + avatar: string; +} + +// Mock agents data - will be replaced with API calls +const mockAgents: MockAgent[] = [ + { + id: 'agent-001', + name: 'Product Owner', + role: 'product_owner', + status: 'working', + current_task: 'Reviewing user story acceptance criteria', + last_activity_at: new Date(Date.now() - 2 * 60 * 1000).toISOString(), + avatar: 'PO', + }, + { + id: 'agent-002', + name: 'Architect', + role: 'architect', + status: 'working', + current_task: 'Designing API contract for checkout flow', + last_activity_at: new Date(Date.now() - 5 * 60 * 1000).toISOString(), + avatar: 'AR', + }, + { + id: 'agent-003', + name: 'Backend Engineer', + role: 'backend_engineer', + status: 'idle', + current_task: 'Waiting for architecture review', + last_activity_at: new Date(Date.now() - 15 * 60 * 1000).toISOString(), + avatar: 'BE', + }, + { + id: 'agent-004', + name: 'Frontend Engineer', + role: 'frontend_engineer', + status: 'working', + current_task: 'Implementing product catalog component', + last_activity_at: new Date(Date.now() - 1 * 60 * 1000).toISOString(), + avatar: 'FE', + }, + { + id: 'agent-005', + name: 'QA Engineer', + role: 'qa_engineer', + status: 'waiting', + current_task: 'Preparing test cases for Sprint 3', + last_activity_at: new Date(Date.now() - 30 * 60 * 1000).toISOString(), + avatar: 'QA', + }, +]; + +const statusColors = { + working: 'bg-green-500', + idle: 'bg-yellow-500', + waiting: 'bg-blue-500', + error: 'bg-red-500', + terminated: 'bg-gray-500', +}; + +const statusLabels = { + working: 'Working', + idle: 'Idle', + waiting: 'Waiting', + error: 'Error', + terminated: 'Terminated', +}; + +function formatTimeAgo(timestamp: string): string { + const now = new Date(); + const then = new Date(timestamp); + const diffMs = now.getTime() - then.getTime(); + const diffMins = Math.floor(diffMs / (1000 * 60)); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins}m ago`; + + const diffHours = Math.floor(diffMins / 60); + if (diffHours < 24) return `${diffHours}h ago`; + + const diffDays = Math.floor(diffHours / 24); + return `${diffDays}d ago`; +} + +export default function ProjectAgentsPage({ params }: ProjectAgentsPageProps) { + const { id: projectId } = use(params); + const router = useRouter(); + + const handleBack = useCallback(() => { + router.push(`/projects/${projectId}`); + }, [router, projectId]); + + const handleAgentClick = useCallback( + (agentId: string) => { + router.push(`/projects/${projectId}/agents/${agentId}`); + }, + [router, projectId] + ); + + const handleSpawnAgent = useCallback(() => { + // TODO: Open spawn agent dialog + console.log('Spawn agent clicked'); + }, []); + + const handleConfigureAgents = useCallback(() => { + router.push('/agents'); + }, [router]); + + return ( +
+
+ {/* Header */} +
+
+ +
+

Project Agents

+

+ {mockAgents.length} agents assigned to this project +

+
+
+
+ + +
+
+ + {/* Agent Grid */} +
+ {mockAgents.map((agent) => ( + handleAgentClick(agent.id)} + > + +
+
+ + + {agent.avatar} + + +
+ {agent.name} + + {agent.role.replace(/_/g, ' ')} + +
+
+ + +
+
+ +
+

{agent.current_task}

+

+ Last active: {formatTimeAgo(agent.last_activity_at)} +

+
+
+
+ ))} +
+ + {/* Empty State */} + {mockAgents.length === 0 && ( + + + +

No agents assigned

+

+ Spawn agents to start working on this project +

+ +
+
+ )} +
+
+ ); +} diff --git a/frontend/src/app/[locale]/(authenticated)/projects/[id]/settings/page.tsx b/frontend/src/app/[locale]/(authenticated)/projects/[id]/settings/page.tsx new file mode 100644 index 0000000..5f7ef5b --- /dev/null +++ b/frontend/src/app/[locale]/(authenticated)/projects/[id]/settings/page.tsx @@ -0,0 +1,296 @@ +'use client'; + +/** + * Project Settings Page + * + * Configuration page for project-level settings including: + * - Basic info (name, description) + * - Autonomy level + * - Repository integration + * - Danger zone (archive, delete) + * + * @module app/[locale]/(authenticated)/projects/[id]/settings/page + */ + +import { use, useState, useCallback } from 'react'; +import { useRouter } from '@/lib/i18n/routing'; +import { ArrowLeft, Save, Trash2, Archive, AlertTriangle } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Separator } from '@/components/ui/separator'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; +import { toast } from 'sonner'; + +interface ProjectSettingsPageProps { + params: Promise<{ + locale: string; + id: string; + }>; +} + +// Mock project data - will be replaced with API calls +const mockProject = { + id: 'proj-001', + name: 'E-Commerce Platform Redesign', + slug: 'ecommerce-redesign', + description: 'Complete redesign of the e-commerce platform with modern UI/UX', + autonomy_level: 'milestone', + repository_url: 'https://github.com/example/ecommerce', + status: 'active', +}; + +export default function ProjectSettingsPage({ params }: ProjectSettingsPageProps) { + const { id: projectId } = use(params); + const router = useRouter(); + + // Form state + const [name, setName] = useState(mockProject.name); + const [description, setDescription] = useState(mockProject.description); + const [autonomyLevel, setAutonomyLevel] = useState(mockProject.autonomy_level); + const [repositoryUrl, setRepositoryUrl] = useState(mockProject.repository_url); + const [isSaving, setIsSaving] = useState(false); + + const handleBack = useCallback(() => { + router.push(`/projects/${projectId}`); + }, [router, projectId]); + + const handleSave = useCallback(async () => { + setIsSaving(true); + // TODO: Call API to update project + await new Promise((resolve) => setTimeout(resolve, 1000)); + toast.success('Settings saved', { + description: 'Project settings have been updated successfully.', + }); + setIsSaving(false); + }, []); + + const handleArchive = useCallback(() => { + // TODO: Call API to archive project + toast.info('Project archived', { + description: 'The project has been archived and is no longer active.', + }); + router.push('/projects'); + }, [router]); + + const handleDelete = useCallback(() => { + // TODO: Call API to delete project + toast.success('Project deleted', { + description: 'The project has been permanently deleted.', + }); + router.push('/projects'); + }, [router]); + + return ( +
+
+ {/* Header */} +
+ +
+

Project Settings

+

Configure project preferences and integrations

+
+
+ + {/* General Settings */} + + + General + Basic project information and configuration + + +
+ + setName(e.target.value)} + placeholder="Enter project name" + /> +
+ +
+ +