fix: resolve architecture disconnects and codebase weaknesses
- Add TEXT_CHANNEL_ID and TEXT_GUILD_ID to config schema (fix silent channel monitoring) - Remove dead files: message-capture/broadcaster.ts, voice-recording/index.ts - Fix WebSocket voice_command payload to forward from frontend - Implement moderation:action handler in commandHandler - Fix useMascotChat to use canonical request() wrapper - Fix useAudioPlayback userId hash collision (use string not parseInt) - Add catch blocks to useMediaControl.skip/stop - Add typed broadcast functions (messageAnalyzed, voicePcmData, voiceActiveUser) - Apply Biome formatting and lint fixes Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
30c9e86edc
commit
3a7b005d95
@@ -16,6 +16,8 @@ export const configSchema = z
|
|||||||
.min(1, "DISCORD_TOKEN is required")
|
.min(1, "DISCORD_TOKEN is required")
|
||||||
.transform((value) => value.replace(/^("|')|(?:("|'))$/g, "")),
|
.transform((value) => value.replace(/^("|')|(?:("|'))$/g, "")),
|
||||||
MONITOR_GUILD_ID: z.string().min(1).optional(),
|
MONITOR_GUILD_ID: z.string().min(1).optional(),
|
||||||
|
TEXT_GUILD_ID: z.string().min(1).optional(),
|
||||||
|
TEXT_CHANNEL_ID: z.string().min(1).optional(),
|
||||||
|
|
||||||
// ── Legacy voice ─────────────────────────────────────────────────────
|
// ── Legacy voice ─────────────────────────────────────────────────────
|
||||||
VOICE_GUILD_ID: z.string().min(1).optional(),
|
VOICE_GUILD_ID: z.string().min(1).optional(),
|
||||||
@@ -231,7 +233,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
|
|||||||
const parsed = configSchema.parse(env);
|
const parsed = configSchema.parse(env);
|
||||||
return {
|
return {
|
||||||
...parsed,
|
...parsed,
|
||||||
EFFECTIVE_TEXT_GUILD_ID: parsed.MONITOR_GUILD_ID,
|
EFFECTIVE_TEXT_GUILD_ID: parsed.TEXT_GUILD_ID ?? parsed.MONITOR_GUILD_ID,
|
||||||
EFFECTIVE_VOICE_GUILD_ID: parsed.VOICE_GUILD_ID,
|
EFFECTIVE_VOICE_GUILD_ID: parsed.VOICE_GUILD_ID,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ export interface BroadcastFunctions {
|
|||||||
messageCreated: BroadcastFn;
|
messageCreated: BroadcastFn;
|
||||||
messageUpdated: BroadcastFn;
|
messageUpdated: BroadcastFn;
|
||||||
messageDeleted: BroadcastFn;
|
messageDeleted: BroadcastFn;
|
||||||
|
messageAnalyzed: BroadcastFn;
|
||||||
attachmentUploaded: BroadcastFn;
|
attachmentUploaded: BroadcastFn;
|
||||||
|
voicePcmData: BroadcastFn;
|
||||||
|
voiceActiveUser: BroadcastFn;
|
||||||
raw: BroadcastRawFn;
|
raw: BroadcastRawFn;
|
||||||
binary: BroadcastBinaryFn;
|
binary: BroadcastBinaryFn;
|
||||||
}
|
}
|
||||||
@@ -53,6 +56,15 @@ export const broadcastMessageDeleted: BroadcastFn = (data) =>
|
|||||||
export const broadcastAttachmentUploaded: BroadcastFn = (data) =>
|
export const broadcastAttachmentUploaded: BroadcastFn = (data) =>
|
||||||
(_fns?.attachmentUploaded ?? noop)(data);
|
(_fns?.attachmentUploaded ?? noop)(data);
|
||||||
|
|
||||||
|
export const broadcastMessageAnalyzed: BroadcastFn = (data) =>
|
||||||
|
(_fns?.messageAnalyzed ?? noop)(data);
|
||||||
|
|
||||||
|
export const broadcastVoicePcmData: BroadcastFn = (data) =>
|
||||||
|
(_fns?.voicePcmData ?? noop)(data);
|
||||||
|
|
||||||
|
export const broadcastVoiceActiveUser: BroadcastFn = (data) =>
|
||||||
|
(_fns?.voiceActiveUser ?? noop)(data);
|
||||||
|
|
||||||
export const broadcastRaw: BroadcastRawFn = (type, data) =>
|
export const broadcastRaw: BroadcastRawFn = (type, data) =>
|
||||||
(_fns?.raw ?? noopRaw)(type, data);
|
(_fns?.raw ?? noopRaw)(type, data);
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export function createWebSocketServer(server: Server): WebSocketServer {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else if (message.type === "voice_command" && message.command) {
|
} else if (message.type === "voice_command" && message.command) {
|
||||||
// Forward voice commands to discord-gateway
|
// Forward voice commands to discord-gateway with payload
|
||||||
import("../shared/redis/index.js").then(
|
import("../shared/redis/index.js").then(
|
||||||
({ getCommandPublisher }) => {
|
({ getCommandPublisher }) => {
|
||||||
const publisher = getCommandPublisher();
|
const publisher = getCommandPublisher();
|
||||||
@@ -107,7 +107,7 @@ export function createWebSocketServer(server: Server): WebSocketServer {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
id: commandId,
|
id: commandId,
|
||||||
type: message.command,
|
type: message.command,
|
||||||
payload: {},
|
payload: message.payload ?? {},
|
||||||
replyChannel: `reply:${commandId}`,
|
replyChannel: `reply:${commandId}`,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -186,8 +186,14 @@ export function createWebSocketServer(server: Server): WebSocketServer {
|
|||||||
broadcast({ type: "message_updated", data }),
|
broadcast({ type: "message_updated", data }),
|
||||||
messageDeleted: (data: unknown) =>
|
messageDeleted: (data: unknown) =>
|
||||||
broadcast({ type: "message_deleted", data }),
|
broadcast({ type: "message_deleted", data }),
|
||||||
|
messageAnalyzed: (data: unknown) =>
|
||||||
|
broadcast({ type: "message_analyzed", data }),
|
||||||
attachmentUploaded: (data: unknown) =>
|
attachmentUploaded: (data: unknown) =>
|
||||||
broadcast({ type: "attachment_uploaded", data }),
|
broadcast({ type: "attachment_uploaded", data }),
|
||||||
|
voicePcmData: (data: unknown) =>
|
||||||
|
broadcast({ type: "voice_pcm_data", data }),
|
||||||
|
voiceActiveUser: (data: unknown) =>
|
||||||
|
broadcast({ type: "voice_active_user", data }),
|
||||||
raw: (type: string, data: unknown) => broadcast({ type, data }),
|
raw: (type: string, data: unknown) => broadcast({ type, data }),
|
||||||
binary: broadcastBinary,
|
binary: broadcastBinary,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("backend", () => {
|
describe("backend", () => {
|
||||||
it("should load without errors", () => {
|
it("should load without errors", () => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createChildLogger } from "@bete/shared/logger";
|
|||||||
import type { Client } from "discord.js-selfbot-v13";
|
import type { Client } from "discord.js-selfbot-v13";
|
||||||
import Redis from "ioredis";
|
import Redis from "ioredis";
|
||||||
import { config } from "../../shared/config/config.js";
|
import { config } from "../../shared/config/config.js";
|
||||||
|
import { createModerationAction } from "../message-capture/messageStore.js";
|
||||||
import { discordPlayer } from "../voice-recording/player.js";
|
import { discordPlayer } from "../voice-recording/player.js";
|
||||||
import { voiceTransmitter } from "../voice-recording/transmitter.js";
|
import { voiceTransmitter } from "../voice-recording/transmitter.js";
|
||||||
import type { VoiceController } from "../voice-recording/voiceController.js";
|
import type { VoiceController } from "../voice-recording/voiceController.js";
|
||||||
@@ -155,6 +156,9 @@ export class CommandHandler {
|
|||||||
case "media:volume":
|
case "media:volume":
|
||||||
reply = await this.handleMediaVolume(cmd);
|
reply = await this.handleMediaVolume(cmd);
|
||||||
break;
|
break;
|
||||||
|
case "moderation:action":
|
||||||
|
reply = await this.handleModerationAction(cmd);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
logger.warn({ type: cmd.type }, "Unknown command type");
|
logger.warn({ type: cmd.type }, "Unknown command type");
|
||||||
reply = {
|
reply = {
|
||||||
@@ -437,6 +441,128 @@ export class CommandHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleModerationAction(
|
||||||
|
cmd: BackendCommand,
|
||||||
|
): Promise<CommandReply> {
|
||||||
|
const payload = cmd.payload as {
|
||||||
|
message_id?: string;
|
||||||
|
user_id?: string;
|
||||||
|
guild_id?: string;
|
||||||
|
channel_id?: string;
|
||||||
|
action_type?: string;
|
||||||
|
reason?: string;
|
||||||
|
executed_by?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
!payload.message_id ||
|
||||||
|
!payload.user_id ||
|
||||||
|
!payload.guild_id ||
|
||||||
|
!payload.action_type
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
id: cmd.id,
|
||||||
|
success: false,
|
||||||
|
data: null,
|
||||||
|
error: "message_id, user_id, guild_id, and action_type are required",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const validActions = [
|
||||||
|
"delete_message",
|
||||||
|
"mute_user",
|
||||||
|
"warn_user",
|
||||||
|
"kick_user",
|
||||||
|
"ban_user",
|
||||||
|
] as const;
|
||||||
|
if (
|
||||||
|
!validActions.includes(
|
||||||
|
payload.action_type as (typeof validActions)[number],
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
id: cmd.id,
|
||||||
|
success: false,
|
||||||
|
data: null,
|
||||||
|
error: `Invalid action_type: ${payload.action_type}. Must be one of: ${validActions.join(", ")}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For delete_message, also actually delete via Discord if client is available
|
||||||
|
if (payload.action_type === "delete_message" && this.client) {
|
||||||
|
try {
|
||||||
|
const channelId = String(cmd.payload.channel_id ?? "");
|
||||||
|
if (channelId) {
|
||||||
|
const channel = await this.client.channels.fetch(channelId);
|
||||||
|
if (channel?.isText()) {
|
||||||
|
const msg = await channel.messages
|
||||||
|
.fetch(payload.message_id)
|
||||||
|
.catch(() => null);
|
||||||
|
if (msg) {
|
||||||
|
await msg.delete().catch((err: unknown) => {
|
||||||
|
logger.warn(
|
||||||
|
{ error: err, messageId: payload.message_id },
|
||||||
|
"Failed to delete message via Discord",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
{ error: err, messageId: payload.message_id },
|
||||||
|
"Failed to fetch channel/message for deletion",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = await createModerationAction({
|
||||||
|
message_id: payload.message_id,
|
||||||
|
user_id: payload.user_id,
|
||||||
|
guild_id: payload.guild_id,
|
||||||
|
action_type: payload.action_type as
|
||||||
|
| "delete_message"
|
||||||
|
| "mute_user"
|
||||||
|
| "warn_user"
|
||||||
|
| "kick_user"
|
||||||
|
| "ban_user",
|
||||||
|
reason: payload.reason ?? null,
|
||||||
|
executed_by: payload.executed_by ?? "command-handler",
|
||||||
|
status: "executed",
|
||||||
|
error: null,
|
||||||
|
executed_at: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
actionId: action.id,
|
||||||
|
actionType: payload.action_type,
|
||||||
|
userId: payload.user_id,
|
||||||
|
},
|
||||||
|
"Moderation action executed",
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: cmd.id,
|
||||||
|
success: true,
|
||||||
|
data: action,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
logger.error(
|
||||||
|
{ error: message, commandId: cmd.id },
|
||||||
|
"Failed to execute moderation action",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: cmd.id,
|
||||||
|
success: false,
|
||||||
|
data: null,
|
||||||
|
error: message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Status publishing ----
|
// ---- Status publishing ----
|
||||||
|
|
||||||
private publishVoiceStatus(): void {
|
private publishVoiceStatus(): void {
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
import { createChildLogger } from "@bete/shared/logger";
|
|
||||||
import type { WebSocket } from "ws";
|
|
||||||
import type {
|
|
||||||
AnalysisQueueStatus,
|
|
||||||
AttachmentRecord,
|
|
||||||
MessageRecord,
|
|
||||||
ModerationWsEvent,
|
|
||||||
} from "../message-capture/types.js";
|
|
||||||
import type { MediaState } from "../voice-recording/mediaTypes.js";
|
|
||||||
|
|
||||||
export type BroadcasterClient = Pick<WebSocket, "readyState" | "send">;
|
|
||||||
|
|
||||||
const log = createChildLogger("broadcaster");
|
|
||||||
|
|
||||||
function sendJson(
|
|
||||||
clients: Set<BroadcasterClient>,
|
|
||||||
event: ModerationWsEvent,
|
|
||||||
): void {
|
|
||||||
const payload = JSON.stringify({ ...event, timestamp: Date.now() });
|
|
||||||
for (const client of clients) {
|
|
||||||
if (client.readyState === 1) {
|
|
||||||
try {
|
|
||||||
client.send(payload);
|
|
||||||
} catch (error) {
|
|
||||||
log.warn(
|
|
||||||
{ error, eventType: event.type },
|
|
||||||
"Failed to send event to client",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createBroadcaster() {
|
|
||||||
const clients = new Set<BroadcasterClient>();
|
|
||||||
|
|
||||||
return {
|
|
||||||
addClient(client: BroadcasterClient) {
|
|
||||||
clients.add(client);
|
|
||||||
log.debug({ clientCount: clients.size }, "Client added");
|
|
||||||
},
|
|
||||||
removeClient(client: BroadcasterClient) {
|
|
||||||
clients.delete(client);
|
|
||||||
log.debug({ clientCount: clients.size }, "Client removed");
|
|
||||||
},
|
|
||||||
clientCount() {
|
|
||||||
return clients.size;
|
|
||||||
},
|
|
||||||
getClients() {
|
|
||||||
return Array.from(clients);
|
|
||||||
},
|
|
||||||
uiState(state: unknown) {
|
|
||||||
sendJson(clients, { type: "ui_state", state });
|
|
||||||
},
|
|
||||||
userState(users: unknown[]) {
|
|
||||||
sendJson(clients, { type: "user_state", users });
|
|
||||||
},
|
|
||||||
messageCreated(data: MessageRecord) {
|
|
||||||
sendJson(clients, { type: "message_created", data });
|
|
||||||
},
|
|
||||||
messageUpdated(data: Partial<MessageRecord> & { id: string }) {
|
|
||||||
sendJson(clients, { type: "message_updated", data });
|
|
||||||
},
|
|
||||||
messageDeleted(data: { id: string; deleted_at: number }) {
|
|
||||||
sendJson(clients, { type: "message_deleted", data });
|
|
||||||
},
|
|
||||||
messageAnalyzed(data: MessageRecord) {
|
|
||||||
sendJson(clients, { type: "message_analyzed", data });
|
|
||||||
},
|
|
||||||
attachmentCreated(data: AttachmentRecord) {
|
|
||||||
sendJson(clients, { type: "attachment_created", data });
|
|
||||||
},
|
|
||||||
analysisQueueStatus(data: AnalysisQueueStatus) {
|
|
||||||
sendJson(clients, { type: "analysis_queue_status", data });
|
|
||||||
},
|
|
||||||
mediaState(state: MediaState) {
|
|
||||||
sendJson(clients, { type: "media_state", state });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ModerationBroadcaster = ReturnType<typeof createBroadcaster>;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export { OpusDecoder } from "./recorder/decoder.js";
|
|
||||||
export { SegmentManager } from "./recorder/segment.js";
|
|
||||||
export { startRecording, stopRecording } from "./recorder.js";
|
|
||||||
export { voiceTransmitter } from "./transmitter.js";
|
|
||||||
export { VoiceController } from "./voiceController.js";
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import type { AppConfig as SharedAppConfig } from "@bete/shared/config";
|
import type { AppConfig as SharedAppConfig } from "@bete/shared/config";
|
||||||
import {
|
import { loadConfig as sharedLoadConfig } from "@bete/shared/config";
|
||||||
config as sharedConfig,
|
|
||||||
loadConfig as sharedLoadConfig,
|
|
||||||
} from "@bete/shared/config";
|
|
||||||
|
|
||||||
// Re-export the unified config with EFFECTIVE_* fields added
|
// Re-export the unified config with EFFECTIVE_* fields added
|
||||||
export type AppConfig = SharedAppConfig & {
|
export type AppConfig = SharedAppConfig & {
|
||||||
@@ -15,7 +12,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
|
|||||||
const parsed = sharedLoadConfig(env);
|
const parsed = sharedLoadConfig(env);
|
||||||
return {
|
return {
|
||||||
...parsed,
|
...parsed,
|
||||||
EFFECTIVE_TEXT_GUILD_ID: parsed.MONITOR_GUILD_ID,
|
EFFECTIVE_TEXT_GUILD_ID: parsed.TEXT_GUILD_ID ?? parsed.MONITOR_GUILD_ID,
|
||||||
EFFECTIVE_VOICE_GUILD_ID: parsed.VOICE_GUILD_ID ?? parsed.GUILD_ID,
|
EFFECTIVE_VOICE_GUILD_ID: parsed.VOICE_GUILD_ID ?? parsed.GUILD_ID,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("discord-gateway", () => {
|
describe("discord-gateway", () => {
|
||||||
it("should load without errors", () => {
|
it("should load without errors", () => {
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ export function useMediaControl() {
|
|||||||
const state = await skipMedia();
|
const state = await skipMedia();
|
||||||
setMediaState(state);
|
setMediaState(state);
|
||||||
return state;
|
return state;
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
setError(message);
|
||||||
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -64,6 +68,10 @@ export function useMediaControl() {
|
|||||||
const state = await stopMedia();
|
const state = await stopMedia();
|
||||||
setMediaState(state);
|
setMediaState(state);
|
||||||
return state;
|
return state;
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
setError(message);
|
||||||
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,10 @@ export interface AppConfig {
|
|||||||
monitorGuildId: string | null;
|
monitorGuildId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChatResponse {
|
||||||
|
response?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type DashboardTab = "live" | "messages";
|
export type DashboardTab = "live" | "messages";
|
||||||
|
|
||||||
// ─── Messages ────────────────────────────────────────────────────────────────
|
// ─── Messages ────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export function useAudioPlayback() {
|
|||||||
Array.from({ length: 32 }, () => 0.04),
|
Array.from({ length: 32 }, () => 0.04),
|
||||||
);
|
);
|
||||||
const audioContextRef = useRef<AudioContext | null>(null);
|
const audioContextRef = useRef<AudioContext | null>(null);
|
||||||
const userTimelinesRef = useRef(new Map<number, number>());
|
const userTimelinesRef = useRef(new Map<string, number>());
|
||||||
|
|
||||||
const handleIncomingPcm = useCallback(
|
const handleIncomingPcm = useCallback(
|
||||||
(data: { userId: string; pcm: string }) => {
|
(data: { userId: string; pcm: string }) => {
|
||||||
@@ -57,13 +57,12 @@ export function useAudioPlayback() {
|
|||||||
source.connect(audioContext.destination);
|
source.connect(audioContext.destination);
|
||||||
|
|
||||||
// Schedule playback per user to avoid overlaps
|
// Schedule playback per user to avoid overlaps
|
||||||
const userIdHash = parseInt(data.userId, 10);
|
|
||||||
const currentTime = audioContext.currentTime;
|
const currentTime = audioContext.currentTime;
|
||||||
let nextStart = userTimelinesRef.current.get(userIdHash) || 0;
|
let nextStart = userTimelinesRef.current.get(data.userId) || 0;
|
||||||
if (nextStart < currentTime) nextStart = currentTime + 0.05;
|
if (nextStart < currentTime) nextStart = currentTime + 0.05;
|
||||||
source.start(nextStart);
|
source.start(nextStart);
|
||||||
userTimelinesRef.current.set(
|
userTimelinesRef.current.set(
|
||||||
userIdHash,
|
data.userId,
|
||||||
nextStart + audioBuffer.duration,
|
nextStart + audioBuffer.duration,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
import type { ChatResponse } from "../api/client";
|
||||||
|
import { request } from "../api/client";
|
||||||
import { createChildLogger } from "../logger";
|
import { createChildLogger } from "../logger";
|
||||||
|
|
||||||
const logger = createChildLogger("useMascotChat");
|
const logger = createChildLogger("useMascotChat");
|
||||||
@@ -18,17 +20,10 @@ export function useMascotChat(context?: ChatContext) {
|
|||||||
const handleSendMessage = useCallback(
|
const handleSendMessage = useCallback(
|
||||||
async (message: string): Promise<string> => {
|
async (message: string): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/mascot/chat", {
|
const data = await request<ChatResponse>("/api/chat", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ message, context }),
|
body: JSON.stringify({ message, context }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Mascot backend responded with ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await response.json()) as { response?: string };
|
|
||||||
return data.response || fallbackResponse(message, context);
|
return data.response || fallbackResponse(message, context);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn("Mascot backend unavailable, using fallback", { error });
|
logger.warn("Mascot backend unavailable, using fallback", { error });
|
||||||
|
|||||||
Reference in New Issue
Block a user