feat: connection lost/reconnected banner with i18n
Adds a slim animated banner below the header that: - Shows 'Connection lost — reconnecting…' with spinner when WS drops - Flashes 'Reconnected!' with auto-dismiss after 3s on recovery - Only appears after initial connection (not on first load) - Supports EN/FR via i18n system - Uses aria role=alert for accessibility
This commit is contained in:
@@ -4,6 +4,7 @@ import { Header } from './components/Header';
|
||||
import { Sidebar } from './components/Sidebar';
|
||||
import { Chat } from './components/Chat';
|
||||
import { LoginScreen } from './components/LoginScreen';
|
||||
import { ConnectionBanner } from './components/ConnectionBanner';
|
||||
|
||||
export default function App() {
|
||||
const {
|
||||
@@ -38,6 +39,7 @@ export default function App() {
|
||||
/>
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<Header status={status} sessionKey={activeSession} onToggleSidebar={() => setSidebarOpen(!sidebarOpen)} activeSessionData={sessions.find(s => s.key === activeSession)} onLogout={logout} />
|
||||
<ConnectionBanner status={status} />
|
||||
<Chat messages={messages} isGenerating={isGenerating} status={status} onSend={sendMessage} onAbort={abort} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
72
src/components/ConnectionBanner.tsx
Normal file
72
src/components/ConnectionBanner.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Wifi, Loader2 } from 'lucide-react';
|
||||
import type { ConnectionStatus } from '../types';
|
||||
import { useT } from '../hooks/useLocale';
|
||||
|
||||
interface Props {
|
||||
status: ConnectionStatus;
|
||||
}
|
||||
|
||||
type BannerState = 'hidden' | 'reconnecting' | 'reconnected';
|
||||
|
||||
export function ConnectionBanner({ status }: Props) {
|
||||
const t = useT();
|
||||
const [banner, setBanner] = useState<BannerState>('hidden');
|
||||
const prevStatus = useRef(status);
|
||||
const dismissTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const prev = prevStatus.current;
|
||||
prevStatus.current = status;
|
||||
|
||||
if (dismissTimer.current) {
|
||||
clearTimeout(dismissTimer.current);
|
||||
dismissTimer.current = null;
|
||||
}
|
||||
|
||||
if (status === 'disconnected' || status === 'connecting') {
|
||||
// Only show reconnecting if we were previously connected (not initial load)
|
||||
if (prev === 'connected') {
|
||||
setBanner('reconnecting');
|
||||
}
|
||||
} else if (status === 'connected' && prev !== 'connected') {
|
||||
// Just reconnected — flash success only if we were showing the banner
|
||||
if (banner === 'reconnecting' || prev === 'disconnected' || prev === 'connecting') {
|
||||
setBanner('reconnected');
|
||||
dismissTimer.current = setTimeout(() => setBanner('hidden'), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (dismissTimer.current) clearTimeout(dismissTimer.current);
|
||||
};
|
||||
}, [status]);
|
||||
|
||||
if (banner === 'hidden') return null;
|
||||
|
||||
const isReconnecting = banner === 'reconnecting';
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
className={`flex items-center justify-center gap-2 px-4 py-2 text-xs font-medium transition-all duration-500 animate-in slide-in-from-top ${
|
||||
isReconnecting
|
||||
? 'bg-amber-500/10 text-amber-300 border-b border-amber-500/20'
|
||||
: 'bg-emerald-500/10 text-emerald-300 border-b border-emerald-500/20'
|
||||
}`}
|
||||
>
|
||||
{isReconnecting ? (
|
||||
<>
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
<span>{t('connection.reconnecting')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Wifi size={14} />
|
||||
<span>{t('connection.reconnected')}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -49,6 +49,10 @@ const en = {
|
||||
// Tool call
|
||||
'tool.result': 'Result',
|
||||
|
||||
// Connection banner
|
||||
'connection.reconnecting': 'Connection lost — reconnecting…',
|
||||
'connection.reconnected': 'Reconnected!',
|
||||
|
||||
// Timestamps
|
||||
'time.yesterday': 'Yesterday',
|
||||
} as const;
|
||||
@@ -89,6 +93,9 @@ const fr: Record<keyof typeof en, string> = {
|
||||
|
||||
'tool.result': 'Résultat',
|
||||
|
||||
'connection.reconnecting': 'Connexion perdue — reconnexion…',
|
||||
'connection.reconnected': 'Reconnecté !',
|
||||
|
||||
'time.yesterday': 'Hier',
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user