mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-25 19:15:18 +02:00
feat: enhance document mention editor and picker for better user experience
- Added document type icons in InlineMentionEditor for improved visual context. - Updated DocumentMentionPicker to include client-side filtering, reducing false positives in search results. - Enhanced loading state management by incorporating fetching indicators for better UX during data retrieval.
This commit is contained in:
parent
720c13667e
commit
0b5568d7ab
2 changed files with 35 additions and 6 deletions
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from "react";
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
import type { Document } from "@/contracts/types/document.types";
|
||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface MentionedDocument {
|
||||
|
|
@ -166,12 +167,19 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
|||
chip.setAttribute(CHIP_DOCTYPE_ATTR, doc.document_type ?? "UNKNOWN");
|
||||
chip.contentEditable = "false";
|
||||
chip.className =
|
||||
"inline-flex items-center gap-0.5 mx-0.5 pl-1 pr-0.5 py-0.5 rounded bg-primary/10 text-xs font-bold text-primary border border-primary/10 select-none";
|
||||
"inline-flex items-center gap-1 mx-0.5 pl-1 pr-0.5 py-0.5 rounded bg-primary/10 text-xs font-bold text-primary border border-primary/10 select-none";
|
||||
chip.style.userSelect = "none";
|
||||
chip.style.verticalAlign = "baseline";
|
||||
|
||||
// Add document type icon
|
||||
const iconSpan = document.createElement("span");
|
||||
iconSpan.className = "shrink-0 flex items-center text-muted-foreground";
|
||||
iconSpan.innerHTML = ReactDOMServer.renderToString(
|
||||
getConnectorIcon(doc.document_type ?? "UNKNOWN", "h-3 w-3")
|
||||
);
|
||||
|
||||
const titleSpan = document.createElement("span");
|
||||
titleSpan.className = "max-w-[80px] truncate";
|
||||
titleSpan.className = "max-w-[120px] truncate";
|
||||
titleSpan.textContent = doc.title;
|
||||
titleSpan.title = doc.title;
|
||||
|
||||
|
|
@ -197,6 +205,7 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
|||
focusAtEnd();
|
||||
};
|
||||
|
||||
chip.appendChild(iconSpan);
|
||||
chip.appendChild(titleSpan);
|
||||
chip.appendChild(removeBtn);
|
||||
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ export const DocumentMentionPicker = forwardRef<
|
|||
// Use the new lightweight endpoint for document title search
|
||||
// TanStack Query provides signal for automatic request cancellation
|
||||
// keepPreviousData: shows old results while fetching new ones (no spinner flicker)
|
||||
const { data: titleSearchResults, isLoading: isTitleSearchLoading } = useQuery({
|
||||
const { data: titleSearchResults, isLoading: isTitleSearchLoading, isFetching: isTitleSearchFetching } = useQuery({
|
||||
queryKey: ["document-titles", titleSearchParams],
|
||||
queryFn: ({ signal }) =>
|
||||
documentsApiService.searchDocumentTitles({ queryParams: titleSearchParams }, signal),
|
||||
|
|
@ -168,7 +168,7 @@ export const DocumentMentionPicker = forwardRef<
|
|||
// Use query for fetching first page of SurfSense docs
|
||||
// TanStack Query provides signal for automatic request cancellation
|
||||
// keepPreviousData: shows old results while fetching new ones (no spinner flicker)
|
||||
const { data: surfsenseDocs, isLoading: isSurfsenseDocsLoading } = useQuery({
|
||||
const { data: surfsenseDocs, isLoading: isSurfsenseDocsLoading, isFetching: isSurfsenseDocsFetching } = useQuery({
|
||||
queryKey: ["surfsense-docs-mention", debouncedSearch, isSearchValid],
|
||||
queryFn: ({ signal }) =>
|
||||
documentsApiService.getSurfsenseDocs({ queryParams: surfsenseDocsQueryParams }, signal),
|
||||
|
|
@ -177,6 +177,16 @@ export const DocumentMentionPicker = forwardRef<
|
|||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
// Client-side filter to verify search term is actually in the title (handles backend fuzzy false positives)
|
||||
const filterBySearchTerm = useCallback(
|
||||
(docs: Pick<Document, "id" | "title" | "document_type">[]) => {
|
||||
if (!isSearchValid) return docs; // No filtering when not searching
|
||||
const searchLower = debouncedSearch.trim().toLowerCase();
|
||||
return docs.filter((doc) => doc.title.toLowerCase().includes(searchLower));
|
||||
},
|
||||
[debouncedSearch, isSearchValid]
|
||||
);
|
||||
|
||||
// Update accumulated documents when first page loads - combine both sources
|
||||
useEffect(() => {
|
||||
if (currentPage === 0) {
|
||||
|
|
@ -199,9 +209,10 @@ export const DocumentMentionPicker = forwardRef<
|
|||
setHasMore(titleSearchResults.has_more);
|
||||
}
|
||||
|
||||
setAccumulatedDocuments(combinedDocs);
|
||||
// Apply client-side filter to remove fuzzy false positives
|
||||
setAccumulatedDocuments(filterBySearchTerm(combinedDocs));
|
||||
}
|
||||
}, [titleSearchResults, surfsenseDocs, currentPage]);
|
||||
}, [titleSearchResults, surfsenseDocs, currentPage, filterBySearchTerm]);
|
||||
|
||||
// Function to load next page using lightweight endpoint
|
||||
const loadNextPage = useCallback(async () => {
|
||||
|
|
@ -246,10 +257,14 @@ export const DocumentMentionPicker = forwardRef<
|
|||
|
||||
const actualDocuments = accumulatedDocuments;
|
||||
const actualLoading = (isTitleSearchLoading || isSurfsenseDocsLoading) && currentPage === 0;
|
||||
const isFetchingResults = isTitleSearchFetching || isSurfsenseDocsFetching;
|
||||
|
||||
// Show hint when search is too short
|
||||
const showSearchHint = shouldSearch && !isSearchValid;
|
||||
|
||||
// Hide popup entirely when user is searching and no documents match (only after fetch completes)
|
||||
const hasNoSearchResults = isSearchValid && !actualLoading && !isFetchingResults && actualDocuments.length === 0;
|
||||
|
||||
// Split documents into SurfSense docs and user docs for grouped rendering
|
||||
const surfsenseDocsList = useMemo(
|
||||
() => actualDocuments.filter((doc) => doc.document_type === "SURFSENSE_DOCS"),
|
||||
|
|
@ -345,6 +360,11 @@ export const DocumentMentionPicker = forwardRef<
|
|||
[selectableDocuments, highlightedIndex, handleSelectDocument, onDone]
|
||||
);
|
||||
|
||||
// Don't show popup when user is searching and no documents match
|
||||
if (hasNoSearchResults) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col w-[280px] sm:w-[320px] bg-popover rounded-lg"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue