Merge remote-tracking branch 'upstream/dev' into feat/replace-logs

This commit is contained in:
Anish Sarkar 2026-01-14 02:04:54 +05:30
commit 99bd2df463
59 changed files with 2788 additions and 1579 deletions

View file

@ -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]

View file

@ -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>

View file

@ -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);
}
}
}

View file

@ -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]}

View file

@ -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}
>

View file

@ -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}
>