From ce0ec401dc8899118bc689dfd2f79b7827101b26 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Thu, 14 May 2026 14:01:14 +0800 Subject: [PATCH] feat(agents): add category filter pills to template picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 25 templates across 7 categories made the picker scroll-heavy on first open. Add a single-select category filter row above the grid so a PM can isolate Product templates in one click, an engineer can jump straight to Engineering, etc. Visual reuses the IssuesHeader scope-toggle pattern verbatim — Button variant="outline" + active class swap (bg-accent / text-muted-foreground) — so the affordance reads the same as the existing filter pills in issues / squads / runtimes / my-issues. flex-wrap keeps the 8 pills (All + 7 categories) honest on narrow widths. Counts are inlined into the label ("Engineering (8)") rather than shown as a separate badge — single-line-tall pills look right next to the picker grid, and surfacing the per-category density up front doubles as a hint at the catalog's "less but sharper" intent. When a specific category is active, the grid renders flat (no section headers) — the active pill already names what's on screen, and a header reading "Engineering" above an only-Engineering grid is visual duplication. "All" falls back to the prior grouped layout. State is component-local (no URL sync, no persistence) since the picker is dialog-internal transient state — closing the dialog naturally resets the filter, which is the expected behaviour for a "choose from a catalog" surface. i18n: new `create_dialog.template_picker.filter_all` key in en + zh. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../agents/components/template-picker.tsx | 123 +++++++++++++++--- packages/views/locales/en/agents.json | 3 +- packages/views/locales/zh-Hans/agents.json | 3 +- 3 files changed, 109 insertions(+), 20 deletions(-) diff --git a/packages/views/agents/components/template-picker.tsx b/packages/views/agents/components/template-picker.tsx index 0ce88ec683..b94c4ecf0b 100644 --- a/packages/views/agents/components/template-picker.tsx +++ b/packages/views/agents/components/template-picker.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { AlertTriangle, AlignLeft, @@ -38,6 +38,7 @@ import type { LucideIcon } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import { agentTemplateListOptions } from "@multica/core/agents/queries"; import type { AgentTemplateSummary } from "@multica/core/types"; +import { Button } from "@multica/ui/components/ui/button"; import { cn } from "@multica/ui/lib/utils"; import { useT } from "../../i18n"; @@ -68,6 +69,11 @@ export function TemplatePicker({ onSelect }: TemplatePickerProps) { agentTemplateListOptions(), ); + // `null` = "All" (default). When a specific category is selected, the + // grid renders flat (no section headers) — the active pill already + // tells the user what they're looking at, so headers would be noise. + const [selectedCategory, setSelectedCategory] = useState(null); + // Group by category. Templates without a category fall into the // localised "Other" bucket so they still render. Preserves the load // order within each group for deterministic UI (matches the @@ -83,6 +89,18 @@ export function TemplatePicker({ onSelect }: TemplatePickerProps) { return Array.from(byCategory.entries()); }, [templates, otherCategory]); + // Templates currently visible given the filter. When "All" is active + // we show every template (grouped by category below); otherwise we + // only show the matching category. + const visibleTemplates = useMemo(() => { + if (selectedCategory === null) return templates; + return templates.filter( + (tmpl) => + (tmpl.category?.trim() ? tmpl.category : otherCategory) === + selectedCategory, + ); + }, [templates, selectedCategory, otherCategory]); + if (isLoading) { return (
@@ -113,28 +131,97 @@ export function TemplatePicker({ onSelect }: TemplatePickerProps) { return (
-
- {groups.map(([category, tmpls]) => ( -
-

- {category} -

-
- {tmpls.map((tmpl) => ( - onSelect(tmpl)} - /> - ))} -
-
- ))} +
+ {/* Category filter — mirrors the IssuesHeader scope pattern + (Button variant="outline" + active-class swap). `flex-wrap` + so the 8 pills (All + 7 categories) degrade gracefully on + narrow widths. Counts are inlined into the label rather than + shown as a separate badge because we want the pill row to + stay one-line-tall per pill. */} +
+ $.create_dialog.template_picker.filter_all)} (${templates.length})`} + active={selectedCategory === null} + onClick={() => setSelectedCategory(null)} + /> + {groups.map(([category, tmpls]) => ( + setSelectedCategory(category)} + /> + ))} +
+ + {/* Grid — grouped with sticky headers when "All" is active; + flat when a single category is filtered (the active pill + already tells the user what they're looking at). */} + {selectedCategory === null ? ( +
+ {groups.map(([category, tmpls]) => ( +
+

+ {category} +

+
+ {tmpls.map((tmpl) => ( + onSelect(tmpl)} + /> + ))} +
+
+ ))} +
+ ) : ( +
+ {visibleTemplates.map((tmpl) => ( + onSelect(tmpl)} + /> + ))} +
+ )}
); } +/** Single filter pill. Visual matches IssuesHeader's scope toggle + * (Button outline + bg-accent on active) so the catalog feels + * consistent with the rest of the app's filter affordances. */ +function FilterPill({ + label, + active, + onClick, +}: { + label: string; + active: boolean; + onClick: () => void; +}) { + return ( + + ); +} + interface TemplateCardProps { template: AgentTemplateSummary; onClick: () => void; diff --git a/packages/views/locales/en/agents.json b/packages/views/locales/en/agents.json index af928c1b96..7eb7131268 100644 --- a/packages/views/locales/en/agents.json +++ b/packages/views/locales/en/agents.json @@ -234,7 +234,8 @@ "title": "Pick a template", "empty": "No templates available yet.", "load_failed": "Failed to load templates", - "other_category": "Other" + "other_category": "Other", + "filter_all": "All" }, "template_card": { "skills_one": "{{count}} skill", diff --git a/packages/views/locales/zh-Hans/agents.json b/packages/views/locales/zh-Hans/agents.json index 8f01864d4f..854615ce21 100644 --- a/packages/views/locales/zh-Hans/agents.json +++ b/packages/views/locales/zh-Hans/agents.json @@ -229,7 +229,8 @@ "title": "选择模板", "empty": "暂无可用模板。", "load_failed": "加载模板失败", - "other_category": "其他" + "other_category": "其他", + "filter_all": "全部" }, "template_card": { "skills_other": "{{count}} 个 skill",