Add complete provider configuration system with the following features: - Secure API key storage using Electron's safeStorage encryption - Provider CRUD operations with IPC handlers - Lazy-loaded electron-store for ESM compatibility - Provider settings UI component with add/edit/delete functionality - API key masking for display (shows first/last 4 chars) - Basic API key format validation per provider type - Default provider selection - Provider enable/disable toggle New files: - electron/utils/secure-storage.ts: Encrypted key storage and provider config - src/stores/providers.ts: Zustand store for provider state - src/components/settings/ProvidersSettings.tsx: Provider management UI
199 lines
5.8 KiB
TypeScript
199 lines
5.8 KiB
TypeScript
/**
|
|
* Provider State Store
|
|
* Manages AI provider configurations
|
|
*/
|
|
import { create } from 'zustand';
|
|
|
|
/**
|
|
* Provider configuration
|
|
*/
|
|
export interface ProviderConfig {
|
|
id: string;
|
|
name: string;
|
|
type: 'anthropic' | 'openai' | 'google' | 'ollama' | 'custom';
|
|
baseUrl?: string;
|
|
model?: string;
|
|
enabled: boolean;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
/**
|
|
* Provider with key info (for display)
|
|
*/
|
|
export interface ProviderWithKeyInfo extends ProviderConfig {
|
|
hasKey: boolean;
|
|
keyMasked: string | null;
|
|
}
|
|
|
|
interface ProviderState {
|
|
providers: ProviderWithKeyInfo[];
|
|
defaultProviderId: string | null;
|
|
loading: boolean;
|
|
error: string | null;
|
|
|
|
// Actions
|
|
fetchProviders: () => Promise<void>;
|
|
addProvider: (config: Omit<ProviderConfig, 'createdAt' | 'updatedAt'>, apiKey?: string) => Promise<void>;
|
|
updateProvider: (providerId: string, updates: Partial<ProviderConfig>, apiKey?: string) => Promise<void>;
|
|
deleteProvider: (providerId: string) => Promise<void>;
|
|
setApiKey: (providerId: string, apiKey: string) => Promise<void>;
|
|
deleteApiKey: (providerId: string) => Promise<void>;
|
|
setDefaultProvider: (providerId: string) => Promise<void>;
|
|
validateApiKey: (providerId: string, apiKey: string) => Promise<{ valid: boolean; error?: string }>;
|
|
getApiKey: (providerId: string) => Promise<string | null>;
|
|
}
|
|
|
|
export const useProviderStore = create<ProviderState>((set, get) => ({
|
|
providers: [],
|
|
defaultProviderId: null,
|
|
loading: false,
|
|
error: null,
|
|
|
|
fetchProviders: async () => {
|
|
set({ loading: true, error: null });
|
|
|
|
try {
|
|
const providers = await window.electron.ipcRenderer.invoke('provider:list') as ProviderWithKeyInfo[];
|
|
const defaultId = await window.electron.ipcRenderer.invoke('provider:getDefault') as string | null;
|
|
|
|
set({
|
|
providers,
|
|
defaultProviderId: defaultId,
|
|
loading: false
|
|
});
|
|
} catch (error) {
|
|
set({ error: String(error), loading: false });
|
|
}
|
|
},
|
|
|
|
addProvider: async (config, apiKey) => {
|
|
try {
|
|
const fullConfig: ProviderConfig = {
|
|
...config,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
const result = await window.electron.ipcRenderer.invoke('provider:save', fullConfig, apiKey) as { success: boolean; error?: string };
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.error || 'Failed to save provider');
|
|
}
|
|
|
|
// Refresh the list
|
|
await get().fetchProviders();
|
|
} catch (error) {
|
|
console.error('Failed to add provider:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
updateProvider: async (providerId, updates, apiKey) => {
|
|
try {
|
|
const existing = get().providers.find((p) => p.id === providerId);
|
|
if (!existing) {
|
|
throw new Error('Provider not found');
|
|
}
|
|
|
|
const updatedConfig: ProviderConfig = {
|
|
...existing,
|
|
...updates,
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
const result = await window.electron.ipcRenderer.invoke('provider:save', updatedConfig, apiKey) as { success: boolean; error?: string };
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.error || 'Failed to update provider');
|
|
}
|
|
|
|
// Refresh the list
|
|
await get().fetchProviders();
|
|
} catch (error) {
|
|
console.error('Failed to update provider:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
deleteProvider: async (providerId) => {
|
|
try {
|
|
const result = await window.electron.ipcRenderer.invoke('provider:delete', providerId) as { success: boolean; error?: string };
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.error || 'Failed to delete provider');
|
|
}
|
|
|
|
// Refresh the list
|
|
await get().fetchProviders();
|
|
} catch (error) {
|
|
console.error('Failed to delete provider:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
setApiKey: async (providerId, apiKey) => {
|
|
try {
|
|
const result = await window.electron.ipcRenderer.invoke('provider:setApiKey', providerId, apiKey) as { success: boolean; error?: string };
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.error || 'Failed to set API key');
|
|
}
|
|
|
|
// Refresh the list
|
|
await get().fetchProviders();
|
|
} catch (error) {
|
|
console.error('Failed to set API key:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
deleteApiKey: async (providerId) => {
|
|
try {
|
|
const result = await window.electron.ipcRenderer.invoke('provider:deleteApiKey', providerId) as { success: boolean; error?: string };
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.error || 'Failed to delete API key');
|
|
}
|
|
|
|
// Refresh the list
|
|
await get().fetchProviders();
|
|
} catch (error) {
|
|
console.error('Failed to delete API key:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
setDefaultProvider: async (providerId) => {
|
|
try {
|
|
const result = await window.electron.ipcRenderer.invoke('provider:setDefault', providerId) as { success: boolean; error?: string };
|
|
|
|
if (!result.success) {
|
|
throw new Error(result.error || 'Failed to set default provider');
|
|
}
|
|
|
|
set({ defaultProviderId: providerId });
|
|
} catch (error) {
|
|
console.error('Failed to set default provider:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
validateApiKey: async (providerId, apiKey) => {
|
|
try {
|
|
const result = await window.electron.ipcRenderer.invoke('provider:validateKey', providerId, apiKey) as { valid: boolean; error?: string };
|
|
return result;
|
|
} catch (error) {
|
|
return { valid: false, error: String(error) };
|
|
}
|
|
},
|
|
|
|
getApiKey: async (providerId) => {
|
|
try {
|
|
return await window.electron.ipcRenderer.invoke('provider:getApiKey', providerId) as string | null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
}));
|