From 75f79798c2d25dd908b3411177c77790a51f47ad Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Mon, 13 Apr 2026 17:23:25 +0600 Subject: [PATCH 1/9] feat: enhance PopoverCascaderVariant with multi-select support, grouped item display, and adapter-driven filtering --- ...26-04-13-popover-cascader-update-design.md | 171 +++++ .../components/EvaluatorTemplateDropdown.tsx | 22 +- .../Components/PlaygroundHeader/index.tsx | 158 ++++- .../src/selection/adapters/createAdapter.ts | 2 + .../adapters/createAdapterFromRelations.ts | 3 + .../src/selection/adapters/types.ts | 9 + .../adapters/useEnrichedEvaluatorAdapter.ts | 44 +- .../workflowRevisionRelationAdapter.ts | 6 +- .../components/UnifiedEntityPicker/types.ts | 50 ++ .../variants/PopoverCascaderVariant.tsx | 630 ++++++++++++++---- .../agenta-entity-ui/src/selection/index.ts | 1 + .../agenta-entity-ui/src/selection/types.ts | 20 + 12 files changed, 963 insertions(+), 153 deletions(-) create mode 100644 docs/superpowers/specs/2026-04-13-popover-cascader-update-design.md diff --git a/docs/superpowers/specs/2026-04-13-popover-cascader-update-design.md b/docs/superpowers/specs/2026-04-13-popover-cascader-update-design.md new file mode 100644 index 0000000000..6dfad8dc06 --- /dev/null +++ b/docs/superpowers/specs/2026-04-13-popover-cascader-update-design.md @@ -0,0 +1,171 @@ +# PopoverCascaderVariant Update — Design Spec + +**Date:** 2026-04-13 +**Scope:** Extend `PopoverCascaderVariant` in `@agenta/entity-ui` with multi-select, adapter-driven tabs, grouped items, and layout improvements. +**Constraint:** All changes must be generic — usable by evaluators, testsets, app revisions, and any future entity type. No evaluator-specific logic in the component. + +--- + +## Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Multi-select | Generic component supports optional `multiSelect` mode | Any entity type may need multi-select | +| Tabs | Adapter-driven via `HierarchyLevel.tabs` | Evaluators define category tabs, testsets provide none — component is agnostic | +| Tabs & groups relationship | Tabs = group filters. "All" shows grouped items; specific tab filters to that group | Matches Figma design, one concept with two presentations | +| Action button position | Moves to top-right header row next to search | Consistent, higher visibility | +| Child panel header | Always shows parent name; multi-select adds count + "Select all" | Provides context in both modes | +| Select all callback | Separate `onSelectAll` for bulk; `onSelect` for individual toggle | Clean atomic `onSelect`, efficient batch path for "select all" | + +--- + +## 1. New & Modified Props on `PopoverCascaderVariantProps` + +All new props are optional. Existing usages work unchanged. + +```typescript +interface PopoverCascaderVariantProps { + // ── EXISTING (unchanged) ── + adapter, onSelect, instanceId, className, disabled, + size, placeholder, icon, showDropdownIcon, placement, + panelMinWidth, maxHeight, popupFooter, + selectedParentId, selectedChildId, + disabledChildIds, disabledChildTooltip, openChildOnHover, + + // ── EXISTING (kept, repositioned in UI) ── + onCreateNew // button moves from bottom to header row + createNewLabel // still works + + // ── NEW: Multi-select ── + multiSelect?: boolean // enables checkbox UI in child panel + selectedChildIds?: Set // controlled: which children are checked + onSelectAll?: (selections: TSelection[]) => void // bulk select/deselect + + // ── NEW: Selection summary ── + selectionSummary?: string // e.g., "No versions selected" — shown above root list +} +``` + +- `onSelect` remains the callback for both single-select clicks and multi-select checkbox toggles. +- In multi-select mode, `selectedChildId` is ignored in favor of `selectedChildIds`. +- `selectionSummary` is a simple string prop. Consumer owns the text. + +--- + +## 2. Adapter-Driven Tabs via `HierarchyLevel` Extension + +One new optional field on `HierarchyLevel`: + +```typescript +interface HierarchyLevel { + // ── EXISTING (all unchanged) ── + type, label, getId, getLabel, getLabelNode, getIcon, + getGroupKey, getGroupLabel, + listAtom, listAtomFamily, filterItems, ... + + // ── NEW ── + tabs?: TabDefinition[] +} + +interface TabDefinition { + key: string // matches getGroupKey() values, or "all" for the everything tab + label: string // display text, e.g., "AI/LLM", "Classifiers" +} +``` + +**How tabs interact with `getGroupKey` / `getGroupLabel`:** + +1. Adapter defines `tabs` on the root level (e.g., `[{key: "all", label: "All"}, {key: "ai_llm", label: "AI/LLM"}, ...]`). +2. Adapter defines `getGroupKey(entity)` returning the group key (e.g., `"ai_llm"`, `"classifiers"`). +3. Component renders: + - **"All" tab active**: Shows all items, grouped by `getGroupKey`, headers via `getGroupLabel`. + - **Specific tab active**: Filters to items where `getGroupKey(item) === tab.key`, no group headers. + +**When adapter provides no `tabs`:** No tabs render. Flat list (current behavior). + +--- + +## 3. Component Layout + +``` +┌─────────────────────────────────────────────────────────────┐ +│ HEADER ROW │ +│ ┌─────────────────────────┐ ┌────────────────────────┐ │ +│ │ Search ... │ │ + New evaluator │ │ +│ └─────────────────────────┘ └────────────────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ TABS (optional, only when adapter provides tabs) │ +│ All | AI/LLM | Classifiers | Similarity | Custom │ +├──────────────────────────────┬──────────────────────────────┤ +│ SELECTION SUMMARY (optional) │ CHILD PANEL HEADER │ +│ "No versions selected" │ │ +│ │ 0 of 4 selected Select all │ +├──────────────────────────────┼──────────────────────────────┤ +│ ROOT PANEL │ CHILD PANEL │ +│ │ │ +│ ── Group Header ────── │ [ ] v4 │ +│ item_name [Tag] │ [ ] v3 │ +│ 12 versions · Jan 6, 2026 │ [ ] v2 │ +│ │ [ ] v1 │ +│ item_name [Tag] │ │ +│ 12 versions · Jan 6, 2026 │ │ +│ │ │ +│ ── Group Header ───── │ │ +│ item_name [Tag] │ │ +│ │ │ +├──────────────────────────────┤ │ +│ POPUP FOOTER (optional) │ │ +└──────────────────────────────┴──────────────────────────────┘ +``` + +**Changes from current layout:** + +1. **Header row**: Search + action button side-by-side at top. `onCreateNew` button moves from bottom to here. `popupFooter` stays at the bottom for other uses (e.g., "Disconnect all"). +2. **Tabs row**: Renders below header when `rootLevel.tabs` is present. Active tab state is component-internal. +3. **Selection summary**: Optional text above root list when `selectionSummary` is provided. +4. **Group headers**: When "All" tab is active and `getGroupKey` is defined, items grouped with divider-style headers. When a specific tab is active, no group headers. +5. **Root item rendering**: No change to `EntityListItem`. Rich metadata (tags, counts, dates) handled by adapter's `getLabelNode`. +6. **Child panel header**: New. Always shows parent name. In multi-select mode, adds "X of Y selected" + "Select all" link. +7. **Child panel items**: Single-select: click to select (current). Multi-select: checkboxes. + +--- + +## 4. Behavioral Changes & Edge Cases + +**Popover close behavior:** +- Single-select (current): Clicking a child fires `onSelect` and closes the popover. No change. +- Multi-select: Toggling a checkbox fires `onSelect` but the popover stays open. User closes by clicking outside or pressing Escape. + +**Tab state:** +- Internal component state, defaults to the first tab (typically "All"). +- Resets to first tab when popover reopens (same as search resetting today). +- Search applies within the active tab's filtered items. + +**Select all / Deselect all:** +- Appears in child panel header when `multiSelect` is true. +- Some children selected: label shows "Select all", clicking calls `onSelectAll` with all unselected children. +- All children selected: label shows "Deselect all", clicking calls `onSelectAll` with an empty array (consumer interprets as "clear all for this parent"). + +**Disabled children in multi-select:** +- `disabledChildIds` still works. Disabled items show a checked-but-greyed-out checkbox and are excluded from "Select all". + +**Backward compatibility:** +- No `multiSelect` prop: single-select, no checkboxes, no count in child header. Same as today. +- No `tabs` on adapter: no tabs row. Same as today. +- No `selectionSummary`: no summary text. Same as today. +- `onCreateNew` still works: renders in new position (header row instead of bottom). +- `popupFooter` still works: stays at bottom of root panel. +- All existing consumers (playground header, evaluator config, workflow drawer) keep working with zero changes. + +--- + +## 5. Files to Modify + +| File | Change | +|------|--------| +| `web/packages/agenta-entity-ui/src/selection/types.ts` | Add `TabDefinition` type, add optional `tabs` field to `HierarchyLevel` | +| `web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/types.ts` | Add `multiSelect`, `selectedChildIds`, `onSelectAll`, `selectionSummary` to `PopoverCascaderVariantProps` | +| `web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx` | Implement header row layout, tabs, groups, multi-select child panel, child panel header | +| Evaluator adapter (when consumed) | Add `tabs` and `getGroupKey`/`getGroupLabel` to root level config | + +No new files needed. All changes extend existing files. diff --git a/web/oss/src/components/Evaluators/components/EvaluatorTemplateDropdown.tsx b/web/oss/src/components/Evaluators/components/EvaluatorTemplateDropdown.tsx index 97bb1b7f9a..18ec4beb23 100644 --- a/web/oss/src/components/Evaluators/components/EvaluatorTemplateDropdown.tsx +++ b/web/oss/src/components/Evaluators/components/EvaluatorTemplateDropdown.tsx @@ -26,6 +26,10 @@ interface EvaluatorTemplateDropdownProps { trigger?: React.ReactNode /** Additional class name for the trigger wrapper */ className?: string + /** Controlled open state (optional — when provided, component is controlled) */ + open?: boolean + /** Callback when open state changes (required when using controlled `open`) */ + onOpenChange?: (open: boolean) => void } /** @@ -36,9 +40,25 @@ const EvaluatorTemplateDropdown = ({ onSelect, trigger, className, + open: controlledOpen, + onOpenChange: controlledOnOpenChange, }: EvaluatorTemplateDropdownProps) => { const [activeTab, setActiveTab] = useState(DEFAULT_TAB_KEY) - const [open, setOpen] = useState(false) + const [internalOpen, setInternalOpen] = useState(false) + + // Support both controlled and uncontrolled modes + const isControlled = controlledOpen !== undefined + const open = isControlled ? controlledOpen : internalOpen + const setOpen = useCallback( + (next: boolean) => { + if (isControlled) { + controlledOnOpenChange?.(next) + } else { + setInternalOpen(next) + } + }, + [isControlled, controlledOnOpenChange], + ) const nonArchivedEvaluators = useAtomValue(evaluatorTemplatesDataAtom) const {isPending: isLoadingEvaluators} = useAtomValue(evaluatorTemplatesQueryAtom) diff --git a/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx b/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx index 60f9a18a6a..94153b38a2 100644 --- a/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx +++ b/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx @@ -1,8 +1,12 @@ -import React, {useCallback, useMemo} from "react" +import React, {useCallback, useMemo, useState} from "react" import type {PlaygroundNode} from "@agenta/entities/runnable" -import {getEvaluatorColor, workflowMolecule} from "@agenta/entities/workflow" -import type {EvaluatorColor} from "@agenta/entities/workflow" +import { + getEvaluatorColor, + workflowMolecule, + createEvaluatorFromTemplate, +} from "@agenta/entities/workflow" +import type {EvaluatorColor, EvaluatorCatalogTemplate} from "@agenta/entities/workflow" import {EntityPicker} from "@agenta/entity-ui" import {type WorkflowRevisionSelectionResult} from "@agenta/entity-ui/selection" import {useEnrichedEvaluatorOnlyAdapter as useEvaluatorOnlyAdapter} from "@agenta/entity-ui/selection" @@ -12,14 +16,16 @@ import {bgColors, textColors} from "@agenta/ui" import {VersionBadge} from "@agenta/ui/components/presentational" import {CloseOutlined, DownOutlined, MoreOutlined} from "@ant-design/icons" import {Gavel, PencilSimple, Plus} from "@phosphor-icons/react" -import {Button, Divider, Dropdown, Space, Tag, Tooltip, Typography} from "antd" +import {Button, Divider, Dropdown, Space, Tag, Tooltip, Typography, message} from "antd" import clsx from "clsx" import {useAtomValue, useSetAtom} from "jotai" import dynamic from "next/dynamic" +import EvaluatorTemplateDropdown from "@/oss/components/Evaluators/components/EvaluatorTemplateDropdown" import useCustomWorkflowConfig from "@/oss/components/pages/app-management/modals/CustomWorkflowModal/hooks/useCustomWorkflowConfig" import {currentAppAtom} from "@/oss/state/app" import {routerAppIdAtom} from "@/oss/state/app/selectors/app" +import {openEvaluatorDrawerAtom} from "@/oss/state/evaluator/evaluatorDrawerStore" import {writePlaygroundSelectionToQuery} from "@/oss/state/url/playground" import {workspaceMemberByIdFamily} from "@/oss/state/workspace/atoms/selectors" @@ -168,24 +174,6 @@ const PlaygroundHeader: React.FC = ({className, ...divPro [connectedEvaluatorNodes], ) - const handleEvaluatorSelect = useCallback( - (selection: WorkflowRevisionSelectionResult) => { - const rootNode = nodes.find((n) => n.depth === 0) - if (!rootNode) return - - connectDownstreamNode({ - sourceNodeId: rootNode.id, - entity: { - type: "workflow", - id: selection.id, - label: selection.label, - metadata: selection.metadata, - }, - }) - }, - [nodes, connectDownstreamNode], - ) - const handleDisconnectAll = useCallback(() => { disconnectDownstreamNode("workflow") }, [disconnectDownstreamNode]) @@ -200,6 +188,114 @@ const PlaygroundHeader: React.FC = ({className, ...divPro // Evaluator-only adapter with colored type tags, human filtering, and custom revision labels const evaluatorWorkflowAdapter = useEvaluatorOnlyAdapter(renderWorkflowRevisionLabel) + // Controlled state for EvaluatorTemplateDropdown + const [templateDropdownOpen, setTemplateDropdownOpen] = useState(false) + + // Open the evaluator template dropdown (called from EntityPicker's onCreateNew) + const handleOpenTemplateDropdown = useCallback(() => { + // Small delay to let the EntityPicker popover close first + setTimeout(() => { + setTemplateDropdownOpen(true) + }, 100) + }, []) + + const openEvaluatorDrawer = useSetAtom(openEvaluatorDrawerAtom) + + // Handle template selection from EvaluatorTemplateDropdown + const handleTemplateSelect = useCallback( + async (template: EvaluatorCatalogTemplate) => { + const templateKey = template.key + if (!templateKey) { + message.error("Unable to open evaluator template") + return + } + + const localId = await createEvaluatorFromTemplate(templateKey) + if (!localId) { + message.error("Unable to create evaluator from template") + return + } + + openEvaluatorDrawer({ + entityId: localId, + mode: "create", + }) + }, + [openEvaluatorDrawer], + ) + + // Multi-select: toggle evaluator connection/disconnection + const handleEvaluatorToggle = useCallback( + (selection: WorkflowRevisionSelectionResult) => { + const rootNode = nodes.find((n) => n.depth === 0) + if (!rootNode) return + + // Check if this revision is already connected + const existingNode = connectedEvaluatorNodes.find((n) => n.entityId === selection.id) + + if (existingNode) { + // Disconnect + disconnectSingleDownstreamNode(existingNode.id) + } else { + // Connect + connectDownstreamNode({ + sourceNodeId: rootNode.id, + entity: { + type: "workflow", + id: selection.id, + label: selection.label, + metadata: selection.metadata, + }, + }) + } + }, + [nodes, connectedEvaluatorNodes, connectDownstreamNode, disconnectSingleDownstreamNode], + ) + + // Multi-select: bulk connect or disconnect + const handleEvaluatorsSelectAll = useCallback( + ( + selections: WorkflowRevisionSelectionResult[], + action: "select" | "deselect", + parentId: string, + ) => { + const rootNode = nodes.find((n) => n.depth === 0) + if (!rootNode) return + + if (action === "select") { + // Connect all new selections + selections.forEach((selection) => { + connectDownstreamNode({ + sourceNodeId: rootNode.id, + entity: { + type: "workflow", + id: selection.id, + label: selection.label, + metadata: selection.metadata, + }, + }) + }) + } else if (action === "deselect") { + // Determine which nodes belong to the selections and remove them + const selectionIds = new Set(selections.map((s) => s.id)) + const nodesToRemove = connectedEvaluatorNodes.filter((n) => + selectionIds.has(n.entityId), + ) + nodesToRemove.forEach((node) => { + disconnectSingleDownstreamNode(node.id) + }) + } + }, + [nodes, connectedEvaluatorNodes, connectDownstreamNode, disconnectSingleDownstreamNode], + ) + + // Selection summary text + const selectionSummary = useMemo(() => { + const count = connectedEvaluatorNodes.length + if (count === 0) return "No evaluators selected" + return `${count} evaluator${count === 1 ? "" : "s"} selected` + }, [connectedEvaluatorNodes.length]) + // Simplified refresh function - atoms will handle the data updates automatically const handleUpdate = useCallback(async () => { // For now, use a simple page reload since atoms auto-refresh on mount @@ -298,15 +394,21 @@ const PlaygroundHeader: React.FC = ({className, ...divPro variant="popover-cascader" adapter={evaluatorWorkflowAdapter} - onSelect={handleEvaluatorSelect} + onSelect={handleEvaluatorToggle} + onSelectAll={handleEvaluatorsSelectAll} size="small" placeholder="Evaluator" icon={} disabled={!hasRootNode} - disabledChildIds={connectedRevisionIds} + multiSelect + selectedChildIds={connectedRevisionIds} + selectionSummary={selectionSummary} + childItemLabelMode="simple" + onCreateNew={handleOpenTemplateDropdown} + createNewLabel="New evaluator" popupFooter={ connectedEvaluatorNodes.length > 0 ? ( -
+
+ )} +
+ + {/* Child items */} +
+ {filteredItems.length === 0 ? ( + + ) : multiSelect ? ( + // Multi-select: checkboxes + filteredItems.map((item) => { + const itemId = childLevelConfig.getId(item) + const label = childLevelConfig.getLabel(item) + const labelNode = getItemLabelNode?.(item) + const isDisabled = disabledIds?.has(itemId) ?? false + const isChecked = selectedChildIds?.has(itemId) ?? false + + return ( +
{ + if (!isDisabled) onSelect(item) + }} + > + + + {labelNode ?? label} + +
+ ) + }) + ) : ( + // Single-select: click items + filteredItems.map((item) => { + const itemId = childLevelConfig.getId(item) + const label = childLevelConfig.getLabel(item) + const labelNode = getItemLabelNode?.(item) + const isSelected = itemId === selectedId + const isDisabled = disabledIds?.has(itemId) ?? false + + return ( + !isDisabled && onSelect(item)} + onSelect={() => !isDisabled && onSelect(item)} + className="!py-1.5" + /> + ) + }) + )} +
+
) +} + +// ============================================================================ +// ROOT ITEM RENDERER (shared between grouped and flat rendering) +// ============================================================================ +function RootItemRenderer({ + item, + rootLevel, + totalLevels, + selectedParentId, + selectedRootId, + openChildOnHover, + onRootItemClick, +}: { + item: unknown + rootLevel: HierarchyLevel + totalLevels: number + selectedParentId?: string | null + selectedRootId: string | null + openChildOnHover: boolean + onRootItemClick: (item: unknown) => void +}) { + const id = rootLevel.getId(item) return ( - +
1 ? () => onRootItemClick(item) : undefined + } + > + 1} + isSelectable={totalLevels <= 1} + isSelected={id === selectedParentId} + isHovered={id === selectedRootId} + onClick={() => onRootItemClick(item)} + onSelect={() => onRootItemClick(item)} + /> +
) } @@ -119,6 +334,12 @@ export function PopoverCascaderVariant({ disabledChildIds, disabledChildTooltip = "Already connected", openChildOnHover = false, + // New props + multiSelect = false, + selectedChildIds, + onSelectAll, + selectionSummary, + childItemLabelMode = "full", }: PopoverCascaderVariantProps) { const {hierarchyLevels, createSelection} = useEntitySelectionCore({ adapter: adapterProp, @@ -136,6 +357,10 @@ export function PopoverCascaderVariant({ const [selectedRootId, setSelectedRootId] = useState(null) const [selectedRootEntity, setSelectedRootEntity] = useState(null) + // Tab state (driven by adapter's rootLevel.tabs) + const tabs = rootLevel?.tabs + const [activeTabKey, setActiveTabKey] = useState(tabs?.[0]?.key ?? "all") + // Fetch root items const {items: rootItems, query: rootQuery} = useLevelData({ levelConfig: rootLevel, @@ -150,16 +375,84 @@ export function PopoverCascaderVariant({ return rootItems.filter((item) => rootLevel.getLabel(item).toLowerCase().includes(term)) }, [rootItems, searchTerm, rootLevel]) + // Filter by active tab + const tabFilteredRootItems = useMemo(() => { + if (!tabs || activeTabKey === "all") return filteredRootItems + if (!rootLevel.getGroupKey) return filteredRootItems + return filteredRootItems.filter((item) => rootLevel.getGroupKey!(item) === activeTabKey) + }, [filteredRootItems, tabs, activeTabKey, rootLevel]) + + // Group items for display (only when "all" tab is active and getGroupKey exists) + const groupedItems = useMemo(() => { + if (!tabs || activeTabKey !== "all" || !rootLevel.getGroupKey) { + return null // No grouping — render flat list + } + const groups = new Map() + const ungrouped: unknown[] = [] + + for (const item of tabFilteredRootItems) { + const key = rootLevel.getGroupKey(item) + if (key) { + if (!groups.has(key)) groups.set(key, []) + groups.get(key)!.push(item) + } else { + ungrouped.push(item) + } + } + + return {groups, ungrouped} + }, [tabs, activeTabKey, rootLevel, tabFilteredRootItems]) + + // Maintain auto-selection to prevent pixel shifts when searching/filtering useEffect(() => { - if (!open || selectedRootId || !selectedParentId) return + if (!open || totalLevels <= 1) return + + // Wait until rootItems are loaded + if (rootQuery.isPending && rootItems.length === 0) return + + // On open/mount, if we have a parent ID pre-selected and no root ID is selected locally yet + if (!selectedRootId && selectedParentId) { + const matchingRoot = rootItems.find( + (item) => rootLevel.getId(item) === selectedParentId, + ) + if (matchingRoot) { + setSelectedRootId(selectedParentId) + setSelectedRootEntity(matchingRoot) + hierarchyLevels[1]?.onBeforeLoad?.(selectedParentId) + return + } + } - const matchingRoot = rootItems.find((item) => rootLevel.getId(item) === selectedParentId) - if (!matchingRoot) return + // If something is already selected locally, ensure it's still in the filtered view + if (selectedRootId) { + const stillExists = tabFilteredRootItems.some( + (item) => rootLevel.getId(item) === selectedRootId, + ) + if (stillExists) return + } - setSelectedRootId(selectedParentId) - setSelectedRootEntity(matchingRoot) - hierarchyLevels[1]?.onBeforeLoad?.(selectedParentId) - }, [hierarchyLevels, open, rootItems, rootLevel, selectedParentId, selectedRootId]) + // Auto-select the first available item in the filtered view (UI ONLY, don't trigger selection) + if (tabFilteredRootItems.length > 0) { + const firstItem = tabFilteredRootItems[0] + const id = rootLevel.getId(firstItem) + setSelectedRootId(id) + setSelectedRootEntity(firstItem) + hierarchyLevels[1]?.onBeforeLoad?.(id) + } else { + setSelectedRootId(null) + setSelectedRootEntity(null) + } + }, [ + open, + totalLevels, + selectedRootId, + selectedParentId, + tabFilteredRootItems, + rootLevel, + hierarchyLevels, + rootQuery.isPending, + rootItems, + ]) // Handle root item click const handleRootItemClick = useCallback( @@ -208,119 +501,206 @@ export function PopoverCascaderVariant({ const selection = createSelection(path, childEntity) onSelect?.(selection) - setOpen(false) - setSelectedRootId(null) - setSelectedRootEntity(null) + + // Only close popover in single-select mode + if (!multiSelect) { + setOpen(false) + setSelectedRootId(null) + setSelectedRootEntity(null) + } }, - [selectedRootId, selectedRootEntity, rootLevel, hierarchyLevels, createSelection, onSelect], + [ + selectedRootId, + selectedRootEntity, + rootLevel, + hierarchyLevels, + createSelection, + onSelect, + multiSelect, + ], ) // Reset state when popover closes - const handleOpenChange = useCallback((newOpen: boolean) => { - setOpen(newOpen) - if (!newOpen) { - setSearchTerm("") - setSelectedRootId(null) - setSelectedRootEntity(null) - } - }, []) + const handleOpenChange = useCallback( + (newOpen: boolean) => { + setOpen(newOpen) + if (!newOpen) { + setSearchTerm("") + setSelectedRootId(null) + setSelectedRootEntity(null) + setActiveTabKey(tabs?.[0]?.key ?? "all") + } + }, + [tabs], + ) const handleCreateNew = useCallback(() => { onCreateNew?.() setOpen(false) }, [onCreateNew]) + // Shared props for RootItemRenderer + const rootItemProps = useMemo( + () => ({ + rootLevel, + totalLevels, + selectedParentId, + selectedRootId, + openChildOnHover, + onRootItemClick: handleRootItemClick, + }), + [ + rootLevel, + totalLevels, + selectedParentId, + selectedRootId, + openChildOnHover, + handleRootItemClick, + ], + ) + // Popover content const content = ( -
- {/* ROOT PANEL */} -
- {/* Search */} -
+
+ {/* HEADER ROW: Search + Action Button */} +
+
+ {onCreateNew && ( + + )} +
+ + {/* TABS (optional, only when adapter provides tabs) */} + {tabs && tabs.length > 0 && ( + ({ + key: tab.key, + label: tab.label, + }))} + size="small" + tabBarGutter={16} + className={cn( + "[&_.ant-tabs-nav]:px-3 [&_.ant-tabs-nav]:mb-0 [&_.ant-tabs-nav::before]:border-b-0", + "[&_.ant-tabs-tab]:text-xs [&_.ant-tabs-tab]:py-2", + "[&_.ant-tabs-nav-wrap]:pb-0", + "border-0 border-b border-solid border-[rgba(5,23,41,0.06)]", + )} + /> + )} - {/* Root items */} -
- {rootQuery.isPending ? ( -
- + {/* PANELS: Root + Child side-by-side */} +
+ {/* ROOT PANEL */} +
+ {/* Selection summary */} + {selectionSummary ? ( +
+ {selectionSummary}
- ) : filteredRootItems.length === 0 ? ( - ) : ( - filteredRootItems.map((item) => { - const id = rootLevel.getId(item) - return ( -
1 - ? () => handleRootItemClick(item) - : undefined - } - > - 1} - isSelectable={totalLevels <= 1} - isSelected={id === selectedParentId} - isHovered={id === selectedRootId} - onClick={() => handleRootItemClick(item)} - onSelect={() => handleRootItemClick(item)} - /> -
- ) - }) +
)} + + {/* Root items */} +
+ {rootQuery.isPending ? ( +
+ +
+ ) : tabFilteredRootItems.length === 0 ? ( + + ) : groupedItems ? ( + // Grouped rendering (when "All" tab is active with getGroupKey) + <> + {Array.from(groupedItems.groups.entries()).map( + ([groupKey, items]) => ( +
+
+ + {rootLevel.getGroupLabel?.(groupKey) ?? + groupKey} + +
+
+ {items.map((item) => ( + + ))} +
+ ), + )} + {groupedItems.ungrouped.length > 0 && + groupedItems.ungrouped.map((item) => ( + + ))} + + ) : ( + // Flat rendering (no grouping) + tabFilteredRootItems.map((item) => ( + + )) + )} +
+ + {/* Footer (e.g., Disconnect all) */} + {popupFooter}
- {/* Create new button */} - {onCreateNew && ( -
- + {/* CHILD PANEL */} + {selectedRootId && totalLevels > 1 && ( +
+ + parentId={selectedRootId} + parentLabel={rootLevel.getLabel(selectedRootEntity!)} + childLevelConfig={hierarchyLevels[1]} + onSelect={handleChildSelect} + selectedId={ + selectedRootId === selectedParentId ? selectedChildId : null + } + maxHeight={maxHeight} + panelWidth={panelMinWidth} + disabledIds={disabledChildIds} + disabledTooltip={disabledChildTooltip} + multiSelect={multiSelect} + selectedChildIds={selectedChildIds} + onSelectAll={onSelectAll} + createSelection={createSelection} + rootLevel={rootLevel} + rootEntity={selectedRootEntity} + hierarchyLevels={hierarchyLevels} + childItemLabelMode={childItemLabelMode} + />
)} - - {/* Footer (e.g., Disconnect all) */} - {popupFooter}
- - {/* CHILD PANEL */} - {selectedRootId && totalLevels > 1 && ( -
- -
- )}
) diff --git a/web/packages/agenta-entity-ui/src/selection/index.ts b/web/packages/agenta-entity-ui/src/selection/index.ts index 747d61c78f..6f46018939 100644 --- a/web/packages/agenta-entity-ui/src/selection/index.ts +++ b/web/packages/agenta-entity-ui/src/selection/index.ts @@ -35,6 +35,7 @@ export type { EntitySelectorConfig, EntitySelectorResolver, ListQueryState, + TabDefinition, // Pagination types PaginationParams, PaginationInfo, diff --git a/web/packages/agenta-entity-ui/src/selection/types.ts b/web/packages/agenta-entity-ui/src/selection/types.ts index 37fb928c27..cae3e40386 100644 --- a/web/packages/agenta-entity-ui/src/selection/types.ts +++ b/web/packages/agenta-entity-ui/src/selection/types.ts @@ -119,6 +119,18 @@ export interface PaginatedListQueryState extends ListQueryState { fetchNextPage?: () => void } +/** + * Defines a tab for filtering items by group. + * Tabs map to `getGroupKey` values — selecting a tab filters items to that group. + * The "all" key is special: it shows all items grouped by their `getGroupKey`. + */ +export interface TabDefinition { + /** Group key this tab filters to, or "all" to show all items grouped */ + key: string + /** Display label for the tab (e.g., "AI/LLM", "Classifiers") */ + label: string +} + /** * Defines a level in the entity hierarchy */ @@ -266,6 +278,14 @@ export interface HierarchyLevel { * Falls back to the key itself if not provided. */ getGroupLabel?: (key: string) => string + + /** + * Optional tab definitions for filtering items by group. + * When provided, the component renders tabs above the item list. + * Each tab filters items by `getGroupKey` match. The "all" key shows all items grouped. + * Requires `getGroupKey` to be defined for meaningful filtering. + */ + tabs?: TabDefinition[] } /** From a39e27238aea017c4d7ee8c779d456d59a3a4bb8 Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Wed, 15 Apr 2026 16:57:17 +0600 Subject: [PATCH 2/9] refactor and fix --- .../Components/PlaygroundHeader/index.tsx | 9 +--- .../components/UnifiedEntityPicker/types.ts | 8 +-- .../variants/PopoverCascaderVariant.tsx | 50 ++++++++++++------- 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx b/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx index 94153b38a2..c9c6905d06 100644 --- a/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx +++ b/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx @@ -289,13 +289,6 @@ const PlaygroundHeader: React.FC = ({className, ...divPro [nodes, connectedEvaluatorNodes, connectDownstreamNode, disconnectSingleDownstreamNode], ) - // Selection summary text - const selectionSummary = useMemo(() => { - const count = connectedEvaluatorNodes.length - if (count === 0) return "No evaluators selected" - return `${count} evaluator${count === 1 ? "" : "s"} selected` - }, [connectedEvaluatorNodes.length]) - // Simplified refresh function - atoms will handle the data updates automatically const handleUpdate = useCallback(async () => { // For now, use a simple page reload since atoms auto-refresh on mount @@ -402,7 +395,7 @@ const PlaygroundHeader: React.FC = ({className, ...divPro disabled={!hasRootNode} multiSelect selectedChildIds={connectedRevisionIds} - selectionSummary={selectionSummary} + selectionSummary childItemLabelMode="simple" onCreateNew={handleOpenTemplateDropdown} createNewLabel="New evaluator" diff --git a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/types.ts b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/types.ts index 331a463e05..1a5d1382ba 100644 --- a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/types.ts +++ b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/types.ts @@ -684,11 +684,11 @@ export interface PopoverCascaderVariantProps< // ======================================================================== /** - * Optional summary text displayed above the root item list. - * Consumer controls the content (e.g., "No versions selected", "3 versions selected"). - * When omitted, no summary is rendered. + * Whether to display an internally generated selection summary above the root item list. + * The picker derives the summary text from the current selection state. + * @default false */ - selectionSummary?: string + selectionSummary?: boolean } // ============================================================================ diff --git a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx index 413f54e480..f8193360e3 100644 --- a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx +++ b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx @@ -179,23 +179,25 @@ function ChildPanelContent({ return (
{/* Child panel header */} -
-
- - {parentLabel} - - {multiSelect && ( - - {selectedCount} of {filteredItems.length} selected + {multiSelect && ( +
+
+ + {parentLabel} + {multiSelect && ( + + {selectedCount} of {filteredItems.length} selected + + )} +
+ {multiSelect && enabledChildren.length > 0 && ( + )}
- {multiSelect && enabledChildren.length > 0 && ( - - )} -
+ )} {/* Child items */}
@@ -403,6 +405,16 @@ export function PopoverCascaderVariant({ return {groups, ungrouped} }, [tabs, activeTabKey, rootLevel, tabFilteredRootItems]) + const selectionSummaryText = useMemo(() => { + if (!selectionSummary) return null + + const selectionCount = selectedChildIds?.size ?? (selectedChildId ? 1 : 0) + + if (selectionCount === 0) return "No selections" + if (selectionCount === 1) return "1 selected" + return `${selectionCount} selected` + }, [selectionSummary, selectedChildIds, selectedChildId]) + // Maintain auto-selection to prevent pixel shifts when searching/filtering useEffect(() => { if (!open || totalLevels <= 1) return @@ -606,13 +618,13 @@ export function PopoverCascaderVariant({ style={{minWidth: panelMinWidth}} > {/* Selection summary */} - {selectionSummary ? ( + {selectionSummaryText ? (
- {selectionSummary} + + {selectionSummaryText} +
- ) : ( -
- )} + ) : null} {/* Root items */}
From f55f5b8bcc29c7d762d7e214cd14e8a6ee604a94 Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Wed, 15 Apr 2026 19:59:13 +0600 Subject: [PATCH 3/9] fix tabs and aligned the human eval selector ui --- .../loadables/test_loadable_strategies.py | 23 +- .../2026-04-15-dynamic-tabs-buildtabs.md | 376 ++++++++++++++++++ .../p/[project_id]/annotations/index.tsx | 9 +- .../components/AnnotationQueuesView/index.tsx | 13 +- .../EntityEvaluatorSelector.tsx | 6 + .../components/CreateQueueDrawer/index.tsx | 16 +- .../src/selection/adapters/createAdapter.ts | 2 +- .../adapters/createAdapterFromRelations.ts | 7 +- .../adapters/createLevelFromRelation.ts | 7 + .../selection/adapters/evaluatorLabelUtils.ts | 7 +- .../src/selection/adapters/types.ts | 6 +- .../adapters/useEnrichedEvaluatorAdapter.ts | 26 +- .../workflowRevisionRelationAdapter.ts | 4 +- .../variants/PopoverCascaderVariant.tsx | 36 +- .../agenta-entity-ui/src/selection/types.ts | 6 +- 15 files changed, 488 insertions(+), 56 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-15-dynamic-tabs-buildtabs.md diff --git a/api/oss/tests/pytest/acceptance/loadables/test_loadable_strategies.py b/api/oss/tests/pytest/acceptance/loadables/test_loadable_strategies.py index 0428e2cdf5..5187cf6c19 100644 --- a/api/oss/tests/pytest/acceptance/loadables/test_loadable_strategies.py +++ b/api/oss/tests/pytest/acceptance/loadables/test_loadable_strategies.py @@ -881,10 +881,15 @@ def test_edge_a1_query_windowing_limit_1(self, authed_api, mock_data): "include_trace_ids": True, "windowing": {"limit": 1}, }, - condition_fn=lambda r: len( - r.json().get("query_revision", {}).get("data", {}).get("trace_ids", []) - ) - == 1, + condition_fn=lambda r: ( + len( + r.json() + .get("query_revision", {}) + .get("data", {}) + .get("trace_ids", []) + ) + == 1 + ), ) # --------------------------------------------------------------------- @@ -919,10 +924,12 @@ def test_edge_a2_query_request_windowing_overrides_stored( "include_traces": True, "windowing": {"limit": 1}, }, - condition_fn=lambda r: len( - r.json().get("query_revision", {}).get("data", {}).get("traces", []) - ) - == 1, + condition_fn=lambda r: ( + len( + r.json().get("query_revision", {}).get("data", {}).get("traces", []) + ) + == 1 + ), ) # --------------------------------------------------------------------- diff --git a/docs/superpowers/plans/2026-04-15-dynamic-tabs-buildtabs.md b/docs/superpowers/plans/2026-04-15-dynamic-tabs-buildtabs.md new file mode 100644 index 0000000000..2f1a435b4c --- /dev/null +++ b/docs/superpowers/plans/2026-04-15-dynamic-tabs-buildtabs.md @@ -0,0 +1,376 @@ +# Dynamic Tabs via `buildTabs` Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the static `tabs?: TabDefinition[]` field on `HierarchyLevel` with a `buildTabs(items) => TabDefinition[]` function so that the `PopoverCascaderVariant` derives tabs dynamically from loaded data — only showing tabs for categories that have items. + +**Architecture:** Four files need changes in order: (1) the core `HierarchyLevel` type, (2) the adapter-level `LevelOverride` interface and its merge helper, (3) the workflow revision adapter's `grandparentOverrides` interface and pass-through, and (4) the `PopoverCascaderVariant` component that swaps static tab reading for a `useMemo` call. A fifth file wires the evaluator-specific `buildTabs` logic in `useEnrichedEvaluatorOnlyAdapter`. + +**Tech Stack:** TypeScript, React (useMemo, useState, useCallback), Jotai, Ant Design Tabs + +--- + +## File Map + +| File | Change | +|---|---| +| `web/packages/agenta-entity-ui/src/selection/types.ts` | Replace `tabs?: TabDefinition[]` with `buildTabs?: (items: T[]) => TabDefinition[]` on `HierarchyLevel` | +| `web/packages/agenta-entity-ui/src/selection/adapters/createAdapterFromRelations.ts` | Same replacement on `LevelOverride`; update `applyOverrides` pass-through | +| `web/packages/agenta-entity-ui/src/selection/adapters/workflowRevisionRelationAdapter.ts` | Update `grandparentOverrides` interface; update the skipVariantLevel pass-through | +| `web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx` | Swap static `rootLevel?.tabs` for `useMemo(() => rootLevel?.buildTabs?.(rootItems))` | +| `web/packages/agenta-entity-ui/src/selection/adapters/useEnrichedEvaluatorAdapter.ts` | Replace hardcoded `tabs: [...]` with `buildTabs: (entities) => {...}` closure | + +--- + +## Task 1: Update `HierarchyLevel` type + +**Files:** +- Modify: `web/packages/agenta-entity-ui/src/selection/types.ts:282-288` + +- [ ] **Step 1: Replace the `tabs` field with `buildTabs`** + +In `types.ts`, find lines 282–288 (the JSDoc + `tabs?: TabDefinition[]` line) and replace: + +```typescript +// Before (lines 282-288): + /** + * Optional tab definitions for filtering items by group. + * When provided, the component renders tabs above the item list. + * Each tab filters items by `getGroupKey` match. The "all" key shows all items grouped. + * Requires `getGroupKey` to be defined for meaningful filtering. + */ + tabs?: TabDefinition[] + +// After: + /** + * Optional function that derives tab definitions from loaded items. + * Called after items load — only shows tabs for groups that actually have data. + * Each tab filters items by `getGroupKey` match. The "all" key shows all items grouped. + * Requires `getGroupKey` to be defined for meaningful filtering. + */ + buildTabs?: (items: T[]) => TabDefinition[] +``` + +- [ ] **Step 2: Type-check** + +```bash +cd web/packages/agenta-entity-ui && pnpm types:check 2>&1 | head -40 +``` + +Expected: Errors for `tabs` usages in other files (those get fixed in subsequent tasks). No errors inside `types.ts` itself. + +--- + +## Task 2: Update `LevelOverride` in `createAdapterFromRelations.ts` + +**Files:** +- Modify: `web/packages/agenta-entity-ui/src/selection/adapters/createAdapterFromRelations.ts:87` (interface) +- Modify: `web/packages/agenta-entity-ui/src/selection/adapters/createAdapterFromRelations.ts:235` (applyOverrides) + +- [ ] **Step 1: Replace `tabs` in the `LevelOverride` interface** + +Find line ~87 and replace: + +```typescript +// Before: + /** Tab definitions for filtering items by group */ + tabs?: import("../types").TabDefinition[] + +// After: + /** Function to derive tab definitions from loaded items */ + buildTabs?: (items: T[]) => import("../types").TabDefinition[] +``` + +- [ ] **Step 2: Update `applyOverrides` pass-through** + +Find line ~235 (inside the `applyOverrides` function) and replace: + +```typescript +// Before: + tabs: overrides.tabs ?? baseLevel.tabs, + +// After: + buildTabs: overrides.buildTabs ?? baseLevel.buildTabs, +``` + +- [ ] **Step 3: Type-check** + +```bash +cd web/packages/agenta-entity-ui && pnpm types:check 2>&1 | head -40 +``` + +Expected: Remaining errors only in `workflowRevisionRelationAdapter.ts` and `useEnrichedEvaluatorAdapter.ts` and `PopoverCascaderVariant.tsx` — the files we haven't fixed yet. + +--- + +## Task 3: Update `workflowRevisionRelationAdapter.ts` + +**Files:** +- Modify: `web/packages/agenta-entity-ui/src/selection/adapters/workflowRevisionRelationAdapter.ts:294-299` (interface) +- Modify: `web/packages/agenta-entity-ui/src/selection/adapters/workflowRevisionRelationAdapter.ts:480` (pass-through) + +- [ ] **Step 1: Update `grandparentOverrides` interface** + +Find the `grandparentOverrides` block in `CreateWorkflowRevisionAdapterOptions` (around line 294) and replace the `tabs` field: + +```typescript +// Before: + tabs?: import("../types").TabDefinition[] + +// After: + buildTabs?: (items: unknown[]) => import("../types").TabDefinition[] +``` + +The full updated `grandparentOverrides` block should look like: + +```typescript + grandparentOverrides?: { + getLabelNode?: (entity: unknown) => React.ReactNode + getGroupKey?: (entity: unknown) => string | null | undefined + getGroupLabel?: (key: string) => string + buildTabs?: (items: unknown[]) => import("../types").TabDefinition[] + } +``` + +- [ ] **Step 2: Update the skipVariantLevel pass-through** + +In the `skipVariantLevel && !workflowId && !workflowIdAtom` branch (around line 467–518), inside `createTwoLevelAdapter`'s `parentOverrides`, find: + +```typescript +// Before: + tabs: grandparentOverrides.tabs, + +// After: + buildTabs: grandparentOverrides.buildTabs, +``` + +The full `parentOverrides` block in that branch should look like: + +```typescript + parentOverrides: { + getId: (entity: unknown) => (entity as {id: string}).id, + getLabel: getWorkflowDisplayName, + getLabelNode: grandparentOverrides.getLabelNode ?? renderWorkflowLabelNode, + hasChildren: true, + isSelectable: false, + getGroupKey: grandparentOverrides.getGroupKey, + getGroupLabel: grandparentOverrides.getGroupLabel, + buildTabs: grandparentOverrides.buildTabs, + }, +``` + +- [ ] **Step 3: Type-check** + +```bash +cd web/packages/agenta-entity-ui && pnpm types:check 2>&1 | head -40 +``` + +Expected: Remaining errors only in `useEnrichedEvaluatorAdapter.ts` and `PopoverCascaderVariant.tsx`. + +--- + +## Task 4: Update `PopoverCascaderVariant.tsx` + +**Files:** +- Modify: `web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx:362-365` (tab state) +- Modify: `web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx` (add dynamic tabs memo after rootItems) +- Modify: `web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx:536-547` (handleOpenChange) + +- [ ] **Step 1: Remove static tab reading and simplify initial state** + +Find lines 362–364: +```typescript + // Tab state (driven by adapter's rootLevel.tabs) + const tabs = rootLevel?.tabs + const [activeTabKey, setActiveTabKey] = useState(tabs?.[0]?.key ?? "all") +``` + +Replace with just the state (no static read): +```typescript + // Active tab state — always starts on "all", reset on close + const [activeTabKey, setActiveTabKey] = useState("all") +``` + +- [ ] **Step 2: Add dynamic `tabs` derivation after `rootItems` is fetched** + +Find lines 367–372 (the `useLevelData` call for root items): +```typescript + // Fetch root items + const {items: rootItems, query: rootQuery} = useLevelData({ + levelConfig: rootLevel, + parentId: null, + isEnabled: true, + }) +``` + +Immediately **after** this block, insert: +```typescript + + // Derive tabs dynamically from loaded items (adapter provides buildTabs function) + const tabs = useMemo( + () => rootLevel?.buildTabs?.(rootItems) ?? null, + [rootItems, rootLevel], + ) +``` + +- [ ] **Step 3: Fix `handleOpenChange` reset and dependency array** + +Find the `handleOpenChange` callback (around lines 536–547): +```typescript + const handleOpenChange = useCallback( + (newOpen: boolean) => { + setOpen(newOpen) + if (!newOpen) { + setSearchTerm("") + setSelectedRootId(null) + setSelectedRootEntity(null) + setActiveTabKey(tabs?.[0]?.key ?? "all") + } + }, + [tabs], + ) +``` + +Replace with: +```typescript + const handleOpenChange = useCallback( + (newOpen: boolean) => { + setOpen(newOpen) + if (!newOpen) { + setSearchTerm("") + setSelectedRootId(null) + setSelectedRootEntity(null) + setActiveTabKey("all") + } + }, + [], + ) +``` + +- [ ] **Step 4: Type-check** + +```bash +cd web/packages/agenta-entity-ui && pnpm types:check 2>&1 | head -40 +``` + +Expected: Remaining errors only in `useEnrichedEvaluatorAdapter.ts` (the `tabs: [...]` still present). + +--- + +## Task 5: Replace static tabs with `buildTabs` in the evaluator adapter + +**Files:** +- Modify: `web/packages/agenta-entity-ui/src/selection/adapters/useEnrichedEvaluatorAdapter.ts:195-212` (grandparentOverrides in `useEnrichedEvaluatorOnlyAdapter`) + +- [ ] **Step 1: Replace the static `tabs` array with `buildTabs`** + +In `useEnrichedEvaluatorOnlyAdapter`, inside the `useMemo` that builds `options`, find the `grandparentOverrides` block (lines ~195–212): + +```typescript + grandparentOverrides: { + getLabelNode, + getGroupKey, + getGroupLabel, + tabs: [ + {key: "all", label: "All"}, + {key: "ai_llm", label: "AI / LLM"}, + {key: "classifiers", label: "Classifiers"}, + {key: "similarity", label: "Similarity"}, + {key: "custom", label: "Custom"}, + ], + }, +``` + +Replace with: + +```typescript + grandparentOverrides: { + getLabelNode, + getGroupKey, + getGroupLabel, + buildTabs: (entities: unknown[]) => { + const ORDERED_CATEGORIES = ["ai_llm", "classifiers", "similarity", "custom"] + const categorySeen = new Set() + for (const e of entities) { + const w = e as {id: string} + const key = evaluatorKeyMapRef.current.get(w.id) + const cat = key + ? templateCategoryMapRef.current.get(key) ?? "custom" + : "custom" + categorySeen.add(cat) + } + const result: {key: string; label: string}[] = [{key: "all", label: "All"}] + for (const cat of ORDERED_CATEGORIES) { + if (categorySeen.has(cat)) { + result.push({key: cat, label: CATEGORY_LABELS[cat]}) + } + } + return result + }, + }, +``` + +Note: `CATEGORY_LABELS` and `evaluatorKeyMapRef` / `templateCategoryMapRef` are already in scope — they are defined earlier in the same `useMemo` closure body. + +- [ ] **Step 2: Type-check — expect zero errors** + +```bash +cd web/packages/agenta-entity-ui && pnpm types:check 2>&1 | head -60 +``` + +Expected: No errors. + +--- + +## Task 6: Lint and verify + +**Files:** All modified files above. + +- [ ] **Step 1: Run lint-fix from web root** + +```bash +cd web && pnpm lint-fix 2>&1 | tail -20 +``` + +Expected: No unfixable lint errors. + +- [ ] **Step 2: Final typecheck** + +```bash +cd web/packages/agenta-entity-ui && pnpm types:check +``` + +Expected: Exit 0. + +- [ ] **Step 3: Manual smoke-test checklist** + +Open the playground evaluator connect popover (uses `useEnrichedEvaluatorOnlyAdapter` → `PopoverCascaderVariant`): + +- [ ] Tabs appear only for categories that have evaluators (no empty "Classifiers" tab if no classifiers exist) +- [ ] "All" tab is always present and shows all evaluators +- [ ] Clicking a category tab filters the left panel to that category only +- [ ] Closing and reopening the popover resets to "All" tab +- [ ] Search still works within the active tab + +- [ ] **Step 4: Commit** + +```bash +cd /Users/ashrasfchowdury/Documents/company/agenta +git add \ + web/packages/agenta-entity-ui/src/selection/types.ts \ + web/packages/agenta-entity-ui/src/selection/adapters/createAdapterFromRelations.ts \ + web/packages/agenta-entity-ui/src/selection/adapters/workflowRevisionRelationAdapter.ts \ + web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx \ + web/packages/agenta-entity-ui/src/selection/adapters/useEnrichedEvaluatorAdapter.ts +git commit -m "$(cat <<'EOF' +refactor(entity-ui): replace static tabs with dynamic buildTabs on HierarchyLevel + +Tabs in PopoverCascaderVariant are now derived from loaded items via a +buildTabs(items) function on the adapter, so only categories with data +appear. Removes the hardcoded evaluator tab list from +useEnrichedEvaluatorOnlyAdapter. + +Co-Authored-By: Claude Sonnet 4.6 (1M context) +EOF +)" +``` diff --git a/web/oss/src/pages/w/[workspace_id]/p/[project_id]/annotations/index.tsx b/web/oss/src/pages/w/[workspace_id]/p/[project_id]/annotations/index.tsx index a05d558899..9351bf394f 100644 --- a/web/oss/src/pages/w/[workspace_id]/p/[project_id]/annotations/index.tsx +++ b/web/oss/src/pages/w/[workspace_id]/p/[project_id]/annotations/index.tsx @@ -3,8 +3,10 @@ import {useMemo} from "react" import {AnnotationUIProvider, type AnnotationUINavigation} from "@agenta/annotation-ui/context" import AnnotationQueuesView from "@agenta/annotation-ui/queue-list" import {PageLayout} from "@agenta/ui" +import {useSetAtom} from "jotai" import {useRouter} from "next/router" +import {openHumanEvaluatorDrawerAtom} from "@/oss/components/Evaluators/Drawers/HumanEvaluatorDrawer/store" import {useProjectPermissions} from "@/oss/hooks/useProjectPermissions" import useURL from "@/oss/hooks/useURL" @@ -12,6 +14,7 @@ const AnnotationQueuesPage = () => { const router = useRouter() const {projectURL} = useURL() const {canExportData} = useProjectPermissions() + const openHumanEvaluatorDrawer = useSetAtom(openHumanEvaluatorDrawerAtom) const navigation = useMemo( () => ({ @@ -30,7 +33,11 @@ const AnnotationQueuesPage = () => { title={Queues} className="h-full min-h-0" > - + openHumanEvaluatorDrawer({mode: "create"})} + feedbackCreateLabel="Create evaluator" + /> ) diff --git a/web/packages/agenta-annotation-ui/src/components/AnnotationQueuesView/index.tsx b/web/packages/agenta-annotation-ui/src/components/AnnotationQueuesView/index.tsx index 10cb833266..cf8c162ea0 100644 --- a/web/packages/agenta-annotation-ui/src/components/AnnotationQueuesView/index.tsx +++ b/web/packages/agenta-annotation-ui/src/components/AnnotationQueuesView/index.tsx @@ -222,9 +222,15 @@ const QueuesHeaderFilters = () => { export interface AnnotationQueuesViewProps { canExportData?: boolean + feedbackOnCreate?: () => void + feedbackCreateLabel?: string } -const AnnotationQueuesView = ({canExportData = true}: AnnotationQueuesViewProps) => { +const AnnotationQueuesView = ({ + canExportData = true, + feedbackOnCreate, + feedbackCreateLabel, +}: AnnotationQueuesViewProps) => { const navigation = useAnnotationNavigation() const searchTerm = useAtomValue(simpleQueueSearchTermAtom) const kindFilter = useAtomValue(simpleQueueKindFilterAtom) @@ -514,7 +520,10 @@ const AnnotationQueuesView = ({canExportData = true}: AnnotationQueuesViewProps) autoHeight store={getDefaultStore()} /> - +
) } diff --git a/web/packages/agenta-annotation-ui/src/components/CreateQueueDrawer/EntityEvaluatorSelector.tsx b/web/packages/agenta-annotation-ui/src/components/CreateQueueDrawer/EntityEvaluatorSelector.tsx index 3767566773..2f023f2559 100644 --- a/web/packages/agenta-annotation-ui/src/components/CreateQueueDrawer/EntityEvaluatorSelector.tsx +++ b/web/packages/agenta-annotation-ui/src/components/CreateQueueDrawer/EntityEvaluatorSelector.tsx @@ -25,6 +25,8 @@ export interface EntityEvaluatorSelectorProps { onSelect: (selection: WorkflowRevisionSelectionResult) => void instanceId: string buttonLabel?: string + onCreate?: () => void + createLabel?: string disabledRevisionIds?: Set disabledRevisionTooltip?: string panelMinWidth?: number @@ -124,6 +126,8 @@ export function EntityEvaluatorSelector({ onSelect, instanceId, buttonLabel = "Add evaluator", + onCreate, + createLabel = "Create evaluator", disabledRevisionIds, disabledRevisionTooltip = "Already added", panelMinWidth = 280, @@ -179,6 +183,8 @@ export function EntityEvaluatorSelector({ disabledChildTooltip={disabledRevisionTooltip} openChildOnHover={openVersionOnHover} size="middle" + onCreateNew={onCreate} + createNewLabel={createLabel} />
) diff --git a/web/packages/agenta-annotation-ui/src/components/CreateQueueDrawer/index.tsx b/web/packages/agenta-annotation-ui/src/components/CreateQueueDrawer/index.tsx index 030b0dbf6e..4db82b8aa2 100644 --- a/web/packages/agenta-annotation-ui/src/components/CreateQueueDrawer/index.tsx +++ b/web/packages/agenta-annotation-ui/src/components/CreateQueueDrawer/index.tsx @@ -50,6 +50,8 @@ interface CreateQueueDrawerContentProps { selection: {itemType: "traces" | "testcases"; itemIds: string[]} | null onClearSelection: () => void onItemsAdded?: () => void + feedbackOnCreate?: () => void + feedbackCreateLabel?: string } const INITIAL_FORM_VALUES: Pick = { @@ -94,6 +96,8 @@ function CreateQueueDrawerContent({ selection, onClearSelection, onItemsAdded, + feedbackOnCreate, + feedbackCreateLabel, }: CreateQueueDrawerContentProps) { const projectId = useAtomValue(projectIdAtom) const createQueue = useSetAtom(createSimpleQueueAtom) @@ -346,6 +350,8 @@ function CreateQueueDrawerContent({ void + feedbackOnCreate?: () => void + feedbackCreateLabel?: string } -const CreateQueueDrawer = ({onItemsAdded}: CreateQueueDrawerProps) => { +const CreateQueueDrawer = ({ + onItemsAdded, + feedbackOnCreate, + feedbackCreateLabel, +}: CreateQueueDrawerProps) => { const [open, setOpen] = useAtom(createQueueDrawerOpenAtom) const defaultKind = useAtomValue(createQueueDrawerDefaultKindAtom) const [selection, setSelection] = useAtom(createQueueDrawerSelectionAtom) @@ -455,6 +467,8 @@ const CreateQueueDrawer = ({onItemsAdded}: CreateQueueDrawerProps) => { selection={selection} onClearSelection={handleClearSelection} onItemsAdded={onItemsAdded} + feedbackOnCreate={feedbackOnCreate} + feedbackCreateLabel={feedbackCreateLabel} /> ) : null} diff --git a/web/packages/agenta-entity-ui/src/selection/adapters/createAdapter.ts b/web/packages/agenta-entity-ui/src/selection/adapters/createAdapter.ts index 209893c1fb..046792c790 100644 --- a/web/packages/agenta-entity-ui/src/selection/adapters/createAdapter.ts +++ b/web/packages/agenta-entity-ui/src/selection/adapters/createAdapter.ts @@ -101,7 +101,7 @@ export function createAdapter( getGroupKey: level.getGroupKey, getGroupLabel: level.getGroupLabel, // Tabs - tabs: level.tabs, + buildTabs: level.buildTabs, } }) diff --git a/web/packages/agenta-entity-ui/src/selection/adapters/createAdapterFromRelations.ts b/web/packages/agenta-entity-ui/src/selection/adapters/createAdapterFromRelations.ts index c1514fd4ae..8ceff54b87 100644 --- a/web/packages/agenta-entity-ui/src/selection/adapters/createAdapterFromRelations.ts +++ b/web/packages/agenta-entity-ui/src/selection/adapters/createAdapterFromRelations.ts @@ -83,8 +83,8 @@ export interface LevelOverride { getGroupKey?: (entity: T) => string | null | undefined /** Map a group key to a human-readable display label */ getGroupLabel?: (key: string) => string - /** Tab definitions for filtering items by group */ - tabs?: import("../types").TabDefinition[] + /** Function to derive tab definitions from loaded items */ + buildTabs?: (items: T[]) => import("../types").TabDefinition[] } /** @@ -232,7 +232,7 @@ function applyOverrides( filterItems: overrides.filterItems ?? baseLevel.filterItems, getGroupKey: overrides.getGroupKey ?? baseLevel.getGroupKey, getGroupLabel: overrides.getGroupLabel ?? baseLevel.getGroupLabel, - tabs: overrides.tabs ?? baseLevel.tabs, + buildTabs: overrides.buildTabs ?? baseLevel.buildTabs, } } @@ -357,6 +357,7 @@ export function createAdapterFromRelations< | ((entity: unknown) => string | null | undefined) | undefined, getGroupLabel: rootLevel.getGroupLabel, + buildTabs: rootLevel.buildTabs, }) levels.push(rootLevelConfig) diff --git a/web/packages/agenta-entity-ui/src/selection/adapters/createLevelFromRelation.ts b/web/packages/agenta-entity-ui/src/selection/adapters/createLevelFromRelation.ts index 5a2d59ef0c..c752bbe7f8 100644 --- a/web/packages/agenta-entity-ui/src/selection/adapters/createLevelFromRelation.ts +++ b/web/packages/agenta-entity-ui/src/selection/adapters/createLevelFromRelation.ts @@ -124,6 +124,9 @@ export interface CreateLevelFromRelationOptions { /** Map a group key to a human-readable display label */ getGroupLabel?: (key: string) => string + + /** Function to derive tab definitions from loaded items */ + buildTabs?: (items: TChild[]) => import("../types").TabDefinition[] } // ============================================================================ @@ -243,6 +246,7 @@ export function createLevelFromRelation( listAtomFamily, getGroupKey, getGroupLabel, + buildTabs, } = options // Derive from relation.selection if available @@ -305,6 +309,9 @@ export function createLevelFromRelation( onBeforeLoad, getGroupKey, getGroupLabel, + buildTabs: buildTabs as + | ((items: unknown[]) => import("../types").TabDefinition[]) + | undefined, } } diff --git a/web/packages/agenta-entity-ui/src/selection/adapters/evaluatorLabelUtils.ts b/web/packages/agenta-entity-ui/src/selection/adapters/evaluatorLabelUtils.ts index d84ec9a2f3..1ee1b581ba 100644 --- a/web/packages/agenta-entity-ui/src/selection/adapters/evaluatorLabelUtils.ts +++ b/web/packages/agenta-entity-ui/src/selection/adapters/evaluatorLabelUtils.ts @@ -49,9 +49,11 @@ export function renderEvaluatorPickerLabelNode( ): React.ReactNode { const w = entity as EvaluatorWorkflowLike const name = w.name ?? "Unnamed" + const evaluatorKey = evaluatorKeyMap.get(w.id) + const isHumanEvaluator = Boolean(w.flags?.is_feedback) || evaluatorKey === "feedback" // Only show colored tags for evaluator-type workflows - if (!w.flags?.is_evaluator) { + if (!w.flags?.is_evaluator && !isHumanEvaluator) { return React.createElement(EntityListItemLabel, {label: name}) } @@ -59,14 +61,13 @@ export function renderEvaluatorPickerLabelNode( let tagLabel: string | null = null let colorSource: string | null = null - if (w.flags?.is_feedback) { + if (isHumanEvaluator) { tagLabel = "Human" colorSource = "human" } else if (w.flags?.is_custom) { tagLabel = "Custom Code" colorSource = "custom" } else { - const evaluatorKey = evaluatorKeyMap.get(w.id) if (evaluatorKey) { tagLabel = evaluatorDefsByKey.get(evaluatorKey) ?? null colorSource = evaluatorKey diff --git a/web/packages/agenta-entity-ui/src/selection/adapters/types.ts b/web/packages/agenta-entity-ui/src/selection/adapters/types.ts index 5ce625c04b..1f54ddfb85 100644 --- a/web/packages/agenta-entity-ui/src/selection/adapters/types.ts +++ b/web/packages/agenta-entity-ui/src/selection/adapters/types.ts @@ -175,12 +175,12 @@ export interface CreateHierarchyLevelOptions { getGroupLabel?: (key: string) => string /** - * Optional tab definitions for filtering items by group. - * When provided, the component renders tabs above the item list. + * Optional function that derives tab definitions from loaded items. + * Called after items load — only shows tabs for groups that actually have data. * Each tab filters items by `getGroupKey` match. The "all" key shows all items grouped. * Requires `getGroupKey` to be defined for meaningful filtering. */ - tabs?: TabDefinition[] + buildTabs?: (items: T[]) => TabDefinition[] } /** diff --git a/web/packages/agenta-entity-ui/src/selection/adapters/useEnrichedEvaluatorAdapter.ts b/web/packages/agenta-entity-ui/src/selection/adapters/useEnrichedEvaluatorAdapter.ts index b5836ba92c..8333582859 100644 --- a/web/packages/agenta-entity-ui/src/selection/adapters/useEnrichedEvaluatorAdapter.ts +++ b/web/packages/agenta-entity-ui/src/selection/adapters/useEnrichedEvaluatorAdapter.ts @@ -200,13 +200,25 @@ export function useEnrichedEvaluatorOnlyAdapter( getLabelNode, getGroupKey, getGroupLabel, - tabs: [ - {key: "all", label: "All"}, - {key: "ai_llm", label: "AI / LLM"}, - {key: "classifiers", label: "Classifiers"}, - {key: "similarity", label: "Similarity"}, - {key: "custom", label: "Custom"}, - ], + buildTabs: (entities: unknown[]) => { + const ORDERED_CATEGORIES = ["ai_llm", "classifiers", "similarity", "custom"] + const categorySeen = new Set() + for (const e of entities) { + const w = e as {id: string} + const key = evaluatorKeyMapRef.current.get(w.id) + const cat = key + ? (templateCategoryMapRef.current.get(key) ?? "custom") + : "custom" + categorySeen.add(cat) + } + const result: {key: string; label: string}[] = [{key: "all", label: "All"}] + for (const cat of ORDERED_CATEGORIES) { + if (categorySeen.has(cat)) { + result.push({key: cat, label: CATEGORY_LABELS[cat]}) + } + } + return result + }, }, } diff --git a/web/packages/agenta-entity-ui/src/selection/adapters/workflowRevisionRelationAdapter.ts b/web/packages/agenta-entity-ui/src/selection/adapters/workflowRevisionRelationAdapter.ts index 52490cd7cb..0546025735 100644 --- a/web/packages/agenta-entity-ui/src/selection/adapters/workflowRevisionRelationAdapter.ts +++ b/web/packages/agenta-entity-ui/src/selection/adapters/workflowRevisionRelationAdapter.ts @@ -295,7 +295,7 @@ export interface CreateWorkflowRevisionAdapterOptions { getLabelNode?: (entity: unknown) => React.ReactNode getGroupKey?: (entity: unknown) => string | null | undefined getGroupLabel?: (key: string) => string - tabs?: import("../types").TabDefinition[] + buildTabs?: (items: unknown[]) => import("../types").TabDefinition[] } /** @@ -477,7 +477,7 @@ export function createWorkflowRevisionAdapter( isSelectable: false, getGroupKey: grandparentOverrides.getGroupKey, getGroupLabel: grandparentOverrides.getGroupLabel, - tabs: grandparentOverrides.tabs, + buildTabs: grandparentOverrides.buildTabs, }, childType: "workflowRevision", childLabel: "Revision", diff --git a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx index f8193360e3..8bfafaacda 100644 --- a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx +++ b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx @@ -359,9 +359,8 @@ export function PopoverCascaderVariant({ const [selectedRootId, setSelectedRootId] = useState(null) const [selectedRootEntity, setSelectedRootEntity] = useState(null) - // Tab state (driven by adapter's rootLevel.tabs) - const tabs = rootLevel?.tabs - const [activeTabKey, setActiveTabKey] = useState(tabs?.[0]?.key ?? "all") + // Active tab state — always starts on "all", reset on close + const [activeTabKey, setActiveTabKey] = useState("all") // Fetch root items const {items: rootItems, query: rootQuery} = useLevelData({ @@ -370,6 +369,9 @@ export function PopoverCascaderVariant({ isEnabled: true, }) + // Derive tabs dynamically from loaded items (adapter provides buildTabs function) + const tabs = useMemo(() => rootLevel?.buildTabs?.(rootItems) ?? null, [rootItems, rootLevel]) + // Filter root items by search const filteredRootItems = useMemo(() => { if (!searchTerm) return rootItems @@ -533,18 +535,15 @@ export function PopoverCascaderVariant({ ) // Reset state when popover closes - const handleOpenChange = useCallback( - (newOpen: boolean) => { - setOpen(newOpen) - if (!newOpen) { - setSearchTerm("") - setSelectedRootId(null) - setSelectedRootEntity(null) - setActiveTabKey(tabs?.[0]?.key ?? "all") - } - }, - [tabs], - ) + const handleOpenChange = useCallback((newOpen: boolean) => { + setOpen(newOpen) + if (!newOpen) { + setSearchTerm("") + setSelectedRootId(null) + setSelectedRootEntity(null) + setActiveTabKey("all") + } + }, []) const handleCreateNew = useCallback(() => { onCreateNew?.() @@ -644,13 +643,6 @@ export function PopoverCascaderVariant({ {Array.from(groupedItems.groups.entries()).map( ([groupKey, items]) => (
-
- - {rootLevel.getGroupLabel?.(groupKey) ?? - groupKey} - -
-
{items.map((item) => ( { getGroupLabel?: (key: string) => string /** - * Optional tab definitions for filtering items by group. - * When provided, the component renders tabs above the item list. + * Optional function that derives tab definitions from loaded items. + * Called after items load — only shows tabs for groups that actually have data. * Each tab filters items by `getGroupKey` match. The "all" key shows all items grouped. * Requires `getGroupKey` to be defined for meaningful filtering. */ - tabs?: TabDefinition[] + buildTabs?: (items: T[]) => TabDefinition[] } /** From 690f38a433607a03b4063c2f7c86e038626d5edb Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Wed, 15 Apr 2026 20:05:30 +0600 Subject: [PATCH 4/9] cleanup --- .../2026-04-15-dynamic-tabs-buildtabs.md | 376 ------------------ ...26-04-13-popover-cascader-update-design.md | 171 -------- 2 files changed, 547 deletions(-) delete mode 100644 docs/superpowers/plans/2026-04-15-dynamic-tabs-buildtabs.md delete mode 100644 docs/superpowers/specs/2026-04-13-popover-cascader-update-design.md diff --git a/docs/superpowers/plans/2026-04-15-dynamic-tabs-buildtabs.md b/docs/superpowers/plans/2026-04-15-dynamic-tabs-buildtabs.md deleted file mode 100644 index 2f1a435b4c..0000000000 --- a/docs/superpowers/plans/2026-04-15-dynamic-tabs-buildtabs.md +++ /dev/null @@ -1,376 +0,0 @@ -# Dynamic Tabs via `buildTabs` Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Replace the static `tabs?: TabDefinition[]` field on `HierarchyLevel` with a `buildTabs(items) => TabDefinition[]` function so that the `PopoverCascaderVariant` derives tabs dynamically from loaded data — only showing tabs for categories that have items. - -**Architecture:** Four files need changes in order: (1) the core `HierarchyLevel` type, (2) the adapter-level `LevelOverride` interface and its merge helper, (3) the workflow revision adapter's `grandparentOverrides` interface and pass-through, and (4) the `PopoverCascaderVariant` component that swaps static tab reading for a `useMemo` call. A fifth file wires the evaluator-specific `buildTabs` logic in `useEnrichedEvaluatorOnlyAdapter`. - -**Tech Stack:** TypeScript, React (useMemo, useState, useCallback), Jotai, Ant Design Tabs - ---- - -## File Map - -| File | Change | -|---|---| -| `web/packages/agenta-entity-ui/src/selection/types.ts` | Replace `tabs?: TabDefinition[]` with `buildTabs?: (items: T[]) => TabDefinition[]` on `HierarchyLevel` | -| `web/packages/agenta-entity-ui/src/selection/adapters/createAdapterFromRelations.ts` | Same replacement on `LevelOverride`; update `applyOverrides` pass-through | -| `web/packages/agenta-entity-ui/src/selection/adapters/workflowRevisionRelationAdapter.ts` | Update `grandparentOverrides` interface; update the skipVariantLevel pass-through | -| `web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx` | Swap static `rootLevel?.tabs` for `useMemo(() => rootLevel?.buildTabs?.(rootItems))` | -| `web/packages/agenta-entity-ui/src/selection/adapters/useEnrichedEvaluatorAdapter.ts` | Replace hardcoded `tabs: [...]` with `buildTabs: (entities) => {...}` closure | - ---- - -## Task 1: Update `HierarchyLevel` type - -**Files:** -- Modify: `web/packages/agenta-entity-ui/src/selection/types.ts:282-288` - -- [ ] **Step 1: Replace the `tabs` field with `buildTabs`** - -In `types.ts`, find lines 282–288 (the JSDoc + `tabs?: TabDefinition[]` line) and replace: - -```typescript -// Before (lines 282-288): - /** - * Optional tab definitions for filtering items by group. - * When provided, the component renders tabs above the item list. - * Each tab filters items by `getGroupKey` match. The "all" key shows all items grouped. - * Requires `getGroupKey` to be defined for meaningful filtering. - */ - tabs?: TabDefinition[] - -// After: - /** - * Optional function that derives tab definitions from loaded items. - * Called after items load — only shows tabs for groups that actually have data. - * Each tab filters items by `getGroupKey` match. The "all" key shows all items grouped. - * Requires `getGroupKey` to be defined for meaningful filtering. - */ - buildTabs?: (items: T[]) => TabDefinition[] -``` - -- [ ] **Step 2: Type-check** - -```bash -cd web/packages/agenta-entity-ui && pnpm types:check 2>&1 | head -40 -``` - -Expected: Errors for `tabs` usages in other files (those get fixed in subsequent tasks). No errors inside `types.ts` itself. - ---- - -## Task 2: Update `LevelOverride` in `createAdapterFromRelations.ts` - -**Files:** -- Modify: `web/packages/agenta-entity-ui/src/selection/adapters/createAdapterFromRelations.ts:87` (interface) -- Modify: `web/packages/agenta-entity-ui/src/selection/adapters/createAdapterFromRelations.ts:235` (applyOverrides) - -- [ ] **Step 1: Replace `tabs` in the `LevelOverride` interface** - -Find line ~87 and replace: - -```typescript -// Before: - /** Tab definitions for filtering items by group */ - tabs?: import("../types").TabDefinition[] - -// After: - /** Function to derive tab definitions from loaded items */ - buildTabs?: (items: T[]) => import("../types").TabDefinition[] -``` - -- [ ] **Step 2: Update `applyOverrides` pass-through** - -Find line ~235 (inside the `applyOverrides` function) and replace: - -```typescript -// Before: - tabs: overrides.tabs ?? baseLevel.tabs, - -// After: - buildTabs: overrides.buildTabs ?? baseLevel.buildTabs, -``` - -- [ ] **Step 3: Type-check** - -```bash -cd web/packages/agenta-entity-ui && pnpm types:check 2>&1 | head -40 -``` - -Expected: Remaining errors only in `workflowRevisionRelationAdapter.ts` and `useEnrichedEvaluatorAdapter.ts` and `PopoverCascaderVariant.tsx` — the files we haven't fixed yet. - ---- - -## Task 3: Update `workflowRevisionRelationAdapter.ts` - -**Files:** -- Modify: `web/packages/agenta-entity-ui/src/selection/adapters/workflowRevisionRelationAdapter.ts:294-299` (interface) -- Modify: `web/packages/agenta-entity-ui/src/selection/adapters/workflowRevisionRelationAdapter.ts:480` (pass-through) - -- [ ] **Step 1: Update `grandparentOverrides` interface** - -Find the `grandparentOverrides` block in `CreateWorkflowRevisionAdapterOptions` (around line 294) and replace the `tabs` field: - -```typescript -// Before: - tabs?: import("../types").TabDefinition[] - -// After: - buildTabs?: (items: unknown[]) => import("../types").TabDefinition[] -``` - -The full updated `grandparentOverrides` block should look like: - -```typescript - grandparentOverrides?: { - getLabelNode?: (entity: unknown) => React.ReactNode - getGroupKey?: (entity: unknown) => string | null | undefined - getGroupLabel?: (key: string) => string - buildTabs?: (items: unknown[]) => import("../types").TabDefinition[] - } -``` - -- [ ] **Step 2: Update the skipVariantLevel pass-through** - -In the `skipVariantLevel && !workflowId && !workflowIdAtom` branch (around line 467–518), inside `createTwoLevelAdapter`'s `parentOverrides`, find: - -```typescript -// Before: - tabs: grandparentOverrides.tabs, - -// After: - buildTabs: grandparentOverrides.buildTabs, -``` - -The full `parentOverrides` block in that branch should look like: - -```typescript - parentOverrides: { - getId: (entity: unknown) => (entity as {id: string}).id, - getLabel: getWorkflowDisplayName, - getLabelNode: grandparentOverrides.getLabelNode ?? renderWorkflowLabelNode, - hasChildren: true, - isSelectable: false, - getGroupKey: grandparentOverrides.getGroupKey, - getGroupLabel: grandparentOverrides.getGroupLabel, - buildTabs: grandparentOverrides.buildTabs, - }, -``` - -- [ ] **Step 3: Type-check** - -```bash -cd web/packages/agenta-entity-ui && pnpm types:check 2>&1 | head -40 -``` - -Expected: Remaining errors only in `useEnrichedEvaluatorAdapter.ts` and `PopoverCascaderVariant.tsx`. - ---- - -## Task 4: Update `PopoverCascaderVariant.tsx` - -**Files:** -- Modify: `web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx:362-365` (tab state) -- Modify: `web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx` (add dynamic tabs memo after rootItems) -- Modify: `web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx:536-547` (handleOpenChange) - -- [ ] **Step 1: Remove static tab reading and simplify initial state** - -Find lines 362–364: -```typescript - // Tab state (driven by adapter's rootLevel.tabs) - const tabs = rootLevel?.tabs - const [activeTabKey, setActiveTabKey] = useState(tabs?.[0]?.key ?? "all") -``` - -Replace with just the state (no static read): -```typescript - // Active tab state — always starts on "all", reset on close - const [activeTabKey, setActiveTabKey] = useState("all") -``` - -- [ ] **Step 2: Add dynamic `tabs` derivation after `rootItems` is fetched** - -Find lines 367–372 (the `useLevelData` call for root items): -```typescript - // Fetch root items - const {items: rootItems, query: rootQuery} = useLevelData({ - levelConfig: rootLevel, - parentId: null, - isEnabled: true, - }) -``` - -Immediately **after** this block, insert: -```typescript - - // Derive tabs dynamically from loaded items (adapter provides buildTabs function) - const tabs = useMemo( - () => rootLevel?.buildTabs?.(rootItems) ?? null, - [rootItems, rootLevel], - ) -``` - -- [ ] **Step 3: Fix `handleOpenChange` reset and dependency array** - -Find the `handleOpenChange` callback (around lines 536–547): -```typescript - const handleOpenChange = useCallback( - (newOpen: boolean) => { - setOpen(newOpen) - if (!newOpen) { - setSearchTerm("") - setSelectedRootId(null) - setSelectedRootEntity(null) - setActiveTabKey(tabs?.[0]?.key ?? "all") - } - }, - [tabs], - ) -``` - -Replace with: -```typescript - const handleOpenChange = useCallback( - (newOpen: boolean) => { - setOpen(newOpen) - if (!newOpen) { - setSearchTerm("") - setSelectedRootId(null) - setSelectedRootEntity(null) - setActiveTabKey("all") - } - }, - [], - ) -``` - -- [ ] **Step 4: Type-check** - -```bash -cd web/packages/agenta-entity-ui && pnpm types:check 2>&1 | head -40 -``` - -Expected: Remaining errors only in `useEnrichedEvaluatorAdapter.ts` (the `tabs: [...]` still present). - ---- - -## Task 5: Replace static tabs with `buildTabs` in the evaluator adapter - -**Files:** -- Modify: `web/packages/agenta-entity-ui/src/selection/adapters/useEnrichedEvaluatorAdapter.ts:195-212` (grandparentOverrides in `useEnrichedEvaluatorOnlyAdapter`) - -- [ ] **Step 1: Replace the static `tabs` array with `buildTabs`** - -In `useEnrichedEvaluatorOnlyAdapter`, inside the `useMemo` that builds `options`, find the `grandparentOverrides` block (lines ~195–212): - -```typescript - grandparentOverrides: { - getLabelNode, - getGroupKey, - getGroupLabel, - tabs: [ - {key: "all", label: "All"}, - {key: "ai_llm", label: "AI / LLM"}, - {key: "classifiers", label: "Classifiers"}, - {key: "similarity", label: "Similarity"}, - {key: "custom", label: "Custom"}, - ], - }, -``` - -Replace with: - -```typescript - grandparentOverrides: { - getLabelNode, - getGroupKey, - getGroupLabel, - buildTabs: (entities: unknown[]) => { - const ORDERED_CATEGORIES = ["ai_llm", "classifiers", "similarity", "custom"] - const categorySeen = new Set() - for (const e of entities) { - const w = e as {id: string} - const key = evaluatorKeyMapRef.current.get(w.id) - const cat = key - ? templateCategoryMapRef.current.get(key) ?? "custom" - : "custom" - categorySeen.add(cat) - } - const result: {key: string; label: string}[] = [{key: "all", label: "All"}] - for (const cat of ORDERED_CATEGORIES) { - if (categorySeen.has(cat)) { - result.push({key: cat, label: CATEGORY_LABELS[cat]}) - } - } - return result - }, - }, -``` - -Note: `CATEGORY_LABELS` and `evaluatorKeyMapRef` / `templateCategoryMapRef` are already in scope — they are defined earlier in the same `useMemo` closure body. - -- [ ] **Step 2: Type-check — expect zero errors** - -```bash -cd web/packages/agenta-entity-ui && pnpm types:check 2>&1 | head -60 -``` - -Expected: No errors. - ---- - -## Task 6: Lint and verify - -**Files:** All modified files above. - -- [ ] **Step 1: Run lint-fix from web root** - -```bash -cd web && pnpm lint-fix 2>&1 | tail -20 -``` - -Expected: No unfixable lint errors. - -- [ ] **Step 2: Final typecheck** - -```bash -cd web/packages/agenta-entity-ui && pnpm types:check -``` - -Expected: Exit 0. - -- [ ] **Step 3: Manual smoke-test checklist** - -Open the playground evaluator connect popover (uses `useEnrichedEvaluatorOnlyAdapter` → `PopoverCascaderVariant`): - -- [ ] Tabs appear only for categories that have evaluators (no empty "Classifiers" tab if no classifiers exist) -- [ ] "All" tab is always present and shows all evaluators -- [ ] Clicking a category tab filters the left panel to that category only -- [ ] Closing and reopening the popover resets to "All" tab -- [ ] Search still works within the active tab - -- [ ] **Step 4: Commit** - -```bash -cd /Users/ashrasfchowdury/Documents/company/agenta -git add \ - web/packages/agenta-entity-ui/src/selection/types.ts \ - web/packages/agenta-entity-ui/src/selection/adapters/createAdapterFromRelations.ts \ - web/packages/agenta-entity-ui/src/selection/adapters/workflowRevisionRelationAdapter.ts \ - web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx \ - web/packages/agenta-entity-ui/src/selection/adapters/useEnrichedEvaluatorAdapter.ts -git commit -m "$(cat <<'EOF' -refactor(entity-ui): replace static tabs with dynamic buildTabs on HierarchyLevel - -Tabs in PopoverCascaderVariant are now derived from loaded items via a -buildTabs(items) function on the adapter, so only categories with data -appear. Removes the hardcoded evaluator tab list from -useEnrichedEvaluatorOnlyAdapter. - -Co-Authored-By: Claude Sonnet 4.6 (1M context) -EOF -)" -``` diff --git a/docs/superpowers/specs/2026-04-13-popover-cascader-update-design.md b/docs/superpowers/specs/2026-04-13-popover-cascader-update-design.md deleted file mode 100644 index 6dfad8dc06..0000000000 --- a/docs/superpowers/specs/2026-04-13-popover-cascader-update-design.md +++ /dev/null @@ -1,171 +0,0 @@ -# PopoverCascaderVariant Update — Design Spec - -**Date:** 2026-04-13 -**Scope:** Extend `PopoverCascaderVariant` in `@agenta/entity-ui` with multi-select, adapter-driven tabs, grouped items, and layout improvements. -**Constraint:** All changes must be generic — usable by evaluators, testsets, app revisions, and any future entity type. No evaluator-specific logic in the component. - ---- - -## Decisions - -| Decision | Choice | Rationale | -|----------|--------|-----------| -| Multi-select | Generic component supports optional `multiSelect` mode | Any entity type may need multi-select | -| Tabs | Adapter-driven via `HierarchyLevel.tabs` | Evaluators define category tabs, testsets provide none — component is agnostic | -| Tabs & groups relationship | Tabs = group filters. "All" shows grouped items; specific tab filters to that group | Matches Figma design, one concept with two presentations | -| Action button position | Moves to top-right header row next to search | Consistent, higher visibility | -| Child panel header | Always shows parent name; multi-select adds count + "Select all" | Provides context in both modes | -| Select all callback | Separate `onSelectAll` for bulk; `onSelect` for individual toggle | Clean atomic `onSelect`, efficient batch path for "select all" | - ---- - -## 1. New & Modified Props on `PopoverCascaderVariantProps` - -All new props are optional. Existing usages work unchanged. - -```typescript -interface PopoverCascaderVariantProps { - // ── EXISTING (unchanged) ── - adapter, onSelect, instanceId, className, disabled, - size, placeholder, icon, showDropdownIcon, placement, - panelMinWidth, maxHeight, popupFooter, - selectedParentId, selectedChildId, - disabledChildIds, disabledChildTooltip, openChildOnHover, - - // ── EXISTING (kept, repositioned in UI) ── - onCreateNew // button moves from bottom to header row - createNewLabel // still works - - // ── NEW: Multi-select ── - multiSelect?: boolean // enables checkbox UI in child panel - selectedChildIds?: Set // controlled: which children are checked - onSelectAll?: (selections: TSelection[]) => void // bulk select/deselect - - // ── NEW: Selection summary ── - selectionSummary?: string // e.g., "No versions selected" — shown above root list -} -``` - -- `onSelect` remains the callback for both single-select clicks and multi-select checkbox toggles. -- In multi-select mode, `selectedChildId` is ignored in favor of `selectedChildIds`. -- `selectionSummary` is a simple string prop. Consumer owns the text. - ---- - -## 2. Adapter-Driven Tabs via `HierarchyLevel` Extension - -One new optional field on `HierarchyLevel`: - -```typescript -interface HierarchyLevel { - // ── EXISTING (all unchanged) ── - type, label, getId, getLabel, getLabelNode, getIcon, - getGroupKey, getGroupLabel, - listAtom, listAtomFamily, filterItems, ... - - // ── NEW ── - tabs?: TabDefinition[] -} - -interface TabDefinition { - key: string // matches getGroupKey() values, or "all" for the everything tab - label: string // display text, e.g., "AI/LLM", "Classifiers" -} -``` - -**How tabs interact with `getGroupKey` / `getGroupLabel`:** - -1. Adapter defines `tabs` on the root level (e.g., `[{key: "all", label: "All"}, {key: "ai_llm", label: "AI/LLM"}, ...]`). -2. Adapter defines `getGroupKey(entity)` returning the group key (e.g., `"ai_llm"`, `"classifiers"`). -3. Component renders: - - **"All" tab active**: Shows all items, grouped by `getGroupKey`, headers via `getGroupLabel`. - - **Specific tab active**: Filters to items where `getGroupKey(item) === tab.key`, no group headers. - -**When adapter provides no `tabs`:** No tabs render. Flat list (current behavior). - ---- - -## 3. Component Layout - -``` -┌─────────────────────────────────────────────────────────────┐ -│ HEADER ROW │ -│ ┌─────────────────────────┐ ┌────────────────────────┐ │ -│ │ Search ... │ │ + New evaluator │ │ -│ └─────────────────────────┘ └────────────────────────┘ │ -├─────────────────────────────────────────────────────────────┤ -│ TABS (optional, only when adapter provides tabs) │ -│ All | AI/LLM | Classifiers | Similarity | Custom │ -├──────────────────────────────┬──────────────────────────────┤ -│ SELECTION SUMMARY (optional) │ CHILD PANEL HEADER │ -│ "No versions selected" │ │ -│ │ 0 of 4 selected Select all │ -├──────────────────────────────┼──────────────────────────────┤ -│ ROOT PANEL │ CHILD PANEL │ -│ │ │ -│ ── Group Header ────── │ [ ] v4 │ -│ item_name [Tag] │ [ ] v3 │ -│ 12 versions · Jan 6, 2026 │ [ ] v2 │ -│ │ [ ] v1 │ -│ item_name [Tag] │ │ -│ 12 versions · Jan 6, 2026 │ │ -│ │ │ -│ ── Group Header ───── │ │ -│ item_name [Tag] │ │ -│ │ │ -├──────────────────────────────┤ │ -│ POPUP FOOTER (optional) │ │ -└──────────────────────────────┴──────────────────────────────┘ -``` - -**Changes from current layout:** - -1. **Header row**: Search + action button side-by-side at top. `onCreateNew` button moves from bottom to here. `popupFooter` stays at the bottom for other uses (e.g., "Disconnect all"). -2. **Tabs row**: Renders below header when `rootLevel.tabs` is present. Active tab state is component-internal. -3. **Selection summary**: Optional text above root list when `selectionSummary` is provided. -4. **Group headers**: When "All" tab is active and `getGroupKey` is defined, items grouped with divider-style headers. When a specific tab is active, no group headers. -5. **Root item rendering**: No change to `EntityListItem`. Rich metadata (tags, counts, dates) handled by adapter's `getLabelNode`. -6. **Child panel header**: New. Always shows parent name. In multi-select mode, adds "X of Y selected" + "Select all" link. -7. **Child panel items**: Single-select: click to select (current). Multi-select: checkboxes. - ---- - -## 4. Behavioral Changes & Edge Cases - -**Popover close behavior:** -- Single-select (current): Clicking a child fires `onSelect` and closes the popover. No change. -- Multi-select: Toggling a checkbox fires `onSelect` but the popover stays open. User closes by clicking outside or pressing Escape. - -**Tab state:** -- Internal component state, defaults to the first tab (typically "All"). -- Resets to first tab when popover reopens (same as search resetting today). -- Search applies within the active tab's filtered items. - -**Select all / Deselect all:** -- Appears in child panel header when `multiSelect` is true. -- Some children selected: label shows "Select all", clicking calls `onSelectAll` with all unselected children. -- All children selected: label shows "Deselect all", clicking calls `onSelectAll` with an empty array (consumer interprets as "clear all for this parent"). - -**Disabled children in multi-select:** -- `disabledChildIds` still works. Disabled items show a checked-but-greyed-out checkbox and are excluded from "Select all". - -**Backward compatibility:** -- No `multiSelect` prop: single-select, no checkboxes, no count in child header. Same as today. -- No `tabs` on adapter: no tabs row. Same as today. -- No `selectionSummary`: no summary text. Same as today. -- `onCreateNew` still works: renders in new position (header row instead of bottom). -- `popupFooter` still works: stays at bottom of root panel. -- All existing consumers (playground header, evaluator config, workflow drawer) keep working with zero changes. - ---- - -## 5. Files to Modify - -| File | Change | -|------|--------| -| `web/packages/agenta-entity-ui/src/selection/types.ts` | Add `TabDefinition` type, add optional `tabs` field to `HierarchyLevel` | -| `web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/types.ts` | Add `multiSelect`, `selectedChildIds`, `onSelectAll`, `selectionSummary` to `PopoverCascaderVariantProps` | -| `web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx` | Implement header row layout, tabs, groups, multi-select child panel, child panel header | -| Evaluator adapter (when consumed) | Add `tabs` and `getGroupKey`/`getGroupLabel` to root level config | - -No new files needed. All changes extend existing files. From 1849dbe0e6f5cb585824fe69659edd843a59d00b Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Wed, 15 Apr 2026 20:18:30 +0600 Subject: [PATCH 5/9] refactor: remove bulk select/deselect functionality from UnifiedEntityPicker and PlaygroundHeader --- .../Components/PlaygroundHeader/index.tsx | 38 -------- .../components/UnifiedEntityPicker/types.ts | 12 --- .../variants/PopoverCascaderVariant.tsx | 87 +------------------ 3 files changed, 3 insertions(+), 134 deletions(-) diff --git a/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx b/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx index c9c6905d06..0bf303cd98 100644 --- a/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx +++ b/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx @@ -252,43 +252,6 @@ const PlaygroundHeader: React.FC = ({className, ...divPro [nodes, connectedEvaluatorNodes, connectDownstreamNode, disconnectSingleDownstreamNode], ) - // Multi-select: bulk connect or disconnect - const handleEvaluatorsSelectAll = useCallback( - ( - selections: WorkflowRevisionSelectionResult[], - action: "select" | "deselect", - parentId: string, - ) => { - const rootNode = nodes.find((n) => n.depth === 0) - if (!rootNode) return - - if (action === "select") { - // Connect all new selections - selections.forEach((selection) => { - connectDownstreamNode({ - sourceNodeId: rootNode.id, - entity: { - type: "workflow", - id: selection.id, - label: selection.label, - metadata: selection.metadata, - }, - }) - }) - } else if (action === "deselect") { - // Determine which nodes belong to the selections and remove them - const selectionIds = new Set(selections.map((s) => s.id)) - const nodesToRemove = connectedEvaluatorNodes.filter((n) => - selectionIds.has(n.entityId), - ) - nodesToRemove.forEach((node) => { - disconnectSingleDownstreamNode(node.id) - }) - } - }, - [nodes, connectedEvaluatorNodes, connectDownstreamNode, disconnectSingleDownstreamNode], - ) - // Simplified refresh function - atoms will handle the data updates automatically const handleUpdate = useCallback(async () => { // For now, use a simple page reload since atoms auto-refresh on mount @@ -388,7 +351,6 @@ const PlaygroundHeader: React.FC = ({className, ...divPro variant="popover-cascader" adapter={evaluatorWorkflowAdapter} onSelect={handleEvaluatorToggle} - onSelectAll={handleEvaluatorsSelectAll} size="small" placeholder="Evaluator" icon={} diff --git a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/types.ts b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/types.ts index 1a5d1382ba..fc21834e51 100644 --- a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/types.ts +++ b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/types.ts @@ -659,18 +659,6 @@ export interface PopoverCascaderVariantProps< */ selectedChildIds?: Set - /** - * Callback for bulk select/deselect operations (e.g., "Select all"). - * Called with all newly selected items when "Select all" is clicked. - * Called with an empty array when "Deselect all" is clicked - * (consumer interprets empty array as "clear all for this parent"). - */ - onSelectAll?: ( - selections: TSelection[], - action: "select" | "deselect", - parentId: string, - ) => void - /** * Controls the rendering mode for child item labels. * - "full": Render using `labelNode` from adapter (if available), which may contain avatars/metadata. diff --git a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx index 8bfafaacda..d1073784b4 100644 --- a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx +++ b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx @@ -23,7 +23,7 @@ import {Button, Checkbox, Empty, Popover, Spin, Tabs} from "antd" import {useEntitySelectionCore} from "../../../hooks/useEntitySelectionCore" import {useLevelData} from "../../../hooks/utilities" -import type {EntitySelectionResult, HierarchyLevel, SelectionPathItem} from "../../../types" +import type {EntitySelectionResult, HierarchyLevel} from "../../../types" import type {PopoverCascaderVariantProps} from "../types" // ============================================================================ @@ -35,7 +35,7 @@ import type {PopoverCascaderVariantProps} from "../types" * - Header showing parent name + selection count (multi-select) * - List of child items (checkboxes in multi-select, click in single-select) */ -function ChildPanelContent({ +function ChildPanelContent({ parentId, parentLabel, childLevelConfig, @@ -48,11 +48,6 @@ function ChildPanelContent({ // Multi-select props multiSelect = false, selectedChildIds, - onSelectAll, - createSelection, - rootLevel, - rootEntity, - hierarchyLevels, childItemLabelMode = "full", }: { parentId: string @@ -67,15 +62,6 @@ function ChildPanelContent({ // Multi-select props multiSelect?: boolean selectedChildIds?: Set - onSelectAll?: ( - selections: TSelection[], - action: "select" | "deselect", - parentId: string, - ) => void - createSelection?: (path: SelectionPathItem[], leafEntity: unknown) => TSelection - rootLevel?: HierarchyLevel - rootEntity?: unknown - hierarchyLevels?: HierarchyLevel[] childItemLabelMode?: "full" | "simple" }) { const {items, query} = useLevelData({ @@ -109,62 +95,6 @@ function ChildPanelContent({ .length : 0 - const allSelected = - multiSelect && selectedCount === enabledChildren.length && enabledChildren.length > 0 - - const handleSelectAll = useCallback(() => { - if (!onSelectAll || !createSelection || !rootLevel || !rootEntity || !hierarchyLevels) - return - - const childLevel = hierarchyLevels[1] - if (!childLevel) return - - if (allSelected) { - // Deselect all — send full items and 'deselect' - const allSelections = enabledChildren.map((childEntity) => { - const path: SelectionPathItem[] = [ - {type: rootLevel.type, id: parentId, label: parentLabel}, - { - type: childLevel.type, - id: childLevel.getId(childEntity), - label: childLevel.getLabel(childEntity), - }, - ] - return createSelection(path, childEntity) - }) - onSelectAll(allSelections, "deselect", parentId) - } else { - // Select all unselected enabled children - const unselected = enabledChildren.filter( - (item) => !selectedChildIds?.has(childLevelConfig.getId(item)), - ) - const selections = unselected.map((childEntity) => { - const path: SelectionPathItem[] = [ - {type: rootLevel.type, id: parentId, label: parentLabel}, - { - type: childLevel.type, - id: childLevel.getId(childEntity), - label: childLevel.getLabel(childEntity), - }, - ] - return createSelection(path, childEntity) - }) - onSelectAll(selections, "select", parentId) - } - }, [ - onSelectAll, - createSelection, - rootLevel, - rootEntity, - hierarchyLevels, - allSelected, - enabledChildren, - selectedChildIds, - childLevelConfig, - parentId, - parentLabel, - ]) - if (query.isPending) { return (
({ )}
- {multiSelect && enabledChildren.length > 0 && ( - - )}
)} @@ -339,7 +264,6 @@ export function PopoverCascaderVariant({ // New props multiSelect = false, selectedChildIds, - onSelectAll, selectionSummary, childItemLabelMode = "full", }: PopoverCascaderVariantProps) { @@ -681,7 +605,7 @@ export function PopoverCascaderVariant({ {/* CHILD PANEL */} {selectedRootId && totalLevels > 1 && (
- + ({ disabledTooltip={disabledChildTooltip} multiSelect={multiSelect} selectedChildIds={selectedChildIds} - onSelectAll={onSelectAll} - createSelection={createSelection} - rootLevel={rootLevel} - rootEntity={selectedRootEntity} - hierarchyLevels={hierarchyLevels} childItemLabelMode={childItemLabelMode} />
From 24af85816d7c1d990eae6e375461ae6589a26511 Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Wed, 15 Apr 2026 20:21:42 +0600 Subject: [PATCH 6/9] fix --- .../WorkflowRevisionDrawer/WorkflowRevisionDrawer.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/packages/agenta-playground-ui/src/components/WorkflowRevisionDrawer/WorkflowRevisionDrawer.tsx b/web/packages/agenta-playground-ui/src/components/WorkflowRevisionDrawer/WorkflowRevisionDrawer.tsx index 2a77844ec8..fb30969a06 100644 --- a/web/packages/agenta-playground-ui/src/components/WorkflowRevisionDrawer/WorkflowRevisionDrawer.tsx +++ b/web/packages/agenta-playground-ui/src/components/WorkflowRevisionDrawer/WorkflowRevisionDrawer.tsx @@ -24,6 +24,7 @@ import DrawerContent from "./DrawerContent" import DrawerHeader from "./DrawerHeader" import { closeWorkflowRevisionDrawerAtom, + workflowRevisionDrawerContextAtom, workflowRevisionDrawerEntityIdAtom, workflowRevisionDrawerExpandedAtom, workflowRevisionDrawerOpenAtom, @@ -38,8 +39,10 @@ const WorkflowRevisionDrawer = ({playgroundContent}: WorkflowRevisionDrawerProps const isOpen = useAtomValue(workflowRevisionDrawerOpenAtom) const entityId = useAtomValue(workflowRevisionDrawerEntityIdAtom) const isExpanded = useAtomValue(workflowRevisionDrawerExpandedAtom) + const context = useAtomValue(workflowRevisionDrawerContextAtom) const closeDrawer = useSetAtom(closeWorkflowRevisionDrawerAtom) const [shouldRender, setShouldRender] = useState(!!isOpen) + const isEvaluatorDrawer = context === "evaluator-view" || context === "evaluator-create" useEffect(() => { if (isOpen) { @@ -96,7 +99,7 @@ const WorkflowRevisionDrawer = ({playgroundContent}: WorkflowRevisionDrawerProps Date: Wed, 15 Apr 2026 20:36:30 +0600 Subject: [PATCH 7/9] fic: import SelectionPathItem type in PopoverCascaderVariant component --- .../UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx index d1073784b4..e3b0e5cf35 100644 --- a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx +++ b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx @@ -23,7 +23,7 @@ import {Button, Checkbox, Empty, Popover, Spin, Tabs} from "antd" import {useEntitySelectionCore} from "../../../hooks/useEntitySelectionCore" import {useLevelData} from "../../../hooks/utilities" -import type {EntitySelectionResult, HierarchyLevel} from "../../../types" +import type {EntitySelectionResult, HierarchyLevel, SelectionPathItem} from "../../../types" import type {PopoverCascaderVariantProps} from "../types" // ============================================================================ From c540e4a1491b26b6a522246a3ef4ea28539459dc Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Thu, 16 Apr 2026 14:26:14 +0600 Subject: [PATCH 8/9] added fixed width to PopoverCascaderVariant to avoid pixel shift --- .../Components/PlaygroundHeader/index.tsx | 1 + .../EntityEvaluatorSelector.tsx | 3 ++ .../components/UnifiedEntityPicker/types.ts | 7 ++++ .../variants/PopoverCascaderVariant.tsx | 33 ++++++++++++------- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx b/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx index 0bf303cd98..5ea910b095 100644 --- a/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx +++ b/web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx @@ -359,6 +359,7 @@ const PlaygroundHeader: React.FC = ({className, ...divPro selectedChildIds={connectedRevisionIds} selectionSummary childItemLabelMode="simple" + panelWidth={280} onCreateNew={handleOpenTemplateDropdown} createNewLabel="New evaluator" popupFooter={ diff --git a/web/packages/agenta-annotation-ui/src/components/CreateQueueDrawer/EntityEvaluatorSelector.tsx b/web/packages/agenta-annotation-ui/src/components/CreateQueueDrawer/EntityEvaluatorSelector.tsx index 2f023f2559..f2cdcdca7c 100644 --- a/web/packages/agenta-annotation-ui/src/components/CreateQueueDrawer/EntityEvaluatorSelector.tsx +++ b/web/packages/agenta-annotation-ui/src/components/CreateQueueDrawer/EntityEvaluatorSelector.tsx @@ -30,6 +30,7 @@ export interface EntityEvaluatorSelectorProps { disabledRevisionIds?: Set disabledRevisionTooltip?: string panelMinWidth?: number + panelWidth?: number disabled?: boolean selectedEvaluatorId?: string | null selectedRevisionId?: string | null @@ -131,6 +132,7 @@ export function EntityEvaluatorSelector({ disabledRevisionIds, disabledRevisionTooltip = "Already added", panelMinWidth = 280, + panelWidth, disabled = false, selectedEvaluatorId, selectedRevisionId, @@ -176,6 +178,7 @@ export function EntityEvaluatorSelector({ icon={} showDropdownIcon={false} panelMinWidth={panelMinWidth} + panelWidth={panelWidth} disabled={disabled} selectedParentId={selectedEvaluatorId} selectedChildId={selectedRevisionId} diff --git a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/types.ts b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/types.ts index fc21834e51..d11f058fab 100644 --- a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/types.ts +++ b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/types.ts @@ -598,6 +598,13 @@ export interface PopoverCascaderVariantProps< */ panelMinWidth?: number + /** + * Fixed width of each cascading panel (px). + * When set, this takes precedence over `panelMinWidth` and prevents + * content-driven panel resizing. + */ + panelWidth?: number + /** * Maximum height of item lists (px) * @default 340 diff --git a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx index e3b0e5cf35..168e1e3289 100644 --- a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx +++ b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx @@ -14,7 +14,7 @@ * Pattern: Button trigger → Popover → [Root Panel | Child Panel] */ -import React, {useCallback, useEffect, useMemo, useState} from "react" +import React, {useCallback, useEffect, useMemo, useState, type CSSProperties} from "react" import {cn} from "@agenta/ui" import {EntityListItem, SearchInput} from "@agenta/ui/components/selection" @@ -42,7 +42,7 @@ function ChildPanelContent({ onSelect, selectedId, maxHeight, - panelWidth, + panelStyle, disabledIds, disabledTooltip, // Multi-select props @@ -56,7 +56,7 @@ function ChildPanelContent({ onSelect: (child: unknown) => void selectedId?: string | null maxHeight: number - panelWidth: number + panelStyle: CSSProperties disabledIds?: Set disabledTooltip?: string // Multi-select props @@ -97,17 +97,14 @@ function ChildPanelContent({ if (query.isPending) { return ( -
+
) } return ( -
+
{/* Child panel header */} {multiSelect && (
@@ -252,6 +249,7 @@ export function PopoverCascaderVariant({ showDropdownIcon = true, placement = "bottomLeft", panelMinWidth = 220, + panelWidth, maxHeight = 340, popupFooter, onCreateNew, @@ -341,6 +339,19 @@ export function PopoverCascaderVariant({ return `${selectionCount} selected` }, [selectionSummary, selectedChildIds, selectedChildId]) + const panelStyle = useMemo( + () => (panelWidth != null ? {width: panelWidth} : {minWidth: panelMinWidth}), + [panelWidth, panelMinWidth], + ) + + const childPanelStyle = useMemo( + () => + panelWidth != null + ? {width: panelWidth} + : {minWidth: panelMinWidth, maxWidth: panelMinWidth}, + [panelWidth, panelMinWidth], + ) + // Maintain auto-selection to prevent pixel shifts when searching/filtering useEffect(() => { if (!open || totalLevels <= 1) return @@ -538,7 +549,7 @@ export function PopoverCascaderVariant({ {/* ROOT PANEL */}
{/* Selection summary */} {selectionSummaryText ? ( @@ -604,7 +615,7 @@ export function PopoverCascaderVariant({ {/* CHILD PANEL */} {selectedRootId && totalLevels > 1 && ( -
+
({ selectedRootId === selectedParentId ? selectedChildId : null } maxHeight={maxHeight} - panelWidth={panelMinWidth} + panelStyle={childPanelStyle} disabledIds={disabledChildIds} disabledTooltip={disabledChildTooltip} multiSelect={multiSelect} From 6093e70fe5b99f40c573399e690e7b5dfe791d8c Mon Sep 17 00:00:00 2001 From: ashrafchowdury Date: Thu, 16 Apr 2026 19:08:15 +0600 Subject: [PATCH 9/9] fix tests? --- .../tests/playwright/acceptance/evaluators/tests.ts | 12 ++++++++---- .../variants/PopoverCascaderVariant.tsx | 11 +++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/web/oss/tests/playwright/acceptance/evaluators/tests.ts b/web/oss/tests/playwright/acceptance/evaluators/tests.ts index 3617e3c5e2..7a08280550 100644 --- a/web/oss/tests/playwright/acceptance/evaluators/tests.ts +++ b/web/oss/tests/playwright/acceptance/evaluators/tests.ts @@ -36,6 +36,9 @@ const EVALUATOR_CREATE_SUCCESS_MESSAGE = "Evaluator created successfully" // Playground - app selection const EVALUATOR_SELECT_APP_PLACEHOLDER = "Select app" const EVALUATOR_NO_APPS_TEXT = "No items found" +const EVALUATOR_POPOVER_TEST_ID = "popover-cascader-content" +const EVALUATOR_POPOVER_ROOT_PANEL_TEST_ID = "popover-cascader-root-panel" +const EVALUATOR_POPOVER_CHILD_PANEL_TEST_ID = "popover-cascader-child-panel" // Type labels shown on non-completion apps inside the popover const EVALUATOR_NON_COMPLETION_TYPE_LABELS = ["Chat", "Custom"] @@ -209,7 +212,7 @@ const selectCompletionAppFromDrawer = async ( await selectAppButton.click() // Wait for the popover to open - const popover = page.locator(".ant-popover").last() + const popover = page.getByTestId(EVALUATOR_POPOVER_TEST_ID).last() await expect(popover).toBeVisible({timeout: 5000}) // Check for empty state — no apps in this environment @@ -220,7 +223,9 @@ const selectCompletionAppFromDrawer = async ( } // Wait for app items to load in the left panel - const appItems = popover.locator('[role="option"]') + const appItems = popover + .getByTestId(EVALUATOR_POPOVER_ROOT_PANEL_TEST_ID) + .locator('[role="option"]') await expect(appItems.first()).toBeVisible({timeout: 10000}) // Find a completion-type app (items without Chat/Custom type badge text) @@ -245,8 +250,7 @@ const selectCompletionAppFromDrawer = async ( // Click the completion app to reveal the revision panel on the right await completionItem.click() - // The right panel uses a border-l separator from the root panel - const revisionPanel = popover.locator(".border-l.border-solid").first() + const revisionPanel = popover.getByTestId(EVALUATOR_POPOVER_CHILD_PANEL_TEST_ID) await expect(revisionPanel).toBeVisible({timeout: 5000}) // Select the first available revision diff --git a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx index 168e1e3289..3a9aabe71b 100644 --- a/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx +++ b/web/packages/agenta-entity-ui/src/selection/components/UnifiedEntityPicker/variants/PopoverCascaderVariant.tsx @@ -26,6 +26,12 @@ import {useLevelData} from "../../../hooks/utilities" import type {EntitySelectionResult, HierarchyLevel, SelectionPathItem} from "../../../types" import type {PopoverCascaderVariantProps} from "../types" +const POPOVER_CASCADER_TEST_IDS = { + content: "popover-cascader-content", + rootPanel: "popover-cascader-root-panel", + childPanel: "popover-cascader-child-panel", +} as const + // ============================================================================ // CHILD PANEL (internal component) // ============================================================================ @@ -104,7 +110,7 @@ function ChildPanelContent({ } return ( -
+
{/* Child panel header */} {multiSelect && (
@@ -507,7 +513,7 @@ export function PopoverCascaderVariant({ // Popover content const content = ( -
+
{/* HEADER ROW: Search + Action Button */}
@@ -548,6 +554,7 @@ export function PopoverCascaderVariant({
{/* ROOT PANEL */}