mirror of
https://github.com/R0m1k3/EveFlow.git
synced 2026-10-12 01:39:13 +02:00
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
100 lines
3.5 KiB
TypeScript
100 lines
3.5 KiB
TypeScript
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>
|
||
);
|
||
}
|