mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-25 19:15:18 +02:00
refactor: streamline document mention handling by consolidating atom logic and removing redundant state updates, enhancing code clarity and maintainability in chat components
This commit is contained in:
parent
8d5d8e490c
commit
c1ba3a9b6d
6 changed files with 47 additions and 90 deletions
|
|
@ -169,7 +169,7 @@ export function DocumentsFilters({
|
||||||
className="peer h-9 w-full pl-9 pr-9 text-sm bg-sidebar border-border/60 focus-visible:ring-1 focus-visible:ring-ring/30 select-none focus:select-text"
|
className="peer h-9 w-full pl-9 pr-9 text-sm bg-sidebar border-border/60 focus-visible:ring-1 focus-visible:ring-ring/30 select-none focus:select-text"
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={(e) => onSearch(e.target.value)}
|
onChange={(e) => onSearch(e.target.value)}
|
||||||
placeholder="Search"
|
placeholder="Search docs"
|
||||||
type="text"
|
type="text"
|
||||||
aria-label={t("filter_placeholder")}
|
aria-label={t("filter_placeholder")}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -180,11 +180,10 @@ export default function NewChatPage() {
|
||||||
interruptData: Record<string, unknown>;
|
interruptData: Record<string, unknown>;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
// Get mentioned document IDs from the composer (combines @ mentions + sidebar selections)
|
// Get mentioned document IDs from the composer (derived from @ mentions + sidebar selections)
|
||||||
const mentionedDocumentIds = useAtomValue(mentionedDocumentIdsAtom);
|
const mentionedDocumentIds = useAtomValue(mentionedDocumentIdsAtom);
|
||||||
const mentionedDocuments = useAtomValue(mentionedDocumentsAtom);
|
const mentionedDocuments = useAtomValue(mentionedDocumentsAtom);
|
||||||
const sidebarDocuments = useAtomValue(sidebarSelectedDocumentsAtom);
|
const sidebarDocuments = useAtomValue(sidebarSelectedDocumentsAtom);
|
||||||
const setMentionedDocumentIds = useSetAtom(mentionedDocumentIdsAtom);
|
|
||||||
const setMentionedDocuments = useSetAtom(mentionedDocumentsAtom);
|
const setMentionedDocuments = useSetAtom(mentionedDocumentsAtom);
|
||||||
const setSidebarDocuments = useSetAtom(sidebarSelectedDocumentsAtom);
|
const setSidebarDocuments = useSetAtom(sidebarSelectedDocumentsAtom);
|
||||||
const setMessageDocumentsMap = useSetAtom(messageDocumentsMapAtom);
|
const setMessageDocumentsMap = useSetAtom(messageDocumentsMapAtom);
|
||||||
|
|
@ -278,11 +277,8 @@ export default function NewChatPage() {
|
||||||
setThreadId(null);
|
setThreadId(null);
|
||||||
setCurrentThread(null);
|
setCurrentThread(null);
|
||||||
setMessageThinkingSteps(new Map());
|
setMessageThinkingSteps(new Map());
|
||||||
setMentionedDocumentIds({
|
|
||||||
surfsense_doc_ids: [],
|
|
||||||
document_ids: [],
|
|
||||||
});
|
|
||||||
setMentionedDocuments([]);
|
setMentionedDocuments([]);
|
||||||
|
setSidebarDocuments([]);
|
||||||
setMessageDocumentsMap({});
|
setMessageDocumentsMap({});
|
||||||
clearPlanOwnerRegistry(); // Reset plan ownership for new chat
|
clearPlanOwnerRegistry(); // Reset plan ownership for new chat
|
||||||
closeReportPanel(); // Close report panel when switching chats
|
closeReportPanel(); // Close report panel when switching chats
|
||||||
|
|
@ -347,8 +343,8 @@ export default function NewChatPage() {
|
||||||
}, [
|
}, [
|
||||||
urlChatId,
|
urlChatId,
|
||||||
setMessageDocumentsMap,
|
setMessageDocumentsMap,
|
||||||
setMentionedDocumentIds,
|
|
||||||
setMentionedDocuments,
|
setMentionedDocuments,
|
||||||
|
setSidebarDocuments,
|
||||||
closeReportPanel,
|
closeReportPanel,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -619,10 +615,6 @@ export default function NewChatPage() {
|
||||||
|
|
||||||
// Clear mentioned documents after capturing them
|
// Clear mentioned documents after capturing them
|
||||||
if (hasDocumentIds || hasSurfsenseDocIds) {
|
if (hasDocumentIds || hasSurfsenseDocIds) {
|
||||||
setMentionedDocumentIds({
|
|
||||||
surfsense_doc_ids: [],
|
|
||||||
document_ids: [],
|
|
||||||
});
|
|
||||||
setMentionedDocuments([]);
|
setMentionedDocuments([]);
|
||||||
setSidebarDocuments([]);
|
setSidebarDocuments([]);
|
||||||
}
|
}
|
||||||
|
|
@ -914,7 +906,6 @@ export default function NewChatPage() {
|
||||||
mentionedDocumentIds,
|
mentionedDocumentIds,
|
||||||
mentionedDocuments,
|
mentionedDocuments,
|
||||||
sidebarDocuments,
|
sidebarDocuments,
|
||||||
setMentionedDocumentIds,
|
|
||||||
setMentionedDocuments,
|
setMentionedDocuments,
|
||||||
setSidebarDocuments,
|
setSidebarDocuments,
|
||||||
setMessageDocumentsMap,
|
setMessageDocumentsMap,
|
||||||
|
|
|
||||||
|
|
@ -3,18 +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";
|
||||||
|
|
||||||
/**
|
|
||||||
* Atom to store the IDs of documents mentioned in the current chat composer.
|
|
||||||
* This is used to pass document context to the backend when sending a message.
|
|
||||||
*/
|
|
||||||
export const mentionedDocumentIdsAtom = atom<{
|
|
||||||
surfsense_doc_ids: number[];
|
|
||||||
document_ids: number[];
|
|
||||||
}>({
|
|
||||||
surfsense_doc_ids: [],
|
|
||||||
document_ids: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Atom to store the full document objects mentioned via @-mention chips
|
* Atom to store the full document objects mentioned via @-mention chips
|
||||||
* in the current chat composer. This persists across component remounts.
|
* in the current chat composer. This persists across component remounts.
|
||||||
|
|
@ -27,6 +15,31 @@ export const mentionedDocumentsAtom = atom<Pick<Document, "id" | "title" | "docu
|
||||||
*/
|
*/
|
||||||
export const sidebarSelectedDocumentsAtom = atom<Pick<Document, "id" | "title" | "document_type">[]>([]);
|
export const sidebarSelectedDocumentsAtom = atom<Pick<Document, "id" | "title" | "document_type">[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derived read-only atom that merges @-mention chips and sidebar selections
|
||||||
|
* into a single deduplicated set of document IDs for the backend.
|
||||||
|
*/
|
||||||
|
export const mentionedDocumentIdsAtom = atom((get) => {
|
||||||
|
const chipDocs = get(mentionedDocumentsAtom);
|
||||||
|
const sidebarDocs = get(sidebarSelectedDocumentsAtom);
|
||||||
|
const allDocs = [...chipDocs, ...sidebarDocs];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const deduped = allDocs.filter((d) => {
|
||||||
|
const key = `${d.document_type}:${d.id}`;
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
surfsense_doc_ids: deduped
|
||||||
|
.filter((doc) => doc.document_type === "SURFSENSE_DOCS")
|
||||||
|
.map((doc) => doc.id),
|
||||||
|
document_ids: deduped
|
||||||
|
.filter((doc) => doc.document_type !== "SURFSENSE_DOCS")
|
||||||
|
.map((doc) => doc.id),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simplified document info for display purposes
|
* Simplified document info for display purposes
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom";
|
||||||
import { showCommentsGutterAtom } from "@/atoms/chat/current-thread.atom";
|
import { showCommentsGutterAtom } from "@/atoms/chat/current-thread.atom";
|
||||||
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
|
||||||
import {
|
import {
|
||||||
mentionedDocumentIdsAtom,
|
|
||||||
mentionedDocumentsAtom,
|
mentionedDocumentsAtom,
|
||||||
sidebarSelectedDocumentsAtom,
|
sidebarSelectedDocumentsAtom,
|
||||||
} from "@/atoms/chat/mentioned-documents.atom";
|
} from "@/atoms/chat/mentioned-documents.atom";
|
||||||
|
|
@ -227,14 +226,13 @@ const ThreadWelcome: FC = () => {
|
||||||
const Composer: FC = () => {
|
const Composer: FC = () => {
|
||||||
// Document mention state (atoms persist across component remounts)
|
// Document mention state (atoms persist across component remounts)
|
||||||
const [mentionedDocuments, setMentionedDocuments] = useAtom(mentionedDocumentsAtom);
|
const [mentionedDocuments, setMentionedDocuments] = useAtom(mentionedDocumentsAtom);
|
||||||
const [sidebarDocs, setSidebarDocs] = useAtom(sidebarSelectedDocumentsAtom);
|
const setSidebarDocs = useSetAtom(sidebarSelectedDocumentsAtom);
|
||||||
const [showDocumentPopover, setShowDocumentPopover] = useState(false);
|
const [showDocumentPopover, setShowDocumentPopover] = useState(false);
|
||||||
const [mentionQuery, setMentionQuery] = useState("");
|
const [mentionQuery, setMentionQuery] = useState("");
|
||||||
const editorRef = useRef<InlineMentionEditorRef>(null);
|
const editorRef = useRef<InlineMentionEditorRef>(null);
|
||||||
const editorContainerRef = useRef<HTMLDivElement>(null);
|
const editorContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const documentPickerRef = useRef<DocumentMentionPickerRef>(null);
|
const documentPickerRef = useRef<DocumentMentionPickerRef>(null);
|
||||||
const { search_space_id, chat_id } = useParams();
|
const { search_space_id, chat_id } = useParams();
|
||||||
const setMentionedDocumentIds = useSetAtom(mentionedDocumentIdsAtom);
|
|
||||||
const composerRuntime = useComposerRuntime();
|
const composerRuntime = useComposerRuntime();
|
||||||
const hasAutoFocusedRef = useRef(false);
|
const hasAutoFocusedRef = useRef(false);
|
||||||
|
|
||||||
|
|
@ -309,26 +307,6 @@ const Composer: FC = () => {
|
||||||
}
|
}
|
||||||
}, [isThreadEmpty]);
|
}, [isThreadEmpty]);
|
||||||
|
|
||||||
// Combine sidebar selections + @-mention chips → single ID atom for the backend
|
|
||||||
useEffect(() => {
|
|
||||||
const allDocs = [...mentionedDocuments, ...sidebarDocs];
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const deduped = allDocs.filter((d) => {
|
|
||||||
const key = `${d.document_type}:${d.id}`;
|
|
||||||
if (seen.has(key)) return false;
|
|
||||||
seen.add(key);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
setMentionedDocumentIds({
|
|
||||||
surfsense_doc_ids: deduped
|
|
||||||
.filter((doc) => doc.document_type === "SURFSENSE_DOCS")
|
|
||||||
.map((doc) => doc.id),
|
|
||||||
document_ids: deduped
|
|
||||||
.filter((doc) => doc.document_type !== "SURFSENSE_DOCS")
|
|
||||||
.map((doc) => doc.id),
|
|
||||||
});
|
|
||||||
}, [mentionedDocuments, sidebarDocs, setMentionedDocumentIds]);
|
|
||||||
|
|
||||||
// Sync editor text with assistant-ui composer runtime
|
// Sync editor text with assistant-ui composer runtime
|
||||||
const handleEditorChange = useCallback(
|
const handleEditorChange = useCallback(
|
||||||
(text: string) => {
|
(text: string) => {
|
||||||
|
|
@ -391,10 +369,6 @@ const Composer: FC = () => {
|
||||||
editorRef.current?.clear();
|
editorRef.current?.clear();
|
||||||
setMentionedDocuments([]);
|
setMentionedDocuments([]);
|
||||||
setSidebarDocs([]);
|
setSidebarDocs([]);
|
||||||
setMentionedDocumentIds({
|
|
||||||
surfsense_doc_ids: [],
|
|
||||||
document_ids: [],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
showDocumentPopover,
|
showDocumentPopover,
|
||||||
|
|
@ -403,29 +377,17 @@ const Composer: FC = () => {
|
||||||
composerRuntime,
|
composerRuntime,
|
||||||
setMentionedDocuments,
|
setMentionedDocuments,
|
||||||
setSidebarDocs,
|
setSidebarDocs,
|
||||||
setMentionedDocumentIds,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Remove document from mentions and sync IDs to atom
|
|
||||||
const handleDocumentRemove = useCallback(
|
const handleDocumentRemove = useCallback(
|
||||||
(docId: number, docType?: string) => {
|
(docId: number, docType?: string) => {
|
||||||
setMentionedDocuments((prev) => {
|
setMentionedDocuments((prev) =>
|
||||||
const updated = prev.filter((doc) => !(doc.id === docId && doc.document_type === docType));
|
prev.filter((doc) => !(doc.id === docId && doc.document_type === docType))
|
||||||
setMentionedDocumentIds({
|
);
|
||||||
surfsense_doc_ids: updated
|
|
||||||
.filter((doc) => doc.document_type === "SURFSENSE_DOCS")
|
|
||||||
.map((doc) => doc.id),
|
|
||||||
document_ids: updated
|
|
||||||
.filter((doc) => doc.document_type !== "SURFSENSE_DOCS")
|
|
||||||
.map((doc) => doc.id),
|
|
||||||
});
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[setMentionedDocuments, setMentionedDocumentIds]
|
[setMentionedDocuments]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add selected documents from picker, insert chips, and sync IDs to atom
|
|
||||||
const handleDocumentsMention = useCallback(
|
const handleDocumentsMention = useCallback(
|
||||||
(documents: Pick<Document, "id" | "title" | "document_type">[]) => {
|
(documents: Pick<Document, "id" | "title" | "document_type">[]) => {
|
||||||
const existingKeys = new Set(mentionedDocuments.map((d) => `${d.document_type}:${d.id}`));
|
const existingKeys = new Set(mentionedDocuments.map((d) => `${d.document_type}:${d.id}`));
|
||||||
|
|
@ -442,21 +404,12 @@ const Composer: FC = () => {
|
||||||
const uniqueNewDocs = documents.filter(
|
const uniqueNewDocs = documents.filter(
|
||||||
(doc) => !existingKeySet.has(`${doc.document_type}:${doc.id}`)
|
(doc) => !existingKeySet.has(`${doc.document_type}:${doc.id}`)
|
||||||
);
|
);
|
||||||
const updated = [...prev, ...uniqueNewDocs];
|
return [...prev, ...uniqueNewDocs];
|
||||||
setMentionedDocumentIds({
|
|
||||||
surfsense_doc_ids: updated
|
|
||||||
.filter((doc) => doc.document_type === "SURFSENSE_DOCS")
|
|
||||||
.map((doc) => doc.id),
|
|
||||||
document_ids: updated
|
|
||||||
.filter((doc) => doc.document_type !== "SURFSENSE_DOCS")
|
|
||||||
.map((doc) => doc.id),
|
|
||||||
});
|
|
||||||
return updated;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setMentionQuery("");
|
setMentionQuery("");
|
||||||
},
|
},
|
||||||
[mentionedDocuments, setMentionedDocuments, setMentionedDocumentIds]
|
[mentionedDocuments, setMentionedDocuments]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -178,12 +178,12 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="w-[280px] md:w-[320px] p-0 rounded-lg shadow-lg border-border/60"
|
className="w-[280px] md:w-[320px] p-0 rounded-lg shadow-lg border-border/60 dark:bg-muted dark:border dark:border-neutral-700 select-none"
|
||||||
align="end"
|
align="end"
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<div className="p-1.5 space-y-1 select-none">
|
<div className="p-1.5 space-y-1">
|
||||||
{/* Visibility Options */}
|
{/* Visibility Options */}
|
||||||
{visibilityOptions.map((option) => {
|
{visibilityOptions.map((option) => {
|
||||||
const isSelected = currentVisibility === option.value;
|
const isSelected = currentVisibility === option.value;
|
||||||
|
|
@ -196,27 +196,27 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
||||||
onClick={() => handleVisibilityChange(option.value)}
|
onClick={() => handleVisibilityChange(option.value)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full flex items-center gap-2.5 px-2.5 py-2 rounded-md transition-all",
|
"w-full flex items-center gap-2.5 px-2.5 py-2 rounded-md transition-all",
|
||||||
"hover:bg-accent/50 cursor-pointer",
|
"hover:bg-accent/50 dark:hover:bg-white/10 cursor-pointer",
|
||||||
"focus:outline-none",
|
"focus:outline-none",
|
||||||
isSelected && "bg-accent/80"
|
isSelected && "bg-accent/80 dark:bg-white/10"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-7 rounded-md shrink-0 grid place-items-center",
|
"size-7 rounded-md shrink-0 grid place-items-center",
|
||||||
isSelected ? "bg-primary/10" : "bg-muted"
|
isSelected ? "bg-primary/10 dark:bg-white/10" : "bg-muted dark:bg-white/5"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-4 block",
|
"size-4 block",
|
||||||
isSelected ? "text-primary" : "text-muted-foreground"
|
isSelected ? "text-primary dark:text-white" : "text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 text-left min-w-0">
|
<div className="flex-1 text-left min-w-0">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className={cn("text-sm font-medium", isSelected && "text-primary")}>
|
<span className={cn("text-sm font-medium", isSelected && "text-primary dark:text-white")}>
|
||||||
{option.label}
|
{option.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -231,7 +231,7 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
||||||
{canCreatePublicLink && (
|
{canCreatePublicLink && (
|
||||||
<>
|
<>
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="border-t border-border my-1" />
|
<div className="border-t border-border dark:border-white/5 my-1" />
|
||||||
|
|
||||||
{/* Public Link Option */}
|
{/* Public Link Option */}
|
||||||
<button
|
<button
|
||||||
|
|
@ -240,12 +240,12 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
||||||
disabled={isCreatingSnapshot}
|
disabled={isCreatingSnapshot}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full flex items-center gap-2.5 px-2.5 py-2 rounded-md transition-all",
|
"w-full flex items-center gap-2.5 px-2.5 py-2 rounded-md transition-all",
|
||||||
"hover:bg-accent/50 cursor-pointer",
|
"hover:bg-accent/50 dark:hover:bg-white/10 cursor-pointer",
|
||||||
"focus:outline-none",
|
"focus:outline-none",
|
||||||
"disabled:opacity-50 disabled:cursor-not-allowed"
|
"disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="size-7 rounded-md shrink-0 grid place-items-center bg-muted">
|
<div className="size-7 rounded-md shrink-0 grid place-items-center bg-muted dark:bg-white/5">
|
||||||
<Earth className="size-4 block text-muted-foreground" />
|
<Earth className="size-4 block text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 text-left min-w-0">
|
<div className="flex-1 text-left min-w-0">
|
||||||
|
|
|
||||||
|
|
@ -264,7 +264,7 @@ export function ModelSelector({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="h-4 w-px bg-border/60 mx-0.5" />
|
<div className="h-4 w-px bg-border/60 dark:bg-white/10 mx-0.5" />
|
||||||
|
|
||||||
{/* Image section */}
|
{/* Image section */}
|
||||||
{currentImageConfig ? (
|
{currentImageConfig ? (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue