chore(services): update components based on recent changes
Changes: services/frontend/src/features/dashboard/components/ChannelProfileDetail.tsx | 258 +++++++++++----------------------------------------------------------------- services/frontend/src/features/dashboard/components/ChannelSummaryList.tsx | 171 ++++++++------------------------------------------- services/frontend/src/features/dashboard/components/DashboardStats.tsx | 8 ++- services/frontend/src/features/dashboard/components/UserProfileDetail.tsx | 297 +++++++++++++--------------------------------------------------------------------------- services/frontend/src/features/dashboard/components/UserSummaryList.tsx | 204 +++++++++++------------------------------------------------- services/frontend/src/features/dashboard/hooks/useDashboard.ts | 203 +++++++++++++++--------------------------------------------- services/frontend/src/features/messages/components/MessageCard.tsx | 10 +-- services/frontend/src/shared/api/client.ts | 87 +++++++++++++++++++------- services/frontend/src/shared/hooks/useItemDetail.ts | 58 +++++++++++++++++ services/frontend/src/shared/hooks/usePaginatedList.ts | 105 +++++++++++++++++++++++++++++++
This commit is contained in:
@@ -1,30 +1,64 @@
|
||||
// ─── Typed event map for WebSocket events ────────────────────────────────────
|
||||
//
|
||||
// Types correspond to the data field sent by the backend's broadcastEvent().
|
||||
// The backend unwraps the DiscordGatewayEvent envelope and forwards only
|
||||
// the inner `data` field to frontend WebSocket clients.
|
||||
|
||||
import type {
|
||||
AnalysisQueueStatus,
|
||||
AttachmentRecord,
|
||||
MessageRecord,
|
||||
VoiceRecordingUploadData,
|
||||
} from "@bete/shared";
|
||||
|
||||
export interface ActiveSpeakerData {
|
||||
userId: string;
|
||||
username: string;
|
||||
avatar: string;
|
||||
speaking: boolean;
|
||||
}
|
||||
|
||||
export interface WsEventMap {
|
||||
message_created: { data: unknown };
|
||||
message_updated: { data: unknown };
|
||||
message_deleted: { data: { id: string } };
|
||||
message_analyzed: { data: unknown };
|
||||
attachment_uploaded: { data: unknown };
|
||||
message_created: { data: MessageRecord };
|
||||
message_updated: { data: MessageRecord & { edited_content?: string | null } };
|
||||
message_deleted: {
|
||||
data: { id: string; channel_id?: string; deleted_at: number };
|
||||
};
|
||||
message_analyzed: { data: MessageRecord };
|
||||
attachment_created: { data: AttachmentRecord };
|
||||
attachment_uploaded: { data: AttachmentRecord };
|
||||
voice_recording_started: { data: Record<string, unknown> };
|
||||
voice_recording_stopped: {
|
||||
data: {
|
||||
guild_id: string;
|
||||
session_id: string;
|
||||
duration_ms: number;
|
||||
participants: number;
|
||||
segment_count: number;
|
||||
status: string;
|
||||
stopped_at: number;
|
||||
};
|
||||
};
|
||||
voice_recording_uploaded: { data: VoiceRecordingUploadData };
|
||||
voice_pcm_data: {
|
||||
data: { userId: string; pcm: string; metadata?: Record<string, unknown> };
|
||||
};
|
||||
voice_analyzed: { data: Record<string, unknown> };
|
||||
voice_active_user: { data: ActiveSpeakerData };
|
||||
user_state: { users: unknown[] };
|
||||
ui_state: { state: unknown };
|
||||
media_state: { state: unknown };
|
||||
voice_recording_uploaded: { data: unknown };
|
||||
voice_recording_started: { data: unknown };
|
||||
voice_recording_stopped: { data: unknown };
|
||||
voice_pcm_data: { data: unknown };
|
||||
voice_active_user: { data: unknown };
|
||||
attachment_created: { data: unknown };
|
||||
analysis_queue_status: { data: unknown };
|
||||
reaction_added: { data: unknown };
|
||||
reaction_removed: { data: unknown };
|
||||
thread_created: { data: unknown };
|
||||
thread_deleted: { data: unknown };
|
||||
thread_updated: { data: unknown };
|
||||
channel_topic_updated: { data: unknown };
|
||||
presence_updated: { data: unknown };
|
||||
guild_member_added: { data: unknown };
|
||||
guild_member_removed: { data: unknown };
|
||||
ui_state: { state: Record<string, unknown> };
|
||||
media_state: { state: Record<string, unknown> };
|
||||
analysis_queue_status: { data: AnalysisQueueStatus };
|
||||
reaction_added: { data: Record<string, unknown> };
|
||||
reaction_removed: { data: Record<string, unknown> };
|
||||
thread_created: { data: Record<string, unknown> };
|
||||
thread_deleted: { data: Record<string, unknown> };
|
||||
thread_updated: { data: Record<string, unknown> };
|
||||
channel_topic_updated: { data: Record<string, unknown> };
|
||||
presence_updated: { data: Record<string, unknown> };
|
||||
guild_member_added: { data: Record<string, unknown> };
|
||||
guild_member_removed: { data: Record<string, unknown> };
|
||||
heartbeat: { data?: { timestamp: number } };
|
||||
}
|
||||
|
||||
export type WsEventType = keyof WsEventMap;
|
||||
@@ -34,7 +68,7 @@ export function parseWsMessage(
|
||||
): { type: WsEventType; payload: Record<string, unknown> } | null {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed.type) return null;
|
||||
if (!parsed.type || typeof parsed.type !== "string") return null;
|
||||
const { type, ...rest } = parsed;
|
||||
return { type: type as WsEventType, payload: rest };
|
||||
} catch {
|
||||
|
||||
@@ -8,6 +8,19 @@ export type WsStatus = "connecting" | "connected" | "disconnected" | "error";
|
||||
|
||||
export type BinaryHandler = (data: ArrayBuffer) => void;
|
||||
|
||||
const RECONNECT_BASE_MS = 1000;
|
||||
const RECONNECT_MAX_MS = 30000;
|
||||
const MAX_RECONNECT_ATTEMPTS = 20;
|
||||
|
||||
function computeBackoff(attempt: number): number {
|
||||
const delay = Math.min(
|
||||
RECONNECT_BASE_MS * Math.pow(2, attempt),
|
||||
RECONNECT_MAX_MS,
|
||||
);
|
||||
// Full jitter: random between 50% and 100% of delay
|
||||
return delay * (0.5 + Math.random() * 0.5);
|
||||
}
|
||||
|
||||
export interface WsHandlers {
|
||||
onBinary?: BinaryHandler;
|
||||
onMessageCreated?: (data: unknown) => void;
|
||||
@@ -24,6 +37,16 @@ export interface WsHandlers {
|
||||
onVoiceRecordingUploaded?: (data: unknown) => void;
|
||||
onVoicePcmData?: (data: unknown) => void;
|
||||
onVoiceActiveUser?: (data: unknown) => void;
|
||||
onReactionAdded?: (data: unknown) => void;
|
||||
onReactionRemoved?: (data: unknown) => void;
|
||||
onThreadCreated?: (data: unknown) => void;
|
||||
onThreadDeleted?: (data: unknown) => void;
|
||||
onThreadUpdated?: (data: unknown) => void;
|
||||
onChannelTopicUpdated?: (data: unknown) => void;
|
||||
onPresenceUpdated?: (data: unknown) => void;
|
||||
onGuildMemberAdded?: (data: unknown) => void;
|
||||
onGuildMemberRemoved?: (data: unknown) => void;
|
||||
onVoiceAnalyzed?: (data: unknown) => void;
|
||||
}
|
||||
|
||||
let _wsInstance: WebSocket | null = null;
|
||||
@@ -61,8 +84,19 @@ function doConnect(): WebSocket {
|
||||
logger.info("Disconnected", { code: event.code, reason: event.reason });
|
||||
if (!_closed && _listeners.size > 0) {
|
||||
_reconnectAttempts++;
|
||||
logger.warn("Reconnecting", { attempt: _reconnectAttempts });
|
||||
_reconnectTimer = setTimeout(() => doReconnect(), 2500);
|
||||
if (_reconnectAttempts > MAX_RECONNECT_ATTEMPTS) {
|
||||
logger.error("Max reconnect attempts reached, giving up", {
|
||||
attempts: _reconnectAttempts,
|
||||
});
|
||||
dispatchStatus("disconnected");
|
||||
return;
|
||||
}
|
||||
const delay = computeBackoff(_reconnectAttempts);
|
||||
logger.warn("Reconnecting", {
|
||||
attempt: _reconnectAttempts,
|
||||
delayMs: Math.round(delay),
|
||||
});
|
||||
_reconnectTimer = setTimeout(() => doReconnect(), delay);
|
||||
}
|
||||
});
|
||||
ws.addEventListener("message", (event) => {
|
||||
@@ -76,19 +110,22 @@ function doConnect(): WebSocket {
|
||||
for (const h of _listeners) {
|
||||
switch (msg.type) {
|
||||
case "message_created":
|
||||
h.onMessageCreated?.(msg.data);
|
||||
if (msg.data !== undefined) h.onMessageCreated?.(msg.data);
|
||||
break;
|
||||
case "message_updated":
|
||||
h.onMessageUpdated?.(msg.data);
|
||||
if (msg.data !== undefined) h.onMessageUpdated?.(msg.data);
|
||||
break;
|
||||
case "message_deleted":
|
||||
h.onMessageDeleted?.(msg.data);
|
||||
if (msg.data !== undefined) h.onMessageDeleted?.(msg.data);
|
||||
break;
|
||||
case "message_analyzed":
|
||||
h.onMessageAnalyzed?.(msg.data);
|
||||
if (msg.data !== undefined) h.onMessageAnalyzed?.(msg.data);
|
||||
break;
|
||||
case "attachment_created":
|
||||
if (msg.data !== undefined) h.onAttachmentCreated?.(msg.data);
|
||||
break;
|
||||
case "attachment_uploaded":
|
||||
h.onAttachmentUploaded?.(msg.data);
|
||||
if (msg.data !== undefined) h.onAttachmentUploaded?.(msg.data);
|
||||
break;
|
||||
case "user_state":
|
||||
h.onUserState?.((msg.users as unknown[]) || []);
|
||||
@@ -99,26 +136,53 @@ function doConnect(): WebSocket {
|
||||
case "media_state":
|
||||
h.onMediaState?.(msg.state);
|
||||
break;
|
||||
case "voice_recording_uploaded":
|
||||
h.onVoiceRecordingUploaded?.(msg.data);
|
||||
break;
|
||||
case "voice_recording_started":
|
||||
h.onVoiceRecordingStarted?.(msg.data);
|
||||
if (msg.data !== undefined) h.onVoiceRecordingStarted?.(msg.data);
|
||||
break;
|
||||
case "voice_recording_stopped":
|
||||
h.onVoiceRecordingStopped?.(msg.data);
|
||||
if (msg.data !== undefined) h.onVoiceRecordingStopped?.(msg.data);
|
||||
break;
|
||||
case "voice_recording_uploaded":
|
||||
if (msg.data !== undefined) h.onVoiceRecordingUploaded?.(msg.data);
|
||||
break;
|
||||
case "voice_pcm_data":
|
||||
h.onVoicePcmData?.(msg.data);
|
||||
if (msg.data !== undefined) h.onVoicePcmData?.(msg.data);
|
||||
break;
|
||||
case "voice_active_user":
|
||||
h.onVoiceActiveUser?.(msg.data);
|
||||
if (msg.data !== undefined) h.onVoiceActiveUser?.(msg.data);
|
||||
break;
|
||||
case "attachment_created":
|
||||
h.onAttachmentCreated?.(msg.data);
|
||||
case "voice_analyzed":
|
||||
if (msg.data !== undefined) h.onVoiceAnalyzed?.(msg.data);
|
||||
break;
|
||||
case "reaction_added":
|
||||
if (msg.data !== undefined) h.onReactionAdded?.(msg.data);
|
||||
break;
|
||||
case "reaction_removed":
|
||||
if (msg.data !== undefined) h.onReactionRemoved?.(msg.data);
|
||||
break;
|
||||
case "thread_created":
|
||||
if (msg.data !== undefined) h.onThreadCreated?.(msg.data);
|
||||
break;
|
||||
case "thread_deleted":
|
||||
if (msg.data !== undefined) h.onThreadDeleted?.(msg.data);
|
||||
break;
|
||||
case "thread_updated":
|
||||
if (msg.data !== undefined) h.onThreadUpdated?.(msg.data);
|
||||
break;
|
||||
case "channel_topic_updated":
|
||||
if (msg.data !== undefined) h.onChannelTopicUpdated?.(msg.data);
|
||||
break;
|
||||
case "presence_updated":
|
||||
if (msg.data !== undefined) h.onPresenceUpdated?.(msg.data);
|
||||
break;
|
||||
case "guild_member_added":
|
||||
if (msg.data !== undefined) h.onGuildMemberAdded?.(msg.data);
|
||||
break;
|
||||
case "guild_member_removed":
|
||||
if (msg.data !== undefined) h.onGuildMemberRemoved?.(msg.data);
|
||||
break;
|
||||
case "analysis_queue_status":
|
||||
// analysis_queue_status is monitoring-only — no UI action needed
|
||||
// monitoring-only — no UI action needed
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -178,6 +242,18 @@ export function useDashboardSocket(handlers: WsHandlers) {
|
||||
handlersRef.current.onVoiceRecordingUploaded?.(d),
|
||||
onVoicePcmData: (d) => handlersRef.current.onVoicePcmData?.(d),
|
||||
onVoiceActiveUser: (d) => handlersRef.current.onVoiceActiveUser?.(d),
|
||||
onReactionAdded: (d) => handlersRef.current.onReactionAdded?.(d),
|
||||
onReactionRemoved: (d) => handlersRef.current.onReactionRemoved?.(d),
|
||||
onThreadCreated: (d) => handlersRef.current.onThreadCreated?.(d),
|
||||
onThreadDeleted: (d) => handlersRef.current.onThreadDeleted?.(d),
|
||||
onThreadUpdated: (d) => handlersRef.current.onThreadUpdated?.(d),
|
||||
onChannelTopicUpdated: (d) =>
|
||||
handlersRef.current.onChannelTopicUpdated?.(d),
|
||||
onPresenceUpdated: (d) => handlersRef.current.onPresenceUpdated?.(d),
|
||||
onGuildMemberAdded: (d) => handlersRef.current.onGuildMemberAdded?.(d),
|
||||
onGuildMemberRemoved: (d) =>
|
||||
handlersRef.current.onGuildMemberRemoved?.(d),
|
||||
onVoiceAnalyzed: (d) => handlersRef.current.onVoiceAnalyzed?.(d),
|
||||
};
|
||||
|
||||
_listeners.add(wrapper);
|
||||
@@ -207,5 +283,3 @@ export function useDashboardSocket(handlers: WsHandlers) {
|
||||
|
||||
return { status, send, socketRef: { current: _wsInstance } };
|
||||
}
|
||||
|
||||
// Alias for backward compatibility
|
||||
|
||||
Reference in New Issue
Block a user