Refactor: Migrate Settings from electron-store to Centralized Database #113
This commit is contained in:
parent
9e0c74eed4
commit
9f29fa5873
29
package-lock.json
generated
29
package-lock.json
generated
@ -14,7 +14,7 @@
|
|||||||
"@google/genai": "^1.8.0",
|
"@google/genai": "^1.8.0",
|
||||||
"@google/generative-ai": "^0.24.1",
|
"@google/generative-ai": "^0.24.1",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
"better-sqlite3": "^9.4.3",
|
"better-sqlite3": "^9.6.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.0.0",
|
"dotenv": "^17.0.0",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
@ -27,6 +27,7 @@
|
|||||||
"keytar": "^7.9.0",
|
"keytar": "^7.9.0",
|
||||||
"node-fetch": "^2.7.0",
|
"node-fetch": "^2.7.0",
|
||||||
"openai": "^4.70.0",
|
"openai": "^4.70.0",
|
||||||
|
"portkey-ai": "^1.10.1",
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
"sharp": "^0.34.2",
|
"sharp": "^0.34.2",
|
||||||
"validator": "^13.11.0",
|
"validator": "^13.11.0",
|
||||||
@ -2283,6 +2284,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/better-sqlite3": {
|
"node_modules/better-sqlite3": {
|
||||||
"version": "9.6.0",
|
"version": "9.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.6.0.tgz",
|
||||||
|
"integrity": "sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -5922,6 +5925,30 @@
|
|||||||
"node": ">=10.4.0"
|
"node": ">=10.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/portkey-ai": {
|
||||||
|
"version": "1.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/portkey-ai/-/portkey-ai-1.10.1.tgz",
|
||||||
|
"integrity": "sha512-mRGDxm4xBMexYlk/bS8i+G5C/Ww+KaXcKlHtzzsmh0X4Awd1bPBGq5dlUmCrHGgN/umLpphxcOcLHsDa9NbjrQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agentkeepalive": "^4.6.0",
|
||||||
|
"dotenv": "^16.5.0",
|
||||||
|
"openai": "4.104.0",
|
||||||
|
"ws": "^8.18.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/portkey-ai/node_modules/dotenv": {
|
||||||
|
"version": "16.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
|
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postject": {
|
"node_modules/postject": {
|
||||||
"version": "1.0.0-alpha.6",
|
"version": "1.0.0-alpha.6",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
"@google/genai": "^1.8.0",
|
"@google/genai": "^1.8.0",
|
||||||
"@google/generative-ai": "^0.24.1",
|
"@google/generative-ai": "^0.24.1",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
"better-sqlite3": "^9.4.3",
|
"better-sqlite3": "^9.6.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.0.0",
|
"dotenv": "^17.0.0",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
@ -49,6 +49,7 @@
|
|||||||
"keytar": "^7.9.0",
|
"keytar": "^7.9.0",
|
||||||
"node-fetch": "^2.7.0",
|
"node-fetch": "^2.7.0",
|
||||||
"openai": "^4.70.0",
|
"openai": "^4.70.0",
|
||||||
|
"portkey-ai": "^1.10.1",
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
"sharp": "^0.34.2",
|
"sharp": "^0.34.2",
|
||||||
"validator": "^13.11.0",
|
"validator": "^13.11.0",
|
||||||
|
@ -46,7 +46,8 @@ router.post('/find-or-create', async (req, res) => {
|
|||||||
|
|
||||||
router.post('/api-key', async (req, res) => {
|
router.post('/api-key', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await ipcRequest(req, 'save-api-key', req.body.apiKey);
|
const { apiKey, provider = 'openai' } = req.body;
|
||||||
|
await ipcRequest(req, 'save-api-key', { apiKey, provider });
|
||||||
res.json({ message: 'API key saved successfully' });
|
res.json({ message: 'API key saved successfully' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save API key via IPC:', error);
|
console.error('Failed to save API key via IPC:', error);
|
||||||
|
@ -66,15 +66,14 @@ const PROVIDERS = {
|
|||||||
'whisper': {
|
'whisper': {
|
||||||
name: 'Whisper (Local)',
|
name: 'Whisper (Local)',
|
||||||
handler: () => {
|
handler: () => {
|
||||||
// Only load in main process
|
// This needs to remain a function due to its conditional logic for renderer/main process
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return require("./providers/whisper");
|
return require("./providers/whisper");
|
||||||
}
|
}
|
||||||
// Return dummy for renderer
|
// Return a dummy object for the renderer process
|
||||||
return {
|
return {
|
||||||
|
validateApiKey: async () => ({ success: true }), // Mock validate for renderer
|
||||||
createSTT: () => { throw new Error('Whisper STT is only available in main process'); },
|
createSTT: () => { throw new Error('Whisper STT is only available in main process'); },
|
||||||
createLLM: () => { throw new Error('Whisper does not support LLM'); },
|
|
||||||
createStreamingLLM: () => { throw new Error('Whisper does not support LLM'); }
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
llmModels: [],
|
llmModels: [],
|
||||||
@ -130,6 +129,32 @@ function createStreamingLLM(provider, opts) {
|
|||||||
return handler.createStreamingLLM(opts);
|
return handler.createStreamingLLM(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProviderClass(providerId) {
|
||||||
|
const providerConfig = PROVIDERS[providerId];
|
||||||
|
if (!providerConfig) return null;
|
||||||
|
|
||||||
|
// Handle special cases for glass providers
|
||||||
|
let actualProviderId = providerId;
|
||||||
|
if (providerId === 'openai-glass') {
|
||||||
|
actualProviderId = 'openai';
|
||||||
|
}
|
||||||
|
|
||||||
|
// The handler function returns the module, from which we get the class.
|
||||||
|
const module = providerConfig.handler();
|
||||||
|
|
||||||
|
// Map provider IDs to their actual exported class names
|
||||||
|
const classNameMap = {
|
||||||
|
'openai': 'OpenAIProvider',
|
||||||
|
'anthropic': 'AnthropicProvider',
|
||||||
|
'gemini': 'GeminiProvider',
|
||||||
|
'ollama': 'OllamaProvider',
|
||||||
|
'whisper': 'WhisperProvider'
|
||||||
|
};
|
||||||
|
|
||||||
|
const className = classNameMap[actualProviderId];
|
||||||
|
return className ? module[className] : null;
|
||||||
|
}
|
||||||
|
|
||||||
function getAvailableProviders() {
|
function getAvailableProviders() {
|
||||||
const stt = [];
|
const stt = [];
|
||||||
const llm = [];
|
const llm = [];
|
||||||
@ -145,5 +170,6 @@ module.exports = {
|
|||||||
createSTT,
|
createSTT,
|
||||||
createLLM,
|
createLLM,
|
||||||
createStreamingLLM,
|
createStreamingLLM,
|
||||||
|
getProviderClass,
|
||||||
getAvailableProviders,
|
getAvailableProviders,
|
||||||
};
|
};
|
@ -1,4 +1,38 @@
|
|||||||
const Anthropic = require("@anthropic-ai/sdk")
|
const { Anthropic } = require("@anthropic-ai/sdk")
|
||||||
|
|
||||||
|
class AnthropicProvider {
|
||||||
|
static async validateApiKey(key) {
|
||||||
|
if (!key || typeof key !== 'string' || !key.startsWith('sk-ant-')) {
|
||||||
|
return { success: false, error: 'Invalid Anthropic API key format.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": key,
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: "claude-3-haiku-20240307",
|
||||||
|
max_tokens: 1,
|
||||||
|
messages: [{ role: "user", content: "Hi" }],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok || response.status === 400) { // 400 is a valid response for a bad request, not a bad key
|
||||||
|
return { success: true };
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
return { success: false, error: errorData.error?.message || `Validation failed with status: ${response.status}` };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[AnthropicProvider] Network error during key validation:`, error);
|
||||||
|
return { success: false, error: 'A network error occurred during validation.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an Anthropic STT session
|
* Creates an Anthropic STT session
|
||||||
@ -286,7 +320,8 @@ function createStreamingLLM({
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createSTT,
|
AnthropicProvider,
|
||||||
createLLM,
|
createSTT,
|
||||||
createStreamingLLM,
|
createLLM,
|
||||||
}
|
createStreamingLLM
|
||||||
|
};
|
||||||
|
@ -1,6 +1,31 @@
|
|||||||
const { GoogleGenerativeAI } = require("@google/generative-ai")
|
const { GoogleGenerativeAI } = require("@google/generative-ai")
|
||||||
const { GoogleGenAI } = require("@google/genai")
|
const { GoogleGenAI } = require("@google/genai")
|
||||||
|
|
||||||
|
class GeminiProvider {
|
||||||
|
static async validateApiKey(key) {
|
||||||
|
if (!key || typeof key !== 'string') {
|
||||||
|
return { success: false, error: 'Invalid Gemini API key format.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const validationUrl = `https://generativelanguage.googleapis.com/v1beta/models?key=${key}`;
|
||||||
|
const response = await fetch(validationUrl);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return { success: true };
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
const message = errorData.error?.message || `Validation failed with status: ${response.status}`;
|
||||||
|
return { success: false, error: message };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[GeminiProvider] Network error during key validation:`, error);
|
||||||
|
return { success: false, error: 'A network error occurred during validation.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Gemini STT session
|
* Creates a Gemini STT session
|
||||||
* @param {object} opts - Configuration options
|
* @param {object} opts - Configuration options
|
||||||
@ -296,7 +321,8 @@ function createStreamingLLM({ apiKey, model = "gemini-2.5-flash", temperature =
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createSTT,
|
GeminiProvider,
|
||||||
createLLM,
|
createSTT,
|
||||||
createStreamingLLM,
|
createLLM,
|
||||||
}
|
createStreamingLLM
|
||||||
|
};
|
||||||
|
@ -1,6 +1,22 @@
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
class OllamaProvider {
|
||||||
|
static async validateApiKey() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:11434/api/tags');
|
||||||
|
if (response.ok) {
|
||||||
|
return { success: true };
|
||||||
|
} else {
|
||||||
|
return { success: false, error: 'Ollama service is not running. Please start Ollama first.' };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: 'Cannot connect to Ollama. Please ensure Ollama is installed and running.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function convertMessagesToOllamaFormat(messages) {
|
function convertMessagesToOllamaFormat(messages) {
|
||||||
return messages.map(msg => {
|
return messages.map(msg => {
|
||||||
if (Array.isArray(msg.content)) {
|
if (Array.isArray(msg.content)) {
|
||||||
@ -237,6 +253,8 @@ function createStreamingLLM({
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
OllamaProvider,
|
||||||
createLLM,
|
createLLM,
|
||||||
createStreamingLLM
|
createStreamingLLM,
|
||||||
|
convertMessagesToOllamaFormat
|
||||||
};
|
};
|
@ -1,5 +1,35 @@
|
|||||||
const OpenAI = require('openai');
|
const OpenAI = require('openai');
|
||||||
const WebSocket = require('ws');
|
const WebSocket = require('ws');
|
||||||
|
const { Portkey } = require('portkey-ai');
|
||||||
|
const { Readable } = require('stream');
|
||||||
|
const { getProviderForModel } = require('../factory.js');
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAIProvider {
|
||||||
|
static async validateApiKey(key) {
|
||||||
|
if (!key || typeof key !== 'string' || !key.startsWith('sk-')) {
|
||||||
|
return { success: false, error: 'Invalid OpenAI API key format.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.openai.com/v1/models', {
|
||||||
|
headers: { 'Authorization': `Bearer ${key}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return { success: true };
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
const message = errorData.error?.message || `Validation failed with status: ${response.status}`;
|
||||||
|
return { success: false, error: message };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[OpenAIProvider] Network error during key validation:`, error);
|
||||||
|
return { success: false, error: 'A network error occurred during validation.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an OpenAI STT session
|
* Creates an OpenAI STT session
|
||||||
@ -206,7 +236,7 @@ function createLLM({ apiKey, model = 'gpt-4.1', temperature = 0.7, maxTokens = 2
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an OpenAI streaming LLM instance
|
* Creates an OpenAI streaming LLM instance
|
||||||
* @param {object} opts - Configuration options
|
* @param {object} opts - Configuration options
|
||||||
* @param {string} opts.apiKey - OpenAI API key
|
* @param {string} opts.apiKey - OpenAI API key
|
||||||
@ -257,7 +287,8 @@ function createStreamingLLM({ apiKey, model = 'gpt-4.1', temperature = 0.7, maxT
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createSTT,
|
OpenAIProvider,
|
||||||
createLLM,
|
createSTT,
|
||||||
createStreamingLLM
|
createLLM,
|
||||||
|
createStreamingLLM
|
||||||
};
|
};
|
@ -173,6 +173,11 @@ class WhisperSTTSession extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class WhisperProvider {
|
class WhisperProvider {
|
||||||
|
static async validateApiKey() {
|
||||||
|
// Whisper is a local service, no API key validation needed.
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.whisperService = null;
|
this.whisperService = null;
|
||||||
}
|
}
|
||||||
@ -224,8 +229,12 @@ class WhisperProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createStreamingLLM() {
|
async createStreamingLLM() {
|
||||||
throw new Error('Whisper provider does not support streaming LLM functionality');
|
console.warn('[WhisperProvider] Streaming LLM is not supported by Whisper.');
|
||||||
|
throw new Error('Whisper does not support LLM.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new WhisperProvider();
|
module.exports = {
|
||||||
|
WhisperProvider,
|
||||||
|
WhisperSTTSession
|
||||||
|
};
|
@ -5,8 +5,6 @@ const LATEST_SCHEMA = {
|
|||||||
{ name: 'display_name', type: 'TEXT NOT NULL' },
|
{ name: 'display_name', type: 'TEXT NOT NULL' },
|
||||||
{ name: 'email', type: 'TEXT NOT NULL' },
|
{ name: 'email', type: 'TEXT NOT NULL' },
|
||||||
{ name: 'created_at', type: 'INTEGER' },
|
{ name: 'created_at', type: 'INTEGER' },
|
||||||
{ name: 'api_key', type: 'TEXT' },
|
|
||||||
{ name: 'provider', type: 'TEXT DEFAULT \'openai\'' },
|
|
||||||
{ name: 'auto_update_enabled', type: 'INTEGER DEFAULT 1' },
|
{ name: 'auto_update_enabled', type: 'INTEGER DEFAULT 1' },
|
||||||
{ name: 'has_migrated_to_firebase', type: 'INTEGER DEFAULT 0' }
|
{ name: 'has_migrated_to_firebase', type: 'INTEGER DEFAULT 0' }
|
||||||
]
|
]
|
||||||
@ -90,6 +88,28 @@ const LATEST_SCHEMA = {
|
|||||||
{ name: 'installed', type: 'INTEGER DEFAULT 0' },
|
{ name: 'installed', type: 'INTEGER DEFAULT 0' },
|
||||||
{ name: 'installing', type: 'INTEGER DEFAULT 0' }
|
{ name: 'installing', type: 'INTEGER DEFAULT 0' }
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
provider_settings: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'uid', type: 'TEXT NOT NULL' },
|
||||||
|
{ name: 'provider', type: 'TEXT NOT NULL' },
|
||||||
|
{ name: 'api_key', type: 'TEXT' },
|
||||||
|
{ name: 'selected_llm_model', type: 'TEXT' },
|
||||||
|
{ name: 'selected_stt_model', type: 'TEXT' },
|
||||||
|
{ name: 'created_at', type: 'INTEGER' },
|
||||||
|
{ name: 'updated_at', type: 'INTEGER' }
|
||||||
|
],
|
||||||
|
constraints: ['PRIMARY KEY (uid, provider)']
|
||||||
|
},
|
||||||
|
user_model_selections: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'uid', type: 'TEXT PRIMARY KEY' },
|
||||||
|
{ name: 'selected_llm_provider', type: 'TEXT' },
|
||||||
|
{ name: 'selected_llm_model', type: 'TEXT' },
|
||||||
|
{ name: 'selected_stt_provider', type: 'TEXT' },
|
||||||
|
{ name: 'selected_stt_model', type: 'TEXT' },
|
||||||
|
{ name: 'updated_at', type: 'INTEGER' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
const { collection, doc, getDoc, getDocs, setDoc, deleteDoc, query, where } = require('firebase/firestore');
|
||||||
|
const { getFirestoreInstance: getFirestore } = require('../../services/firebaseClient');
|
||||||
|
const { createEncryptedConverter } = require('../firestoreConverter');
|
||||||
|
|
||||||
|
// Create encrypted converter for provider settings
|
||||||
|
const providerSettingsConverter = createEncryptedConverter([
|
||||||
|
'api_key', // Encrypt API keys
|
||||||
|
'selected_llm_model', // Encrypt model selections for privacy
|
||||||
|
'selected_stt_model'
|
||||||
|
]);
|
||||||
|
|
||||||
|
function providerSettingsCol() {
|
||||||
|
const db = getFirestore();
|
||||||
|
return collection(db, 'provider_settings').withConverter(providerSettingsConverter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getByProvider(uid, provider) {
|
||||||
|
try {
|
||||||
|
const docRef = doc(providerSettingsCol(), `${uid}_${provider}`);
|
||||||
|
const docSnap = await getDoc(docRef);
|
||||||
|
return docSnap.exists() ? { id: docSnap.id, ...docSnap.data() } : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ProviderSettings Firebase] Error getting provider settings:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllByUid(uid) {
|
||||||
|
try {
|
||||||
|
const q = query(providerSettingsCol(), where('uid', '==', uid));
|
||||||
|
const querySnapshot = await getDocs(q);
|
||||||
|
return querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ProviderSettings Firebase] Error getting all provider settings:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upsert(uid, provider, settings) {
|
||||||
|
try {
|
||||||
|
const docRef = doc(providerSettingsCol(), `${uid}_${provider}`);
|
||||||
|
await setDoc(docRef, settings, { merge: true });
|
||||||
|
return { changes: 1 };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ProviderSettings Firebase] Error upserting provider settings:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(uid, provider) {
|
||||||
|
try {
|
||||||
|
const docRef = doc(providerSettingsCol(), `${uid}_${provider}`);
|
||||||
|
await deleteDoc(docRef);
|
||||||
|
return { changes: 1 };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ProviderSettings Firebase] Error removing provider settings:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeAllByUid(uid) {
|
||||||
|
try {
|
||||||
|
const settings = await getAllByUid(uid);
|
||||||
|
const deletePromises = settings.map(setting => {
|
||||||
|
const docRef = doc(providerSettingsCol(), setting.id);
|
||||||
|
return deleteDoc(docRef);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(deletePromises);
|
||||||
|
return { changes: settings.length };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ProviderSettings Firebase] Error removing all provider settings:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getByProvider,
|
||||||
|
getAllByUid,
|
||||||
|
upsert,
|
||||||
|
remove,
|
||||||
|
removeAllByUid
|
||||||
|
};
|
65
src/common/repositories/providerSettings/index.js
Normal file
65
src/common/repositories/providerSettings/index.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
const firebaseRepository = require('./firebase.repository');
|
||||||
|
const sqliteRepository = require('./sqlite.repository');
|
||||||
|
|
||||||
|
let authService = null;
|
||||||
|
|
||||||
|
function setAuthService(service) {
|
||||||
|
authService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBaseRepository() {
|
||||||
|
if (!authService) {
|
||||||
|
throw new Error('AuthService not set for providerSettings repository');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = authService.getCurrentUser();
|
||||||
|
return user.isLoggedIn ? firebaseRepository : sqliteRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerSettingsRepositoryAdapter = {
|
||||||
|
// Core CRUD operations
|
||||||
|
async getByProvider(provider) {
|
||||||
|
const repo = getBaseRepository();
|
||||||
|
const uid = authService.getCurrentUserId();
|
||||||
|
return await repo.getByProvider(uid, provider);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getAllByUid() {
|
||||||
|
const repo = getBaseRepository();
|
||||||
|
const uid = authService.getCurrentUserId();
|
||||||
|
return await repo.getAllByUid(uid);
|
||||||
|
},
|
||||||
|
|
||||||
|
async upsert(provider, settings) {
|
||||||
|
const repo = getBaseRepository();
|
||||||
|
const uid = authService.getCurrentUserId();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
const settingsWithMeta = {
|
||||||
|
...settings,
|
||||||
|
uid,
|
||||||
|
provider,
|
||||||
|
updated_at: now,
|
||||||
|
created_at: settings.created_at || now
|
||||||
|
};
|
||||||
|
|
||||||
|
return await repo.upsert(uid, provider, settingsWithMeta);
|
||||||
|
},
|
||||||
|
|
||||||
|
async remove(provider) {
|
||||||
|
const repo = getBaseRepository();
|
||||||
|
const uid = authService.getCurrentUserId();
|
||||||
|
return await repo.remove(uid, provider);
|
||||||
|
},
|
||||||
|
|
||||||
|
async removeAllByUid() {
|
||||||
|
const repo = getBaseRepository();
|
||||||
|
const uid = authService.getCurrentUserId();
|
||||||
|
return await repo.removeAllByUid(uid);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
...providerSettingsRepositoryAdapter,
|
||||||
|
setAuthService
|
||||||
|
};
|
@ -0,0 +1,62 @@
|
|||||||
|
const sqliteClient = require('../../services/sqliteClient');
|
||||||
|
|
||||||
|
function getByProvider(uid, provider) {
|
||||||
|
const db = sqliteClient.getDb();
|
||||||
|
const stmt = db.prepare('SELECT * FROM provider_settings WHERE uid = ? AND provider = ?');
|
||||||
|
return stmt.get(uid, provider) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllByUid(uid) {
|
||||||
|
const db = sqliteClient.getDb();
|
||||||
|
const stmt = db.prepare('SELECT * FROM provider_settings WHERE uid = ? ORDER BY provider');
|
||||||
|
return stmt.all(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsert(uid, provider, settings) {
|
||||||
|
const db = sqliteClient.getDb();
|
||||||
|
|
||||||
|
// Use SQLite's UPSERT syntax (INSERT ... ON CONFLICT ... DO UPDATE)
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
INSERT INTO provider_settings (uid, provider, api_key, selected_llm_model, selected_stt_model, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(uid, provider) DO UPDATE SET
|
||||||
|
api_key = excluded.api_key,
|
||||||
|
selected_llm_model = excluded.selected_llm_model,
|
||||||
|
selected_stt_model = excluded.selected_stt_model,
|
||||||
|
updated_at = excluded.updated_at
|
||||||
|
`);
|
||||||
|
|
||||||
|
const result = stmt.run(
|
||||||
|
uid,
|
||||||
|
provider,
|
||||||
|
settings.api_key || null,
|
||||||
|
settings.selected_llm_model || null,
|
||||||
|
settings.selected_stt_model || null,
|
||||||
|
settings.created_at || Date.now(),
|
||||||
|
settings.updated_at
|
||||||
|
);
|
||||||
|
|
||||||
|
return { changes: result.changes };
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(uid, provider) {
|
||||||
|
const db = sqliteClient.getDb();
|
||||||
|
const stmt = db.prepare('DELETE FROM provider_settings WHERE uid = ? AND provider = ?');
|
||||||
|
const result = stmt.run(uid, provider);
|
||||||
|
return { changes: result.changes };
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAllByUid(uid) {
|
||||||
|
const db = sqliteClient.getDb();
|
||||||
|
const stmt = db.prepare('DELETE FROM provider_settings WHERE uid = ?');
|
||||||
|
const result = stmt.run(uid);
|
||||||
|
return { changes: result.changes };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getByProvider,
|
||||||
|
getAllByUid,
|
||||||
|
upsert,
|
||||||
|
remove,
|
||||||
|
removeAllByUid
|
||||||
|
};
|
@ -3,7 +3,7 @@ const { getFirestoreInstance } = require('../../services/firebaseClient');
|
|||||||
const { createEncryptedConverter } = require('../firestoreConverter');
|
const { createEncryptedConverter } = require('../firestoreConverter');
|
||||||
const encryptionService = require('../../services/encryptionService');
|
const encryptionService = require('../../services/encryptionService');
|
||||||
|
|
||||||
const userConverter = createEncryptedConverter(['api_key']);
|
const userConverter = createEncryptedConverter([]);
|
||||||
|
|
||||||
function usersCol() {
|
function usersCol() {
|
||||||
const db = getFirestoreInstance();
|
const db = getFirestoreInstance();
|
||||||
@ -38,11 +38,7 @@ async function getById(uid) {
|
|||||||
return docSnap.exists() ? docSnap.data() : null;
|
return docSnap.exists() ? docSnap.data() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveApiKey(uid, apiKey, provider = 'openai') {
|
|
||||||
const docRef = doc(usersCol(), uid);
|
|
||||||
await setDoc(docRef, { api_key: apiKey, provider }, { merge: true });
|
|
||||||
return { changes: 1 };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function update({ uid, displayName }) {
|
async function update({ uid, displayName }) {
|
||||||
const docRef = doc(usersCol(), uid);
|
const docRef = doc(usersCol(), uid);
|
||||||
@ -85,7 +81,6 @@ async function deleteById(uid) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
findOrCreate,
|
findOrCreate,
|
||||||
getById,
|
getById,
|
||||||
saveApiKey,
|
|
||||||
update,
|
update,
|
||||||
deleteById,
|
deleteById,
|
||||||
};
|
};
|
@ -1,8 +1,16 @@
|
|||||||
const sqliteRepository = require('./sqlite.repository');
|
const sqliteRepository = require('./sqlite.repository');
|
||||||
const firebaseRepository = require('./firebase.repository');
|
const firebaseRepository = require('./firebase.repository');
|
||||||
const authService = require('../../services/authService');
|
|
||||||
|
let authService = null;
|
||||||
|
|
||||||
|
function setAuthService(service) {
|
||||||
|
authService = service;
|
||||||
|
}
|
||||||
|
|
||||||
function getBaseRepository() {
|
function getBaseRepository() {
|
||||||
|
if (!authService) {
|
||||||
|
throw new Error('AuthService has not been set for the user repository.');
|
||||||
|
}
|
||||||
const user = authService.getCurrentUser();
|
const user = authService.getCurrentUser();
|
||||||
if (user && user.isLoggedIn) {
|
if (user && user.isLoggedIn) {
|
||||||
return firebaseRepository;
|
return firebaseRepository;
|
||||||
@ -21,10 +29,7 @@ const userRepositoryAdapter = {
|
|||||||
return getBaseRepository().getById(uid);
|
return getBaseRepository().getById(uid);
|
||||||
},
|
},
|
||||||
|
|
||||||
saveApiKey: (apiKey, provider) => {
|
|
||||||
const uid = authService.getCurrentUserId();
|
|
||||||
return getBaseRepository().saveApiKey(uid, apiKey, provider);
|
|
||||||
},
|
|
||||||
|
|
||||||
update: (updateData) => {
|
update: (updateData) => {
|
||||||
const uid = authService.getCurrentUserId();
|
const uid = authService.getCurrentUserId();
|
||||||
@ -37,4 +42,7 @@ const userRepositoryAdapter = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = userRepositoryAdapter;
|
module.exports = {
|
||||||
|
...userRepositoryAdapter,
|
||||||
|
setAuthService
|
||||||
|
};
|
@ -40,17 +40,7 @@ function getById(uid) {
|
|||||||
return db.prepare('SELECT * FROM users WHERE uid = ?').get(uid);
|
return db.prepare('SELECT * FROM users WHERE uid = ?').get(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveApiKey(uid, apiKey, provider = 'openai') {
|
|
||||||
const db = sqliteClient.getDb();
|
|
||||||
try {
|
|
||||||
const result = db.prepare('UPDATE users SET api_key = ?, provider = ? WHERE uid = ?').run(apiKey, provider, uid);
|
|
||||||
console.log(`SQLite: API key saved for user ${uid} with provider ${provider}.`);
|
|
||||||
return { changes: result.changes };
|
|
||||||
} catch (err) {
|
|
||||||
console.error('SQLite: Failed to save API key:', err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function update({ uid, displayName }) {
|
function update({ uid, displayName }) {
|
||||||
const db = sqliteClient.getDb();
|
const db = sqliteClient.getDb();
|
||||||
@ -96,7 +86,6 @@ function deleteById(uid) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
findOrCreate,
|
findOrCreate,
|
||||||
getById,
|
getById,
|
||||||
saveApiKey,
|
|
||||||
update,
|
update,
|
||||||
setMigrationComplete,
|
setMigrationComplete,
|
||||||
deleteById
|
deleteById
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
const { collection, doc, getDoc, setDoc, deleteDoc } = require('firebase/firestore');
|
||||||
|
const { getFirestoreInstance: getFirestore } = require('../../services/firebaseClient');
|
||||||
|
const { createEncryptedConverter } = require('../firestoreConverter');
|
||||||
|
|
||||||
|
// Create encrypted converter for user model selections
|
||||||
|
const userModelSelectionsConverter = createEncryptedConverter([
|
||||||
|
'selected_llm_provider',
|
||||||
|
'selected_llm_model',
|
||||||
|
'selected_stt_provider',
|
||||||
|
'selected_stt_model'
|
||||||
|
]);
|
||||||
|
|
||||||
|
function userModelSelectionsCol() {
|
||||||
|
const db = getFirestore();
|
||||||
|
return collection(db, 'user_model_selections').withConverter(userModelSelectionsConverter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(uid) {
|
||||||
|
try {
|
||||||
|
const docRef = doc(userModelSelectionsCol(), uid);
|
||||||
|
const docSnap = await getDoc(docRef);
|
||||||
|
return docSnap.exists() ? { id: docSnap.id, ...docSnap.data() } : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UserModelSelections Firebase] Error getting user model selections:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upsert(uid, selections) {
|
||||||
|
try {
|
||||||
|
const docRef = doc(userModelSelectionsCol(), uid);
|
||||||
|
await setDoc(docRef, selections, { merge: true });
|
||||||
|
return { changes: 1 };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UserModelSelections Firebase] Error upserting user model selections:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(uid) {
|
||||||
|
try {
|
||||||
|
const docRef = doc(userModelSelectionsCol(), uid);
|
||||||
|
await deleteDoc(docRef);
|
||||||
|
return { changes: 1 };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UserModelSelections Firebase] Error removing user model selections:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
get,
|
||||||
|
upsert,
|
||||||
|
remove
|
||||||
|
};
|
50
src/common/repositories/userModelSelections/index.js
Normal file
50
src/common/repositories/userModelSelections/index.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
const firebaseRepository = require('./firebase.repository');
|
||||||
|
const sqliteRepository = require('./sqlite.repository');
|
||||||
|
|
||||||
|
let authService = null;
|
||||||
|
|
||||||
|
function setAuthService(service) {
|
||||||
|
authService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBaseRepository() {
|
||||||
|
if (!authService) {
|
||||||
|
throw new Error('AuthService not set for userModelSelections repository');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = authService.getCurrentUser();
|
||||||
|
return user.isLoggedIn ? firebaseRepository : sqliteRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userModelSelectionsRepositoryAdapter = {
|
||||||
|
async get() {
|
||||||
|
const repo = getBaseRepository();
|
||||||
|
const uid = authService.getCurrentUserId();
|
||||||
|
return await repo.get(uid);
|
||||||
|
},
|
||||||
|
|
||||||
|
async upsert(selections) {
|
||||||
|
const repo = getBaseRepository();
|
||||||
|
const uid = authService.getCurrentUserId();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
const selectionsWithMeta = {
|
||||||
|
...selections,
|
||||||
|
uid,
|
||||||
|
updated_at: now
|
||||||
|
};
|
||||||
|
|
||||||
|
return await repo.upsert(uid, selectionsWithMeta);
|
||||||
|
},
|
||||||
|
|
||||||
|
async remove() {
|
||||||
|
const repo = getBaseRepository();
|
||||||
|
const uid = authService.getCurrentUserId();
|
||||||
|
return await repo.remove(uid);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
...userModelSelectionsRepositoryAdapter,
|
||||||
|
setAuthService
|
||||||
|
};
|
@ -0,0 +1,48 @@
|
|||||||
|
const sqliteClient = require('../../services/sqliteClient');
|
||||||
|
|
||||||
|
function get(uid) {
|
||||||
|
const db = sqliteClient.getDb();
|
||||||
|
const stmt = db.prepare('SELECT * FROM user_model_selections WHERE uid = ?');
|
||||||
|
return stmt.get(uid) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsert(uid, selections) {
|
||||||
|
const db = sqliteClient.getDb();
|
||||||
|
|
||||||
|
// Use SQLite's UPSERT syntax (INSERT ... ON CONFLICT ... DO UPDATE)
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
INSERT INTO user_model_selections (uid, selected_llm_provider, selected_llm_model,
|
||||||
|
selected_stt_provider, selected_stt_model, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(uid) DO UPDATE SET
|
||||||
|
selected_llm_provider = excluded.selected_llm_provider,
|
||||||
|
selected_llm_model = excluded.selected_llm_model,
|
||||||
|
selected_stt_provider = excluded.selected_stt_provider,
|
||||||
|
selected_stt_model = excluded.selected_stt_model,
|
||||||
|
updated_at = excluded.updated_at
|
||||||
|
`);
|
||||||
|
|
||||||
|
const result = stmt.run(
|
||||||
|
uid,
|
||||||
|
selections.selected_llm_provider || null,
|
||||||
|
selections.selected_llm_model || null,
|
||||||
|
selections.selected_stt_provider || null,
|
||||||
|
selections.selected_stt_model || null,
|
||||||
|
selections.updated_at
|
||||||
|
);
|
||||||
|
|
||||||
|
return { changes: result.changes };
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(uid) {
|
||||||
|
const db = sqliteClient.getDb();
|
||||||
|
const stmt = db.prepare('DELETE FROM user_model_selections WHERE uid = ?');
|
||||||
|
const result = stmt.run(uid);
|
||||||
|
return { changes: result.changes };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
get,
|
||||||
|
upsert,
|
||||||
|
remove
|
||||||
|
};
|
@ -5,6 +5,8 @@ const fetch = require('node-fetch');
|
|||||||
const encryptionService = require('./encryptionService');
|
const encryptionService = require('./encryptionService');
|
||||||
const migrationService = require('./migrationService');
|
const migrationService = require('./migrationService');
|
||||||
const sessionRepository = require('../repositories/session');
|
const sessionRepository = require('../repositories/session');
|
||||||
|
const providerSettingsRepository = require('../repositories/providerSettings');
|
||||||
|
const userModelSelectionsRepository = require('../repositories/userModelSelections');
|
||||||
|
|
||||||
async function getVirtualKeyByEmail(email, idToken) {
|
async function getVirtualKeyByEmail(email, idToken) {
|
||||||
if (!idToken) {
|
if (!idToken) {
|
||||||
@ -40,10 +42,13 @@ class AuthService {
|
|||||||
this.currentUser = null;
|
this.currentUser = null;
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
|
|
||||||
// Initialize immediately for the default local user on startup.
|
|
||||||
// This ensures the key is ready before any login/logout state change.
|
// This ensures the key is ready before any login/logout state change.
|
||||||
encryptionService.initializeKey(this.currentUserId);
|
encryptionService.initializeKey(this.currentUserId);
|
||||||
this.initializationPromise = null;
|
this.initializationPromise = null;
|
||||||
|
|
||||||
|
sessionRepository.setAuthService(this);
|
||||||
|
providerSettingsRepository.setAuthService(this);
|
||||||
|
userModelSelectionsRepository.setAuthService(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
|
@ -1,21 +1,28 @@
|
|||||||
const Store = require('electron-store');
|
const Store = require('electron-store');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const { ipcMain, webContents } = require('electron');
|
const { ipcMain, webContents } = require('electron');
|
||||||
const { PROVIDERS } = require('../ai/factory');
|
const { PROVIDERS, getProviderClass } = require('../ai/factory');
|
||||||
const encryptionService = require('./encryptionService');
|
const encryptionService = require('./encryptionService');
|
||||||
|
const providerSettingsRepository = require('../repositories/providerSettings');
|
||||||
|
const userModelSelectionsRepository = require('../repositories/userModelSelections');
|
||||||
|
|
||||||
class ModelStateService {
|
class ModelStateService {
|
||||||
constructor(authService) {
|
constructor(authService) {
|
||||||
this.authService = authService;
|
this.authService = authService;
|
||||||
this.store = new Store({ name: 'pickle-glass-model-state' });
|
this.store = new Store({ name: 'pickle-glass-model-state' });
|
||||||
this.state = {};
|
this.state = {};
|
||||||
|
this.hasMigrated = false;
|
||||||
|
|
||||||
|
// Set auth service for repositories
|
||||||
|
providerSettingsRepository.setAuthService(authService);
|
||||||
|
userModelSelectionsRepository.setAuthService(authService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
|
console.log('[ModelStateService] Initializing...');
|
||||||
await this._loadStateForCurrentUser();
|
await this._loadStateForCurrentUser();
|
||||||
|
|
||||||
this.setupIpcHandlers();
|
this.setupIpcHandlers();
|
||||||
console.log('[ModelStateService] Initialized.');
|
console.log('[ModelStateService] Initialization complete');
|
||||||
}
|
}
|
||||||
|
|
||||||
_logCurrentSelection() {
|
_logCurrentSelection() {
|
||||||
@ -64,44 +71,186 @@ class ModelStateService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _migrateFromElectronStore() {
|
||||||
|
console.log('[ModelStateService] Starting migration from electron-store to database...');
|
||||||
|
const userId = this.authService.getCurrentUserId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get data from electron-store
|
||||||
|
const legacyData = this.store.get(`users.${userId}`, null);
|
||||||
|
|
||||||
|
if (!legacyData) {
|
||||||
|
console.log('[ModelStateService] No legacy data to migrate');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[ModelStateService] Found legacy data, migrating...');
|
||||||
|
|
||||||
|
// Migrate provider settings (API keys and selected models per provider)
|
||||||
|
const { apiKeys = {}, selectedModels = {} } = legacyData;
|
||||||
|
|
||||||
|
for (const [provider, apiKey] of Object.entries(apiKeys)) {
|
||||||
|
if (apiKey && PROVIDERS[provider]) {
|
||||||
|
// For encrypted keys, they are already decrypted in _loadStateForCurrentUser
|
||||||
|
await providerSettingsRepository.upsert(provider, {
|
||||||
|
api_key: apiKey
|
||||||
|
});
|
||||||
|
console.log(`[ModelStateService] Migrated API key for ${provider}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate global model selections
|
||||||
|
if (selectedModels.llm || selectedModels.stt) {
|
||||||
|
const llmProvider = selectedModels.llm ? this.getProviderForModel('llm', selectedModels.llm) : null;
|
||||||
|
const sttProvider = selectedModels.stt ? this.getProviderForModel('stt', selectedModels.stt) : null;
|
||||||
|
|
||||||
|
await userModelSelectionsRepository.upsert({
|
||||||
|
selected_llm_provider: llmProvider,
|
||||||
|
selected_llm_model: selectedModels.llm,
|
||||||
|
selected_stt_provider: sttProvider,
|
||||||
|
selected_stt_model: selectedModels.stt
|
||||||
|
});
|
||||||
|
console.log('[ModelStateService] Migrated global model selections');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark migration as complete by removing legacy data
|
||||||
|
this.store.delete(`users.${userId}`);
|
||||||
|
console.log('[ModelStateService] Migration completed and legacy data cleaned up');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ModelStateService] Migration failed:', error);
|
||||||
|
// Don't throw - continue with normal operation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _loadStateFromDatabase() {
|
||||||
|
console.log('[ModelStateService] Loading state from database...');
|
||||||
|
const userId = this.authService.getCurrentUserId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load provider settings
|
||||||
|
const providerSettings = await providerSettingsRepository.getAllByUid();
|
||||||
|
const apiKeys = {};
|
||||||
|
|
||||||
|
// Reconstruct apiKeys object
|
||||||
|
Object.keys(PROVIDERS).forEach(provider => {
|
||||||
|
apiKeys[provider] = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const setting of providerSettings) {
|
||||||
|
if (setting.api_key) {
|
||||||
|
// API keys are stored encrypted in database, decrypt them
|
||||||
|
if (setting.provider !== 'ollama' && setting.provider !== 'whisper') {
|
||||||
|
try {
|
||||||
|
apiKeys[setting.provider] = encryptionService.decrypt(setting.api_key);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[ModelStateService] Failed to decrypt API key for ${setting.provider}, resetting`);
|
||||||
|
apiKeys[setting.provider] = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
apiKeys[setting.provider] = setting.api_key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load global model selections
|
||||||
|
const modelSelections = await userModelSelectionsRepository.get();
|
||||||
|
const selectedModels = {
|
||||||
|
llm: modelSelections?.selected_llm_model || null,
|
||||||
|
stt: modelSelections?.selected_stt_model || null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
apiKeys,
|
||||||
|
selectedModels
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`[ModelStateService] State loaded from database for user: ${userId}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ModelStateService] Failed to load state from database:', error);
|
||||||
|
// Fall back to default state
|
||||||
|
const initialApiKeys = Object.keys(PROVIDERS).reduce((acc, key) => {
|
||||||
|
acc[key] = null;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
apiKeys: initialApiKeys,
|
||||||
|
selectedModels: { llm: null, stt: null },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async _loadStateForCurrentUser() {
|
async _loadStateForCurrentUser() {
|
||||||
const userId = this.authService.getCurrentUserId();
|
const userId = this.authService.getCurrentUserId();
|
||||||
|
|
||||||
// Initialize encryption service for current user
|
// Initialize encryption service for current user
|
||||||
await encryptionService.initializeKey(userId);
|
await encryptionService.initializeKey(userId);
|
||||||
|
|
||||||
const initialApiKeys = Object.keys(PROVIDERS).reduce((acc, key) => {
|
// Try to load from database first
|
||||||
acc[key] = null;
|
await this._loadStateFromDatabase();
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const defaultState = {
|
|
||||||
apiKeys: initialApiKeys,
|
|
||||||
selectedModels: { llm: null, stt: null },
|
|
||||||
};
|
|
||||||
this.state = this.store.get(`users.${userId}`, defaultState);
|
|
||||||
console.log(`[ModelStateService] State loaded for user: ${userId}`);
|
|
||||||
|
|
||||||
for (const p of Object.keys(PROVIDERS)) {
|
// Check if we need to migrate from electron-store
|
||||||
if (!(p in this.state.apiKeys)) {
|
const legacyData = this.store.get(`users.${userId}`, null);
|
||||||
this.state.apiKeys[p] = null;
|
if (legacyData && !this.hasMigrated) {
|
||||||
} else if (this.state.apiKeys[p] && p !== 'ollama' && p !== 'whisper') {
|
await this._migrateFromElectronStore();
|
||||||
try {
|
// Reload state after migration
|
||||||
this.state.apiKeys[p] = encryptionService.decrypt(this.state.apiKeys[p]);
|
await this._loadStateFromDatabase();
|
||||||
} catch (error) {
|
this.hasMigrated = true;
|
||||||
console.error(`[ModelStateService] Failed to decrypt API key for ${p}, resetting`);
|
|
||||||
this.state.apiKeys[p] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._autoSelectAvailableModels();
|
this._autoSelectAvailableModels();
|
||||||
this._saveState();
|
await this._saveState();
|
||||||
this._logCurrentSelection();
|
this._logCurrentSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _saveState() {
|
||||||
|
console.log('[ModelStateService] Saving state to database...');
|
||||||
|
const userId = this.authService.getCurrentUserId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Save provider settings (API keys)
|
||||||
|
for (const [provider, apiKey] of Object.entries(this.state.apiKeys)) {
|
||||||
|
if (apiKey) {
|
||||||
|
const encryptedKey = (provider !== 'ollama' && provider !== 'whisper')
|
||||||
|
? encryptionService.encrypt(apiKey)
|
||||||
|
: apiKey;
|
||||||
|
|
||||||
|
await providerSettingsRepository.upsert(provider, {
|
||||||
|
api_key: encryptedKey
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Remove empty API keys
|
||||||
|
await providerSettingsRepository.remove(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save global model selections
|
||||||
|
const llmProvider = this.state.selectedModels.llm ? this.getProviderForModel('llm', this.state.selectedModels.llm) : null;
|
||||||
|
const sttProvider = this.state.selectedModels.stt ? this.getProviderForModel('stt', this.state.selectedModels.stt) : null;
|
||||||
|
|
||||||
|
if (llmProvider || sttProvider || this.state.selectedModels.llm || this.state.selectedModels.stt) {
|
||||||
|
await userModelSelectionsRepository.upsert({
|
||||||
|
selected_llm_provider: llmProvider,
|
||||||
|
selected_llm_model: this.state.selectedModels.llm,
|
||||||
|
selected_stt_provider: sttProvider,
|
||||||
|
selected_stt_model: this.state.selectedModels.stt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[ModelStateService] State saved to database for user: ${userId}`);
|
||||||
|
this._logCurrentSelection();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ModelStateService] Failed to save state to database:', error);
|
||||||
|
// Fall back to electron-store for now
|
||||||
|
this._saveStateToElectronStore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_saveState() {
|
_saveStateToElectronStore() {
|
||||||
|
console.log('[ModelStateService] Falling back to electron-store...');
|
||||||
const userId = this.authService.getCurrentUserId();
|
const userId = this.authService.getCurrentUserId();
|
||||||
const stateToSave = {
|
const stateToSave = {
|
||||||
...this.state,
|
...this.state,
|
||||||
@ -120,93 +269,34 @@ class ModelStateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.store.set(`users.${userId}`, stateToSave);
|
this.store.set(`users.${userId}`, stateToSave);
|
||||||
console.log(`[ModelStateService] State saved for user: ${userId}`);
|
console.log(`[ModelStateService] State saved to electron-store for user: ${userId}`);
|
||||||
this._logCurrentSelection();
|
this._logCurrentSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateApiKey(provider, key) {
|
async validateApiKey(provider, key) {
|
||||||
if (!key || key.trim() === '') {
|
if (!key || (key.trim() === '' && provider !== 'ollama' && provider !== 'whisper')) {
|
||||||
return { success: false, error: 'API key cannot be empty.' };
|
return { success: false, error: 'API key cannot be empty.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
let validationUrl, headers;
|
const ProviderClass = getProviderClass(provider);
|
||||||
const body = undefined;
|
|
||||||
|
|
||||||
switch (provider) {
|
if (!ProviderClass || typeof ProviderClass.validateApiKey !== 'function') {
|
||||||
case 'ollama':
|
// Default to success if no specific validator is found
|
||||||
// Ollama doesn't need API key validation
|
console.warn(`[ModelStateService] No validateApiKey function for provider: ${provider}. Assuming valid.`);
|
||||||
// Just check if the service is running
|
|
||||||
try {
|
|
||||||
const response = await fetch('http://localhost:11434/api/tags');
|
|
||||||
if (response.ok) {
|
|
||||||
console.log(`[ModelStateService] Ollama service is accessible.`);
|
|
||||||
this.setApiKey(provider, 'local'); // Use 'local' as a placeholder
|
|
||||||
return { success: true };
|
|
||||||
} else {
|
|
||||||
return { success: false, error: 'Ollama service is not running. Please start Ollama first.' };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: 'Cannot connect to Ollama. Please ensure Ollama is installed and running.' };
|
|
||||||
}
|
|
||||||
case 'whisper':
|
|
||||||
// Whisper is a local service, no API key validation needed
|
|
||||||
console.log(`[ModelStateService] Whisper is a local service.`);
|
|
||||||
this.setApiKey(provider, 'local'); // Use 'local' as a placeholder
|
|
||||||
return { success: true };
|
|
||||||
case 'openai':
|
|
||||||
validationUrl = 'https://api.openai.com/v1/models';
|
|
||||||
headers = { 'Authorization': `Bearer ${key}` };
|
|
||||||
break;
|
|
||||||
case 'gemini':
|
|
||||||
validationUrl = `https://generativelanguage.googleapis.com/v1beta/models?key=${key}`;
|
|
||||||
headers = {};
|
|
||||||
break;
|
|
||||||
case 'anthropic': {
|
|
||||||
if (!key.startsWith('sk-ant-')) {
|
|
||||||
throw new Error('Invalid Anthropic key format.');
|
|
||||||
}
|
|
||||||
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-api-key": key,
|
|
||||||
"anthropic-version": "2023-06-01",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: "claude-3-haiku-20240307",
|
|
||||||
max_tokens: 1,
|
|
||||||
messages: [{ role: "user", content: "Hi" }],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok && response.status !== 400) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
return { success: false, error: errorData.error?.message || `Validation failed with status: ${response.status}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[ModelStateService] API key for ${provider} is valid.`);
|
|
||||||
this.setApiKey(provider, key);
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
|
||||||
default:
|
|
||||||
return { success: false, error: 'Unknown provider.' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(validationUrl, { headers, body });
|
const result = await ProviderClass.validateApiKey(key);
|
||||||
if (response.ok) {
|
if (result.success) {
|
||||||
console.log(`[ModelStateService] API key for ${provider} is valid.`);
|
console.log(`[ModelStateService] API key for ${provider} is valid.`);
|
||||||
this.setApiKey(provider, key);
|
|
||||||
return { success: true };
|
|
||||||
} else {
|
} else {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
console.log(`[ModelStateService] API key for ${provider} is invalid: ${result.error}`);
|
||||||
const message = errorData.error?.message || `Validation failed with status: ${response.status}`;
|
|
||||||
console.log(`[ModelStateService] API key for ${provider} is invalid: ${message}`);
|
|
||||||
return { success: false, error: message };
|
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[ModelStateService] Network error during ${provider} key validation:`, error);
|
console.error(`[ModelStateService] Error during ${provider} key validation:`, error);
|
||||||
return { success: false, error: 'A network error occurred during validation.' };
|
return { success: false, error: 'An unexpected error occurred during validation.' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,33 +329,14 @@ class ModelStateService {
|
|||||||
setApiKey(provider, key) {
|
setApiKey(provider, key) {
|
||||||
if (provider in this.state.apiKeys) {
|
if (provider in this.state.apiKeys) {
|
||||||
this.state.apiKeys[provider] = key;
|
this.state.apiKeys[provider] = key;
|
||||||
|
|
||||||
const llmModels = PROVIDERS[provider]?.llmModels;
|
|
||||||
const sttModels = PROVIDERS[provider]?.sttModels;
|
|
||||||
|
|
||||||
// Prioritize newly set API key provider over existing selections
|
|
||||||
// Only for non-local providers or if no model is currently selected
|
|
||||||
if (llmModels?.length > 0) {
|
|
||||||
if (!this.state.selectedModels.llm || provider !== 'ollama') {
|
|
||||||
this.state.selectedModels.llm = llmModels[0].id;
|
|
||||||
console.log(`[ModelStateService] Selected LLM model from newly configured provider ${provider}: ${llmModels[0].id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sttModels?.length > 0) {
|
|
||||||
if (!this.state.selectedModels.stt || provider !== 'whisper') {
|
|
||||||
this.state.selectedModels.stt = sttModels[0].id;
|
|
||||||
console.log(`[ModelStateService] Selected STT model from newly configured provider ${provider}: ${sttModels[0].id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._saveState();
|
this._saveState();
|
||||||
this._logCurrentSelection();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getApiKey(provider) {
|
getApiKey(provider) {
|
||||||
return this.state.apiKeys[provider] || null;
|
return this.state.apiKeys[provider];
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllApiKeys() {
|
getAllApiKeys() {
|
||||||
@ -351,6 +422,18 @@ class ModelStateService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasValidApiKey() {
|
||||||
|
if (this.isLoggedInWithFirebase()) return true;
|
||||||
|
|
||||||
|
// Check if any provider has a valid API key
|
||||||
|
return Object.entries(this.state.apiKeys).some(([provider, key]) => {
|
||||||
|
if (provider === 'ollama' || provider === 'whisper') {
|
||||||
|
return key === 'local';
|
||||||
|
}
|
||||||
|
return key && key.trim().length > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
getAvailableModels(type) {
|
getAvailableModels(type) {
|
||||||
const available = [];
|
const available = [];
|
||||||
@ -445,10 +528,28 @@ class ModelStateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupIpcHandlers() {
|
setupIpcHandlers() {
|
||||||
ipcMain.handle('model:validate-key', (e, { provider, key }) => this.validateApiKey(provider, key));
|
ipcMain.handle('model:validate-key', async (e, { provider, key }) => {
|
||||||
|
const result = await this.validateApiKey(provider, key);
|
||||||
|
if (result.success) {
|
||||||
|
// Use 'local' as placeholder for local services
|
||||||
|
const finalKey = (provider === 'ollama' || provider === 'whisper') ? 'local' : key;
|
||||||
|
this.setApiKey(provider, finalKey);
|
||||||
|
// After setting the key, auto-select models
|
||||||
|
this._autoSelectAvailableModels();
|
||||||
|
this._saveState(); // Ensure state is saved after model selection
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
ipcMain.handle('model:get-all-keys', () => this.getAllApiKeys());
|
ipcMain.handle('model:get-all-keys', () => this.getAllApiKeys());
|
||||||
ipcMain.handle('model:set-api-key', (e, { provider, key }) => this.setApiKey(provider, key));
|
ipcMain.handle('model:set-api-key', async (e, { provider, key }) => {
|
||||||
ipcMain.handle('model:remove-api-key', (e, { provider }) => {
|
const success = this.setApiKey(provider, key);
|
||||||
|
if (success) {
|
||||||
|
this._autoSelectAvailableModels();
|
||||||
|
await this._saveState();
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
});
|
||||||
|
ipcMain.handle('model:remove-api-key', async (e, { provider }) => {
|
||||||
const success = this.removeApiKey(provider);
|
const success = this.removeApiKey(provider);
|
||||||
if (success) {
|
if (success) {
|
||||||
const selectedModels = this.getSelectedModels();
|
const selectedModels = this.getSelectedModels();
|
||||||
@ -461,7 +562,7 @@ class ModelStateService {
|
|||||||
return success;
|
return success;
|
||||||
});
|
});
|
||||||
ipcMain.handle('model:get-selected-models', () => this.getSelectedModels());
|
ipcMain.handle('model:get-selected-models', () => this.getSelectedModels());
|
||||||
ipcMain.handle('model:set-selected-model', (e, { type, modelId }) => this.setSelectedModel(type, modelId));
|
ipcMain.handle('model:set-selected-model', async (e, { type, modelId }) => this.setSelectedModel(type, modelId));
|
||||||
ipcMain.handle('model:get-available-models', (e, { type }) => this.getAvailableModels(type));
|
ipcMain.handle('model:get-available-models', (e, { type }) => this.getAvailableModels(type));
|
||||||
ipcMain.handle('model:are-providers-configured', () => this.areProvidersConfigured());
|
ipcMain.handle('model:are-providers-configured', () => this.areProvidersConfigured());
|
||||||
ipcMain.handle('model:get-current-model-info', (e, { type }) => this.getCurrentModelInfo(type));
|
ipcMain.handle('model:get-current-model-info', (e, { type }) => this.getCurrentModelInfo(type));
|
||||||
|
@ -33,6 +33,13 @@ class SQLiteClient {
|
|||||||
return this.db;
|
return this.db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_validateAndQuoteIdentifier(identifier) {
|
||||||
|
if (!/^[a-zA-Z0-9_]+$/.test(identifier)) {
|
||||||
|
throw new Error(`Invalid database identifier used: ${identifier}. Only alphanumeric characters and underscores are allowed.`);
|
||||||
|
}
|
||||||
|
return `"${identifier}"`;
|
||||||
|
}
|
||||||
|
|
||||||
synchronizeSchema() {
|
synchronizeSchema() {
|
||||||
console.log('[DB Sync] Starting schema synchronization...');
|
console.log('[DB Sync] Starting schema synchronization...');
|
||||||
const tablesInDb = this.getTablesFromDb();
|
const tablesInDb = this.getTablesFromDb();
|
||||||
@ -57,25 +64,42 @@ class SQLiteClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createTable(tableName, tableSchema) {
|
createTable(tableName, tableSchema) {
|
||||||
const columnDefs = tableSchema.columns.map(col => `"${col.name}" ${col.type}`).join(', ');
|
const safeTableName = this._validateAndQuoteIdentifier(tableName);
|
||||||
const query = `CREATE TABLE IF NOT EXISTS "${tableName}" (${columnDefs})`;
|
const columnDefs = tableSchema.columns
|
||||||
|
.map(col => `${this._validateAndQuoteIdentifier(col.name)} ${col.type}`)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
const constraints = tableSchema.constraints || [];
|
||||||
|
const constraintsDef = constraints.length > 0 ? ', ' + constraints.join(', ') : '';
|
||||||
|
|
||||||
|
const query = `CREATE TABLE IF NOT EXISTS ${safeTableName} (${columnDefs}${constraintsDef})`;
|
||||||
console.log(`[DB Sync] Creating table: ${tableName}`);
|
console.log(`[DB Sync] Creating table: ${tableName}`);
|
||||||
this.db.prepare(query).run();
|
this.db.exec(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTable(tableName, tableSchema) {
|
updateTable(tableName, tableSchema) {
|
||||||
const existingColumns = this.db.prepare(`PRAGMA table_info("${tableName}")`).all();
|
const safeTableName = this._validateAndQuoteIdentifier(tableName);
|
||||||
const existingColumnNames = existingColumns.map(c => c.name);
|
|
||||||
const columnsToAdd = tableSchema.columns.filter(col => !existingColumnNames.includes(col.name));
|
// Get current columns
|
||||||
|
const currentColumns = this.db.prepare(`PRAGMA table_info(${safeTableName})`).all();
|
||||||
|
const currentColumnNames = currentColumns.map(col => col.name);
|
||||||
|
|
||||||
if (columnsToAdd.length > 0) {
|
// Check for new columns to add
|
||||||
console.log(`[DB Sync] Updating table: ${tableName}. Adding columns: ${columnsToAdd.map(c=>c.name).join(', ')}`);
|
const newColumns = tableSchema.columns.filter(col => !currentColumnNames.includes(col.name));
|
||||||
for (const column of columnsToAdd) {
|
|
||||||
const addColumnQuery = `ALTER TABLE "${tableName}" ADD COLUMN "${column.name}" ${column.type}`;
|
if (newColumns.length > 0) {
|
||||||
this.db.prepare(addColumnQuery).run();
|
console.log(`[DB Sync] Adding ${newColumns.length} new column(s) to ${tableName}`);
|
||||||
|
for (const col of newColumns) {
|
||||||
|
const safeColName = this._validateAndQuoteIdentifier(col.name);
|
||||||
|
const addColumnQuery = `ALTER TABLE ${safeTableName} ADD COLUMN ${safeColName} ${col.type}`;
|
||||||
|
this.db.exec(addColumnQuery);
|
||||||
|
console.log(`[DB Sync] Added column ${col.name} to ${tableName}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tableSchema.constraints && tableSchema.constraints.length > 0) {
|
||||||
|
console.log(`[DB Sync] Note: Constraints for ${tableName} can only be set during table creation`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runQuery(query, params = []) {
|
runQuery(query, params = []) {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
const { ipcMain, BrowserWindow } = require('electron');
|
const { ipcMain, BrowserWindow } = require('electron');
|
||||||
const Store = require('electron-store');
|
const Store = require('electron-store');
|
||||||
const authService = require('../../common/services/authService');
|
const authService = require('../../common/services/authService');
|
||||||
const userRepository = require('../../common/repositories/user');
|
|
||||||
const settingsRepository = require('./repositories');
|
const settingsRepository = require('./repositories');
|
||||||
const { getStoredApiKey, getStoredProvider, windowPool } = require('../../electron/windowManager');
|
const { getStoredApiKey, getStoredProvider, windowPool } = require('../../electron/windowManager');
|
||||||
|
|
||||||
@ -282,26 +281,13 @@ async function deletePreset(id) {
|
|||||||
|
|
||||||
async function saveApiKey(apiKey, provider = 'openai') {
|
async function saveApiKey(apiKey, provider = 'openai') {
|
||||||
try {
|
try {
|
||||||
const user = authService.getCurrentUser();
|
// Use ModelStateService as the single source of truth for API key management
|
||||||
if (!user.isLoggedIn) {
|
const modelStateService = global.modelStateService;
|
||||||
// For non-logged-in users, save to local storage
|
if (!modelStateService) {
|
||||||
const Store = require('electron-store');
|
throw new Error('ModelStateService not initialized');
|
||||||
const store = new Store();
|
|
||||||
store.set('apiKey', apiKey);
|
|
||||||
store.set('provider', provider);
|
|
||||||
|
|
||||||
// Notify windows
|
|
||||||
BrowserWindow.getAllWindows().forEach(win => {
|
|
||||||
if (!win.isDestroyed()) {
|
|
||||||
win.webContents.send('api-key-validated', apiKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For logged-in users, use the repository adapter which injects the UID.
|
await modelStateService.setApiKey(provider, apiKey);
|
||||||
await userRepository.saveApiKey(apiKey, provider);
|
|
||||||
|
|
||||||
// Notify windows
|
// Notify windows
|
||||||
BrowserWindow.getAllWindows().forEach(win => {
|
BrowserWindow.getAllWindows().forEach(win => {
|
||||||
@ -319,16 +305,16 @@ async function saveApiKey(apiKey, provider = 'openai') {
|
|||||||
|
|
||||||
async function removeApiKey() {
|
async function removeApiKey() {
|
||||||
try {
|
try {
|
||||||
const user = authService.getCurrentUser();
|
// Use ModelStateService as the single source of truth for API key management
|
||||||
if (!user.isLoggedIn) {
|
const modelStateService = global.modelStateService;
|
||||||
// For non-logged-in users, remove from local storage
|
if (!modelStateService) {
|
||||||
const Store = require('electron-store');
|
throw new Error('ModelStateService not initialized');
|
||||||
const store = new Store();
|
}
|
||||||
store.delete('apiKey');
|
|
||||||
store.delete('provider');
|
// Remove all API keys for all providers
|
||||||
} else {
|
const providers = ['openai', 'anthropic', 'gemini', 'ollama', 'whisper'];
|
||||||
// For logged-in users, use the repository adapter.
|
for (const provider of providers) {
|
||||||
await userRepository.saveApiKey(null, null);
|
await modelStateService.removeApiKey(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify windows
|
// Notify windows
|
||||||
|
10
src/index.js
10
src/index.js
@ -680,13 +680,13 @@ function setupWebDataHandlers() {
|
|||||||
result = await userRepository.findOrCreate(payload);
|
result = await userRepository.findOrCreate(payload);
|
||||||
break;
|
break;
|
||||||
case 'save-api-key':
|
case 'save-api-key':
|
||||||
// Assuming payload is { apiKey, provider }
|
// Use ModelStateService as the single source of truth for API key management
|
||||||
result = await userRepository.saveApiKey(payload.apiKey, payload.provider);
|
result = await modelStateService.setApiKey(payload.provider, payload.apiKey);
|
||||||
break;
|
break;
|
||||||
case 'check-api-key-status':
|
case 'check-api-key-status':
|
||||||
// Adapter injects UID
|
// Use ModelStateService to check API key status
|
||||||
const user = await userRepository.getById();
|
const hasApiKey = await modelStateService.hasValidApiKey();
|
||||||
result = { hasApiKey: !!user?.api_key && user.api_key.length > 0 };
|
result = { hasApiKey };
|
||||||
break;
|
break;
|
||||||
case 'delete-account':
|
case 'delete-account':
|
||||||
// Adapter injects UID
|
// Adapter injects UID
|
||||||
|
Loading…
x
Reference in New Issue
Block a user