feat(frontend): implement comprehensive API Settings Management Interface

Implement a complete API Management Interface for React frontend with split-
panel layout, migrating all features from legacy JS frontend.

New Features:
- API Settings page with 5 tabs: Providers, Endpoints, Cache, Model Pools, CLI Settings
- Provider Management: CRUD operations, multi-key rotation, health checks, test connection
- Endpoint Management: CRUD operations, cache strategy configuration, enable/disable toggle
- Cache Settings: Global configuration, statistics display, clear cache functionality
- Model Pool Management: CRUD operations, auto-discovery feature, provider exclusion
- CLI Settings Management: Provider-based and Direct modes, full CRUD support
- Multi-Key Settings Modal: Manage API keys with rotation strategies and weights
- Manage Models Modal: View and manage models per provider (LLM and Embedding)
- Sync to CodexLens: Integration handler for provider configuration sync

Technical Implementation:
- Created 12 new React components in components/api-settings/
- Extended lib/api.ts with 460+ lines of API client functions
- Created hooks/useApiSettings.ts with 772 lines of TanStack Query hooks
- Added RadioGroup UI component for form selections
- Implemented unified error handling with useNotifications across all operations
- Complete i18n support (500+ keys in English and Chinese)
- Route integration (/api-settings) and sidebar navigation

Code Quality:
- All acceptance criteria from plan.json verified
- Code review passed with Gemini (all 7 IMPL tasks complete)
- Follows existing patterns: Shadcn UI, TanStack Query, react-intl, Lucide icons
This commit is contained in:
catlog22
2026-02-01 23:58:04 +08:00
parent 690597bae8
commit abce912ee5
33 changed files with 5874 additions and 45 deletions

View File

@@ -220,6 +220,11 @@ export {
useUpdateIgnorePatterns,
useCodexLensMutations,
codexLensKeys,
useCodexLensIndexes,
useCodexLensIndexingStatus,
useRebuildIndex,
useUpdateIndex,
useCancelIndexing,
} from './useCodexLens';
export type {
UseCodexLensDashboardOptions,
@@ -248,4 +253,10 @@ export type {
UseUpdateCodexLensEnvReturn,
UseSelectGpuReturn,
UseUpdateIgnorePatternsReturn,
UseCodexLensIndexesOptions,
UseCodexLensIndexesReturn,
UseCodexLensIndexingStatusReturn,
UseRebuildIndexReturn,
UseUpdateIndexReturn,
UseCancelIndexingReturn,
} from './useCodexLens';

View File

@@ -33,6 +33,11 @@ import {
checkCcwLitellmStatus,
installCcwLitellm,
uninstallCcwLitellm,
fetchCliSettings,
createCliSettings,
updateCliSettings,
deleteCliSettings,
toggleCliSettingsEnabled,
type ProviderCredential,
type CustomEndpoint,
type CacheStats,
@@ -40,6 +45,8 @@ import {
type ModelPoolConfig,
type ModelPoolType,
type DiscoveredProvider,
type CliSettingsEndpoint,
type SaveCliSettingsRequest,
} from '../lib/api';
// Query key factory
@@ -53,6 +60,8 @@ export const apiSettingsKeys = {
modelPools: () => [...apiSettingsKeys.all, 'modelPools'] as const,
modelPool: (id: string) => [...apiSettingsKeys.modelPools(), id] as const,
ccwLitellm: () => [...apiSettingsKeys.all, 'ccwLitellm'] as const,
cliSettings: () => [...apiSettingsKeys.all, 'cliSettings'] as const,
cliSetting: (id: string) => [...apiSettingsKeys.cliSettings(), id] as const,
};
const STALE_TIME = 2 * 60 * 1000;
@@ -621,3 +630,142 @@ export function useUninstallCcwLitellm() {
error: mutation.error,
};
}
// ========================================
// CLI Settings Hooks
// ========================================
export interface UseCliSettingsOptions {
staleTime?: number;
enabled?: boolean;
}
export interface UseCliSettingsReturn {
cliSettings: CliSettingsEndpoint[];
totalCount: number;
enabledCount: number;
providerBasedCount: number;
directCount: number;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
refetch: () => Promise<void>;
invalidate: () => Promise<void>;
}
export function useCliSettings(options: UseCliSettingsOptions = {}): UseCliSettingsReturn {
const { staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const query = useQuery({
queryKey: apiSettingsKeys.cliSettings(),
queryFn: fetchCliSettings,
staleTime,
enabled,
retry: 2,
});
const cliSettings = query.data?.endpoints ?? [];
const enabledCliSettings = cliSettings.filter((s) => s.enabled);
// Determine mode based on whether settings have providerId in description or env vars
const providerBasedCount = cliSettings.filter((s) => {
// Provider-based: has ANTHROPIC_BASE_URL set to provider's apiBase
return s.settings.env.ANTHROPIC_BASE_URL && !s.settings.env.ANTHROPIC_BASE_URL.includes('api.anthropic.com');
}).length;
const directCount = cliSettings.length - providerBasedCount;
const refetch = async () => {
await query.refetch();
};
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cliSettings() });
};
return {
cliSettings,
totalCount: cliSettings.length,
enabledCount: enabledCliSettings.length,
providerBasedCount,
directCount,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
invalidate,
};
}
export function useCreateCliSettings() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (request: SaveCliSettingsRequest) => createCliSettings(request),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cliSettings() });
},
});
return {
createCliSettings: mutation.mutateAsync,
isCreating: mutation.isPending,
error: mutation.error,
};
}
export function useUpdateCliSettings() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ endpointId, request }: { endpointId: string; request: Partial<SaveCliSettingsRequest> }) =>
updateCliSettings(endpointId, request),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cliSettings() });
},
});
return {
updateCliSettings: (endpointId: string, request: Partial<SaveCliSettingsRequest>) =>
mutation.mutateAsync({ endpointId, request }),
isUpdating: mutation.isPending,
error: mutation.error,
};
}
export function useDeleteCliSettings() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (endpointId: string) => deleteCliSettings(endpointId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cliSettings() });
},
});
return {
deleteCliSettings: mutation.mutateAsync,
isDeleting: mutation.isPending,
error: mutation.error,
};
}
export function useToggleCliSettings() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ endpointId, enabled }: { endpointId: string; enabled: boolean }) =>
toggleCliSettingsEnabled(endpointId, enabled),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: apiSettingsKeys.cliSettings() });
},
});
return {
toggleCliSettings: (endpointId: string, enabled: boolean) =>
mutation.mutateAsync({ endpointId, enabled }),
isToggling: mutation.isPending,
error: mutation.error,
};
}

View File

@@ -11,6 +11,7 @@ import {
fetchCodexLensConfig,
updateCodexLensConfig,
bootstrapCodexLens,
installCodexLensSemantic,
uninstallCodexLens,
fetchCodexLensModels,
fetchCodexLensModelInfo,
@@ -52,6 +53,7 @@ import {
type CodexLensSymbolSearchResponse,
type CodexLensIndexesResponse,
type CodexLensIndexingStatusResponse,
type CodexLensSemanticInstallResponse,
} from '../lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
@@ -553,6 +555,33 @@ export function useBootstrapCodexLens(): UseBootstrapCodexLensReturn {
};
}
export interface UseInstallSemanticReturn {
installSemantic: (gpuMode: 'cpu' | 'cuda' | 'directml') => Promise<{ success: boolean; message?: string; gpuMode?: string }>;
isInstalling: boolean;
error: Error | null;
}
/**
* Hook for installing CodexLens semantic dependencies
*/
export function useInstallSemantic(): UseInstallSemanticReturn {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: installCodexLensSemantic,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: codexLensKeys.all });
queryClient.invalidateQueries({ queryKey: codexLensKeys.dashboard() });
},
});
return {
installSemantic: mutation.mutateAsync,
isInstalling: mutation.isPending,
error: mutation.error,
};
}
export interface UseUninstallCodexLensReturn {
uninstall: () => Promise<{ success: boolean; message?: string }>;
isUninstalling: boolean;
@@ -922,6 +951,7 @@ export function useCancelIndexing(): UseCancelIndexingReturn {
export function useCodexLensMutations() {
const updateConfig = useUpdateCodexLensConfig();
const bootstrap = useBootstrapCodexLens();
const installSemantic = useInstallSemantic();
const uninstall = useUninstallCodexLens();
const download = useDownloadModel();
const deleteModel = useDeleteModel();
@@ -937,6 +967,8 @@ export function useCodexLensMutations() {
isUpdatingConfig: updateConfig.isUpdating,
bootstrap: bootstrap.bootstrap,
isBootstrapping: bootstrap.isBootstrapping,
installSemantic: installSemantic.installSemantic,
isInstallingSemantic: installSemantic.isInstalling,
uninstall: uninstall.uninstall,
isUninstalling: uninstall.isUninstalling,
downloadModel: download.downloadModel,
@@ -961,6 +993,7 @@ export function useCodexLensMutations() {
isMutating:
updateConfig.isUpdating ||
bootstrap.isBootstrapping ||
installSemantic.isInstalling ||
uninstall.isUninstalling ||
download.isDownloading ||
deleteModel.isDeleting ||