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".
This commit is contained in:
@@ -32,7 +32,9 @@
|
|||||||
"demos": "Demos",
|
"demos": "Demos",
|
||||||
"design": "Design System",
|
"design": "Design System",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"adminPanel": "Admin Panel"
|
"adminPanel": "Admin Panel",
|
||||||
|
"projects": "Projects",
|
||||||
|
"agents": "Agents"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": {
|
"login": {
|
||||||
|
|||||||
@@ -32,7 +32,9 @@
|
|||||||
"demos": "Demo",
|
"demos": "Demo",
|
||||||
"design": "Design System",
|
"design": "Design System",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"adminPanel": "Pannello Admin"
|
"adminPanel": "Pannello Admin",
|
||||||
|
"projects": "Progetti",
|
||||||
|
"agents": "Agenti"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": {
|
"login": {
|
||||||
|
|||||||
@@ -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<string, unknown>;
|
||||||
|
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 (
|
||||||
|
<div className="container mx-auto px-4 py-6">
|
||||||
|
<div className="mx-auto max-w-4xl space-y-6">
|
||||||
|
{/* Page Header */}
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button variant="ghost" size="icon" onClick={handleBack}>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">Project Activity</h1>
|
||||||
|
<p className="mt-1 text-muted-foreground">Real-time updates from this project</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{pendingCount > 0 && (
|
||||||
|
<Badge variant="destructive" className="gap-1">
|
||||||
|
<AlertTriangle className="h-3 w-3" />
|
||||||
|
{pendingCount} pending
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setNotificationsEnabled(!notificationsEnabled)}
|
||||||
|
aria-label={notificationsEnabled ? 'Disable notifications' : 'Enable notifications'}
|
||||||
|
>
|
||||||
|
{notificationsEnabled ? (
|
||||||
|
<Bell className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<BellOff className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" onClick={reconnect} aria-label="Refresh connection">
|
||||||
|
<RefreshCw
|
||||||
|
className={connectionState === 'connecting' ? 'h-4 w-4 animate-spin' : 'h-4 w-4'}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Demo Mode Banner */}
|
||||||
|
{(!isConnected || sseEvents.length === 0) && (
|
||||||
|
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-4 dark:border-yellow-800 dark:bg-yellow-950">
|
||||||
|
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||||
|
<strong>Demo Mode:</strong> Showing sample events. Events will appear here when agents
|
||||||
|
are active.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Activity Feed Component */}
|
||||||
|
<ActivityFeed
|
||||||
|
events={events}
|
||||||
|
connectionState={isConnected ? connectionState : 'disconnected'}
|
||||||
|
onReconnect={reconnect}
|
||||||
|
onApprove={handleApprove}
|
||||||
|
onReject={handleReject}
|
||||||
|
onEventClick={handleEventClick}
|
||||||
|
maxHeight={600}
|
||||||
|
showHeader={false}
|
||||||
|
enableFiltering
|
||||||
|
enableSearch
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="container mx-auto px-4 py-6">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button variant="ghost" size="icon" onClick={handleBack}>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">Project Agents</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{mockAgents.length} agents assigned to this project
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={handleConfigureAgents}>
|
||||||
|
<Settings2 className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
|
Configure Agent Types
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onClick={handleSpawnAgent}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
|
Spawn Agent
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Agent Grid */}
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{mockAgents.map((agent) => (
|
||||||
|
<Card
|
||||||
|
key={agent.id}
|
||||||
|
className="cursor-pointer transition-shadow hover:shadow-md"
|
||||||
|
onClick={() => handleAgentClick(agent.id)}
|
||||||
|
>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Avatar className="h-10 w-10">
|
||||||
|
<AvatarFallback className="bg-primary/10 text-primary">
|
||||||
|
{agent.avatar}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-base">{agent.name}</CardTitle>
|
||||||
|
<CardDescription className="text-xs capitalize">
|
||||||
|
{agent.role.replace(/_/g, ' ')}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-1.5',
|
||||||
|
agent.status === 'working' && 'border-green-200 text-green-700',
|
||||||
|
agent.status === 'idle' && 'border-yellow-200 text-yellow-700',
|
||||||
|
agent.status === 'waiting' && 'border-blue-200 text-blue-700',
|
||||||
|
agent.status === 'error' && 'border-red-200 text-red-700'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn('h-2 w-2 rounded-full', statusColors[agent.status])}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{statusLabels[agent.status]}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm text-muted-foreground line-clamp-2">{agent.current_task}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Last active: {formatTimeAgo(agent.last_activity_at)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Empty State */}
|
||||||
|
{mockAgents.length === 0 && (
|
||||||
|
<Card className="py-12 text-center">
|
||||||
|
<CardContent>
|
||||||
|
<Bot className="mx-auto h-12 w-12 text-muted-foreground/50" />
|
||||||
|
<h3 className="mt-4 text-lg font-semibold">No agents assigned</h3>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
Spawn agents to start working on this project
|
||||||
|
</p>
|
||||||
|
<Button className="mt-4" onClick={handleSpawnAgent}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
|
Spawn First Agent
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="container mx-auto px-4 py-6">
|
||||||
|
<div className="mx-auto max-w-2xl space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button variant="ghost" size="icon" onClick={handleBack}>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">Project Settings</h1>
|
||||||
|
<p className="text-muted-foreground">Configure project preferences and integrations</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* General Settings */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>General</CardTitle>
|
||||||
|
<CardDescription>Basic project information and configuration</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">Project Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="Enter project name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="description">Description</Label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
placeholder="Describe your project"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="repository">Repository URL</Label>
|
||||||
|
<Input
|
||||||
|
id="repository"
|
||||||
|
type="url"
|
||||||
|
value={repositoryUrl}
|
||||||
|
onChange={(e) => setRepositoryUrl(e.target.value)}
|
||||||
|
placeholder="https://github.com/..."
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Link to the project's Git repository for code integration
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Autonomy Settings */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Autonomy Level</CardTitle>
|
||||||
|
<CardDescription>Control how much oversight you want over agent actions</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="autonomy">Agent Autonomy</Label>
|
||||||
|
<Select value={autonomyLevel} onValueChange={setAutonomyLevel}>
|
||||||
|
<SelectTrigger id="autonomy">
|
||||||
|
<SelectValue placeholder="Select autonomy level" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="full_control">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">Full Control</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Approve every agent action
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="milestone">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">Milestone</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Approve at sprint boundaries
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="autonomous">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">Autonomous</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Only major decisions need approval
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={handleSave} disabled={isSaving}>
|
||||||
|
<Save className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
|
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Danger Zone */}
|
||||||
|
<Card className="border-destructive/50">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-destructive">
|
||||||
|
<AlertTriangle className="h-5 w-5" />
|
||||||
|
Danger Zone
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Irreversible actions for this project</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* Archive */}
|
||||||
|
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Archive Project</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Archive this project and stop all agent activity
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="outline">
|
||||||
|
<Archive className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
|
Archive
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Archive Project?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will stop all agent activity and mark the project as archived. You can
|
||||||
|
unarchive it later if needed.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleArchive}>Archive Project</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Delete */}
|
||||||
|
<div className="flex items-center justify-between rounded-lg border border-destructive/50 p-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-destructive">Delete Project</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Permanently delete this project and all its data
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="destructive">
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Delete Project?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently delete the project, all
|
||||||
|
agents, issues, sprints, and activity history.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleDelete}
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
>
|
||||||
|
Delete Project
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -99,6 +99,8 @@ export function Header() {
|
|||||||
<NavLink href="/dashboard" exact>
|
<NavLink href="/dashboard" exact>
|
||||||
{t('home')}
|
{t('home')}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink href="/projects">{t('projects')}</NavLink>
|
||||||
|
<NavLink href="/agents">{t('agents')}</NavLink>
|
||||||
{user?.is_superuser && <NavLink href="/admin">{t('admin')}</NavLink>}
|
{user?.is_superuser && <NavLink href="/admin">{t('admin')}</NavLink>}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user