mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
74 lines
2.7 KiB
TypeScript
74 lines
2.7 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import Box from '@mui/material/Box';
|
|
import Grow from '@mui/material/Grow';
|
|
import Typography from '@mui/material/Typography';
|
|
import CircularProgress from '@mui/material/CircularProgress';
|
|
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
|
import { backendReachable, onBackendReachability } from '@/shared/backendConnection';
|
|
|
|
// Honest "the local backend went away" state, so an unreachable backend never reads as a silent
|
|
// forever-spinner (ENG-242). The interceptor's background probe self-heals; this only tells the
|
|
// user what is happening while it does, and lets them force a reload if they are impatient.
|
|
const ReconnectingPill: React.FC = () => {
|
|
const c = useClaudeTokens();
|
|
const [reachable, setReachable] = useState(true);
|
|
// Only show after a short grace so a normal ~4s backend respawn heals invisibly; the pill is
|
|
// for the case that actually worried the user, a backend that stays gone.
|
|
const [showable, setShowable] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setReachable(backendReachable());
|
|
return onBackendReachability(setReachable);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (reachable) { setShowable(false); return undefined; }
|
|
const t = setTimeout(() => setShowable(true), 6000);
|
|
return () => clearTimeout(t);
|
|
}, [reachable]);
|
|
|
|
const show = !reachable && showable;
|
|
|
|
// Portal to body: `position: fixed` resolves against a transformed ancestor, not the viewport, and
|
|
// AppShell sits inside one, so the pill rendered visibly off-centre (measured on a screenshot).
|
|
return createPortal(
|
|
<Grow in={show} unmountOnExit>
|
|
<Box
|
|
onClick={() => window.location.reload()}
|
|
role="button"
|
|
aria-label="Reconnecting to OpenSwarm; click to reload"
|
|
sx={{
|
|
position: 'fixed',
|
|
bottom: 16,
|
|
left: '50%',
|
|
transform: 'translateX(-50%)',
|
|
zIndex: 1400,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1,
|
|
px: 1.75,
|
|
py: 1,
|
|
borderRadius: 999,
|
|
cursor: 'pointer',
|
|
WebkitAppRegion: 'no-drag',
|
|
background: c.bg.elevated,
|
|
border: `1px solid ${c.border.strong}`,
|
|
boxShadow: c.shadow.lg,
|
|
} as object}
|
|
>
|
|
<CircularProgress size={14} sx={{ color: c.text.secondary }} />
|
|
<Typography sx={{ fontSize: '0.8125rem', color: c.text.primary, fontWeight: 500 }}>
|
|
Reconnecting to OpenSwarm…
|
|
</Typography>
|
|
<Typography sx={{ fontSize: '0.75rem', color: c.text.tertiary }}>
|
|
click to reload
|
|
</Typography>
|
|
</Box>
|
|
</Grow>,
|
|
document.body,
|
|
);
|
|
};
|
|
|
|
export default ReconnectingPill;
|