refactor(fe): overhaul — split types, type-safe WS, remove dead deps/code
- Remove unused deps: gsap, @tanstack/react-query, three, r3f, drei, autoprefixer
- Split types from shared/api/client.ts into entities/{guild,voice,media,ui,recording,dashboard}
- Type-safe WebSocket handlers using WsEventMap — 0 'as' casts in App.tsx
- Replace gsap with framer-motion in AuthOverlay
- Remove dead code: useMascotSummary, gsapCardHover, live/components/index barrel
- Fix bare console calls → use createLogger from @bete/shared/logger
- Clean up unused imports and variables (aiVariant, etc.)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,34 @@
|
||||
// ─── Shared HTTP client — all API endpoints in one file ──────────────────────
|
||||
|
||||
import type { MessageRecord, PageResult } from "@bete/shared";
|
||||
import type {
|
||||
ChatResponse,
|
||||
DashboardChannel,
|
||||
DashboardChannelDetail,
|
||||
DashboardStats,
|
||||
DashboardUser,
|
||||
DashboardUserDetail,
|
||||
} from "../../entities/dashboard/types.js";
|
||||
import type {
|
||||
Channel,
|
||||
Guild,
|
||||
GuildVoiceEntry,
|
||||
} from "../../entities/guild/types.js";
|
||||
import type {
|
||||
MediaItem,
|
||||
MediaMode,
|
||||
MediaState,
|
||||
} from "../../entities/media/types.js";
|
||||
import type {
|
||||
VoiceRecording,
|
||||
VoiceRecordingListResponse,
|
||||
} from "../../entities/recording/types.js";
|
||||
import type {
|
||||
AppConfig,
|
||||
DashboardTab,
|
||||
UIState,
|
||||
} from "../../entities/ui/types.js";
|
||||
import type { ActiveSpeaker, VoiceStatus } from "../../entities/voice/types.js";
|
||||
import { createLogger } from "../lib/logger.js";
|
||||
|
||||
const logger = createLogger("api");
|
||||
@@ -85,86 +113,31 @@ export function getAPIURL(): string {
|
||||
return BE_API_URL;
|
||||
}
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
// ─── Re-exports ──────────────────────────────────────────────────────────────
|
||||
|
||||
export type { MessageRecord, PageResult };
|
||||
|
||||
export interface Guild {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string | null;
|
||||
}
|
||||
|
||||
export interface Channel {
|
||||
id: string;
|
||||
name: string;
|
||||
type?: string;
|
||||
parentId?: string | null;
|
||||
}
|
||||
|
||||
export interface GuildVoiceEntry {
|
||||
guildId: string;
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
connectedAt: number;
|
||||
}
|
||||
|
||||
export interface VoiceStatus {
|
||||
connected: boolean;
|
||||
activeGuildId: string | null;
|
||||
activeChannelId: string | null;
|
||||
activeChannelName: string | null;
|
||||
connections: GuildVoiceEntry[];
|
||||
}
|
||||
|
||||
export interface ActiveSpeaker {
|
||||
id?: string;
|
||||
userId?: string;
|
||||
username: string;
|
||||
avatar: string;
|
||||
speaking: boolean;
|
||||
}
|
||||
|
||||
export type MediaMode = "music" | "screen";
|
||||
|
||||
export interface MediaItem {
|
||||
id?: string;
|
||||
source: string;
|
||||
title: string;
|
||||
mode?: "music" | "screen";
|
||||
durationMs?: number | null;
|
||||
thumbnailUrl?: string | null;
|
||||
}
|
||||
|
||||
export interface MediaState {
|
||||
playing: boolean;
|
||||
musicVolume: number;
|
||||
current: MediaItem | null;
|
||||
queue: MediaItem[];
|
||||
}
|
||||
|
||||
export interface UIState {
|
||||
selectedGuild?: string;
|
||||
selectedVoiceGuild?: string;
|
||||
selectedVoiceChannel?: string;
|
||||
selectedTextGuild?: string;
|
||||
selectedTextChannel?: string;
|
||||
selectedAnalyticsGuild?: string;
|
||||
selectedAnalyticsChannel?: string;
|
||||
activeTab?: "live" | "messages" | "dashboard";
|
||||
isListening?: boolean;
|
||||
isStreaming?: boolean;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
monitorGuildId: string | null;
|
||||
}
|
||||
|
||||
export interface ChatResponse {
|
||||
response?: string;
|
||||
}
|
||||
|
||||
export type DashboardTab = "live" | "messages" | "dashboard";
|
||||
export type {
|
||||
ActiveSpeaker,
|
||||
AppConfig,
|
||||
Channel,
|
||||
ChatResponse,
|
||||
DashboardChannel,
|
||||
DashboardChannelDetail,
|
||||
DashboardStats,
|
||||
DashboardTab,
|
||||
DashboardUser,
|
||||
DashboardUserDetail,
|
||||
Guild,
|
||||
GuildVoiceEntry,
|
||||
MediaItem,
|
||||
MediaMode,
|
||||
MediaState,
|
||||
MessageRecord,
|
||||
PageResult,
|
||||
UIState,
|
||||
VoiceRecording,
|
||||
VoiceRecordingListResponse,
|
||||
VoiceStatus,
|
||||
};
|
||||
|
||||
// ─── Messages ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -275,30 +248,6 @@ export function setMediaVolume(volume: number): Promise<MediaState> {
|
||||
|
||||
// ─── Recordings ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface VoiceRecording {
|
||||
id: string;
|
||||
user_id: string;
|
||||
username: string;
|
||||
avatar_url: string | null;
|
||||
guild_id: string | null;
|
||||
channel_id: string | null;
|
||||
channel_name: string | null;
|
||||
filename: string;
|
||||
size_bytes: number;
|
||||
download_url: string | null;
|
||||
upload_status: "pending" | "uploaded" | "failed";
|
||||
upload_error: string | null;
|
||||
transcription?: string | null;
|
||||
created_at: number;
|
||||
uploaded_at: number | null;
|
||||
}
|
||||
|
||||
export interface VoiceRecordingListResponse {
|
||||
items: VoiceRecording[];
|
||||
nextCursor: string | null;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
export function listRecordings(params?: {
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
@@ -325,55 +274,6 @@ export function login(password: string): Promise<{ ok: boolean }> {
|
||||
|
||||
// ─── Dashboard ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface DashboardStats {
|
||||
total_messages: number;
|
||||
total_users: number;
|
||||
total_flagged: number;
|
||||
total_clean: number;
|
||||
total_warned: number;
|
||||
total_error: number;
|
||||
total_voice_recordings: number;
|
||||
total_profiles: number;
|
||||
today_messages: number;
|
||||
today_flagged: number;
|
||||
active_users_24h: number;
|
||||
top_channels: Array<{
|
||||
channel_id: string;
|
||||
channel_name: string | null;
|
||||
message_count: number;
|
||||
}>;
|
||||
moderation_overview: {
|
||||
pending: number;
|
||||
processing: number;
|
||||
error: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DashboardUser {
|
||||
user_id: string;
|
||||
username: string | null;
|
||||
avatar_url: string | null;
|
||||
profile_summary: string | null;
|
||||
total_messages: number;
|
||||
flagged_count: number;
|
||||
last_message_at: number | null;
|
||||
trust_score: number | null;
|
||||
}
|
||||
|
||||
export interface DashboardUserDetail extends DashboardUser {
|
||||
last_analyzed_at: number | null;
|
||||
clean_message_streak: number | null;
|
||||
total_infractions: number | null;
|
||||
clean_count: number;
|
||||
recent_messages: Array<{
|
||||
id: string;
|
||||
content: string;
|
||||
channel_id: string;
|
||||
created_at: number;
|
||||
ai_status: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function getDashboardStats(): Promise<DashboardStats> {
|
||||
return request<DashboardStats>("/api/dashboard/stats");
|
||||
}
|
||||
@@ -399,29 +299,6 @@ export function getDashboardUserDetail(
|
||||
|
||||
// ─── Dashboard Channels ─────────────────────────────────────────────────────────
|
||||
|
||||
export interface DashboardChannel {
|
||||
channel_id: string;
|
||||
channel_name: string | null;
|
||||
guild_id: string | null;
|
||||
total_messages: number;
|
||||
flagged_count: number;
|
||||
last_message_at: number | null;
|
||||
culture_summary: string | null;
|
||||
last_analyzed_at: number | null;
|
||||
}
|
||||
|
||||
export interface DashboardChannelDetail extends DashboardChannel {
|
||||
clean_count: number;
|
||||
recent_messages: Array<{
|
||||
id: string;
|
||||
content: string;
|
||||
channel_id: string;
|
||||
created_at: number;
|
||||
ai_status: string | null;
|
||||
username: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function listDashboardChannels(
|
||||
params: {
|
||||
limit?: number;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// ─── Audio transmit hook — captures mic, encodes to PCM, sends via WebSocket ──
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { getAPIURL } from "../api/client.js";
|
||||
// note: this hook only uses API functions, not entity types
|
||||
import { createLogger } from "../lib/logger";
|
||||
|
||||
const SAMPLE_RATE = 24000;
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import gsap from "gsap";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
function prefersReducedMotion(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
}
|
||||
|
||||
export function useGsapTransition(tabKey: string) {
|
||||
const pageRef = useRef<HTMLDivElement>(null);
|
||||
const ctxRef = useRef<gsap.Context | null>(null);
|
||||
|
||||
const animateIn = useCallback(() => {
|
||||
// Kill any previously recorded animations to prevent conflicts
|
||||
ctxRef.current?.kill();
|
||||
|
||||
const instant = prefersReducedMotion();
|
||||
const scope = pageRef.current ?? undefined;
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
const tl = gsap.timeline();
|
||||
|
||||
// Page container: fade-in + slide-up (400ms ease-out)
|
||||
if (pageRef.current) {
|
||||
tl.fromTo(
|
||||
pageRef.current,
|
||||
{ opacity: 0, y: 20 },
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: instant ? 0 : 0.4,
|
||||
ease: "power2.out",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Stagger children with data-stagger attribute
|
||||
const staggerEls =
|
||||
pageRef.current?.querySelectorAll<HTMLElement>("[data-stagger]");
|
||||
if (staggerEls && staggerEls.length > 0) {
|
||||
tl.fromTo(
|
||||
staggerEls,
|
||||
{ opacity: 0, y: 15 },
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: instant ? 0 : 0.3,
|
||||
stagger: instant ? 0 : 0.05,
|
||||
ease: "power2.out",
|
||||
},
|
||||
"-=0.1",
|
||||
);
|
||||
}
|
||||
}, scope);
|
||||
|
||||
ctxRef.current = ctx;
|
||||
}, [tabKey]);
|
||||
|
||||
const animateOut = useCallback((): Promise<void> => {
|
||||
// Kill any previously recorded animations to prevent conflicts
|
||||
ctxRef.current?.kill();
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
const instant = prefersReducedMotion();
|
||||
const scope = pageRef.current ?? undefined;
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
const tl = gsap.timeline({
|
||||
onComplete: () => {
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
|
||||
if (pageRef.current) {
|
||||
// Page container: fade-out + slide-down (300ms ease-in)
|
||||
tl.to(pageRef.current, {
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
duration: instant ? 0 : 0.3,
|
||||
ease: "power2.in",
|
||||
});
|
||||
} else {
|
||||
// No element to animate — resolve immediately
|
||||
resolve();
|
||||
}
|
||||
}, scope);
|
||||
|
||||
ctxRef.current = ctx;
|
||||
});
|
||||
}, [tabKey]);
|
||||
|
||||
// Cleanup all recorded animations on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
ctxRef.current?.kill();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { pageRef, animateIn, animateOut };
|
||||
}
|
||||
|
||||
function gsapCardHover() {
|
||||
return {
|
||||
onMouseEnter: (e: React.MouseEvent<HTMLElement>) => {
|
||||
gsap.to(e.currentTarget, {
|
||||
y: -4,
|
||||
boxShadow: "0 8px 25px rgba(0,0,0,0.15)",
|
||||
duration: 0.2,
|
||||
ease: "power2.out",
|
||||
overwrite: "auto",
|
||||
});
|
||||
},
|
||||
onMouseLeave: (e: React.MouseEvent<HTMLElement>) => {
|
||||
gsap.to(e.currentTarget, {
|
||||
y: 0,
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
|
||||
duration: 0.2,
|
||||
ease: "power2.out",
|
||||
overwrite: "auto",
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import type { ChatResponse } from "../api/client";
|
||||
import type { ChatResponse } from "../../entities/dashboard/types.js";
|
||||
import { request } from "../api/client";
|
||||
import { createLogger } from "../lib/logger";
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { MessageRecord } from "../api/client";
|
||||
|
||||
/**
|
||||
* useMascotSummary — Generates AI-powered summary/insights from recent messages
|
||||
* Used by mascot's floating chat bubble to display conversation insights
|
||||
*/
|
||||
|
||||
interface UseMascotSummaryOptions {
|
||||
messages: MessageRecord[];
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
const summaryPrompts = [
|
||||
"📊 Diskusi sangat aktif dengan {count} pesan",
|
||||
"💬 Topik populer: {topic} ({percentage}%)",
|
||||
"👥 Partisipan utama: {users}",
|
||||
"⏰ Aktivitas puncak: {time}",
|
||||
"🔥 Buzz level: {level}",
|
||||
"💡 Insight: {insight}",
|
||||
];
|
||||
|
||||
function generateInsight(messages: MessageRecord[]): string {
|
||||
if (messages.length === 0) {
|
||||
return "Menunggu pesan...";
|
||||
}
|
||||
|
||||
const totalMessages = messages.length;
|
||||
const recentMessages = messages.slice(-10);
|
||||
|
||||
// Hitung user yang berbeda
|
||||
const uniqueUsers = new Set(recentMessages.map((m) => m.user_id)).size;
|
||||
|
||||
// Hitung average panjang pesan
|
||||
const avgLength = Math.round(
|
||||
recentMessages.reduce((sum, m) => sum + (m.content?.length || 0), 0) /
|
||||
recentMessages.length,
|
||||
);
|
||||
|
||||
// Tentukan tipe percakapan
|
||||
let insight = "";
|
||||
if (avgLength > 150) {
|
||||
insight = "Diskusi mendalam sedang berlangsung";
|
||||
} else if (avgLength > 80) {
|
||||
insight = "Percakapan normal dan interaktif";
|
||||
} else {
|
||||
insight = "Chat cepat dan ringkas";
|
||||
}
|
||||
|
||||
// Tambah info partisipan
|
||||
if (uniqueUsers > 5) {
|
||||
insight += ` • ${uniqueUsers} orang aktif`;
|
||||
}
|
||||
|
||||
// Tambah info volume
|
||||
if (totalMessages > 50) {
|
||||
insight += " • Volume tinggi 🔥";
|
||||
} else if (totalMessages > 20) {
|
||||
insight += " • Percakapan aktif";
|
||||
}
|
||||
|
||||
return insight;
|
||||
}
|
||||
|
||||
function extractTopics(messages: MessageRecord[]): string {
|
||||
if (messages.length === 0) return "Tidak ada topik";
|
||||
|
||||
// Extract keywords dari recent messages
|
||||
const recentMessages = messages.slice(-15);
|
||||
const content = recentMessages
|
||||
.map((m) => m.content?.toLowerCase() || "")
|
||||
.join(" ");
|
||||
|
||||
// Simple keyword extraction
|
||||
const keywords = [
|
||||
{ word: "voice", label: "Voice" },
|
||||
{ word: "recording", label: "Recording" },
|
||||
{ word: "audio", label: "Audio" },
|
||||
{ word: "chat", label: "Chat" },
|
||||
{ word: "message", label: "Message" },
|
||||
{ word: "user", label: "User" },
|
||||
];
|
||||
|
||||
for (const { word, label } of keywords) {
|
||||
if (content.includes(word)) {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
|
||||
return "Umum";
|
||||
}
|
||||
|
||||
export function useMascotSummary({
|
||||
messages,
|
||||
enabled = true,
|
||||
}: UseMascotSummaryOptions): string {
|
||||
const [summary, setSummary] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || messages.length === 0) {
|
||||
setSummary("");
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate summary berdasarkan messages
|
||||
const insight = generateInsight(messages);
|
||||
setSummary(insight);
|
||||
|
||||
// Rotate summary setiap 5 detik
|
||||
const interval = setInterval(() => {
|
||||
setSummary((prev) => {
|
||||
if (prev.includes("aktif")) {
|
||||
return `📈 Total: ${messages.length} pesan`;
|
||||
} else if (prev.includes("Total")) {
|
||||
return generateInsight(messages);
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [messages, enabled]);
|
||||
|
||||
return summary;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from "react";
|
||||
import type { UIState } from "../api/client";
|
||||
import type { UIState } from "../../entities/ui/types.js";
|
||||
import { uiStateValidator, useLocalStorage } from "./useLocalStorage";
|
||||
|
||||
export function useUIState() {
|
||||
|
||||
@@ -16,3 +16,25 @@ export function formatBytes(bytes: number): string {
|
||||
export function formatDate(value: number): string {
|
||||
return new Date(value).toLocaleString();
|
||||
}
|
||||
|
||||
export interface MessageMetadata {
|
||||
stickers?: Array<{ name?: string; url?: string }>;
|
||||
attachments?: Array<{ name: string; url: string; contentType?: string }>;
|
||||
embeds?: Array<{ title?: string; image?: string; thumbnail?: string }>;
|
||||
channel?: {
|
||||
channelId: string;
|
||||
channelName?: string;
|
||||
threadId?: string;
|
||||
threadName?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function parseMetadata(value: string | null): MessageMetadata {
|
||||
if (!value) return {};
|
||||
try {
|
||||
const parsed = JSON.parse(value) as MessageMetadata;
|
||||
return parsed;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LayoutDashboard, MessageSquare, Radio } from "lucide-react";
|
||||
import type { DashboardTab } from "../api/client";
|
||||
import type { DashboardTab } from "../../entities/ui/types.js";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
const tabs: Array<{ id: DashboardTab; label: string; Icon: typeof Radio }> = [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// ─── Toast notification system ──────────────────────────────────────────────
|
||||
// (no entity type imports needed — only uses string/ReactNode)
|
||||
import {
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
// ─── WebSocket singleton with reconnect, typed events, and observable status ─
|
||||
|
||||
import type {
|
||||
AttachmentRecord,
|
||||
MessageRecord,
|
||||
VoiceRecordingUploadData,
|
||||
} from "@bete/shared";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { MediaState } from "../../entities/media/types.js";
|
||||
import { createLogger } from "../lib/logger.js";
|
||||
import type { ActiveSpeakerData } from "./events.js";
|
||||
|
||||
const logger = createLogger("socket");
|
||||
|
||||
@@ -23,30 +31,48 @@ function computeBackoff(attempt: number): number {
|
||||
|
||||
export interface WsHandlers {
|
||||
onBinary?: BinaryHandler;
|
||||
onMessageCreated?: (data: unknown) => void;
|
||||
onMessageUpdated?: (data: unknown) => void;
|
||||
onMessageDeleted?: (data: unknown) => void;
|
||||
onMessageAnalyzed?: (data: unknown) => void;
|
||||
onAttachmentCreated?: (data: unknown) => void;
|
||||
onAttachmentUploaded?: (data: unknown) => void;
|
||||
onUserState?: (users: unknown[]) => void;
|
||||
onUiState?: (state: unknown) => void;
|
||||
onMediaState?: (state: unknown) => void;
|
||||
onVoiceRecordingStarted?: (data: unknown) => void;
|
||||
onVoiceRecordingStopped?: (data: unknown) => void;
|
||||
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;
|
||||
onMessageCreated?: (data: MessageRecord) => void;
|
||||
onMessageUpdated?: (
|
||||
data: MessageRecord & { edited_content?: string | null },
|
||||
) => void;
|
||||
onMessageDeleted?: (data: {
|
||||
id: string;
|
||||
channel_id?: string;
|
||||
deleted_at: number;
|
||||
}) => void;
|
||||
onMessageAnalyzed?: (data: MessageRecord) => void;
|
||||
onAttachmentCreated?: (data: AttachmentRecord) => void;
|
||||
onAttachmentUploaded?: (data: AttachmentRecord) => void;
|
||||
onUserState?: (users: ActiveSpeakerData[]) => void;
|
||||
onUiState?: (state: Record<string, unknown>) => void;
|
||||
onMediaState?: (state: MediaState) => void;
|
||||
onVoiceRecordingStarted?: (data: Record<string, unknown>) => void;
|
||||
onVoiceRecordingStopped?: (data: {
|
||||
guild_id: string;
|
||||
session_id: string;
|
||||
duration_ms: number;
|
||||
participants: number;
|
||||
segment_count: number;
|
||||
status: string;
|
||||
stopped_at: number;
|
||||
}) => void;
|
||||
onVoiceRecordingUploaded?: (data: VoiceRecordingUploadData) => void;
|
||||
onVoicePcmData?: (data: {
|
||||
userId: string;
|
||||
pcm: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}) => void;
|
||||
onVoiceActiveUser?: (data: ActiveSpeakerData) => void;
|
||||
onReactionAdded?: (data: Record<string, unknown>) => void;
|
||||
onReactionRemoved?: (data: Record<string, unknown>) => void;
|
||||
onThreadCreated?: (data: Record<string, unknown>) => void;
|
||||
onThreadDeleted?: (data: Record<string, unknown>) => void;
|
||||
onThreadUpdated?: (data: Record<string, unknown>) => void;
|
||||
onChannelTopicUpdated?: (data: Record<string, unknown>) => void;
|
||||
onPresenceUpdated?: (data: Record<string, unknown>) => void;
|
||||
onGuildMemberAdded?: (data: Record<string, unknown>) => void;
|
||||
onGuildMemberRemoved?: (data: Record<string, unknown>) => void;
|
||||
onVoiceAnalyzed?: (data: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
let _wsInstance: WebSocket | null = null;
|
||||
@@ -110,76 +136,125 @@ function doConnect(): WebSocket {
|
||||
for (const h of _listeners) {
|
||||
switch (msg.type) {
|
||||
case "message_created":
|
||||
if (msg.data !== undefined) h.onMessageCreated?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onMessageCreated?.(msg.data as MessageRecord);
|
||||
break;
|
||||
case "message_updated":
|
||||
if (msg.data !== undefined) h.onMessageUpdated?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onMessageUpdated?.(
|
||||
msg.data as MessageRecord & { edited_content?: string | null },
|
||||
);
|
||||
break;
|
||||
case "message_deleted":
|
||||
if (msg.data !== undefined) h.onMessageDeleted?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onMessageDeleted?.(
|
||||
msg.data as {
|
||||
id: string;
|
||||
channel_id?: string;
|
||||
deleted_at: number;
|
||||
},
|
||||
);
|
||||
break;
|
||||
case "message_analyzed":
|
||||
if (msg.data !== undefined) h.onMessageAnalyzed?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onMessageAnalyzed?.(msg.data as MessageRecord);
|
||||
break;
|
||||
case "attachment_created":
|
||||
if (msg.data !== undefined) h.onAttachmentCreated?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onAttachmentCreated?.(msg.data as AttachmentRecord);
|
||||
break;
|
||||
case "attachment_uploaded":
|
||||
if (msg.data !== undefined) h.onAttachmentUploaded?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onAttachmentUploaded?.(msg.data as AttachmentRecord);
|
||||
break;
|
||||
case "user_state":
|
||||
h.onUserState?.((msg.users as unknown[]) || []);
|
||||
h.onUserState?.(
|
||||
(msg.users as unknown as ActiveSpeakerData[]) || [],
|
||||
);
|
||||
break;
|
||||
case "ui_state":
|
||||
h.onUiState?.(msg.state);
|
||||
h.onUiState?.(msg.state as Record<string, unknown>);
|
||||
break;
|
||||
case "media_state":
|
||||
h.onMediaState?.(msg.state);
|
||||
h.onMediaState?.(msg.state as MediaState);
|
||||
break;
|
||||
case "voice_recording_started":
|
||||
if (msg.data !== undefined) h.onVoiceRecordingStarted?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onVoiceRecordingStarted?.(msg.data as Record<string, unknown>);
|
||||
break;
|
||||
case "voice_recording_stopped":
|
||||
if (msg.data !== undefined) h.onVoiceRecordingStopped?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onVoiceRecordingStopped?.(
|
||||
msg.data as {
|
||||
guild_id: string;
|
||||
session_id: string;
|
||||
duration_ms: number;
|
||||
participants: number;
|
||||
segment_count: number;
|
||||
status: string;
|
||||
stopped_at: number;
|
||||
},
|
||||
);
|
||||
break;
|
||||
case "voice_recording_uploaded":
|
||||
if (msg.data !== undefined) h.onVoiceRecordingUploaded?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onVoiceRecordingUploaded?.(
|
||||
msg.data as VoiceRecordingUploadData,
|
||||
);
|
||||
break;
|
||||
case "voice_pcm_data":
|
||||
if (msg.data !== undefined) h.onVoicePcmData?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onVoicePcmData?.(
|
||||
msg.data as {
|
||||
userId: string;
|
||||
pcm: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
},
|
||||
);
|
||||
break;
|
||||
case "voice_active_user":
|
||||
if (msg.data !== undefined) h.onVoiceActiveUser?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onVoiceActiveUser?.(msg.data as ActiveSpeakerData);
|
||||
break;
|
||||
case "voice_analyzed":
|
||||
if (msg.data !== undefined) h.onVoiceAnalyzed?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onVoiceAnalyzed?.(msg.data as Record<string, unknown>);
|
||||
break;
|
||||
case "reaction_added":
|
||||
if (msg.data !== undefined) h.onReactionAdded?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onReactionAdded?.(msg.data as Record<string, unknown>);
|
||||
break;
|
||||
case "reaction_removed":
|
||||
if (msg.data !== undefined) h.onReactionRemoved?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onReactionRemoved?.(msg.data as Record<string, unknown>);
|
||||
break;
|
||||
case "thread_created":
|
||||
if (msg.data !== undefined) h.onThreadCreated?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onThreadCreated?.(msg.data as Record<string, unknown>);
|
||||
break;
|
||||
case "thread_deleted":
|
||||
if (msg.data !== undefined) h.onThreadDeleted?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onThreadDeleted?.(msg.data as Record<string, unknown>);
|
||||
break;
|
||||
case "thread_updated":
|
||||
if (msg.data !== undefined) h.onThreadUpdated?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onThreadUpdated?.(msg.data as Record<string, unknown>);
|
||||
break;
|
||||
case "channel_topic_updated":
|
||||
if (msg.data !== undefined) h.onChannelTopicUpdated?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onChannelTopicUpdated?.(msg.data as Record<string, unknown>);
|
||||
break;
|
||||
case "presence_updated":
|
||||
if (msg.data !== undefined) h.onPresenceUpdated?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onPresenceUpdated?.(msg.data as Record<string, unknown>);
|
||||
break;
|
||||
case "guild_member_added":
|
||||
if (msg.data !== undefined) h.onGuildMemberAdded?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onGuildMemberAdded?.(msg.data as Record<string, unknown>);
|
||||
break;
|
||||
case "guild_member_removed":
|
||||
if (msg.data !== undefined) h.onGuildMemberRemoved?.(msg.data);
|
||||
if (msg.data !== undefined)
|
||||
h.onGuildMemberRemoved?.(msg.data as Record<string, unknown>);
|
||||
break;
|
||||
case "analysis_queue_status":
|
||||
// monitoring-only — no UI action needed
|
||||
|
||||
Reference in New Issue
Block a user