Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion apps/web/src/components/settings/ProviderModelsSection.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from "vite-plus/test";
import type { ServerProviderModel } from "@t3tools/contracts";

import { groupModelsForDisplay } from "./ProviderModelsSection";
import { groupModelsForDisplay, nextHiddenModelsForBulkToggle } from "./ProviderModelsSection";

function model(slug: string, isCustom = false): ServerProviderModel {
return { slug, name: slug, isCustom, capabilities: null };
Expand All @@ -21,3 +21,20 @@ describe("groupModelsForDisplay", () => {
expect(display.map((entry) => entry.slug)).toEqual(["c", "d", "b", "custom", "a"]);
});
});

describe("nextHiddenModelsForBulkToggle", () => {
it("hides every built-in model without hiding custom models", () => {
const models = [model("a"), model("b"), model("custom", true)];

expect(nextHiddenModelsForBulkToggle(models, ["a"])).toEqual(["a", "b"]);
});

it("shows every built-in model while preserving unrelated hidden entries", () => {
const models = [model("a"), model("b"), model("custom", true)];

expect(nextHiddenModelsForBulkToggle(models, ["a", "b", "legacy", "custom"])).toEqual([
"legacy",
"custom",
]);
});
});
43 changes: 38 additions & 5 deletions apps/web/src/components/settings/ProviderModelsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ export function groupModelsForDisplay<
];
}

export function nextHiddenModelsForBulkToggle(
models: ReadonlyArray<Pick<ServerProviderModel, "slug" | "isCustom">>,
hiddenModels: ReadonlyArray<string>,
): string[] {
const builtInSlugs = models.filter((model) => !model.isCustom).map((model) => model.slug);
const builtInSlugSet = new Set(builtInSlugs);
const allBuiltInModelsHidden = builtInSlugs.every((slug) => hiddenModels.includes(slug));

if (allBuiltInModelsHidden) {
return hiddenModels.filter((slug) => !builtInSlugSet.has(slug));
}

return [...new Set([...hiddenModels, ...builtInSlugs])];
}

interface ProviderModelsSectionProps {
/** Identifier used to namespace input ids within the DOM. */
readonly instanceId: ProviderInstanceId;
Expand Down Expand Up @@ -181,6 +196,8 @@ export function ProviderModelsSection({
(model) => !model.isCustom && hiddenModelSet.has(model.slug),
).length;
const builtInModels = useMemo(() => models.filter((model) => !model.isCustom), [models]);
const allBuiltInModelsHidden =
builtInModels.length > 0 && builtInModels.every((model) => hiddenModelSet.has(model.slug));
const showFilter = models.length > FILTER_THRESHOLD;
const normalizedFilter = filter.trim().toLowerCase();
const isFiltering = showFilter && normalizedFilter.length > 0;
Expand Down Expand Up @@ -502,11 +519,27 @@ export function ProviderModelsSection({
aria-label="Filter models"
/>
) : null}
<span className="text-xs text-muted-foreground">
{models.length} model{models.length === 1 ? "" : "s"}
{favoriteCount > 0 ? ` · ${favoriteCount} favorite${favoriteCount === 1 ? "" : "s"}` : ""}
{hiddenCount > 0 ? ` · ${hiddenCount} hidden` : ""}
</span>
<div className="flex items-center gap-2">
{builtInModels.length > 0 ? (
<Button
type="button"
size="xs"
variant="ghost-muted"
onClick={() =>
onHiddenModelsChange(nextHiddenModelsForBulkToggle(models, hiddenModels))
}
>
{allBuiltInModelsHidden ? "Enable all" : "Disable all"}
</Button>
) : null}
<span className="text-xs text-muted-foreground">
{models.length} model{models.length === 1 ? "" : "s"}
{favoriteCount > 0
? ` · ${favoriteCount} favorite${favoriteCount === 1 ? "" : "s"}`
: ""}
{hiddenCount > 0 ? ` · ${hiddenCount} hidden` : ""}
</span>
</div>
</div>
<div
ref={listRef}
Expand Down
Loading