refactor(fe): DRY components, hooks, a11y fixes, and WS/client improvements
- Extract StatusBadge, SummaryList, ProfileDetail shared components (~300 lines deduplicated) - Extract usePaginatedList<T>, useItemDetail<T> generic hooks (~170 lines deduplicated) - Fix a11y: Badge (role=status, dark variants), Button (aria-disabled, motion-safe), Input (aria-invalid, errorId), Select (error variant), Skeleton (aria-hidden), MobileTabBar (full tab ARIA), Toast (timer leak fix, role=alert, keyboard dismiss), Card (role=region) - Fix API client: buildSearchParams helper, request timeout, password caching, named types - Fix WS: typed 24 event payloads (was all unknown), exponential backoff reconnect, max 20 attempts, msg.data guard - Fix bug: listDashboardChannels cursor pagination was silently dropped - Remove duplicate shimmer keyframes from tailwind.config.js - Fix WaveformPlayer non-null assertion Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
// ─── Audio playback hook — receives PCM from WebSocket and plays through Web Audio API ──
|
||||
import { useCallback, useEffect, useRef, useState, type RefObject } from "react";
|
||||
import {
|
||||
type RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { createLogger } from "../lib/logger.js";
|
||||
|
||||
const logger = createLogger("use-audio-playback");
|
||||
@@ -47,7 +53,8 @@ export function useAudioPlayback(): {
|
||||
|
||||
// Prune stale timeline entries (> 30s old) based on current audioContext time
|
||||
const pruneTimelines = useCallback(() => {
|
||||
const now = audioContextRef.current?.currentTime ?? performance.now() / 1000;
|
||||
const now =
|
||||
audioContextRef.current?.currentTime ?? performance.now() / 1000;
|
||||
for (const [userId, endTime] of userTimelinesRef.current) {
|
||||
if (endTime + 30 < now) userTimelinesRef.current.delete(userId);
|
||||
}
|
||||
@@ -67,11 +74,7 @@ export function useAudioPlayback(): {
|
||||
const pcmBytes = buffer.byteLength - 4;
|
||||
if (pcmBytes === 0) return;
|
||||
|
||||
const int16Array = new Int16Array(
|
||||
buffer,
|
||||
4,
|
||||
pcmBytes / 2,
|
||||
);
|
||||
const int16Array = new Int16Array(buffer, 4, pcmBytes / 2);
|
||||
if (int16Array.length === 0) return;
|
||||
|
||||
// RMS + level computation (same as before)
|
||||
@@ -109,10 +112,7 @@ export function useAudioPlayback(): {
|
||||
let nextStart = userTimelinesRef.current.get(userId) || 0;
|
||||
if (nextStart < currentTime) nextStart = currentTime + 0.05;
|
||||
source.start(nextStart);
|
||||
userTimelinesRef.current.set(
|
||||
userId,
|
||||
nextStart + audioBuffer.duration,
|
||||
);
|
||||
userTimelinesRef.current.set(userId, nextStart + audioBuffer.duration);
|
||||
pruneTimelines();
|
||||
},
|
||||
[isListening, pruneTimelines],
|
||||
@@ -233,4 +233,3 @@ function fnv1a32(str: string): number {
|
||||
}
|
||||
return hash >>> 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,11 +41,9 @@ function sendWsCommand(
|
||||
return false;
|
||||
}
|
||||
|
||||
export function useAudioTransmit(
|
||||
socketRef: {
|
||||
readonly current: WebSocket | null;
|
||||
},
|
||||
): {
|
||||
export function useAudioTransmit(socketRef: {
|
||||
readonly current: WebSocket | null;
|
||||
}): {
|
||||
isStreaming: boolean;
|
||||
micError: string | null;
|
||||
micLevel: number;
|
||||
@@ -195,5 +193,14 @@ export function useAudioTransmit(
|
||||
}
|
||||
}, [isStreaming, startTransmit, stopTransmit, start]);
|
||||
|
||||
return { isStreaming, micError, micLevel, toggle, stopTransmit, startTransmit, stop, start };
|
||||
return {
|
||||
isStreaming,
|
||||
micError,
|
||||
micLevel,
|
||||
toggle,
|
||||
stopTransmit,
|
||||
startTransmit,
|
||||
stop,
|
||||
start,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user