Files
AIRegulation-DocAnalysis/frontend/src/pages/Status/StatusPage.tsx
T

392 lines
14 KiB
TypeScript
Raw Normal View History

import React, { useCallback, useEffect, useState } from 'react';
import { useTheme } from '../../contexts';
2026-05-14 15:07:34 +08:00
import { TPattern } from '../../components/common/TPattern';
import { getSystemStats, getSystemConfig, getSystemHealth, type SystemStats, type SystemConfig, type SystemHealth } from '../../api/status';
2026-05-14 15:07:34 +08:00
import { getDocumentList, type DocInfo } from '../../api/docs';
const StatsCard = ({ label, value, accent = false }: {
label: string;
value: number;
accent?: boolean;
}) => {
const { theme, isDark } = useTheme();
return (
<div style={{
padding: 20,
background: theme.bgCard,
borderRadius: 12,
border: `1px solid ${accent ? theme.accent : theme.border}`,
position: 'relative',
overflow: 'hidden',
boxShadow: !isDark ? '0 2px 8px rgba(226,0,116,0.06)' : 'none',
}}>
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 3,
background: theme.gradientAccent,
}} />
<div className="mono" style={{ fontSize: 11, color: theme.text3, marginBottom: 8, letterSpacing: '1px' }}>{label}</div>
<div className="mono" style={{ fontSize: 32, fontWeight: 700, color: accent ? theme.accent : theme.text }}>{value}</div>
</div>
);
};
const ServiceBadge = ({
label,
status,
detail,
}: {
label: string;
status: 'ok' | 'error' | 'unknown' | boolean;
detail?: string;
}) => {
const { theme } = useTheme();
const isOk = status === 'ok' || status === true;
const isUnknown = status === 'unknown';
const color = isUnknown ? theme.text3 : isOk ? theme.green : '#d64545';
return (
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 16px',
background: theme.bgCard,
borderRadius: 10,
border: `1px solid ${theme.border}`,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ color, fontSize: 10 }}></span>
<span className="mono" style={{ fontSize: 12, color: theme.text2 }}>{label}</span>
</div>
<span style={{ fontSize: 12, color, fontWeight: 600 }}>
{detail ?? (isUnknown ? '—' : isOk ? 'OK' : 'ERROR')}
</span>
</div>
);
};
2026-05-14 15:07:34 +08:00
export const StatusPage: React.FC = () => {
const { theme, isDark } = useTheme();
const [stats, setStats] = useState<SystemStats>({
documents_total: 0,
documents_indexed: 0,
documents_failed: 0,
chunks_total: 0,
});
2026-05-14 15:07:34 +08:00
const [config, setConfig] = useState<SystemConfig | null>(null);
const [docs, setDocs] = useState<DocInfo[]>([]);
const [health, setHealth] = useState<SystemHealth | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
2026-05-14 15:07:34 +08:00
const loadData = useCallback(async () => {
setLoading(true);
setError(null);
2026-05-14 15:07:34 +08:00
try {
const [statsRes, configRes, docsRes, healthRes] = await Promise.all([
2026-05-14 15:07:34 +08:00
getSystemStats(),
getSystemConfig(),
getDocumentList(),
getSystemHealth(),
2026-05-14 15:07:34 +08:00
]);
setStats(statsRes);
setConfig(configRes);
setDocs(docsRes.docs);
setHealth(healthRes);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load status data');
} finally {
setLoading(false);
2026-05-14 15:07:34 +08:00
}
}, []);
2026-05-14 15:07:34 +08:00
// Initial load
useEffect(() => {
const timerId = window.setTimeout(() => { void loadData(); }, 0);
return () => window.clearTimeout(timerId);
}, [loadData]);
// Auto-poll every 5 s while any document is still processing
useEffect(() => {
const hasProcessing = docs.some(d => d.status === 'parsing' || d.status === 'pending');
if (!hasProcessing) return;
const id = window.setInterval(() => void loadData(), 5000);
return () => window.clearInterval(id);
}, [docs, loadData]);
2026-05-14 15:07:34 +08:00
return (
<div className="relative w-full">
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
2026-05-14 15:07:34 +08:00
<TPattern />
{/* Loading indicator */}
{loading && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16, color: theme.text3, fontSize: 13 }}>
<span style={{
display: 'inline-block',
width: 12,
height: 12,
borderRadius: '50%',
border: `2px solid ${theme.accent}`,
borderTopColor: 'transparent',
animation: 'spin 0.8s linear infinite',
}} />
<span className="mono">LOADING...</span>
</div>
)}
{/* Error banner */}
{error && (
<div style={{
marginBottom: 16,
padding: '12px 16px',
background: '#d6454520',
border: '1px solid #d64545',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}>
<span style={{ fontSize: 13, color: '#d64545' }}>{error}</span>
<button
onClick={() => void loadData()}
style={{ background: 'none', border: '1px solid #d64545', borderRadius: 6, color: '#d64545', cursor: 'pointer', padding: '4px 10px', fontSize: 12 }}
>
重试
</button>
</div>
)}
{/* Stats section */}
2026-05-14 15:07:34 +08:00
<section style={{ marginBottom: 48 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
<h2 style={{ fontSize: 14, fontWeight: 600, color: theme.accent, letterSpacing: '1px', margin: 0 }}>
DOCUMENT STATISTICS
</h2>
<button
onClick={() => void loadData()}
disabled={loading}
style={{
background: 'none',
border: `1px solid ${theme.border}`,
borderRadius: 6,
color: theme.text3,
cursor: loading ? 'not-allowed' : 'pointer',
padding: '4px 12px',
fontSize: 11,
opacity: loading ? 0.5 : 1,
}}
>
刷新
</button>
</div>
2026-05-14 15:07:34 +08:00
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: 16,
}}>
<StatsCard label="DOCUMENTS" value={stats.documents_total} />
<StatsCard label="INDEXED" value={stats.documents_indexed} />
<StatsCard label="FAILED" value={stats.documents_failed} />
<StatsCard label="CHUNKS" value={stats.chunks_total} accent />
2026-05-14 15:07:34 +08:00
</div>
</section>
{/* Service health section */}
<section style={{ marginBottom: 48 }}>
<h2 style={{ fontSize: 14, fontWeight: 600, color: theme.accent, marginBottom: 20, letterSpacing: '1px' }}>
SERVICE HEALTH
</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12, marginBottom: 12 }}>
<ServiceBadge
label="MILVUS"
status={health?.milvus.status ?? 'unknown'}
detail={health ? (health.milvus.status === 'ok' ? `${health.milvus.num_entities ?? 0} entities` : 'disconnected') : '—'}
/>
<ServiceBadge
label="MINIO"
status={health?.minio.status ?? 'unknown'}
detail={health ? (health.minio.status === 'ok' ? 'connected' : 'disconnected') : '—'}
/>
<ServiceBadge
label="BM25 HYBRID"
status={health?.bm25.available ?? false}
detail={health ? (health.bm25.available ? 'enabled' : 'unavailable') : '—'}
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12 }}>
<ServiceBadge
label="RERANKER"
status={health?.reranker.enabled ?? false}
detail={health ? (health.reranker.enabled ? (health.reranker.model ?? 'enabled') : 'disabled') : '—'}
/>
<ServiceBadge
label="SESSIONS"
status="ok"
detail={health ? `${health.sessions.active} / ${health.sessions.max}` : '—'}
/>
<ServiceBadge
label="LLM"
status="ok"
detail={config ? `${config.llm_provider} · ${config.llm_model}` : '—'}
/>
</div>
</section>
{/* System configuration section */}
2026-05-14 15:07:34 +08:00
<section style={{ marginBottom: 48 }}>
<h2 style={{
fontSize: 14,
fontWeight: 600,
color: theme.accent,
marginBottom: 20,
letterSpacing: '1px',
}}>SYSTEM CONFIGURATION</h2>
<div style={{ marginBottom: 20 }}>
<div className="mono" style={{ fontSize: 11, color: theme.text3, marginBottom: 12, letterSpacing: '1px' }}>MODELS</div>
2026-05-14 15:07:34 +08:00
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: 12,
}}>
{[
['LLM Provider', config?.llm_provider || '-'],
['LLM Model', config?.llm_model || '-'],
['Embedding Model', config?.embedding_model || '-'],
['Embedding Dim', String(config?.embedding_dim || 0)],
2026-05-14 15:07:34 +08:00
].map(([k, v]) => (
<div key={k} style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
background: theme.bgCard,
borderRadius: 10,
border: `1px solid ${theme.border}`,
boxShadow: !isDark ? '0 2px 8px rgba(226,0,116,0.04)' : 'none',
overflow: 'hidden',
2026-05-14 15:07:34 +08:00
}}>
<span className="mono" style={{ fontSize: 12, color: theme.text3, flexShrink: 0 }}>{k}</span>
<span
title={v}
style={{
fontSize: 13,
fontWeight: 500,
maxWidth: 200,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
cursor: 'help',
marginLeft: 8,
}}
>
{v}
</span>
2026-05-14 15:07:34 +08:00
</div>
))}
</div>
</div>
<div style={{ marginBottom: 20 }}>
<div className="mono" style={{ fontSize: 11, color: theme.text3, marginBottom: 12, letterSpacing: '1px' }}>STORAGE AND PATHS</div>
2026-05-14 15:07:34 +08:00
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: 12,
}}>
{[
['Milvus Collection', config?.milvus_collection || '-'],
['Metadata Path', config?.document_metadata_path || '-'],
['Embedding Base URL', config?.embedding_base_url || '-'],
2026-05-14 15:07:34 +08:00
].map(([k, v]) => (
<div key={k} style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
background: theme.bgCard,
borderRadius: 10,
border: `1px solid ${theme.border}`,
boxShadow: !isDark ? '0 2px 8px rgba(226,0,116,0.04)' : 'none',
overflow: 'hidden',
2026-05-14 15:07:34 +08:00
}}>
<span className="mono" style={{ fontSize: 12, color: theme.text3, flexShrink: 0 }}>{k}</span>
<span
title={v}
style={{
fontSize: 13,
fontWeight: 500,
maxWidth: 200,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
cursor: 'help',
marginLeft: 8,
}}
>
{v}
</span>
2026-05-14 15:07:34 +08:00
</div>
))}
</div>
</div>
</section>
{/* Document index section */}
2026-05-14 15:07:34 +08:00
<section>
<h2 style={{
fontSize: 14,
fontWeight: 600,
color: theme.accent,
marginBottom: 20,
letterSpacing: '1px',
}}>DOCUMENT INDEX</h2>
{docs.map(d => (
<div key={d.id} style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: 16,
background: theme.bgCard,
borderRadius: 10,
marginBottom: 10,
border: `1px solid ${
d.status === 'failed' ? '#d64545' :
d.status === 'parsing' || d.status === 'pending' ? theme.accent + '80' :
theme.border
}`,
2026-05-14 15:07:34 +08:00
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, overflow: 'hidden' }}>
<span style={{ fontSize: 14, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{d.name}</span>
<span className="mono" style={{ fontSize: 11, color: theme.text3, flexShrink: 0 }}>
{d.updated_at ? new Date(d.updated_at).toLocaleString() : d.status}
</span>
2026-05-14 15:07:34 +08:00
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 16, flexShrink: 0 }}>
2026-05-14 15:07:34 +08:00
<span className="mono" style={{ fontSize: 12, color: theme.text2 }}>{d.chunks} chunks</span>
<div style={{
padding: '4px 12px',
background:
d.status === 'failed' ? '#d64545' :
d.status === 'parsing' || d.status === 'pending' ? theme.accent :
theme.green,
2026-05-14 15:07:34 +08:00
borderRadius: 6,
opacity: d.status === 'parsing' || d.status === 'pending' ? 0.85 : 1,
2026-05-14 15:07:34 +08:00
}}>
<span className="mono" style={{ fontSize: 10, fontWeight: 600, color: '#fff' }}>
{d.status === 'parsing' ? '⟳ ' : ''}{d.status.toUpperCase()}
</span>
2026-05-14 15:07:34 +08:00
</div>
</div>
</div>
))}
</section>
</div>
2026-05-14 15:07:34 +08:00
);
};