Download vetcopilot-app/src/components/ConnectionStatusBar.tsx from WWsCa/VetNet-LCPS: direct link, hf CLI and curl.
- Browser
- Download file 8.74 kB
-
https://huggingface.co/WWsCa/VetNet-LCPS/resolve/main/vetcopilot-app/src/components/ConnectionStatusBar.tsx
- Command line
-
hf download hf://WWsCa/VetNet-LCPS/vetcopilot-app/src/components/ConnectionStatusBar.tsx
-
curl -L -o ConnectionStatusBar.tsx https://huggingface.co/WWsCa/VetNet-LCPS/resolve/main/vetcopilot-app/src/components/ConnectionStatusBar.tsx
8.74 kB
| 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<ConnStatus, { dot: string; bg: string; text: string }> = { | |
| 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<ReturnType<typeof api.getDetailedHealth>>, 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 ( | |
| <div | |
| className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-lg border ${c.bg}`} | |
| title={detail} | |
| > | |
| <span | |
| className={`w-2 h-2 rounded-full ${c.dot} ${isOnline ? 'animate-pulse' : ''}`} | |
| /> | |
| <span className={`text-xs font-medium ${c.text}`}>{label}</span> | |
| <span className="text-[10px] text-gray-500 hidden sm:inline"> | |
| {isOffline ? t('components.connection.offline') : isOnline ? t('components.connection.online') : state === 'timeout' ? t('components.connection.timeout') : state === 'auth_error' ? t('components.connection.authError') : detail} | |
| </span> | |
| </div> | |
| ) | |
| } | |
| const Pill: React.FC<{ | |
| label: string | |
| status: ConnStatus | |
| message: string | |
| }> = ({ label, status, message }) => { | |
| const c = colorMap[status] | |
| return ( | |
| <div className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-lg border ${c.bg}`}> | |
| <span className={`w-2 h-2 rounded-full ${c.dot} ${status === 'ready' ? 'animate-pulse' : ''}`} /> | |
| <span className={`text-xs font-medium ${c.text}`}>{label}</span> | |
| <span className="text-[10px] text-gray-500 hidden sm:inline" title={message}> | |
| {message} | |
| </span> | |
| </div> | |
| ) | |
| } | |
| export const ConnectionStatusBar: React.FC<ConnectionStatusBarProps> = ({ | |
| pollIntervalMs = 10000, | |
| className = '', | |
| }) => { | |
| const { t } = useTranslation() | |
| const { formatDate, formatDateTime, formatTime } = useDateFormatter() | |
| const [snapshot, setSnapshot] = useState<Snapshot | null>(null) | |
| const [lastUpdated, setLastUpdated] = useState<number>(0) | |
| /** 真实连接状态(每 30 秒轮询) */ | |
| const [systemStatus, setSystemStatus] = useState<SystemStatus | null>(null) | |
| const [anyOffline, setAnyOffline] = useState(false) | |
| // 轮询详细健康检查(配置/凭证状态) | |
| useEffect(() => { | |
| let timer: ReturnType<typeof setInterval> | 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 ( | |
| <div className={`flex items-center gap-2 ${className}`}> | |
| <span className="text-[10px] text-gray-400">{t('components.connection.checking')}</span> | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div className={`flex flex-col gap-1 ${className}`}> | |
| {/* 离线警告横幅 */} | |
| {anyOffline && systemStatus && ( | |
| <div className="flex items-center gap-2 px-2 py-1 bg-red-50 border border-red-300 rounded text-xs text-red-700"> | |
| <span className="w-2 h-2 rounded-full bg-red-500 animate-pulse" /> | |
| <span> | |
| {!systemStatus.internet | |
| ? t('components.connection.networkDisconnected') | |
| : systemStatus.dns === 'fail' | |
| ? t('components.connection.dnsFailed') | |
| : t('components.connection.aiServiceUnavailable')} | |
| </span> | |
| </div> | |
| )} | |
| <div className="flex items-center gap-1.5 flex-wrap"> | |
| {/* 优先显示实时连接状态 */} | |
| {systemStatus ? ( | |
| <> | |
| <LivePill | |
| label={`${t('components.connection.xunfei')} ${snapshot.xunfeiLabel}`} | |
| state={systemStatus.xunfei} | |
| detail={systemStatus.xunfei_detail} | |
| /> | |
| <LivePill | |
| label="DeepSeek" | |
| state={systemStatus.deepseek} | |
| detail={systemStatus.deepseek_detail} | |
| /> | |
| </> | |
| ) : ( | |
| <> | |
| <Pill | |
| label={`${t('components.connection.xunfei')} ${snapshot.xunfeiLabel}`} | |
| status={snapshot.xunfei} | |
| message={snapshot.xunfeiMessage} | |
| /> | |
| <Pill | |
| label="DeepSeek" | |
| status={snapshot.deepseek} | |
| message={snapshot.deepseekMessage} | |
| /> | |
| </> | |
| )} | |
| <Pill label="Whisper" status={snapshot.whisper} message={snapshot.whisperMessage} /> | |
| <span className="text-[10px] text-gray-400 hidden md:inline"> | |
| {t('components.connection.activeEngine')}: {snapshot.asrActiveEngine} | |
| {lastUpdated > 0 && ( | |
| <> · {t('components.connection.updatedAt')} {formatTime(new Date(lastUpdated))}</> | |
| )} | |
| </span> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| export default ConnectionStatusBar |