rag stats
This commit is contained in:
parent
0367563ea8
commit
064635ae74
5 changed files with 360 additions and 1 deletions
|
|
@ -156,6 +156,7 @@ function App() {
|
||||||
|
|
||||||
{/* Workspace Editor */}
|
{/* Workspace Editor */}
|
||||||
<Route path="editor" element={<FeatureViewPage view="editor" />} />
|
<Route path="editor" element={<FeatureViewPage view="editor" />} />
|
||||||
|
<Route path="rag-insights" element={<FeatureViewPage view="rag-insights" />} />
|
||||||
|
|
||||||
{/* Teams Bot Feature Views */}
|
{/* Teams Bot Feature Views */}
|
||||||
<Route path="sessions" element={<FeatureViewPage view="sessions" />} />
|
<Route path="sessions" element={<FeatureViewPage view="sessions" />} />
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import { AutomationDefinitionsView, AutomationTemplatesView } from './views/auto
|
||||||
import { WorkspacePage } from './views/workspace/WorkspacePage';
|
import { WorkspacePage } from './views/workspace/WorkspacePage';
|
||||||
import { WorkspaceEditorPage } from './views/workspace/WorkspaceEditorPage';
|
import { WorkspaceEditorPage } from './views/workspace/WorkspaceEditorPage';
|
||||||
import { WorkspaceSettingsPage } from './views/workspace/WorkspaceSettingsPage';
|
import { WorkspaceSettingsPage } from './views/workspace/WorkspaceSettingsPage';
|
||||||
|
import { WorkspaceRagInsightsPage } from './views/workspace/WorkspaceRagInsightsPage';
|
||||||
|
|
||||||
// Teamsbot Views
|
// Teamsbot Views
|
||||||
import { TeamsbotDashboardView } from './views/teamsbot/TeamsbotDashboardView';
|
import { TeamsbotDashboardView } from './views/teamsbot/TeamsbotDashboardView';
|
||||||
|
|
@ -130,6 +131,7 @@ const VIEW_COMPONENTS: Record<string, Record<string, ViewComponent>> = {
|
||||||
workspace: {
|
workspace: {
|
||||||
dashboard: WorkspacePage,
|
dashboard: WorkspacePage,
|
||||||
editor: WorkspaceEditorPage,
|
editor: WorkspaceEditorPage,
|
||||||
|
'rag-insights': WorkspaceRagInsightsPage,
|
||||||
settings: WorkspaceSettingsPage,
|
settings: WorkspaceSettingsPage,
|
||||||
},
|
},
|
||||||
teamsbot: {
|
teamsbot: {
|
||||||
|
|
@ -196,7 +198,7 @@ export const FeatureViewPage: React.FC<FeatureViewPageProps> = ({ view }) => {
|
||||||
|
|
||||||
// Workspace dashboard is rendered persistently by WorkspaceKeepAlive at MainLayout level;
|
// Workspace dashboard is rendered persistently by WorkspaceKeepAlive at MainLayout level;
|
||||||
// other workspace views (e.g. settings, editor) use the standard FeatureViewPage rendering.
|
// 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
273
src/pages/views/workspace/WorkspaceRagInsightsPage.tsx
Normal file
273
src/pages/views/workspace/WorkspaceRagInsightsPage.tsx
Normal file
|
|
@ -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<string, string> = {
|
||||||
|
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<string, number>;
|
||||||
|
documentsByMimeCategory?: Record<string, number>;
|
||||||
|
chunksByContentType?: Record<string, number>;
|
||||||
|
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<string | null>(null);
|
||||||
|
const [stats, setStats] = useState<RagStatsResponse | null>(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 (
|
||||||
|
<div style={{ padding: 32, textAlign: 'center', color: '#999' }}>
|
||||||
|
Keine Workspace-Instanz ausgewählt.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className={styles.wrap} style={{ padding: 24 }}>Lade Kennzahlen …</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className={styles.error}>{error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={styles.wrap}>
|
||||||
|
<p className={styles.disclaimer}>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{stats?.scope?.workspaceFileIdsResolved !== undefined && (
|
||||||
|
<p className={styles.meta} style={{ marginTop: 0 }}>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{kpis && (
|
||||||
|
<div className={styles.kpiGrid}>
|
||||||
|
<div className={styles.kpiCard}>
|
||||||
|
<p className={styles.kpiValue}>{kpis.indexedDocuments}</p>
|
||||||
|
<p className={styles.kpiLabel}>Indexierte Dokumente</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.kpiCard}>
|
||||||
|
<p className={styles.kpiValue}>{_formatBytes(kpis.indexedBytesTotal)}</p>
|
||||||
|
<p className={styles.kpiLabel}>Indexiertes Datenvolumen (geschätzt)</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.kpiCard}>
|
||||||
|
<p className={styles.kpiValue}>{kpis.contentChunks}</p>
|
||||||
|
<p className={styles.kpiLabel}>Inhalts-Fragmente (Chunks)</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.kpiCard}>
|
||||||
|
<p className={styles.kpiValue}>
|
||||||
|
{kpis.embeddingCoveragePercent}%
|
||||||
|
</p>
|
||||||
|
<p className={styles.kpiLabel}>Anteil Fragmente mit Embedding</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.kpiCard}>
|
||||||
|
<p className={styles.kpiValue}>{kpis.contributorUsers}</p>
|
||||||
|
<p className={styles.kpiLabel}>Beitragende Benutzer (Anzahl)</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.kpiCard}>
|
||||||
|
<p className={styles.kpiValue}>{kpis.workflowEntities}</p>
|
||||||
|
<p className={styles.kpiLabel}>Workflow-Entitäten (Cache)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.chartBlock}>
|
||||||
|
<h3 className={styles.chartTitle}>Neu indexierte Dokumente pro Tag (letzte Wochen)</h3>
|
||||||
|
{timeline.length === 0 ? (
|
||||||
|
<p className={styles.meta}>Keine Zeitreihen-Daten für den gewählten Zeitraum.</p>
|
||||||
|
) : (
|
||||||
|
<ResponsiveContainer width="100%" height={240}>
|
||||||
|
<LineChart data={timeline}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="date" tick={{ fontSize: 11 }} />
|
||||||
|
<YAxis allowDecimals={false} tick={{ fontSize: 11 }} />
|
||||||
|
<Tooltip />
|
||||||
|
<Line type="monotone" dataKey="indexedDocuments" name="Dokumente" stroke="#1976d2" dot={false} strokeWidth={2} />
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.row2}>
|
||||||
|
<div className={styles.chartBlock}>
|
||||||
|
<h3 className={styles.chartTitle}>Dokumente nach Format-Kategorie</h3>
|
||||||
|
{mimeRows.length === 0 ? (
|
||||||
|
<p className={styles.meta}>Keine Daten.</p>
|
||||||
|
) : (
|
||||||
|
<ResponsiveContainer width="100%" height={260}>
|
||||||
|
<BarChart data={mimeRows} layout="vertical" margin={{ left: 8, right: 16 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis type="number" allowDecimals={false} />
|
||||||
|
<YAxis type="category" dataKey="name" width={120} tick={{ fontSize: 11 }} />
|
||||||
|
<Tooltip />
|
||||||
|
<Bar dataKey="value" name="Anzahl" fill="#00897b" radius={[0, 4, 4, 0]} />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.chartBlock}>
|
||||||
|
<h3 className={styles.chartTitle}>Index-Status</h3>
|
||||||
|
{statusRows.length === 0 ? (
|
||||||
|
<p className={styles.meta}>Keine Daten.</p>
|
||||||
|
) : (
|
||||||
|
<ResponsiveContainer width="100%" height={260}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={statusRows}
|
||||||
|
dataKey="value"
|
||||||
|
nameKey="name"
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
outerRadius={88}
|
||||||
|
label={({ name, percent }) =>
|
||||||
|
`${name ?? ''} ${percent != null ? (percent * 100).toFixed(0) : '0'}%`}
|
||||||
|
>
|
||||||
|
{statusRows.map((_, i) => (
|
||||||
|
<Cell key={`st-${i}`} fill={CHART_COLORS[i % CHART_COLORS.length]} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.chartBlock}>
|
||||||
|
<h3 className={styles.chartTitle}>Fragmente nach Inhaltstyp</h3>
|
||||||
|
{chunkTypeRows.length === 0 ? (
|
||||||
|
<p className={styles.meta}>Keine Chunk-Daten.</p>
|
||||||
|
) : (
|
||||||
|
<ResponsiveContainer width="100%" height={240}>
|
||||||
|
<BarChart data={chunkTypeRows}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" tick={{ fontSize: 11 }} />
|
||||||
|
<YAxis allowDecimals={false} tick={{ fontSize: 11 }} />
|
||||||
|
<Tooltip />
|
||||||
|
<Bar dataKey="value" name="Fragmente" fill="#6a1b9a" radius={[4, 4, 0, 0]} />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{stats?.generatedAtUtc && (
|
||||||
|
<p className={styles.meta}>Stand (UTC): {stats.generatedAtUtc}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -290,6 +290,7 @@ export const FEATURE_REGISTRY: Record<string, FeatureConfig> = {
|
||||||
views: [
|
views: [
|
||||||
{ code: 'dashboard', label: { de: 'Dashboard', en: 'Dashboard', fr: 'Tableau de bord' }, path: 'dashboard' },
|
{ 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: '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' },
|
{ code: 'settings', label: { de: 'Einstellungen', en: 'Settings', fr: 'Parametres' }, path: 'settings' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue