forked from cardosofelipe/fast-next-template
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",
|
||||
"design": "Design System",
|
||||
"admin": "Admin",
|
||||
"adminPanel": "Admin Panel"
|
||||
"adminPanel": "Admin Panel",
|
||||
"projects": "Projects",
|
||||
"agents": "Agents"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
"demos": "Demo",
|
||||
"design": "Design System",
|
||||
"admin": "Admin",
|
||||
"adminPanel": "Pannello Admin"
|
||||
"adminPanel": "Pannello Admin",
|
||||
"projects": "Progetti",
|
||||
"agents": "Agenti"
|
||||
},
|
||||
"auth": {
|
||||
"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>
|
||||
{t('home')}
|
||||
</NavLink>
|
||||
<NavLink href="/projects">{t('projects')}</NavLink>
|
||||
<NavLink href="/agents">{t('agents')}</NavLink>
|
||||
{user?.is_superuser && <NavLink href="/admin">{t('admin')}</NavLink>}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user