Merge pull request #1536 from CREDO23/feature-mention-chat-in-chat

[Feat] Chat : Reference past chats via @-mention as read-only context
This commit is contained in:
Rohan Verma 2026-06-25 13:32:25 -07:00 committed by GitHub
commit 96e42a1003
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 835 additions and 74 deletions

View file

@ -1,6 +1,11 @@
"use client";
import { Folder as FolderIcon, Plug as PlugIcon, X as XIcon } from "lucide-react";
import {
Folder as FolderIcon,
MessageSquare as MessageSquareIcon,
Plug as PlugIcon,
X as XIcon,
} from "lucide-react";
import type { NodeEntry, TElement } from "platejs";
import type { PlateElementProps } from "platejs/react";
import {
@ -26,7 +31,7 @@ import type { Document } from "@/contracts/types/document.types";
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
import { cn } from "@/lib/utils";
export type MentionKind = "doc" | "folder" | "connector";
export type MentionKind = "doc" | "folder" | "connector" | "thread";
export interface MentionedDocument {
id: number;
@ -165,6 +170,7 @@ const MentionElement: FC<PlateElementProps<MentionElementNode>> = ({
const isFolder = element.kind === "folder";
const isConnector = element.kind === "connector";
const isThread = element.kind === "thread";
const ctx = useContext(MentionEditorContext);
return (
@ -175,6 +181,8 @@ const MentionElement: FC<PlateElementProps<MentionElementNode>> = ({
<span className="flex items-center justify-center transition-opacity group-hover:opacity-0">
{isFolder ? (
<FolderIcon className="h-3 w-3" />
) : isThread ? (
<MessageSquareIcon className="h-3 w-3" />
) : isConnector ? (
(getConnectorIcon(
element.connector_type ?? element.document_type ?? "UNKNOWN",

View file

@ -38,6 +38,7 @@ import { currentThreadAtom } from "@/atoms/chat/current-thread.atom";
import {
type MentionedDocumentInfo,
mentionedDocumentsAtom,
submittedMentionsAtom,
} from "@/atoms/chat/mentioned-documents.atom";
import { pendingUserImageDataUrlsAtom } from "@/atoms/chat/pending-user-images.atom";
import {
@ -446,6 +447,7 @@ const ClipboardChip: FC<{ text: string; onDismiss: () => void }> = ({ text, onDi
const Composer: FC = () => {
const [mentionedDocuments, setMentionedDocuments] = useAtom(mentionedDocumentsAtom);
const setSubmittedMentions = useSetAtom(submittedMentionsAtom);
const [showDocumentPopover, setShowDocumentPopover] = useState(false);
const [showPromptPicker, setShowPromptPicker] = useState(false);
const [mentionQuery, setMentionQuery] = useState("");
@ -575,6 +577,13 @@ const Composer: FC = () => {
kind: "folder",
};
}
if (d.kind === "thread") {
return {
id: d.id,
title: d.title,
kind: "thread",
};
}
return {
id: d.id,
title: d.title,
@ -770,6 +779,10 @@ const Composer: FC = () => {
setClipboardInitialText(undefined);
}
// Capture chips before the reset below clears the live atom, so
// the async ``onNew`` still sees them.
setSubmittedMentions(mentionedDocuments);
aui.composer().send();
editorRef.current?.clear();
setIsComposerInputEmpty(true);
@ -781,6 +794,8 @@ const Composer: FC = () => {
isBlockedByOtherUser,
clipboardInitialText,
aui,
mentionedDocuments,
setSubmittedMentions,
setMentionedDocuments,
]);
@ -788,7 +803,7 @@ const Composer: FC = () => {
(
docId: number,
docType?: string,
kind?: "doc" | "folder" | "connector",
kind?: "doc" | "folder" | "connector" | "thread",
connectorType?: string
) => {
setMentionedDocuments((prev) => {
@ -876,6 +891,8 @@ const Composer: FC = () => {
<DocumentMentionPicker
ref={documentPickerRef}
searchSpaceId={Number(search_space_id)}
enableChatMentions
currentChatId={threadId}
onSelectionChange={handleDocumentsMention}
onDone={() => {
setShowDocumentPopover(false);

View file

@ -6,9 +6,16 @@ import {
useMessagePartText,
} from "@assistant-ui/react";
import { useAtomValue, useSetAtom } from "jotai";
import { CheckIcon, CopyIcon, Folder as FolderIcon, Pencil, Plug } from "lucide-react";
import {
CheckIcon,
CopyIcon,
Folder as FolderIcon,
MessageSquare,
Pencil,
Plug,
} from "lucide-react";
import Image from "next/image";
import { useParams } from "next/navigation";
import { useParams, useRouter } from "next/navigation";
import { type FC, useCallback, useState } from "react";
import { toast } from "sonner";
import { currentThreadAtom } from "@/atoms/chat/current-thread.atom";
@ -66,6 +73,7 @@ const UserTextPart: FC = () => {
const messageDocumentsMap = useAtomValue(messageDocumentsMapAtom);
const mentionedDocs = (messageId ? messageDocumentsMap[messageId] : undefined) ?? [];
const openEditorPanel = useSetAtom(openEditorPanelAtom);
const router = useRouter();
const params = useParams();
const searchSpaceIdParam = params?.search_space_id;
const parsedSearchSpaceId = Array.isArray(searchSpaceIdParam)
@ -91,6 +99,17 @@ const UserTextPart: FC = () => {
[openEditorPanel, resolvedSearchSpaceId]
);
const handleOpenThread = useCallback(
(threadId: number) => {
if (!resolvedSearchSpaceId) {
toast.error("Cannot open chat outside a search space.");
return;
}
router.push(`/dashboard/${resolvedSearchSpaceId}/new-chat/${threadId}`);
},
[resolvedSearchSpaceId, router]
);
const segments = parseMentionSegments(text, mentionedDocs);
return (
@ -101,8 +120,11 @@ const UserTextPart: FC = () => {
}
const isFolder = segment.doc.kind === "folder";
const isConnector = segment.doc.kind === "connector";
const isThread = segment.doc.kind === "thread";
const icon = isFolder ? (
<FolderIcon className="size-3.5" />
) : isThread ? (
<MessageSquare className="size-3.5" />
) : isConnector ? (
(getConnectorIcon(segment.doc.connector_type, "size-3.5") ?? (
<Plug className="size-3.5" />
@ -118,14 +140,18 @@ const UserTextPart: FC = () => {
tooltip={
isFolder
? `Folder: ${segment.doc.title}`
: isConnector
? `Connector account: ${segment.doc.title}`
: segment.doc.title
: isThread
? `Chat: ${segment.doc.title}`
: isConnector
? `Connector account: ${segment.doc.title}`
: segment.doc.title
}
onClick={
isFolder || isConnector
? undefined
: () => handleOpenDoc(segment.doc.id, segment.doc.title)
isThread
? () => handleOpenThread(segment.doc.id)
: isFolder || isConnector
? undefined
: () => handleOpenDoc(segment.doc.id, segment.doc.title)
}
className="mx-0.5"
/>