diff --git a/src/app/dashboard/ai/approvals/page.tsx b/src/app/dashboard/ai/approvals/page.tsx new file mode 100644 index 0000000..9305e5a --- /dev/null +++ b/src/app/dashboard/ai/approvals/page.tsx @@ -0,0 +1,168 @@ +'use client'; + +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../../lib/api'; +import { useSession } from '../../../../components/session-provider'; + +interface AiRequest { + id: string; + purpose: string; + actionClass: string; + model: string; + templateVersion: string | null; + resultStatus: string; + costUsd: string | null; + contextManifestHash: string; + createdAt: string; + completedAt: string | null; +} + +const RISK_BY_CLASS: Record = { + read: { label: 'Citire', cls: 'bg-sky-500/10 text-sky-700 dark:text-sky-300' }, + write: { label: 'Scriere', cls: 'bg-amber-500/10 text-amber-700 dark:text-amber-300' }, + send: { label: 'Trimitere', cls: 'bg-orange-500/10 text-orange-700 dark:text-orange-300' }, + delete: { label: 'Ștergere', cls: 'bg-signal-danger/10 text-signal-danger' }, + high_risk: { label: 'Risc înalt', cls: 'bg-signal-danger/10 text-signal-danger font-bold' }, +}; + +export default function ApprovalQueuePage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const [rejecting, setRejecting] = useState(null); + + const { data: pending = [], isLoading } = useQuery({ + queryKey: ['ai-approvals', tenantId], + queryFn: () => apiFetch('/v1/ai-requests?status=pending&limit=100', { tenantId }), + enabled: Boolean(tenantId), + refetchInterval: 15_000, + }); + + const { mutate: updateStatus, isPending: isUpdating } = useMutation({ + mutationFn: ({ id, resultStatus }: { id: string; resultStatus: 'completed' | 'cancelled' }) => + apiFetch(`/v1/ai-requests/${id}/status`, { method: 'PATCH', body: { resultStatus }, tenantId }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['ai-approvals', tenantId] }), + }); + + const approve = (id: string) => updateStatus({ id, resultStatus: 'completed' }); + const reject = (id: string) => { updateStatus({ id, resultStatus: 'cancelled' }); setRejecting(null); }; + + return ( +
+
+
+

Coadă de Aprobare

+

+ Acțiuni AI care necesită autorizare înainte de execuție +

+
+ {pending.length > 0 && ( + + + {pending.length} în așteptare + + )} +
+ + {isLoading ? ( +
Se încarcă…
+ ) : pending.length === 0 ? ( +
+

+

Coada este goală

+

+ Nicio acțiune AI nu este în așteptare de aprobare. +

+
+ ) : ( +
+ {pending.map((req) => { + const risk = RISK_BY_CLASS[req.actionClass] ?? { label: req.actionClass, cls: 'bg-muted text-ink-faint' }; + const estimatedCost = req.costUsd ? `$${parseFloat(req.costUsd).toFixed(5)}` : null; + const waitingSince = new Date(req.createdAt); + const minsWaiting = Math.round((Date.now() - waitingSince.getTime()) / 60_000); + const isConfirmingReject = rejecting === req.id; + + return ( +
+
+
+
+

{req.purpose}

+ + {risk.label} + +
+
+ {req.model.split('/').pop()} + {estimatedCost && cuv. estimat {estimatedCost}} + {req.templateVersion && template v{req.templateVersion}} + în așteptare {minsWaiting > 60 ? `${Math.round(minsWaiting / 60)}h` : `${minsWaiting}m`} +
+
+
+ + {/* Context hash */} +
+
+

+ Context Manifest Hash +

+

{req.contextManifestHash}

+
+
+ + {/* Actions */} + {isConfirmingReject ? ( +
+

Confirmi respingerea acestei acțiuni?

+ + +
+ ) : ( +
+ + +
+ )} +
+ ); + })} +
+ )} + + {/* Explainer */} +
+

+ Cum funcționează: Acțiunile AI cu risc de scriere, trimitere sau ștergere + necesită aprobare explicită înainte de execuție. Aprobarea autorizează acțiunea; respingerea o anulează permanent. + Pagina se actualizează automat la fiecare 15 secunde. +

+
+
+ ); +}