Assessor_2.0/src/compliance/ui/IsoCorpusLibrary.tsx

85 lines
5.8 KiB
TypeScript

import { useCallback, useEffect, useState, type FormEvent } from 'react';
import { AssessorApiClient } from '../../api/assessor-api-client';
import { useEnginePreferences } from '../../app/engine-preferences';
import { useBackendWorkActivity } from './use-backend-work-activity';
import { statusPollApi } from './status-poll-client';
import type { RegulatoryCorpusStatus } from '../regulatory-corpus-management';
import { TriangleIcon } from '../../components/TriangleIcon';
const api = new AssessorApiClient();
const MAXIMUM_PDF_BYTES = 32 * 1024 * 1024;
/** Data Hub ISO corpus tile; a switched-off Compliance Engine renders nothing and requests nothing. */
export function IsoCorpusLibrary() {
return useEnginePreferences().compliance ? <IsoCorpusLibraryPanel/> : null;
}
function IsoCorpusLibraryPanel() {
const [status, setStatus] = useState<RegulatoryCorpusStatus | null>(null);
const [iso27001, setIso27001] = useState<File | null>(null);
const [iso27002, setIso27002] = useState<File | null>(null);
const [token, setToken] = useState('');
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const load = useCallback(async (poll = false) => {
try { setStatus(await (poll ? statusPollApi : api).getIsoCorpusStatus()); setError(null); }
catch (reason) { setError(reason instanceof Error ? reason.message : 'ISO corpus status could not be loaded.'); }
}, []);
useEffect(() => { void load(); }, [load]);
const job = status?.job?.status;
useBackendWorkActivity('Embedding ISO corpus', job === 'running' ? 'running' : job === 'failed' ? 'failed' : job === 'succeeded' ? 'finished' : 'idle');
useEffect(() => {
if (status?.job?.status !== 'running') return;
const interval = setInterval(() => { void load(true); }, 2_000);
return () => clearInterval(interval);
}, [load, status?.job?.status]);
const submit = async (event: FormEvent) => {
event.preventDefault();
const form = event.currentTarget as HTMLFormElement;
if (!iso27001 || !iso27002 || !token) return;
if (iso27001.size > MAXIMUM_PDF_BYTES || iso27002.size > MAXIMUM_PDF_BYTES) {
setError('Each PDF must be no larger than 32 MiB.'); return;
}
setSubmitting(true); setError(null);
try {
setStatus(await api.submitIsoCorpus(iso27001, iso27002, token));
setIso27001(null); setIso27002(null); setToken('');
form.reset();
} catch (reason) { setError(reason instanceof Error ? reason.message : 'ISO corpus upload failed.'); }
finally { setSubmitting(false); }
};
const running = status?.job?.status === 'running';
return <details className="tenant-scan-history baseline-catalog iso-corpus-catalog">
<summary className="tenant-section-bar">
<span className="tenant-history-meta baseline-title"><b>{status?.active ? '1' : '0'}</b><strong>ISO Control Corpus</strong></span>
<span className="tenant-history-meta"><span>{running ? status.job?.phase === 'embedding' && status.job.totalChunks
? `Embedding ${status.job.completedChunks ?? 0} / ${status.job.totalChunks}`
: `Processing: ${status.job?.phase}` : status?.active
? status.indexStatus === 'incomplete' ? 'Rebuild required' : status.indexStatus === 'unavailable' ? 'Index unavailable' : 'Ready'
: 'Not indexed'}</span><TriangleIcon direction="right"/></span>
</summary>
<div className="iso-corpus-content">
{status?.active && <div className="iso-corpus-summary" role="status">
<span><strong>Active version</strong> {status.active.corpusVersion}</span>
<span><strong>Controls and sections</strong> {status.active.documentCount.toLocaleString('en-US')} · {status.active.chunkCount.toLocaleString('en-US')} indexed chunks</span>
<span><strong>Embedding model</strong> {status.active.embeddingModel}</span>
{status.active.sources.map(source => <span key={source.standard}><strong>{source.standard}</strong> {source.pageCount} pages · SHA-256 {source.sha256.slice(0, 12)}…</span>)}
</div>}
{status?.indexStatus === 'incomplete' && <div className="datahub-alert" role="alert">The index is incomplete. Upload the same PDFs to rebuild it.</div>}
{status?.job?.status === 'failed' && <div className="datahub-alert" role="alert">{status.job.error}</div>}
{status?.job?.status === 'succeeded' && <div className="tenant-save-feedback success" role="status"><strong>ISO corpus indexed and active.</strong></div>}
{!status?.configured && status?.disabledReason && <div className="datahub-alert" role="alert">{status.disabledReason}</div>}
<form className="iso-corpus-form" onSubmit={event => void submit(event)}>
<label><span>DIN EN ISO/IEC 27001:2024-01 PDF</span><input className="input" type="file" accept=".pdf,application/pdf" required disabled={submitting || running} onChange={event => setIso27001(event.target.files?.[0] ?? null)}/></label>
<label><span>DIN EN ISO/IEC 27002:2024-01 PDF</span><input className="input" type="file" accept=".pdf,application/pdf" required disabled={submitting || running} onChange={event => setIso27002(event.target.files?.[0] ?? null)}/></label>
<label><span>ISO upload token</span><input className="input" type="password" autoComplete="off" value={token} required disabled={submitting || running} onChange={event => setToken(event.target.value)}/></label>
<button className="btn datahub-maintenance-action" type="submit" disabled={!status?.configured || !iso27001 || !iso27002 || !token || submitting || running}>{submitting ? 'Uploading …' : running ? 'Embedding …' : 'Upload and embed PDFs'}</button>
</form>
<p className="iso-corpus-note">The server keeps the generated corpus privately for rebuilding the index. Original PDFs are removed after parsing.</p>
{error && <div className="datahub-alert" role="alert">{error}</div>}
</div>
</details>;
}