ci: fix lint errors for CI
Deploy to VPS / deploy (push) Successful in 1m47s
Deploy to VPS / deploy (push) Successful in 1m47s
- Fix noImplicitAnyLet: add type to let match variable - Fix noAssignInExpressions: use matchAll() + for-of instead of while - Suppress useExhaustiveDependencies in mascot scroll effect - Suppress useSemanticElements for message card click handler
This commit is contained in:
+6
-6
@@ -14,12 +14,12 @@
|
|||||||
"packages/**/*.ts",
|
"packages/**/*.ts",
|
||||||
"*.json",
|
"*.json",
|
||||||
"*.ts",
|
"*.ts",
|
||||||
"!vendor/**",
|
"!vendor",
|
||||||
"!.claude/**",
|
"!.claude",
|
||||||
"!services/**/dist/**",
|
"!services/**/dist",
|
||||||
"!packages/**/dist/**",
|
"!packages/**/dist",
|
||||||
"!services/**/.next/**",
|
"!services/**/.next",
|
||||||
"!services/**/out/**"
|
"!services/**/out"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
COMMAND_VOICE_CHANNELS,
|
COMMAND_VOICE_CHANNELS,
|
||||||
COMMAND_VOICE_CONNECT,
|
COMMAND_VOICE_CONNECT,
|
||||||
COMMAND_VOICE_DISCONNECT,
|
COMMAND_VOICE_DISCONNECT,
|
||||||
CommandReply,
|
type CommandReply,
|
||||||
VOICE_STATUS_KEY,
|
VOICE_STATUS_KEY,
|
||||||
} from "@bete/shared";
|
} from "@bete/shared";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import {
|
import { AppError, ValidationError } from "@bete/shared/errors";
|
||||||
AppError,
|
|
||||||
ValidationError,
|
|
||||||
} from "@bete/shared/errors";
|
|
||||||
import { createChildLogger } from "@bete/shared/logger";
|
import { createChildLogger } from "@bete/shared/logger";
|
||||||
import type { NextFunction, Request, Response } from "express";
|
import type { NextFunction, Request, Response } from "express";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { createChildLogger } from "@bete/shared/logger";
|
|||||||
import type { Client } from "discord.js-selfbot-v13";
|
import type { Client } from "discord.js-selfbot-v13";
|
||||||
import type { CommandHandler } from "../modules/command-handler/commandHandler.js";
|
import type { CommandHandler } from "../modules/command-handler/commandHandler.js";
|
||||||
import type { EventBroadcaster } from "../modules/event-broadcaster/index.js";
|
import type { EventBroadcaster } from "../modules/event-broadcaster/index.js";
|
||||||
import { stopMetricsServer } from "../modules/gateway-metrics/index.js";
|
import type { stopMetricsServer } from "../modules/gateway-metrics/index.js";
|
||||||
import type { VoicePcmWsClient } from "../modules/voice-pcm-ws/index.js";
|
import type { VoicePcmWsClient } from "../modules/voice-pcm-ws/index.js";
|
||||||
import { stopMuxerWorker } from "../modules/voice-recording/muxer.js";
|
import { stopMuxerWorker } from "../modules/voice-recording/muxer.js";
|
||||||
import type { VoiceController } from "../modules/voice-recording/voiceController.js";
|
import type { VoiceController } from "../modules/voice-recording/voiceController.js";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createChildLogger } from "@bete/shared/logger";
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { getDatabase } from "../../shared/database/drizzle.js";
|
import { getDatabase } from "../../shared/database/drizzle.js";
|
||||||
import {
|
import {
|
||||||
ChannelCulture,
|
type ChannelCulture,
|
||||||
channelCulturesTable,
|
channelCulturesTable,
|
||||||
} from "../../shared/database/schema.js";
|
} from "../../shared/database/schema.js";
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ Pedoman ini mencerminkan nilai-nilai yang dijunjung server. Terapkan dengan bija
|
|||||||
Prioritas tertinggi (ANCAMAN KESELAMATAN):
|
Prioritas tertinggi (ANCAMAN KESELAMATAN):
|
||||||
- child_safety, violence, illegal_content — flag jika ada indikasi nyata
|
- child_safety, violence, illegal_content — flag jika ada indikasi nyata
|
||||||
- self_harm: BEDAKAN antara ancaman klinis nyata dengan hiperbola stres remaja (mis. "mati aja gua ngerjain tugas", "bunuh aku sekarang"). Hiperbola ekspresi stres adalah AMAN dan JANGAN di-flag sebagai self_harm.
|
- self_harm: BEDAKAN antara ancaman klinis nyata dengan hiperbola stres remaja (mis. "mati aja gua ngerjain tugas", "bunuh aku sekarang"). Hiperbola ekspresi stres adalah AMAN dan JANGAN di-flag sebagai self_harm.
|
||||||
- Pornografi/NSFW, ajakan seksual, roleplay seksual → "sexual_content". PENGECUALIAN: Roleplay aksi kasual/RPG non-seksual (misal \*memeluk teman karena sedih\*, \*menebas naga\*) adalah AMAN dan bukan sexual_content.
|
- Pornografi/NSFW, ajakan seksual, roleplay seksual → "sexual_content". PENGECUALIAN: Roleplay aksi kasual/RPG non-seksual (misal *memeluk teman karena sedih*, *menebas naga*) adalah AMAN dan bukan sexual_content.
|
||||||
- Judi/promosi judi → "gambling"
|
- Judi/promosi judi → "gambling"
|
||||||
- Narkoba/promosi → "drugs"
|
- Narkoba/promosi → "drugs"
|
||||||
|
|
||||||
@@ -425,7 +425,7 @@ const ALL_EXAMPLES: ExampleDef[] = [
|
|||||||
id: "13",
|
id: "13",
|
||||||
title: "Obfuscation / Zalgo Text (Evasion)",
|
title: "Obfuscation / Zalgo Text (Evasion)",
|
||||||
input:
|
input:
|
||||||
'[target] id=14141 user=hater: ++++++K1[[ your $€/F" "~\`| \\\\0ve $ 1F ¥°U |}iE ®©',
|
'[target] id=14141 user=hater: ++++++K1[[ your $€/F" "~`| \\\\0ve $ 1F ¥°U |}iE ®©',
|
||||||
output:
|
output:
|
||||||
'{"results":[{"message_id":"14141","status":"flagged","flags":["harassment","hate_speech"],"score":0.95,"categories":["harassment","hate_speech"],"severity":"critical","confidence":0.95,"recommended_action":"delete","policy_version":"default-2026-05-30","evidence":["++++++K1[[ your $€/F","\\\\\\\\0ve $ 1F ¥°U |}iE"],"analysis":"Pesan menggunakan teknik obfuscation/simbol untuk menyembunyikan frasa \'Kill yourself I love if you die\'. Ini adalah ancaman dan pelecehan berat yang disamarkan."}]}',
|
'{"results":[{"message_id":"14141","status":"flagged","flags":["harassment","hate_speech"],"score":0.95,"categories":["harassment","hate_speech"],"severity":"critical","confidence":0.95,"recommended_action":"delete","policy_version":"default-2026-05-30","evidence":["++++++K1[[ your $€/F","\\\\\\\\0ve $ 1F ¥°U |}iE"],"analysis":"Pesan menggunakan teknik obfuscation/simbol untuk menyembunyikan frasa \'Kill yourself I love if you die\'. Ini adalah ancaman dan pelecehan berat yang disamarkan."}]}',
|
||||||
modes: ["text", "media", "mixed"],
|
modes: ["text", "media", "mixed"],
|
||||||
@@ -522,7 +522,7 @@ const ALL_EXAMPLES: ExampleDef[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "24",
|
id: "24",
|
||||||
title: "Nabi palsu (SARA agama, Ba\'al)",
|
title: "Nabi palsu (SARA agama, Ba'al)",
|
||||||
input:
|
input:
|
||||||
"[target] id=24245 user=provokator: nabi palsu ba'al, sembah aja patung",
|
"[target] id=24245 user=provokator: nabi palsu ba'al, sembah aja patung",
|
||||||
output:
|
output:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createChildLogger } from "@bete/shared/logger";
|
import { createChildLogger } from "@bete/shared/logger";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import {
|
import type {
|
||||||
RecommendedActionSchema,
|
RecommendedActionSchema,
|
||||||
SeveritySchema,
|
SeveritySchema,
|
||||||
} from "./moderationSchemas.js";
|
} from "./moderationSchemas.js";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createChildLogger } from "@bete/shared/logger";
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { getDatabase } from "../../shared/database/drizzle.js";
|
import { getDatabase } from "../../shared/database/drizzle.js";
|
||||||
import {
|
import {
|
||||||
UserProfile,
|
type UserProfile,
|
||||||
userProfilesTable,
|
userProfilesTable,
|
||||||
} from "../../shared/database/schema.js";
|
} from "../../shared/database/schema.js";
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { and, desc, eq } from "drizzle-orm";
|
|||||||
import { getDatabase } from "../../shared/database/drizzle.js";
|
import { getDatabase } from "../../shared/database/drizzle.js";
|
||||||
import {
|
import {
|
||||||
messagesTable,
|
messagesTable,
|
||||||
UserReputation,
|
type UserReputation,
|
||||||
userReputationsTable,
|
userReputationsTable,
|
||||||
} from "../../shared/database/schema.js";
|
} from "../../shared/database/schema.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type CommandMessage, type CommandReply } from "@bete/shared";
|
import type { CommandMessage, CommandReply } from "@bete/shared";
|
||||||
import { createChildLogger } from "@bete/shared/logger";
|
import { createChildLogger } from "@bete/shared/logger";
|
||||||
import type { Client } from "discord.js-selfbot-v13";
|
import type { Client } from "discord.js-selfbot-v13";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { type CommandMessage, type CommandReply } from "@bete/shared";
|
import type { CommandMessage, CommandReply } from "@bete/shared";
|
||||||
import { createChildLogger } from "@bete/shared/logger";
|
import { createChildLogger } from "@bete/shared/logger";
|
||||||
import { StreamType } from "@discordjs/voice";
|
import { StreamType } from "@discordjs/voice";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type CommandMessage, type CommandReply } from "@bete/shared";
|
import type { CommandMessage, CommandReply } from "@bete/shared";
|
||||||
import { createChildLogger } from "@bete/shared/logger";
|
import { createChildLogger } from "@bete/shared/logger";
|
||||||
import type { Client } from "discord.js-selfbot-v13";
|
import type { Client } from "discord.js-selfbot-v13";
|
||||||
import { createModerationAction } from "../message-capture/messageStore.js";
|
import { createModerationAction } from "../message-capture/messageStore.js";
|
||||||
|
|||||||
@@ -153,8 +153,8 @@ export function getCustomEmojiMetadata(
|
|||||||
): RichMessageMetadata["customEmojis"] {
|
): RichMessageMetadata["customEmojis"] {
|
||||||
const CUSTOM_EMOJI_PATTERN = /<(a)?:([a-zA-Z0-9_]+):(\d+)>/g;
|
const CUSTOM_EMOJI_PATTERN = /<(a)?:([a-zA-Z0-9_]+):(\d+)>/g;
|
||||||
const emojis: CustomEmojiEvidence[] = [];
|
const emojis: CustomEmojiEvidence[] = [];
|
||||||
let match;
|
const matches = [...message.content.matchAll(CUSTOM_EMOJI_PATTERN)];
|
||||||
while ((match = CUSTOM_EMOJI_PATTERN.exec(message.content)) !== null) {
|
for (const match of matches) {
|
||||||
const [, animated, name, id] = match;
|
const [, animated, name, id] = match;
|
||||||
const ext = animated ? "gif" : "png";
|
const ext = animated ? "gif" : "png";
|
||||||
emojis.push({
|
emojis.push({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type ChildProcess, spawn } from "node:child_process";
|
import { type ChildProcess, spawn } from "node:child_process";
|
||||||
import { PassThrough, Readable } from "node:stream";
|
import { PassThrough, type Readable } from "node:stream";
|
||||||
import { createChildLogger } from "@bete/shared/logger";
|
import { createChildLogger } from "@bete/shared/logger";
|
||||||
import { StreamType } from "@discordjs/voice";
|
import { StreamType } from "@discordjs/voice";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Transform, TransformCallback } from "node:stream";
|
import { Transform, type TransformCallback } from "node:stream";
|
||||||
import { createChildLogger } from "@bete/shared/logger";
|
import { createChildLogger } from "@bete/shared/logger";
|
||||||
|
|
||||||
const logger = createChildLogger("packet-filter");
|
const logger = createChildLogger("packet-filter");
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Readable } from "node:stream";
|
import type { Readable } from "node:stream";
|
||||||
import { createChildLogger } from "@bete/shared/logger";
|
import { createChildLogger } from "@bete/shared/logger";
|
||||||
import {
|
import {
|
||||||
AudioPlayer,
|
type AudioPlayer,
|
||||||
AudioPlayerStatus,
|
AudioPlayerStatus,
|
||||||
type AudioResource,
|
type AudioResource,
|
||||||
createAudioPlayer,
|
createAudioPlayer,
|
||||||
createAudioResource,
|
createAudioResource,
|
||||||
StreamType,
|
StreamType,
|
||||||
VoiceConnection,
|
type VoiceConnection,
|
||||||
} from "@discordjs/voice";
|
} from "@discordjs/voice";
|
||||||
import type { DiscordPlayerOwner, DiscordPlayOptions } from "./mediaTypes.js";
|
import type { DiscordPlayerOwner, DiscordPlayOptions } from "./mediaTypes.js";
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export function createSpeakingHandler(
|
|||||||
|
|
||||||
// Step 4: Open the first segment
|
// Step 4: Open the first segment
|
||||||
const activeSession = activeSessions.get(channel.guild.id);
|
const activeSession = activeSessions.get(channel.guild.id);
|
||||||
let currentSegment = segmentManager.open(oggPacketStream);
|
const currentSegment = segmentManager.open(oggPacketStream);
|
||||||
|
|
||||||
// Step 5: Handle segment file completion
|
// Step 5: Handle segment file completion
|
||||||
currentSegment.out.on("finish", () => {
|
currentSegment.out.on("finish", () => {
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { Suspense, useEffect, useRef } from "react";
|
import { Suspense, useEffect, useRef } from "react";
|
||||||
import { uiStateApi } from "@/lib/api";
|
|
||||||
import { Header } from "@/components/layout/header";
|
import { Header } from "@/components/layout/header";
|
||||||
import { MobileTabBar } from "@/components/layout/mobile-tab-bar";
|
import { MobileTabBar } from "@/components/layout/mobile-tab-bar";
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
import { Sidebar } from "@/components/layout/sidebar";
|
||||||
import { MascotChatbot } from "@/features/mascot/mascot-chatbot";
|
import { MascotChatbot } from "@/features/mascot/mascot-chatbot";
|
||||||
|
import { uiStateApi } from "@/lib/api";
|
||||||
import { WsProvider } from "@/lib/ws/context";
|
import { WsProvider } from "@/lib/ws/context";
|
||||||
|
|
||||||
function DashboardShell({ children }: { children: React.ReactNode }) {
|
function DashboardShell({ children }: { children: React.ReactNode }) {
|
||||||
|
|||||||
@@ -489,9 +489,7 @@ export function LivePanel() {
|
|||||||
setMicActive(!micActive);
|
setMicActive(!micActive);
|
||||||
try {
|
try {
|
||||||
await voiceApi.sendCommand(
|
await voiceApi.sendCommand(
|
||||||
micActive
|
micActive ? "voice:transmit:stop" : "voice:transmit:start",
|
||||||
? "voice:transmit:stop"
|
|
||||||
: "voice:transmit:start",
|
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
setMicActive(micActive);
|
setMicActive(micActive);
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Bot, Loader2, MessageCircle, Send, Trash2, User, X } from "lucide-react";
|
import {
|
||||||
|
Bot,
|
||||||
|
Loader2,
|
||||||
|
MessageCircle,
|
||||||
|
Send,
|
||||||
|
Trash2,
|
||||||
|
User,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { mascotApi } from "@/lib/api";
|
import { mascotApi } from "@/lib/api";
|
||||||
import type { ChatHistoryMessage } from "@/lib/types";
|
import type { ChatHistoryMessage } from "@/lib/types";
|
||||||
@@ -20,6 +28,7 @@ export function MascotChatbot() {
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/useExhaustiveDependencies: scrollRef doesn't need messages in deps
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scrollRef.current) {
|
if (scrollRef.current) {
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
|
||||||
import { messagesApi, voiceApi } from "@/lib/api";
|
|
||||||
import type { MessageRecord, Channel, AttachmentRecord } from "@/lib/types";
|
|
||||||
import { useWebSocket } from "@/lib/ws/context";
|
|
||||||
import {
|
import {
|
||||||
Search,
|
|
||||||
RefreshCw,
|
|
||||||
Loader2,
|
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Flag,
|
|
||||||
X,
|
|
||||||
Download,
|
Download,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
|
Flag,
|
||||||
|
Loader2,
|
||||||
|
RefreshCw,
|
||||||
|
Search,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { messagesApi, voiceApi } from "@/lib/api";
|
||||||
import { useAppConfig } from "@/lib/hooks/use-config";
|
import { useAppConfig } from "@/lib/hooks/use-config";
|
||||||
|
import type { AttachmentRecord, Channel, MessageRecord } from "@/lib/types";
|
||||||
|
import { useWebSocket } from "@/lib/ws/context";
|
||||||
|
|
||||||
export function MessagesPanel() {
|
export function MessagesPanel() {
|
||||||
const { config } = useAppConfig();
|
const { config } = useAppConfig();
|
||||||
@@ -27,14 +27,20 @@ export function MessagesPanel() {
|
|||||||
const [cursor, setCursor] = useState<string | null>(null);
|
const [cursor, setCursor] = useState<string | null>(null);
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [searchResults, setSearchResults] = useState<MessageRecord[] | null>(null);
|
const [searchResults, setSearchResults] = useState<MessageRecord[] | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const [viewTab, setViewTab] = useState<"all" | "images" | "review">("all");
|
const [viewTab, setViewTab] = useState<"all" | "images" | "review">("all");
|
||||||
const [imageMessages, setImageMessages] = useState<MessageRecord[]>([]);
|
const [imageMessages, setImageMessages] = useState<MessageRecord[]>([]);
|
||||||
const [reviewMessages, setReviewMessages] = useState<MessageRecord[]>([]);
|
const [reviewMessages, setReviewMessages] = useState<MessageRecord[]>([]);
|
||||||
const [channels, setChannels] = useState<Channel[]>([]);
|
const [channels, setChannels] = useState<Channel[]>([]);
|
||||||
const [detailMessage, setDetailMessage] = useState<MessageRecord | null>(null);
|
const [detailMessage, setDetailMessage] = useState<MessageRecord | null>(
|
||||||
const [detailAttachments, setDetailAttachments] = useState<AttachmentRecord[]>([]);
|
null,
|
||||||
|
);
|
||||||
|
const [detailAttachments, setDetailAttachments] = useState<
|
||||||
|
AttachmentRecord[]
|
||||||
|
>([]);
|
||||||
const [detailLoading, setDetailLoading] = useState(false);
|
const [detailLoading, setDetailLoading] = useState(false);
|
||||||
const [selectedChannel, setSelectedChannel] = useState("");
|
const [selectedChannel, setSelectedChannel] = useState("");
|
||||||
|
|
||||||
@@ -43,7 +49,10 @@ export function MessagesPanel() {
|
|||||||
// Fetch available text channels for filtering
|
// Fetch available text channels for filtering
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!guildId) return;
|
if (!guildId) return;
|
||||||
voiceApi.getTextChannels(guildId).then(setChannels).catch(() => {});
|
voiceApi
|
||||||
|
.getTextChannels(guildId)
|
||||||
|
.then(setChannels)
|
||||||
|
.catch(() => {});
|
||||||
}, [guildId]);
|
}, [guildId]);
|
||||||
|
|
||||||
// Fetch initial messages
|
// Fetch initial messages
|
||||||
@@ -51,7 +60,11 @@ export function MessagesPanel() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const result = await messagesApi.list(guildId, 50, selectedChannel || undefined);
|
const result = await messagesApi.list(
|
||||||
|
guildId,
|
||||||
|
50,
|
||||||
|
selectedChannel || undefined,
|
||||||
|
);
|
||||||
setMessages(result.data);
|
setMessages(result.data);
|
||||||
setCursor(result.nextCursor);
|
setCursor(result.nextCursor);
|
||||||
setHasMore(result.nextCursor !== null);
|
setHasMore(result.nextCursor !== null);
|
||||||
@@ -75,7 +88,10 @@ export function MessagesPanel() {
|
|||||||
// Fetch review (flagged) messages
|
// Fetch review (flagged) messages
|
||||||
const fetchReview = useCallback(async () => {
|
const fetchReview = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const result = await messagesApi.getReview(50, selectedChannel || undefined);
|
const result = await messagesApi.getReview(
|
||||||
|
50,
|
||||||
|
selectedChannel || undefined,
|
||||||
|
);
|
||||||
setReviewMessages(result.results);
|
setReviewMessages(result.results);
|
||||||
} catch {
|
} catch {
|
||||||
// silently fail
|
// silently fail
|
||||||
@@ -146,7 +162,12 @@ export function MessagesPanel() {
|
|||||||
if (!cursor || loadingMore) return;
|
if (!cursor || loadingMore) return;
|
||||||
setLoadingMore(true);
|
setLoadingMore(true);
|
||||||
try {
|
try {
|
||||||
const result = await messagesApi.list(guildId, 50, selectedChannel || undefined, cursor);
|
const result = await messagesApi.list(
|
||||||
|
guildId,
|
||||||
|
50,
|
||||||
|
selectedChannel || undefined,
|
||||||
|
cursor,
|
||||||
|
);
|
||||||
setMessages((prev) => [...prev, ...result.data]);
|
setMessages((prev) => [...prev, ...result.data]);
|
||||||
setCursor(result.nextCursor);
|
setCursor(result.nextCursor);
|
||||||
setHasMore(result.nextCursor !== null);
|
setHasMore(result.nextCursor !== null);
|
||||||
@@ -506,7 +527,9 @@ export function MessagesPanel() {
|
|||||||
{detailMessage.ai_recommended_action &&
|
{detailMessage.ai_recommended_action &&
|
||||||
detailMessage.ai_recommended_action !== "none" && (
|
detailMessage.ai_recommended_action !== "none" && (
|
||||||
<div className="rounded-lg border p-2">
|
<div className="rounded-lg border p-2">
|
||||||
<p className="text-xs text-muted-foreground">Action</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Action
|
||||||
|
</p>
|
||||||
<p className="text-sm font-medium">
|
<p className="text-sm font-medium">
|
||||||
{detailMessage.ai_recommended_action}
|
{detailMessage.ai_recommended_action}
|
||||||
</p>
|
</p>
|
||||||
@@ -602,13 +625,14 @@ function MessageCard({
|
|||||||
const timeStr = date.toLocaleString();
|
const timeStr = date.toLocaleString();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// biome-ignore lint/a11y/useSemanticElements: complex nested content prevents using button
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => onClick(msg.id)}
|
onClick={() => onClick(msg.id)}
|
||||||
onKeyDown={(e) => e.key === "Enter" && onClick(msg.id)}
|
onKeyDown={(e) => e.key === "Enter" && onClick(msg.id)}
|
||||||
className={`rounded-lg border p-4 space-y-2 transition-colors cursor-pointer hover:bg-muted/50 ${
|
className={`rounded-lg border p-4 space-y-2 transition-colors cursor-pointer hover:bg-muted/50 ${
|
||||||
msg.ai_severity ? severityColor[msg.ai_severity] ?? "" : ""
|
msg.ai_severity ? (severityColor[msg.ai_severity] ?? "") : ""
|
||||||
} ${msg.ai_severity && msg.ai_severity !== "none" ? "border-l-2" : ""}`}
|
} ${msg.ai_severity && msg.ai_severity !== "none" ? "border-l-2" : ""}`}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -676,21 +700,18 @@ function MessageCard({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* AI Details */}
|
{/* AI Details */}
|
||||||
{msg.ai_moderation_flags &&
|
{msg.ai_moderation_flags && msg.ai_moderation_flags !== "[]" && (
|
||||||
msg.ai_moderation_flags !== "[]" && (
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
<div className="flex flex-wrap gap-1 mt-1">
|
{safeParseJsonArray(msg.ai_moderation_flags).map((flag) => (
|
||||||
{safeParseJsonArray(msg.ai_moderation_flags).map(
|
<span
|
||||||
(flag) => (
|
key={flag}
|
||||||
<span
|
className="inline-flex items-center rounded-md bg-destructive/10 px-1.5 py-0.5 text-xs font-medium text-destructive"
|
||||||
key={flag}
|
>
|
||||||
className="inline-flex items-center rounded-md bg-destructive/10 px-1.5 py-0.5 text-xs font-medium text-destructive"
|
{flag}
|
||||||
>
|
</span>
|
||||||
{flag}
|
))}
|
||||||
</span>
|
</div>
|
||||||
),
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{msg.ai_analysis && (
|
{msg.ai_analysis && (
|
||||||
<p className="text-xs text-muted-foreground mt-1 italic line-clamp-2">
|
<p className="text-xs text-muted-foreground mt-1 italic line-clamp-2">
|
||||||
@@ -699,22 +720,21 @@ function MessageCard({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Confidence score */}
|
{/* Confidence score */}
|
||||||
{msg.ai_confidence !== undefined &&
|
{msg.ai_confidence !== undefined && msg.ai_confidence !== null && (
|
||||||
msg.ai_confidence !== null && (
|
<div className="flex items-center gap-2 mt-1">
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex-1 h-1.5 rounded-full bg-muted overflow-hidden max-w-24">
|
||||||
<div className="flex-1 h-1.5 rounded-full bg-muted overflow-hidden max-w-24">
|
<div
|
||||||
<div
|
className="h-full rounded-full bg-primary"
|
||||||
className="h-full rounded-full bg-primary"
|
style={{
|
||||||
style={{
|
width: msg.ai_confidence * 100 + "%",
|
||||||
width: msg.ai_confidence * 100 + "%",
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{(msg.ai_confidence * 100).toFixed(0)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{(msg.ai_confidence * 100).toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-2 mt-2">
|
<div className="flex gap-2 mt-2">
|
||||||
@@ -755,9 +775,7 @@ function formatBytes(bytes: number): string {
|
|||||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeParseJsonArray(
|
function safeParseJsonArray(value: string | null | undefined): string[] {
|
||||||
value: string | null | undefined,
|
|
||||||
): string[] {
|
|
||||||
if (!value) return [];
|
if (!value) return [];
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(value);
|
const parsed = JSON.parse(value);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { configApi } from "@/lib/api";
|
import { configApi } from "@/lib/api";
|
||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
|
|||||||
Reference in New Issue
Block a user