diff --git a/services/backend/src/modules/messages/messages.service.ts b/services/backend/src/modules/messages/messages.service.ts index 41628417..945aee10 100644 --- a/services/backend/src/modules/messages/messages.service.ts +++ b/services/backend/src/modules/messages/messages.service.ts @@ -132,6 +132,8 @@ function mapSearchHit(hit: { channel_id?: string; guild_id?: string; thread_id?: string | null; + channel_name?: string | null; + thread_name?: string | null; created_at?: number; }; }) { @@ -146,6 +148,8 @@ function mapSearchHit(hit: { channel_id: hit.payload.channel_id ?? null, guild_id: hit.payload.guild_id ?? null, thread_id: hit.payload.thread_id ?? null, + channel_name: hit.payload.channel_name ?? null, + thread_name: hit.payload.thread_name ?? null, }; } diff --git a/services/backend/src/modules/messages/qdrant.ts b/services/backend/src/modules/messages/qdrant.ts index d995cd78..e3467753 100644 --- a/services/backend/src/modules/messages/qdrant.ts +++ b/services/backend/src/modules/messages/qdrant.ts @@ -14,6 +14,8 @@ export interface ArchiveHit { channel_id?: string; guild_id?: string; thread_id?: string | null; + channel_name?: string | null; + thread_name?: string | null; created_at?: number; }; } diff --git a/services/discord-gateway/src/modules/ai-moderation/qdrantClient.ts b/services/discord-gateway/src/modules/ai-moderation/qdrantClient.ts index 8f718b0e..018b7de7 100644 --- a/services/discord-gateway/src/modules/ai-moderation/qdrantClient.ts +++ b/services/discord-gateway/src/modules/ai-moderation/qdrantClient.ts @@ -44,6 +44,8 @@ export interface QdrantVerdictPayload { channel_id?: string; guild_id?: string; thread_id?: string | null; + channel_name?: string | null; + thread_name?: string | null; created_at?: number; } diff --git a/services/discord-gateway/src/modules/message-capture/archiveEmbedder.ts b/services/discord-gateway/src/modules/message-capture/archiveEmbedder.ts index fbbbce91..d22807cb 100644 --- a/services/discord-gateway/src/modules/message-capture/archiveEmbedder.ts +++ b/services/discord-gateway/src/modules/message-capture/archiveEmbedder.ts @@ -20,12 +20,42 @@ export interface ArchiveMessage { guild_id: string; thread_id: string | null; created_at: number; + /** JSON string of RichMessageMetadata (parsed for channel/thread names). */ + metadata?: string | null; /** True when the message came from an age-restricted (NSFW) channel. NSFW * content is deliberately NOT embedded into the public archive so it can't * be found via public semantic search. Defaults to false. */ isAgeRestricted?: boolean; } +/** + * Extract a human-readable channel label from the message's metadata JSON. + * The gateway captures `metadata.channel.{channelName,threadName}` per message; + * prefer the thread name (thread messages read better by their thread title), + * then the channel name. Returns null when unavailable (old messages without + * the metadata field, or malformed JSON). + */ +export function extractChannelLabel(metadata: string | null | undefined): { + channel_name: string | null; + thread_name: string | null; +} { + if (!metadata) return { channel_name: null, thread_name: null }; + try { + const m = JSON.parse(metadata) as { + channel?: { + channelName?: string | null; + threadName?: string | null; + }; + }; + return { + channel_name: m?.channel?.channelName ?? null, + thread_name: m?.channel?.threadName ?? null, + }; + } catch { + return { channel_name: null, thread_name: null }; + } +} + /** * Fire-and-forget: embed a captured message and upsert it into the persistent * archive collection so the public web can semantic-search the corpus. @@ -52,6 +82,9 @@ export function archiveMessageEmbedded(message: ArchiveMessage): void { if (!normalized) return; // nothing meaningful left after cleanup const vector = await embedText(normalized); if (!vector) return; + const { channel_name, thread_name } = extractChannelLabel( + message.metadata, + ); const ok = await upsertQdrantPointV2( ARCHIVE_COLLECTION, qdrantPointId(`archive:${message.id}`), @@ -65,6 +98,8 @@ export function archiveMessageEmbedded(message: ArchiveMessage): void { channel_id: message.channel_id ?? "", guild_id: message.guild_id ?? "", thread_id: message.thread_id ?? null, + channel_name: channel_name ?? null, + thread_name: thread_name ?? null, created_at: message.created_at ?? Date.now(), analyzed_at: Date.now(), // 5-year persistent window (archive is NOT a TTL cache). diff --git a/services/frontend/src/app/(dashboard)/messages/view.tsx b/services/frontend/src/app/(dashboard)/messages/view.tsx index 0fc7ee2e..c792d761 100644 --- a/services/frontend/src/app/(dashboard)/messages/view.tsx +++ b/services/frontend/src/app/(dashboard)/messages/view.tsx @@ -366,8 +366,18 @@ export function MessagesView({ {r.username} )} - {r.channel_id && ( + {r.thread_name && ( + ▶ {r.thread_name} + + )} + {!r.thread_name && r.channel_name && ( + + #{r.channel_name} + + )} + {!r.thread_name && !r.channel_name && r.channel_id && ( + #{r.channel_id} )} diff --git a/services/frontend/src/lib/types/message.ts b/services/frontend/src/lib/types/message.ts index 17b7924e..79e71b8d 100644 --- a/services/frontend/src/lib/types/message.ts +++ b/services/frontend/src/lib/types/message.ts @@ -177,6 +177,8 @@ export interface SemanticSearchResult { channel_id: string | null; guild_id: string | null; thread_id: string | null; + channel_name: string | null; + thread_name: string | null; } export interface MessageActivityBucket {