Files
EveFlow/src/components/chat/ChatPanel.tsx
T
Claude 5f0fa7a1e2 fix: full review pass (main process, voice pipeline, Hermes client, HUD) and e2e-validated local voice (v2.1.0.1)
Electron main
- utility-process channel: sherpa output copied into V8 buffers (Electron rejects external
  buffers), audio exchanged as base64; worker restart on timeout, identity-safe exit handling,
  deterministic native unload by restart
- webhook: loopback by default without secret, UTF-8-safe body assembly, clean restart, port validation
- files IPC: realpath-based root check, openPath allow-list (reveal-only outside document folders),
  Windows reserved names; store: debounced async atomic writes with backup of corrupt files;
  logger: streaming writes with rotation; log message size cap
- HTTP stream proxy: socket released on idle timeout / renderer destroyed, id validation
- model manager: inactivity timeout, retrying rm/rename (Windows locks), engine stopped before
  replacing a model; WAV decoder handles float/24-bit; IPC payload validation
- window: opaque rounded window on Windows, navigation lock-down, visibility events;
  Ctrl+Alt+Escape instead of the Task Manager shortcut; single-instance guard; EVEFLOW_USER_DATA

Voice pipeline
- abort semantics (SendHandle.aborted), abort before the stream opens, session id prefixes per transport
- hands-free re-arm after replies without speech, start/stop race, no chime on auto re-arm,
  no silence shipped to STT (400 ms pre-roll), no transcription of empty manual stops
- TTS: bounded prefetch, cancellable segments, non-interrupting notices, volume applied at play time
- SSE CRLF split, usage in chat completions, finish_reason length, phonetic regex hoisted

HUD
- core renderer: no canvas shadows, cached colours, reusable spectrum buffer, theme read on change,
  30 fps idle, stops when the window is hidden; ping flashes on send / tool / speech
- deltas coalesced per animation frame; stable auto-scroll; narrow selectors everywhere
- bundled fonts (offline), reduce-motion fix, error toasts, ops drawer below 1180 px,
  interim transcript and first-token latency in the core caption, Ctrl+K, dialog semantics,
  switch/aria roles, compact widget cleanup, Whisper small recommended for French
- docs/ROADMAP.md: audit results, e2e results and the plan towards a real JARVIS

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Wn5VX9HNbJ7N54hR24u9Y
2026-09-03 18:08:09 +00:00

100 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from 'react';
import { MessageSquare, Trash2, X } from 'lucide-react';
import { useChat } from '../../state/chat';
import { useSettings } from '../../state/settings';
import { MessageBubble } from './MessageBubble';
import { CommandBar } from './CommandBar';
interface Props {
compact?: boolean;
}
export function ChatPanel({ compact }: Props) {
const messages = useChat((s) => s.messages);
const clear = useChat((s) => s.clear);
const assistantName = useSettings((s) => s.settings.assistantName);
const userName = useSettings((s) => s.settings.userName);
const showReasoning = useSettings((s) => s.settings.ui.showReasoning);
const listRef = useRef<HTMLDivElement>(null);
const programmatic = useRef(false);
const [zoom, setZoom] = useState<string | null>(null);
const [stick, setStick] = useState(true);
useEffect(() => {
if (!stick) return;
const el = listRef.current;
if (!el) return;
programmatic.current = true;
el.scrollTop = el.scrollHeight;
}, [messages, stick]);
useEffect(() => {
if (!zoom) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setZoom(null);
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [zoom]);
const onScroll = () => {
if (programmatic.current) {
programmatic.current = false;
return;
}
const el = listRef.current;
if (!el) return;
setStick(el.scrollHeight - el.scrollTop - el.clientHeight < 80);
};
const visible = compact ? messages.slice(-6) : messages;
return (
<section className={`panel bracket chat-panel${compact ? ' compact' : ''}`}>
{!compact && (
<header className="panel-head">
<MessageSquare size={14} />
<span>Transmission</span>
<span className="spacer" />
<span className="chip">{messages.length} msg</span>
<button className="icon-btn" title="Effacer la conversation" onClick={clear} disabled={messages.length === 0}>
<Trash2 size={14} />
</button>
</header>
)}
<div ref={listRef} className={`chat-list${compact ? ' compact' : ''}`} onScroll={onScroll}>
{visible.length === 0 && (
<div className="empty">
{compact ? 'En attente…' : `Canal ouvert. Parlez ou écrivez à ${assistantName}.`}
{!compact && (
<div className="muted" style={{ marginTop: 8, fontSize: 12 }}>
<span className="kbd">Ctrl</span> + <span className="kbd">Shift</span> + <span className="kbd">Espace</span> active le micro depuis n’importe où.
</div>
)}
</div>
)}
{visible.map((m) => (
<MessageBubble
key={m.id}
message={m}
assistantName={assistantName}
userName={userName}
showReasoning={showReasoning}
compact={compact}
onOpenImage={setZoom}
/>
))}
</div>
<CommandBar compact={compact} />
{zoom && (
<div className="overlay" onClick={() => setZoom(null)}>
<img src={zoom} alt="aperçu" style={{ maxWidth: '92%', maxHeight: '90%', borderRadius: 10, border: '1px solid var(--line-strong)' }} />
<button className="icon-btn" style={{ position: 'absolute', top: 12, right: 12 }} onClick={() => setZoom(null)}>
<X size={16} />
</button>
</div>
)}
</section>
);
}