featureBridge init

This commit is contained in:
samtiz 2025-07-13 04:25:35 +09:00
parent 2cf71f1034
commit bf20d002ba
8 changed files with 95 additions and 81 deletions

View File

@ -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));

View File

@ -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 - 대화 내용 문자열의 배열

View File

@ -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') {

View File

@ -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();

View File

@ -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;

View File

@ -197,8 +197,6 @@ app.whenReady().then(async () => {
await modelStateService.initialize();
//////// after_modelStateService ////////
listenService.initialize();
askService.initialize();
featureBridge.initialize(); // 추가: featureBridge 초기화
setupWebDataHandlers();

View File

@ -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();
}

View File

@ -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;