| import { useEffect, useState } from 'react' |
| import { useI18n } from '../i18n' |
| import { Callout, Spinner } from './ui' |
| import { federatedStatus, type FederatedStatus } from '../api/ai' |
|
|
| |
| |
| |
| export function FederatedStatusCard({ detailed = false }: { detailed?: boolean }) { |
| const { t } = useI18n() |
| const [status, setStatus] = useState<FederatedStatus | null>(null) |
| const [loading, setLoading] = useState(true) |
| const [err, setErr] = useState(false) |
|
|
| useEffect(() => { |
| const c = new AbortController() |
| federatedStatus(c.signal) |
| .then(setStatus) |
| .catch(() => setErr(true)) |
| .finally(() => setLoading(false)) |
| return () => c.abort() |
| }, []) |
|
|
| if (loading) return <Spinner label={t('common.loading')} /> |
| if (err || !status) return <Callout tone="warn">{t('fed.unavailable')}</Callout> |
|
|
| const dp = status.differential_privacy_detail |
| const rows: [string, string][] = [ |
| ['client_type', status.client_type], |
| ['federation', status.federation], |
| ['secure_aggregation', status.secure_aggregation], |
| ['differential_privacy', status.differential_privacy], |
| ['real_device_training', status.real_device_training], |
| ['native_local_only_training', status.native_local_only_training], |
| ] |
|
|
| return ( |
| <div className="federated-status" data-testid="federated-status"> |
| <ul className="status-list"> |
| {rows.map(([key, value]) => ( |
| <li key={key}> |
| <span>{t(`federated.label.${key}` as never)}</span> |
| <span className="badge tone-info mono" data-testid={`fed-label-${key}`}>{value}</span> |
| </li> |
| ))} |
| </ul> |
| |
| {dp?.budget_exhausted && ( |
| <Callout tone="warn"> |
| <span data-testid="dp-exhausted">{t('fed.dp.exhausted')}</span> |
| </Callout> |
| )} |
| |
| <p className="muted small" data-testid="fed-no-authority">{t('federated.noAuthority')}</p> |
| |
| {detailed && ( |
| <> |
| <p className="muted small" data-testid="fed-synthetic">{t('fed.syntheticClients')}</p> |
| {status.aggregators && status.aggregators.length > 0 && ( |
| <p className="muted small">{t('fed.aggregators')}: <span className="mono">{status.aggregators.join(', ')}</span></p> |
| )} |
| <p className="muted small">{t('fed.candidate')}: <span className="mono">{status.candidate_status}</span> |
| {' · '}<span data-testid="fed-not-promotable">{t('fed.notPromotable')}</span></p> |
| <p className="muted small">{status.disclaimer}</p> |
| </> |
| )} |
| </div> |
| ) |
| } |
|
|