feat(archive): show real channel/thread names in semantic search UI

Gateway archive embedder now parses metadata.channel.{channelName,threadName}
from each message and stores channel_name/thread_name in the Qdrant payload.
Backend exposes them; the semantic results card renders the thread name (or
channel name) instead of a raw #snowflake, with the ID as a last-resort
fallback for legacy points. Matches the message feed's channel-label logic
(getMessageChannelLabel).
This commit is contained in:
asepharyana
2026-09-01 19:27:56 +07:00
parent 26a690943d
commit fa72fe03cd
6 changed files with 56 additions and 1 deletions
@@ -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,
};
}
@@ -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;
};
}
@@ -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;
}
@@ -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).
@@ -366,8 +366,18 @@ export function MessagesView({
{r.username}
</span>
)}
{r.channel_id && (
{r.thread_name && (
<span className="font-mono text-[10px] text-ink-muted">
▶ {r.thread_name}
</span>
)}
{!r.thread_name && r.channel_name && (
<span className="font-mono text-[10px] text-ink-muted">
#{r.channel_name}
</span>
)}
{!r.thread_name && !r.channel_name && r.channel_id && (
<span className="font-mono text-[10px] text-ink-faint">
#{r.channel_id}
</span>
)}
@@ -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 {