diff --git a/surfsense_web/components/assistant-ui/inline-mention-editor.tsx b/surfsense_web/components/assistant-ui/inline-mention-editor.tsx index 2e5d1938d..56926de5e 100644 --- a/surfsense_web/components/assistant-ui/inline-mention-editor.tsx +++ b/surfsense_web/components/assistant-ui/inline-mention-editor.tsx @@ -1,14 +1,16 @@ "use client"; -import { X } from "lucide-react"; import { forwardRef, useCallback, + createElement, useEffect, useImperativeHandle, useRef, useState, } from "react"; +import { X } from "lucide-react"; +import ReactDOMServer from "react-dom/server"; import type { Document } from "@/contracts/types/document.types"; import { cn } from "@/lib/utils"; @@ -65,7 +67,6 @@ export const InlineMentionEditor = forwardRef new Map(initialDocuments.map((d) => [d.id, d])) ); const isComposingRef = useRef(false); - const lastCaretPositionRef = useRef<{ node: Node; offset: number } | null>(null); // Sync initial documents useEffect(() => { @@ -74,36 +75,6 @@ export const InlineMentionEditor = forwardRef { - const selection = window.getSelection(); - if (selection && selection.rangeCount > 0) { - const range = selection.getRangeAt(0); - lastCaretPositionRef.current = { - node: range.startContainer, - offset: range.startOffset, - }; - } - }, []); - - // Restore caret position - const restoreCaretPosition = useCallback(() => { - if (lastCaretPositionRef.current && editorRef.current) { - const { node, offset } = lastCaretPositionRef.current; - try { - const selection = window.getSelection(); - const range = document.createRange(); - range.setStart(node, offset); - range.collapse(true); - selection?.removeAllRanges(); - selection?.addRange(range); - } catch { - // Node might not exist anymore, focus at end - focusAtEnd(); - } - } - }, []); - // Focus at the end of the editor const focusAtEnd = useCallback(() => { if (!editorRef.current) return; @@ -139,11 +110,12 @@ export const InlineMentionEditor = forwardRef`; + removeBtn.innerHTML = ReactDOMServer.renderToString( + createElement(X, { className: "h-2.5 w-2.5", strokeWidth: 2.5 }) + ); removeBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); @@ -248,8 +222,8 @@ export const InlineMentionEditor = forwardRef 0) { const range = selection.getRangeAt(0); const textNode = range.startContainer; @@ -350,16 +327,26 @@ export const InlineMentionEditor = forwardRef 0 && textContent[0] === "@") { + // Will delete @, close mention popover + setTimeout(() => { + onMentionClose?.(); + }, 0); + } + } else if (node.nodeType === Node.TEXT_NODE && offset > 0) { + // Check if we're about to delete @ + const textContent = node.textContent || ""; + if (textContent[offset - 1] === "@") { + // Will delete @, close mention popover + setTimeout(() => { + onMentionClose?.(); + }, 0); } } else if (node.nodeType === Node.ELEMENT_NODE && offset > 0) { // Check if previous child is a chip @@ -425,7 +430,7 @@ export const InlineMentionEditor = forwardRef + {/** biome-ignore lint/a11y/useSemanticElements: */}
{ const { search_space_id } = useParams(); const setMentionedDocumentIds = useSetAtom(mentionedDocumentIdsAtom); const composerRuntime = useComposerRuntime(); + const hasAutoFocusedRef = useRef(false); + + // Check if thread is empty (new chat) + const isThreadEmpty = useAssistantState(({ thread }) => thread.isEmpty); + + // Auto-focus editor when on new chat page + useEffect(() => { + if (isThreadEmpty && !hasAutoFocusedRef.current && editorRef.current) { + // Small delay to ensure the editor is fully mounted + const timeoutId = setTimeout(() => { + editorRef.current?.focus(); + hasAutoFocusedRef.current = true; + }, 100); + return () => clearTimeout(timeoutId); + } + }, [isThreadEmpty]); + + // Reset auto-focus flag when thread becomes non-empty (user sent a message) + useEffect(() => { + if (!isThreadEmpty) { + hasAutoFocusedRef.current = false; + } + }, [isThreadEmpty]); // Sync mentioned document IDs to atom for use in chat request useEffect(() => {