Merge pull request #887 from AnishSarkar22/fix/mobile-ui

feat: enhance ComposerAction with dropdown menu on mobile
This commit is contained in:
Rohan Verma 2026-03-15 14:27:59 -07:00 committed by GitHub
commit e0143e61ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -19,10 +19,12 @@ import {
ChevronRightIcon, ChevronRightIcon,
CopyIcon, CopyIcon,
DownloadIcon, DownloadIcon,
Plus,
RefreshCwIcon, RefreshCwIcon,
Settings2,
SquareIcon, SquareIcon,
Unplug, Unplug,
Wrench, Upload,
X, X,
} from "lucide-react"; } from "lucide-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
@ -53,6 +55,7 @@ import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { AssistantMessage } from "@/components/assistant-ui/assistant-message"; import { AssistantMessage } from "@/components/assistant-ui/assistant-message";
import { ChatSessionStatus } from "@/components/assistant-ui/chat-session-status"; import { ChatSessionStatus } from "@/components/assistant-ui/chat-session-status";
import { ConnectorIndicator } from "@/components/assistant-ui/connector-popup"; import { ConnectorIndicator } from "@/components/assistant-ui/connector-popup";
import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
import { import {
InlineMentionEditor, InlineMentionEditor,
type InlineMentionEditorRef, type InlineMentionEditorRef,
@ -73,6 +76,13 @@ import {
import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking"; import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking";
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
@ -278,21 +288,14 @@ const ConnectToolsBanner: FC = () => {
</Avatar> </Avatar>
))} ))}
</AvatarGroup> </AvatarGroup>
<span <button
role="button" type="button"
tabIndex={0}
onClick={handleDismiss} onClick={handleDismiss}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleDismiss(e as unknown as React.MouseEvent);
}
}}
className="shrink-0 ml-0.5 p-0.5 text-muted-foreground/40 hover:text-foreground transition-colors" className="shrink-0 ml-0.5 p-0.5 text-muted-foreground/40 hover:text-foreground transition-colors"
aria-label="Dismiss" aria-label="Dismiss"
> >
<X className="size-3.5" /> <X className="size-3.5" />
</span> </button>
</button> </button>
</div> </div>
); );
@ -564,6 +567,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
const setConnectorDialogOpen = useSetAtom(connectorDialogOpenAtom); const setConnectorDialogOpen = useSetAtom(connectorDialogOpenAtom);
const [toolsPopoverOpen, setToolsPopoverOpen] = useState(false); const [toolsPopoverOpen, setToolsPopoverOpen] = useState(false);
const isDesktop = useMediaQuery("(min-width: 640px)"); const isDesktop = useMediaQuery("(min-width: 640px)");
const { openDialog: openUploadDialog } = useDocumentUploadDialog();
const [toolsScrollPos, setToolsScrollPos] = useState<"top" | "middle" | "bottom">("top"); const [toolsScrollPos, setToolsScrollPos] = useState<"top" | "middle" | "bottom">("top");
const handleToolsScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => { const handleToolsScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
const el = e.currentTarget; const el = e.currentTarget;
@ -607,87 +611,144 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
return ( return (
<div className="aui-composer-action-wrapper relative mx-3 mb-2 flex items-center justify-between"> <div className="aui-composer-action-wrapper relative mx-3 mb-2 flex items-center justify-between">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Popover open={toolsPopoverOpen} onOpenChange={setToolsPopoverOpen}> {!isDesktop ? (
<PopoverTrigger asChild> <>
<TooltipIconButton <DropdownMenu>
tooltip="Manage tools" <DropdownMenuTrigger asChild>
side="bottom" <Button
variant="ghost"
size="icon"
className="size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
aria-label="More actions"
data-joyride="connector-icon"
>
<Plus className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start" sideOffset={8}>
<DropdownMenuItem onSelect={() => setToolsPopoverOpen(true)}>
<Settings2 className="size-4" />
Manage Tools
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => openUploadDialog()}>
<Upload className="size-4" />
Upload Files
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Drawer open={toolsPopoverOpen} onOpenChange={setToolsPopoverOpen}>
<DrawerContent className="max-h-[60dvh]">
<DrawerHandle />
<div className="flex items-center justify-between px-4 py-2">
<DrawerTitle className="text-sm font-medium">Agent Tools</DrawerTitle>
<span className="text-xs text-muted-foreground">
{enabledCount}/{agentTools?.length ?? 0} enabled
</span>
</div>
<div className="overflow-y-auto pb-6" onScroll={handleToolsScroll}>
{agentTools?.map((tool) => {
const isDisabled = disabledTools.includes(tool.name);
return (
<div
key={tool.name}
className="flex w-full items-center gap-3 px-4 py-2 hover:bg-muted-foreground/10 transition-colors"
>
<span className="flex-1 min-w-0 text-sm font-medium truncate">
{formatToolName(tool.name)}
</span>
<Switch
checked={!isDisabled}
onCheckedChange={() => toggleTool(tool.name)}
className="shrink-0"
/>
</div>
);
})}
{!agentTools?.length && (
<div className="px-4 py-6 text-center text-sm text-muted-foreground">
Loading tools...
</div>
)}
</div>
</DrawerContent>
</Drawer>
<Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30" className="size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
aria-label="Manage tools" aria-label="Manage connectors"
data-joyride="connector-icon" onClick={() => setConnectorDialogOpen(true)}
> >
<Wrench className="size-4" /> <Unplug className="size-4" />
</TooltipIconButton> </Button>
</PopoverTrigger> </>
<PopoverContent ) : (
side="bottom" <Popover open={toolsPopoverOpen} onOpenChange={setToolsPopoverOpen}>
align="start" <PopoverTrigger asChild>
sideOffset={12} <TooltipIconButton
className="w-[calc(100vw-2rem)] max-w-56 sm:max-w-72 sm:w-72 p-0 select-none" tooltip="Manage tools"
onOpenAutoFocus={(e) => e.preventDefault()} side="bottom"
> variant="ghost"
<div className="flex items-center justify-between px-2.5 py-2 sm:px-3 sm:py-2.5 border-b"> size="icon"
<span className="text-xs sm:text-sm font-medium">Agent Tools</span> className="size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
<span className="text-[10px] sm:text-xs text-muted-foreground"> aria-label="Manage tools"
{enabledCount}/{agentTools?.length ?? 0} enabled data-joyride="connector-icon"
</span> >
</div> <Settings2 className="size-4" />
<div </TooltipIconButton>
className="max-h-48 sm:max-h-64 overflow-y-auto py-0.5 sm:py-1" </PopoverTrigger>
onScroll={handleToolsScroll} <PopoverContent
style={{ side="bottom"
maskImage: `linear-gradient(to bottom, ${toolsScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${toolsScrollPos === "bottom" ? "black" : "transparent"})`, align="start"
WebkitMaskImage: `linear-gradient(to bottom, ${toolsScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${toolsScrollPos === "bottom" ? "black" : "transparent"})`, sideOffset={12}
}} className="w-[calc(100vw-2rem)] max-w-56 sm:max-w-72 sm:w-72 p-0 select-none"
onOpenAutoFocus={(e) => e.preventDefault()}
> >
{agentTools?.map((tool) => { <div className="flex items-center justify-between px-2.5 py-2 sm:px-3 sm:py-2.5 border-b">
const isDisabled = disabledTools.includes(tool.name); <span className="text-xs sm:text-sm font-medium">Agent Tools</span>
const row = ( <span className="text-[10px] sm:text-xs text-muted-foreground">
<label className="flex items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 cursor-pointer hover:bg-muted-foreground/10 transition-colors"> {enabledCount}/{agentTools?.length ?? 0} enabled
<span className="flex-1 min-w-0 text-xs sm:text-sm font-medium truncate"> </span>
{formatToolName(tool.name)} </div>
</span> <div
<Switch className="max-h-48 sm:max-h-64 overflow-y-auto py-0.5 sm:py-1"
checked={!isDisabled} onScroll={handleToolsScroll}
onCheckedChange={() => toggleTool(tool.name)} style={{
className="shrink-0 scale-[0.6] sm:scale-75" maskImage: `linear-gradient(to bottom, ${toolsScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${toolsScrollPos === "bottom" ? "black" : "transparent"})`,
/> WebkitMaskImage: `linear-gradient(to bottom, ${toolsScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${toolsScrollPos === "bottom" ? "black" : "transparent"})`,
</label> }}
); >
if (!isDesktop) { {agentTools?.map((tool) => {
return <div key={tool.name}>{row}</div>; const isDisabled = disabledTools.includes(tool.name);
} const row = (
return ( <div className="flex w-full items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 hover:bg-muted-foreground/10 transition-colors">
<Tooltip key={tool.name}> <span className="flex-1 min-w-0 text-xs sm:text-sm font-medium truncate">
<TooltipTrigger asChild>{row}</TooltipTrigger> {formatToolName(tool.name)}
<TooltipContent side="right" className="max-w-64 text-xs"> </span>
{tool.description} <Switch
</TooltipContent> checked={!isDisabled}
</Tooltip> onCheckedChange={() => toggleTool(tool.name)}
); className="shrink-0 scale-[0.6] sm:scale-75"
})} />
{!agentTools?.length && ( </div>
<div className="px-3 py-4 text-center text-xs text-muted-foreground"> );
Loading tools... return (
</div> <Tooltip key={tool.name}>
)} <TooltipTrigger asChild>{row}</TooltipTrigger>
</div> <TooltipContent side="right" className="max-w-64 text-xs">
</PopoverContent> {tool.description}
</Popover> </TooltipContent>
{!isDesktop && ( </Tooltip>
<TooltipIconButton );
tooltip="Manage connectors" })}
side="bottom" {!agentTools?.length && (
variant="ghost" <div className="px-3 py-4 text-center text-xs text-muted-foreground">
size="icon" Loading tools...
className="size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30" </div>
aria-label="Manage connectors" )}
onClick={() => setConnectorDialogOpen(true)} </div>
> </PopoverContent>
<Unplug className="size-4" /> </Popover>
</TooltipIconButton>
)} )}
{sidebarDocs.length > 0 && ( {sidebarDocs.length > 0 && (
<button <button