From 9c8690865e9e1d9cea6b09a9d2b1c410b6b2e435 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Wed, 10 Jun 2026 16:53:16 +0800 Subject: [PATCH] feat(toolbar): implement provider resource toolbar with sorting and filtering functionality --- src/components/providers/utils.ts | 9 + .../providers/ProvidersWorkbenchPage.tsx | 169 +++++++++++------- .../components/ProviderResourcePanel.tsx | 35 ++-- ...ss => ProviderResourceToolbar.module.scss} | 0 ...oolbar.tsx => ProviderResourceToolbar.tsx} | 16 +- 5 files changed, 141 insertions(+), 88 deletions(-) rename src/features/providers/components/{OpenAIBrandToolbar.module.scss => ProviderResourceToolbar.module.scss} (100%) rename src/features/providers/components/{OpenAIBrandToolbar.tsx => ProviderResourceToolbar.tsx} (92%) diff --git a/src/components/providers/utils.ts b/src/components/providers/utils.ts index 75dd447..0768c86 100644 --- a/src/components/providers/utils.ts +++ b/src/components/providers/utils.ts @@ -180,6 +180,15 @@ export function getProviderTotalStats( return { success: entry.success, failure: entry.failed }; } +export function getProviderRecentWindowStats( + usageByProvider: ProviderRecentUsageMap, + provider: string, + apiKey?: string, + baseUrl?: string +): { success: number; failure: number } { + return sumRecentRequests(getProviderRecentBuckets(usageByProvider, provider, apiKey, baseUrl)); +} + const collectOpenAIProviderRecentBuckets = ( provider: OpenAIProviderConfig, usageByProvider: ProviderRecentUsageMap diff --git a/src/features/providers/ProvidersWorkbenchPage.tsx b/src/features/providers/ProvidersWorkbenchPage.tsx index 27e004b..a256da2 100644 --- a/src/features/providers/ProvidersWorkbenchPage.tsx +++ b/src/features/providers/ProvidersWorkbenchPage.tsx @@ -5,16 +5,20 @@ import { useHeaderRefresh } from '@/hooks/useHeaderRefresh'; import { Skeleton } from '@/components/ui/Skeleton'; import { useAuthStore, useNotificationStore } from '@/stores'; import { useProviderRecentRequests } from '@/components/providers/hooks/useProviderRecentRequests'; -import { getOpenAIProviderRecentWindowStats } from '@/components/providers/utils'; +import { + getOpenAIProviderRecentWindowStats, + getProviderRecentWindowStats, + type ProviderRecentUsageMap, +} from '@/components/providers/utils'; import type { OpenAIProviderConfig } from '@/types'; import { ProviderHeaderCard } from './components/ProviderHeaderCard'; import { ProviderCategoryList } from './components/ProviderCategoryList'; import { ProviderResourcePanel } from './components/ProviderResourcePanel'; -import type { OpenAIPanelControls } from './components/ProviderResourcePanel'; +import type { ProviderPanelControls } from './components/ProviderResourcePanel'; import type { - OpenAISortBy, + ProviderSortBy, SortDir, -} from './components/OpenAIBrandToolbar'; +} from './components/ProviderResourceToolbar'; import { ProviderSheet, type ProviderSheetHandle } from './sheets/ProviderSheet'; import { useProviderWorkbench } from './useProviderWorkbench'; import type { ProviderBrand, ProviderResource } from './types'; @@ -59,6 +63,63 @@ const matchesFilter = (r: ProviderResource, normalized: string): boolean => { return haystack.some((v) => v.includes(normalized)); }; +const isRecord = (value: unknown): value is Record => + Boolean(value && typeof value === 'object' && !Array.isArray(value)); + +const getResourceModels = (resource: ProviderResource): string[] => { + if (!isRecord(resource.raw)) return []; + if (resource.brand === 'ampcode') { + const mappings = resource.raw.modelMappings; + if (!Array.isArray(mappings)) return []; + const seen = new Set(); + mappings.forEach((mapping) => { + if (!isRecord(mapping)) return; + const from = typeof mapping.from === 'string' ? mapping.from.trim() : ''; + const to = typeof mapping.to === 'string' ? mapping.to.trim() : ''; + if (from) seen.add(from); + if (to) seen.add(to); + }); + return Array.from(seen); + } + const models = resource.raw.models; + if (!Array.isArray(models)) return []; + const seen = new Set(); + models.forEach((model) => { + if (!isRecord(model)) return; + const name = typeof model.name === 'string' ? model.name.trim() : ''; + if (name) seen.add(name); + }); + return Array.from(seen); +}; + +const getResourcePriority = (resource: ProviderResource): number => { + if (!isRecord(resource.raw)) return 0; + const priority = resource.raw.priority; + return typeof priority === 'number' && Number.isFinite(priority) ? priority : 0; +}; + +const getResourceSortName = (resource: ProviderResource): string => + (resource.name ?? resource.identifier ?? resource.apiKeyPreview ?? '').toLowerCase(); + +const getResourceRecentSuccess = ( + resource: ProviderResource, + usageByProvider: ProviderRecentUsageMap +): number => { + if (resource.brand === 'openaiCompatibility') { + return getOpenAIProviderRecentWindowStats( + resource.raw as OpenAIProviderConfig, + usageByProvider + ).success; + } + if (resource.brand === 'ampcode') return 0; + return getProviderRecentWindowStats( + usageByProvider, + resource.brand, + resource.apiKey ?? undefined, + resource.baseUrl ?? undefined + ).success; +}; + export function ProvidersWorkbenchPage() { const { t, i18n } = useTranslation(); const connectionStatus = useAuthStore((s) => s.connectionStatus); @@ -70,11 +131,9 @@ export function ProvidersWorkbenchPage() { const workbench = useProviderWorkbench(); const [activeBrand, setActiveBrand] = useState('gemini'); const [filter, setFilter] = useState(''); - const [openaiSortBy, setOpenaiSortBy] = useState('name'); - const [openaiSortDir, setOpenaiSortDir] = useState('asc'); - const [openaiSelectedModels, setOpenaiSelectedModels] = useState>( - () => new Set() - ); + const [providerSortBy, setProviderSortBy] = useState('name'); + const [providerSortDir, setProviderSortDir] = useState('asc'); + const [selectedModels, setSelectedModels] = useState>(() => new Set()); const [sheetState, setSheetState] = useState({ open: false, brand: 'gemini', @@ -109,85 +168,69 @@ export function ProvidersWorkbenchPage() { return activeGroup.resources.filter((r) => matchesFilter(r, normalized)); }, [activeGroup, filter]); - const isOpenAI = activeGroup?.id === 'openaiCompatibility'; - - const availableOpenaiModels = useMemo(() => { - if (!isOpenAI || !activeGroup) return []; + const availableModels = useMemo(() => { + if (!activeGroup) return []; const seen = new Set(); activeGroup.resources.forEach((r) => { - const cfg = r.raw as OpenAIProviderConfig; - cfg.models?.forEach((m) => { - const name = (m.name ?? '').trim(); - if (name) seen.add(name); - }); + getResourceModels(r).forEach((name) => seen.add(name)); }); return Array.from(seen).sort(); - }, [activeGroup, isOpenAI]); + }, [activeGroup]); const visibleResources = useMemo(() => { - if (!isOpenAI) return filteredResources; - let arr = filteredResources; - if (openaiSelectedModels.size > 0) { + if (selectedModels.size > 0) { arr = arr.filter((r) => { - const cfg = r.raw as OpenAIProviderConfig; - return Boolean( - cfg.models?.some((m) => openaiSelectedModels.has((m.name ?? '').trim())) - ); + const models = getResourceModels(r); + return models.some((name) => selectedModels.has(name)); }); } const sorted = [...arr].sort((a, b) => { let diff = 0; - if (openaiSortBy === 'name') { - const an = (a.name ?? a.identifier ?? '').toLowerCase(); - const bn = (b.name ?? b.identifier ?? '').toLowerCase(); - diff = an.localeCompare(bn); - } else if (openaiSortBy === 'priority') { - const ap = (a.raw as OpenAIProviderConfig).priority ?? 0; - const bp = (b.raw as OpenAIProviderConfig).priority ?? 0; + if (providerSortBy === 'name') { + diff = getResourceSortName(a).localeCompare(getResourceSortName(b)); + } else if (providerSortBy === 'priority') { + const ap = getResourcePriority(a); + const bp = getResourcePriority(b); diff = ap - bp; } else { - const aStats = getOpenAIProviderRecentWindowStats( - a.raw as OpenAIProviderConfig, - usageByProvider - ); - const bStats = getOpenAIProviderRecentWindowStats( - b.raw as OpenAIProviderConfig, - usageByProvider - ); - diff = aStats.success - bStats.success; + diff = + getResourceRecentSuccess(a, usageByProvider) - + getResourceRecentSuccess(b, usageByProvider); } - return openaiSortDir === 'asc' ? diff : -diff; + if (diff === 0) { + diff = a.originalIndex - b.originalIndex; + } + return providerSortDir === 'asc' ? diff : -diff; }); return sorted; }, [ filteredResources, - isOpenAI, - openaiSelectedModels, - openaiSortBy, - openaiSortDir, + providerSortBy, + providerSortDir, + selectedModels, usageByProvider, ]); - const openaiControls = useMemo(() => { - if (!isOpenAI) return undefined; + const toolbarControls = useMemo(() => { + if (!activeGroup) return undefined; return { - sortBy: openaiSortBy, - sortDir: openaiSortDir, - onSortBy: setOpenaiSortBy, - onSortDir: setOpenaiSortDir, - availableModels: availableOpenaiModels, - selectedModels: openaiSelectedModels, - onSelectedModelsChange: setOpenaiSelectedModels, + sortBy: providerSortBy, + sortDir: providerSortDir, + onSortBy: setProviderSortBy, + onSortDir: setProviderSortDir, + availableModels, + selectedModels, + onSelectedModelsChange: setSelectedModels, }; }, [ - availableOpenaiModels, - isOpenAI, - openaiSelectedModels, - openaiSortBy, - openaiSortDir, + activeGroup, + availableModels, + providerSortBy, + providerSortDir, + selectedModels, ]); const totalResources = useMemo( @@ -381,7 +424,7 @@ export function ProvidersWorkbenchPage() { if (!ok) return; setActiveBrand(brand); setFilter(''); - setOpenaiSelectedModels(new Set()); + setSelectedModels(new Set()); if (isSwitching) { closeSheet(); } @@ -396,7 +439,7 @@ export function ProvidersWorkbenchPage() { selectedId={sheetState.open ? sheetState.resource?.id ?? null : null} disableMutations={disableMutations} usageByProvider={usageByProvider} - openaiControls={openaiControls} + toolbarControls={toolbarControls} onView={openView} onEdit={openEdit} onDelete={handleDelete} diff --git a/src/features/providers/components/ProviderResourcePanel.tsx b/src/features/providers/components/ProviderResourcePanel.tsx index e128914..49fafa2 100644 --- a/src/features/providers/components/ProviderResourcePanel.tsx +++ b/src/features/providers/components/ProviderResourcePanel.tsx @@ -10,10 +10,10 @@ import type { ProviderRecentUsageMap } from '@/components/providers/utils'; import type { ProviderBrand, ProviderGroup, ProviderResource } from '../types'; import { ProviderResourceTable } from './ProviderResourceTable'; import { - OpenAIBrandToolbar, - type OpenAISortBy, + ProviderResourceToolbar, + type ProviderSortBy, type SortDir, -} from './OpenAIBrandToolbar'; +} from './ProviderResourceToolbar'; import styles from './ProviderResourcePanel.module.scss'; const LOGOS: Record = { @@ -25,10 +25,10 @@ const LOGOS: Record = { ampcode: { src: ampcodeLogo }, }; -export interface OpenAIPanelControls { - sortBy: OpenAISortBy; +export interface ProviderPanelControls { + sortBy: ProviderSortBy; sortDir: SortDir; - onSortBy: (value: OpenAISortBy) => void; + onSortBy: (value: ProviderSortBy) => void; onSortDir: (value: SortDir) => void; availableModels: ReadonlyArray; selectedModels: ReadonlySet; @@ -43,7 +43,7 @@ interface ProviderResourcePanelProps { selectedId: string | null; disableMutations?: boolean; usageByProvider?: ProviderRecentUsageMap; - openaiControls?: OpenAIPanelControls; + toolbarControls?: ProviderPanelControls; onView: (resource: ProviderResource) => void; onEdit: (resource: ProviderResource) => void; onDelete: (resource: ProviderResource) => void; @@ -59,7 +59,7 @@ export function ProviderResourcePanel({ selectedId, disableMutations, usageByProvider, - openaiControls, + toolbarControls, onView, onEdit, onDelete, @@ -105,16 +105,17 @@ export function ProviderResourcePanel({ ) : null} - {openaiControls ? ( + {toolbarControls ? (
-
) : null} diff --git a/src/features/providers/components/OpenAIBrandToolbar.module.scss b/src/features/providers/components/ProviderResourceToolbar.module.scss similarity index 100% rename from src/features/providers/components/OpenAIBrandToolbar.module.scss rename to src/features/providers/components/ProviderResourceToolbar.module.scss diff --git a/src/features/providers/components/OpenAIBrandToolbar.tsx b/src/features/providers/components/ProviderResourceToolbar.tsx similarity index 92% rename from src/features/providers/components/OpenAIBrandToolbar.tsx rename to src/features/providers/components/ProviderResourceToolbar.tsx index 6556aaa..b4acb35 100644 --- a/src/features/providers/components/OpenAIBrandToolbar.tsx +++ b/src/features/providers/components/ProviderResourceToolbar.tsx @@ -7,22 +7,22 @@ import { } from '@/components/ui/icons'; import { Select } from '@/components/ui/Select'; import { SelectionCheckbox } from '@/components/ui/SelectionCheckbox'; -import styles from './OpenAIBrandToolbar.module.scss'; +import styles from './ProviderResourceToolbar.module.scss'; -export type OpenAISortBy = 'name' | 'priority' | 'recent-success'; +export type ProviderSortBy = 'name' | 'priority' | 'recent-success'; export type SortDir = 'asc' | 'desc'; -interface OpenAIBrandToolbarProps { - sortBy: OpenAISortBy; +interface ProviderResourceToolbarProps { + sortBy: ProviderSortBy; sortDir: SortDir; - onSortBy: (value: OpenAISortBy) => void; + onSortBy: (value: ProviderSortBy) => void; onSortDir: (value: SortDir) => void; availableModels: ReadonlyArray; selectedModels: ReadonlySet; onSelectedModelsChange: (next: Set) => void; } -export function OpenAIBrandToolbar({ +export function ProviderResourceToolbar({ sortBy, sortDir, onSortBy, @@ -30,7 +30,7 @@ export function OpenAIBrandToolbar({ availableModels, selectedModels, onSelectedModelsChange, -}: OpenAIBrandToolbarProps) { +}: ProviderResourceToolbarProps) { const { t } = useTranslation(); const [filterOpen, setFilterOpen] = useState(false); const containerRef = useRef(null); @@ -86,7 +86,7 @@ export function OpenAIBrandToolbar({