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:
@@ -132,6 +132,8 @@ function mapSearchHit(hit: {
|
|||||||
channel_id?: string;
|
channel_id?: string;
|
||||||
guild_id?: string;
|
guild_id?: string;
|
||||||
thread_id?: string | null;
|
thread_id?: string | null;
|
||||||
|
channel_name?: string | null;
|
||||||
|
thread_name?: string | null;
|
||||||
created_at?: number;
|
created_at?: number;
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
@@ -146,6 +148,8 @@ function mapSearchHit(hit: {
|
|||||||
channel_id: hit.payload.channel_id ?? null,
|
channel_id: hit.payload.channel_id ?? null,
|
||||||
guild_id: hit.payload.guild_id ?? null,
|
guild_id: hit.payload.guild_id ?? null,
|
||||||
thread_id: hit.payload.thread_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;
|
channel_id?: string;
|
||||||
guild_id?: string;
|
guild_id?: string;
|
||||||
thread_id?: string | null;
|
thread_id?: string | null;
|
||||||
|
channel_name?: string | null;
|
||||||
|
thread_name?: string | null;
|
||||||
created_at?: number;
|
created_at?: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export interface QdrantVerdictPayload {
|
|||||||
channel_id?: string;
|
channel_id?: string;
|
||||||
guild_id?: string;
|
guild_id?: string;
|
||||||
thread_id?: string | null;
|
thread_id?: string | null;
|
||||||
|
channel_name?: string | null;
|
||||||
|
thread_name?: string | null;
|
||||||
created_at?: number;
|
created_at?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,42 @@ export interface ArchiveMessage {
|
|||||||
guild_id: string;
|
guild_id: string;
|
||||||
thread_id: string | null;
|
thread_id: string | null;
|
||||||
created_at: number;
|
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
|
/** 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
|
* content is deliberately NOT embedded into the public archive so it can't
|
||||||
* be found via public semantic search. Defaults to false. */
|
* be found via public semantic search. Defaults to false. */
|
||||||
isAgeRestricted?: boolean;
|
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
|
* Fire-and-forget: embed a captured message and upsert it into the persistent
|
||||||
* archive collection so the public web can semantic-search the corpus.
|
* 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
|
if (!normalized) return; // nothing meaningful left after cleanup
|
||||||
const vector = await embedText(normalized);
|
const vector = await embedText(normalized);
|
||||||
if (!vector) return;
|
if (!vector) return;
|
||||||
|
const { channel_name, thread_name } = extractChannelLabel(
|
||||||
|
message.metadata,
|
||||||
|
);
|
||||||
const ok = await upsertQdrantPointV2(
|
const ok = await upsertQdrantPointV2(
|
||||||
ARCHIVE_COLLECTION,
|
ARCHIVE_COLLECTION,
|
||||||
qdrantPointId(`archive:${message.id}`),
|
qdrantPointId(`archive:${message.id}`),
|
||||||
@@ -65,6 +98,8 @@ export function archiveMessageEmbedded(message: ArchiveMessage): void {
|
|||||||
channel_id: message.channel_id ?? "",
|
channel_id: message.channel_id ?? "",
|
||||||
guild_id: message.guild_id ?? "",
|
guild_id: message.guild_id ?? "",
|
||||||
thread_id: message.thread_id ?? null,
|
thread_id: message.thread_id ?? null,
|
||||||
|
channel_name: channel_name ?? null,
|
||||||
|
thread_name: thread_name ?? null,
|
||||||
created_at: message.created_at ?? Date.now(),
|
created_at: message.created_at ?? Date.now(),
|
||||||
analyzed_at: Date.now(),
|
analyzed_at: Date.now(),
|
||||||
// 5-year persistent window (archive is NOT a TTL cache).
|
// 5-year persistent window (archive is NOT a TTL cache).
|
||||||
|
|||||||
@@ -366,8 +366,18 @@ export function MessagesView({
|
|||||||
{r.username}
|
{r.username}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{r.channel_id && (
|
{r.thread_name && (
|
||||||
<span className="font-mono text-[10px] text-ink-muted">
|
<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}
|
#{r.channel_id}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -177,6 +177,8 @@ export interface SemanticSearchResult {
|
|||||||
channel_id: string | null;
|
channel_id: string | null;
|
||||||
guild_id: string | null;
|
guild_id: string | null;
|
||||||
thread_id: string | null;
|
thread_id: string | null;
|
||||||
|
channel_name: string | null;
|
||||||
|
thread_name: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageActivityBucket {
|
export interface MessageActivityBucket {
|
||||||
|
|||||||
Reference in New Issue
Block a user