mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-31 19:45:15 +02:00
feat(web): enhance mention handling to support connectors and improve document key management
This commit is contained in:
parent
a41b16b73e
commit
2d134439ec
11 changed files with 160 additions and 164 deletions
|
|
@ -203,13 +203,11 @@ class NewChatUserImagePart(BaseModel):
|
||||||
class MentionedDocumentInfo(BaseModel):
|
class MentionedDocumentInfo(BaseModel):
|
||||||
"""Display metadata for a single ``@``-mention chip.
|
"""Display metadata for a single ``@``-mention chip.
|
||||||
|
|
||||||
Carries either a knowledge-base document or a knowledge-base folder
|
Carries a knowledge-base document, knowledge-base folder, or
|
||||||
(discriminated by ``kind``). The full triple
|
connected account (discriminated by ``kind``). Each kind uses its
|
||||||
``{id, title, document_type}`` is forwarded by the frontend mention
|
real identity fields: docs carry ``document_type``, folders carry
|
||||||
chip so the server can embed it in the persisted user message
|
only their folder id/title, and connectors carry ``connector_type``
|
||||||
``ContentPart[]`` (single ``mentioned-documents`` part). The
|
plus account metadata.
|
||||||
history loader then renders the chips on reload without an extra
|
|
||||||
fetch — mirrors the pre-refactor frontend ``persistUserTurn`` shape.
|
|
||||||
|
|
||||||
``kind`` defaults to ``"doc"`` so legacy clients and persisted rows
|
``kind`` defaults to ``"doc"`` so legacy clients and persisted rows
|
||||||
that predate folder mentions deserialise unchanged.
|
that predate folder mentions deserialise unchanged.
|
||||||
|
|
@ -217,17 +215,14 @@ class MentionedDocumentInfo(BaseModel):
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
title: str = Field(..., min_length=1, max_length=500)
|
title: str = Field(..., min_length=1, max_length=500)
|
||||||
document_type: str = Field(..., min_length=1, max_length=100)
|
document_type: str | None = Field(default=None, min_length=1, max_length=100)
|
||||||
kind: Literal["doc", "folder", "connector"] = Field(
|
kind: Literal["doc", "folder", "connector"] = Field(
|
||||||
default="doc",
|
default="doc",
|
||||||
description=(
|
description=(
|
||||||
"Discriminator for the chip's referent: ``doc`` is a "
|
"Discriminator for the chip's referent: ``doc`` is a "
|
||||||
"knowledge-base ``Document`` row, ``folder`` is a "
|
"knowledge-base ``Document`` row, ``folder`` is a "
|
||||||
"knowledge-base ``Folder`` row, and ``connector`` is a "
|
"knowledge-base ``Folder`` row, and ``connector`` is a "
|
||||||
"concrete connected account. Folders carry the sentinel "
|
"concrete connected account."
|
||||||
"``document_type='FOLDER'`` to keep the frontend dedup key "
|
|
||||||
"``(kind:document_type:id)`` from colliding doc and folder "
|
|
||||||
"ids that happen to share an integer value."
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
connector_type: str | None = Field(default=None, max_length=100)
|
connector_type: str | None = Field(default=None, max_length=100)
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ def _build_user_content(
|
||||||
[{"type": "text", "text": "..."},
|
[{"type": "text", "text": "..."},
|
||||||
{"type": "image", "image": "data:..."},
|
{"type": "image", "image": "data:..."},
|
||||||
{"type": "mentioned-documents", "documents": [{"id": int,
|
{"type": "mentioned-documents", "documents": [{"id": int,
|
||||||
"title": str, "document_type": str, "kind": "doc" | "folder"},
|
"title": str, "kind": "doc" | "folder" | "connector", ...},
|
||||||
...]}]
|
...]}]
|
||||||
|
|
||||||
The companion reader is
|
The companion reader is
|
||||||
|
|
@ -117,8 +117,8 @@ def _build_user_content(
|
||||||
which expects exactly this shape — keep them in sync.
|
which expects exactly this shape — keep them in sync.
|
||||||
|
|
||||||
``mentioned_documents``: optional list of mention chip dicts. Each
|
``mentioned_documents``: optional list of mention chip dicts. Each
|
||||||
dict may include a ``kind`` discriminator (``"doc"`` or ``"folder"``)
|
dict may include a ``kind`` discriminator so the persisted
|
||||||
so the persisted ContentPart round-trips folder chips on reload.
|
ContentPart round-trips folder and connector chips on reload.
|
||||||
When ``kind`` is missing we default to ``"doc"`` so legacy clients
|
When ``kind`` is missing we default to ``"doc"`` so legacy clients
|
||||||
that haven't migrated to the union schema still persist correctly.
|
that haven't migrated to the union schema still persist correctly.
|
||||||
"""
|
"""
|
||||||
|
|
@ -134,18 +134,23 @@ def _build_user_content(
|
||||||
doc_id = doc.get("id")
|
doc_id = doc.get("id")
|
||||||
title = doc.get("title")
|
title = doc.get("title")
|
||||||
document_type = doc.get("document_type")
|
document_type = doc.get("document_type")
|
||||||
if doc_id is None or title is None or document_type is None:
|
|
||||||
continue
|
|
||||||
kind_raw = doc.get("kind", "doc")
|
kind_raw = doc.get("kind", "doc")
|
||||||
kind = kind_raw if kind_raw in ("doc", "folder", "connector") else "doc"
|
kind = kind_raw if kind_raw in ("doc", "folder", "connector") else "doc"
|
||||||
|
if doc_id is None or title is None:
|
||||||
|
continue
|
||||||
|
if kind == "doc" and document_type is None:
|
||||||
|
continue
|
||||||
item = {
|
item = {
|
||||||
"id": doc_id,
|
"id": doc_id,
|
||||||
"title": str(title),
|
"title": str(title),
|
||||||
"document_type": str(document_type),
|
|
||||||
"kind": kind,
|
"kind": kind,
|
||||||
}
|
}
|
||||||
|
if document_type is not None:
|
||||||
|
item["document_type"] = str(document_type)
|
||||||
if kind == "connector":
|
if kind == "connector":
|
||||||
connector_type = doc.get("connector_type") or document_type
|
connector_type = doc.get("connector_type")
|
||||||
|
if connector_type is None:
|
||||||
|
continue
|
||||||
account_name = doc.get("account_name") or title
|
account_name = doc.get("account_name") or title
|
||||||
item["connector_type"] = str(connector_type)
|
item["connector_type"] = str(connector_type)
|
||||||
item["account_name"] = str(account_name)
|
item["account_name"] = str(account_name)
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ import {
|
||||||
convertToThreadMessage,
|
convertToThreadMessage,
|
||||||
reconcileInterruptedAssistantMessages,
|
reconcileInterruptedAssistantMessages,
|
||||||
} from "@/lib/chat/message-utils";
|
} from "@/lib/chat/message-utils";
|
||||||
|
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
|
||||||
import {
|
import {
|
||||||
isPodcastGenerating,
|
isPodcastGenerating,
|
||||||
looksLikePodcastRequest,
|
looksLikePodcastRequest,
|
||||||
|
|
@ -206,7 +207,7 @@ function pairBundleToolCallIds(
|
||||||
const MentionedDocumentInfoSchema = z.object({
|
const MentionedDocumentInfoSchema = z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
document_type: z.string(),
|
document_type: z.string().optional(),
|
||||||
kind: z
|
kind: z
|
||||||
.union([z.literal("doc"), z.literal("folder"), z.literal("connector")])
|
.union([z.literal("doc"), z.literal("folder"), z.literal("connector")])
|
||||||
.optional()
|
.optional()
|
||||||
|
|
@ -234,9 +235,8 @@ function extractMentionedDocuments(content: unknown): MentionedDocumentInfo[] {
|
||||||
return {
|
return {
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
title: doc.title,
|
title: doc.title,
|
||||||
document_type: doc.document_type,
|
|
||||||
kind: "connector",
|
kind: "connector",
|
||||||
connector_type: doc.connector_type ?? doc.document_type,
|
connector_type: doc.connector_type ?? doc.document_type ?? "UNKNOWN",
|
||||||
account_name: doc.account_name ?? doc.title,
|
account_name: doc.account_name ?? doc.title,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -244,14 +244,13 @@ function extractMentionedDocuments(content: unknown): MentionedDocumentInfo[] {
|
||||||
return {
|
return {
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
title: doc.title,
|
title: doc.title,
|
||||||
document_type: "FOLDER",
|
|
||||||
kind: "folder",
|
kind: "folder",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
title: doc.title,
|
title: doc.title,
|
||||||
document_type: doc.document_type,
|
document_type: doc.document_type ?? "UNKNOWN",
|
||||||
kind: "doc",
|
kind: "doc",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -957,15 +956,13 @@ export default function NewChatPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Collect unique mention chips for display & persistence.
|
// Collect unique mention chips for display & persistence.
|
||||||
// Dedup key is ``kind:document_type:id`` so a folder and a
|
// The ``kind`` field is forwarded to the backend
|
||||||
// doc with the same integer id never collapse into one
|
|
||||||
// entry. The ``kind`` field is forwarded to the backend
|
|
||||||
// so the persisted ``mentioned-documents`` content part
|
// so the persisted ``mentioned-documents`` content part
|
||||||
// can render the correct chip type on reload.
|
// can render the correct chip type on reload.
|
||||||
const allMentionedDocs: MentionedDocumentInfo[] = [];
|
const allMentionedDocs: MentionedDocumentInfo[] = [];
|
||||||
const seenDocKeys = new Set<string>();
|
const seenDocKeys = new Set<string>();
|
||||||
for (const doc of mentionedDocuments) {
|
for (const doc of mentionedDocuments) {
|
||||||
const key = `${doc.kind}:${doc.document_type}:${doc.id}`;
|
const key = getMentionDocKey(doc);
|
||||||
if (seenDocKeys.has(key)) continue;
|
if (seenDocKeys.has(key)) continue;
|
||||||
seenDocKeys.add(key);
|
seenDocKeys.add(key);
|
||||||
allMentionedDocs.push(doc);
|
allMentionedDocs.push(doc);
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,6 @@
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import type { Document } from "@/contracts/types/document.types";
|
import type { Document } from "@/contracts/types/document.types";
|
||||||
|
|
||||||
/**
|
|
||||||
* Sentinel ``document_type`` used for folder mention chips so the
|
|
||||||
* dedup key (`kind:document_type:id`) never collides a document with a
|
|
||||||
* folder that happens to share an integer id.
|
|
||||||
*/
|
|
||||||
export const FOLDER_MENTION_DOCUMENT_TYPE = "FOLDER";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display metadata for a single ``@``-mention chip.
|
* Display metadata for a single ``@``-mention chip.
|
||||||
*
|
*
|
||||||
|
|
@ -27,13 +20,11 @@ export type MentionedDocumentInfo =
|
||||||
| {
|
| {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
document_type: typeof FOLDER_MENTION_DOCUMENT_TYPE;
|
|
||||||
kind: "folder";
|
kind: "folder";
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
document_type: string;
|
|
||||||
kind: "connector";
|
kind: "connector";
|
||||||
connector_type: string;
|
connector_type: string;
|
||||||
account_name: string;
|
account_name: string;
|
||||||
|
|
@ -51,8 +42,7 @@ type LegacyDocMention = Pick<Document, "id" | "title" | "document_type">;
|
||||||
* Normalize an arbitrary chip-like input into the discriminated
|
* Normalize an arbitrary chip-like input into the discriminated
|
||||||
* ``MentionedDocumentInfo`` shape. Existing call sites that only have
|
* ``MentionedDocumentInfo`` shape. Existing call sites that only have
|
||||||
* ``{id, title, document_type}`` flow through here so they don't have
|
* ``{id, title, document_type}`` flow through here so they don't have
|
||||||
* to thread ``kind`` everywhere — the helper defaults to ``"doc"`` and
|
* to thread ``kind`` everywhere — the helper defaults to ``"doc"``.
|
||||||
* rewrites the document type for folders.
|
|
||||||
*/
|
*/
|
||||||
export function toMentionedDocumentInfo(
|
export function toMentionedDocumentInfo(
|
||||||
input: LegacyDocMention | MentionedDocumentInfo
|
input: LegacyDocMention | MentionedDocumentInfo
|
||||||
|
|
@ -78,31 +68,32 @@ export function makeFolderMention(input: { id: number; name: string }): Mentione
|
||||||
return {
|
return {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
title: input.name,
|
title: input.name,
|
||||||
document_type: FOLDER_MENTION_DOCUMENT_TYPE,
|
|
||||||
kind: "folder",
|
kind: "folder",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Atom to store the full mention objects (documents + folders) attached
|
* Atom to store the full context objects attached via @-mention chips in
|
||||||
* via @-mention chips in the current chat composer. Persists across
|
* the current chat composer. Persists across component remounts.
|
||||||
* component remounts.
|
|
||||||
*/
|
*/
|
||||||
export const mentionedDocumentsAtom = atom<MentionedDocumentInfo[]>([]);
|
export const mentionedDocumentsAtom = atom<MentionedDocumentInfo[]>([]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derived read-only atom that maps deduplicated mention chips into
|
* Derived read-only atom that maps deduplicated mention chips into
|
||||||
* backend payload fields. Doc chips split by ``document_type`` exactly
|
* backend payload fields. Each mention kind maps to its own explicit
|
||||||
* like before; folder chips are projected into a separate
|
* payload bucket so non-document context never has to masquerade as a
|
||||||
* ``folder_ids`` bucket so the route can forward
|
* document type.
|
||||||
* ``mentioned_folder_ids`` to the agent without the priority middleware
|
|
||||||
* conflating them with hybrid-search ids.
|
|
||||||
*/
|
*/
|
||||||
export const mentionedDocumentIdsAtom = atom((get) => {
|
export const mentionedDocumentIdsAtom = atom((get) => {
|
||||||
const allMentions = get(mentionedDocumentsAtom);
|
const allMentions = get(mentionedDocumentsAtom);
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const deduped = allMentions.filter((m) => {
|
const deduped = allMentions.filter((m) => {
|
||||||
const key = `${m.kind}:${m.document_type}:${m.id}`;
|
const key =
|
||||||
|
m.kind === "doc"
|
||||||
|
? `doc:${m.document_type}:${m.id}`
|
||||||
|
: m.kind === "connector"
|
||||||
|
? `connector:${m.connector_type}:${m.id}`
|
||||||
|
: `folder:${m.id}`;
|
||||||
if (seen.has(key)) return false;
|
if (seen.has(key)) return false;
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -120,7 +111,6 @@ export const mentionedDocumentIdsAtom = atom((get) => {
|
||||||
connectors: connectors.map((c) => ({
|
connectors: connectors.map((c) => ({
|
||||||
id: c.id,
|
id: c.id,
|
||||||
title: c.title,
|
title: c.title,
|
||||||
document_type: c.document_type,
|
|
||||||
kind: c.kind,
|
kind: c.kind,
|
||||||
connector_type: c.connector_type,
|
connector_type: c.connector_type,
|
||||||
account_name: c.account_name,
|
account_name: c.account_name,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import {
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { FOLDER_MENTION_DOCUMENT_TYPE } from "@/atoms/chat/mentioned-documents.atom";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||||
import type { Document } from "@/contracts/types/document.types";
|
import type { Document } from "@/contracts/types/document.types";
|
||||||
|
|
@ -40,8 +39,6 @@ export interface MentionedDocument {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Input shape for inserting a chip. ``kind`` defaults to ``"doc"``.
|
* Input shape for inserting a chip. ``kind`` defaults to ``"doc"``.
|
||||||
* Folder chips default ``document_type`` to ``FOLDER_MENTION_DOCUMENT_TYPE``
|
|
||||||
* so the dedup key never collides with a doc chip sharing the same id.
|
|
||||||
*/
|
*/
|
||||||
export type MentionChipInput = {
|
export type MentionChipInput = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -78,7 +75,12 @@ export interface InlineMentionEditorRef {
|
||||||
doc: Pick<Document, "id" | "title" | "document_type">,
|
doc: Pick<Document, "id" | "title" | "document_type">,
|
||||||
options?: { removeTriggerText?: boolean }
|
options?: { removeTriggerText?: boolean }
|
||||||
) => void;
|
) => void;
|
||||||
removeDocumentChip: (docId: number, docType?: string) => void;
|
removeDocumentChip: (
|
||||||
|
docId: number,
|
||||||
|
docType?: string,
|
||||||
|
kind?: MentionKind,
|
||||||
|
connectorType?: string
|
||||||
|
) => void;
|
||||||
setDocumentChipStatus: (
|
setDocumentChipStatus: (
|
||||||
docId: number,
|
docId: number,
|
||||||
docType: string | undefined,
|
docType: string | undefined,
|
||||||
|
|
@ -95,7 +97,7 @@ interface InlineMentionEditorProps {
|
||||||
onActionClose?: () => void;
|
onActionClose?: () => void;
|
||||||
onSubmit?: () => void;
|
onSubmit?: () => void;
|
||||||
onChange?: (text: string, docs: MentionedDocument[]) => void;
|
onChange?: (text: string, docs: MentionedDocument[]) => void;
|
||||||
onDocumentRemove?: (docId: number, docType?: string) => void;
|
onDocumentRemove?: (docId: number, docType?: string, kind?: MentionKind, connectorType?: string) => void;
|
||||||
onKeyDown?: (e: React.KeyboardEvent) => void;
|
onKeyDown?: (e: React.KeyboardEvent) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
@ -135,7 +137,12 @@ const EMPTY_VALUE: ComposerValue = [{ type: "p", children: [{ text: "" }] }];
|
||||||
* the X button and Backspace go through the same call site.
|
* the X button and Backspace go through the same call site.
|
||||||
*/
|
*/
|
||||||
type MentionEditorContextValue = {
|
type MentionEditorContextValue = {
|
||||||
removeChip: (docId: number, docType: string | undefined) => void;
|
removeChip: (
|
||||||
|
docId: number,
|
||||||
|
docType: string | undefined,
|
||||||
|
kind: MentionKind | undefined,
|
||||||
|
connectorType: string | undefined
|
||||||
|
) => void;
|
||||||
};
|
};
|
||||||
const MentionEditorContext = createContext<MentionEditorContextValue | null>(null);
|
const MentionEditorContext = createContext<MentionEditorContextValue | null>(null);
|
||||||
|
|
||||||
|
|
@ -181,7 +188,12 @@ const MentionElement: FC<PlateElementProps<MentionElementNode>> = ({
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
ctx.removeChip(element.id, element.document_type);
|
ctx.removeChip(
|
||||||
|
element.id,
|
||||||
|
element.document_type,
|
||||||
|
element.kind,
|
||||||
|
element.connector_type
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
className="absolute inset-0 size-3 rounded-sm p-0 opacity-0 transition-opacity hover:bg-transparent hover:text-primary focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-0 group-hover:opacity-100 [&_svg]:size-3"
|
className="absolute inset-0 size-3 rounded-sm p-0 opacity-0 transition-opacity hover:bg-transparent hover:text-primary focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-0 group-hover:opacity-100 [&_svg]:size-3"
|
||||||
>
|
>
|
||||||
|
|
@ -456,18 +468,11 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
||||||
|
|
||||||
const removeTriggerText = options?.removeTriggerText ?? true;
|
const removeTriggerText = options?.removeTriggerText ?? true;
|
||||||
const kind: MentionKind = mention.kind ?? "doc";
|
const kind: MentionKind = mention.kind ?? "doc";
|
||||||
const document_type =
|
|
||||||
mention.document_type ??
|
|
||||||
(kind === "folder"
|
|
||||||
? FOLDER_MENTION_DOCUMENT_TYPE
|
|
||||||
: kind === "connector"
|
|
||||||
? mention.connector_type
|
|
||||||
: undefined);
|
|
||||||
const mentionNode: MentionElementNode = {
|
const mentionNode: MentionElementNode = {
|
||||||
type: MENTION_TYPE,
|
type: MENTION_TYPE,
|
||||||
id: mention.id,
|
id: mention.id,
|
||||||
title: mention.title,
|
title: mention.title,
|
||||||
document_type,
|
document_type: mention.document_type,
|
||||||
kind,
|
kind,
|
||||||
connector_type: mention.connector_type,
|
connector_type: mention.connector_type,
|
||||||
account_name: mention.account_name,
|
account_name: mention.account_name,
|
||||||
|
|
@ -526,17 +531,33 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
||||||
[insertMentionChip]
|
[insertMentionChip]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove chip(s) matching (id, document_type). Iterates in
|
// Remove chip(s) matching the mention identity. Iterates in
|
||||||
// descending path order so removing one entry can't invalidate
|
// descending path order so removing one entry can't invalidate
|
||||||
// later paths. Chips are deduped today, so this typically runs
|
// later paths. Chips are deduped today, so this typically runs
|
||||||
// at most once.
|
// at most once.
|
||||||
const removeDocumentChip = useCallback(
|
const removeDocumentChip = useCallback(
|
||||||
(docId: number, docType?: string) => {
|
(docId: number, docType?: string, kind?: MentionKind, connectorType?: string) => {
|
||||||
const match = (n: unknown) => {
|
const match = (n: unknown) => {
|
||||||
if (!n || typeof n !== "object" || !("type" in n)) return false;
|
if (!n || typeof n !== "object" || !("type" in n)) return false;
|
||||||
const node = n as MentionElementNode;
|
const node = n as MentionElementNode;
|
||||||
if (node.type !== MENTION_TYPE) return false;
|
if (node.type !== MENTION_TYPE) return false;
|
||||||
if (node.id !== docId) return false;
|
if (node.id !== docId) return false;
|
||||||
|
if (kind) {
|
||||||
|
return (
|
||||||
|
getMentionDocKey({
|
||||||
|
id: node.id,
|
||||||
|
kind: node.kind ?? "doc",
|
||||||
|
document_type: node.document_type,
|
||||||
|
connector_type: node.connector_type,
|
||||||
|
}) ===
|
||||||
|
getMentionDocKey({
|
||||||
|
id: docId,
|
||||||
|
kind,
|
||||||
|
document_type: docType,
|
||||||
|
connector_type: connectorType,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
return (node.document_type ?? "UNKNOWN") === (docType ?? "UNKNOWN");
|
return (node.document_type ?? "UNKNOWN") === (docType ?? "UNKNOWN");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -554,9 +575,14 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
||||||
// Single removal call site for Backspace and the X button so the
|
// Single removal call site for Backspace and the X button so the
|
||||||
// two can never diverge (e.g. one forgetting to notify the parent).
|
// two can never diverge (e.g. one forgetting to notify the parent).
|
||||||
const removeChip = useCallback(
|
const removeChip = useCallback(
|
||||||
(docId: number, docType: string | undefined) => {
|
(
|
||||||
removeDocumentChip(docId, docType);
|
docId: number,
|
||||||
onDocumentRemove?.(docId, docType);
|
docType: string | undefined,
|
||||||
|
kind: MentionKind | undefined,
|
||||||
|
connectorType: string | undefined
|
||||||
|
) => {
|
||||||
|
removeDocumentChip(docId, docType, kind, connectorType);
|
||||||
|
onDocumentRemove?.(docId, docType, kind, connectorType);
|
||||||
},
|
},
|
||||||
[onDocumentRemove, removeDocumentChip]
|
[onDocumentRemove, removeDocumentChip]
|
||||||
);
|
);
|
||||||
|
|
@ -679,7 +705,7 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
||||||
if (!isMentionNode(prev)) return;
|
if (!isMentionNode(prev)) return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
removeChip(prev.id, prev.document_type);
|
removeChip(prev.id, prev.document_type, prev.kind, prev.connector_type);
|
||||||
},
|
},
|
||||||
[editor.selection, getCurrentValue, onKeyDown, onSubmit, removeChip]
|
[editor.selection, getCurrentValue, onKeyDown, onSubmit, removeChip]
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ import {
|
||||||
import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom";
|
import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom";
|
||||||
import { currentThreadAtom } from "@/atoms/chat/current-thread.atom";
|
import { currentThreadAtom } from "@/atoms/chat/current-thread.atom";
|
||||||
import {
|
import {
|
||||||
FOLDER_MENTION_DOCUMENT_TYPE,
|
|
||||||
type MentionedDocumentInfo,
|
type MentionedDocumentInfo,
|
||||||
mentionedDocumentsAtom,
|
mentionedDocumentsAtom,
|
||||||
} from "@/atoms/chat/mentioned-documents.atom";
|
} from "@/atoms/chat/mentioned-documents.atom";
|
||||||
|
|
@ -544,14 +543,12 @@ const Composer: FC = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return docs.map<MentionedDocumentInfo>((d) => {
|
return docs.map<MentionedDocumentInfo>((d) => {
|
||||||
const documentType = d.document_type ?? "UNKNOWN";
|
|
||||||
if (d.kind === "connector") {
|
if (d.kind === "connector") {
|
||||||
return {
|
return {
|
||||||
id: d.id,
|
id: d.id,
|
||||||
title: d.title,
|
title: d.title,
|
||||||
document_type: documentType,
|
|
||||||
kind: "connector",
|
kind: "connector",
|
||||||
connector_type: d.connector_type ?? documentType,
|
connector_type: d.connector_type ?? "UNKNOWN",
|
||||||
account_name: d.account_name ?? d.title,
|
account_name: d.account_name ?? d.title,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -559,17 +556,13 @@ const Composer: FC = () => {
|
||||||
return {
|
return {
|
||||||
id: d.id,
|
id: d.id,
|
||||||
title: d.title,
|
title: d.title,
|
||||||
document_type: FOLDER_MENTION_DOCUMENT_TYPE,
|
|
||||||
kind: "folder",
|
kind: "folder",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: d.id,
|
id: d.id,
|
||||||
title: d.title,
|
title: d.title,
|
||||||
// Atom requires a string; ``"UNKNOWN"`` matches the
|
document_type: d.document_type ?? "UNKNOWN",
|
||||||
// sentinel ``getMentionDocKey`` and the editor's
|
|
||||||
// match predicates use.
|
|
||||||
document_type: documentType,
|
|
||||||
kind: "doc",
|
kind: "doc",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -760,13 +753,14 @@ const Composer: FC = () => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleDocumentRemove = useCallback(
|
const handleDocumentRemove = useCallback(
|
||||||
(docId: number, docType?: string) => {
|
(docId: number, docType?: string, kind?: "doc" | "folder" | "connector", connectorType?: string) => {
|
||||||
setMentionedDocuments((prev) => {
|
setMentionedDocuments((prev) => {
|
||||||
if (!docType) {
|
const removedKey = getMentionDocKey({
|
||||||
// Fallback when chip type is unavailable.
|
id: docId,
|
||||||
return prev.filter((doc) => doc.id !== docId);
|
document_type: docType,
|
||||||
}
|
kind,
|
||||||
const removedKey = getMentionDocKey({ id: docId, document_type: docType });
|
connector_type: connectorType,
|
||||||
|
});
|
||||||
return prev.filter((doc) => getMentionDocKey(doc) !== removedKey);
|
return prev.filter((doc) => getMentionDocKey(doc) !== removedKey);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -810,7 +804,12 @@ const Composer: FC = () => {
|
||||||
|
|
||||||
for (const [key, doc] of prevDocsMap) {
|
for (const [key, doc] of prevDocsMap) {
|
||||||
if (!nextDocsMap.has(key)) {
|
if (!nextDocsMap.has(key)) {
|
||||||
editor.removeDocumentChip(doc.id, doc.document_type);
|
editor.removeDocumentChip(
|
||||||
|
doc.id,
|
||||||
|
doc.kind === "doc" ? doc.document_type : undefined,
|
||||||
|
doc.kind,
|
||||||
|
doc.kind === "connector" ? doc.connector_type : undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ const UserTextPart: FC = () => {
|
||||||
const icon = isFolder ? (
|
const icon = isFolder ? (
|
||||||
<FolderIcon className="size-3.5" />
|
<FolderIcon className="size-3.5" />
|
||||||
) : isConnector ? (
|
) : isConnector ? (
|
||||||
getConnectorIcon(segment.doc.connector_type ?? segment.doc.document_type, "size-3.5") ?? (
|
getConnectorIcon(segment.doc.connector_type, "size-3.5") ?? (
|
||||||
<Plug className="size-3.5" />
|
<Plug className="size-3.5" />
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,6 @@ export function FolderTreeView({
|
||||||
for (const f of folders) {
|
for (const f of folders) {
|
||||||
const folderMentionKey = getMentionDocKey({
|
const folderMentionKey = getMentionDocKey({
|
||||||
id: f.id,
|
id: f.id,
|
||||||
document_type: "FOLDER",
|
|
||||||
kind: "folder",
|
kind: "folder",
|
||||||
});
|
});
|
||||||
states[f.id] = mentionedDocKeys.has(folderMentionKey) ? "all" : "none";
|
states[f.id] = mentionedDocKeys.has(folderMentionKey) ? "all" : "none";
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,7 @@ import {
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import type * as React from "react";
|
import type * as React from "react";
|
||||||
import {
|
import type { MentionedDocumentInfo } from "@/atoms/chat/mentioned-documents.atom";
|
||||||
FOLDER_MENTION_DOCUMENT_TYPE,
|
|
||||||
type MentionedDocumentInfo,
|
|
||||||
} from "@/atoms/chat/mentioned-documents.atom";
|
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
||||||
import {
|
import {
|
||||||
|
|
@ -78,6 +75,10 @@ type ResourceNodeValue =
|
||||||
| { kind: "view"; view: BrowseView }
|
| { kind: "view"; view: BrowseView }
|
||||||
| { kind: "mention"; mention: MentionedDocumentInfo };
|
| { kind: "mention"; mention: MentionedDocumentInfo };
|
||||||
|
|
||||||
|
function isConnectorActive(connector: SearchSourceConnector) {
|
||||||
|
return connector.is_active !== false;
|
||||||
|
}
|
||||||
|
|
||||||
function useDebounced<T>(value: T, delay = DEBOUNCE_MS) {
|
function useDebounced<T>(value: T, delay = DEBOUNCE_MS) {
|
||||||
const [debounced, setDebounced] = useState(value);
|
const [debounced, setDebounced] = useState(value);
|
||||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||||
|
|
@ -115,22 +116,24 @@ function makeDocMention(doc: Pick<Document, "id" | "title" | "document_type">):
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeFolderMention(folder: { id: number; title: string }): MentionedDocumentInfo {
|
function makeFolderMention(
|
||||||
|
folder: { id: number; title: string }
|
||||||
|
): Extract<MentionedDocumentInfo, { kind: "folder" }> {
|
||||||
return {
|
return {
|
||||||
id: folder.id,
|
id: folder.id,
|
||||||
title: folder.title,
|
title: folder.title,
|
||||||
document_type: FOLDER_MENTION_DOCUMENT_TYPE,
|
|
||||||
kind: "folder",
|
kind: "folder",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeConnectorMention(connector: SearchSourceConnector): MentionedDocumentInfo {
|
function makeConnectorMention(
|
||||||
|
connector: SearchSourceConnector
|
||||||
|
): Extract<MentionedDocumentInfo, { kind: "connector" }> {
|
||||||
const accountName = getConnectorDisplayName(connector.name);
|
const accountName = getConnectorDisplayName(connector.name);
|
||||||
const connectorTitle = titleForConnectorType(connector.connector_type);
|
const connectorTitle = titleForConnectorType(connector.connector_type);
|
||||||
return {
|
return {
|
||||||
id: connector.id,
|
id: connector.id,
|
||||||
title: `${connectorTitle}: ${accountName}`,
|
title: `${connectorTitle}: ${accountName}`,
|
||||||
document_type: connector.connector_type,
|
|
||||||
kind: "connector",
|
kind: "connector",
|
||||||
connector_type: connector.connector_type,
|
connector_type: connector.connector_type,
|
||||||
account_name: accountName,
|
account_name: accountName,
|
||||||
|
|
@ -140,8 +143,8 @@ function makeConnectorMention(connector: SearchSourceConnector): MentionedDocume
|
||||||
function mentionMatchesSearch(mention: MentionedDocumentInfo, searchLower: string) {
|
function mentionMatchesSearch(mention: MentionedDocumentInfo, searchLower: string) {
|
||||||
return [
|
return [
|
||||||
mention.title,
|
mention.title,
|
||||||
mention.document_type,
|
|
||||||
mention.kind,
|
mention.kind,
|
||||||
|
mention.kind === "doc" ? mention.document_type : "",
|
||||||
mention.kind === "connector" ? mention.connector_type : "",
|
mention.kind === "connector" ? mention.connector_type : "",
|
||||||
mention.kind === "connector" ? mention.account_name : "",
|
mention.kind === "connector" ? mention.account_name : "",
|
||||||
].some((value) => value.toLowerCase().includes(searchLower));
|
].some((value) => value.toLowerCase().includes(searchLower));
|
||||||
|
|
@ -171,6 +174,7 @@ export const DocumentMentionPicker = forwardRef<
|
||||||
|
|
||||||
const [zeroFolders] = useZeroQuery(queries.folders.bySpace({ searchSpaceId }));
|
const [zeroFolders] = useZeroQuery(queries.folders.bySpace({ searchSpaceId }));
|
||||||
const { data: connectors = [], isLoading: isConnectorsLoading } = useAtomValue(connectorsAtom);
|
const { data: connectors = [], isLoading: isConnectorsLoading } = useAtomValue(connectorsAtom);
|
||||||
|
const activeConnectors = useMemo(() => connectors.filter(isConnectorActive), [connectors]);
|
||||||
const paginationScopeKey = useMemo(
|
const paginationScopeKey = useMemo(
|
||||||
() => `${searchSpaceId}:${debouncedSearch}`,
|
() => `${searchSpaceId}:${debouncedSearch}`,
|
||||||
[searchSpaceId, debouncedSearch]
|
[searchSpaceId, debouncedSearch]
|
||||||
|
|
@ -307,8 +311,8 @@ export const DocumentMentionPicker = forwardRef<
|
||||||
}, [zeroFolders, debouncedSearch, deferredSearch, isSingleCharSearch, hasSearch]);
|
}, [zeroFolders, debouncedSearch, deferredSearch, isSingleCharSearch, hasSearch]);
|
||||||
|
|
||||||
const connectorMentions = useMemo(
|
const connectorMentions = useMemo(
|
||||||
() => connectors.filter((c) => c.is_active).map(makeConnectorMention),
|
() => activeConnectors.map(makeConnectorMention),
|
||||||
[connectors]
|
[activeConnectors]
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedKeys = useMemo(
|
const selectedKeys = useMemo(
|
||||||
|
|
@ -345,16 +349,16 @@ export const DocumentMentionPicker = forwardRef<
|
||||||
{
|
{
|
||||||
id: "connectors",
|
id: "connectors",
|
||||||
label: "Connectors",
|
label: "Connectors",
|
||||||
subtitle: connectors.length
|
subtitle: activeConnectors.length
|
||||||
? "Choose the exact account for tool use"
|
? "Choose the exact account for tool use"
|
||||||
: "No connected accounts yet",
|
: "No connected accounts yet",
|
||||||
icon: <Plug className="size-4" />,
|
icon: <Plug className="size-4" />,
|
||||||
type: "branch",
|
type: "branch",
|
||||||
disabled: connectors.length === 0,
|
disabled: activeConnectors.length === 0,
|
||||||
value: { kind: "view", view: { kind: "connectors" } },
|
value: { kind: "view", view: { kind: "connectors" } },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[connectors.length]
|
[activeConnectors.length]
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchNodes = useMemo<ComposerSuggestionNode<ResourceNodeValue>[]>(() => {
|
const searchNodes = useMemo<ComposerSuggestionNode<ResourceNodeValue>[]>(() => {
|
||||||
|
|
@ -385,7 +389,7 @@ export const DocumentMentionPicker = forwardRef<
|
||||||
id: getMentionDocKey(mention),
|
id: getMentionDocKey(mention),
|
||||||
label: mention.title,
|
label: mention.title,
|
||||||
subtitle: "Connector account",
|
subtitle: "Connector account",
|
||||||
icon: getConnectorIcon(mention.document_type, "size-4") ?? <Plug className="size-4" />,
|
icon: getConnectorIcon(mention.connector_type, "size-4") ?? <Plug className="size-4" />,
|
||||||
type: "item" as const,
|
type: "item" as const,
|
||||||
disabled: selectedKeys.has(getMentionDocKey(mention)),
|
disabled: selectedKeys.has(getMentionDocKey(mention)),
|
||||||
value: { kind: "mention" as const, mention },
|
value: { kind: "mention" as const, mention },
|
||||||
|
|
@ -404,7 +408,7 @@ export const DocumentMentionPicker = forwardRef<
|
||||||
|
|
||||||
const connectorTypeEntries = useMemo(() => {
|
const connectorTypeEntries = useMemo(() => {
|
||||||
const byType = new Map<string, SearchSourceConnector[]>();
|
const byType = new Map<string, SearchSourceConnector[]>();
|
||||||
for (const connector of connectors.filter((c) => c.is_active)) {
|
for (const connector of activeConnectors) {
|
||||||
const list = byType.get(connector.connector_type) ?? [];
|
const list = byType.get(connector.connector_type) ?? [];
|
||||||
list.push(connector);
|
list.push(connector);
|
||||||
byType.set(connector.connector_type, list);
|
byType.set(connector.connector_type, list);
|
||||||
|
|
@ -412,7 +416,7 @@ export const DocumentMentionPicker = forwardRef<
|
||||||
return Array.from(byType.entries()).sort(([a], [b]) =>
|
return Array.from(byType.entries()).sort(([a], [b]) =>
|
||||||
titleForConnectorType(a).localeCompare(titleForConnectorType(b))
|
titleForConnectorType(a).localeCompare(titleForConnectorType(b))
|
||||||
);
|
);
|
||||||
}, [connectors]);
|
}, [activeConnectors]);
|
||||||
|
|
||||||
const browseNodes = useMemo<ComposerSuggestionNode<ResourceNodeValue>[]>(() => {
|
const browseNodes = useMemo<ComposerSuggestionNode<ResourceNodeValue>[]>(() => {
|
||||||
if (view.kind === "root") return rootNodes;
|
if (view.kind === "root") return rootNodes;
|
||||||
|
|
@ -469,8 +473,8 @@ export const DocumentMentionPicker = forwardRef<
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return connectors
|
return activeConnectors
|
||||||
.filter((connector) => connector.is_active && connector.connector_type === view.connectorType)
|
.filter((connector) => connector.connector_type === view.connectorType)
|
||||||
.map((connector) => {
|
.map((connector) => {
|
||||||
const mention = makeConnectorMention(connector);
|
const mention = makeConnectorMention(connector);
|
||||||
return {
|
return {
|
||||||
|
|
@ -484,7 +488,7 @@ export const DocumentMentionPicker = forwardRef<
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
connectors,
|
activeConnectors,
|
||||||
connectorTypeEntries,
|
connectorTypeEntries,
|
||||||
folderMentions,
|
folderMentions,
|
||||||
rootNodes,
|
rootNodes,
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
type MentionKeyInput = {
|
type MentionKeyInput = {
|
||||||
id: number;
|
id: number;
|
||||||
document_type?: string | null;
|
document_type?: string | null;
|
||||||
|
connector_type?: string | null;
|
||||||
kind?: "doc" | "folder" | "connector";
|
kind?: "doc" | "folder" | "connector";
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a stable dedup key for a mention chip.
|
* Build a stable dedup key for a mention chip.
|
||||||
*
|
*
|
||||||
* The ``kind:document_type:id`` shape prevents a document and a folder
|
* Each mention kind keys off its real identity fields:
|
||||||
* with the same integer id from colliding in the chip array (folders
|
* docs by document type, folders by folder id, and connectors by
|
||||||
* use the ``FOLDER`` sentinel ``document_type``; the ``kind`` prefix
|
* connector type + account id.
|
||||||
* is the belt-and-braces guard).
|
|
||||||
*/
|
*/
|
||||||
export function getMentionDocKey(doc: MentionKeyInput): string {
|
export function getMentionDocKey(doc: MentionKeyInput): string {
|
||||||
const kind = doc.kind ?? "doc";
|
const kind = doc.kind ?? "doc";
|
||||||
return `${kind}:${doc.document_type ?? "UNKNOWN"}:${doc.id}`;
|
if (kind === "folder") return `folder:${doc.id}`;
|
||||||
|
if (kind === "connector") return `connector:${doc.connector_type ?? "UNKNOWN"}:${doc.id}`;
|
||||||
|
return `doc:${doc.document_type ?? "UNKNOWN"}:${doc.id}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,17 @@
|
||||||
import { EnumConnectorName } from "@/contracts/enums/connector";
|
|
||||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
|
||||||
import {
|
import {
|
||||||
OAUTH_CONNECTORS,
|
|
||||||
COMPOSIO_CONNECTORS,
|
COMPOSIO_CONNECTORS,
|
||||||
CRAWLERS,
|
CRAWLERS,
|
||||||
|
OAUTH_CONNECTORS,
|
||||||
OTHER_CONNECTORS,
|
OTHER_CONNECTORS,
|
||||||
} from "@/components/assistant-ui/connector-popup/constants/connector-constants";
|
} from "@/components/assistant-ui/connector-popup/constants/connector-constants";
|
||||||
|
import { EnumConnectorName } from "@/contracts/enums/connector";
|
||||||
|
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Connector Telemetry Types & Registry
|
// Connector Telemetry Types & Registry
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export type ConnectorTelemetryGroup =
|
export type ConnectorTelemetryGroup = "oauth" | "composio" | "crawler" | "other" | "unknown";
|
||||||
| "oauth"
|
|
||||||
| "composio"
|
|
||||||
| "crawler"
|
|
||||||
| "other"
|
|
||||||
| "unknown";
|
|
||||||
|
|
||||||
export interface ConnectorTelemetryMeta {
|
export interface ConnectorTelemetryMeta {
|
||||||
connector_type: string;
|
connector_type: string;
|
||||||
|
|
@ -31,10 +26,11 @@ export interface ConnectorTelemetryMeta {
|
||||||
* picked up here, so adding a new integration does NOT require touching
|
* picked up here, so adding a new integration does NOT require touching
|
||||||
* `lib/posthog/events.ts` or per-connector tracking code.
|
* `lib/posthog/events.ts` or per-connector tracking code.
|
||||||
*/
|
*/
|
||||||
const CONNECTOR_TELEMETRY_REGISTRY: ReadonlyMap<
|
let connectorTelemetryRegistry: ReadonlyMap<string, ConnectorTelemetryMeta> | undefined;
|
||||||
string,
|
|
||||||
ConnectorTelemetryMeta
|
function getConnectorTelemetryRegistry(): ReadonlyMap<string, ConnectorTelemetryMeta> {
|
||||||
> = (() => {
|
if (connectorTelemetryRegistry) return connectorTelemetryRegistry;
|
||||||
|
|
||||||
const map = new Map<string, ConnectorTelemetryMeta>();
|
const map = new Map<string, ConnectorTelemetryMeta>();
|
||||||
|
|
||||||
for (const c of OAUTH_CONNECTORS) {
|
for (const c of OAUTH_CONNECTORS) {
|
||||||
|
|
@ -70,18 +66,17 @@ const CONNECTOR_TELEMETRY_REGISTRY: ReadonlyMap<
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return map;
|
connectorTelemetryRegistry = map;
|
||||||
})();
|
return connectorTelemetryRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns telemetry metadata for a connector_type, or a minimal "unknown"
|
* Returns telemetry metadata for a connector_type, or a minimal "unknown"
|
||||||
* record so tracking never no-ops for connectors that exist in the backend
|
* record so tracking never no-ops for connectors that exist in the backend
|
||||||
* but were forgotten in the UI registry.
|
* but were forgotten in the UI registry.
|
||||||
*/
|
*/
|
||||||
export function getConnectorTelemetryMeta(
|
export function getConnectorTelemetryMeta(connectorType: string): ConnectorTelemetryMeta {
|
||||||
connectorType: string,
|
const hit = getConnectorTelemetryRegistry().get(connectorType);
|
||||||
): ConnectorTelemetryMeta {
|
|
||||||
const hit = CONNECTOR_TELEMETRY_REGISTRY.get(connectorType);
|
|
||||||
if (hit) return hit;
|
if (hit) return hit;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -101,34 +96,20 @@ export function getConnectorTelemetryMeta(
|
||||||
* These are used for connectors that were NOT created via MCP OAuth.
|
* These are used for connectors that were NOT created via MCP OAuth.
|
||||||
*/
|
*/
|
||||||
const LEGACY_REAUTH_ENDPOINTS: Partial<Record<string, string>> = {
|
const LEGACY_REAUTH_ENDPOINTS: Partial<Record<string, string>> = {
|
||||||
[EnumConnectorName.LINEAR_CONNECTOR]:
|
[EnumConnectorName.LINEAR_CONNECTOR]: "/api/v1/auth/linear/connector/reauth",
|
||||||
"/api/v1/auth/linear/connector/reauth",
|
[EnumConnectorName.JIRA_CONNECTOR]: "/api/v1/auth/jira/connector/reauth",
|
||||||
[EnumConnectorName.JIRA_CONNECTOR]:
|
[EnumConnectorName.NOTION_CONNECTOR]: "/api/v1/auth/notion/connector/reauth",
|
||||||
"/api/v1/auth/jira/connector/reauth",
|
[EnumConnectorName.GOOGLE_DRIVE_CONNECTOR]: "/api/v1/auth/google/drive/connector/reauth",
|
||||||
[EnumConnectorName.NOTION_CONNECTOR]:
|
[EnumConnectorName.GOOGLE_GMAIL_CONNECTOR]: "/api/v1/auth/google/gmail/connector/reauth",
|
||||||
"/api/v1/auth/notion/connector/reauth",
|
[EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR]: "/api/v1/auth/google/calendar/connector/reauth",
|
||||||
[EnumConnectorName.GOOGLE_DRIVE_CONNECTOR]:
|
[EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
|
||||||
"/api/v1/auth/google/drive/connector/reauth",
|
[EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
|
||||||
[EnumConnectorName.GOOGLE_GMAIL_CONNECTOR]:
|
[EnumConnectorName.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR]: "/api/v1/auth/composio/connector/reauth",
|
||||||
"/api/v1/auth/google/gmail/connector/reauth",
|
[EnumConnectorName.ONEDRIVE_CONNECTOR]: "/api/v1/auth/onedrive/connector/reauth",
|
||||||
[EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR]:
|
[EnumConnectorName.DROPBOX_CONNECTOR]: "/api/v1/auth/dropbox/connector/reauth",
|
||||||
"/api/v1/auth/google/calendar/connector/reauth",
|
[EnumConnectorName.CONFLUENCE_CONNECTOR]: "/api/v1/auth/confluence/connector/reauth",
|
||||||
[EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR]:
|
[EnumConnectorName.TEAMS_CONNECTOR]: "/api/v1/auth/teams/connector/reauth",
|
||||||
"/api/v1/auth/composio/connector/reauth",
|
[EnumConnectorName.DISCORD_CONNECTOR]: "/api/v1/auth/discord/connector/reauth",
|
||||||
[EnumConnectorName.COMPOSIO_GMAIL_CONNECTOR]:
|
|
||||||
"/api/v1/auth/composio/connector/reauth",
|
|
||||||
[EnumConnectorName.COMPOSIO_GOOGLE_CALENDAR_CONNECTOR]:
|
|
||||||
"/api/v1/auth/composio/connector/reauth",
|
|
||||||
[EnumConnectorName.ONEDRIVE_CONNECTOR]:
|
|
||||||
"/api/v1/auth/onedrive/connector/reauth",
|
|
||||||
[EnumConnectorName.DROPBOX_CONNECTOR]:
|
|
||||||
"/api/v1/auth/dropbox/connector/reauth",
|
|
||||||
[EnumConnectorName.CONFLUENCE_CONNECTOR]:
|
|
||||||
"/api/v1/auth/confluence/connector/reauth",
|
|
||||||
[EnumConnectorName.TEAMS_CONNECTOR]:
|
|
||||||
"/api/v1/auth/teams/connector/reauth",
|
|
||||||
[EnumConnectorName.DISCORD_CONNECTOR]:
|
|
||||||
"/api/v1/auth/discord/connector/reauth",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -138,9 +119,7 @@ const LEGACY_REAUTH_ENDPOINTS: Partial<Record<string, string>> = {
|
||||||
* the URL from the service key. Legacy OAuth connectors fall back to the
|
* the URL from the service key. Legacy OAuth connectors fall back to the
|
||||||
* static ``LEGACY_REAUTH_ENDPOINTS`` map.
|
* static ``LEGACY_REAUTH_ENDPOINTS`` map.
|
||||||
*/
|
*/
|
||||||
export function getReauthEndpoint(
|
export function getReauthEndpoint(connector: SearchSourceConnector): string | undefined {
|
||||||
connector: SearchSourceConnector,
|
|
||||||
): string | undefined {
|
|
||||||
const mcpService = connector.config?.mcp_service as string | undefined;
|
const mcpService = connector.config?.mcp_service as string | undefined;
|
||||||
if (mcpService) {
|
if (mcpService) {
|
||||||
return `/api/v1/auth/mcp/${mcpService}/connector/reauth`;
|
return `/api/v1/auth/mcp/${mcpService}/connector/reauth`;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue