update for 1. 优化 2.中英切换
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import { Topbar } from '../../components/layout/Topbar';
|
||||
import { Search, Upload, Download, RefreshCw, CheckCircle, XCircle, AlertTriangle, Info } from 'lucide-react';
|
||||
import { UploadModal } from '../Docs/UploadModal';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
const TOKEN_KEY = 'auth_token';
|
||||
function authHeader(): Record<string, string> {
|
||||
@@ -48,13 +49,14 @@ function StatusIcon({ status }: { status: 'ok' | 'error' | 'warn' | 'info' }) {
|
||||
}
|
||||
|
||||
function ServiceRow({ name, status, detail }: { name: string; status: 'ok' | 'error' | 'warn' | 'info'; detail?: string }) {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<div className="service-row">
|
||||
<StatusIcon status={status} />
|
||||
<span className="service-name" style={{ marginLeft: 8 }}>{name}</span>
|
||||
{detail && <span style={{ fontSize: 11, color: 'var(--muted)', marginLeft: 6 }}>{detail}</span>}
|
||||
<span className={`status ${status}`} style={{ marginLeft: 'auto' }}>
|
||||
{status === 'ok' ? 'Online' : status === 'error' ? 'Error' : status === 'warn' ? 'Degraded' : 'Unknown'}
|
||||
{status === 'ok' ? t.status.badgeOnline : status === 'error' ? t.status.badgeError : status === 'warn' ? t.status.badgeDegraded : t.status.badgeUnknown}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -73,6 +75,7 @@ function ConfigRow({ label, value }: { label: string; value: string | number | n
|
||||
|
||||
// ── Main component ─────────────────────────────────────────────────────────
|
||||
export function StatusPage() {
|
||||
const { t } = useLanguage();
|
||||
const [stats, setStats] = useState<Stats | null>(null);
|
||||
const [health, setHealth] = useState<Health | null>(null);
|
||||
const [config, setConfig] = useState<Config | null>(null);
|
||||
@@ -136,21 +139,21 @@ export function StatusPage() {
|
||||
return (
|
||||
<div className="status-page">
|
||||
<Topbar
|
||||
title="System Status"
|
||||
title={t.status.topbarTitle}
|
||||
actions={
|
||||
<>
|
||||
<div className="search-box">
|
||||
<Search size={13} />
|
||||
<input placeholder="Search..." />
|
||||
<input placeholder={t.status.searchPlaceholder} />
|
||||
</div>
|
||||
<button className="btn sm" onClick={handleExport}>
|
||||
<Download size={13} />Export
|
||||
<Download size={13} />{t.status.exportBtn}
|
||||
</button>
|
||||
<button className="btn sm" onClick={() => setRefreshKey(k => k + 1)}>
|
||||
<RefreshCw size={13} />Refresh
|
||||
<RefreshCw size={13} />{t.status.refreshBtn}
|
||||
</button>
|
||||
<button className="btn sm primary" onClick={() => setShowUpload(true)}>
|
||||
<Upload size={13} />New upload
|
||||
<Upload size={13} />{t.status.newUploadBtn}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
@@ -162,26 +165,26 @@ export function StatusPage() {
|
||||
<div className="stats-grid">
|
||||
<div className="stat-cell">
|
||||
{loading ? <span className="loading-shimmer stat-value-loading" /> : <div className="stat-value">{stats?.documents_total ?? '—'}</div>}
|
||||
<div className="stat-label">Documents total</div>
|
||||
<div className="stat-label">{t.status.statTotal}</div>
|
||||
</div>
|
||||
<div className="stat-cell">
|
||||
{loading ? <span className="loading-shimmer stat-value-loading" /> : <div className="stat-value">{stats?.documents_indexed ?? '—'}</div>}
|
||||
<div className="stat-label">Indexed</div>
|
||||
<div className="stat-label">{t.status.statIndexed}</div>
|
||||
</div>
|
||||
<div className="stat-cell danger">
|
||||
{loading ? <span className="loading-shimmer stat-value-loading" /> : <div className="stat-value">{stats?.documents_failed ?? '—'}</div>}
|
||||
<div className="stat-label">Failed</div>
|
||||
<div className="stat-label">{t.status.statFailed}</div>
|
||||
</div>
|
||||
<div className="stat-cell">
|
||||
{loading ? <span className="loading-shimmer stat-value-loading" /> : <div className="stat-value">{stats?.chunks_total?.toLocaleString() ?? '—'}</div>}
|
||||
<div className="stat-label">Vector chunks</div>
|
||||
<div className="stat-label">{t.status.statChunks}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Indexed progress bar */}
|
||||
{!loading && stats && stats.documents_total > 0 && (
|
||||
<div style={{ padding: '0 0 20px', display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<span style={{ fontSize: 12, color: 'var(--muted)', whiteSpace: 'nowrap' }}>Index coverage</span>
|
||||
<span style={{ fontSize: 12, color: 'var(--muted)', whiteSpace: 'nowrap' }}>{t.status.statCoverage}</span>
|
||||
<div style={{ flex: 1, height: 6, background: 'var(--border)', borderRadius: 3, overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
height: '100%', borderRadius: 3,
|
||||
@@ -203,7 +206,7 @@ export function StatusPage() {
|
||||
{/* System health */}
|
||||
<div className="card">
|
||||
<div className="card-header" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span>System health</span>
|
||||
<span>{t.status.cardHealth}</span>
|
||||
{lastRefresh && (
|
||||
<span style={{ fontSize: 11, color: 'var(--muted)', fontWeight: 400 }}>
|
||||
Updated {lastRefresh.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
||||
@@ -231,12 +234,12 @@ export function StatusPage() {
|
||||
<ServiceRow
|
||||
name="BM25 keyword retriever"
|
||||
status={health.bm25.available ? 'ok' : 'warn'}
|
||||
detail={health.bm25.available ? undefined : 'Not loaded'}
|
||||
detail={health.bm25.available ? undefined : t.status.serviceNotLoaded}
|
||||
/>
|
||||
<ServiceRow
|
||||
name={`Reranker${health.reranker.model ? ` (${health.reranker.model})` : ''}`}
|
||||
status={health.reranker.enabled ? 'ok' : 'info'}
|
||||
detail={health.reranker.enabled ? 'Enabled' : 'Disabled'}
|
||||
detail={health.reranker.enabled ? t.status.serviceEnabled : t.status.serviceDisabled}
|
||||
/>
|
||||
<ServiceRow
|
||||
name="Active sessions"
|
||||
@@ -246,7 +249,7 @@ export function StatusPage() {
|
||||
</>
|
||||
) : (
|
||||
<div style={{ padding: '12px 0', color: 'var(--muted)', fontSize: 13 }}>
|
||||
Could not reach health endpoint
|
||||
{t.status.healthEndpointError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -257,7 +260,7 @@ export function StatusPage() {
|
||||
onClick={() => setConfigOpen(v => !v)}
|
||||
style={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between', background: 'none', border: 'none', cursor: 'pointer', padding: 0 }}
|
||||
>
|
||||
<div className="card-header" style={{ margin: 0, padding: 0, flex: 1, textAlign: 'left' }}>System configuration</div>
|
||||
<div className="card-header" style={{ margin: 0, padding: 0, flex: 1, textAlign: 'left' }}>{t.status.cardConfig}</div>
|
||||
<span style={{ fontSize: 11, color: 'var(--muted)', transform: configOpen ? 'rotate(180deg)' : 'none', transition: 'transform 0.2s' }}>▾</span>
|
||||
</button>
|
||||
|
||||
@@ -265,17 +268,17 @@ export function StatusPage() {
|
||||
<div style={{ marginTop: 12 }}>
|
||||
{config ? (
|
||||
<>
|
||||
<ConfigRow label="LLM provider" value={config.llm_provider} />
|
||||
<ConfigRow label="LLM model" value={config.llm_model} />
|
||||
<ConfigRow label="Embedding model" value={config.embedding_model} />
|
||||
<ConfigRow label="Embedding dim" value={config.embedding_dim} />
|
||||
<ConfigRow label="Milvus collection" value={config.milvus_collection} />
|
||||
<ConfigRow label="Parser backend" value={config.parser_backend} />
|
||||
<ConfigRow label="Chunk backend" value={config.chunk_backend} />
|
||||
<ConfigRow label="Parser failure mode" value={config.parser_failure_mode} />
|
||||
<ConfigRow label={t.status.labelLLMProvider} value={config.llm_provider} />
|
||||
<ConfigRow label={t.status.labelLLMModel} value={config.llm_model} />
|
||||
<ConfigRow label={t.status.labelEmbeddingModel} value={config.embedding_model} />
|
||||
<ConfigRow label={t.status.labelEmbeddingDim} value={config.embedding_dim} />
|
||||
<ConfigRow label={t.status.labelMilvusCollection} value={config.milvus_collection} />
|
||||
<ConfigRow label={t.status.labelParserBackend} value={config.parser_backend} />
|
||||
<ConfigRow label={t.status.labelChunkBackend} value={config.chunk_backend} />
|
||||
<ConfigRow label={t.status.labelParserFailureMode} value={config.parser_failure_mode} />
|
||||
</>
|
||||
) : (
|
||||
<div style={{ color: 'var(--muted)', fontSize: 13 }}>Could not load config</div>
|
||||
<div style={{ color: 'var(--muted)', fontSize: 13 }}>{t.status.configLoadError}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -286,7 +289,7 @@ export function StatusPage() {
|
||||
|
||||
{/* Document breakdown */}
|
||||
<div className="card">
|
||||
<div className="card-header">Document breakdown</div>
|
||||
<div className="card-header">{t.status.cardBreakdown}</div>
|
||||
{loading ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{[1, 2, 3].map(i => <div key={i} className="loading-shimmer" style={{ height: 24, borderRadius: 4 }} />)}
|
||||
@@ -294,9 +297,9 @@ export function StatusPage() {
|
||||
) : stats ? (
|
||||
<>
|
||||
{[
|
||||
{ label: 'Indexed', value: stats.documents_indexed, total: stats.documents_total, color: 'var(--ok)' },
|
||||
{ label: 'Processing / Parsed', value: stats.documents_total - stats.documents_indexed - stats.documents_failed, total: stats.documents_total, color: 'var(--warn)' },
|
||||
{ label: 'Failed', value: stats.documents_failed, total: stats.documents_total, color: 'var(--danger)' },
|
||||
{ label: t.status.breakdownIndexed, value: stats.documents_indexed, total: stats.documents_total, color: 'var(--ok)' },
|
||||
{ label: t.status.breakdownProcessing, value: stats.documents_total - stats.documents_indexed - stats.documents_failed, total: stats.documents_total, color: 'var(--warn)' },
|
||||
{ label: t.status.breakdownFailed, value: stats.documents_failed, total: stats.documents_total, color: 'var(--danger)' },
|
||||
].map(row => {
|
||||
const pct = stats.documents_total > 0 ? Math.round((Math.max(0, row.value) / stats.documents_total) * 100) : 0;
|
||||
return (
|
||||
@@ -312,7 +315,7 @@ export function StatusPage() {
|
||||
);
|
||||
})}
|
||||
<div style={{ marginTop: 8, paddingTop: 8, borderTop: '1px solid var(--border)', display: 'flex', justifyContent: 'space-between', fontSize: 12 }}>
|
||||
<span style={{ color: 'var(--muted)' }}>Total vector chunks</span>
|
||||
<span style={{ color: 'var(--muted)' }}>{t.status.totalChunks}</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontWeight: 600 }}>{stats.chunks_total.toLocaleString()}</span>
|
||||
</div>
|
||||
</>
|
||||
@@ -322,26 +325,26 @@ export function StatusPage() {
|
||||
{/* Sessions & reranker quick facts */}
|
||||
{health && (
|
||||
<div className="card">
|
||||
<div className="card-header">Runtime info</div>
|
||||
<div className="card-header">{t.status.cardRuntime}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, padding: '4px 0' }}>
|
||||
<span style={{ color: 'var(--muted)' }}>Active chat sessions</span>
|
||||
<span style={{ color: 'var(--muted)' }}>{t.status.labelActiveSessions}</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)' }}>{health.sessions.active}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, padding: '4px 0' }}>
|
||||
<span style={{ color: 'var(--muted)' }}>Session capacity</span>
|
||||
<span style={{ color: 'var(--muted)' }}>{t.status.labelSessionCapacity}</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)' }}>{health.sessions.max}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, padding: '4px 0' }}>
|
||||
<span style={{ color: 'var(--muted)' }}>Cross-encoder reranker</span>
|
||||
<span style={{ color: 'var(--muted)' }}>{t.status.labelReranker}</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', color: health.reranker.enabled ? 'var(--ok)' : 'var(--muted)' }}>
|
||||
{health.reranker.enabled ? (health.reranker.model ?? 'Enabled') : 'Disabled'}
|
||||
{health.reranker.enabled ? (health.reranker.model ?? t.status.serviceEnabled) : t.status.serviceDisabled}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, padding: '4px 0' }}>
|
||||
<span style={{ color: 'var(--muted)' }}>BM25 hybrid retrieval</span>
|
||||
<span style={{ color: 'var(--muted)' }}>{t.status.labelBM25}</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', color: health.bm25.available ? 'var(--ok)' : 'var(--muted)' }}>
|
||||
{health.bm25.available ? 'Active' : 'Unavailable'}
|
||||
{health.bm25.available ? t.status.statusActive : t.status.statusUnavailable}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -353,7 +356,7 @@ export function StatusPage() {
|
||||
|
||||
<footer className="page-footer">
|
||||
<div className="live-dot" />
|
||||
<span>Regulation Hub · T-Systems AI · {health ? (health.milvus.status === 'ok' && health.minio.connected ? 'All systems operational' : 'Degraded') : 'Checking…'}</span>
|
||||
<span>Regulation Hub · T-Systems AI · {health ? (health.milvus.status === 'ok' && health.minio.connected ? t.status.footerAllOk : t.status.footerDegraded) : t.status.footerChecking}</span>
|
||||
</footer>
|
||||
|
||||
{showUpload && <UploadModal onClose={() => setShowUpload(false)} />}
|
||||
|
||||
Reference in New Issue
Block a user