2026-06-01 16:42:36 +07:00
|
|
|
// ─── Shared HTTP client — all API endpoints in one file ──────────────────────
|
|
|
|
|
|
2026-06-09 16:56:44 +07:00
|
|
|
import type { MessageRecord, PageResult } from "@bete/shared";
|
2026-06-09 13:07:01 +07:00
|
|
|
|
2026-06-01 21:44:29 +07:00
|
|
|
const BE_API_URL = import.meta.env.VITE_BE_API_URL || "http://localhost:3001";
|
|
|
|
|
const BE_WS_URL = import.meta.env.VITE_BE_WS_URL || "ws://localhost:3001";
|
|
|
|
|
|
2026-06-01 16:42:36 +07:00
|
|
|
class ApiError extends Error {
|
|
|
|
|
code: string;
|
|
|
|
|
statusCode: number;
|
|
|
|
|
|
|
|
|
|
constructor(code: string, message: string, statusCode: number) {
|
|
|
|
|
super(message);
|
|
|
|
|
this.name = "ApiError";
|
|
|
|
|
this.code = code;
|
|
|
|
|
this.statusCode = statusCode;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
|
|
|
const password = localStorage.getItem("admin-password");
|
2026-06-01 21:44:29 +07:00
|
|
|
const url = path.startsWith("http") ? path : `${BE_API_URL}${path}`;
|
|
|
|
|
const res = await fetch(url, {
|
2026-06-01 16:42:36 +07:00
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
...(password ? { "X-Admin-Password": password } : {}),
|
|
|
|
|
},
|
|
|
|
|
...init,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
let message = res.statusText;
|
|
|
|
|
let code = "REQUEST_FAILED";
|
|
|
|
|
try {
|
|
|
|
|
const body = (await res.json()) as { error?: string; message?: string };
|
|
|
|
|
if (body.message) message = body.message;
|
|
|
|
|
if (body.error) code = body.error;
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore parse errors
|
|
|
|
|
}
|
|
|
|
|
throw new ApiError(code, message, res.status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return res.json() as Promise<T>;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 21:44:29 +07:00
|
|
|
export function getWebSocketURL(): string {
|
|
|
|
|
return BE_WS_URL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getAPIURL(): string {
|
|
|
|
|
return BE_API_URL;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 16:42:36 +07:00
|
|
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-06-09 16:56:44 +07:00
|
|
|
export type { MessageRecord, PageResult };
|
2026-06-01 16:42:36 +07:00
|
|
|
|
|
|
|
|
export interface Guild {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
icon: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface Channel {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
type?: string;
|
|
|
|
|
parentId?: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface VoiceStatus {
|
|
|
|
|
connected: boolean;
|
2026-06-09 16:56:44 +07:00
|
|
|
activeGuildId: string | null;
|
|
|
|
|
activeChannelId: string | null;
|
|
|
|
|
activeChannelName: string | null;
|
2026-06-01 16:42:36 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ActiveSpeaker {
|
|
|
|
|
id?: string;
|
|
|
|
|
userId?: string;
|
|
|
|
|
username: string;
|
|
|
|
|
avatar: string;
|
|
|
|
|
speaking: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 21:06:42 +07:00
|
|
|
export type MediaMode = "music" | "screen";
|
|
|
|
|
|
2026-06-01 16:42:36 +07:00
|
|
|
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;
|
2026-06-09 10:16:04 +07:00
|
|
|
activeTab?: "live" | "messages";
|
2026-06-01 16:42:36 +07:00
|
|
|
isListening?: boolean;
|
|
|
|
|
isStreaming?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AppConfig {
|
|
|
|
|
monitorGuildId: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 16:36:23 +07:00
|
|
|
export interface ChatResponse {
|
|
|
|
|
response?: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 10:16:04 +07:00
|
|
|
export type DashboardTab = "live" | "messages";
|
2026-06-01 16:42:36 +07:00
|
|
|
|
|
|
|
|
// ─── Messages ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-06-03 20:29:34 +07:00
|
|
|
export function listMessages(params: {
|
|
|
|
|
guildId: string;
|
|
|
|
|
channelId?: string;
|
|
|
|
|
limit?: number;
|
|
|
|
|
cursor?: string;
|
|
|
|
|
}): Promise<PageResult<MessageRecord>> {
|
|
|
|
|
const sp = new URLSearchParams({
|
|
|
|
|
guildId: params.guildId,
|
|
|
|
|
limit: String(params.limit ?? 100),
|
|
|
|
|
...(params.channelId && { channelId: params.channelId }),
|
|
|
|
|
...(params.cursor && { cursor: params.cursor }),
|
|
|
|
|
});
|
|
|
|
|
return request<PageResult<MessageRecord>>(`/api/messages?${sp}`);
|
2026-06-01 16:42:36 +07:00
|
|
|
}
|
|
|
|
|
|
2026-06-01 21:44:29 +07:00
|
|
|
export function listReview(
|
|
|
|
|
params: URLSearchParams,
|
|
|
|
|
): Promise<PageResult<MessageRecord>> {
|
2026-06-01 16:42:36 +07:00
|
|
|
return request<PageResult<MessageRecord>>(`/api/review?${params}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function reanalyzeMessage(id: string): Promise<void> {
|
|
|
|
|
return request<void>(`/api/messages/${id}/reanalyze`, { method: "POST" });
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 00:17:51 +07:00
|
|
|
export function reanalyzeErrorBatch(opts: {
|
|
|
|
|
guildId?: string;
|
|
|
|
|
channelId?: string;
|
|
|
|
|
messageIds?: string[];
|
|
|
|
|
}): Promise<{ ok: boolean; count: number }> {
|
|
|
|
|
return request<{ ok: boolean; count: number }>(
|
|
|
|
|
"/api/messages/reanalyze-batch",
|
|
|
|
|
{ method: "POST", body: JSON.stringify(opts) },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 16:42:36 +07:00
|
|
|
// ─── Guilds / Config ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export function getGuilds(): Promise<Guild[]> {
|
|
|
|
|
return request<Guild[]>("/api/guilds");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getAppConfig(): Promise<AppConfig> {
|
|
|
|
|
return request<AppConfig>("/api/config");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Voice ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export function getVoiceChannels(guildId: string): Promise<Channel[]> {
|
|
|
|
|
return request<Channel[]>(`/api/guilds/${guildId}/voice-channels`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getTextChannels(guildId: string): Promise<Channel[]> {
|
|
|
|
|
return request<Channel[]>(`/api/guilds/${guildId}/channels`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getVoiceStatus(): Promise<VoiceStatus> {
|
|
|
|
|
return request<VoiceStatus>("/api/status");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 21:44:29 +07:00
|
|
|
export function connectVoice(
|
|
|
|
|
guildId: string,
|
|
|
|
|
channelId: string,
|
|
|
|
|
): Promise<VoiceStatus> {
|
2026-06-01 16:42:36 +07:00
|
|
|
return request<VoiceStatus>("/api/connect", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({ guildId, channelId }),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function disconnectVoice(): Promise<VoiceStatus> {
|
|
|
|
|
return request<VoiceStatus>("/api/disconnect", { method: "POST" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Media ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export function getMediaStatus(): Promise<MediaState> {
|
|
|
|
|
return request<MediaState>("/api/media/status");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 21:44:29 +07:00
|
|
|
export function queueMedia(
|
|
|
|
|
source: string,
|
|
|
|
|
mode: "music" | "screen",
|
|
|
|
|
): Promise<MediaState> {
|
2026-06-01 16:42:36 +07:00
|
|
|
return request<MediaState>("/api/media/queue", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({ source, mode }),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function skipMedia(): Promise<MediaState> {
|
|
|
|
|
return request<MediaState>("/api/media/skip", { method: "POST" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function stopMedia(): Promise<MediaState> {
|
|
|
|
|
return request<MediaState>("/api/media/stop", { method: "POST" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function setMediaVolume(volume: number): Promise<MediaState> {
|
|
|
|
|
return request<MediaState>("/api/media/volume", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({ volume }),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 16:56:44 +07:00
|
|
|
// ─── 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;
|
|
|
|
|
created_at: number;
|
|
|
|
|
uploaded_at: number | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function listRecordings(limit = 50): Promise<VoiceRecording[]> {
|
|
|
|
|
return request<VoiceRecording[]>(`/api/recordings?limit=${limit}`);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 16:42:36 +07:00
|
|
|
// ─── Auth ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export function login(password: string): Promise<{ ok: boolean }> {
|
|
|
|
|
return request<{ ok: boolean }>("/api/auth/login", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({ password }),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── UI State ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export function getUIState(): Promise<UIState> {
|
|
|
|
|
return request<UIState>("/api/ui-state");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function updateUIState(patch: Partial<UIState>): Promise<UIState> {
|
|
|
|
|
return request<UIState>("/api/ui-state", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify(patch),
|
|
|
|
|
});
|
|
|
|
|
}
|