feat: add loading indicator when switching sessions
Show a spinner with 'Loading messages…' text while chat history is being fetched during session switches, instead of briefly flashing the empty welcome screen. Includes EN/FR i18n.
This commit is contained in:
@@ -10,7 +10,7 @@ import { KeyboardShortcuts } from './components/KeyboardShortcuts';
|
||||
|
||||
export default function App() {
|
||||
const {
|
||||
status, messages, sessions, activeSession, isGenerating,
|
||||
status, messages, sessions, activeSession, isGenerating, isLoadingHistory,
|
||||
sendMessage, abort, switchSession,
|
||||
authenticated, login, logout, connectError, isConnecting,
|
||||
} = useGateway();
|
||||
@@ -77,7 +77,7 @@ export default function App() {
|
||||
<div className="flex-1 flex flex-col min-w-0" aria-hidden={sidebarOpen ? true : undefined}>
|
||||
<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} />
|
||||
<Chat messages={messages} isGenerating={isGenerating} isLoadingHistory={isLoadingHistory} status={status} onSend={sendMessage} onAbort={abort} />
|
||||
</div>
|
||||
<KeyboardShortcuts open={shortcutsOpen} onClose={() => setShortcutsOpen(false)} />
|
||||
</div>
|
||||
|
||||
@@ -3,13 +3,14 @@ import { ChatMessageComponent } from './ChatMessage';
|
||||
import { ChatInput } from './ChatInput';
|
||||
import { TypingIndicator } from './TypingIndicator';
|
||||
import type { ChatMessage, ConnectionStatus } from '../types';
|
||||
import { Bot, ArrowDown } from 'lucide-react';
|
||||
import { Bot, ArrowDown, Loader2 } from 'lucide-react';
|
||||
import { useT } from '../hooks/useLocale';
|
||||
import { getLocale, type TranslationKey } from '../lib/i18n';
|
||||
|
||||
interface Props {
|
||||
messages: ChatMessage[];
|
||||
isGenerating: boolean;
|
||||
isLoadingHistory: boolean;
|
||||
status: ConnectionStatus;
|
||||
onSend: (text: string, attachments?: Array<{ mimeType: string; fileName: string; content: string }>) => void;
|
||||
onAbort: () => void;
|
||||
@@ -63,7 +64,7 @@ function getDateKey(ts: number): string {
|
||||
/** Threshold in pixels — if the user is within this distance of the bottom, auto-scroll */
|
||||
const SCROLL_THRESHOLD = 150;
|
||||
|
||||
export function Chat({ messages, isGenerating, status, onSend, onAbort }: Props) {
|
||||
export function Chat({ messages, isGenerating, isLoadingHistory, status, onSend, onAbort }: Props) {
|
||||
const t = useT();
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -128,7 +129,13 @@ export function Chat({ messages, isGenerating, status, onSend, onAbort }: Props)
|
||||
<div className="flex-1 flex flex-col min-h-0 relative">
|
||||
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative" role="log" aria-label={t('chat.messages')} aria-live="polite">
|
||||
<div className="max-w-4xl mx-auto py-4">
|
||||
{messages.length === 0 && (
|
||||
{messages.length === 0 && isLoadingHistory && (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] text-zinc-500">
|
||||
<Loader2 className="h-8 w-8 text-cyan-300/60 animate-spin mb-4" />
|
||||
<div className="text-sm text-zinc-500">{t('chat.loadingHistory')}</div>
|
||||
</div>
|
||||
)}
|
||||
{messages.length === 0 && !isLoadingHistory && (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] text-zinc-500">
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute -inset-4 rounded-3xl bg-gradient-to-r from-cyan-400/10 via-indigo-500/10 to-violet-500/10 blur-2xl" />
|
||||
|
||||
@@ -28,6 +28,7 @@ export function useGateway() {
|
||||
const [sessions, setSessions] = useState<Session[]>([]);
|
||||
const [activeSession, setActiveSession] = useState('agent:main:main');
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
|
||||
const [authenticated, setAuthenticated] = useState<boolean | null>(null); // null = checking
|
||||
const [connectError, setConnectError] = useState<string | null>(null);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
@@ -100,6 +101,7 @@ export function useGateway() {
|
||||
}, []);
|
||||
|
||||
const loadHistory = useCallback(async (sessionKey: string) => {
|
||||
setIsLoadingHistory(true);
|
||||
try {
|
||||
const res = await clientRef.current?.send('chat.history', { sessionKey, limit: 100 });
|
||||
const rawMsgs = res?.messages as Array<Record<string, unknown>> | undefined;
|
||||
@@ -173,6 +175,8 @@ export function useGateway() {
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore history load failures
|
||||
} finally {
|
||||
setIsLoadingHistory(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -388,7 +392,7 @@ export function useGateway() {
|
||||
}));
|
||||
|
||||
return {
|
||||
status, messages, sessions: enrichedSessions, activeSession, isGenerating,
|
||||
status, messages, sessions: enrichedSessions, activeSession, isGenerating, isLoadingHistory,
|
||||
sendMessage, abort, switchSession, loadSessions,
|
||||
authenticated, login, logout, connectError, isConnecting,
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ const en = {
|
||||
// Chat
|
||||
'chat.welcome': 'PinchChat',
|
||||
'chat.welcomeSub': 'Send a message to get started',
|
||||
'chat.loadingHistory': 'Loading messages…',
|
||||
'chat.inputPlaceholder': 'Type a message…',
|
||||
'chat.inputLabel': 'Message',
|
||||
'chat.attachFile': 'Attach file',
|
||||
@@ -106,6 +107,7 @@ const fr: Record<keyof typeof en, string> = {
|
||||
|
||||
'chat.welcome': 'PinchChat',
|
||||
'chat.welcomeSub': 'Envoyez un message pour commencer',
|
||||
'chat.loadingHistory': 'Chargement des messages…',
|
||||
'chat.inputPlaceholder': 'Tapez un message…',
|
||||
'chat.inputLabel': 'Message',
|
||||
'chat.attachFile': 'Joindre un fichier',
|
||||
|
||||
Reference in New Issue
Block a user