diff --git a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx index bfe8599f6..284c1ebc0 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -265,7 +265,7 @@ export function DashboardClientLayout({ -
{children}
+
{children}
diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index 00514facb..e18629b92 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -626,11 +626,11 @@ export default function NewChatPage() { -
- -
- -
+
+ } + />
); diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index 8a7eca58e..6e69296e5 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -8,7 +8,6 @@ import { ThreadPrimitive, useAssistantState, useMessage, - useThreadViewport, } from "@assistant-ui/react"; import { useAtomValue } from "jotai"; import { @@ -69,6 +68,8 @@ import { cn } from "@/lib/utils"; */ interface ThreadProps { messageThinkingSteps?: Map; + /** Optional header component to render at the top of the viewport (sticky) */ + header?: React.ReactNode; } // Context to pass thinking steps to AssistantMessage @@ -212,122 +213,25 @@ const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?: boolea ); }; -/** - * Component that handles auto-scroll when thinking steps update. - * Uses useThreadViewport to scroll to bottom when thinking steps change, - * ensuring the user always sees the latest content during streaming. - */ -const ThinkingStepsScrollHandler: FC = () => { - const thinkingStepsMap = useContext(ThinkingStepsContext); - const viewport = useThreadViewport(); - const isRunning = useAssistantState(({ thread }) => thread.isRunning); - // Track the serialized state to detect any changes - const prevStateRef = useRef(""); - - useEffect(() => { - // Only act during streaming - if (!isRunning) { - prevStateRef.current = ""; - return; - } - - // Serialize the thinking steps state to detect any changes - // This catches new steps, status changes, and item additions - let stateString = ""; - thinkingStepsMap.forEach((steps, msgId) => { - steps.forEach((step) => { - stateString += `${msgId}:${step.id}:${step.status}:${step.items?.length || 0};`; - }); - }); - - // If state changed at all during streaming, scroll - if (stateString !== prevStateRef.current && stateString !== "") { - prevStateRef.current = stateString; - - // Multiple attempts to ensure scroll happens after DOM updates - const scrollAttempt = () => { - try { - viewport.scrollToBottom(); - } catch (e) { - // Ignore errors - viewport might not be ready - } - }; - - // Delayed attempts to handle async DOM updates - requestAnimationFrame(scrollAttempt); - setTimeout(scrollAttempt, 100); - } - }, [thinkingStepsMap, viewport, isRunning]); - - return null; // This component doesn't render anything -}; - -/** - * Component that handles auto-scroll when a new user query is submitted. - * Scrolls to position the new user message at the top of the viewport, - * similar to Cursor's behavior where the current query stays at the top. - */ -const NewQueryScrollHandler: FC = () => { - const messages = useAssistantState(({ thread }) => thread.messages); - const prevMessageCountRef = useRef(0); - const prevLastUserMsgIdRef = useRef(""); - - useEffect(() => { - const currentCount = messages.length; - - // Find the last user message - const lastUserMessage = [...messages].reverse().find((m) => m.role === "user"); - const lastUserMsgId = lastUserMessage?.id || ""; - - // Check if a new user message was added (not on initial load) - if ( - prevMessageCountRef.current > 0 && // Not initial load - currentCount > prevMessageCountRef.current && - lastUserMsgId !== prevLastUserMsgIdRef.current && - lastUserMsgId - ) { - // New user message added - scroll to make it visible at the top - // Use multiple attempts to ensure the DOM has updated - const scrollToNewQuery = () => { - // Find the last user message element - const userMessages = document.querySelectorAll('[data-role="user"]'); - const lastUserMsgElement = userMessages[userMessages.length - 1]; - if (lastUserMsgElement) { - // Scroll so the user message is at the top of the viewport - lastUserMsgElement.scrollIntoView({ behavior: "smooth", block: "start" }); - } - }; - - // Delayed attempts to handle async DOM updates - requestAnimationFrame(scrollToNewQuery); - setTimeout(scrollToNewQuery, 50); - setTimeout(scrollToNewQuery, 150); - } - - prevMessageCountRef.current = currentCount; - prevLastUserMsgIdRef.current = lastUserMsgId; - }, [messages]); - - return null; // This component doesn't render anything -}; - -export const Thread: FC = ({ messageThinkingSteps = new Map() }) => { +export const Thread: FC = ({ messageThinkingSteps = new Map(), header }) => { return ( - {/* Auto-scroll handler for new queries - positions new user messages at the top */} - - {/* Auto-scroll handler for thinking steps - must be inside Viewport */} - + {/* Optional sticky header for model selector etc. */} + {header && ( +
+ {header} +
+ )} thread.isEmpty}> diff --git a/surfsense_web/components/new-chat/chat-header.tsx b/surfsense_web/components/new-chat/chat-header.tsx index 1e0259447..34b2cc814 100644 --- a/surfsense_web/components/new-chat/chat-header.tsx +++ b/surfsense_web/components/new-chat/chat-header.tsx @@ -47,12 +47,7 @@ export function ChatHeader({ searchSpaceId }: ChatHeaderProps) { return ( <> - {/* Header Bar */} -
- -
- - {/* Config Sidebar */} +