mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 08:46:22 +02:00
refactor: fix scroll to last user query ux
- Updated DashboardClientLayout to improve child component overflow handling. - Refactored NewChatPage to streamline the layout and integrate ChatHeader directly within the Thread component. - Added optional header prop to Thread component for better customization. - Cleaned up ChatHeader by removing unnecessary wrapper for improved design.
This commit is contained in:
parent
0a458dde2f
commit
40e982d541
4 changed files with 18 additions and 119 deletions
|
|
@ -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<string, ThinkingStep[]>;
|
||||
/** 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<string>("");
|
||||
|
||||
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<number>(0);
|
||||
const prevLastUserMsgIdRef = useRef<string>("");
|
||||
|
||||
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<ThreadProps> = ({ messageThinkingSteps = new Map() }) => {
|
||||
export const Thread: FC<ThreadProps> = ({ messageThinkingSteps = new Map(), header }) => {
|
||||
return (
|
||||
<ThinkingStepsContext.Provider value={messageThinkingSteps}>
|
||||
<ThreadPrimitive.Root
|
||||
className="aui-root aui-thread-root @container flex h-full flex-col bg-background"
|
||||
className="aui-root aui-thread-root @container flex h-full min-h-0 flex-col bg-background"
|
||||
style={{
|
||||
["--thread-max-width" as string]: "44rem",
|
||||
}}
|
||||
>
|
||||
<ThreadPrimitive.Viewport
|
||||
turnAnchor="top"
|
||||
className="aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4"
|
||||
className="aui-thread-viewport relative flex flex-1 min-h-0 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4"
|
||||
>
|
||||
{/* Auto-scroll handler for new queries - positions new user messages at the top */}
|
||||
<NewQueryScrollHandler />
|
||||
{/* Auto-scroll handler for thinking steps - must be inside Viewport */}
|
||||
<ThinkingStepsScrollHandler />
|
||||
{/* Optional sticky header for model selector etc. */}
|
||||
{header && (
|
||||
<div className="sticky top-0 z-10 mb-4">
|
||||
{header}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AssistantIf condition={({ thread }) => thread.isEmpty}>
|
||||
<ThreadWelcome />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue