authorgravatar for 73941998+UtkarshUsername@users.noreply.github.comUtkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> 2026-09-09 23:36:53+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-09 11:06:53-07:00
loge16b8b059c9f5ff6dfed1addecffb831c6aee043
tree6828d4f45db215973379a92c3f505a04315ec134
parenta29a7cc5853e16d4ca7e07acf364836f71b6101a
signature Signed by PGP key B5690EEEBB952194

feat(web): add provider model bulk toggle (#10947)


2 files changed, 56 insertions(+), 6 deletions(-)

apps/web/src/components/settings/ProviderModelsSection.test.ts+18-1
......@@ -1,7 +1,7 @@
11import { describe, expect, it } from "vite-plus/test";
22import type { ServerProviderModel } from "@t3tools/contracts";
33
4import { groupModelsForDisplay } from "./ProviderModelsSection";
4import { groupModelsForDisplay, nextHiddenModelsForBulkToggle } from "./ProviderModelsSection";
55
66function model(slug: string, isCustom = false): ServerProviderModel {
77 return { slug, name: slug, isCustom, capabilities: null };
......@@ -21,3 +21,20 @@ describe("groupModelsForDisplay", () => {
2121 expect(display.map((entry) => entry.slug)).toEqual(["c", "d", "b", "custom", "a"]);
2222 });
2323});
24
25describe("nextHiddenModelsForBulkToggle", () => {
26 it("hides every built-in model without hiding custom models", () => {
27 const models = [model("a"), model("b"), model("custom", true)];
28
29 expect(nextHiddenModelsForBulkToggle(models, ["a"])).toEqual(["a", "b"]);
30 });
31
32 it("shows every built-in model while preserving unrelated hidden entries", () => {
33 const models = [model("a"), model("b"), model("custom", true)];
34
35 expect(nextHiddenModelsForBulkToggle(models, ["a", "b", "legacy", "custom"])).toEqual([
36 "legacy",
37 "custom",
38 ]);
39 });
40});
apps/web/src/components/settings/ProviderModelsSection.tsx+38-5
......@@ -96,6 +96,21 @@ export function groupModelsForDisplay<
9696 ];
9797}
9898
99export function nextHiddenModelsForBulkToggle(
100 models: ReadonlyArray<Pick<ServerProviderModel, "slug" | "isCustom">>,
101 hiddenModels: ReadonlyArray<string>,
102): string[] {
103 const builtInSlugs = models.filter((model) => !model.isCustom).map((model) => model.slug);
104 const builtInSlugSet = new Set(builtInSlugs);
105 const allBuiltInModelsHidden = builtInSlugs.every((slug) => hiddenModels.includes(slug));
106
107 if (allBuiltInModelsHidden) {
108 return hiddenModels.filter((slug) => !builtInSlugSet.has(slug));
109 }
110
111 return [...new Set([...hiddenModels, ...builtInSlugs])];
112}
113
99114interface ProviderModelsSectionProps {
100115 /** Identifier used to namespace input ids within the DOM. */
101116 readonly instanceId: ProviderInstanceId;
......@@ -181,6 +196,8 @@ export function ProviderModelsSection({
181196 (model) => !model.isCustom && hiddenModelSet.has(model.slug),
182197 ).length;
183198 const builtInModels = useMemo(() => models.filter((model) => !model.isCustom), [models]);
199 const allBuiltInModelsHidden =
200 builtInModels.length > 0 && builtInModels.every((model) => hiddenModelSet.has(model.slug));
184201 const showFilter = models.length > FILTER_THRESHOLD;
185202 const normalizedFilter = filter.trim().toLowerCase();
186203 const isFiltering = showFilter && normalizedFilter.length > 0;
......@@ -502,11 +519,27 @@ export function ProviderModelsSection({
502519 aria-label="Filter models"
503520 />
504521 ) : null}
505 <span className="text-xs text-muted-foreground">
506 {models.length} model{models.length === 1 ? "" : "s"}
507 {favoriteCount > 0 ? ` · ${favoriteCount} favorite${favoriteCount === 1 ? "" : "s"}` : ""}
508 {hiddenCount > 0 ? ` · ${hiddenCount} hidden` : ""}
509 </span>
522 <div className="flex items-center gap-2">
523 {builtInModels.length > 0 ? (
524 <Button
525 type="button"
526 size="xs"
527 variant="ghost-muted"
528 onClick={() =>
529 onHiddenModelsChange(nextHiddenModelsForBulkToggle(models, hiddenModels))
530 }
531 >
532 {allBuiltInModelsHidden ? "Enable all" : "Disable all"}
533 </Button>
534 ) : null}
535 <span className="text-xs text-muted-foreground">
536 {models.length} model{models.length === 1 ? "" : "s"}
537 {favoriteCount > 0
538 ? ` · ${favoriteCount} favorite${favoriteCount === 1 ? "" : "s"}`
539 : ""}
540 {hiddenCount > 0 ? ` · ${hiddenCount} hidden` : ""}
541 </span>
542 </div>
510543 </div>
511544 <div
512545 ref={listRef}