diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx
index 687c4b5..fac480c 100644
--- a/services/frontend/src/App.tsx
+++ b/services/frontend/src/App.tsx
@@ -11,7 +11,9 @@ import {
} from "./features/messages/hooks/useMessages";
import {
type ActiveSpeaker,
+ type Channel,
getAppConfig,
+ getTextChannels,
type MediaState,
type MessageRecord,
} from "./shared/api/client";
@@ -60,6 +62,8 @@ export default function App() {
!!localStorage.getItem("admin-password"),
);
const [monitorGuildId, setMonitorGuildId] = useState("");
+ const [textChannels, setTextChannels] = useState([]);
+ const [selectedMessageChannel, setSelectedMessageChannel] = useState("");
const audio = useAudioPlayback();
const activeTab = uiState.activeTab || "live";
@@ -113,7 +117,7 @@ export default function App() {
},
onAttachmentUploaded: () =>
messages
- .fetchMessages(monitorGuildId || undefined)
+ .fetchMessages(monitorGuildId || undefined, selectedMessageChannel || undefined)
.catch(() => undefined),
onMediaState: (state) => media.setMediaState(state as MediaState),
onVoiceRecordingUploaded: (d) =>
@@ -141,20 +145,38 @@ export default function App() {
voice.loadVoiceChannels(selectedVoiceGuild).catch(() => undefined);
}, [selectedVoiceGuild, voice.loadVoiceChannels]);
- // Auto-fetch messages for the monitor guild (all channels)
+ // Load text channels when monitor guild changes (Messages tab)
useEffect(() => {
if (monitorGuildId)
- messages.fetchMessages(monitorGuildId).catch(() => undefined);
+ getTextChannels(monitorGuildId)
+ .then(setTextChannels)
+ .catch(() => undefined);
+ }, [monitorGuildId]);
+
+ // Auto-fetch messages for the monitor guild
+ useEffect(() => {
+ if (monitorGuildId)
+ messages
+ .fetchMessages(monitorGuildId, selectedMessageChannel || undefined)
+ .catch(() => undefined);
}, [monitorGuildId, messages.fetchMessages]);
+ // Re-fetch when channel filter changes
+ useEffect(() => {
+ if (monitorGuildId)
+ messages
+ .fetchMessages(monitorGuildId, selectedMessageChannel || undefined)
+ .catch(() => undefined);
+ }, [selectedMessageChannel, monitorGuildId, messages.fetchMessages]);
+
// Periodic refetch — keeps dashboard in sync even if WS events missed
useEffect(() => {
if (!monitorGuildId) return;
const interval = setInterval(() => {
- messages.fetchMessages(monitorGuildId).catch(() => undefined);
+ messages.fetchMessages(monitorGuildId, selectedMessageChannel || undefined).catch(() => undefined);
}, 15_000);
return () => clearInterval(interval);
- }, [monitorGuildId, messages.fetchMessages]);
+ }, [monitorGuildId, messages.fetchMessages, selectedMessageChannel]);
return (
) : (
diff --git a/services/frontend/src/features/messages/hooks/useMessages.ts b/services/frontend/src/features/messages/hooks/useMessages.ts
index 3740d01..198558f 100644
--- a/services/frontend/src/features/messages/hooks/useMessages.ts
+++ b/services/frontend/src/features/messages/hooks/useMessages.ts
@@ -113,6 +113,7 @@ export function useMessages() {
);
const { count } = await reanalyzeErrorBatch({
guildId: currentGuild.current ?? undefined,
+ channelId: currentChannel.current,
});
return count;
}, []);
diff --git a/services/frontend/src/features/messages/index.tsx b/services/frontend/src/features/messages/index.tsx
index d682e80..fb1c651 100644
--- a/services/frontend/src/features/messages/index.tsx
+++ b/services/frontend/src/features/messages/index.tsx
@@ -1,7 +1,7 @@
import { motion } from "framer-motion";
-import { Filter, RotateCw, Search, X } from "lucide-react";
+import { Filter, Hash, RotateCw, Search, X } from "lucide-react";
import { useMemo, useState } from "react";
-import type { MessageRecord } from "../../shared/api/client";
+import type { Channel, MessageRecord } from "../../shared/api/client";
import { cardItem, cardStagger } from "../../shared/hooks/useFramerStagger";
import {
Badge,
@@ -11,6 +11,11 @@ import {
CardHeader,
CardTitle,
Input,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
Tabs,
TabsContent,
TabsList,
@@ -27,6 +32,9 @@ interface MessagesPanelProps {
onLoadMore?: () => void;
hasMore?: boolean;
loadingMore?: boolean;
+ textChannels?: Channel[];
+ selectedChannel?: string;
+ onChannelChange?: (channelId: string) => void;
}
type AiFilter = "all" | "clean" | "flagged" | "error" | "pending";
@@ -39,6 +47,9 @@ export function MessagesPanel({
onLoadMore,
hasMore,
loadingMore,
+ textChannels,
+ selectedChannel,
+ onChannelChange,
}: MessagesPanelProps) {
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState([]);
@@ -118,6 +129,27 @@ export function MessagesPanel({
Messages are automatically captured from all text channels in the
monitored guild. Real-time updates arrive via WebSocket.
+ {textChannels && textChannels.length > 0 && (
+
+
+
+
+ )}