mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-11 08:42:39 +02:00
Merge pull request #615 from AnishSarkar22/fix/chatpage-ux
UX improvements
This commit is contained in:
commit
0a458dde2f
5 changed files with 67 additions and 16 deletions
|
|
@ -262,6 +262,55 @@ const ThinkingStepsScrollHandler: FC = () => {
|
||||||
return null; // This component doesn't render anything
|
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() }) => {
|
||||||
return (
|
return (
|
||||||
<ThinkingStepsContext.Provider value={messageThinkingSteps}>
|
<ThinkingStepsContext.Provider value={messageThinkingSteps}>
|
||||||
|
|
@ -275,6 +324,8 @@ export const Thread: FC<ThreadProps> = ({ messageThinkingSteps = new Map() }) =>
|
||||||
turnAnchor="top"
|
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 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 */}
|
{/* Auto-scroll handler for thinking steps - must be inside Viewport */}
|
||||||
<ThinkingStepsScrollHandler />
|
<ThinkingStepsScrollHandler />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export function ChatHeader({ searchSpaceId }: ChatHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Header Bar */}
|
{/* 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} />
|
<ModelSelector onEdit={handleEditConfig} onAddNew={handleAddNew} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -175,9 +175,10 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-9 gap-2 px-3 rounded-xl border border-border/50 bg-background/50 backdrop-blur-sm",
|
"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 transition-all duration-200",
|
"hover:bg-muted/80 hover:border-border/30 transition-all duration-200",
|
||||||
"text-sm font-medium text-foreground",
|
"text-sm font-medium text-foreground",
|
||||||
|
"focus-visible:ring-0 focus-visible:ring-offset-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -206,11 +207,11 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
|
||||||
<PopoverContent
|
<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"
|
align="start"
|
||||||
sideOffset={8}
|
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 */}
|
{/* Switching overlay */}
|
||||||
{isSwitching && (
|
{isSwitching && (
|
||||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-background/80 backdrop-blur-sm rounded-xl">
|
<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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2 border-b px-3 py-2 bg-muted/30">
|
<div className="flex items-center gap-2 border-b border-border/30 px-3 py-2">
|
||||||
<Bot className="size-4 text-muted-foreground" />
|
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search models..."
|
placeholder="Search models..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
|
|
@ -300,7 +300,7 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{filteredGlobalConfigs.length > 0 && filteredUserConfigs.length > 0 && (
|
{filteredGlobalConfigs.length > 0 && filteredUserConfigs.length > 0 && (
|
||||||
<CommandSeparator className="my-1" />
|
<CommandSeparator className="my-1 bg-border/30" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* User Configs Section */}
|
{/* User Configs Section */}
|
||||||
|
|
@ -362,7 +362,7 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Add New Config Button */}
|
{/* Add New Config Button */}
|
||||||
<div className="p-2 border-t border-border/50 bg-muted/20">
|
<div className="p-2 bg-muted/20">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
||||||
|
|
@ -160,8 +160,8 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<SheetContent side="left" className="w-80 p-0 flex flex-col">
|
<SheetContent side="left" className="w-80 p-0 flex flex-col border-0">
|
||||||
<SheetHeader className="mx-3 px-4 py-4 border-b space-y-3">
|
<SheetHeader className="mx-3 px-4 pt-4 pb-0 space-y-2">
|
||||||
<SheetTitle>{t("all_chats") || "All Chats"}</SheetTitle>
|
<SheetTitle>{t("all_chats") || "All Chats"}</SheetTitle>
|
||||||
<SheetDescription className="sr-only">
|
<SheetDescription className="sr-only">
|
||||||
{t("all_chats_description") || "Browse and manage all your chats"}
|
{t("all_chats_description") || "Browse and manage all your chats"}
|
||||||
|
|
@ -175,7 +175,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
|
||||||
placeholder={t("search_chats") || "Search chats..."}
|
placeholder={t("search_chats") || "Search chats..."}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="pl-9 pr-8 h-9"
|
className="pl-9 pr-8 h-9 border-0 focus-visible:ring-0 focus-visible:border-0 shadow-none"
|
||||||
/>
|
/>
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -193,7 +193,7 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
|
||||||
|
|
||||||
{/* Tab toggle for active/archived (only show when not searching) */}
|
{/* Tab toggle for active/archived (only show when not searching) */}
|
||||||
{!isSearchMode && (
|
{!isSearchMode && (
|
||||||
<div className="flex border-b mx-3">
|
<div className="flex border-b mx-3 -mt-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowArchived(false)}
|
onClick={() => setShowArchived(false)}
|
||||||
|
|
|
||||||
|
|
@ -159,8 +159,8 @@ export function AllNotesSidebar({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<SheetContent side="left" className="w-80 p-0 flex flex-col">
|
<SheetContent side="left" className="w-80 p-0 flex flex-col border-0">
|
||||||
<SheetHeader className="mx-3 px-4 py-4 border-b space-y-3">
|
<SheetHeader className="mx-3 px-4 pt-4 pb-2 border-b space-y-2">
|
||||||
<SheetTitle>{t("all_notes") || "All Notes"}</SheetTitle>
|
<SheetTitle>{t("all_notes") || "All Notes"}</SheetTitle>
|
||||||
<SheetDescription className="sr-only">
|
<SheetDescription className="sr-only">
|
||||||
{t("all_notes_description") || "Browse and manage all your notes"}
|
{t("all_notes_description") || "Browse and manage all your notes"}
|
||||||
|
|
@ -174,7 +174,7 @@ export function AllNotesSidebar({
|
||||||
placeholder={t("search_notes") || "Search notes..."}
|
placeholder={t("search_notes") || "Search notes..."}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="pl-9 pr-8 h-9"
|
className="pl-9 pr-8 h-9 border-0 focus-visible:ring-0 focus-visible:border-0 shadow-none"
|
||||||
/>
|
/>
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue