mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +02:00
Merge remote-tracking branch 'upstream/dev' into feat/replace-logs
This commit is contained in:
commit
99bd2df463
59 changed files with 2788 additions and 1579 deletions
|
|
@ -53,7 +53,14 @@ export const Composer: FC = () => {
|
|||
|
||||
// Sync mentioned document IDs to atom for use in chat request
|
||||
useEffect(() => {
|
||||
setMentionedDocumentIds(mentionedDocuments.map((doc) => doc.id));
|
||||
setMentionedDocumentIds({
|
||||
surfsense_doc_ids: mentionedDocuments
|
||||
.filter((doc) => doc.document_type === "SURFSENSE_DOCS")
|
||||
.map((doc) => doc.id),
|
||||
document_ids: mentionedDocuments
|
||||
.filter((doc) => doc.document_type !== "SURFSENSE_DOCS")
|
||||
.map((doc) => doc.id),
|
||||
});
|
||||
}, [mentionedDocuments, setMentionedDocumentIds]);
|
||||
|
||||
// Handle text change from inline editor - sync with assistant-ui composer
|
||||
|
|
@ -119,7 +126,10 @@ export const Composer: FC = () => {
|
|||
// Clear the editor after sending
|
||||
editorRef.current?.clear();
|
||||
setMentionedDocuments([]);
|
||||
setMentionedDocumentIds([]);
|
||||
setMentionedDocumentIds({
|
||||
surfsense_doc_ids: [],
|
||||
document_ids: [],
|
||||
});
|
||||
}
|
||||
}, [
|
||||
showDocumentPopover,
|
||||
|
|
@ -129,41 +139,52 @@ export const Composer: FC = () => {
|
|||
setMentionedDocumentIds,
|
||||
]);
|
||||
|
||||
// Handle document removal from inline editor
|
||||
const handleDocumentRemove = useCallback(
|
||||
(docId: number) => {
|
||||
(docId: number, docType?: string) => {
|
||||
setMentionedDocuments((prev) => {
|
||||
const updated = prev.filter((doc) => doc.id !== docId);
|
||||
// Immediately sync document IDs to avoid race conditions
|
||||
setMentionedDocumentIds(updated.map((doc) => doc.id));
|
||||
const updated = 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]
|
||||
);
|
||||
|
||||
// Handle document selection from picker
|
||||
const handleDocumentsMention = useCallback(
|
||||
(documents: Document[]) => {
|
||||
// Insert chips into the inline editor for each new document
|
||||
const existingIds = new Set(mentionedDocuments.map((d) => d.id));
|
||||
const newDocs = documents.filter((doc) => !existingIds.has(doc.id));
|
||||
(documents: Pick<Document, "id" | "title" | "document_type">[]) => {
|
||||
const existingKeys = new Set(mentionedDocuments.map((d) => `${d.document_type}:${d.id}`));
|
||||
const newDocs = documents.filter(
|
||||
(doc) => !existingKeys.has(`${doc.document_type}:${doc.id}`)
|
||||
);
|
||||
|
||||
for (const doc of newDocs) {
|
||||
editorRef.current?.insertDocumentChip(doc);
|
||||
}
|
||||
|
||||
// Update mentioned documents state
|
||||
setMentionedDocuments((prev) => {
|
||||
const existingIdSet = new Set(prev.map((d) => d.id));
|
||||
const uniqueNewDocs = documents.filter((doc) => !existingIdSet.has(doc.id));
|
||||
const existingKeySet = new Set(prev.map((d) => `${d.document_type}:${d.id}`));
|
||||
const uniqueNewDocs = documents.filter(
|
||||
(doc) => !existingKeySet.has(`${doc.document_type}:${doc.id}`)
|
||||
);
|
||||
const updated = [...prev, ...uniqueNewDocs];
|
||||
// Immediately sync document IDs to avoid race conditions
|
||||
setMentionedDocumentIds(updated.map((doc) => doc.id));
|
||||
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;
|
||||
});
|
||||
|
||||
// Reset mention query but keep popover open for more selections
|
||||
setMentionQuery("");
|
||||
},
|
||||
[mentionedDocuments, setMentionedDocuments, setMentionedDocumentIds]
|
||||
|
|
|
|||
|
|
@ -86,7 +86,6 @@ const DocumentUploadPopupContent: FC<{
|
|||
}> = ({ isOpen, onOpenChange }) => {
|
||||
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
||||
const router = useRouter();
|
||||
const [isAccordionExpanded, setIsAccordionExpanded] = useState(false);
|
||||
|
||||
if (!searchSpaceId) return null;
|
||||
|
||||
|
|
@ -118,19 +117,16 @@ const DocumentUploadPopupContent: FC<{
|
|||
|
||||
{/* Scrollable Content */}
|
||||
<div className="flex-1 min-h-0 relative overflow-hidden">
|
||||
<div className={`h-full ${isAccordionExpanded ? "overflow-y-auto" : ""}`}>
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="px-6 sm:px-12 pb-5 sm:pb-16">
|
||||
<DocumentUploadTab
|
||||
searchSpaceId={searchSpaceId}
|
||||
onSuccess={handleSuccess}
|
||||
onAccordionStateChange={setIsAccordionExpanded}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Bottom fade shadow - only show when scrolling */}
|
||||
{isAccordionExpanded && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-2 sm:h-7 bg-gradient-to-t from-muted via-muted/80 to-transparent pointer-events-none z-10" />
|
||||
)}
|
||||
{/* Bottom fade shadow */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-2 sm:h-7 bg-gradient-to-t from-muted via-muted/80 to-transparent pointer-events-none z-10" />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export interface InlineMentionEditorRef {
|
|||
clear: () => void;
|
||||
getText: () => string;
|
||||
getMentionedDocuments: () => MentionedDocument[];
|
||||
insertDocumentChip: (doc: Document) => void;
|
||||
insertDocumentChip: (doc: Pick<Document, "id" | "title" | "document_type">) => void;
|
||||
}
|
||||
|
||||
interface InlineMentionEditorProps {
|
||||
|
|
@ -34,7 +34,7 @@ interface InlineMentionEditorProps {
|
|||
onMentionClose?: () => void;
|
||||
onSubmit?: () => void;
|
||||
onChange?: (text: string, docs: MentionedDocument[]) => void;
|
||||
onDocumentRemove?: (docId: number) => void;
|
||||
onDocumentRemove?: (docId: number, docType?: string) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
|
|
@ -44,6 +44,7 @@ interface InlineMentionEditorProps {
|
|||
// Unique data attribute to identify chip elements
|
||||
const CHIP_DATA_ATTR = "data-mention-chip";
|
||||
const CHIP_ID_ATTR = "data-mention-id";
|
||||
const CHIP_DOCTYPE_ATTR = "data-mention-doctype";
|
||||
|
||||
/**
|
||||
* Type guard to check if a node is a chip element
|
||||
|
|
@ -66,6 +67,13 @@ function getChipId(element: Element): number | null {
|
|||
return Number.isNaN(id) ? null : id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chip document type from element attribute
|
||||
*/
|
||||
function getChipDocType(element: Element): string {
|
||||
return element.getAttribute(CHIP_DOCTYPE_ATTR) ?? "UNKNOWN";
|
||||
}
|
||||
|
||||
export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMentionEditorProps>(
|
||||
(
|
||||
{
|
||||
|
|
@ -84,15 +92,17 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
|||
) => {
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const [isEmpty, setIsEmpty] = useState(true);
|
||||
const [mentionedDocs, setMentionedDocs] = useState<Map<number, MentionedDocument>>(
|
||||
() => new Map(initialDocuments.map((d) => [d.id, d]))
|
||||
const [mentionedDocs, setMentionedDocs] = useState<Map<string, MentionedDocument>>(
|
||||
() => new Map(initialDocuments.map((d) => [`${d.document_type ?? "UNKNOWN"}:${d.id}`, d]))
|
||||
);
|
||||
const isComposingRef = useRef(false);
|
||||
|
||||
// Sync initial documents
|
||||
useEffect(() => {
|
||||
if (initialDocuments.length > 0) {
|
||||
setMentionedDocs(new Map(initialDocuments.map((d) => [d.id, d])));
|
||||
setMentionedDocs(
|
||||
new Map(initialDocuments.map((d) => [`${d.document_type ?? "UNKNOWN"}:${d.id}`, d]))
|
||||
);
|
||||
}
|
||||
}, [initialDocuments]);
|
||||
|
||||
|
|
@ -153,6 +163,7 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
|||
const chip = document.createElement("span");
|
||||
chip.setAttribute(CHIP_DATA_ATTR, "true");
|
||||
chip.setAttribute(CHIP_ID_ATTR, String(doc.id));
|
||||
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";
|
||||
|
|
@ -175,13 +186,14 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
chip.remove();
|
||||
const docKey = `${doc.document_type ?? "UNKNOWN"}:${doc.id}`;
|
||||
setMentionedDocs((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.delete(doc.id);
|
||||
next.delete(docKey);
|
||||
return next;
|
||||
});
|
||||
// Notify parent that a document was removed
|
||||
onDocumentRemove?.(doc.id);
|
||||
onDocumentRemove?.(doc.id, doc.document_type);
|
||||
focusAtEnd();
|
||||
};
|
||||
|
||||
|
|
@ -195,7 +207,7 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
|||
|
||||
// Insert a document chip at the current cursor position
|
||||
const insertDocumentChip = useCallback(
|
||||
(doc: Document) => {
|
||||
(doc: Pick<Document, "id" | "title" | "document_type">) => {
|
||||
if (!editorRef.current) return;
|
||||
|
||||
// Validate required fields for type safety
|
||||
|
|
@ -210,8 +222,9 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
|||
document_type: doc.document_type,
|
||||
};
|
||||
|
||||
// Add to mentioned docs map
|
||||
setMentionedDocs((prev) => new Map(prev).set(doc.id, mentionDoc));
|
||||
// Add to mentioned docs map using unique key
|
||||
const docKey = `${doc.document_type ?? "UNKNOWN"}:${doc.id}`;
|
||||
setMentionedDocs((prev) => new Map(prev).set(docKey, mentionDoc));
|
||||
|
||||
// Find and remove the @query text
|
||||
const selection = window.getSelection();
|
||||
|
|
@ -413,15 +426,17 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
|||
if (isChipElement(prevSibling)) {
|
||||
e.preventDefault();
|
||||
const chipId = getChipId(prevSibling);
|
||||
const chipDocType = getChipDocType(prevSibling);
|
||||
if (chipId !== null) {
|
||||
prevSibling.remove();
|
||||
const chipKey = `${chipDocType}:${chipId}`;
|
||||
setMentionedDocs((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.delete(chipId);
|
||||
next.delete(chipKey);
|
||||
return next;
|
||||
});
|
||||
// Notify parent that a document was removed
|
||||
onDocumentRemove?.(chipId);
|
||||
onDocumentRemove?.(chipId, chipDocType);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -448,15 +463,17 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
|||
if (isChipElement(prevChild)) {
|
||||
e.preventDefault();
|
||||
const chipId = getChipId(prevChild);
|
||||
const chipDocType = getChipDocType(prevChild);
|
||||
if (chipId !== null) {
|
||||
prevChild.remove();
|
||||
const chipKey = `${chipDocType}:${chipId}`;
|
||||
setMentionedDocs((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.delete(chipId);
|
||||
next.delete(chipKey);
|
||||
return next;
|
||||
});
|
||||
// Notify parent that a document was removed
|
||||
onDocumentRemove?.(chipId);
|
||||
onDocumentRemove?.(chipId, chipDocType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Citation pattern: [citation:CHUNK_ID] or [citation:doc-CHUNK_ID]
|
||||
const CITATION_REGEX = /\[citation:(doc-)?(\d+)\]/g;
|
||||
// Also matches Chinese brackets 【】 and handles zero-width spaces that LLM sometimes inserts
|
||||
const CITATION_REGEX = /[[【]\u200B?citation:(doc-)?(\d+)\u200B?[\]】]/g;
|
||||
|
||||
// Track chunk IDs to citation numbers mapping for consistent numbering
|
||||
// This map is reset when a new message starts rendering
|
||||
|
|
@ -90,10 +91,6 @@ function parseTextWithCitations(text: string): ReactNode[] {
|
|||
}
|
||||
|
||||
const MarkdownTextImpl = () => {
|
||||
// Reset citation counter at the start of each render
|
||||
// This ensures consistent numbering as the message streams in
|
||||
resetCitationCounter();
|
||||
|
||||
return (
|
||||
<MarkdownTextPrimitive
|
||||
remarkPlugins={[remarkGfm]}
|
||||
|
|
|
|||
|
|
@ -229,7 +229,14 @@ const Composer: FC = () => {
|
|||
|
||||
// Sync mentioned document IDs to atom for use in chat request
|
||||
useEffect(() => {
|
||||
setMentionedDocumentIds(mentionedDocuments.map((doc) => doc.id));
|
||||
setMentionedDocumentIds({
|
||||
surfsense_doc_ids: mentionedDocuments
|
||||
.filter((doc) => doc.document_type === "SURFSENSE_DOCS")
|
||||
.map((doc) => doc.id),
|
||||
document_ids: mentionedDocuments
|
||||
.filter((doc) => doc.document_type !== "SURFSENSE_DOCS")
|
||||
.map((doc) => doc.id),
|
||||
});
|
||||
}, [mentionedDocuments, setMentionedDocumentIds]);
|
||||
|
||||
// Handle text change from inline editor - sync with assistant-ui composer
|
||||
|
|
@ -295,7 +302,10 @@ const Composer: FC = () => {
|
|||
// Clear the editor after sending
|
||||
editorRef.current?.clear();
|
||||
setMentionedDocuments([]);
|
||||
setMentionedDocumentIds([]);
|
||||
setMentionedDocumentIds({
|
||||
surfsense_doc_ids: [],
|
||||
document_ids: [],
|
||||
});
|
||||
}
|
||||
}, [
|
||||
showDocumentPopover,
|
||||
|
|
@ -305,41 +315,52 @@ const Composer: FC = () => {
|
|||
setMentionedDocumentIds,
|
||||
]);
|
||||
|
||||
// Handle document removal from inline editor
|
||||
const handleDocumentRemove = useCallback(
|
||||
(docId: number) => {
|
||||
(docId: number, docType?: string) => {
|
||||
setMentionedDocuments((prev) => {
|
||||
const updated = prev.filter((doc) => doc.id !== docId);
|
||||
// Immediately sync document IDs to avoid race conditions
|
||||
setMentionedDocumentIds(updated.map((doc) => doc.id));
|
||||
const updated = 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]
|
||||
);
|
||||
|
||||
// Handle document selection from picker
|
||||
const handleDocumentsMention = useCallback(
|
||||
(documents: Document[]) => {
|
||||
// Insert chips into the inline editor for each new document
|
||||
const existingIds = new Set(mentionedDocuments.map((d) => d.id));
|
||||
const newDocs = documents.filter((doc) => !existingIds.has(doc.id));
|
||||
(documents: Pick<Document, "id" | "title" | "document_type">[]) => {
|
||||
const existingKeys = new Set(mentionedDocuments.map((d) => `${d.document_type}:${d.id}`));
|
||||
const newDocs = documents.filter(
|
||||
(doc) => !existingKeys.has(`${doc.document_type}:${doc.id}`)
|
||||
);
|
||||
|
||||
for (const doc of newDocs) {
|
||||
editorRef.current?.insertDocumentChip(doc);
|
||||
}
|
||||
|
||||
// Update mentioned documents state
|
||||
setMentionedDocuments((prev) => {
|
||||
const existingIdSet = new Set(prev.map((d) => d.id));
|
||||
const uniqueNewDocs = documents.filter((doc) => !existingIdSet.has(doc.id));
|
||||
const existingKeySet = new Set(prev.map((d) => `${d.document_type}:${d.id}`));
|
||||
const uniqueNewDocs = documents.filter(
|
||||
(doc) => !existingKeySet.has(`${doc.document_type}:${doc.id}`)
|
||||
);
|
||||
const updated = [...prev, ...uniqueNewDocs];
|
||||
// Immediately sync document IDs to avoid race conditions
|
||||
setMentionedDocumentIds(updated.map((doc) => doc.id));
|
||||
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;
|
||||
});
|
||||
|
||||
// Reset mention query but keep popover open for more selections
|
||||
setMentionQuery("");
|
||||
},
|
||||
[mentionedDocuments, setMentionedDocuments, setMentionedDocumentIds]
|
||||
|
|
@ -640,7 +661,7 @@ const UserMessage: FC = () => {
|
|||
{/* Mentioned documents as chips */}
|
||||
{mentionedDocs?.map((doc) => (
|
||||
<span
|
||||
key={doc.id}
|
||||
key={`${doc.document_type}:${doc.id}`}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-primary/10 text-xs font-medium text-primary border border-primary/20"
|
||||
title={doc.title}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export const UserMessage: FC = () => {
|
|||
{/* Mentioned documents as chips */}
|
||||
{mentionedDocs?.map((doc) => (
|
||||
<span
|
||||
key={doc.id}
|
||||
key={`${doc.document_type}:${doc.id}`}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-primary/10 text-xs font-medium text-primary border border-primary/20"
|
||||
title={doc.title}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue