refactor: replace DocumentsDataTable with DocumentMentionPicker for improved document selection

- Introduced DocumentMentionPicker component to enhance document selection experience in the chat interface.
- Updated InlineMentionEditor and Composer components to utilize the new DocumentMentionPicker.
- Removed the deprecated DocumentsDataTable component to streamline the codebase and improve maintainability.
- Enhanced type safety and validation in document handling logic.
This commit is contained in:
Anish Sarkar 2025-12-26 00:41:14 +05:30
parent 9bc3f193c3
commit 2fdf567b71
5 changed files with 327 additions and 299 deletions

View file

@ -45,6 +45,27 @@ interface InlineMentionEditorProps {
const CHIP_DATA_ATTR = "data-mention-chip";
const CHIP_ID_ATTR = "data-mention-id";
/**
* Type guard to check if a node is a chip element
*/
function isChipElement(node: Node | null): node is HTMLSpanElement {
return (
node !== null &&
node.nodeType === Node.ELEMENT_NODE &&
(node as Element).hasAttribute(CHIP_DATA_ATTR)
);
}
/**
* Safely parse chip ID from element attribute
*/
function getChipId(element: Element): number | null {
const idStr = element.getAttribute(CHIP_ID_ATTR);
if (!idStr) return null;
const id = parseInt(idStr, 10);
return Number.isNaN(id) ? null : id;
}
export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMentionEditorProps>(
(
{
@ -177,6 +198,12 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
(doc: Document) => {
if (!editorRef.current) return;
// Validate required fields for type safety
if (typeof doc.id !== "number" || typeof doc.title !== "string") {
console.warn("[InlineMentionEditor] Invalid document passed to insertDocumentChip:", doc);
return;
}
const mentionDoc: MentionedDocument = {
id: doc.id,
title: doc.title,
@ -381,19 +408,21 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
const offset = range.startOffset;
if (node.nodeType === Node.TEXT_NODE && offset === 0) {
// Check previous sibling
// Check previous sibling using type guard
const prevSibling = node.previousSibling;
if (prevSibling && (prevSibling as Element).hasAttribute?.(CHIP_DATA_ATTR)) {
if (isChipElement(prevSibling)) {
e.preventDefault();
const chipId = Number((prevSibling as Element).getAttribute(CHIP_ID_ATTR));
prevSibling.parentNode?.removeChild(prevSibling);
setMentionedDocs((prev) => {
const next = new Map(prev);
next.delete(chipId);
return next;
});
// Notify parent that a document was removed
onDocumentRemove?.(chipId);
const chipId = getChipId(prevSibling);
if (chipId !== null) {
prevSibling.remove();
setMentionedDocs((prev) => {
const next = new Map(prev);
next.delete(chipId);
return next;
});
// Notify parent that a document was removed
onDocumentRemove?.(chipId);
}
return;
}
// Check if we're about to delete @ at the start
@ -414,19 +443,21 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
}, 0);
}
} else if (node.nodeType === Node.ELEMENT_NODE && offset > 0) {
// Check if previous child is a chip
// Check if previous child is a chip using type guard
const prevChild = (node as Element).childNodes[offset - 1];
if (prevChild && (prevChild as Element).hasAttribute?.(CHIP_DATA_ATTR)) {
if (isChipElement(prevChild)) {
e.preventDefault();
const chipId = Number((prevChild as Element).getAttribute(CHIP_ID_ATTR));
prevChild.parentNode?.removeChild(prevChild);
setMentionedDocs((prev) => {
const next = new Map(prev);
next.delete(chipId);
return next;
});
// Notify parent that a document was removed
onDocumentRemove?.(chipId);
const chipId = getChipId(prevChild);
if (chipId !== null) {
prevChild.remove();
setMentionedDocs((prev) => {
const next = new Map(prev);
next.delete(chipId);
return next;
});
// Notify parent that a document was removed
onDocumentRemove?.(chipId);
}
}
}
}

View file

@ -72,9 +72,9 @@ import { MarkdownText } from "@/components/assistant-ui/markdown-text";
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import {
DocumentsDataTable,
type DocumentsDataTableRef,
} from "@/components/new-chat/DocumentsDataTable";
DocumentMentionPicker,
type DocumentMentionPickerRef,
} from "@/components/new-chat/DocumentMentionPicker";
import {
ChainOfThought,
ChainOfThoughtContent,
@ -404,7 +404,7 @@ const Composer: FC = () => {
const [mentionQuery, setMentionQuery] = useState("");
const editorRef = useRef<InlineMentionEditorRef>(null);
const editorContainerRef = useRef<HTMLDivElement>(null);
const documentPickerRef = useRef<DocumentsDataTableRef>(null);
const documentPickerRef = useRef<DocumentMentionPickerRef>(null);
const { search_space_id } = useParams();
const setMentionedDocumentIds = useSetAtom(mentionedDocumentIdsAtom);
const composerRuntime = useComposerRuntime();
@ -598,7 +598,7 @@ const Composer: FC = () => {
: "50%",
}}
>
<DocumentsDataTable
<DocumentMentionPicker
ref={documentPickerRef}
searchSpaceId={Number(search_space_id)}
onSelectionChange={handleDocumentsMention}