feat(chat): extend composer mention model for thread references

Add the "thread" mention kind (makeThreadMention + stable dedup key) so a
chat can be referenced like a document. Also introduce submittedMentionsAtom
and a pure deriveMentionedPayload() helper, the building blocks for capturing
chips at submit time and mapping them to backend payload buckets.
This commit is contained in:
CREDO23 2026-06-23 18:30:22 +02:00
parent 5d79f91352
commit 1f6934b980
2 changed files with 41 additions and 13 deletions

View file

@ -28,6 +28,11 @@ export type MentionedDocumentInfo =
kind: "connector";
connector_type: string;
account_name: string;
}
| {
id: number;
title: string;
kind: "thread";
};
/**
@ -49,7 +54,10 @@ export function toMentionedDocumentInfo(
): MentionedDocumentInfo {
if (
"kind" in input &&
(input.kind === "doc" || input.kind === "folder" || input.kind === "connector")
(input.kind === "doc" ||
input.kind === "folder" ||
input.kind === "connector" ||
input.kind === "thread")
) {
return input;
}
@ -72,6 +80,18 @@ export function makeFolderMention(input: { id: number; name: string }): Mentione
};
}
/**
* Build a thread-mention chip from a thread row (id + title). Used to
* reference another conversation as read-only context.
*/
export function makeThreadMention(input: { id: number; title: string }): MentionedDocumentInfo {
return {
id: input.id,
title: input.title,
kind: "thread",
};
}
/**
* Atom to store the full context objects attached via @-mention chips in
* the current chat composer. Persists across component remounts.
@ -79,21 +99,26 @@ export function makeFolderMention(input: { id: number; name: string }): Mentione
export const mentionedDocumentsAtom = atom<MentionedDocumentInfo[]>([]);
/**
* Derived read-only atom that maps deduplicated mention chips into
* backend payload fields. Each mention kind maps to its own explicit
* payload bucket so non-document context never has to masquerade as a
* document type.
* Chips captured at submit time, so they survive the composer resetting
* the live atom on send. Consumed (and reset) by the send handler.
*/
export const mentionedDocumentIdsAtom = atom((get) => {
const allMentions = get(mentionedDocumentsAtom);
export const submittedMentionsAtom = atom<MentionedDocumentInfo[] | null>(null);
/**
* Map mention chips to their backend payload buckets. Each kind gets its
* own bucket so non-document context never masquerades as a document.
*/
export function deriveMentionedPayload(mentions: ReadonlyArray<MentionedDocumentInfo>) {
const seen = new Set<string>();
const deduped = allMentions.filter((m) => {
const deduped = mentions.filter((m) => {
const key =
m.kind === "doc"
? `doc:${m.document_type}:${m.id}`
: m.kind === "connector"
? `connector:${m.connector_type}:${m.id}`
: `folder:${m.id}`;
: m.kind === "thread"
? `thread:${m.id}`
: `folder:${m.id}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
@ -101,10 +126,12 @@ export const mentionedDocumentIdsAtom = atom((get) => {
const docs = deduped.filter((m) => m.kind === "doc");
const folders = deduped.filter((m) => m.kind === "folder");
const connectors = deduped.filter((m) => m.kind === "connector");
const threads = deduped.filter((m) => m.kind === "thread");
return {
document_ids: docs.map((doc) => doc.id),
folder_ids: folders.map((f) => f.id),
connector_ids: connectors.map((c) => c.id),
thread_ids: threads.map((t) => t.id),
connectors: connectors.map((c) => ({
id: c.id,
title: c.title,
@ -113,7 +140,7 @@ export const mentionedDocumentIdsAtom = atom((get) => {
account_name: c.account_name,
})),
};
});
}
/**
* Atom to store mentioned chips per message ID.

View file

@ -2,19 +2,20 @@ type MentionKeyInput = {
id: number;
document_type?: string | null;
connector_type?: string | null;
kind?: "doc" | "folder" | "connector";
kind?: "doc" | "folder" | "connector" | "thread";
};
/**
* Build a stable dedup key for a mention chip.
*
* Each mention kind keys off its real identity fields:
* docs by document type, folders by folder id, and connectors by
* connector type + account id.
* docs by document type, folders by folder id, connectors by
* connector type + account id, and threads by thread id.
*/
export function getMentionDocKey(doc: MentionKeyInput): string {
const kind = doc.kind ?? "doc";
if (kind === "folder") return `folder:${doc.id}`;
if (kind === "thread") return `thread:${doc.id}`;
if (kind === "connector") return `connector:${doc.connector_type ?? "UNKNOWN"}:${doc.id}`;
return `doc:${doc.document_type ?? "UNKNOWN"}:${doc.id}`;
}