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 */}
|
||||
<Route path="editor" element={<FeatureViewPage view="editor" />} />
|
||||
<Route path="rag-insights" element={<FeatureViewPage view="rag-insights" />} />
|
||||
|
||||
{/* Teams Bot Feature Views */}
|
||||
<Route path="sessions" element={<FeatureViewPage view="sessions" />} />
|
||||
|
|
|
|||
|
|
@ -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<string, Record<string, ViewComponent>> = {
|
|||
workspace: {
|
||||
dashboard: WorkspacePage,
|
||||
editor: WorkspaceEditorPage,
|
||||
'rag-insights': WorkspaceRagInsightsPage,
|
||||
settings: WorkspaceSettingsPage,
|
||||
},
|
||||
teamsbot: {
|
||||
|
|
@ -196,7 +198,7 @@ export const FeatureViewPage: React.FC<FeatureViewPageProps> = ({ 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
{ 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' },
|
||||
]
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue