feat(toolbar): implement provider resource toolbar with sorting and filtering functionality

This commit is contained in:
LTbinglingfeng
2026-06-10 16:53:16 +08:00
parent a04ffb5298
commit 9c8690865e
5 changed files with 141 additions and 88 deletions
+9
View File
@@ -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
+106 -63
View File
@@ -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<string, unknown> =>
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<string>();
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<string>();
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<ProviderBrand>('gemini');
const [filter, setFilter] = useState('');
const [openaiSortBy, setOpenaiSortBy] = useState<OpenAISortBy>('name');
const [openaiSortDir, setOpenaiSortDir] = useState<SortDir>('asc');
const [openaiSelectedModels, setOpenaiSelectedModels] = useState<Set<string>>(
() => new Set()
);
const [providerSortBy, setProviderSortBy] = useState<ProviderSortBy>('name');
const [providerSortDir, setProviderSortDir] = useState<SortDir>('asc');
const [selectedModels, setSelectedModels] = useState<Set<string>>(() => new Set());
const [sheetState, setSheetState] = useState<SheetState>({
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<string>();
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<OpenAIPanelControls | undefined>(() => {
if (!isOpenAI) return undefined;
const toolbarControls = useMemo<ProviderPanelControls | undefined>(() => {
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}
@@ -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<ProviderBrand, { src: string; invertOnDark?: boolean }> = {
@@ -25,10 +25,10 @@ const LOGOS: Record<ProviderBrand, { src: string; invertOnDark?: boolean }> = {
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<string>;
selectedModels: ReadonlySet<string>;
@@ -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({
</div>
) : null}
</div>
{openaiControls ? (
{toolbarControls ? (
<div className={styles.headerToolbarRow}>
<OpenAIBrandToolbar
sortBy={openaiControls.sortBy}
sortDir={openaiControls.sortDir}
onSortBy={openaiControls.onSortBy}
onSortDir={openaiControls.onSortDir}
availableModels={openaiControls.availableModels}
selectedModels={openaiControls.selectedModels}
onSelectedModelsChange={openaiControls.onSelectedModelsChange}
<ProviderResourceToolbar
key={group.id}
sortBy={toolbarControls.sortBy}
sortDir={toolbarControls.sortDir}
onSortBy={toolbarControls.onSortBy}
onSortDir={toolbarControls.onSortDir}
availableModels={toolbarControls.availableModels}
selectedModels={toolbarControls.selectedModels}
onSelectedModelsChange={toolbarControls.onSelectedModelsChange}
/>
</div>
) : null}
@@ -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<string>;
selectedModels: ReadonlySet<string>;
onSelectedModelsChange: (next: Set<string>) => 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<HTMLDivElement>(null);
@@ -86,7 +86,7 @@ export function OpenAIBrandToolbar({
<Select
value={sortBy}
options={sortOptions}
onChange={(value) => onSortBy(value as OpenAISortBy)}
onChange={(value) => onSortBy(value as ProviderSortBy)}
ariaLabel={t('providersPage.toolbar.sortBy')}
size="sm"
/>