feat(CC-058): add AI Context & Sources page — per-request context manifest viewer

This commit is contained in:
admin-valentin 2026-08-01 21:01:56 +00:00
parent 3410f3e4f8
commit e9f83d0980

View file

@ -0,0 +1,178 @@
'use client';
import { useState } from 'react';
import { useQuery } 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;
contextManifest?: unknown;
createdAt: string;
completedAt: string | null;
}
const STATUS_CFG: Record<string, { label: string; cls: string }> = {
pending: { label: 'Pending', cls: 'bg-amber-500/10 text-amber-700 dark:text-amber-300' },
completed: { label: 'Completat', cls: 'bg-signal-ok/10 text-signal-ok' },
failed: { label: 'Eșuat', cls: 'bg-signal-danger/10 text-signal-danger' },
cancelled: { label: 'Anulat', cls: 'bg-muted text-ink-faint' },
};
export default function AiContextPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const [selectedId, setSelectedId] = useState<string | null>(null);
const { data: requests = [], isLoading } = useQuery({
queryKey: ['ai-requests', tenantId],
queryFn: () => apiFetch<AiRequest[]>('/v1/ai-requests?limit=100', { tenantId }),
enabled: Boolean(tenantId),
});
const { data: selected } = useQuery({
queryKey: ['ai-requests', tenantId, selectedId],
queryFn: () => apiFetch<AiRequest>(`/v1/ai-requests/${selectedId}`, { tenantId }),
enabled: Boolean(selectedId && tenantId),
});
return (
<div className="flex gap-0" style={{ height: 'calc(100vh - 7rem)' }}>
{/* Sidebar: request list */}
<aside className="w-72 shrink-0 border-r border-border/50 flex flex-col overflow-hidden">
<div className="p-4 border-b border-border/50 shrink-0">
<h1 className="font-semibold text-sm text-ink">Context AI & Surse</h1>
<p className="text-xs text-ink-faint mt-0.5">
{requests.length} cereri AI înregistrate
</p>
</div>
<div className="flex-1 overflow-y-auto">
{isLoading && (
<p className="p-4 text-xs text-ink-faint text-center">Se încarcă</p>
)}
{!isLoading && requests.length === 0 && (
<p className="p-4 text-xs text-ink-faint text-center">
Nicio cerere AI înregistrată.
</p>
)}
{requests.map((req) => {
const cfg = STATUS_CFG[req.resultStatus] ?? STATUS_CFG.pending;
return (
<button
key={req.id}
onClick={() => setSelectedId(req.id)}
className={`w-full text-left px-4 py-3 border-b border-border/30 transition-colors ${
selectedId === req.id ? 'bg-primary/5' : 'hover:bg-muted/40'
}`}
>
<div className="flex items-start justify-between gap-2">
<p className="text-xs font-medium text-ink line-clamp-1">{req.purpose}</p>
<span className={`shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded-full ${cfg.cls}`}>
{cfg.label}
</span>
</div>
<div className="flex items-center gap-2 mt-1">
<span className="text-[10px] text-ink-faint">{req.model.split('/').pop()}</span>
{req.costUsd && parseFloat(req.costUsd) > 0 && (
<span className="text-[10px] text-ink-faint">
${parseFloat(req.costUsd).toFixed(5)}
</span>
)}
<span className="text-[10px] text-ink-faint ml-auto">
{new Date(req.createdAt).toLocaleTimeString('ro-RO', {
hour: '2-digit', minute: '2-digit',
})}
</span>
</div>
</button>
);
})}
</div>
</aside>
{/* Main: context manifest viewer */}
{!selectedId ? (
<div className="flex-1 flex items-center justify-center">
<div className="text-center space-y-2">
<p className="text-3xl">🔍</p>
<p className="text-sm text-ink-faint max-w-sm">
Selectează o cerere AI din stânga pentru a vedea contextul complet ce date au fost permise și ce model a primit.
</p>
</div>
</div>
) : !selected ? (
<div className="flex-1 flex items-center justify-center">
<p className="text-sm text-ink-faint">Se încarcă contextul</p>
</div>
) : (
<main className="flex-1 overflow-y-auto p-6 space-y-5">
<div className="space-y-1">
<h2 className="font-display text-lg font-semibold text-ink">{selected.purpose}</h2>
<div className="flex flex-wrap items-center gap-3 text-xs text-ink-faint">
<span className="font-mono bg-muted px-2 py-0.5 rounded">{selected.actionClass}</span>
<span>{selected.model}</span>
{selected.templateVersion && (
<span>template v{selected.templateVersion}</span>
)}
{selected.costUsd && parseFloat(selected.costUsd) > 0 && (
<span className="text-ink">${parseFloat(selected.costUsd).toFixed(6)}</span>
)}
<span>{new Date(selected.createdAt).toLocaleString('ro-RO')}</span>
</div>
</div>
{/* Status */}
<div>
<span className={`text-xs font-medium px-2 py-1 rounded-full ${
(STATUS_CFG[selected.resultStatus] ?? STATUS_CFG.pending).cls
}`}>
{(STATUS_CFG[selected.resultStatus] ?? STATUS_CFG.pending).label}
</span>
{selected.completedAt && (
<span className="ml-2 text-xs text-ink-faint">
completat la {new Date(selected.completedAt).toLocaleTimeString('ro-RO')}
</span>
)}
</div>
{/* Context Manifest */}
<div className="space-y-2">
<h3 className="text-xs font-semibold uppercase tracking-wider text-ink-faint">
Context Manifest
</h3>
<p className="text-xs text-ink-faint">
Datele permise pentru această cerere AI conform politicii de acces a platformei.
</p>
{selected.contextManifest ? (
<pre className="bg-muted/50 border rounded-xl p-4 text-xs font-mono text-ink overflow-x-auto whitespace-pre-wrap">
{JSON.stringify(selected.contextManifest, null, 2)}
</pre>
) : (
<div className="bg-muted/30 border border-dashed rounded-xl p-4 text-center">
<p className="text-xs text-ink-faint">
Context manifest nu este disponibil pentru această cerere.
</p>
</div>
)}
</div>
{/* Hash */}
<div className="space-y-1">
<h3 className="text-xs font-semibold uppercase tracking-wider text-ink-faint">
Hash Manifest
</h3>
<p className="font-mono text-xs text-ink-faint bg-muted/30 px-3 py-2 rounded-lg break-all">
{(selected as unknown as { contextManifestHash: string }).contextManifestHash ?? '—'}
</p>
</div>
</main>
)}
</div>
);
}