feat(CC-058): add AI Context & Sources page — per-request context manifest viewer
This commit is contained in:
parent
3410f3e4f8
commit
e9f83d0980
1 changed files with 178 additions and 0 deletions
178
src/app/dashboard/ai/context/page.tsx
Normal file
178
src/app/dashboard/ai/context/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue