Skip to content

Commit 9f29fa5

Browse files
committed
Refactor: Migrate Settings from electron-store to Centralized Database #113
1 parent 9e0c74e commit 9f29fa5

24 files changed

Lines changed: 885 additions & 220 deletions

package-lock.json

Lines changed: 28 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"@google/genai": "^1.8.0",
3737
"@google/generative-ai": "^0.24.1",
3838
"axios": "^1.10.0",
39-
"better-sqlite3": "^9.4.3",
39+
"better-sqlite3": "^9.6.0",
4040
"cors": "^2.8.5",
4141
"dotenv": "^17.0.0",
4242
"electron-squirrel-startup": "^1.0.1",
@@ -49,6 +49,7 @@
4949
"keytar": "^7.9.0",
5050
"node-fetch": "^2.7.0",
5151
"openai": "^4.70.0",
52+
"portkey-ai": "^1.10.1",
5253
"react-hot-toast": "^2.5.2",
5354
"sharp": "^0.34.2",
5455
"validator": "^13.11.0",

pickleglass_web/backend_node/routes/user.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ router.post('/find-or-create', async (req, res) => {
4646

4747
router.post('/api-key', async (req, res) => {
4848
try {
49-
await ipcRequest(req, 'save-api-key', req.body.apiKey);
49+
const { apiKey, provider = 'openai' } = req.body;
50+
await ipcRequest(req, 'save-api-key', { apiKey, provider });
5051
res.json({ message: 'API key saved successfully' });
5152
} catch (error) {
5253
console.error('Failed to save API key via IPC:', error);

src/common/ai/factory.js

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,14 @@ const PROVIDERS = {
6666
'whisper': {
6767
name: 'Whisper (Local)',
6868
handler: () => {
69-
// Only load in main process
69+
// This needs to remain a function due to its conditional logic for renderer/main process
7070
if (typeof window === 'undefined') {
7171
return require("./providers/whisper");
7272
}
73-
// Return dummy for renderer
73+
// Return a dummy object for the renderer process
7474
return {
75+
validateApiKey: async () => ({ success: true }), // Mock validate for renderer
7576
createSTT: () => { throw new Error('Whisper STT is only available in main process'); },
76-
createLLM: () => { throw new Error('Whisper does not support LLM'); },
77-
createStreamingLLM: () => { throw new Error('Whisper does not support LLM'); }
7877
};
7978
},
8079
llmModels: [],
@@ -130,6 +129,32 @@ function createStreamingLLM(provider, opts) {
130129
return handler.createStreamingLLM(opts);
131130
}
132131

132+
function getProviderClass(providerId) {
133+
const providerConfig = PROVIDERS[providerId];
134+
if (!providerConfig) return null;
135+
136+
// Handle special cases for glass providers
137+
let actualProviderId = providerId;
138+
if (providerId === 'openai-glass') {
139+
actualProviderId = 'openai';
140+
}
141+
142+
// The handler function returns the module, from which we get the class.
143+
const module = providerConfig.handler();
144+
145+
// Map provider IDs to their actual exported class names
146+
const classNameMap = {
147+
'openai': 'OpenAIProvider',
148+
'anthropic': 'AnthropicProvider',
149+
'gemini': 'GeminiProvider',
150+
'ollama': 'OllamaProvider',
151+
'whisper': 'WhisperProvider'
152+
};
153+
154+
const className = classNameMap[actualProviderId];
155+
return className ? module[className] : null;
156+
}
157+
133158
function getAvailableProviders() {
134159
const stt = [];
135160
const llm = [];
@@ -145,5 +170,6 @@ module.exports = {
145170
createSTT,
146171
createLLM,
147172
createStreamingLLM,
173+
getProviderClass,
148174
getAvailableProviders,
149175
};

src/common/ai/providers/anthropic.js

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,38 @@
1-
const Anthropic = require("@anthropic-ai/sdk")
1+
const { Anthropic } = require("@anthropic-ai/sdk")
2+
3+
class AnthropicProvider {
4+
static async validateApiKey(key) {
5+
if (!key || typeof key !== 'string' || !key.startsWith('sk-ant-')) {
6+
return { success: false, error: 'Invalid Anthropic API key format.' };
7+
}
8+
9+
try {
10+
const response = await fetch("https://api.anthropic.com/v1/messages", {
11+
method: "POST",
12+
headers: {
13+
"Content-Type": "application/json",
14+
"x-api-key": key,
15+
"anthropic-version": "2023-06-01",
16+
},
17+
body: JSON.stringify({
18+
model: "claude-3-haiku-20240307",
19+
max_tokens: 1,
20+
messages: [{ role: "user", content: "Hi" }],
21+
}),
22+
});
23+
24+
if (response.ok || response.status === 400) { // 400 is a valid response for a bad request, not a bad key
25+
return { success: true };
26+
} else {
27+
const errorData = await response.json().catch(() => ({}));
28+
return { success: false, error: errorData.error?.message || `Validation failed with status: ${response.status}` };
29+
}
30+
} catch (error) {
31+
console.error(`[AnthropicProvider] Network error during key validation:`, error);
32+
return { success: false, error: 'A network error occurred during validation.' };
33+
}
34+
}
35+
}
236

337
/**
438
* Creates an Anthropic STT session
@@ -286,7 +320,8 @@ function createStreamingLLM({
286320
}
287321

288322
module.exports = {
289-
createSTT,
290-
createLLM,
291-
createStreamingLLM,
292-
}
323+
AnthropicProvider,
324+
createSTT,
325+
createLLM,
326+
createStreamingLLM
327+
};

src/common/ai/providers/gemini.js

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
const { GoogleGenerativeAI } = require("@google/generative-ai")
22
const { GoogleGenAI } = require("@google/genai")
33

4+
class GeminiProvider {
5+
static async validateApiKey(key) {
6+
if (!key || typeof key !== 'string') {
7+
return { success: false, error: 'Invalid Gemini API key format.' };
8+
}
9+
10+
try {
11+
const validationUrl = `https://generativelanguage.googleapis.com/v1beta/models?key=${key}`;
12+
const response = await fetch(validationUrl);
13+
14+
if (response.ok) {
15+
return { success: true };
16+
} else {
17+
const errorData = await response.json().catch(() => ({}));
18+
const message = errorData.error?.message || `Validation failed with status: ${response.status}`;
19+
return { success: false, error: message };
20+
}
21+
} catch (error) {
22+
console.error(`[GeminiProvider] Network error during key validation:`, error);
23+
return { success: false, error: 'A network error occurred during validation.' };
24+
}
25+
}
26+
}
27+
28+
429
/**
530
* Creates a Gemini STT session
631
* @param {object} opts - Configuration options
@@ -296,7 +321,8 @@ function createStreamingLLM({ apiKey, model = "gemini-2.5-flash", temperature =
296321
}
297322

298323
module.exports = {
299-
createSTT,
300-
createLLM,
301-
createStreamingLLM,
302-
}
324+
GeminiProvider,
325+
createSTT,
326+
createLLM,
327+
createStreamingLLM
328+
};

src/common/ai/providers/ollama.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
const http = require('http');
22
const fetch = require('node-fetch');
33

4+
class OllamaProvider {
5+
static async validateApiKey() {
6+
try {
7+
const response = await fetch('http://localhost:11434/api/tags');
8+
if (response.ok) {
9+
return { success: true };
10+
} else {
11+
return { success: false, error: 'Ollama service is not running. Please start Ollama first.' };
12+
}
13+
} catch (error) {
14+
return { success: false, error: 'Cannot connect to Ollama. Please ensure Ollama is installed and running.' };
15+
}
16+
}
17+
}
18+
19+
420
function convertMessagesToOllamaFormat(messages) {
521
return messages.map(msg => {
622
if (Array.isArray(msg.content)) {
@@ -237,6 +253,8 @@ function createStreamingLLM({
237253
}
238254

239255
module.exports = {
256+
OllamaProvider,
240257
createLLM,
241-
createStreamingLLM
258+
createStreamingLLM,
259+
convertMessagesToOllamaFormat
242260
};

src/common/ai/providers/openai.js

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
const OpenAI = require('openai');
22
const WebSocket = require('ws');
3+
const { Portkey } = require('portkey-ai');
4+
const { Readable } = require('stream');
5+
const { getProviderForModel } = require('../factory.js');
6+
7+
8+
class OpenAIProvider {
9+
static async validateApiKey(key) {
10+
if (!key || typeof key !== 'string' || !key.startsWith('sk-')) {
11+
return { success: false, error: 'Invalid OpenAI API key format.' };
12+
}
13+
14+
try {
15+
const response = await fetch('https://api.openai.com/v1/models', {
16+
headers: { 'Authorization': `Bearer ${key}` }
17+
});
18+
19+
if (response.ok) {
20+
return { success: true };
21+
} else {
22+
const errorData = await response.json().catch(() => ({}));
23+
const message = errorData.error?.message || `Validation failed with status: ${response.status}`;
24+
return { success: false, error: message };
25+
}
26+
} catch (error) {
27+
console.error(`[OpenAIProvider] Network error during key validation:`, error);
28+
return { success: false, error: 'A network error occurred during validation.' };
29+
}
30+
}
31+
}
32+
333

434
/**
535
* Creates an OpenAI STT session
@@ -206,7 +236,7 @@ function createLLM({ apiKey, model = 'gpt-4.1', temperature = 0.7, maxTokens = 2
206236
};
207237
}
208238

209-
/**
239+
/**
210240
* Creates an OpenAI streaming LLM instance
211241
* @param {object} opts - Configuration options
212242
* @param {string} opts.apiKey - OpenAI API key
@@ -257,7 +287,8 @@ function createStreamingLLM({ apiKey, model = 'gpt-4.1', temperature = 0.7, maxT
257287
}
258288

259289
module.exports = {
260-
createSTT,
261-
createLLM,
262-
createStreamingLLM
290+
OpenAIProvider,
291+
createSTT,
292+
createLLM,
293+
createStreamingLLM
263294
};

src/common/ai/providers/whisper.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ class WhisperSTTSession extends EventEmitter {
173173
}
174174

175175
class WhisperProvider {
176+
static async validateApiKey() {
177+
// Whisper is a local service, no API key validation needed.
178+
return { success: true };
179+
}
180+
176181
constructor() {
177182
this.whisperService = null;
178183
}
@@ -224,8 +229,12 @@ class WhisperProvider {
224229
}
225230

226231
async createStreamingLLM() {
227-
throw new Error('Whisper provider does not support streaming LLM functionality');
232+
console.warn('[WhisperProvider] Streaming LLM is not supported by Whisper.');
233+
throw new Error('Whisper does not support LLM.');
228234
}
229235
}
230236

231-
module.exports = new WhisperProvider();
237+
module.exports = {
238+
WhisperProvider,
239+
WhisperSTTSession
240+
};

0 commit comments

Comments
 (0)