From 649c3b5c3150b5640d4d4479652275b7b3347c0e Mon Sep 17 00:00:00 2001 From: sanio Date: Tue, 8 Jul 2025 06:09:14 +0900 Subject: [PATCH] add modelStateService for provider, model, apikey selection --- src/app/ApiKeyHeader.js | 344 +++++-------- src/app/HeaderController.js | 42 +- src/common/ai/factory.js | 141 +++-- src/common/services/authService.js | 53 +- src/common/services/modelStateService.js | 324 ++++++++++++ src/electron/windowManager.js | 220 +++++--- src/features/ask/askService.js | 45 +- src/features/listen/stt/sttService.js | 123 +++-- src/features/listen/summary/summaryService.js | 59 +-- src/features/settings/SettingsView.js | 483 ++++++++++++++++-- src/index.js | 9 + 11 files changed, 1282 insertions(+), 561 deletions(-) create mode 100644 src/common/services/modelStateService.js diff --git a/src/app/ApiKeyHeader.js b/src/app/ApiKeyHeader.js index 25f1571..92962c8 100644 --- a/src/app/ApiKeyHeader.js +++ b/src/app/ApiKeyHeader.js @@ -1,12 +1,17 @@ import { html, css, LitElement } from "../assets/lit-core-2.7.4.min.js" export class ApiKeyHeader extends LitElement { + //////// after_modelStateService //////// static properties = { - apiKey: { type: String }, + llmApiKey: { type: String }, + sttApiKey: { type: String }, + llmProvider: { type: String }, + sttProvider: { type: String }, isLoading: { type: Boolean }, errorMessage: { type: String }, - selectedProvider: { type: String }, + providers: { type: Object, state: true }, } + //////// after_modelStateService //////// static styles = css` :host { @@ -45,7 +50,7 @@ export class ApiKeyHeader extends LitElement { } .container { - width: 285px; + width: 350px; min-height: 260px; padding: 18px 20px; background: rgba(0, 0, 0, 0.3); @@ -153,28 +158,22 @@ export class ApiKeyHeader extends LitElement { outline: none; } - .provider-select { + .providers-container { display: flex; gap: 12px; width: 100%; } + .provider-column { flex: 1; display: flex; flex-direction: column; align-items: center; } + .provider-label { color: rgba(255, 255, 255, 0.7); font-size: 11px; font-weight: 500; margin-bottom: 6px; } + .api-input, .provider-select { width: 100%; height: 34px; + text-align: center; background: rgba(255, 255, 255, 0.1); border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.2); padding: 0 10px; color: white; font-size: 12px; - font-weight: 400; margin-bottom: 6px; - text-align: center; - cursor: pointer; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2714%27%20height%3D%278%27%20viewBox%3D%270%200%2014%208%27%20xmlns%3D%27http%3A//www.w3.org/2000/svg%27%3E%3Cpath%20d%3D%27M1%201l6%206%206-6%27%20stroke%3D%27%23ffffff%27%20stroke-width%3D%271.5%27%20fill%3D%27none%27%20fill-rule%3D%27evenodd%27/%3E%3C/svg%3E'); - background-repeat: no-repeat; - background-position: right 10px center; - background-size: 12px; - padding-right: 30px; } + .provider-select option { background: #1a1a1a; color: white; } .provider-select:hover { background-color: rgba(255, 255, 255, 0.15); @@ -187,11 +186,6 @@ export class ApiKeyHeader extends LitElement { border-color: rgba(255, 255, 255, 0.4); } - .provider-select option { - background: #1a1a1a; - color: white; - padding: 5px; - } .action-button { width: 100%; @@ -239,15 +233,7 @@ export class ApiKeyHeader extends LitElement { font-weight: 500; /* Medium */ margin: 10px 0; } - - .provider-label { - color: rgba(255, 255, 255, 0.7); - font-size: 11px; - font-weight: 400; - margin-bottom: 4px; - width: 100%; - text-align: left; - } + /* ────────────────[ GLASS BYPASS ]─────────────── */ :host-context(body.has-glass) .container, @@ -278,11 +264,16 @@ export class ApiKeyHeader extends LitElement { super() this.dragState = null this.wasJustDragged = false - this.apiKey = "" this.isLoading = false this.errorMessage = "" - this.validatedApiKey = null - this.selectedProvider = "openai" + //////// after_modelStateService //////// + this.llmApiKey = ""; + this.sttApiKey = ""; + this.llmProvider = "openai"; + this.sttProvider = "openai"; + this.providers = { llm: [], stt: [] }; // 초기화 + this.loadProviderConfig(); + //////// after_modelStateService //////// this.handleMouseMove = this.handleMouseMove.bind(this) this.handleMouseUp = this.handleMouseUp.bind(this) @@ -303,6 +294,35 @@ export class ApiKeyHeader extends LitElement { this.requestUpdate() } + async loadProviderConfig() { + if (!window.require) return; + const { ipcRenderer } = window.require('electron'); + const config = await ipcRenderer.invoke('model:get-provider-config'); + + const llmProviders = []; + const sttProviders = []; + + for (const id in config) { + // 'openai-glass' 같은 가상 Provider는 UI에 표시하지 않음 + if (id.includes('-glass')) continue; + + if (config[id].llmModels.length > 0) { + llmProviders.push({ id, name: config[id].name }); + } + if (config[id].sttModels.length > 0) { + sttProviders.push({ id, name: config[id].name }); + } + } + + this.providers = { llm: llmProviders, stt: sttProviders }; + + // 기본 선택 값 설정 + if (llmProviders.length > 0) this.llmProvider = llmProviders[0].id; + if (sttProviders.length > 0) this.sttProvider = sttProviders[0].id; + + this.requestUpdate(); +} + async handleMouseDown(e) { if (e.target.tagName === "INPUT" || e.target.tagName === "BUTTON" || e.target.tagName === "SELECT") { return @@ -409,144 +429,45 @@ export class ApiKeyHeader extends LitElement { } } + //////// after_modelStateService //////// async handleSubmit() { - if (this.wasJustDragged || this.isLoading || !this.apiKey.trim()) { - console.log("Submit blocked:", { - wasJustDragged: this.wasJustDragged, - isLoading: this.isLoading, - hasApiKey: !!this.apiKey.trim(), - }) - return + console.log('[ApiKeyHeader] handleSubmit: Submitting API keys...'); + if (this.isLoading || !this.llmApiKey.trim() || !this.sttApiKey.trim()) { + this.errorMessage = "Please enter keys for both LLM and STT."; + return; } - console.log("Starting API key validation...") - this.isLoading = true - this.errorMessage = "" - this.requestUpdate() + this.isLoading = true; + this.errorMessage = ""; + this.requestUpdate(); - const apiKey = this.apiKey.trim() - const isValid = false - try { - const isValid = await this.validateApiKey(this.apiKey.trim(), this.selectedProvider) + const { ipcRenderer } = window.require('electron'); - if (isValid) { - console.log("API key valid - starting slide out animation") - this.startSlideOutAnimation() - this.validatedApiKey = this.apiKey.trim() - this.validatedProvider = this.selectedProvider - } else { - this.errorMessage = "Invalid API key - please check and try again" - console.log("API key validation failed") - } - } catch (error) { - console.error("API key validation error:", error) - this.errorMessage = "Validation error - please try again" - } finally { - this.isLoading = false - this.requestUpdate() - } - } + console.log('[ApiKeyHeader] handleSubmit: Validating LLM key...'); + const llmValidation = ipcRenderer.invoke('model:validate-key', { provider: this.llmProvider, key: this.llmApiKey.trim() }); + const sttValidation = ipcRenderer.invoke('model:validate-key', { provider: this.sttProvider, key: this.sttApiKey.trim() }); - async validateApiKey(apiKey, provider = "openai") { - if (!apiKey || apiKey.length < 15) return false + const [llmResult, sttResult] = await Promise.all([llmValidation, sttValidation]); - if (provider === "openai") { - if (!apiKey.match(/^[A-Za-z0-9_-]+$/)) return false - - try { - console.log("Validating OpenAI API key...") - - const response = await fetch("https://api.openai.com/v1/models", { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - }) - - if (response.ok) { - const data = await response.json() - - const hasGPTModels = data.data && data.data.some((m) => m.id.startsWith("gpt-")) - if (hasGPTModels) { - console.log("OpenAI API key validation successful") - return true - } else { - console.log("API key valid but no GPT models available") - return false - } - } else { - const errorData = await response.json().catch(() => ({})) - console.log("API key validation failed:", response.status, errorData.error?.message || "Unknown error") - return false - } - } catch (error) { - console.error("API key validation network error:", error) - return apiKey.length >= 20 // Fallback for network issues - } - } else if (provider === "gemini") { - // Gemini API keys typically start with 'AIza' - if (!apiKey.match(/^[A-Za-z0-9_-]+$/)) return false - - try { - console.log("Validating Gemini API key...") - - // Test the API key with a simple models list request - const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`) - - if (response.ok) { - const data = await response.json() - if (data.models && data.models.length > 0) { - console.log("Gemini API key validation successful") - return true - } - } - - console.log("Gemini API key validation failed") - return false - } catch (error) { - console.error("Gemini API key validation network error:", error) - return apiKey.length >= 20 // Fallback - } - } else if (provider === "anthropic") { - // Anthropic API keys typically start with 'sk-ant-' - if (!apiKey.startsWith("sk-ant-") || !apiKey.match(/^[A-Za-z0-9_-]+$/)) return false - - try { - console.log("Validating Anthropic API key...") - - // Test the API key with a simple request - const response = await fetch("https://api.anthropic.com/v1/messages", { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-api-key": apiKey, - "anthropic-version": "2023-06-01", - }, - body: JSON.stringify({ - model: "claude-3-haiku-20240307", - max_tokens: 10, - messages: [{ role: "user", content: "Hi" }], - }), - }) - - if (response.ok || response.status === 400) { - // 400 is also acceptable as it means the API key is valid but request format might be wrong - console.log("Anthropic API key validation successful") - return true - } - - console.log("Anthropic API key validation failed:", response.status) - return false - } catch (error) { - console.error("Anthropic API key validation network error:", error) - return apiKey.length >= 20 // Fallback - } + if (llmResult.success && sttResult.success) { + console.log('[ApiKeyHeader] handleSubmit: Both LLM and STT keys are valid.'); + this.startSlideOutAnimation(); + } else { + console.log('[ApiKeyHeader] handleSubmit: Validation failed.'); + let errorParts = []; + if (!llmResult.success) errorParts.push(`LLM Key: ${llmResult.error || 'Invalid'}`); + if (!sttResult.success) errorParts.push(`STT Key: ${sttResult.error || 'Invalid'}`); + this.errorMessage = errorParts.join(' | '); } - return false - } + this.isLoading = false; + this.requestUpdate(); +} +//////// after_modelStateService //////// + startSlideOutAnimation() { + console.log('[ApiKeyHeader] startSlideOutAnimation: Starting slide out animation.'); this.classList.add("sliding-out") } @@ -567,25 +488,18 @@ export class ApiKeyHeader extends LitElement { } } + + //////// after_modelStateService //////// handleAnimationEnd(e) { - if (e.target !== this) return - - if (this.classList.contains("sliding-out")) { - this.classList.remove("sliding-out") - this.classList.add("hidden") - - if (this.validatedApiKey) { - if (window.require) { - window.require("electron").ipcRenderer.invoke("api-key-validated", { - apiKey: this.validatedApiKey, - provider: this.validatedProvider || "openai", - }) - } - this.validatedApiKey = null - this.validatedProvider = null - } - } + if (e.target !== this || !this.classList.contains('sliding-out')) return; + this.classList.remove("sliding-out"); + this.classList.add("hidden"); + window.require('electron').ipcRenderer.invoke('get-current-user').then(userState => { + console.log('[ApiKeyHeader] handleAnimationEnd: User state updated:', userState); + this.stateUpdateCallback?.(userState); + }); } +//////// after_modelStateService //////// connectedCallback() { super.connectedCallback() @@ -598,64 +512,40 @@ export class ApiKeyHeader extends LitElement { } render() { - const isButtonDisabled = this.isLoading || !this.apiKey || !this.apiKey.trim() - console.log("Rendering with provider:", this.selectedProvider) + const isButtonDisabled = this.isLoading || !this.llmApiKey.trim() || !this.sttApiKey.trim(); return html` -
- -

Choose how to power your AI

+
+

Enter Your API Keys

-
-
${this.errorMessage}
-
Select AI Provider:
- this.llmProvider = e.target.value} ?disabled=${this.isLoading}> + ${this.providers.llm.map(p => html``)} - (this.errorMessage = "")} - ?disabled=${this.isLoading} - autocomplete="off" - spellcheck="false" - tabindex="0" - /> + this.llmApiKey = e.target.value} ?disabled=${this.isLoading}> +
- - -
or
- - +
+
+ + this.sttApiKey = e.target.value} ?disabled=${this.isLoading}>
- ` - } + +
${this.errorMessage}
+ + +
or
+ +
+ `; +} } customElements.define("apikey-header", ApiKeyHeader) diff --git a/src/app/HeaderController.js b/src/app/HeaderController.js index 26313f1..e2c5fe8 100644 --- a/src/app/HeaderController.js +++ b/src/app/HeaderController.js @@ -15,7 +15,11 @@ class HeaderTransitionManager { * @param {'apikey'|'main'|'permission'} type */ this.ensureHeader = (type) => { - if (this.currentHeaderType === type) return; + console.log('[HeaderController] ensureHeader: Ensuring header of type:', type); + if (this.currentHeaderType === type) { + console.log('[HeaderController] ensureHeader: Header of type:', type, 'already exists.'); + return; + } this.headerContainer.innerHTML = ''; @@ -26,6 +30,7 @@ class HeaderTransitionManager { // Create new header element if (type === 'apikey') { this.apiKeyHeader = document.createElement('apikey-header'); + this.apiKeyHeader.stateUpdateCallback = (userState) => this.handleStateUpdate(userState); this.headerContainer.appendChild(this.apiKeyHeader); } else if (type === 'permission') { this.permissionHeader = document.createElement('permission-setup'); @@ -60,6 +65,11 @@ class HeaderTransitionManager { this.apiKeyHeader.isLoading = false; } }); + ipcRenderer.on('force-show-apikey-header', async () => { + console.log('[HeaderController] Received broadcast to show apikey header. Switching now.'); + await this._resizeForApiKey(); + this.ensureHeader('apikey'); + }); } } @@ -83,26 +93,30 @@ class HeaderTransitionManager { } } - async handleStateUpdate(userState) { - const { isLoggedIn, hasApiKey } = userState; - if (isLoggedIn) { - // Firebase user: Check permissions, then show Main or Permission header - const permissionResult = await this.checkPermissions(); - if (permissionResult.success) { - this.transitionToMainHeader(); + //////// after_modelStateService //////// + async handleStateUpdate(userState) { + const { ipcRenderer } = window.require('electron'); + const isConfigured = await ipcRenderer.invoke('model:are-providers-configured'); + + if (isConfigured) { + const { isLoggedIn } = userState; + if (isLoggedIn) { + const permissionResult = await this.checkPermissions(); + if (permissionResult.success) { + this.transitionToMainHeader(); + } else { + this.transitionToPermissionHeader(); + } } else { - this.transitionToPermissionHeader(); + this.transitionToMainHeader(); } - } else if (hasApiKey) { - // API Key only user: Skip permission check, go directly to Main - this.transitionToMainHeader(); } else { - // No auth at all await this._resizeForApiKey(); this.ensureHeader('apikey'); } } + //////// after_modelStateService //////// async transitionToPermissionHeader() { // Prevent duplicate transitions @@ -159,7 +173,7 @@ class HeaderTransitionManager { if (!window.require) return; return window .require('electron') - .ipcRenderer.invoke('resize-header-window', { width: 285, height: 300 }) + .ipcRenderer.invoke('resize-header-window', { width: 350, height: 300 }) .catch(() => {}); } diff --git a/src/common/ai/factory.js b/src/common/ai/factory.js index 8d0d6b5..ea86ff0 100644 --- a/src/common/ai/factory.js +++ b/src/common/ai/factory.js @@ -1,68 +1,121 @@ -const providers = { - openai: require("./providers/openai"), - gemini: require("./providers/gemini"), - anthropic: require("./providers/anthropic"), - // 추가 provider는 여기에 등록 -} +// factory.js /** - * Creates an STT session based on provider - * @param {string} provider - Provider name ('openai', 'gemini', etc.) - * @param {object} opts - Configuration options (apiKey, language, callbacks, etc.) - * @returns {Promise} STT session object with sendRealtimeInput and close methods + * @typedef {object} ModelOption + * @property {string} id + * @property {string} name */ + +/** + * @typedef {object} Provider + * @property {string} name + * @property {() => any} handler + * @property {ModelOption[]} llmModels + * @property {ModelOption[]} sttModels + */ + +/** + * @type {Object.} + */ +const PROVIDERS = { + 'openai': { + name: 'OpenAI', + handler: () => require("./providers/openai"), + llmModels: [ + { id: 'gpt-4.1', name: 'GPT-4.1' }, + ], + sttModels: [ + { id: 'gpt-4o-mini-transcribe', name: 'GPT-4o Mini Transcribe' } + ], + }, + + 'openai-glass': { + name: 'OpenAI (Glass)', + handler: () => require("./providers/openai"), + llmModels: [ + { id: 'gpt-4.1-glass', name: 'GPT-4.1 (glass)' }, + ], + sttModels: [ + { id: 'gpt-4o-mini-transcribe-glass', name: 'GPT-4o Mini Transcribe (glass)' } + ], + }, + 'gemini': { + name: 'Gemini', + handler: () => require("./providers/gemini"), + llmModels: [ + { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash' }, + ], + sttModels: [ + { id: 'gemini-live-2.5-flash-preview', name: 'Gemini Live 2.5 Flash' } + ], + }, + 'anthropic': { + name: 'Anthropic', + handler: () => require("./providers/anthropic"), + llmModels: [ + { id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet' }, + ], + sttModels: [], + }, +}; + +function sanitizeModelId(model) { + return (typeof model === 'string') ? model.replace(/-glass$/, '') : model; +} + function createSTT(provider, opts) { - if (!providers[provider]?.createSTT) { - throw new Error(`STT not supported for provider: ${provider}`) + if (provider === 'openai-glass') provider = 'openai'; + + const handler = PROVIDERS[provider]?.handler(); + if (!handler?.createSTT) { + throw new Error(`STT not supported for provider: ${provider}`); } - return providers[provider].createSTT(opts) + if (opts && opts.model) { + opts = { ...opts, model: sanitizeModelId(opts.model) }; + } + return handler.createSTT(opts); } -/** - * Creates an LLM instance based on provider - * @param {string} provider - Provider name ('openai', 'gemini', etc.) - * @param {object} opts - Configuration options (apiKey, model, temperature, etc.) - * @returns {object} LLM instance with generateContent method - */ function createLLM(provider, opts) { - if (!providers[provider]?.createLLM) { - throw new Error(`LLM not supported for provider: ${provider}`) + if (provider === 'openai-glass') provider = 'openai'; + + const handler = PROVIDERS[provider]?.handler(); + if (!handler?.createLLM) { + throw new Error(`LLM not supported for provider: ${provider}`); } - return providers[provider].createLLM(opts) + if (opts && opts.model) { + opts = { ...opts, model: sanitizeModelId(opts.model) }; + } + return handler.createLLM(opts); } -/** - * Creates a streaming LLM instance based on provider - * @param {string} provider - Provider name ('openai', 'gemini', etc.) - * @param {object} opts - Configuration options (apiKey, model, temperature, etc.) - * @returns {object} Streaming LLM instance - */ function createStreamingLLM(provider, opts) { - if (!providers[provider]?.createStreamingLLM) { - throw new Error(`Streaming LLM not supported for provider: ${provider}`) + if (provider === 'openai-glass') provider = 'openai'; + + const handler = PROVIDERS[provider]?.handler(); + if (!handler?.createStreamingLLM) { + throw new Error(`Streaming LLM not supported for provider: ${provider}`); } - return providers[provider].createStreamingLLM(opts) + if (opts && opts.model) { + opts = { ...opts, model: sanitizeModelId(opts.model) }; + } + return handler.createStreamingLLM(opts); } -/** - * Gets list of available providers - * @returns {object} Object with stt and llm arrays - */ function getAvailableProviders() { - const sttProviders = [] - const llmProviders = [] - - for (const [name, provider] of Object.entries(providers)) { - if (provider.createSTT) sttProviders.push(name) - if (provider.createLLM) llmProviders.push(name) + const stt = []; + const llm = []; + for (const [id, provider] of Object.entries(PROVIDERS)) { + if (provider.sttModels.length > 0) stt.push(id); + if (provider.llmModels.length > 0) llm.push(id); } - - return { stt: sttProviders, llm: llmProviders } + return { stt: [...new Set(stt)], llm: [...new Set(llm)] }; } module.exports = { + PROVIDERS, createSTT, createLLM, createStreamingLLM, getAvailableProviders, -} +}; \ No newline at end of file diff --git a/src/common/services/authService.js b/src/common/services/authService.js index f41f87e..a664ba1 100644 --- a/src/common/services/authService.js +++ b/src/common/services/authService.js @@ -36,7 +36,6 @@ class AuthService { this.currentUserId = 'default_user'; this.currentUserMode = 'local'; // 'local' or 'firebase' this.currentUser = null; - this.hasApiKey = false; // Add a flag for API key status this.isInitialized = false; } @@ -53,20 +52,18 @@ class AuthService { this.currentUser = user; this.currentUserId = user.uid; this.currentUserMode = 'firebase'; - this.hasApiKey = false; // Optimistically assume no key yet - - // Broadcast immediately to make UI feel responsive - this.broadcastUserState(); // Start background task to fetch and save virtual key (async () => { try { const idToken = await user.getIdToken(true); const virtualKey = await getVirtualKeyByEmail(user.email, idToken); - await userRepository.saveApiKey(virtualKey, user.uid, 'openai'); - console.log(`[AuthService] BG: Virtual key for ${user.email} has been saved.`); - // Now update the key status, which will trigger another broadcast - await this.updateApiKeyStatus(); + + if (global.modelStateService) { + global.modelStateService.setFirebaseVirtualKey(virtualKey); + } + console.log(`[AuthService] BG: Virtual key for ${user.email} has been processed.`); + } catch (error) { console.error('[AuthService] BG: Failed to fetch or save virtual key:', error); } @@ -74,23 +71,20 @@ class AuthService { } else { // User signed OUT - console.log(`[AuthService] Firebase user signed out.`); + console.log(`[AuthService] No Firebase user.`); if (previousUser) { console.log(`[AuthService] Clearing API key for logged-out user: ${previousUser.uid}`); - await userRepository.saveApiKey(null, previousUser.uid); + if (global.modelStateService) { + global.modelStateService.setFirebaseVirtualKey(null); + } } this.currentUser = null; this.currentUserId = 'default_user'; this.currentUserMode = 'local'; - // Update API key status (e.g., if a local key for default_user exists) - // This will also broadcast the final logged-out state. - await this.updateApiKeyStatus(); } + this.broadcastUserState(); }); - // Check for initial API key state - this.updateApiKeyStatus(); - this.isInitialized = true; console.log('[AuthService] Initialized and attached to Firebase Auth state.'); } @@ -129,23 +123,6 @@ class AuthService { }); } - /** - * Updates the internal API key status from the repository and broadcasts if changed. - */ - async updateApiKeyStatus() { - try { - const user = await userRepository.getById(this.currentUserId); - const newStatus = !!(user && user.api_key); - if (this.hasApiKey !== newStatus) { - console.log(`[AuthService] API key status changed to: ${newStatus}`); - this.hasApiKey = newStatus; - this.broadcastUserState(); - } - } catch (error) { - console.error('[AuthService] Error checking API key status:', error); - this.hasApiKey = false; - } - } getCurrentUserId() { return this.currentUserId; @@ -161,7 +138,9 @@ class AuthService { displayName: this.currentUser.displayName, mode: 'firebase', isLoggedIn: true, - hasApiKey: this.hasApiKey // Always true for firebase users, but good practice + //////// before_modelStateService //////// + // hasApiKey: this.hasApiKey // Always true for firebase users, but good practice + //////// before_modelStateService //////// }; } return { @@ -170,7 +149,9 @@ class AuthService { displayName: 'Default User', mode: 'local', isLoggedIn: false, - hasApiKey: this.hasApiKey + //////// before_modelStateService //////// + // hasApiKey: this.hasApiKey + //////// before_modelStateService //////// }; } } diff --git a/src/common/services/modelStateService.js b/src/common/services/modelStateService.js new file mode 100644 index 0000000..33de127 --- /dev/null +++ b/src/common/services/modelStateService.js @@ -0,0 +1,324 @@ +const Store = require('electron-store'); +const fetch = require('node-fetch'); +const { ipcMain, webContents } = require('electron'); +const { PROVIDERS } = require('../ai/factory'); + +class ModelStateService { + constructor(authService) { + this.authService = authService; + this.store = new Store({ name: 'pickle-glass-model-state' }); + this.state = {}; + } + + initialize() { + this._loadStateForCurrentUser(); + + this.setupIpcHandlers(); + console.log('[ModelStateService] Initialized.'); + } + + _logCurrentSelection() { + const llmModel = this.state.selectedModels.llm; + const sttModel = this.state.selectedModels.stt; + const llmProvider = this.getProviderForModel('llm', llmModel) || 'None'; + const sttProvider = this.getProviderForModel('stt', sttModel) || 'None'; + + console.log(`[ModelStateService] 🌟 Current Selection -> LLM: ${llmModel || 'None'} (Provider: ${llmProvider}), STT: ${sttModel || 'None'} (Provider: ${sttProvider})`); + } + + _autoSelectAvailableModels() { + console.log('[ModelStateService] Running auto-selection for models...'); + const types = ['llm', 'stt']; + + types.forEach(type => { + const currentModelId = this.state.selectedModels[type]; + let isCurrentModelValid = false; + + if (currentModelId) { + const provider = this.getProviderForModel(type, currentModelId); + if (provider && this.getApiKey(provider)) { + isCurrentModelValid = true; + } + } + + if (!isCurrentModelValid) { + console.log(`[ModelStateService] No valid ${type.toUpperCase()} model selected. Finding an alternative...`); + const availableModels = this.getAvailableModels(type); + if (availableModels.length > 0) { + this.state.selectedModels[type] = availableModels[0].id; + console.log(`[ModelStateService] Auto-selected ${type.toUpperCase()} model: ${availableModels[0].id}`); + } else { + this.state.selectedModels[type] = null; + } + } + }); + } + + _loadStateForCurrentUser() { + const userId = this.authService.getCurrentUserId(); + const initialApiKeys = Object.keys(PROVIDERS).reduce((acc, key) => { + acc[key] = null; + 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}`); + this._autoSelectAvailableModels(); + this._saveState(); + this._logCurrentSelection(); + } + + + _saveState() { + const userId = this.authService.getCurrentUserId(); + this.store.set(`users.${userId}`, this.state); + console.log(`[ModelStateService] State saved for user: ${userId}`); + this._logCurrentSelection(); + } + + async validateApiKey(provider, key) { + if (!key || key.trim() === '') { + return { success: false, error: 'API key cannot be empty.' }; + } + + let validationUrl, headers; + const body = undefined; + + switch (provider) { + 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 }; + } + default: + return { success: false, error: 'Unknown provider.' }; + } + + try { + const response = await fetch(validationUrl, { headers, body }); + if (response.ok) { + console.log(`[ModelStateService] API key for ${provider} is valid.`); + this.setApiKey(provider, key); + return { success: true }; + } else { + const errorData = await response.json().catch(() => ({})); + 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 }; + } + } catch (error) { + console.error(`[ModelStateService] Network error during ${provider} key validation:`, error); + return { success: false, error: 'A network error occurred during validation.' }; + } + } + + setFirebaseVirtualKey(virtualKey) { + console.log(`[ModelStateService] Setting Firebase virtual key (for openai-glass).`); + this.state.apiKeys['openai-glass'] = virtualKey; + + const llmModels = PROVIDERS['openai-glass']?.llmModels; + const sttModels = PROVIDERS['openai-glass']?.sttModels; + + if (!this.state.selectedModels.llm && llmModels?.length > 0) { + this.state.selectedModels.llm = llmModels[0].id; + } + if (!this.state.selectedModels.stt && sttModels?.length > 0) { + this.state.selectedModels.stt = sttModels[0].id; + } + this._autoSelectAvailableModels(); + this._saveState(); + this._logCurrentSelection(); + } + + setApiKey(provider, key) { + if (provider in this.state.apiKeys) { + this.state.apiKeys[provider] = key; + + const llmModels = PROVIDERS[provider]?.llmModels; + const sttModels = PROVIDERS[provider]?.sttModels; + + if (!this.state.selectedModels.llm && llmModels?.length > 0) { + this.state.selectedModels.llm = llmModels[0].id; + } + if (!this.state.selectedModels.stt && sttModels?.length > 0) { + this.state.selectedModels.stt = sttModels[0].id; + } + this._saveState(); + this._logCurrentSelection(); + return true; + } + return false; + } + + getApiKey(provider) { + return this.state.apiKeys[provider] || null; + } + + getAllApiKeys() { + const { 'openai-glass': _, ...displayKeys } = this.state.apiKeys; + return displayKeys; + } + + removeApiKey(provider) { + if (provider in this.state.apiKeys) { + this.state.apiKeys[provider] = null; + const llmProvider = this.getProviderForModel('llm', this.state.selectedModels.llm); + if (llmProvider === provider) this.state.selectedModels.llm = null; + + const sttProvider = this.getProviderForModel('stt', this.state.selectedModels.stt); + if (sttProvider === provider) this.state.selectedModels.stt = null; + + this._autoSelectAvailableModels(); + this._saveState(); + this._logCurrentSelection(); + return true; + } + return false; + } + + getProviderForModel(type, modelId) { + if (!modelId) return null; + for (const providerId in PROVIDERS) { + const models = type === 'llm' ? PROVIDERS[providerId].llmModels : PROVIDERS[providerId].sttModels; + if (models.some(m => m.id === modelId)) { + return providerId; + } + } + return null; + } + + getCurrentProvider(type) { + const selectedModel = this.state.selectedModels[type]; + return this.getProviderForModel(type, selectedModel); + } + + isLoggedInWithFirebase() { + return this.authService.getCurrentUser().isLoggedIn; + } + + areProvidersConfigured() { + if (this.isLoggedInWithFirebase()) return true; + + // LLM과 STT 모델을 제공하는 Provider 중 하나라도 API 키가 설정되었는지 확인 + const hasLlmKey = Object.entries(this.state.apiKeys).some(([provider, key]) => key && PROVIDERS[provider]?.llmModels.length > 0); + const hasSttKey = Object.entries(this.state.apiKeys).some(([provider, key]) => key && PROVIDERS[provider]?.sttModels.length > 0); + + return hasLlmKey && hasSttKey; + } + + + getAvailableModels(type) { + const available = []; + const modelList = type === 'llm' ? 'llmModels' : 'sttModels'; + + Object.entries(this.state.apiKeys).forEach(([providerId, key]) => { + if (key && PROVIDERS[providerId]?.[modelList]) { + available.push(...PROVIDERS[providerId][modelList]); + } + }); + return [...new Map(available.map(item => [item.id, item])).values()]; + } + + getSelectedModels() { + return this.state.selectedModels; + } + + setSelectedModel(type, modelId) { + const provider = this.getProviderForModel(type, modelId); + if (provider && this.state.apiKeys[provider]) { + this.state.selectedModels[type] = modelId; + this._saveState(); + return true; + } + return false; + } + + /** + * + * @param {('llm' | 'stt')} type + * @returns {{provider: string, model: string, apiKey: string} | null} + */ + getCurrentModelInfo(type) { + this._logCurrentSelection(); + const model = this.state.selectedModels[type]; + if (!model) { + return null; + } + + const provider = this.getProviderForModel(type, model); + if (!provider) { + return null; + } + + const apiKey = this.getApiKey(provider); + return { provider, model, apiKey }; + } + + setupIpcHandlers() { + ipcMain.handle('model:validate-key', (e, { provider, key }) => this.validateApiKey(provider, key)); + ipcMain.handle('model:get-all-keys', () => this.getAllApiKeys()); + ipcMain.handle('model:set-api-key', (e, { provider, key }) => this.setApiKey(provider, key)); + ipcMain.handle('model:remove-api-key', (e, { provider }) => { + const success = this.removeApiKey(provider); + if (success) { + const selectedModels = this.getSelectedModels(); + if (!selectedModels.llm || !selectedModels.stt) { + webContents.getAllWebContents().forEach(wc => { + wc.send('force-show-apikey-header'); + }); + } + } + return success; + }); + ipcMain.handle('model:get-selected-models', () => this.getSelectedModels()); + ipcMain.handle('model:set-selected-model', (e, { type, modelId }) => this.setSelectedModel(type, modelId)); + ipcMain.handle('model:get-available-models', (e, { type }) => this.getAvailableModels(type)); + ipcMain.handle('model:are-providers-configured', () => this.areProvidersConfigured()); + ipcMain.handle('model:get-current-model-info', (e, { type }) => this.getCurrentModelInfo(type)); + + ipcMain.handle('model:get-provider-config', () => { + const serializableProviders = {}; + for (const key in PROVIDERS) { + const { handler, ...rest } = PROVIDERS[key]; + serializableProviders[key] = rest; + } + return serializableProviders; + }); + } +} + +module.exports = ModelStateService; \ No newline at end of file diff --git a/src/electron/windowManager.js b/src/electron/windowManager.js index 4b540f6..32441b7 100644 --- a/src/electron/windowManager.js +++ b/src/electron/windowManager.js @@ -971,13 +971,15 @@ function setupIpcHandlers(movementManager) { console.log('[WindowManager] Received request to log out.'); await authService.signOut(); - await setApiKey(null); + //////// before_modelStateService //////// + // await setApiKey(null); - windowPool.forEach(win => { - if (win && !win.isDestroyed()) { - win.webContents.send('api-key-removed'); - } - }); + // windowPool.forEach(win => { + // if (win && !win.isDestroyed()) { + // win.webContents.send('api-key-removed'); + // } + // }); + //////// before_modelStateService //////// }); ipcMain.handle('check-system-permissions', async () => { @@ -1112,95 +1114,150 @@ function setupIpcHandlers(movementManager) { } -async function setApiKey(apiKey, provider = 'openai') { - console.log('[WindowManager] Persisting API key and provider to DB'); +//////// before_modelStateService //////// +// async function setApiKey(apiKey, provider = 'openai') { +// console.log('[WindowManager] Persisting API key and provider to DB'); - try { - await userRepository.saveApiKey(apiKey, authService.getCurrentUserId(), provider); - console.log('[WindowManager] API key and provider saved to SQLite'); +// try { +// await userRepository.saveApiKey(apiKey, authService.getCurrentUserId(), provider); +// console.log('[WindowManager] API key and provider saved to SQLite'); - // Notify authService that the key status may have changed - await authService.updateApiKeyStatus(); +// // Notify authService that the key status may have changed +// await authService.updateApiKeyStatus(); - } catch (err) { - console.error('[WindowManager] Failed to save API key to SQLite:', err); - } +// } catch (err) { +// console.error('[WindowManager] Failed to save API key to SQLite:', err); +// } - windowPool.forEach(win => { - if (win && !win.isDestroyed()) { - const js = apiKey ? ` - localStorage.setItem('openai_api_key', ${JSON.stringify(apiKey)}); - localStorage.setItem('ai_provider', ${JSON.stringify(provider)}); - ` : ` - localStorage.removeItem('openai_api_key'); - localStorage.removeItem('ai_provider'); - `; - win.webContents.executeJavaScript(js).catch(() => {}); - } - }); -} +// windowPool.forEach(win => { +// if (win && !win.isDestroyed()) { +// const js = apiKey ? ` +// localStorage.setItem('openai_api_key', ${JSON.stringify(apiKey)}); +// localStorage.setItem('ai_provider', ${JSON.stringify(provider)}); +// ` : ` +// localStorage.removeItem('openai_api_key'); +// localStorage.removeItem('ai_provider'); +// `; +// win.webContents.executeJavaScript(js).catch(() => {}); +// } +// }); +// } + +// async function getStoredApiKey() { +// const userId = authService.getCurrentUserId(); +// if (!userId) return null; +// const user = await userRepository.getById(userId); +// return user?.api_key || null; +// } + +// async function getStoredProvider() { +// const userId = authService.getCurrentUserId(); +// if (!userId) return 'openai'; +// const user = await userRepository.getById(userId); +// return user?.provider || 'openai'; +// } + +// function setupApiKeyIPC() { +// const { ipcMain } = require('electron'); + +// // Both handlers now do the same thing: fetch the key from the source of truth. +// ipcMain.handle('get-stored-api-key', getStoredApiKey); + +// ipcMain.handle('api-key-validated', async (event, data) => { +// console.log('[WindowManager] API key validation completed, saving...'); + +// // Support both old format (string) and new format (object) +// const apiKey = typeof data === 'string' ? data : data.apiKey; +// const provider = typeof data === 'string' ? 'openai' : (data.provider || 'openai'); + +// await setApiKey(apiKey, provider); + +// windowPool.forEach((win, name) => { +// if (win && !win.isDestroyed()) { +// win.webContents.send('api-key-validated', { apiKey, provider }); +// } +// }); + +// return { success: true }; +// }); + +// ipcMain.handle('remove-api-key', async () => { +// console.log('[WindowManager] API key removal requested'); +// await setApiKey(null); + +// windowPool.forEach((win, name) => { +// if (win && !win.isDestroyed()) { +// win.webContents.send('api-key-removed'); +// } +// }); + +// const settingsWindow = windowPool.get('settings'); +// if (settingsWindow && settingsWindow.isVisible()) { +// settingsWindow.hide(); +// console.log('[WindowManager] Settings window hidden after clearing API key.'); +// } + +// return { success: true }; +// }); + +// ipcMain.handle('get-ai-provider', getStoredProvider); + +// console.log('[WindowManager] API key related IPC handlers registered (SQLite-backed)'); +// } +//////// before_modelStateService //////// + + + + +//////// after_modelStateService //////// async function getStoredApiKey() { - const userId = authService.getCurrentUserId(); - if (!userId) return null; - const user = await userRepository.getById(userId); - return user?.api_key || null; + if (global.modelStateService) { + const provider = await getStoredProvider(); + return global.modelStateService.getApiKey(provider); + } + return null; // Fallback } async function getStoredProvider() { - const userId = authService.getCurrentUserId(); - if (!userId) return 'openai'; - const user = await userRepository.getById(userId); - return user?.provider || 'openai'; + if (global.modelStateService) { + return global.modelStateService.getCurrentProvider('llm'); + } + return 'openai'; // Fallback +} + +/** + * 렌더러에서 요청한 타입('llm' 또는 'stt')에 대한 모델 정보를 반환합니다. + * @param {IpcMainInvokeEvent} event - 일렉트론 IPC 이벤트 객체 + * @param {{type: 'llm' | 'stt'}} { type } - 요청할 모델 타입 + */ +async function getCurrentModelInfo(event, { type }) { + if (global.modelStateService && (type === 'llm' || type === 'stt')) { + return global.modelStateService.getCurrentModelInfo(type); + } + return null; // 서비스가 없거나 유효하지 않은 타입일 경우 null 반환 } function setupApiKeyIPC() { const { ipcMain } = require('electron'); - // Both handlers now do the same thing: fetch the key from the source of truth. ipcMain.handle('get-stored-api-key', getStoredApiKey); + ipcMain.handle('get-ai-provider', getStoredProvider); + ipcMain.handle('get-current-model-info', getCurrentModelInfo); ipcMain.handle('api-key-validated', async (event, data) => { - console.log('[WindowManager] API key validation completed, saving...'); - - // Support both old format (string) and new format (object) - const apiKey = typeof data === 'string' ? data : data.apiKey; - const provider = typeof data === 'string' ? 'openai' : (data.provider || 'openai'); - - await setApiKey(apiKey, provider); - - windowPool.forEach((win, name) => { - if (win && !win.isDestroyed()) { - win.webContents.send('api-key-validated', { apiKey, provider }); - } - }); - + console.warn("[DEPRECATED] 'api-key-validated' IPC was called. This logic is now handled by 'model:validate-key'."); return { success: true }; }); ipcMain.handle('remove-api-key', async () => { - console.log('[WindowManager] API key removal requested'); - await setApiKey(null); - - windowPool.forEach((win, name) => { - if (win && !win.isDestroyed()) { - win.webContents.send('api-key-removed'); - } - }); - - const settingsWindow = windowPool.get('settings'); - if (settingsWindow && settingsWindow.isVisible()) { - settingsWindow.hide(); - console.log('[WindowManager] Settings window hidden after clearing API key.'); - } - + console.warn("[DEPRECATED] 'remove-api-key' IPC was called. This is now handled by 'model:remove-api-key'."); return { success: true }; }); - ipcMain.handle('get-ai-provider', getStoredProvider); - - console.log('[WindowManager] API key related IPC handlers registered (SQLite-backed)'); + console.log('[WindowManager] API key related IPC handlers have been updated for ModelStateService.'); } +//////// after_modelStateService //////// function getDefaultKeybinds() { @@ -1222,7 +1279,7 @@ function getDefaultKeybinds() { } function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementManager) { - console.log('Updating global shortcuts with:', keybinds); + // console.log('Updating global shortcuts with:', keybinds); // Unregister all existing shortcuts globalShortcut.unregisterAll(); @@ -1276,7 +1333,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan movementManager.moveStep(direction); } }); - console.log(`Registered global shortcut: ${key} -> ${direction}`); + // console.log(`Registered global shortcut: ${key} -> ${direction}`); } catch (error) { console.error(`Failed to register ${key}:`, error); } @@ -1316,7 +1373,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan } mainWindow.webContents.send('click-through-toggled', mouseEventsIgnored); }); - console.log(`Registered toggleClickThrough: ${keybinds.toggleClickThrough}`); + // console.log(`Registered toggleClickThrough: ${keybinds.toggleClickThrough}`); } catch (error) { console.error(`Failed to register toggleClickThrough (${keybinds.toggleClickThrough}):`, error); } @@ -1352,7 +1409,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan } } }); - console.log(`Registered Ask shortcut (nextStep): ${keybinds.nextStep}`); + // console.log(`Registered Ask shortcut (nextStep): ${keybinds.nextStep}`); } catch (error) { console.error(`Failed to register Ask shortcut (${keybinds.nextStep}):`, error); } @@ -1370,7 +1427,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan } `); }); - console.log(`Registered manualScreenshot: ${keybinds.manualScreenshot}`); + // console.log(`Registered manualScreenshot: ${keybinds.manualScreenshot}`); } catch (error) { console.error(`Failed to register manualScreenshot (${keybinds.manualScreenshot}):`, error); } @@ -1382,7 +1439,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan console.log('Previous response shortcut triggered'); sendToRenderer('navigate-previous-response'); }); - console.log(`Registered previousResponse: ${keybinds.previousResponse}`); + // console.log(`Registered previousResponse: ${keybinds.previousResponse}`); } catch (error) { console.error(`Failed to register previousResponse (${keybinds.previousResponse}):`, error); } @@ -1394,7 +1451,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan console.log('Next response shortcut triggered'); sendToRenderer('navigate-next-response'); }); - console.log(`Registered nextResponse: ${keybinds.nextResponse}`); + // console.log(`Registered nextResponse: ${keybinds.nextResponse}`); } catch (error) { console.error(`Failed to register nextResponse (${keybinds.nextResponse}):`, error); } @@ -1406,7 +1463,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan console.log('Scroll up shortcut triggered'); sendToRenderer('scroll-response-up'); }); - console.log(`Registered scrollUp: ${keybinds.scrollUp}`); + // console.log(`Registered scrollUp: ${keybinds.scrollUp}`); } catch (error) { console.error(`Failed to register scrollUp (${keybinds.scrollUp}):`, error); } @@ -1418,7 +1475,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan console.log('Scroll down shortcut triggered'); sendToRenderer('scroll-response-down'); }); - console.log(`Registered scrollDown: ${keybinds.scrollDown}`); + // console.log(`Registered scrollDown: ${keybinds.scrollDown}`); } catch (error) { console.error(`Failed to register scrollDown (${keybinds.scrollDown}):`, error); } @@ -1495,8 +1552,11 @@ module.exports = { createWindows, windowPool, fixedYPosition, - setApiKey, + //////// before_modelStateService //////// + // setApiKey, + //////// before_modelStateService //////// getStoredApiKey, getStoredProvider, + getCurrentModelInfo, captureScreenshot, }; \ No newline at end of file diff --git a/src/features/ask/askService.js b/src/features/ask/askService.js index 33428be..b59b65d 100644 --- a/src/features/ask/askService.js +++ b/src/features/ask/askService.js @@ -1,6 +1,6 @@ const { ipcMain, BrowserWindow } = require('electron'); const { createStreamingLLM } = require('../../common/ai/factory'); -const { getStoredApiKey, getStoredProvider, windowPool, captureScreenshot } = require('../../electron/windowManager'); +const { getStoredApiKey, getStoredProvider, getCurrentModelInfo, windowPool, captureScreenshot } = require('../../electron/windowManager'); const authService = require('../../common/services/authService'); const sessionRepository = require('../../common/repositories/session'); const askRepository = require('./repositories'); @@ -31,6 +31,12 @@ async function sendMessage(userPrompt) { try { console.log(`[AskService] 🤖 Processing message: ${userPrompt.substring(0, 50)}...`); + const modelInfo = await getCurrentModelInfo(null, { type: 'llm' }); + if (!modelInfo || !modelInfo.apiKey) { + throw new Error('AI model or API key not configured.'); + } + console.log(`[AskService] Using model: ${modelInfo.model} for provider: ${modelInfo.provider}`); + const screenshotResult = await captureScreenshot({ quality: 'medium' }); const screenshotBase64 = screenshotResult.success ? screenshotResult.base64 : null; @@ -39,10 +45,6 @@ async function sendMessage(userPrompt) { const systemPrompt = getSystemPrompt('pickle_glass_analysis', conversationHistory, false); - const API_KEY = await getStoredApiKey(); - if (!API_KEY) { - throw new Error('No API key found'); - } const messages = [ { role: 'system', content: systemPrompt }, @@ -61,36 +63,13 @@ async function sendMessage(userPrompt) { }); } - const provider = await getStoredProvider(); - const { isLoggedIn } = authService.getCurrentUser(); - - console.log(`[AskService] 🚀 Sending request to ${provider} AI...`); - - // FIX: Proper model selection for each provider - let model; - switch (provider) { - case 'openai': - model = 'gpt-4o'; // Use a valid OpenAI model - break; - case 'gemini': - model = 'gemini-2.0-flash-exp'; // Use a valid Gemini model - break; - case 'anthropic': - model = 'claude-3-5-sonnet-20241022'; // Use a valid Claude model - break; - default: - model = 'gpt-4o'; // Default fallback - } - - console.log(`[AskService] Using model: ${model} for provider: ${provider}`); - - const streamingLLM = createStreamingLLM(provider, { - apiKey: API_KEY, - model: model, + const streamingLLM = createStreamingLLM(modelInfo.provider, { + apiKey: modelInfo.apiKey, + model: modelInfo.model, temperature: 0.7, maxTokens: 2048, - usePortkey: provider === 'openai' && isLoggedIn, - portkeyVirtualKey: isLoggedIn ? API_KEY : undefined + usePortkey: modelInfo.provider === 'openai-glass', + portkeyVirtualKey: modelInfo.provider === 'openai-glass' ? modelInfo.apiKey : undefined, }); const response = await streamingLLM.streamChat(messages); diff --git a/src/features/listen/stt/sttService.js b/src/features/listen/stt/sttService.js index 294bd74..3722549 100644 --- a/src/features/listen/stt/sttService.js +++ b/src/features/listen/stt/sttService.js @@ -1,7 +1,7 @@ const { BrowserWindow } = require('electron'); const { spawn } = require('child_process'); const { createSTT } = require('../../../common/ai/factory'); -const { getStoredApiKey, getStoredProvider } = require('../../../electron/windowManager'); +const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../electron/windowManager'); const COMPLETION_DEBOUNCE_MS = 2000; @@ -29,6 +29,8 @@ class SttService { // Callbacks this.onTranscriptionComplete = null; this.onStatusUpdate = null; + + this.modelInfo = null; } setCallbacks({ onTranscriptionComplete, onStatusUpdate }) { @@ -36,32 +38,32 @@ class SttService { this.onStatusUpdate = onStatusUpdate; } - async getApiKey() { - const storedKey = await getStoredApiKey(); - if (storedKey) { - console.log('[SttService] Using stored API key'); - return storedKey; - } + // async getApiKey() { + // const storedKey = await getStoredApiKey(); + // if (storedKey) { + // console.log('[SttService] Using stored API key'); + // return storedKey; + // } - const envKey = process.env.OPENAI_API_KEY; - if (envKey) { - console.log('[SttService] Using environment API key'); - return envKey; - } + // const envKey = process.env.OPENAI_API_KEY; + // if (envKey) { + // console.log('[SttService] Using environment API key'); + // return envKey; + // } - console.error('[SttService] No API key found in storage or environment'); - return null; - } + // console.error('[SttService] No API key found in storage or environment'); + // return null; + // } - async getAiProvider() { - try { - const { ipcRenderer } = require('electron'); - const provider = await ipcRenderer.invoke('get-ai-provider'); - return provider || 'openai'; - } catch (error) { - return getStoredProvider ? getStoredProvider() : 'openai'; - } - } + // async getAiProvider() { + // try { + // const { ipcRenderer } = require('electron'); + // const provider = await ipcRenderer.invoke('get-ai-provider'); + // return provider || 'openai'; + // } catch (error) { + // return getStoredProvider ? getStoredProvider() : 'openai'; + // } + // } sendToRenderer(channel, data) { BrowserWindow.getAllWindows().forEach(win => { @@ -156,17 +158,25 @@ class SttService { async initializeSttSessions(language = 'en') { const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || language || 'en'; - const API_KEY = await this.getApiKey(); - if (!API_KEY) { - throw new Error('No API key available'); - } + // const API_KEY = await this.getApiKey(); + // if (!API_KEY) { + // throw new Error('No API key available'); + // } + // const provider = await this.getAiProvider(); - const provider = await this.getAiProvider(); - const isGemini = provider === 'gemini'; - console.log(`[SttService] Initializing STT for provider: ${provider}`); + const modelInfo = await getCurrentModelInfo(null, { type: 'stt' }); + if (!modelInfo || !modelInfo.apiKey) { + throw new Error('AI model or API key is not configured.'); + } + this.modelInfo = modelInfo; + console.log(`[SttService] Initializing STT for ${modelInfo.provider} using model ${modelInfo.model}`); + + + // const isGemini = modelInfo.provider === 'gemini'; + // console.log(`[SttService] Initializing STT for provider: ${modelInfo.provider}`); const handleMyMessage = message => { - if (isGemini) { + if (this.modelInfo.provider === 'gemini') { const text = message.serverContent?.inputTranscription?.text || ''; if (text && text.trim()) { const finalUtteranceText = text.trim().replace(//g, '').trim(); @@ -207,7 +217,7 @@ class SttService { }; const handleTheirMessage = message => { - if (isGemini) { + if (this.modelInfo.provider === 'gemini') { const text = message.serverContent?.inputTranscription?.text || ''; if (text && text.trim()) { const finalUtteranceText = text.trim().replace(//g, '').trim(); @@ -265,20 +275,20 @@ class SttService { }; // Determine auth options for providers that support it - const authService = require('../../../common/services/authService'); - const userState = authService.getCurrentUser(); - const loggedIn = userState.isLoggedIn; + // const authService = require('../../../common/services/authService'); + // const userState = authService.getCurrentUser(); + // const loggedIn = userState.isLoggedIn; const sttOptions = { - apiKey: API_KEY, + apiKey: this.modelInfo.apiKey, language: effectiveLanguage, - usePortkey: !isGemini && loggedIn, // Only OpenAI supports Portkey - portkeyVirtualKey: loggedIn ? API_KEY : undefined + usePortkey: this.modelInfo.provider === 'openai-glass', + portkeyVirtualKey: this.modelInfo.provider === 'openai-glass' ? this.modelInfo.apiKey : undefined, }; [this.mySttSession, this.theirSttSession] = await Promise.all([ - createSTT(provider, { ...sttOptions, callbacks: mySttConfig.callbacks }), - createSTT(provider, { ...sttOptions, callbacks: theirSttConfig.callbacks }), + createSTT(this.modelInfo.provider, { ...sttOptions, callbacks: mySttConfig.callbacks }), + createSTT(this.modelInfo.provider, { ...sttOptions, callbacks: theirSttConfig.callbacks }), ]); console.log('✅ Both STT sessions initialized successfully.'); @@ -286,14 +296,23 @@ class SttService { } async sendAudioContent(data, mimeType) { - const provider = await this.getAiProvider(); - const isGemini = provider === 'gemini'; + // const provider = await this.getAiProvider(); + // const isGemini = provider === 'gemini'; if (!this.mySttSession) { throw new Error('User STT session not active'); } - const payload = isGemini + let modelInfo = this.modelInfo; + if (!modelInfo) { + console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...'); + modelInfo = await getCurrentModelInfo(null, { type: 'stt' }); + } + if (!modelInfo) { + throw new Error('STT model info could not be retrieved.'); + } + + const payload = modelInfo.provider === 'gemini' ? { audio: { data, mimeType: mimeType || 'audio/pcm;rate=24000' } } : data; @@ -362,8 +381,17 @@ class SttService { let audioBuffer = Buffer.alloc(0); - const provider = await this.getAiProvider(); - const isGemini = provider === 'gemini'; + // const provider = await this.getAiProvider(); + // const isGemini = provider === 'gemini'; + + let modelInfo = this.modelInfo; + if (!modelInfo) { + console.warn('[SttService] modelInfo not found, fetching on-the-fly as a fallback...'); + modelInfo = await getCurrentModelInfo(null, { type: 'stt' }); + } + if (!modelInfo) { + throw new Error('STT model info could not be retrieved.'); + } this.systemAudioProc.stdout.on('data', async data => { audioBuffer = Buffer.concat([audioBuffer, data]); @@ -379,7 +407,7 @@ class SttService { if (this.theirSttSession) { try { - const payload = isGemini + const payload = modelInfo.provider === 'gemini' ? { audio: { data: base64Data, mimeType: 'audio/pcm;rate=24000' } } : base64Data; await this.theirSttSession.sendRealtimeInput(payload); @@ -472,6 +500,7 @@ class SttService { this.theirLastPartialText = ''; this.myCompletionBuffer = ''; this.theirCompletionBuffer = ''; + this.modelInfo = null; } } diff --git a/src/features/listen/summary/summaryService.js b/src/features/listen/summary/summaryService.js index 860fa35..fef6cf0 100644 --- a/src/features/listen/summary/summaryService.js +++ b/src/features/listen/summary/summaryService.js @@ -4,7 +4,7 @@ const { createLLM } = require('../../../common/ai/factory'); const authService = require('../../../common/services/authService'); const sessionRepository = require('../../../common/repositories/session'); const summaryRepository = require('./repositories'); -const { getStoredApiKey, getStoredProvider } = require('../../../electron/windowManager'); +const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../electron/windowManager'); class SummaryService { constructor() { @@ -27,22 +27,22 @@ class SummaryService { this.currentSessionId = sessionId; } - async getApiKey() { - const storedKey = await getStoredApiKey(); - if (storedKey) { - console.log('[SummaryService] Using stored API key'); - return storedKey; - } + // async getApiKey() { + // const storedKey = await getStoredApiKey(); + // if (storedKey) { + // console.log('[SummaryService] Using stored API key'); + // return storedKey; + // } - const envKey = process.env.OPENAI_API_KEY; - if (envKey) { - console.log('[SummaryService] Using environment API key'); - return envKey; - } + // const envKey = process.env.OPENAI_API_KEY; + // if (envKey) { + // console.log('[SummaryService] Using environment API key'); + // return envKey; + // } - console.error('[SummaryService] No API key found in storage or environment'); - return null; - } + // console.error('[SummaryService] No API key found in storage or environment'); + // return null; + // } sendToRenderer(channel, data) { BrowserWindow.getAllWindows().forEach(win => { @@ -114,6 +114,12 @@ Please build upon this context while analyzing the new conversation segments. if (this.currentSessionId) { await sessionRepository.touch(this.currentSessionId); } + + const modelInfo = await getCurrentModelInfo('llm'); + if (!modelInfo || !modelInfo.apiKey) { + throw new Error('AI model or API key is not configured.'); + } + console.log(`🤖 Sending analysis request to ${modelInfo.provider} using model ${modelInfo.model}`); const messages = [ { @@ -148,23 +154,13 @@ Keep all points concise and build upon previous analysis if provided.`, console.log('🤖 Sending analysis request to AI...'); - const API_KEY = await this.getApiKey(); - if (!API_KEY) { - throw new Error('No API key available'); - } - - const provider = getStoredProvider ? await getStoredProvider() : 'openai'; - const loggedIn = authService.getCurrentUser().isLoggedIn; - - console.log(`[SummaryService] provider: ${provider}, loggedIn: ${loggedIn}`); - - const llm = createLLM(provider, { - apiKey: API_KEY, - model: provider === 'openai' ? 'gpt-4.1' : 'gemini-2.5-flash', + const llm = createLLM(modelInfo.provider, { + apiKey: modelInfo.apiKey, + model: modelInfo.model, temperature: 0.7, maxTokens: 1024, - usePortkey: provider === 'openai' && loggedIn, - portkeyVirtualKey: loggedIn ? API_KEY : undefined + usePortkey: modelInfo.provider === 'openai-glass', + portkeyVirtualKey: modelInfo.provider === 'openai-glass' ? modelInfo.apiKey : undefined, }); const completion = await llm.chat(messages); @@ -180,7 +176,7 @@ Keep all points concise and build upon previous analysis if provided.`, tldr: structuredData.summary.join('\n'), bullet_json: JSON.stringify(structuredData.topic.bullets), action_json: JSON.stringify(structuredData.actions), - model: 'gpt-4.1' + model: modelInfo.model }).catch(err => console.error('[DB] Failed to save summary:', err)); } @@ -192,7 +188,6 @@ Keep all points concise and build upon previous analysis if provided.`, conversationLength: conversationTexts.length, }); - // 히스토리 크기 제한 (최근 10개만 유지) if (this.analysisHistory.length > 10) { this.analysisHistory.shift(); } diff --git a/src/features/settings/SettingsView.js b/src/features/settings/SettingsView.js index 49a0854..d78703c 100644 --- a/src/features/settings/SettingsView.js +++ b/src/features/settings/SettingsView.js @@ -374,6 +374,43 @@ export class SettingsView extends LitElement { .hidden { display: none; } + + .api-key-section, .model-selection-section { + padding: 8px 0; + border-top: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + flex-direction: column; + gap: 10px; + } + .provider-key-group, .model-select-group { + display: flex; + flex-direction: column; + gap: 4px; + } + label { + font-size: 11px; + font-weight: 500; + color: rgba(255, 255, 255, 0.8); + margin-left: 2px; + } + label > strong { + color: white; + font-weight: 600; + } + .provider-key-group input { + width: 100%; background: rgba(0,0,0,0.2); border: 1px solid rgba(255,255,255,0.2); + color: white; border-radius: 4px; padding: 5px 8px; font-size: 11px; box-sizing: border-box; + } + .key-buttons { display: flex; gap: 4px; } + .key-buttons .settings-button { flex: 1; padding: 4px; } + .model-list { + display: flex; flex-direction: column; gap: 2px; max-height: 120px; + overflow-y: auto; background: rgba(0,0,0,0.3); border-radius: 4px; + padding: 4px; margin-top: 4px; + } + .model-item { padding: 5px 8px; font-size: 11px; border-radius: 3px; cursor: pointer; transition: background-color 0.15s; } + .model-item:hover { background-color: rgba(255,255,255,0.1); } + .model-item.selected { background-color: rgba(0, 122, 255, 0.4); font-weight: 500; } /* ────────────────[ GLASS BYPASS ]─────────────── */ :host-context(body.has-glass) { @@ -400,71 +437,237 @@ export class SettingsView extends LitElement { } `; + //////// before_modelStateService //////// + // static properties = { + // firebaseUser: { type: Object, state: true }, + // apiKey: { type: String, state: true }, + // isLoading: { type: Boolean, state: true }, + // isContentProtectionOn: { type: Boolean, state: true }, + // settings: { type: Object, state: true }, + // presets: { type: Array, state: true }, + // selectedPreset: { type: Object, state: true }, + // showPresets: { type: Boolean, state: true }, + // saving: { type: Boolean, state: true }, + // }; + //////// before_modelStateService //////// + + //////// after_modelStateService //////// static properties = { firebaseUser: { type: Object, state: true }, - apiKey: { type: String, state: true }, isLoading: { type: Boolean, state: true }, isContentProtectionOn: { type: Boolean, state: true }, - settings: { type: Object, state: true }, + saving: { type: Boolean, state: true }, + providerConfig: { type: Object, state: true }, + apiKeys: { type: Object, state: true }, + availableLlmModels: { type: Array, state: true }, + availableSttModels: { type: Array, state: true }, + selectedLlm: { type: String, state: true }, + selectedStt: { type: String, state: true }, + isLlmListVisible: { type: Boolean }, + isSttListVisible: { type: Boolean }, presets: { type: Array, state: true }, selectedPreset: { type: Object, state: true }, showPresets: { type: Boolean, state: true }, - saving: { type: Boolean, state: true }, }; + //////// after_modelStateService //////// constructor() { super(); + //////// before_modelStateService //////// + // this.firebaseUser = null; + // this.apiKey = null; + // this.isLoading = false; + // this.isContentProtectionOn = true; + // this.settings = null; + // this.presets = []; + // this.selectedPreset = null; + // this.showPresets = false; + // this.saving = false; + // this.loadInitialData(); + //////// before_modelStateService //////// + + //////// after_modelStateService //////// this.firebaseUser = null; - this.apiKey = null; - this.isLoading = false; + this.apiKeys = { openai: '', gemini: '', anthropic: '' }; + this.providerConfig = {}; + this.isLoading = true; this.isContentProtectionOn = true; - this.settings = null; + this.saving = false; + this.availableLlmModels = []; + this.availableSttModels = []; + this.selectedLlm = null; + this.selectedStt = null; + this.isLlmListVisible = false; + this.isSttListVisible = false; this.presets = []; this.selectedPreset = null; this.showPresets = false; - this.saving = false; + this.handleUsePicklesKey = this.handleUsePicklesKey.bind(this) this.loadInitialData(); + //////// after_modelStateService //////// } + + //////// before_modelStateService //////// + // async loadInitialData() { + // if (!window.require) return; + + // try { + // this.isLoading = true; + // const { ipcRenderer } = window.require('electron'); + + // // Load all data in parallel + // const [settings, presets, apiKey, contentProtection, userState] = await Promise.all([ + // ipcRenderer.invoke('settings:getSettings'), + // ipcRenderer.invoke('settings:getPresets'), + // ipcRenderer.invoke('get-stored-api-key'), + // ipcRenderer.invoke('get-content-protection-status'), + // ipcRenderer.invoke('get-current-user') + // ]); + + // this.settings = settings; + // this.presets = presets || []; + // this.apiKey = apiKey; + // this.isContentProtectionOn = contentProtection; + + // // Set first user preset as selected + // if (this.presets.length > 0) { + // const firstUserPreset = this.presets.find(p => p.is_default === 0); + // if (firstUserPreset) { + // this.selectedPreset = firstUserPreset; + // } + // } + + // if (userState && userState.isLoggedIn) { + // this.firebaseUser = userState.user; + // } + // } catch (error) { + // console.error('Error loading initial data:', error); + // } finally { + // this.isLoading = false; + // } + // } + //////// before_modelStateService //////// + + //////// after_modelStateService //////// async loadInitialData() { if (!window.require) return; - + this.isLoading = true; + const { ipcRenderer } = window.require('electron'); try { - this.isLoading = true; - const { ipcRenderer } = window.require('electron'); - - // Load all data in parallel - const [settings, presets, apiKey, contentProtection, userState] = await Promise.all([ - ipcRenderer.invoke('settings:getSettings'), + const [userState, config, storedKeys, availableLlm, availableStt, selectedModels, presets, contentProtection] = await Promise.all([ + ipcRenderer.invoke('get-current-user'), + ipcRenderer.invoke('model:get-provider-config'), // Provider 설정 로드 + ipcRenderer.invoke('model:get-all-keys'), + ipcRenderer.invoke('model:get-available-models', { type: 'llm' }), + ipcRenderer.invoke('model:get-available-models', { type: 'stt' }), + ipcRenderer.invoke('model:get-selected-models'), ipcRenderer.invoke('settings:getPresets'), - ipcRenderer.invoke('get-stored-api-key'), - ipcRenderer.invoke('get-content-protection-status'), - ipcRenderer.invoke('get-current-user') + ipcRenderer.invoke('get-content-protection-status') ]); - this.settings = settings; + if (userState && userState.isLoggedIn) this.firebaseUser = userState; + this.providerConfig = config; + this.apiKeys = storedKeys; + this.availableLlmModels = availableLlm; + this.availableSttModels = availableStt; + this.selectedLlm = selectedModels.llm; + this.selectedStt = selectedModels.stt; this.presets = presets || []; - this.apiKey = apiKey; this.isContentProtectionOn = contentProtection; - - // Set first user preset as selected if (this.presets.length > 0) { const firstUserPreset = this.presets.find(p => p.is_default === 0); - if (firstUserPreset) { - this.selectedPreset = firstUserPreset; - } - } - - if (userState && userState.isLoggedIn) { - this.firebaseUser = userState.user; + if (firstUserPreset) this.selectedPreset = firstUserPreset; } } catch (error) { - console.error('Error loading initial data:', error); + console.error('Error loading initial settings data:', error); } finally { this.isLoading = false; } } + async handleSaveKey(provider) { + const input = this.shadowRoot.querySelector(`#key-input-${provider}`); + if (!input) return; + const key = input.value; + this.saving = true; + + const { ipcRenderer } = window.require('electron'); + const result = await ipcRenderer.invoke('model:validate-key', { provider, key }); + + if (result.success) { + this.apiKeys = { ...this.apiKeys, [provider]: key }; + await this.refreshModelData(); + } else { + alert(`Failed to save ${provider} key: ${result.error}`); + input.value = this.apiKeys[provider] || ''; + } + this.saving = false; + } + + async handleClearKey(provider) { + this.saving = true; + const { ipcRenderer } = window.require('electron'); + await ipcRenderer.invoke('model:remove-api-key', { provider }); + this.apiKeys = { ...this.apiKeys, [provider]: '' }; + await this.refreshModelData(); + this.saving = false; + } + + async refreshModelData() { + const { ipcRenderer } = window.require('electron'); + const [availableLlm, availableStt, selected] = await Promise.all([ + ipcRenderer.invoke('model:get-available-models', { type: 'llm' }), + ipcRenderer.invoke('model:get-available-models', { type: 'stt' }), + ipcRenderer.invoke('model:get-selected-models') + ]); + this.availableLlmModels = availableLlm; + this.availableSttModels = availableStt; + this.selectedLlm = selected.llm; + this.selectedStt = selected.stt; + this.requestUpdate(); + } + + async toggleModelList(type) { + const visibilityProp = type === 'llm' ? 'isLlmListVisible' : 'isSttListVisible'; + + if (!this[visibilityProp]) { + this.saving = true; + this.requestUpdate(); + + await this.refreshModelData(); + + this.saving = false; + } + + // 데이터 새로고침 후, 목록의 표시 상태를 토글합니다. + this[visibilityProp] = !this[visibilityProp]; + this.requestUpdate(); + } + + async selectModel(type, modelId) { + this.saving = true; + const { ipcRenderer } = window.require('electron'); + await ipcRenderer.invoke('model:set-selected-model', { type, modelId }); + if (type === 'llm') this.selectedLlm = modelId; + if (type === 'stt') this.selectedStt = modelId; + this.isLlmListVisible = false; + this.isSttListVisible = false; + this.saving = false; + this.requestUpdate(); + } + + handleUsePicklesKey(e) { + e.preventDefault() + if (this.wasJustDragged) return + + console.log("Requesting Firebase authentication from main process...") + if (window.require) { + window.require("electron").ipcRenderer.invoke("start-firebase-auth") + } + } + //////// after_modelStateService //////// + connectedCallback() { super.connectedCallback(); @@ -697,6 +900,140 @@ export class SettingsView extends LitElement { } } + + //////// before_modelStateService //////// + // render() { + // if (this.isLoading) { + // return html` + //
+ //
+ //
+ // Loading... + //
+ //
+ // `; + // } + + // const loggedIn = !!this.firebaseUser; + + // return html` + //
+ //
+ //
+ //

Pickle Glass

+ // + //
+ //
+ // + // + // + //
+ //
+ + //
+ // + // + //
+ + //
+ // ${this.getMainShortcuts().map(shortcut => html` + //
+ // ${shortcut.name} + //
+ // + // ${shortcut.key} + //
+ //
+ // `)} + //
+ + // + //
+ //
+ // + // My Presets + // (${this.presets.filter(p => p.is_default === 0).length}) + // + // + // ${this.showPresets ? '▼' : '▶'} + // + //
+ + //
+ // ${this.presets.filter(p => p.is_default === 0).length === 0 ? html` + //
+ // No custom presets yet.
+ // + // Create your first preset + // + //
+ // ` : this.presets.filter(p => p.is_default === 0).map(preset => html` + //
this.handlePresetSelect(preset)}> + // ${preset.title} + // ${this.selectedPreset?.id === preset.id ? html`Selected` : ''} + //
+ // `)} + //
+ //
+ + //
+ // + + //
+ // + // + //
+ + // + + //
+ // ${this.firebaseUser + // ? html` + // + // ` + // : html` + // + // ` + // } + // + //
+ //
+ //
+ // `; + // } + //////// before_modelStateService //////// + + //////// after_modelStateService //////// render() { if (this.isLoading) { return html` @@ -711,6 +1048,68 @@ export class SettingsView extends LitElement { const loggedIn = !!this.firebaseUser; + const apiKeyManagementHTML = html` +
+ ${Object.entries(this.providerConfig) + .filter(([id, config]) => !id.includes('-glass')) + .map(([id, config]) => html` +
+ + +
+ + +
+
+ `)} +
+ `; + + const getModelName = (type, id) => { + const models = type === 'llm' ? this.availableLlmModels : this.availableSttModels; + const model = models.find(m => m.id === id); + return model ? model.name : id; + } + + const modelSelectionHTML = html` +
+
+ + + ${this.isLlmListVisible ? html` +
+ ${this.availableLlmModels.map(model => html` +
this.selectModel('llm', model.id)}> + ${model.name} +
+ `)} +
+ ` : ''} +
+
+ + + ${this.isSttListVisible ? html` +
+ ${this.availableSttModels.map(model => html` +
this.selectModel('stt', model.id)}> + ${model.name} +
+ `)} +
+ ` : ''} +
+
+ `; + return html`
@@ -719,9 +1118,7 @@ export class SettingsView extends LitElement {
@@ -732,19 +1129,9 @@ export class SettingsView extends LitElement {
-
- - -
- + ${apiKeyManagementHTML} + ${modelSelectionHTML} +
${this.getMainShortcuts().map(shortcut => html`
@@ -757,7 +1144,6 @@ export class SettingsView extends LitElement { `)}
-
@@ -813,8 +1199,8 @@ export class SettingsView extends LitElement { ` : html` - ` } @@ -826,6 +1212,7 @@ export class SettingsView extends LitElement {
`; } + //////// after_modelStateService //////// } customElements.define('settings-view', SettingsView); \ No newline at end of file diff --git a/src/index.js b/src/index.js index e0fcde4..e7f9a07 100644 --- a/src/index.js +++ b/src/index.js @@ -25,6 +25,7 @@ const { EventEmitter } = require('events'); const askService = require('./features/ask/askService'); const settingsService = require('./features/settings/settingsService'); const sessionRepository = require('./common/repositories/session'); +const ModelStateService = require('./common/services/modelStateService'); const eventBridge = new EventEmitter(); let WEB_PORT = 3000; @@ -33,6 +34,11 @@ const listenService = new ListenService(); // Make listenService globally accessible so other modules (e.g., windowManager, askService) can reuse the same instance global.listenService = listenService; +//////// after_modelStateService //////// +const modelStateService = new ModelStateService(authService); +global.modelStateService = modelStateService; +//////// after_modelStateService //////// + // Native deep link handling - cross-platform compatible let pendingDeepLinkUrl = null; @@ -173,6 +179,9 @@ app.whenReady().then(async () => { sessionRepository.endAllActiveSessions(); authService.initialize(); + //////// after_modelStateService //////// + modelStateService.initialize(); + //////// after_modelStateService //////// listenService.setupIpcHandlers(); askService.initialize(); settingsService.initialize();