fix: resolve all ESLint errors and add lint step to CI

- Extract credential helpers to src/lib/credentials.ts (fixes react-refresh/only-export-components)
- Extract buildImageSrc to src/lib/image.ts (fixes react-refresh/only-export-components)
- Reorder useCallback declarations in useGateway to fix react-hooks/immutability
- Sync refs via useEffect instead of during render (fixes react-hooks/refs)
- Replace useState initializer effect with lazy initializer functions in LoginScreen
- Add comments to empty catch blocks (fixes no-empty)
- Remove unused variable (fixes @typescript-eslint/no-unused-vars)
- Downgrade react-hooks/set-state-in-effect to warning (valid init/status patterns)
- Add lint step to CI workflow (runs before type-check and build)
This commit is contained in:
Nicolas Varrot
2026-02-11 23:37:37 +00:00
parent f8be728842
commit 916910f5ce
9 changed files with 199 additions and 178 deletions

View File

@@ -8,7 +8,8 @@ import type { ChatMessage as ChatMessageType, MessageBlock } from '../types';
import { ThinkingBlock } from './ThinkingBlock';
import { CodeBlock } from './CodeBlock';
import { ToolCall } from './ToolCall';
import { ImageBlock, buildImageSrc } from './ImageBlock';
import { ImageBlock } from './ImageBlock';
import { buildImageSrc } from '../lib/image';
import { Bot, User, Wrench, Copy, Check, RefreshCw } from 'lucide-react';
import { t, getLocale } from '../lib/i18n';
import { useLocale } from '../hooks/useLocale';

View File

@@ -12,7 +12,7 @@ type BannerState = 'hidden' | 'reconnecting' | 'reconnected';
export function ConnectionBanner({ status }: Props) {
const t = useT();
const [banner, setBanner] = useState<BannerState>('hidden');
const prevStatus = useRef(status);
const prevStatus = useRef<ConnectionStatus | null>(null);
const dismissTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
@@ -25,16 +25,12 @@ export function ConnectionBanner({ status }: Props) {
}
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);
}
} else if (status === 'connected' && prev !== null && prev !== 'connected') {
setBanner('reconnected');
dismissTimer.current = setTimeout(() => setBanner('hidden'), 3000);
}
return () => {

View File

@@ -66,9 +66,4 @@ export function ImageBlock({ src, alt }: ImageBlockProps) {
);
}
/** Build a data URL from base64 image data */
export function buildImageSrc(mediaType: string, data?: string, url?: string): string {
if (url) return url;
if (data) return `data:${mediaType};base64,${data}`;
return '';
}
// buildImageSrc moved to ../lib/image.ts

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { useState } from 'react';
import { Sparkles, Eye, EyeOff, Loader2 } from 'lucide-react';
import { useT } from '../hooks/useLocale';
import { getStoredCredentials } from '../lib/credentials';
interface Props {
onConnect: (url: string, token: string) => void;
@@ -8,41 +9,23 @@ interface Props {
isConnecting?: boolean;
}
const STORAGE_KEY = 'pinchchat_credentials';
export function getStoredCredentials(): { url: string; token: string } | null {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (parsed.url && parsed.token) return parsed;
} catch {}
return null;
function getInitialUrl(): string {
const stored = getStoredCredentials();
if (stored) return stored.url;
return import.meta.env.VITE_GATEWAY_WS_URL || `ws://${window.location.hostname}:18789`;
}
export function storeCredentials(url: string, token: string) {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ url, token }));
}
export function clearCredentials() {
localStorage.removeItem(STORAGE_KEY);
function getInitialToken(): string {
const stored = getStoredCredentials();
return stored?.token ?? '';
}
export function LoginScreen({ onConnect, error, isConnecting }: Props) {
const t = useT();
const defaultUrl = import.meta.env.VITE_GATEWAY_WS_URL || `ws://${window.location.hostname}:18789`;
const [url, setUrl] = useState(defaultUrl);
const [token, setToken] = useState('');
const [url, setUrl] = useState(getInitialUrl);
const [token, setToken] = useState(getInitialToken);
const [showToken, setShowToken] = useState(false);
useEffect(() => {
const stored = getStoredCredentials();
if (stored) {
setUrl(stored.url);
setToken(stored.token);
}
}, []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!url.trim() || !token.trim()) return;