-
Notifications
You must be signed in to change notification settings - Fork 615
Expand file tree
/
Copy pathConfigHelper.ts
More file actions
340 lines (303 loc) · 10.9 KB
/
ConfigHelper.ts
File metadata and controls
340 lines (303 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
// ConfigHelper.ts
import fs from "node:fs"
import path from "node:path"
import { app } from "electron"
import { EventEmitter } from "events"
import { OpenAI } from "openai"
interface Config {
apiKey: string;
apiProvider: "openai" | "gemini"; // Added provider selection
extractionModel: string;
solutionModel: string;
debuggingModel: string;
language: string;
opacity: number;
}
export class ConfigHelper extends EventEmitter {
private configPath: string;
private defaultConfig: Config = {
apiKey: "",
apiProvider: "gemini", // Default to Gemini
extractionModel: "gemini-2.0-flash", // Default to Flash for faster responses
solutionModel: "gemini-2.0-flash",
debuggingModel: "gemini-2.0-flash",
language: "python",
opacity: 1.0
};
constructor() {
super();
// Use the app's user data directory to store the config
try {
this.configPath = path.join(app.getPath('userData'), 'config.json');
console.log('Config path:', this.configPath);
} catch (err) {
console.warn('Could not access user data path, using fallback');
this.configPath = path.join(process.cwd(), 'config.json');
}
// Ensure the initial config file exists
this.ensureConfigExists();
}
/**
* Ensure config file exists
*/
private ensureConfigExists(): void {
try {
if (!fs.existsSync(this.configPath)) {
this.saveConfig(this.defaultConfig);
}
} catch (err) {
console.error("Error ensuring config exists:", err);
}
}
/**
* Validate and sanitize model selection to ensure only allowed models are used
*/
private sanitizeModelSelection(model: string, provider: "openai" | "gemini"): string {
if (provider === "openai") {
// Only allow gpt-4o and gpt-4o-mini for OpenAI
const allowedModels = ['gpt-4o', 'gpt-4o-mini'];
if (!allowedModels.includes(model)) {
console.warn(`Invalid OpenAI model specified: ${model}. Using default model: gpt-4o`);
return 'gpt-4o';
}
return model;
} else {
// Only allow gemini-1.5-pro and gemini-2.0-flash for Gemini
const allowedModels = ['gemini-1.5-pro', 'gemini-2.0-flash'];
if (!allowedModels.includes(model)) {
console.warn(`Invalid Gemini model specified: ${model}. Using default model: gemini-2.0-flash`);
return 'gemini-2.0-flash'; // Changed default to flash
}
return model;
}
}
public loadConfig(): Config {
try {
if (fs.existsSync(this.configPath)) {
const configData = fs.readFileSync(this.configPath, 'utf8');
const config = JSON.parse(configData);
// Ensure apiProvider is a valid value
if (config.apiProvider !== "openai" && config.apiProvider !== "gemini") {
config.apiProvider = "gemini"; // Default to Gemini if invalid
}
// Sanitize model selections to ensure only allowed models are used
if (config.extractionModel) {
config.extractionModel = this.sanitizeModelSelection(config.extractionModel, config.apiProvider);
}
if (config.solutionModel) {
config.solutionModel = this.sanitizeModelSelection(config.solutionModel, config.apiProvider);
}
if (config.debuggingModel) {
config.debuggingModel = this.sanitizeModelSelection(config.debuggingModel, config.apiProvider);
}
return {
...this.defaultConfig,
...config
};
}
// If no config exists, create a default one
this.saveConfig(this.defaultConfig);
return this.defaultConfig;
} catch (err) {
console.error("Error loading config:", err);
return this.defaultConfig;
}
}
/**
* Save configuration to disk
*/
public saveConfig(config: Config): void {
try {
// Ensure the directory exists
const configDir = path.dirname(this.configPath);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
// Write the config file
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
} catch (err) {
console.error("Error saving config:", err);
}
}
/**
* Update specific configuration values
*/
public updateConfig(updates: Partial<Config>): Config {
try {
const currentConfig = this.loadConfig();
let provider = updates.apiProvider || currentConfig.apiProvider;
// Auto-detect provider based on API key format if a new key is provided
if (updates.apiKey && !updates.apiProvider) {
// If API key starts with "sk-", it's likely an OpenAI key
if (updates.apiKey.trim().startsWith('sk-')) {
provider = "openai";
console.log("Auto-detected OpenAI API key format");
} else {
provider = "gemini";
console.log("Using Gemini API key format (default)");
}
// Update the provider in the updates object
updates.apiProvider = provider;
}
// If provider is changing, reset models to the default for that provider
if (updates.apiProvider && updates.apiProvider !== currentConfig.apiProvider) {
if (updates.apiProvider === "openai") {
updates.extractionModel = "gpt-4o";
updates.solutionModel = "gpt-4o";
updates.debuggingModel = "gpt-4o";
} else {
updates.extractionModel = "gemini-2.0-flash";
updates.solutionModel = "gemini-2.0-flash";
updates.debuggingModel = "gemini-2.0-flash";
}
}
// Sanitize model selections in the updates
if (updates.extractionModel) {
updates.extractionModel = this.sanitizeModelSelection(updates.extractionModel, provider);
}
if (updates.solutionModel) {
updates.solutionModel = this.sanitizeModelSelection(updates.solutionModel, provider);
}
if (updates.debuggingModel) {
updates.debuggingModel = this.sanitizeModelSelection(updates.debuggingModel, provider);
}
const newConfig = { ...currentConfig, ...updates };
this.saveConfig(newConfig);
// Only emit update event for changes other than opacity
// This prevents re-initializing the AI client when only opacity changes
if (updates.apiKey !== undefined || updates.apiProvider !== undefined ||
updates.extractionModel !== undefined || updates.solutionModel !== undefined ||
updates.debuggingModel !== undefined || updates.language !== undefined) {
this.emit('config-updated', newConfig);
}
return newConfig;
} catch (error) {
console.error('Error updating config:', error);
return this.defaultConfig;
}
}
/**
* Check if the API key is configured
*/
public hasApiKey(): boolean {
const config = this.loadConfig();
return !!config.apiKey && config.apiKey.trim().length > 0;
}
/**
* Validate the API key format
*/
public isValidApiKeyFormat(apiKey: string, provider?: "openai" | "gemini"): boolean {
// If provider is not specified, attempt to auto-detect
if (!provider) {
if (apiKey.trim().startsWith('sk-')) {
provider = "openai";
} else {
provider = "gemini";
}
}
if (provider === "openai") {
// Basic format validation for OpenAI API keys
return /^sk-[a-zA-Z0-9]{32,}$/.test(apiKey.trim());
} else if (provider === "gemini") {
// Basic format validation for Gemini API keys (usually alphanumeric with no specific prefix)
return apiKey.trim().length >= 10; // Assuming Gemini keys are at least 10 chars
}
return false;
}
/**
* Get the stored opacity value
*/
public getOpacity(): number {
const config = this.loadConfig();
return config.opacity !== undefined ? config.opacity : 1.0;
}
/**
* Set the window opacity value
*/
public setOpacity(opacity: number): void {
// Ensure opacity is between 0.1 and 1.0
const validOpacity = Math.min(1.0, Math.max(0.1, opacity));
this.updateConfig({ opacity: validOpacity });
}
/**
* Get the preferred programming language
*/
public getLanguage(): string {
const config = this.loadConfig();
return config.language || "python";
}
/**
* Set the preferred programming language
*/
public setLanguage(language: string): void {
this.updateConfig({ language });
}
/**
* Test API key with the selected provider
*/
public async testApiKey(apiKey: string, provider?: "openai" | "gemini"): Promise<{valid: boolean, error?: string}> {
// Auto-detect provider based on key format if not specified
if (!provider) {
if (apiKey.trim().startsWith('sk-')) {
provider = "openai";
console.log("Auto-detected OpenAI API key format for testing");
} else {
provider = "gemini";
console.log("Using Gemini API key format for testing (default)");
}
}
if (provider === "openai") {
return this.testOpenAIKey(apiKey);
} else if (provider === "gemini") {
return this.testGeminiKey(apiKey);
}
return { valid: false, error: "Unknown API provider" };
}
/**
* Test OpenAI API key
*/
private async testOpenAIKey(apiKey: string): Promise<{valid: boolean, error?: string}> {
try {
const openai = new OpenAI({ apiKey });
// Make a simple API call to test the key
await openai.models.list();
return { valid: true };
} catch (error: any) {
console.error('OpenAI API key test failed:', error);
// Return error without showing dialog
if (error.status === 401) {
return { valid: false, error: 'Invalid API key' };
} else if (error.status === 429) {
return { valid: false, error: 'Rate limit exceeded' };
} else if (error.status === 500) {
return { valid: false, error: 'OpenAI server error' };
} else {
return { valid: false, error: error.message || 'Unknown error' };
}
}
}
/**
* Test Gemini API key
* Note: This is a simplified implementation since we don't have the actual Gemini client
*/
private async testGeminiKey(apiKey: string): Promise<{valid: boolean, error?: string}> {
try {
// For now, we'll just do a basic check to ensure the key exists and has valid format
// In production, you would connect to the Gemini API and validate the key
if (apiKey && apiKey.trim().length >= 20) {
// Here you would actually validate the key with a Gemini API call
return { valid: true };
}
return { valid: false, error: 'Invalid Gemini API key format.' };
} catch (error: any) {
console.error('Gemini API key test failed:', error);
let errorMessage = 'Unknown error validating Gemini API key';
if (error.message) {
errorMessage = `Error: ${error.message}`;
}
return { valid: false, error: errorMessage };
}
}
}
// Export a singleton instance
export const configHelper = new ConfigHelper();