Merge pull request #616 from MODSetter/dev

chore: various ux improvs
This commit is contained in:
Rohan Verma 2025-12-23 18:54:23 -08:00 committed by GitHub
commit ef03fc97e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 32 additions and 83 deletions

View file

@ -265,7 +265,7 @@ export function DashboardClientLayout({
</div> </div>
</div> </div>
</header> </header>
<div className="grow flex-1 overflow-auto min-h-[calc(100vh-64px)]">{children}</div> <div className="flex-1 overflow-hidden">{children}</div>
</main> </main>
</SidebarInset> </SidebarInset>
</SidebarProvider> </SidebarProvider>

View file

@ -626,11 +626,11 @@ export default function NewChatPage() {
<LinkPreviewToolUI /> <LinkPreviewToolUI />
<DisplayImageToolUI /> <DisplayImageToolUI />
<ScrapeWebpageToolUI /> <ScrapeWebpageToolUI />
<div className="flex flex-col h-[calc(100vh-64px)] max-h-[calc(100vh-64px)] overflow-hidden"> <div className="flex flex-col h-[calc(100vh-64px)] overflow-hidden">
<ChatHeader searchSpaceId={searchSpaceId} /> <Thread
<div className="flex-1 min-h-0 overflow-hidden"> messageThinkingSteps={messageThinkingSteps}
<Thread messageThinkingSteps={messageThinkingSteps} /> header={<ChatHeader searchSpaceId={searchSpaceId} />}
</div> />
</div> </div>
</AssistantRuntimeProvider> </AssistantRuntimeProvider>
); );

View file

@ -8,7 +8,6 @@ import {
ThreadPrimitive, ThreadPrimitive,
useAssistantState, useAssistantState,
useMessage, useMessage,
useThreadViewport,
} from "@assistant-ui/react"; } from "@assistant-ui/react";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { import {
@ -69,6 +68,8 @@ import { cn } from "@/lib/utils";
*/ */
interface ThreadProps { interface ThreadProps {
messageThinkingSteps?: Map<string, ThinkingStep[]>; 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 // Context to pass thinking steps to AssistantMessage
@ -212,71 +213,21 @@ const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?: boolea
); );
}; };
/** export const Thread: FC<ThreadProps> = ({ messageThinkingSteps = new Map(), header }) => {
* 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
};
export const Thread: FC<ThreadProps> = ({ messageThinkingSteps = new Map() }) => {
return ( return (
<ThinkingStepsContext.Provider value={messageThinkingSteps}> <ThinkingStepsContext.Provider value={messageThinkingSteps}>
<ThreadPrimitive.Root <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={{ style={{
["--thread-max-width" as string]: "44rem", ["--thread-max-width" as string]: "44rem",
}} }}
> >
<ThreadPrimitive.Viewport <ThreadPrimitive.Viewport
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 min-h-0 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4"
> >
{/* Auto-scroll handler for thinking steps - must be inside Viewport */} {/* Optional sticky header for model selector etc. */}
<ThinkingStepsScrollHandler /> {header && <div className="sticky top-0 z-10 mb-4">{header}</div>}
<AssistantIf condition={({ thread }) => thread.isEmpty}> <AssistantIf condition={({ thread }) => thread.isEmpty}>
<ThreadWelcome /> <ThreadWelcome />

View file

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

View file

@ -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,14 @@ 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 +225,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 +303,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 +365,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"

View file

@ -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)}

View file

@ -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