From 064635ae7430fd2a68d5ce4a6311d920bb9a3ed5 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Mon, 23 Mar 2026 00:18:06 +0100 Subject: [PATCH] rag stats --- src/App.tsx | 1 + src/pages/FeatureView.tsx | 4 +- .../WorkspaceRagInsightsPage.module.css | 82 ++++++ .../workspace/WorkspaceRagInsightsPage.tsx | 273 ++++++++++++++++++ src/types/mandate.ts | 1 + 5 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 src/pages/views/workspace/WorkspaceRagInsightsPage.module.css create mode 100644 src/pages/views/workspace/WorkspaceRagInsightsPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 172ae21..e834cad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -156,6 +156,7 @@ function App() { {/* Workspace Editor */} } /> + } /> {/* Teams Bot Feature Views */} } /> diff --git a/src/pages/FeatureView.tsx b/src/pages/FeatureView.tsx index 02a4563..5ee085c 100644 --- a/src/pages/FeatureView.tsx +++ b/src/pages/FeatureView.tsx @@ -34,6 +34,7 @@ import { AutomationDefinitionsView, AutomationTemplatesView } from './views/auto import { WorkspacePage } from './views/workspace/WorkspacePage'; import { WorkspaceEditorPage } from './views/workspace/WorkspaceEditorPage'; import { WorkspaceSettingsPage } from './views/workspace/WorkspaceSettingsPage'; +import { WorkspaceRagInsightsPage } from './views/workspace/WorkspaceRagInsightsPage'; // Teamsbot Views import { TeamsbotDashboardView } from './views/teamsbot/TeamsbotDashboardView'; @@ -130,6 +131,7 @@ const VIEW_COMPONENTS: Record> = { workspace: { dashboard: WorkspacePage, editor: WorkspaceEditorPage, + 'rag-insights': WorkspaceRagInsightsPage, settings: WorkspaceSettingsPage, }, teamsbot: { @@ -196,7 +198,7 @@ export const FeatureViewPage: React.FC = ({ view }) => { // Workspace dashboard is rendered persistently by WorkspaceKeepAlive at MainLayout level; // other workspace views (e.g. settings, editor) use the standard FeatureViewPage rendering. - if (featureCode === 'workspace' && view !== 'settings' && view !== 'editor') { + if (featureCode === 'workspace' && view !== 'settings' && view !== 'editor' && view !== 'rag-insights') { return null; } diff --git a/src/pages/views/workspace/WorkspaceRagInsightsPage.module.css b/src/pages/views/workspace/WorkspaceRagInsightsPage.module.css new file mode 100644 index 0000000..712c04e --- /dev/null +++ b/src/pages/views/workspace/WorkspaceRagInsightsPage.module.css @@ -0,0 +1,82 @@ +.wrap { + display: flex; + flex-direction: column; + gap: 1.25rem; + max-width: 1200px; +} + +.disclaimer { + font-size: 0.85rem; + line-height: 1.45; + color: var(--text-secondary, #666); + padding: 0.75rem 1rem; + background: var(--bg-secondary, #f5f5f5); + border-radius: 8px; + border: 1px solid var(--border-color, #e8e8e8); +} + +.kpiGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 0.75rem; +} + +.kpiCard { + padding: 1rem; + border-radius: 8px; + background: var(--bg-primary, #fff); + border: 1px solid var(--border-color, #e0e0e0); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); +} + +.kpiValue { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary, #1a1a1a); + margin: 0 0 0.25rem; +} + +.kpiLabel { + font-size: 0.8rem; + color: var(--text-secondary, #666); + margin: 0; + line-height: 1.3; +} + +.chartBlock { + padding: 1rem; + border-radius: 8px; + background: var(--bg-primary, #fff); + border: 1px solid var(--border-color, #e0e0e0); + min-height: 280px; +} + +.chartTitle { + font-size: 0.95rem; + font-weight: 600; + margin: 0 0 0.75rem; + color: var(--text-primary, #1a1a1a); +} + +.row2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +@media (max-width: 900px) { + .row2 { + grid-template-columns: 1fr; + } +} + +.meta { + font-size: 0.75rem; + color: var(--text-secondary, #888); + margin-top: 0.5rem; +} + +.error { + color: #c62828; + padding: 1rem; +} diff --git a/src/pages/views/workspace/WorkspaceRagInsightsPage.tsx b/src/pages/views/workspace/WorkspaceRagInsightsPage.tsx new file mode 100644 index 0000000..9256ade --- /dev/null +++ b/src/pages/views/workspace/WorkspaceRagInsightsPage.tsx @@ -0,0 +1,273 @@ +/** + * 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'; + +const MIME_LABELS: Record = { + pdf: 'PDF', + office_doc: 'Office (Text)', + office_sheet: 'Office (Tabellen)', + office_slides: 'Office (Folien)', + text: 'Text', + image: 'Bild', + html: 'HTML', + other: 'Sonstige', +}; + +const CHART_COLORS = ['#1976d2', '#00897b', '#6a1b9a', '#e65100', '#5d4037', '#455a64', '#c62828']; + +function _formatBytes(n: number): string { + if (!Number.isFinite(n) || n <= 0) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB']; + let v = n; + let i = 0; + while (v >= 1024 && i < units.length - 1) { + v /= 1024; + i += 1; + } + return `${v < 10 && i > 0 ? v.toFixed(1) : Math.round(v)} ${units[i]}`; +} + +interface RagKpis { + indexedDocuments: number; + indexedBytesTotal: number; + contributorUsers: number; + contentChunks: number; + chunksWithEmbedding: number; + embeddingCoveragePercent: number; + workflowEntities: 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 }>; + generatedAtUtc?: string; +} + +export const WorkspaceRagInsightsPage: React.FC = () => { + 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 : 'Laden fehlgeschlagen'); + setStats(null); + } finally { + setLoading(false); + } + }, [instanceId, request]); + + useEffect(() => { + void load(); + }, [load]); + + if (!instanceId) { + return ( +
+ Keine Workspace-Instanz ausgewählt. +
+ ); + } + + if (loading) { + return
Lade Kennzahlen …
; + } + + if (error) { + return
{error}
; + } + + const kpis = stats?.kpis; + const timeline = stats?.timelineIndexedDocuments ?? []; + const mimeRows = Object.entries(stats?.documentsByMimeCategory ?? {}).map(([key, value]) => ({ + name: MIME_LABELS[key] ?? key, + value, + })); + const statusRows = Object.entries(stats?.indexedDocumentsByStatus ?? {}).map(([name, value]) => ({ + name, + value, + })); + const chunkTypeRows = Object.entries(stats?.chunksByContentType ?? {}).map(([name, value]) => ({ + name, + value, + })); + + return ( +
+

+ 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 && ( +

+ Zuordnung Knowledge ↔ Dateien: {stats.scope.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. +

+ )} + + {kpis && ( +
+
+

{kpis.indexedDocuments}

+

Indexierte Dokumente

+
+
+

{_formatBytes(kpis.indexedBytesTotal)}

+

Indexiertes Datenvolumen (geschätzt)

+
+
+

{kpis.contentChunks}

+

Inhalts-Fragmente (Chunks)

+
+
+

+ {kpis.embeddingCoveragePercent}% +

+

Anteil Fragmente mit Embedding

+
+
+

{kpis.contributorUsers}

+

Beitragende Benutzer (Anzahl)

+
+
+

{kpis.workflowEntities}

+

Workflow-Entitäten (Cache)

+
+
+ )} + +
+

Neu indexierte Dokumente pro Tag (letzte Wochen)

+ {timeline.length === 0 ? ( +

Keine Zeitreihen-Daten für den gewählten Zeitraum.

+ ) : ( + + + + + + + + + + )} +
+ +
+
+

Dokumente nach Format-Kategorie

+ {mimeRows.length === 0 ? ( +

Keine Daten.

+ ) : ( + + + + + + + + + + )} +
+ +
+

Index-Status

+ {statusRows.length === 0 ? ( +

Keine Daten.

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

Fragmente nach Inhaltstyp

+ {chunkTypeRows.length === 0 ? ( +

Keine Chunk-Daten.

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

Stand (UTC): {stats.generatedAtUtc}

+ )} +
+ ); +}; diff --git a/src/types/mandate.ts b/src/types/mandate.ts index 9b38cb9..2e34a91 100644 --- a/src/types/mandate.ts +++ b/src/types/mandate.ts @@ -290,6 +290,7 @@ export const FEATURE_REGISTRY: Record = { views: [ { code: 'dashboard', label: { de: 'Dashboard', en: 'Dashboard', fr: 'Tableau de bord' }, path: 'dashboard' }, { code: 'editor', label: { de: 'Editor', en: 'Editor', fr: 'Editeur' }, path: 'editor' }, + { code: 'rag-insights', label: { de: 'Wissens-Insights', en: 'Knowledge insights', fr: 'Aperçu des connaissances' }, path: 'rag-insights' }, { code: 'settings', label: { de: 'Einstellungen', en: 'Settings', fr: 'Parametres' }, path: 'settings' }, ] },