diff --git a/src/app/(dashboard)/dashboard/providers/page.tsx b/src/app/(dashboard)/dashboard/providers/page.tsx index bdeda2ad53..581292c0a9 100644 --- a/src/app/(dashboard)/dashboard/providers/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect, useCallback } from "react"; -import { Card, CardSkeleton, Badge, Button, Input, Toggle } from "@/shared/components"; +import { Card, CardSkeleton, Badge, Button, Input, Toggle, CollapsibleSection } from "@/shared/components"; import { FREE_PROVIDERS, OAUTH_PROVIDERS, @@ -123,7 +123,10 @@ export default function ProvidersPage() { } | null>(null); const [repairingEnv, setRepairingEnv] = useState(false); const [searchQuery, setSearchQuery] = useState(""); + const [showFreeOnly, setShowFreeOnly] = useState(false); + const [activeCategory, setActiveCategory] = useState(null); const notify = useNotificationStore(); + const showSection = (category: string) => !activeCategory || activeCategory === category; const t = useTranslations("providers"); const tc = useTranslations("common"); const ccCompatibleLabel = t("ccCompatibleLabel"); @@ -419,7 +422,8 @@ export default function ProvidersPage() { const oauthProviderEntries = filterConfiguredProviderEntries( oauthProviderEntriesAll, showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const apiKeyProviderEntriesAll = buildStaticProviderEntries("apikey", getProviderStats); @@ -433,74 +437,86 @@ export default function ProvidersPage() { !EMBEDDING_RERANK_PROVIDER_IDS.has(entry.providerId) ), showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const aggregatorProviderEntries = filterConfiguredProviderEntries( apiKeyProviderEntriesAll.filter((entry) => AGGREGATOR_PROVIDER_IDS.has(entry.providerId)), showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const imageProviderEntries = filterConfiguredProviderEntries( apiKeyProviderEntriesAll.filter((entry) => IMAGE_ONLY_PROVIDER_IDS.has(entry.providerId)), showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const enterpriseProviderEntries = filterConfiguredProviderEntries( apiKeyProviderEntriesAll.filter((entry) => ENTERPRISE_CLOUD_PROVIDER_IDS.has(entry.providerId)), showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const videoProviderEntries = filterConfiguredProviderEntries( apiKeyProviderEntriesAll.filter((entry) => VIDEO_PROVIDER_IDS.has(entry.providerId)), showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const embeddingRerankProviderEntries = filterConfiguredProviderEntries( apiKeyProviderEntriesAll.filter((entry) => EMBEDDING_RERANK_PROVIDER_IDS.has(entry.providerId)), showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const webCookieProviderEntriesAll = buildStaticProviderEntries("web-cookie", getProviderStats); const webCookieProviderEntries = filterConfiguredProviderEntries( webCookieProviderEntriesAll, showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const localProviderEntriesAll = buildStaticProviderEntries("local", getProviderStats); const localProviderEntries = filterConfiguredProviderEntries( localProviderEntriesAll, showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const searchProviderEntriesAll = buildStaticProviderEntries("search", getProviderStats); const searchProviderEntries = filterConfiguredProviderEntries( searchProviderEntriesAll, showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const audioProviderEntriesAll = buildStaticProviderEntries("audio", getProviderStats); const audioProviderEntries = filterConfiguredProviderEntries( audioProviderEntriesAll, showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const cloudAgentProviderEntriesAll = buildStaticProviderEntries("cloud-agent", getProviderStats); const cloudAgentProviderEntries = filterConfiguredProviderEntries( cloudAgentProviderEntriesAll, showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const upstreamProxyEntriesAll = buildStaticProviderEntries("upstream-proxy", getProviderStats); const upstreamProxyEntries = filterConfiguredProviderEntries( upstreamProxyEntriesAll, showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const compatibleProviderEntriesAll = [ @@ -529,7 +545,8 @@ export default function ProvidersPage() { const compatibleProviderEntries = filterConfiguredProviderEntries( compatibleProviderEntriesAll, showConfiguredOnly, - searchQuery + searchQuery, + showFreeOnly ); const FREE_SECTION_IDS = new Set([ @@ -754,6 +771,48 @@ export default function ProvidersPage() { )} + {/* Category Filter Chips */} +
+ {[ + { key: null, label: t("allProviders") || "All" }, + { key: "free", label: t("freeProviders") || "Free" }, + { key: "oauth", label: t("oauthProviders") }, + { key: "apikey", label: t("apiKeyProviders") }, + { key: "webcookie", label: t("webCookieProviders") || "Web Cookie" }, + { key: "search", label: t("searchProviders") || "Search" }, + { key: "audio", label: t("audioProviders") || "Audio" }, + { key: "cloudagent", label: t("cloudAgentProviders") || "Cloud Agent" }, + { key: "local", label: t("localProviders") || "Local" }, + { key: "compatible", label: t("compatibleProviders") || "Compatible" }, + ].map((cat) => ( + + ))} +
+ {/* Expiration Banner */} {expirations?.summary && (expirations.summary.expired > 0 || expirations.summary.expiringSoon > 0) && ( @@ -844,6 +903,20 @@ export default function ProvidersPage() {
+ + +
+
{children}
+
+
+ ); +} diff --git a/src/shared/components/InfoTooltip.tsx b/src/shared/components/InfoTooltip.tsx new file mode 100644 index 0000000000..9f62de5b16 --- /dev/null +++ b/src/shared/components/InfoTooltip.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { cn } from "@/shared/utils/cn"; + +interface InfoTooltipProps { + text: string; + className?: string; +} + +export default function InfoTooltip({ text, className }: InfoTooltipProps) { + return ( + + + info + + + {text} + + + ); +} diff --git a/src/shared/components/PresetSlider.tsx b/src/shared/components/PresetSlider.tsx new file mode 100644 index 0000000000..17ffe35471 --- /dev/null +++ b/src/shared/components/PresetSlider.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { cn } from "@/shared/utils/cn"; + +interface PresetSliderProps { + value: number; + onChange: (value: number) => void; + presets: { label: string; value: number }[]; + min?: number; + max?: number; + step?: number; + className?: string; +} + +export default function PresetSlider({ + value, + onChange, + presets, + min = 0, + max = 100, + step = 1, + className, +}: PresetSliderProps) { + return ( +
+
+ {presets.map((preset) => ( + + ))} +
+ onChange(Number(e.target.value))} + className={cn( + "w-full h-1.5 rounded-full appearance-none cursor-pointer", + "bg-black/10 dark:bg-white/10", + "accent-primary" + )} + /> +
+ {min} + {value} + {max} +
+
+ ); +} diff --git a/src/shared/components/index.tsx b/src/shared/components/index.tsx index 568c9e44e0..b891af64b7 100644 --- a/src/shared/components/index.tsx +++ b/src/shared/components/index.tsx @@ -32,6 +32,9 @@ export { default as NotificationToast } from "./NotificationToast"; export { default as FilterBar } from "./FilterBar"; export { default as ColumnToggle } from "./ColumnToggle"; export { default as DataTable } from "./DataTable"; +export { default as CollapsibleSection } from "./CollapsibleSection"; +export { default as InfoTooltip } from "./InfoTooltip"; +export { default as PresetSlider } from "./PresetSlider"; // Layouts export * from "./layouts";