diff --git a/src/App.tsx b/src/App.tsx index ead6a21f..f44f4eaf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,6 +43,8 @@ import { SettingsPage } from './pages/SettingsPage'; import { CityRoomsPage } from './features/city/rooms/CityRoomsPage'; import { CityRoomView } from './features/city/rooms/CityRoomView'; import { SecondMePage } from './features/secondme/SecondMePage'; +// Governance Engine +import { GovernancePage } from './pages/GovernancePage'; function App() { return ( @@ -56,6 +58,8 @@ function App() { } /> } /> } /> + {/* Governance Engine */} + } /> } /> } /> {/* Task 039: Agent Console v2 */} diff --git a/src/features/agentHub/AgentCabinet.tsx b/src/features/agentHub/AgentCabinet.tsx index 1952d849..c4f560d7 100644 --- a/src/features/agentHub/AgentCabinet.tsx +++ b/src/features/agentHub/AgentCabinet.tsx @@ -1,9 +1,14 @@ import { useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; import { useAgentDashboard } from './hooks/useAgentDashboard'; import { VisibilityCard } from './components/VisibilityCard'; import { AgentChatWidget } from './components/AgentChatWidget'; import { MicroDaoWizard } from './components/MicroDaoWizard'; +import { GovernanceLevelBadge } from '../governance/components/GovernanceLevelBadge'; +import { governanceApi } from '../../api/governance'; +import { GOV_LEVEL_LABELS, POWER_LABELS } from '../../types/governance'; +import type { AgentGovLevel, GovernancePower } from '../../types/governance'; export function AgentCabinet() { const { agentId } = useParams<{ agentId: string }>(); @@ -213,6 +218,9 @@ export function AgentCabinet() { + {/* Governance & Roles Block */} + + {/* Node Info */} {node && (
@@ -254,4 +262,106 @@ export function AgentCabinet() { ); } +// ============================================================================ +// Governance Roles Block +// ============================================================================ +function GovernanceRolesBlock({ agentId }: { agentId: string }) { + const { data: roles, isLoading, error } = useQuery({ + queryKey: ['governance', 'agent', agentId, 'roles'], + queryFn: () => governanceApi.getAgentRoles(agentId), + retry: false, + }); + + if (isLoading) { + return ( +
+

🛡️ Ролі та Повноваження

+
+
+
+
+
+
+ ); + } + + if (error || !roles) { + return ( +
+

🛡️ Ролі та Повноваження

+

Governance API недоступний

+
+ ); + } + + const levelLabel = GOV_LEVEL_LABELS[roles.level as AgentGovLevel] || roles.level; + const powerLabels = roles.powers.map((p: GovernancePower) => POWER_LABELS[p] || p); + + return ( +
+

🛡️ Ролі та Повноваження

+ + {/* Level Badge */} +
+ Рівень: + +
+ + {/* Status */} +
+ Статус: + + {roles.status === 'active' ? '✅ Активний' : + roles.status === 'suspended' ? '⏸️ Призупинено' : + '🚫 Заблоковано'} + +
+ + {/* Powers */} + {roles.powers.length > 0 && ( +
+ Повноваження: +
+ {powerLabels.map((power: string, i: number) => ( + + {power} + + ))} +
+
+ )} + + {/* Assignments */} + {roles.assignments.length > 0 && ( +
+ Призначення: +
+ {roles.assignments.map((a: { microdaoId: string; role: string; scope: string }, i: number) => ( +
+ {a.microdaoId} + + {a.role} + +
+ ))} +
+
+ )} + + {/* Actions (placeholder for governance actions) */} + +
+ ); +} diff --git a/src/features/governance/components/ReportButton.tsx b/src/features/governance/components/ReportButton.tsx new file mode 100644 index 00000000..6aea1ad9 --- /dev/null +++ b/src/features/governance/components/ReportButton.tsx @@ -0,0 +1,192 @@ +/** + * Report Button + * Universal button to create incidents from any context + */ + +import React, { useState } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { incidentsApi } from '../../../api/incidents'; +import type { IncidentPriority, TargetScopeType } from '../../../types/governance'; +import { INCIDENT_PRIORITY_LABELS } from '../../../types/governance'; + +interface ReportButtonProps { + targetScopeType: TargetScopeType; + targetScopeId: string; + actorDaisId?: string; + variant?: 'icon' | 'text' | 'full'; + className?: string; + defaultTitle?: string; +} + +export const ReportButton: React.FC = ({ + targetScopeType, + targetScopeId, + actorDaisId, + variant = 'icon', + className = '', + defaultTitle = '', +}) => { + const [isOpen, setIsOpen] = useState(false); + const [form, setForm] = useState({ + title: defaultTitle, + description: '', + priority: 'medium' as IncidentPriority, + }); + + const queryClient = useQueryClient(); + + const createMutation = useMutation({ + mutationFn: () => { + if (!actorDaisId) { + throw new Error('Actor DAIS ID is required'); + } + return incidentsApi.createIncident({ + createdByDaisId: actorDaisId, + targetScopeType, + targetScopeId, + priority: form.priority, + title: form.title, + description: form.description || undefined, + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['incidents'] }); + setIsOpen(false); + setForm({ title: defaultTitle, description: '', priority: 'medium' }); + }, + }); + + if (!actorDaisId) { + return null; + } + + const renderButton = () => { + switch (variant) { + case 'icon': + return ( + + ); + case 'text': + return ( + + ); + case 'full': + return ( + + ); + } + }; + + return ( + <> + {renderButton()} + + {isOpen && ( +
+
+
+

Повідомити про проблему

+ +
+ +
+
+
Об'єкт скарги:
+
+ {targetScopeType}: {targetScopeId} +
+
+ +
+ + setForm(prev => ({ ...prev, title: e.target.value }))} + placeholder="Коротко опишіть проблему..." + className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-red-500 focus:border-red-500" + /> +
+ +
+ +