mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-21 18:55:16 +02:00
refactor: replace Popover with DropdownMenu in ComposerAction and clean up unused state management
This commit is contained in:
parent
f65bc81509
commit
b52e578021
2 changed files with 129 additions and 172 deletions
|
|
@ -14,7 +14,6 @@ import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Clipboard,
|
Clipboard,
|
||||||
Dot,
|
|
||||||
Globe,
|
Globe,
|
||||||
Plus,
|
Plus,
|
||||||
Settings2,
|
Settings2,
|
||||||
|
|
@ -83,12 +82,14 @@ import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||||
import {
|
import {
|
||||||
CONNECTOR_ICON_TO_TYPES,
|
CONNECTOR_ICON_TO_TYPES,
|
||||||
|
|
@ -776,24 +777,6 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
||||||
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 { openDialog: openUploadDialog } = useDocumentUploadDialog();
|
||||||
const [toolsScrollPos, setToolsScrollPos] = useState<"top" | "middle" | "bottom">("top");
|
|
||||||
const toolsRafRef = useRef<number | undefined>(undefined);
|
|
||||||
const handleToolsScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
|
||||||
const el = e.currentTarget;
|
|
||||||
if (toolsRafRef.current) return;
|
|
||||||
toolsRafRef.current = requestAnimationFrame(() => {
|
|
||||||
const atTop = el.scrollTop <= 2;
|
|
||||||
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 2;
|
|
||||||
setToolsScrollPos(atTop ? "top" : atBottom ? "bottom" : "middle");
|
|
||||||
toolsRafRef.current = undefined;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
useEffect(
|
|
||||||
() => () => {
|
|
||||||
if (toolsRafRef.current) cancelAnimationFrame(toolsRafRef.current);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
const pendingScreenImages = useAtomValue(pendingUserImageDataUrlsAtom);
|
const pendingScreenImages = useAtomValue(pendingUserImageDataUrlsAtom);
|
||||||
const setPendingScreenImages = useSetAtom(pendingUserImageDataUrlsAtom);
|
const setPendingScreenImages = useSetAtom(pendingUserImageDataUrlsAtom);
|
||||||
const electronAPI = useElectronAPI();
|
const electronAPI = useElectronAPI();
|
||||||
|
|
@ -943,10 +926,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
||||||
Manage Tools
|
Manage Tools
|
||||||
</DrawerTitle>
|
</DrawerTitle>
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<div
|
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-thin pb-6">
|
||||||
className="min-h-0 flex-1 overflow-y-auto scrollbar-thin pb-6"
|
|
||||||
onScroll={handleToolsScroll}
|
|
||||||
>
|
|
||||||
{groupedTools
|
{groupedTools
|
||||||
.filter((g) => !g.connectorIcon)
|
.filter((g) => !g.connectorIcon)
|
||||||
.map((group) => (
|
.map((group) => (
|
||||||
|
|
@ -1043,153 +1023,143 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Popover open={toolsPopoverOpen} onOpenChange={setToolsPopoverOpen}>
|
<DropdownMenu onOpenChange={(open) => !open && setToolsPopoverOpen(false)}>
|
||||||
<PopoverTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<TooltipIconButton
|
<TooltipIconButton
|
||||||
tooltip="Manage tools"
|
tooltip="More actions"
|
||||||
side="bottom"
|
side="bottom"
|
||||||
disableTooltip={toolsPopoverOpen}
|
disableTooltip={toolsPopoverOpen}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="size-[34px] rounded-full p-1 font-semibold text-xs dark:border-muted-foreground/15 hover:bg-accent hover:text-accent-foreground"
|
className="size-[34px] rounded-full p-1 font-semibold text-xs dark:border-muted-foreground/15 hover:bg-accent hover:text-accent-foreground"
|
||||||
aria-label="Manage tools"
|
aria-label="More actions"
|
||||||
data-joyride="connector-icon"
|
data-joyride="connector-icon"
|
||||||
>
|
>
|
||||||
<Settings2 className="size-4" />
|
<Plus className="size-4" />
|
||||||
</TooltipIconButton>
|
</TooltipIconButton>
|
||||||
</PopoverTrigger>
|
</DropdownMenuTrigger>
|
||||||
<PopoverContent
|
<DropdownMenuContent className="w-48" side="bottom" align="start" sideOffset={8}>
|
||||||
side="bottom"
|
<DropdownMenuItem onSelect={() => openUploadDialog()}>
|
||||||
align="start"
|
<Upload className="h-4 w-4" />
|
||||||
sideOffset={12}
|
Upload Files
|
||||||
className="w-[calc(100vw-2rem)] max-w-48 sm:max-w-56 sm:w-56 p-0 select-none"
|
</DropdownMenuItem>
|
||||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
<DropdownMenuItem onSelect={() => void handleScreenCapture()}>
|
||||||
>
|
<Camera className="h-4 w-4" />
|
||||||
<div className="sr-only">Manage Tools</div>
|
Take a screenshot
|
||||||
<div
|
</DropdownMenuItem>
|
||||||
className="max-h-44 sm:max-h-56 overflow-y-auto overscroll-none py-0.5"
|
<DropdownMenuSub open={toolsPopoverOpen} onOpenChange={setToolsPopoverOpen}>
|
||||||
onScroll={handleToolsScroll}
|
<DropdownMenuSubTrigger>
|
||||||
style={{
|
<Settings2 className="h-4 w-4" />
|
||||||
maskImage: `linear-gradient(to bottom, ${toolsScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${toolsScrollPos === "bottom" ? "black" : "transparent"})`,
|
Manage Tools
|
||||||
WebkitMaskImage: `linear-gradient(to bottom, ${toolsScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${toolsScrollPos === "bottom" ? "black" : "transparent"})`,
|
</DropdownMenuSubTrigger>
|
||||||
}}
|
<DropdownMenuPortal>
|
||||||
>
|
<DropdownMenuSubContent
|
||||||
{groupedTools
|
alignOffset={-192}
|
||||||
.filter((g) => !g.connectorIcon)
|
collisionPadding={8}
|
||||||
.map((group) => (
|
className="w-60 h-56 gap-1 overflow-y-auto overscroll-none"
|
||||||
<div key={group.label}>
|
>
|
||||||
<div className="px-2 sm:px-2.5 pt-1.5 pb-0.5 text-[9px] sm:text-[10px] text-muted-foreground/80 font-normal select-none">
|
|
||||||
{group.label}
|
|
||||||
</div>
|
|
||||||
{group.tools.map((tool) => {
|
|
||||||
const isDisabled = disabledToolsSet.has(tool.name);
|
|
||||||
const ToolIcon = getToolIcon(tool.name);
|
|
||||||
const row = (
|
|
||||||
<div className="flex w-full items-center gap-1.5 sm:gap-2 px-2 sm:px-2.5 py-0.5 sm:py-1 hover:bg-accent hover:text-accent-foreground transition-colors">
|
|
||||||
<ToolIcon className="size-3 sm:size-3.5 shrink-0 text-muted-foreground" />
|
|
||||||
<span className="flex-1 min-w-0 text-[11px] sm:text-xs font-medium truncate">
|
|
||||||
{formatToolName(tool.name)}
|
|
||||||
</span>
|
|
||||||
<Switch
|
|
||||||
checked={!isDisabled}
|
|
||||||
onCheckedChange={() => toggleTool(tool.name)}
|
|
||||||
className="shrink-0 scale-50 sm:scale-[0.6]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<Tooltip key={tool.name}>
|
|
||||||
<TooltipTrigger asChild>{row}</TooltipTrigger>
|
|
||||||
<TooltipContent side="right" className="max-w-64 text-xs">
|
|
||||||
{tool.description}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{groupedTools.some((g) => g.connectorIcon) && (
|
|
||||||
<div>
|
|
||||||
<div className="px-2 sm:px-2.5 pt-1.5 pb-0.5 text-[9px] sm:text-[10px] text-muted-foreground/80 font-normal select-none">
|
|
||||||
Connector Actions
|
|
||||||
</div>
|
|
||||||
{groupedTools
|
{groupedTools
|
||||||
.filter((g) => g.connectorIcon)
|
.filter((g) => !g.connectorIcon)
|
||||||
.map((group) => {
|
.map((group) => (
|
||||||
const iconKey = group.connectorIcon ?? "";
|
<div key={group.label}>
|
||||||
const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey];
|
<div className="px-2 pt-1.5 pb-0.5 text-[10px] text-muted-foreground/80 font-normal select-none">
|
||||||
const toolNames = group.tools.map((t) => t.name);
|
{group.label}
|
||||||
const allDisabled = toolNames.every((n) => disabledToolsSet.has(n));
|
|
||||||
const groupDef = TOOL_GROUPS.find((g) => g.label === group.label);
|
|
||||||
const row = (
|
|
||||||
<div className="flex w-full items-center gap-1.5 sm:gap-2 px-2 sm:px-2.5 py-0.5 sm:py-1 hover:bg-accent hover:text-accent-foreground transition-colors">
|
|
||||||
{iconInfo ? (
|
|
||||||
<Image
|
|
||||||
src={iconInfo.src}
|
|
||||||
alt={iconInfo.alt}
|
|
||||||
width={14}
|
|
||||||
height={14}
|
|
||||||
className="size-3 sm:size-3.5 shrink-0 select-none pointer-events-none"
|
|
||||||
draggable={false}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Wrench className="size-3 sm:size-3.5 shrink-0 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
<span className="flex-1 min-w-0 text-[11px] sm:text-xs font-medium truncate">
|
|
||||||
{group.label}
|
|
||||||
</span>
|
|
||||||
<Switch
|
|
||||||
checked={!allDisabled}
|
|
||||||
onCheckedChange={() => toggleToolGroup(toolNames)}
|
|
||||||
className="shrink-0 scale-50 sm:scale-[0.6]"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
{group.tools.map((tool) => {
|
||||||
return (
|
const isDisabled = disabledToolsSet.has(tool.name);
|
||||||
<Tooltip key={group.label}>
|
const ToolIcon = getToolIcon(tool.name);
|
||||||
<TooltipTrigger asChild>{row}</TooltipTrigger>
|
return (
|
||||||
<TooltipContent side="right" className="max-w-72 text-xs">
|
<DropdownMenuItem
|
||||||
{groupDef?.tooltip ??
|
key={tool.name}
|
||||||
group.tools.flatMap((t, i) =>
|
onSelect={(e) => {
|
||||||
i === 0
|
e.preventDefault();
|
||||||
? [t.description]
|
toggleTool(tool.name);
|
||||||
: [
|
}}
|
||||||
<Dot
|
className={cn(
|
||||||
key={`dot-${group.label}-${t.description}`}
|
"mb-1 last:mb-0 transition-all",
|
||||||
className="inline h-4 w-4"
|
"hover:bg-accent hover:text-accent-foreground",
|
||||||
/>,
|
!isDisabled && "text-primary"
|
||||||
t.description,
|
|
||||||
]
|
|
||||||
)}
|
)}
|
||||||
</TooltipContent>
|
>
|
||||||
</Tooltip>
|
<ToolIcon className="h-4 w-4" />
|
||||||
);
|
<span className="flex-1 min-w-0 truncate">
|
||||||
})}
|
{formatToolName(tool.name)}
|
||||||
</div>
|
</span>
|
||||||
)}
|
<Switch
|
||||||
{!filteredTools?.length && (
|
checked={!isDisabled}
|
||||||
<div className="px-2 sm:px-2.5 pt-1.5 pb-1">
|
tabIndex={-1}
|
||||||
<Skeleton className="h-2 w-12 mb-1.5" />
|
className="pointer-events-none shrink-0 scale-[0.6]"
|
||||||
{["dt1", "dt2", "dt3", "dt4"].map((k) => (
|
/>
|
||||||
<div key={k} className="flex items-center gap-1.5 sm:gap-2 py-0.5 sm:py-1">
|
</DropdownMenuItem>
|
||||||
<Skeleton className="size-3 sm:size-3.5 rounded shrink-0" />
|
);
|
||||||
<Skeleton className="h-2.5 sm:h-3 flex-1" />
|
})}
|
||||||
<Skeleton className="h-3.5 sm:h-4 w-7 sm:w-8 rounded-full shrink-0" />
|
</div>
|
||||||
|
))}
|
||||||
|
{groupedTools.some((g) => g.connectorIcon) && (
|
||||||
|
<div>
|
||||||
|
<div className="px-2 pt-1.5 pb-0.5 text-[10px] text-muted-foreground/80 font-normal select-none">
|
||||||
|
Connector Actions
|
||||||
|
</div>
|
||||||
|
{groupedTools
|
||||||
|
.filter((g) => g.connectorIcon)
|
||||||
|
.map((group) => {
|
||||||
|
const iconKey = group.connectorIcon ?? "";
|
||||||
|
const iconInfo = CONNECTOR_TOOL_ICON_PATHS[iconKey];
|
||||||
|
const toolNames = group.tools.map((t) => t.name);
|
||||||
|
const allDisabled = toolNames.every((n) => disabledToolsSet.has(n));
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={group.label}
|
||||||
|
onSelect={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleToolGroup(toolNames);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"mb-1 last:mb-0 transition-all",
|
||||||
|
"hover:bg-accent hover:text-accent-foreground",
|
||||||
|
!allDisabled && "text-primary"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{iconInfo ? (
|
||||||
|
<Image
|
||||||
|
src={iconInfo.src}
|
||||||
|
alt={iconInfo.alt}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className="h-4 w-4 shrink-0 select-none pointer-events-none"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Wrench className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
<span className="flex-1 min-w-0 truncate">{group.label}</span>
|
||||||
|
<Switch
|
||||||
|
checked={!allDisabled}
|
||||||
|
tabIndex={-1}
|
||||||
|
className="pointer-events-none shrink-0 scale-[0.6]"
|
||||||
|
/>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
<Skeleton className="h-2 w-20 mt-2 mb-1.5" />
|
{!filteredTools?.length && (
|
||||||
{["dc1", "dc2", "dc3"].map((k) => (
|
<div className="px-2 pt-1.5 pb-1">
|
||||||
<div key={k} className="flex items-center gap-1.5 sm:gap-2 py-0.5 sm:py-1">
|
<Skeleton className="h-2 w-12 mb-1.5" />
|
||||||
<Skeleton className="size-3 sm:size-3.5 rounded shrink-0" />
|
{["dt1", "dt2", "dt3", "dt4"].map((k) => (
|
||||||
<Skeleton className="h-2.5 sm:h-3 flex-1" />
|
<div key={k} className="flex items-center gap-2 py-1">
|
||||||
<Skeleton className="h-3.5 sm:h-4 w-7 sm:w-8 rounded-full shrink-0" />
|
<Skeleton className="h-4 w-4 rounded shrink-0" />
|
||||||
|
<Skeleton className="h-3 flex-1" />
|
||||||
|
<Skeleton className="h-4 w-8 rounded-full shrink-0" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</DropdownMenuSubContent>
|
||||||
)}
|
</DropdownMenuPortal>
|
||||||
</div>
|
</DropdownMenuSub>
|
||||||
</PopoverContent>
|
</DropdownMenuContent>
|
||||||
</Popover>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
{hasWebSearchTool && (
|
{hasWebSearchTool && (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -1243,19 +1213,6 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{isDesktop && (
|
|
||||||
<TooltipIconButton
|
|
||||||
tooltip="Capture screen"
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="size-8 rounded-full"
|
|
||||||
aria-label="Capture screen"
|
|
||||||
onClick={() => void handleScreenCapture()}
|
|
||||||
>
|
|
||||||
<Camera className="size-4" />
|
|
||||||
</TooltipIconButton>
|
|
||||||
)}
|
|
||||||
<AuiIf condition={({ thread }) => !thread.isRunning}>
|
<AuiIf condition={({ thread }) => !thread.isRunning}>
|
||||||
<ComposerPrimitive.Send asChild disabled={isSendDisabled}>
|
<ComposerPrimitive.Send asChild disabled={isSendDisabled}>
|
||||||
<TooltipIconButton
|
<TooltipIconButton
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ function TooltipContent({
|
||||||
data-slot="tooltip-content"
|
data-slot="tooltip-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-neutral-800 text-white font-medium shadow-xl px-3 py-1.5 dark:bg-neutral-800 dark:text-white border-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md text-xs text-pretty pointer-events-none select-none",
|
"bg-neutral-800 text-white font-medium shadow-md shadow-neutral-950/40 px-3 py-1.5 dark:bg-neutral-800 dark:text-white border-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md text-xs text-pretty pointer-events-none select-none",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue