feat: add auto-scroll functionality for new user queries

- Introduced NewQueryScrollHandler component to automatically scroll to the latest user message when a new query is submitted, enhancing user experience.
- Updated ChatHeader component to remove unnecessary border for a cleaner design.
- Adjusted ModelSelector styles for improved visual consistency and accessibility.
This commit is contained in:
Anish Sarkar 2025-12-24 02:09:24 +05:30
parent ea411ee1c8
commit ed63e5a1d6
3 changed files with 60 additions and 9 deletions

View file

@ -262,6 +262,55 @@ const ThinkingStepsScrollHandler: FC = () => {
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() }) => {
return (
<ThinkingStepsContext.Provider value={messageThinkingSteps}>
@ -275,6 +324,8 @@ export const Thread: FC<ThreadProps> = ({ messageThinkingSteps = new Map() }) =>
turnAnchor="top"
className="aui-thread-viewport relative flex flex-1 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 />

View file

@ -48,7 +48,7 @@ export function ChatHeader({ searchSpaceId }: ChatHeaderProps) {
return (
<>
{/* Header Bar */}
<div className="flex items-center justify-between px-4 py-2 border-b border-border/30 bg-background/80 backdrop-blur-sm">
<div className="flex items-center justify-between px-4 py-2 bg-background/80 backdrop-blur-sm">
<ModelSelector onEdit={handleEditConfig} onAddNew={handleAddNew} />
</div>

View file

@ -175,9 +175,10 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
role="combobox"
aria-expanded={open}
className={cn(
"h-9 gap-2 px-3 rounded-xl border border-border/50 bg-background/50 backdrop-blur-sm",
"hover:bg-muted/80 hover:border-border transition-all duration-200",
"h-9 gap-2 px-3 rounded-xl border border-border/30 bg-background/50 backdrop-blur-sm",
"hover:bg-muted/80 hover:border-border/30 transition-all duration-200",
"text-sm font-medium text-foreground",
"focus-visible:ring-0 focus-visible:ring-offset-0",
className
)}
>
@ -206,11 +207,11 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
</PopoverTrigger>
<PopoverContent
className="w-[360px] p-0 rounded-xl shadow-lg border-border/50"
className="w-[360px] p-0 rounded-xl shadow-lg border-border/30"
align="start"
sideOffset={8}
>
<Command shouldFilter={false} className="rounded-xl relative">
<Command shouldFilter={false} className="rounded-xl relative [&_[data-slot=command-input-wrapper]]:border-0 [&_[data-slot=command-input-wrapper]]:px-0 [&_[data-slot=command-input-wrapper]]:gap-2">
{/* Switching overlay */}
{isSwitching && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-background/80 backdrop-blur-sm rounded-xl">
@ -221,8 +222,7 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
</div>
)}
<div className="flex items-center gap-2 border-b px-3 py-2 bg-muted/30">
<Bot className="size-4 text-muted-foreground" />
<div className="flex items-center gap-2 border-b border-border/30 px-3 py-2">
<CommandInput
placeholder="Search models..."
value={searchQuery}
@ -300,7 +300,7 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
)}
{filteredGlobalConfigs.length > 0 && filteredUserConfigs.length > 0 && (
<CommandSeparator className="my-1" />
<CommandSeparator className="my-1 bg-border/30" />
)}
{/* User Configs Section */}
@ -362,7 +362,7 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
)}
{/* Add New Config Button */}
<div className="p-2 border-t border-border/50 bg-muted/20">
<div className="p-2 bg-muted/20">
<Button
variant="ghost"
size="sm"