/** * WorkspaceRagInsightsPage — Aggregierte, nicht personenbezogene Kennzahlen zum * Knowledge Store / RAG dieser Workspace-Instanz (Präsentationen, Monitoring). */ import React, { useCallback, useEffect, useState } from 'react'; import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, BarChart, Bar, PieChart, Pie, Cell, } from 'recharts'; import { useInstanceId } from '../../../hooks/useCurrentInstance'; import { useApiRequest } from '../../../hooks/useApi'; import styles from './WorkspaceRagInsightsPage.module.css'; import { formatBinaryDataSizeBytes } from '../../../utils/formatDataSize'; import { useLanguage } from '../../../providers/language/LanguageContext'; function _mimeLabel(key: string, t: (k: string) => string): string { switch (key) { case 'pdf': return t('PDF'); case 'office_doc': return t('Office (Text)'); case 'office_sheet': return t('Office (Tabellen)'); case 'office_slides': return t('Office (Folien)'); case 'text': return t('Text'); case 'image': return t('Bild'); case 'html': return t('HTML'); case 'other': return t('Sonstige'); default: return key; } } const CHART_COLORS = ['#1976d2', '#00897b', '#6a1b9a', '#e65100', '#5d4037', '#455a64', '#c62828']; function _formatTimestamp(ts: number | null | undefined): string { if (ts == null || ts <= 0) return '–'; try { const d = new Date(ts * 1000); return d.toLocaleString('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } catch { return '–'; } } function _shortMime(mime: string): string { const m = (mime || '').toLowerCase(); if (m.includes('pdf')) return 'PDF'; if (m.includes('wordprocessing') || m.includes('msword')) return 'Word'; if (m.includes('spreadsheet') || m.includes('excel')) return 'Excel'; if (m.includes('presentation') || m.includes('powerpoint')) return 'PowerPoint'; if (m.startsWith('text/')) return 'Text'; if (m.startsWith('image/')) return 'Bild'; if (m.includes('html')) return 'HTML'; return mime || '–'; } const _STATUS_COLORS: Record = { indexed: '#2e7d32', extracted: '#1565c0', embedding: '#6a1b9a', pending: '#e65100', failed: '#c62828', }; interface RagKpis { indexedDocuments: number; indexedBytesTotal: number; contributorUsers: number; contentChunks: number; chunksWithEmbedding: number; embeddingCoveragePercent: number; workflowEntities: number; } interface RecentlyIndexedDoc { fileName: string; mimeType: string; status: string; extractedAt: number | null; totalSize: number; } interface RagStatsResponse { error?: string; scope?: { featureInstanceId?: string; mandateScopedShared?: boolean; workspaceFileIdsResolved?: number; }; kpis?: RagKpis; indexedDocumentsByStatus?: Record; documentsByMimeCategory?: Record; chunksByContentType?: Record; timelineIndexedDocuments?: Array<{ date: string; indexedDocuments: number }>; recentlyIndexedDocuments?: RecentlyIndexedDoc[]; generatedAtUtc?: string; } export const WorkspaceRagInsightsPage: React.FC = () => { const { t } = useLanguage(); const instanceId = useInstanceId(); const { request } = useApiRequest(); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [stats, setStats] = useState(null); const load = useCallback(async () => { if (!instanceId) return; setLoading(true); setError(null); try { const data = (await request({ url: `/api/workspace/${instanceId}/rag-statistics`, method: 'get', })) as RagStatsResponse; if (data?.error) { setError(String(data.error)); setStats(null); } else { setStats(data ?? null); } } catch (e) { setError(e instanceof Error ? e.message : t('Laden fehlgeschlagen')); setStats(null); } finally { setLoading(false); } }, [instanceId, request, t]); useEffect(() => { void load(); }, [load]); if (!instanceId) { return (
{t('Keine Workspace-Instanz ausgewählt.')}
); } if (loading) { return
{t('Lade Kennzahlen')}
; } if (error) { return
{error}
; } const kpis = stats?.kpis; const timeline = stats?.timelineIndexedDocuments ?? []; const mimeRows = Object.entries(stats?.documentsByMimeCategory ?? {}).map(([key, value]) => ({ name: _mimeLabel(key, t), value, })); const statusRows = Object.entries(stats?.indexedDocumentsByStatus ?? {}).map(([name, value]) => ({ name, value, })); const chunkTypeRows = Object.entries(stats?.chunksByContentType ?? {}).map(([name, value]) => ({ name, value, })); return (

{t( 'Dargestellt sind ausschliesslich aggregierte technische Masszahlen dieser Instanz (Anzahl Dokumente, Fragmente, Speicherumfang, Verteilungen). Es werden keine Inhalte, Dateinamen oder personenbezogene Angaben ausgewiesen. Geeignet für interne Berichte und Präsentationen.', )}

{stats?.scope?.workspaceFileIdsResolved !== undefined && (

{t( 'Zuordnung Knowledge ↔ Dateien: {workspaceFileIdsResolved} Datei-ID(s) mit dieser Feature-Instanz in der Dateiverwaltung. Neu indexierte Uploads erhalten die Instanz automatisch; ältere Einträge ohne Zuordnung erscheinen erst nach erneuter Indexierung.', { workspaceFileIdsResolved: stats.scope.workspaceFileIdsResolved }, )}

)} {kpis && (

{kpis.indexedDocuments}

{t('Indexierte Dokumente')}

{formatBinaryDataSizeBytes(kpis.indexedBytesTotal)}

{t('Indexiertes Datenvolumen (geschätzt)')}

{kpis.contentChunks}

{t('Inhaltsfragmente (Chunks)')}

{kpis.embeddingCoveragePercent}%

{t('Anteil Fragmente mit Embedding')}

{kpis.contributorUsers}

{t('Beitragende Benutzeranzahl')}

{kpis.workflowEntities}

{t('Workflowentitäten-Cache')}

)} {(stats?.recentlyIndexedDocuments ?? []).length > 0 && (

{t('Zuletzt indexierte Dokumente')}

{(stats?.recentlyIndexedDocuments ?? []).map((doc, i) => ( ))}
{t('Dateiname')} {t('Format')} {t('Grösse')} {t('Status')} {t('Indexiert am')}
{doc.fileName || '–'} {_shortMime(doc.mimeType)} {formatBinaryDataSizeBytes(doc.totalSize)} {doc.status} {_formatTimestamp(doc.extractedAt)}
)}

{t('Neu indexierte Dokumente pro Tag')}

{timeline.length === 0 ? (

{t('Keine Zeitreihendaten für den gewählten')}

) : ( )}

{t('Dokumente nach Formatkategorie')}

{mimeRows.length === 0 ? (

{t('Keine Daten')}

) : ( )}

{t('Index-Status')}

{statusRows.length === 0 ? (

{t('Keine Daten')}

) : ( `${name ?? ''} ${percent != null ? (percent * 100).toFixed(0) : '0'}%`} > {statusRows.map((_, i) => ( ))} )}

{t('Fragmente nach Inhaltstyp')}

{chunkTypeRows.length === 0 ? (

{t('Keine Chunkdaten')}

) : ( )}
{stats?.generatedAtUtc && (

{t('Stand (UTC):')} {stats.generatedAtUtc}

)}
); };