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