Skip to content

Commit

Permalink
enhance: improve tool catalog UX (#1045)
Browse files Browse the repository at this point in the history
- Display only bundle tools initially (when available) and require user action to display granular tool items
  • Loading branch information
ryanhopperlowe authored Dec 24, 2024
1 parent f03c070 commit a689d22
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 132 deletions.
70 changes: 8 additions & 62 deletions ui/admin/app/components/tools/ToolCatalog.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import { AlertTriangleIcon, PlusIcon } from "lucide-react";
import { useCallback } from "react";
import useSWR from "swr";

import { ToolReference } from "~/lib/model/toolReferences";
import { ToolReferenceService } from "~/lib/service/api/toolreferenceService";
import { cn } from "~/lib/utils";

import { ToolCategoryHeader } from "~/components/tools/ToolCategoryHeader";
import { ToolItem } from "~/components/tools/ToolItem";
import { ToolCatalogGroup } from "~/components/tools/ToolCatalogGroup";
import { LoadingSpinner } from "~/components/ui/LoadingSpinner";
import { Button } from "~/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandList,
} from "~/components/ui/command";
Expand Down Expand Up @@ -45,34 +41,6 @@ export function ToolCatalog({
{ fallbackData: {} }
);

const handleSelect = useCallback(
(toolId: string) => {
if (!tools.includes(toolId)) {
onUpdateTools([...tools, toolId]);
}
},
[tools, onUpdateTools]
);

const handleSelectBundle = useCallback(
(bundleToolId: string, categoryTools: ToolReference[]) => {
if (tools.includes(bundleToolId)) {
onUpdateTools(tools.filter((tool) => tool !== bundleToolId));
return;
}

const toolsToRemove = new Set(categoryTools.map((tool) => tool.id));

const newTools = [
...tools.filter((tool) => !toolsToRemove.has(tool)),
bundleToolId,
];

onUpdateTools(newTools);
},
[tools, onUpdateTools]
);

if (isLoading) return <LoadingSpinner />;

return (
Expand All @@ -98,39 +66,17 @@ export function ToolCatalog({
<h1 className="flex items-center justify-center">
<AlertTriangleIcon className="w-4 h-4 mr-2" />
No results found.
</h1>
</h1>{" "}
</CommandEmpty>
{Object.entries(toolCategories).map(
([category, categoryTools]) => (
<CommandGroup
<ToolCatalogGroup
key={category}
heading={
<ToolCategoryHeader
category={category}
categoryTools={categoryTools}
tools={tools}
onSelectBundle={handleSelectBundle}
/>
}
>
{categoryTools.tools.map((categoryTool) => (
<ToolItem
key={categoryTool.id}
tool={categoryTool}
isSelected={tools.includes(categoryTool.id)}
isBundleSelected={
categoryTools.bundleTool
? tools.includes(
categoryTools.bundleTool.id
)
: false
}
onSelect={() =>
handleSelect(categoryTool.id)
}
/>
))}
</CommandGroup>
category={category}
tools={categoryTools}
selectedTools={tools}
onUpdateTools={onUpdateTools}
/>
)
)}
</CommandList>
Expand Down
91 changes: 91 additions & 0 deletions ui/admin/app/components/tools/ToolCatalogGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { useState } from "react";

import { ToolCategory } from "~/lib/service/api/toolreferenceService";
import { cn } from "~/lib/utils";

import { ToolItem } from "~/components/tools/ToolItem";
import { CommandGroup } from "~/components/ui/command";

export function ToolCatalogGroup({
category,
tools,
selectedTools,
onUpdateTools,
}: {
category: string;
tools: ToolCategory;
selectedTools: string[];
onUpdateTools: (tools: string[]) => void;
}) {
const handleSelect = (toolId: string) => {
if (selectedTools.includes(toolId)) {
onUpdateTools(selectedTools.filter((tool) => tool !== toolId));
}

const newTools = selectedTools
.filter((tool) => tool !== tools.bundleTool?.id)
.concat(toolId);

onUpdateTools(newTools);
};

const handleSelectBundle = (bundleToolId: string) => {
if (selectedTools.includes(bundleToolId)) {
onUpdateTools(
selectedTools.filter((tool) => tool !== bundleToolId)
);
return;
}

const toolsToRemove = new Set(tools.tools.map((tool) => tool.id));

const newTools = [
...selectedTools.filter((tool) => !toolsToRemove.has(tool)),
bundleToolId,
];

onUpdateTools(newTools);
};

const [expanded, setExpanded] = useState(() => {
const set = new Set(tools.tools.map((tool) => tool.id));
return selectedTools.some((tool) => set.has(tool));
});

return (
<CommandGroup
key={category}
className={cn({
"has-[.group-heading:hover]:bg-accent": !!tools.bundleTool,
})}
heading={!tools.bundleTool ? category : undefined}
>
{tools.bundleTool && (
<ToolItem
tool={tools.bundleTool}
isSelected={selectedTools.includes(tools.bundleTool.id)}
isBundleSelected={false}
onSelect={() => handleSelectBundle(tools.bundleTool!.id)}
expanded={expanded}
onExpand={setExpanded}
isBundle
/>
)}

{(expanded || !tools.bundleTool) &&
tools.tools.map((categoryTool) => (
<ToolItem
key={categoryTool.id}
tool={categoryTool}
isSelected={selectedTools.includes(categoryTool.id)}
isBundleSelected={
tools.bundleTool
? selectedTools.includes(tools.bundleTool.id)
: false
}
onSelect={() => handleSelect(categoryTool.id)}
/>
))}
</CommandGroup>
);
}
57 changes: 0 additions & 57 deletions ui/admin/app/components/tools/ToolCategoryHeader.tsx

This file was deleted.

65 changes: 53 additions & 12 deletions ui/admin/app/components/tools/ToolItem.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,85 @@
import { ToolReference } from "~/lib/model/toolReferences";
import { cn } from "~/lib/utils";

import { ToolIcon } from "~/components/tools/ToolIcon";
import { ToolTooltip } from "~/components/tools/ToolTooltip";
import { Button } from "~/components/ui/button";
import { Checkbox } from "~/components/ui/checkbox";
import { CommandItem } from "~/components/ui/command";

type ToolItemProps = {
tool: ToolReference;
isSelected: boolean;
isBundleSelected: boolean;
onSelect: () => void;
expanded?: boolean;
onExpand?: (expanded: boolean) => void;
className?: string;
isBundle?: boolean;
};

export function ToolItem({
tool,
isSelected,
isBundleSelected,
onSelect,
expanded,
onExpand,
className,
isBundle,
}: ToolItemProps) {
return (
<CommandItem
className="cursor-pointer"
className={cn("cursor-pointer", className)}
keywords={[
tool.description || "",
tool.name || "",
tool.metadata?.category || "",
]}
onSelect={onSelect}
disabled={isSelected || isBundleSelected}
disabled={isBundleSelected}
>
<ToolTooltip tool={tool}>
<span className="text-sm font-medium flex items-center w-full px-2">
<ToolIcon
icon={tool.metadata?.icon}
category={tool.metadata?.category}
name={tool.name}
className="w-4 h-4 mr-2"
disableTooltip
/>
{tool.name}
</span>
<div
className={cn(
"flex justify-between items-center w-full gap-2"
)}
>
<span
className={cn(
"text-sm font-medium flex items-center w-full gap-2 px-4",
{
"px-0": isBundle,
}
)}
>
<Checkbox checked={isSelected || isBundleSelected} />

<span className={cn("flex items-center")}>
<ToolIcon
icon={tool.metadata?.icon}
category={tool.metadata?.category}
name={tool.name}
className="w-4 h-4 mr-2"
disableTooltip
/>
{tool.name}
</span>
</span>

{isBundle && (
<Button
variant="link"
size="link-sm"
onClick={(e) => {
e.stopPropagation();
onExpand?.(!expanded);
}}
>
{expanded ? "Show Less" : "Show More"}
</Button>
)}
</div>
</ToolTooltip>
</CommandItem>
);
Expand Down
2 changes: 2 additions & 0 deletions ui/admin/app/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const buttonVariants = cva(
},
size: {
none: "",
link: "p-0",
"link-sm": "p-0 text-xs",
default: "h-9 px-4 py-2",
badge: "text-xs py-0.5 px-2",
sm: "h-8 px-3 text-xs",
Expand Down
2 changes: 1 addition & 1 deletion ui/admin/app/lib/service/api/toolreferenceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ async function getToolReferencesCategoryMap(type?: ToolReferenceType) {
};
}

if (toolReference.metadata?.bundle) {
if (toolReference.metadata?.bundle === "true") {
result[category].bundleTool = toolReference;
} else {
result[category].tools.push(toolReference);
Expand Down

0 comments on commit a689d22

Please sign in to comment.