From bf20d002bada7fa3814774aee4b0d2a476583342 Mon Sep 17 00:00:00 2001 From: samtiz Date: Sun, 13 Jul 2025 04:25:35 +0900 Subject: [PATCH] featureBridge init --- src/bridge/featureBridge.js | 18 +++- src/features/ask/askService.js | 13 +-- .../common/services/modelStateService.js | 23 ++++- src/features/listen/listenService.js | 96 ++++++++----------- src/features/listen/stt/sttService.js | 11 +++ src/index.js | 2 - src/ui/app/ApiKeyHeader.js | 9 +- src/ui/app/HeaderController.js | 4 + 8 files changed, 95 insertions(+), 81 deletions(-) diff --git a/src/bridge/featureBridge.js b/src/bridge/featureBridge.js index 587013b..e75527d 100644 --- a/src/bridge/featureBridge.js +++ b/src/bridge/featureBridge.js @@ -56,7 +56,23 @@ module.exports = { ipcMain.handle('ollama:get-warm-up-status', async () => await ollamaService.handleGetWarmUpStatus()); ipcMain.handle('ollama:shutdown', async (event, force = false) => await ollamaService.handleShutdown(event, force)); - // ModelStateService + // Ask + ipcMain.handle('ask:sendMessage', async (event, userPrompt, conversationHistoryRaw = []) => await askService.sendMessage(userPrompt, conversationHistoryRaw)); + + // Listen + ipcMain.handle('send-audio-content', async (event, { data, mimeType }) => await listenService.handleSendAudioContent(data, mimeType)); + ipcMain.handle('send-system-audio-content', async (event, { data, mimeType }) => { + const result = await listenService.sttService.sendSystemAudioContent(data, mimeType); + if(result.success) { + listenService.sendToRenderer('system-audio-data', { data }); + } + return result; + }); + ipcMain.handle('start-macos-audio', async () => await listenService.handleStartMacosAudio()); + ipcMain.handle('stop-macos-audio', async () => await listenService.handleStopMacosAudio()); + ipcMain.handle('update-google-search-setting', async (event, enabled) => await listenService.handleUpdateGoogleSearchSetting(enabled)); + + // ModelStateService ipcMain.handle('model:validate-key', async (e, { provider, key }) => await modelStateService.handleValidateKey(provider, key)); ipcMain.handle('model:get-all-keys', () => modelStateService.getAllApiKeys()); ipcMain.handle('model:set-api-key', async (e, { provider, key }) => await modelStateService.setApiKey(provider, key)); diff --git a/src/features/ask/askService.js b/src/features/ask/askService.js index 92f6f21..537243a 100644 --- a/src/features/ask/askService.js +++ b/src/features/ask/askService.js @@ -1,4 +1,4 @@ -const { ipcMain, BrowserWindow } = require('electron'); +const { BrowserWindow } = require('electron'); const { createStreamingLLM } = require('../common/ai/factory'); const { getCurrentModelInfo, windowPool, captureScreenshot } = require('../../window/windowManager'); const sessionRepository = require('../common/repositories/session'); @@ -17,17 +17,6 @@ class AskService { console.log('[AskService] Service instance created.'); } - /** - * IPC 리스너를 등록하여 렌더러 프로세스로부터의 요청을 처리합니다. - * Electron 애플리케이션의 메인 프로세스에서 한 번만 호출되어야 합니다. - */ - initialize() { - ipcMain.handle('ask:sendMessage', async (event, userPrompt, conversationHistoryRaw=[]) => { - return this.sendMessage(userPrompt, conversationHistoryRaw); - }); - console.log('[AskService] Initialized and ready.'); - } - /** * 대화 기록 배열을 프롬프트에 적합한 단일 문자열로 변환합니다. * @param {string[]} conversationTexts - 대화 내용 문자열의 배열 diff --git a/src/features/common/services/modelStateService.js b/src/features/common/services/modelStateService.js index c96de5e..f2fa1c3 100644 --- a/src/features/common/services/modelStateService.js +++ b/src/features/common/services/modelStateService.js @@ -36,15 +36,17 @@ class ModelStateService { console.log(`[ModelStateService] Current Selection -> LLM: ${llmModel || 'None'} (Provider: ${llmProvider}), STT: ${sttModel || 'None'} (Provider: ${sttProvider})`); } - _autoSelectAvailableModels() { - console.log('[ModelStateService] Running auto-selection for models...'); + _autoSelectAvailableModels(forceReselectionForTypes = []) { + console.log(`[ModelStateService] Running auto-selection for models. Force re-selection for: [${forceReselectionForTypes.join(', ')}]`); const types = ['llm', 'stt']; types.forEach(type => { const currentModelId = this.state.selectedModels[type]; let isCurrentModelValid = false; - if (currentModelId) { + const forceReselection = forceReselectionForTypes.includes(type); + + if (currentModelId && !forceReselection) { const provider = this.getProviderForModel(type, currentModelId); const apiKey = this.getApiKey(provider); // For Ollama, 'local' is a valid API key @@ -54,7 +56,7 @@ class ModelStateService { } if (!isCurrentModelValid) { - console.log(`[ModelStateService] No valid ${type.toUpperCase()} model selected. Finding an alternative...`); + console.log(`[ModelStateService] No valid ${type.toUpperCase()} model selected or re-selection forced. Finding an alternative...`); const availableModels = this.getAvailableModels(type); if (availableModels.length > 0) { // Prefer API providers over local providers for auto-selection @@ -331,7 +333,16 @@ class ModelStateService { async setApiKey(provider, key) { if (provider in this.state.apiKeys) { this.state.apiKeys[provider] = key; - this._autoSelectAvailableModels(); + + const supportedTypes = []; + if (PROVIDERS[provider]?.llmModels.length > 0 || provider === 'ollama') { + supportedTypes.push('llm'); + } + if (PROVIDERS[provider]?.sttModels.length > 0 || provider === 'whisper') { + supportedTypes.push('stt'); + } + + this._autoSelectAvailableModels(supportedTypes); await this._saveState(); return true; } @@ -395,6 +406,8 @@ class ModelStateService { areProvidersConfigured() { if (this.isLoggedInWithFirebase()) return true; + console.log('[DEBUG] Checking configured providers with apiKeys state:', JSON.stringify(this.state.apiKeys, (key, value) => (value ? '***' : null), 2)); + // LLM과 STT 모델을 제공하는 Provider 중 하나라도 API 키가 설정되었는지 확인 const hasLlmKey = Object.entries(this.state.apiKeys).some(([provider, key]) => { if (provider === 'ollama') { diff --git a/src/features/listen/listenService.js b/src/features/listen/listenService.js index 1e091f4..306c4d2 100644 --- a/src/features/listen/listenService.js +++ b/src/features/listen/listenService.js @@ -1,4 +1,4 @@ -const { ipcMain, BrowserWindow } = require('electron'); +const { BrowserWindow } = require('electron'); const SttService = require('./stt/sttService'); const SummaryService = require('./summary/summaryService'); const authService = require('../common/services/authService'); @@ -46,11 +46,6 @@ class ListenService { }); } - initialize() { - this.setupIpcHandlers(); - console.log('[ListenService] Initialized and ready.'); - } - async handleTranscriptionComplete(speaker, text) { console.log(`[ListenService] Transcription complete: ${speaker} - ${text}`); @@ -222,70 +217,57 @@ class ListenService { return this.summaryService.getConversationHistory(); } - setupIpcHandlers() { - ipcMain.handle('send-audio-content', async (event, { data, mimeType }) => { + _createHandler(asyncFn, successMessage, errorMessage) { + return async (...args) => { try { - await this.sendAudioContent(data, mimeType); - return { success: true }; + const result = await asyncFn.apply(this, args); + if (successMessage) console.log(successMessage); + // `startMacOSAudioCapture`는 성공 시 { success, error } 객체를 반환하지 않으므로, + // 핸들러가 일관된 응답을 보내도록 여기서 success 객체를 반환합니다. + // 다른 함수들은 이미 success 객체를 반환합니다. + return result && typeof result.success !== 'undefined' ? result : { success: true }; } catch (e) { - console.error('Error sending user audio:', e); + console.error(errorMessage, e); return { success: false, error: e.message }; } - }); + }; + } - ipcMain.handle('send-system-audio-content', async (event, { data, mimeType }) => { - try { - await this.sttService.sendSystemAudioContent(data, mimeType); - - // Send system audio data back to renderer for AEC reference (like macOS does) - this.sendToRenderer('system-audio-data', { data }); - - return { success: true }; - } catch (error) { - console.error('Error sending system audio:', error); - return { success: false, error: error.message }; - } - }); + // `_createHandler`를 사용하여 핸들러들을 동적으로 생성합니다. + handleSendAudioContent = this._createHandler( + this.sendAudioContent, + null, + 'Error sending user audio:' + ); - ipcMain.handle('start-macos-audio', async () => { + handleStartMacosAudio = this._createHandler( + async () => { if (process.platform !== 'darwin') { return { success: false, error: 'macOS audio capture only available on macOS' }; } if (this.sttService.isMacOSAudioRunning?.()) { return { success: false, error: 'already_running' }; } + await this.startMacOSAudioCapture(); + return { success: true, error: null }; + }, + 'macOS audio capture started.', + 'Error starting macOS audio capture:' + ); + + handleStopMacosAudio = this._createHandler( + this.stopMacOSAudioCapture, + 'macOS audio capture stopped.', + 'Error stopping macOS audio capture:' + ); - try { - const success = await this.startMacOSAudioCapture(); - return { success, error: null }; - } catch (error) { - console.error('Error starting macOS audio capture:', error); - return { success: false, error: error.message }; - } - }); - - ipcMain.handle('stop-macos-audio', async () => { - try { - this.stopMacOSAudioCapture(); - return { success: true }; - } catch (error) { - console.error('Error stopping macOS audio capture:', error); - return { success: false, error: error.message }; - } - }); - - ipcMain.handle('update-google-search-setting', async (event, enabled) => { - try { - console.log('Google Search setting updated to:', enabled); - return { success: true }; - } catch (error) { - console.error('Error updating Google Search setting:', error); - return { success: false, error: error.message }; - } - }); - - console.log('✅ Listen service IPC handlers registered'); - } + handleUpdateGoogleSearchSetting = this._createHandler( + async (enabled) => { + console.log('Google Search setting updated to:', enabled); + }, + null, + 'Error updating Google Search setting:' + ); } const listenService = new ListenService(); diff --git a/src/features/listen/stt/sttService.js b/src/features/listen/stt/sttService.js index c6dcf45..fb317e9 100644 --- a/src/features/listen/stt/sttService.js +++ b/src/features/listen/stt/sttService.js @@ -41,6 +41,17 @@ class SttService { }); } + async handleSendSystemAudioContent(data, mimeType) { + try { + await this.sendSystemAudioContent(data, mimeType); + this.sendToRenderer('system-audio-data', { data }); + return { success: true }; + } catch (error) { + console.error('Error sending system audio:', error); + return { success: false, error: error.message }; + } + } + flushMyCompletion() { const finalText = (this.myCompletionBuffer + this.myCurrentUtterance).trim(); if (!this.modelInfo || !finalText) return; diff --git a/src/index.js b/src/index.js index bdb53bd..f160e67 100644 --- a/src/index.js +++ b/src/index.js @@ -197,8 +197,6 @@ app.whenReady().then(async () => { await modelStateService.initialize(); //////// after_modelStateService //////// - listenService.initialize(); - askService.initialize(); featureBridge.initialize(); // 추가: featureBridge 초기화 setupWebDataHandlers(); diff --git a/src/ui/app/ApiKeyHeader.js b/src/ui/app/ApiKeyHeader.js index a0ed1fa..659b2cc 100644 --- a/src/ui/app/ApiKeyHeader.js +++ b/src/ui/app/ApiKeyHeader.js @@ -1541,7 +1541,7 @@ export class ApiKeyHeader extends LitElement { this.classList.remove("sliding-out"); this.classList.add("hidden"); - console.log('[ApiKeyHeader] handleAnimationEnd: Animation completed, transitioning to next state...'); + console.log('[ApiKeyHeader] handleAnimationEnd: Transition completed, transitioning to next state...'); if (!window.require) { console.error('[ApiKeyHeader] handleAnimationEnd: window.require not available'); @@ -1585,7 +1585,8 @@ export class ApiKeyHeader extends LitElement { connectedCallback() { super.connectedCallback() - this.addEventListener("animationend", this.handleAnimationEnd) + // this.addEventListener("animationend", this.handleAnimationEnd) + this.addEventListener("transitionend", this.handleAnimationEnd) } handleMessageFadeEnd(e) { @@ -1603,8 +1604,8 @@ export class ApiKeyHeader extends LitElement { disconnectedCallback() { super.disconnectedCallback() - this.removeEventListener("animationend", this.handleAnimationEnd) - + // this.removeEventListener("animationend", this.handleAnimationEnd) + this.removeEventListener("transitionend", this.handleAnimationEnd) // Professional cleanup of all resources this._performCompleteCleanup(); } diff --git a/src/ui/app/HeaderController.js b/src/ui/app/HeaderController.js index e2c5fe8..8f56dec 100644 --- a/src/ui/app/HeaderController.js +++ b/src/ui/app/HeaderController.js @@ -96,8 +96,12 @@ class HeaderTransitionManager { //////// after_modelStateService //////// async handleStateUpdate(userState) { + console.log('[HeaderController DEBUG] handleStateUpdate called with userState:', userState); const { ipcRenderer } = window.require('electron'); + + console.log('[HeaderController DEBUG] Invoking "model:are-providers-configured"...'); const isConfigured = await ipcRenderer.invoke('model:are-providers-configured'); + console.log('[HeaderController DEBUG] "model:are-providers-configured" returned:', isConfigured); if (isConfigured) { const { isLoggedIn } = userState;