import React, { useEffect, useState, useCallback } from 'react' import * as api from '../api' import type { SystemStatus, ConnectionState } from '../api' import { useTranslation } from '../i18n' import { useDateFormatter } from '../utils/dateUtils' interface ConnectionStatusBarProps { pollIntervalMs?: number className?: string } type ConnStatus = 'ready' | 'warning' | 'error' | 'unknown' interface Snapshot { xunfei: ConnStatus xunfeiMessage: string xunfeiLabel: string deepseek: ConnStatus deepseekMessage: string whisper: ConnStatus whisperMessage: string asrActiveEngine: string } /** 将 ConnectionState 映射到视觉状态 */ function connStateToDot(state: ConnectionState): { dot: string; bg: string; text: string } { switch (state) { case 'online': return { dot: 'bg-green-500', bg: 'bg-green-50 border-green-200', text: 'text-green-700' } case 'timeout': return { dot: 'bg-yellow-500', bg: 'bg-yellow-50 border-yellow-200', text: 'text-yellow-700' } case 'auth_error': return { dot: 'bg-yellow-500', bg: 'bg-yellow-50 border-yellow-200', text: 'text-yellow-700' } case 'offline': return { dot: 'bg-red-500', bg: 'bg-red-50 border-red-200', text: 'text-red-700' } case 'unconfigured': return { dot: 'bg-gray-400', bg: 'bg-gray-50 border-gray-200', text: 'text-gray-600' } default: return { dot: 'bg-gray-400', bg: 'bg-gray-50 border-gray-200', text: 'text-gray-600' } } } const colorMap: Record = { ready: { dot: 'bg-green-500', bg: 'bg-green-50 border-green-200', text: 'text-green-700' }, warning: { dot: 'bg-yellow-500', bg: 'bg-yellow-50 border-yellow-200', text: 'text-yellow-700' }, error: { dot: 'bg-red-500', bg: 'bg-red-50 border-red-200', text: 'text-red-700' }, unknown: { dot: 'bg-gray-400', bg: 'bg-gray-50 border-gray-200', text: 'text-gray-600' }, } function summarize(h: Awaited>, t: (key: string) => string): Snapshot { return { xunfei: (h.xunfei?.status as ConnStatus) || 'unknown', xunfeiMessage: h.xunfei?.message || (h.xunfei?.available ? t('components.connection.xunfeiReady') : t('components.connection.xunfeiNotConfigured')), xunfeiLabel: h.xunfei?.model_label || h.xunfei?.model || t('components.connection.xunfei'), deepseek: (h.deepseek?.status as ConnStatus) || 'unknown', deepseekMessage: h.deepseek?.message || (h.deepseek?.available ? t('components.connection.deepseekReady') : t('components.connection.deepseekUnavailable')), whisper: (h.whisper?.status as ConnStatus) || 'unknown', whisperMessage: h.whisper?.message || 'Whisper', asrActiveEngine: h.asr_active_engine || 'xunfei', } } /** 单个服务状态指示器(基于真实连接检测) */ const LivePill: React.FC<{ label: string state: ConnectionState detail: string }> = ({ label, state, detail }) => { const { t } = useTranslation() const c = connStateToDot(state) const isOnline = state === 'online' const isOffline = state === 'offline' return (
{label} {isOffline ? t('components.connection.offline') : isOnline ? t('components.connection.online') : state === 'timeout' ? t('components.connection.timeout') : state === 'auth_error' ? t('components.connection.authError') : detail}
) } const Pill: React.FC<{ label: string status: ConnStatus message: string }> = ({ label, status, message }) => { const c = colorMap[status] return (
{label} {message}
) } export const ConnectionStatusBar: React.FC = ({ pollIntervalMs = 10000, className = '', }) => { const { t } = useTranslation() const { formatDate, formatDateTime, formatTime } = useDateFormatter() const [snapshot, setSnapshot] = useState(null) const [lastUpdated, setLastUpdated] = useState(0) /** 真实连接状态(每 30 秒轮询) */ const [systemStatus, setSystemStatus] = useState(null) const [anyOffline, setAnyOffline] = useState(false) // 轮询详细健康检查(配置/凭证状态) useEffect(() => { let timer: ReturnType | null = null let cancelled = false const fetchOnce = async () => { try { const h = await api.getDetailedHealth() if (cancelled) return setSnapshot(summarize(h, t)) setLastUpdated(Date.now()) } catch (e) { if (cancelled) return setSnapshot({ xunfei: 'error', xunfeiMessage: t('components.connection.backendUnreachable'), xunfeiLabel: t('components.connection.xunfei'), deepseek: 'error', deepseekMessage: t('components.connection.backendUnreachable'), whisper: 'error', whisperMessage: t('components.connection.backendUnreachable'), asrActiveEngine: 'unknown', }) } } fetchOnce() timer = setInterval(fetchOnce, pollIntervalMs) return () => { cancelled = true if (timer) clearInterval(timer) } }, [pollIntervalMs]) // 轮询实时连接状态(每 30 秒) const fetchSystemStatus = useCallback(async () => { try { const s = await api.getSystemStatus() setSystemStatus(s) setAnyOffline( s.xunfei === 'offline' || s.deepseek === 'offline' || s.xunfei === 'timeout' || s.deepseek === 'timeout' ) } catch { // 后端不可达时不覆盖已有状态 } }, []) useEffect(() => { fetchSystemStatus() const timer = setInterval(fetchSystemStatus, 30000) return () => clearInterval(timer) }, [fetchSystemStatus]) if (!snapshot) { return (
{t('components.connection.checking')}
) } return (
{/* 离线警告横幅 */} {anyOffline && systemStatus && (
{!systemStatus.internet ? t('components.connection.networkDisconnected') : systemStatus.dns === 'fail' ? t('components.connection.dnsFailed') : t('components.connection.aiServiceUnavailable')}
)}
{/* 优先显示实时连接状态 */} {systemStatus ? ( <> ) : ( <> )} {t('components.connection.activeEngine')}: {snapshot.asrActiveEngine} {lastUpdated > 0 && ( <> · {t('components.connection.updatedAt')} {formatTime(new Date(lastUpdated))} )}
) } export default ConnectionStatusBar