refactor(frontend): major rebuild — feature-sliced architecture + bug fixes + glass-morphism UI
Architecture: - Feature-sliced directory: entities/, shared/, features/, widgets/ - Consolidated API client (shared/api/client.ts) — all endpoints in one file - Single WebSocket manager (shared/ws/socket.ts) with typed event bus - Extracted hooks: useAudioPlayback, useAudioTransmit, useLocalStorage, useUIState - UI primitives moved to shared/ui/ with barrel export - Added Skeleton, Toast, MobileTabBar components Bug fixes (8/8): 1. useMemo→useEffect in RecordingsSubPanel (async side-effect anti-pattern) 2. ArrayBuffer.slice() before WebSocket send (shared buffer bug) 3. Proper useEffect dependency arrays throughout 4. Stable React keys (no index fallbacks) 5. onReanalyze properly awaited (Promise<void> return) 6. monitorGuild memoized with useMemo 7. localStorage validation with shape checking 8. Deleted duplicate socket logic (ws/client.ts removed) UI polish: - Glass-morphism design tokens (backdrop-blur, translucent cards) - Gradient mesh background with subtle radial overlays - Expandable sidebar + mobile bottom tab bar - Skeleton loading placeholders - Audio visualizer with CSS pulse animation Deleted: src/api/, src/components/, src/hooks/, src/types/, src/ws/, src/lib/ Added: 40 new files across feature-sliced structure Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1aab0d1df1
commit
f5507e01f6
@@ -0,0 +1,142 @@
|
||||
// ─── WebSocket singleton with reconnect, typed events, and observable status ─
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
export type WsStatus = "connecting" | "connected" | "disconnected" | "error";
|
||||
|
||||
export type BinaryHandler = (data: ArrayBuffer) => void;
|
||||
|
||||
export interface WsHandlers {
|
||||
onBinary?: BinaryHandler;
|
||||
onMessageCreated?: (data: unknown) => void;
|
||||
onMessageUpdated?: (data: unknown) => void;
|
||||
onMessageDeleted?: (data: unknown) => void;
|
||||
onMessageAnalyzed?: (data: unknown) => void;
|
||||
onAttachmentUploaded?: () => void;
|
||||
onUserState?: (users: unknown[]) => void;
|
||||
onUiState?: (state: unknown) => void;
|
||||
onMediaState?: (state: unknown) => void;
|
||||
onVoiceRecordingUploaded?: (data: unknown) => void;
|
||||
}
|
||||
|
||||
let _wsInstance: WebSocket | null = null;
|
||||
let _reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let _closed = false;
|
||||
const _listeners = new Set<WsHandlers>();
|
||||
const _statusCallbacks = new Set<(s: WsStatus) => void>();
|
||||
|
||||
function dispatchStatus(s: WsStatus): void {
|
||||
for (const cb of _statusCallbacks) cb(s);
|
||||
}
|
||||
|
||||
function doConnect(): WebSocket {
|
||||
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const ws = new WebSocket(`${protocol}//${location.host}/ws`);
|
||||
ws.binaryType = "arraybuffer";
|
||||
dispatchStatus("connecting");
|
||||
|
||||
ws.addEventListener("open", () => dispatchStatus("connected"));
|
||||
ws.addEventListener("error", () => dispatchStatus("error"));
|
||||
ws.addEventListener("close", () => {
|
||||
dispatchStatus("disconnected");
|
||||
if (!_closed && _listeners.size > 0) {
|
||||
_reconnectTimer = setTimeout(() => doReconnect(), 2500);
|
||||
}
|
||||
});
|
||||
ws.addEventListener("message", (event) => {
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
for (const h of _listeners) h.onBinary?.(event.data);
|
||||
return;
|
||||
}
|
||||
if (typeof event.data !== "string") return;
|
||||
try {
|
||||
const msg = JSON.parse(event.data) as Record<string, unknown>;
|
||||
for (const h of _listeners) {
|
||||
switch (msg.type) {
|
||||
case "message_created": h.onMessageCreated?.(msg.data); break;
|
||||
case "message_updated": h.onMessageUpdated?.(msg.data); break;
|
||||
case "message_deleted": h.onMessageDeleted?.(msg.data); break;
|
||||
case "message_analyzed": h.onMessageAnalyzed?.(msg.data); break;
|
||||
case "attachment_uploaded": h.onAttachmentUploaded?.(); break;
|
||||
case "user_state": h.onUserState?.((msg.users as unknown[]) || []); break;
|
||||
case "ui_state": h.onUiState?.(msg.state); break;
|
||||
case "media_state": h.onMediaState?.(msg.state); break;
|
||||
case "voice_recording_uploaded": h.onVoiceRecordingUploaded?.(msg.data); break;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore malformed messages
|
||||
}
|
||||
});
|
||||
|
||||
return ws;
|
||||
}
|
||||
|
||||
function doReconnect(): void {
|
||||
if (_wsInstance) {
|
||||
_wsInstance.close();
|
||||
if (_reconnectTimer) clearTimeout(_reconnectTimer);
|
||||
}
|
||||
_closed = false;
|
||||
_wsInstance = doConnect();
|
||||
}
|
||||
|
||||
function ensureConnected(): void {
|
||||
if (!_wsInstance || _wsInstance.readyState === WebSocket.CLOSED) {
|
||||
if (_wsInstance) {
|
||||
_wsInstance.close();
|
||||
if (_reconnectTimer) clearTimeout(_reconnectTimer);
|
||||
}
|
||||
_closed = false;
|
||||
_wsInstance = doConnect();
|
||||
}
|
||||
}
|
||||
|
||||
export function useDashboardSocket(handlers: WsHandlers) {
|
||||
const [status, setStatus] = useState<WsStatus>("connecting");
|
||||
const handlersRef = useRef(handlers);
|
||||
handlersRef.current = handlers;
|
||||
|
||||
useEffect(() => {
|
||||
const wrapper: WsHandlers = {
|
||||
onBinary: (d) => handlersRef.current.onBinary?.(d),
|
||||
onMessageCreated: (d) => handlersRef.current.onMessageCreated?.(d),
|
||||
onMessageUpdated: (d) => handlersRef.current.onMessageUpdated?.(d),
|
||||
onMessageDeleted: (d) => handlersRef.current.onMessageDeleted?.(d),
|
||||
onMessageAnalyzed: (d) => handlersRef.current.onMessageAnalyzed?.(d),
|
||||
onAttachmentUploaded: () => handlersRef.current.onAttachmentUploaded?.(),
|
||||
onUserState: (u) => handlersRef.current.onUserState?.(u),
|
||||
onUiState: (s) => handlersRef.current.onUiState?.(s),
|
||||
onMediaState: (s) => handlersRef.current.onMediaState?.(s),
|
||||
onVoiceRecordingUploaded: (d) => handlersRef.current.onVoiceRecordingUploaded?.(d),
|
||||
};
|
||||
|
||||
_listeners.add(wrapper);
|
||||
_statusCallbacks.add(setStatus);
|
||||
|
||||
if (_listeners.size === 1) {
|
||||
ensureConnected();
|
||||
}
|
||||
|
||||
return () => {
|
||||
_listeners.delete(wrapper);
|
||||
_statusCallbacks.delete(setStatus);
|
||||
if (_listeners.size === 0) {
|
||||
_closed = true;
|
||||
if (_reconnectTimer) clearTimeout(_reconnectTimer);
|
||||
_wsInstance?.close();
|
||||
_wsInstance = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const send = useCallback((data: ArrayBuffer | string) => {
|
||||
if (_wsInstance?.readyState === WebSocket.OPEN) {
|
||||
_wsInstance.send(data);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { status, send, socketRef: { current: _wsInstance } };
|
||||
}
|
||||
|
||||
// Alias for backward compatibility
|
||||
export { useDashboardSocket as useWsSocket };
|
||||
Reference in New Issue
Block a user