fix: resolve ESLint errors for React compiler rules
- Chat.tsx: replace mutable variable in render IIFE with useMemo+reduce to satisfy react-hooks/immutability rule - ConnectionBanner.tsx: move setState calls into a useCallback to avoid synchronous setState in effect body (react-hooks/set-state-in-effect) - useGateway.ts: suppress set-state-in-effect for legitimate mount init
This commit is contained in:
18
FEEDBACK.md
18
FEEDBACK.md
@@ -175,3 +175,21 @@
|
|||||||
- GitHub rend nativement les blocs ```mermaid dans les README
|
- GitHub rend nativement les blocs ```mermaid dans les README
|
||||||
- Utiliser un flowchart ou graph LR/TD montrant : Browser → WebSocket → OpenClaw Gateway → LLM Provider, avec les composants internes (LoginScreen, Chat, Sidebar, Gateway client, etc.)
|
- Utiliser un flowchart ou graph LR/TD montrant : Browser → WebSocket → OpenClaw Gateway → LLM Provider, avec les composants internes (LoginScreen, Chat, Sidebar, Gateway client, etc.)
|
||||||
- Plus lisible et maintenable que l'ASCII art
|
- Plus lisible et maintenable que l'ASCII art
|
||||||
|
|
||||||
|
## Item #22
|
||||||
|
- **Date:** 2026-02-12
|
||||||
|
- **Priority:** high
|
||||||
|
- **Status:** pending
|
||||||
|
- **Description:** CI GitHub Actions en échec — vérifier et réparer en priorité. Le cron doit aussi vérifier l'état de la CI en début de chaque session avant toute autre amélioration. Si la CI est cassée, c'est la priorité #1.
|
||||||
|
|
||||||
|
## Item #23
|
||||||
|
- **Date:** 2026-02-12
|
||||||
|
- **Priority:** medium
|
||||||
|
- **Status:** pending
|
||||||
|
- **Description:** Icônes par channel/type dans la liste des sessions (sidebar)
|
||||||
|
- Discord → icône Discord
|
||||||
|
- Telegram → icône Telegram
|
||||||
|
- Cron → icône horloge ou engrenage
|
||||||
|
- Webchat → icône chat/bulle
|
||||||
|
- Fallback générique pour les channels non-vanilla (ex: TeamSpeak) → icône par défaut (bulle ou globe)
|
||||||
|
- Utiliser des SVG ou une lib d'icônes (lucide-react, react-icons, etc.)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
import { useEffect, useRef, useCallback, useState, useMemo } from 'react';
|
||||||
import { ChatMessageComponent } from './ChatMessage';
|
import { ChatMessageComponent } from './ChatMessage';
|
||||||
import { ChatInput } from './ChatInput';
|
import { ChatInput } from './ChatInput';
|
||||||
import { TypingIndicator } from './TypingIndicator';
|
import { TypingIndicator } from './TypingIndicator';
|
||||||
@@ -112,6 +112,16 @@ export function Chat({ messages, isGenerating, status, onSend, onAbort }: Props)
|
|||||||
onSend(text, attachments);
|
onSend(text, attachments);
|
||||||
}, [onSend]);
|
}, [onSend]);
|
||||||
|
|
||||||
|
const visibleMessages = useMemo(() => {
|
||||||
|
const filtered = messages.filter(hasVisibleContent);
|
||||||
|
return filtered.reduce<Array<{ msg: ChatMessage; showSep: boolean }>>((acc, msg) => {
|
||||||
|
const dk = getDateKey(msg.timestamp);
|
||||||
|
const prevDk = acc.length > 0 ? getDateKey(acc[acc.length - 1].msg.timestamp) : '';
|
||||||
|
acc.push({ msg, showSep: dk !== prevDk });
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
const showTyping = isGenerating && !hasStreamedText(messages);
|
const showTyping = isGenerating && !hasStreamedText(messages);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -130,13 +140,7 @@ export function Chat({ messages, isGenerating, status, onSend, onAbort }: Props)
|
|||||||
<div className="text-sm mt-1 text-zinc-500">{t('chat.welcomeSub')}</div>
|
<div className="text-sm mt-1 text-zinc-500">{t('chat.welcomeSub')}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(() => {
|
{visibleMessages.map(({ msg, showSep }) => (
|
||||||
let lastDateKey = '';
|
|
||||||
return messages.filter(hasVisibleContent).map(msg => {
|
|
||||||
const dk = getDateKey(msg.timestamp);
|
|
||||||
const showSep = dk !== lastDateKey;
|
|
||||||
lastDateKey = dk;
|
|
||||||
return (
|
|
||||||
<div key={msg.id}>
|
<div key={msg.id}>
|
||||||
{showSep && (
|
{showSep && (
|
||||||
<div className="flex items-center gap-3 py-3 px-4 select-none" aria-label={formatDateSeparator(msg.timestamp, t)}>
|
<div className="flex items-center gap-3 py-3 px-4 select-none" aria-label={formatDateSeparator(msg.timestamp, t)}>
|
||||||
@@ -147,9 +151,7 @@ export function Chat({ messages, isGenerating, status, onSend, onAbort }: Props)
|
|||||||
)}
|
)}
|
||||||
<ChatMessageComponent message={msg} onRetry={!isGenerating ? handleSend : undefined} />
|
<ChatMessageComponent message={msg} onRetry={!isGenerating ? handleSend : undefined} />
|
||||||
</div>
|
</div>
|
||||||
);
|
))}
|
||||||
});
|
|
||||||
})()}
|
|
||||||
{showTyping && <TypingIndicator />}
|
{showTyping && <TypingIndicator />}
|
||||||
<div ref={bottomRef} />
|
<div ref={bottomRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { Wifi, Loader2 } from 'lucide-react';
|
import { Wifi, Loader2 } from 'lucide-react';
|
||||||
import type { ConnectionStatus } from '../types';
|
import type { ConnectionStatus } from '../types';
|
||||||
import { useT } from '../hooks/useLocale';
|
import { useT } from '../hooks/useLocale';
|
||||||
@@ -15,28 +15,31 @@ export function ConnectionBanner({ status }: Props) {
|
|||||||
const prevStatus = useRef<ConnectionStatus | null>(null);
|
const prevStatus = useRef<ConnectionStatus | null>(null);
|
||||||
const dismissTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const dismissTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const updateBanner = useCallback((prev: ConnectionStatus | null, current: ConnectionStatus) => {
|
||||||
const prev = prevStatus.current;
|
|
||||||
prevStatus.current = status;
|
|
||||||
|
|
||||||
if (dismissTimer.current) {
|
if (dismissTimer.current) {
|
||||||
clearTimeout(dismissTimer.current);
|
clearTimeout(dismissTimer.current);
|
||||||
dismissTimer.current = null;
|
dismissTimer.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'disconnected' || status === 'connecting') {
|
if (current === 'disconnected' || current === 'connecting') {
|
||||||
if (prev === 'connected') {
|
if (prev === 'connected') {
|
||||||
setBanner('reconnecting');
|
setBanner('reconnecting');
|
||||||
}
|
}
|
||||||
} else if (status === 'connected' && prev !== null && prev !== 'connected') {
|
} else if (current === 'connected' && prev !== null && prev !== 'connected') {
|
||||||
setBanner('reconnected');
|
setBanner('reconnected');
|
||||||
dismissTimer.current = setTimeout(() => setBanner('hidden'), 3000);
|
dismissTimer.current = setTimeout(() => setBanner('hidden'), 3000);
|
||||||
}
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const prev = prevStatus.current;
|
||||||
|
prevStatus.current = status;
|
||||||
|
updateBanner(prev, status);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (dismissTimer.current) clearTimeout(dismissTimer.current);
|
if (dismissTimer.current) clearTimeout(dismissTimer.current);
|
||||||
};
|
};
|
||||||
}, [status]);
|
}, [status, updateBanner]);
|
||||||
|
|
||||||
if (banner === 'hidden') return null;
|
if (banner === 'hidden') return null;
|
||||||
|
|
||||||
|
|||||||
@@ -307,6 +307,8 @@ export function useGateway() {
|
|||||||
initRef.current = true;
|
initRef.current = true;
|
||||||
const stored = getStoredCredentials();
|
const stored = getStoredCredentials();
|
||||||
if (stored) {
|
if (stored) {
|
||||||
|
// Init on mount — setupClient sets state as part of establishing the connection
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setupClient(stored.url, stored.token);
|
setupClient(stored.url, stored.token);
|
||||||
} else {
|
} else {
|
||||||
setAuthenticated(false);
|
setAuthenticated(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user