[Refactor] full refactor and file structure changed (#125)
* refactoring the bridge * Update aec submodule * folder structure refactor * fixing ask logic * resolve import err * fix askview * fix header content html path * fix systemaudiodump path * centralized ask logic * delete legacy code * change askservice to class * settingsService facade * fix getCurrentModelInfo * common service ipc moved to featureBridge * featureBridge init * ui fix * add featureBridge func for listenservice * fix preload conflict * shortcuts seperated * refactor ask * transfer roles from askview to askservice * modifying windowBridge * delete legacy ask code * retrieve conversation history for askserice * fix legacy code * shortcut moved * change naming for featurebridge * screenshot moved from windowManager * rough refactor done --------- Co-authored-by: sanio <sanio@pickle.com> Co-authored-by: jhyang0 <junhyuck0819@gmail.com>
2
aec
@ -1 +1 @@
|
||||
Subproject commit 3be088c6cff8021c74eca714150e68e2cc74bee0
|
||||
Subproject commit f00bb1fb948053c752b916adfee19f90644a0b2f
|
4
build.js
@ -14,8 +14,8 @@ const baseConfig = {
|
||||
};
|
||||
|
||||
const entryPoints = [
|
||||
{ in: 'src/app/HeaderController.js', out: 'public/build/header' },
|
||||
{ in: 'src/app/PickleGlassApp.js', out: 'public/build/content' },
|
||||
{ in: 'src/ui/app/HeaderController.js', out: 'public/build/header' },
|
||||
{ in: 'src/ui/app/PickleGlassApp.js', out: 'public/build/content' },
|
||||
];
|
||||
|
||||
async function build() {
|
||||
|
19
docs/refactor-plan.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Refactor Plan: Non-Window Logic Migration from windowManager.js
|
||||
|
||||
## Goal
|
||||
`windowManager.js`를 순수 창 관리 모듈로 만들기 위해 비즈니스 로직을 해당 서비스와 `featureBridge.js`로 이전.
|
||||
|
||||
## Steps (based on initial plan)
|
||||
1. **Shortcuts**: Completed. Logic moved to `shortcutsService.js` and IPC to `featureBridge.js`. Used `internalBridge` for coordination.
|
||||
|
||||
2. **Screenshot**: Next. Move `captureScreenshot` function and related IPC handlers from `windowManager.js` to `askService.js` (since it's primarily used there). Update `askService.js` to use its own screenshot method. Add IPC handlers to `featureBridge.js` if needed.
|
||||
|
||||
3. **System Permissions**: Create new `permissionService.js` in `src/features/common/services/`. Move all permission-related logic (check, request, open preferences, mark completed, etc.) and IPC handlers from `windowManager.js` to the new service and `featureBridge.js`.
|
||||
|
||||
4. **API Key / Model State**: Completely remove from `windowManager.js` (e.g., `setupApiKeyIPC` and helpers). Ensure all usages (e.g., in `askService.js`) directly require and use `modelStateService.js` instead.
|
||||
|
||||
## Notes
|
||||
- Maintain original logic without changes.
|
||||
- Break circular dependencies if found.
|
||||
- Use `internalBridge` for inter-module communication where appropriate.
|
||||
- After each step, verify no errors and test functionality.
|
@ -33,7 +33,7 @@ extraResources:
|
||||
to: out
|
||||
|
||||
asarUnpack:
|
||||
- "src/assets/SystemAudioDump"
|
||||
- "src/ui/assets/SystemAudioDump"
|
||||
- "**/node_modules/sharp/**/*"
|
||||
- "**/node_modules/@img/**/*"
|
||||
|
||||
|
125
src/bridge/featureBridge.js
Normal file
@ -0,0 +1,125 @@
|
||||
// src/bridge/featureBridge.js
|
||||
const { ipcMain, app } = require('electron');
|
||||
const settingsService = require('../features/settings/settingsService');
|
||||
const authService = require('../features/common/services/authService');
|
||||
const whisperService = require('../features/common/services/whisperService');
|
||||
const ollamaService = require('../features/common/services/ollamaService');
|
||||
const modelStateService = require('../features/common/services/modelStateService');
|
||||
const shortcutsService = require('../features/shortcuts/shortcutsService');
|
||||
|
||||
const askService = require('../features/ask/askService');
|
||||
const listenService = require('../features/listen/listenService');
|
||||
const permissionService = require('../features/common/services/permissionService');
|
||||
|
||||
module.exports = {
|
||||
// Renderer로부터의 요청을 수신
|
||||
initialize() {
|
||||
|
||||
// Settings Service
|
||||
ipcMain.handle('settings:getPresets', async () => await settingsService.getPresets());
|
||||
ipcMain.handle('settings:get-auto-update', async () => await settingsService.getAutoUpdateSetting());
|
||||
ipcMain.handle('settings:set-auto-update', async (event, isEnabled) => await settingsService.setAutoUpdateSetting(isEnabled));
|
||||
ipcMain.handle('settings:get-model-settings', async () => await settingsService.getModelSettings());
|
||||
ipcMain.handle('settings:validate-and-save-key', async (e, { provider, key }) => await settingsService.validateAndSaveKey(provider, key));
|
||||
ipcMain.handle('settings:clear-api-key', async (e, { provider }) => await settingsService.clearApiKey(provider));
|
||||
ipcMain.handle('settings:set-selected-model', async (e, { type, modelId }) => await settingsService.setSelectedModel(type, modelId));
|
||||
|
||||
ipcMain.handle('settings:get-ollama-status', async () => await settingsService.getOllamaStatus());
|
||||
ipcMain.handle('settings:ensure-ollama-ready', async () => await settingsService.ensureOllamaReady());
|
||||
ipcMain.handle('settings:shutdown-ollama', async () => await settingsService.shutdownOllama());
|
||||
|
||||
// Shortcuts
|
||||
ipcMain.handle('get-current-shortcuts', async () => await shortcutsService.loadKeybinds());
|
||||
ipcMain.handle('get-default-shortcuts', async () => await shortcutsService.handleRestoreDefaults());
|
||||
ipcMain.handle('save-shortcuts', async (event, newKeybinds) => await shortcutsService.handleSaveShortcuts(newKeybinds));
|
||||
|
||||
|
||||
// Permissions
|
||||
ipcMain.handle('check-system-permissions', async () => await permissionService.checkSystemPermissions());
|
||||
ipcMain.handle('request-microphone-permission', async () => await permissionService.requestMicrophonePermission());
|
||||
ipcMain.handle('open-system-preferences', async (event, section) => await permissionService.openSystemPreferences(section));
|
||||
ipcMain.handle('mark-permissions-completed', async () => await permissionService.markPermissionsAsCompleted());
|
||||
ipcMain.handle('check-permissions-completed', async () => await permissionService.checkPermissionsCompleted());
|
||||
|
||||
|
||||
// User/Auth
|
||||
ipcMain.handle('get-current-user', () => authService.getCurrentUser());
|
||||
ipcMain.handle('start-firebase-auth', async () => await authService.startFirebaseAuthFlow());
|
||||
ipcMain.handle('firebase-logout', async () => await authService.signOut());
|
||||
|
||||
// App
|
||||
ipcMain.handle('quit-application', () => app.quit());
|
||||
|
||||
// Whisper
|
||||
ipcMain.handle('whisper:download-model', async (event, modelId) => await whisperService.handleDownloadModel(event, modelId));
|
||||
ipcMain.handle('whisper:get-installed-models', async () => await whisperService.handleGetInstalledModels());
|
||||
|
||||
// General
|
||||
ipcMain.handle('get-preset-templates', () => presetRepository.getPresetTemplates());
|
||||
ipcMain.handle('get-web-url', () => process.env.pickleglass_WEB_URL || 'http://localhost:3000');
|
||||
|
||||
// Ollama
|
||||
ipcMain.handle('ollama:get-status', async () => await ollamaService.handleGetStatus());
|
||||
ipcMain.handle('ollama:install', async (event) => await ollamaService.handleInstall(event));
|
||||
ipcMain.handle('ollama:start-service', async (event) => await ollamaService.handleStartService(event));
|
||||
ipcMain.handle('ollama:ensure-ready', async () => await ollamaService.handleEnsureReady());
|
||||
ipcMain.handle('ollama:get-models', async () => await ollamaService.handleGetModels());
|
||||
ipcMain.handle('ollama:get-model-suggestions', async () => await ollamaService.handleGetModelSuggestions());
|
||||
ipcMain.handle('ollama:pull-model', async (event, modelName) => await ollamaService.handlePullModel(event, modelName));
|
||||
ipcMain.handle('ollama:is-model-installed', async (event, modelName) => await ollamaService.handleIsModelInstalled(modelName));
|
||||
ipcMain.handle('ollama:warm-up-model', async (event, modelName) => await ollamaService.handleWarmUpModel(modelName));
|
||||
ipcMain.handle('ollama:auto-warm-up', async () => await ollamaService.handleAutoWarmUp());
|
||||
ipcMain.handle('ollama:get-warm-up-status', async () => await ollamaService.handleGetWarmUpStatus());
|
||||
ipcMain.handle('ollama:shutdown', async (event, force = false) => await ollamaService.handleShutdown(event, force));
|
||||
|
||||
// Ask
|
||||
ipcMain.handle('ask:sendQuestionFromAsk', async (event, userPrompt) => await askService.sendMessage(userPrompt));
|
||||
ipcMain.handle('ask:sendQuestionFromSummary', async (event, userPrompt) => await askService.sendMessage(userPrompt));
|
||||
ipcMain.handle('ask:toggleAskButton', async () => await askService.toggleAskButton());
|
||||
|
||||
// Listen
|
||||
ipcMain.handle('listen:sendMicAudio', async (event, { data, mimeType }) => await listenService.handleSendMicAudioContent(data, mimeType));
|
||||
ipcMain.handle('listen:sendSystemAudio', 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('listen:startMacosSystemAudio', async () => await listenService.handleStartMacosAudio());
|
||||
ipcMain.handle('listen:stopMacosSystemAudio', async () => await listenService.handleStopMacosAudio());
|
||||
ipcMain.handle('update-google-search-setting', async (event, enabled) => await listenService.handleUpdateGoogleSearchSetting(enabled));
|
||||
ipcMain.handle('listen:changeSession', async (event, listenButtonText) => {
|
||||
console.log('[FeatureBridge] listen:changeSession from mainheader', listenButtonText);
|
||||
try {
|
||||
await listenService.handleListenRequest(listenButtonText);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[FeatureBridge] listen:changeSession failed', error.message);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 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));
|
||||
ipcMain.handle('model:remove-api-key', async (e, provider) => await modelStateService.handleRemoveApiKey(provider));
|
||||
ipcMain.handle('model:get-selected-models', () => modelStateService.getSelectedModels());
|
||||
ipcMain.handle('model:set-selected-model', async (e, { type, modelId }) => await modelStateService.handleSetSelectedModel(type, modelId));
|
||||
ipcMain.handle('model:get-available-models', (e, { type }) => modelStateService.getAvailableModels(type));
|
||||
ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured());
|
||||
ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
|
||||
|
||||
|
||||
|
||||
console.log('[FeatureBridge] Initialized with all feature handlers.');
|
||||
},
|
||||
|
||||
// Renderer로 상태를 전송
|
||||
sendAskProgress(win, progress) {
|
||||
win.webContents.send('feature:ask:progress', progress);
|
||||
},
|
||||
};
|
11
src/bridge/internalBridge.js
Normal file
@ -0,0 +1,11 @@
|
||||
// src/bridge/internalBridge.js
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
// FeatureCore와 WindowCore를 잇는 내부 이벤트 버스
|
||||
const internalBridge = new EventEmitter();
|
||||
module.exports = internalBridge;
|
||||
|
||||
// 예시 이벤트
|
||||
// internalBridge.on('content-protection-changed', (enabled) => {
|
||||
// // windowManager에서 처리
|
||||
// });
|
33
src/bridge/windowBridge.js
Normal file
@ -0,0 +1,33 @@
|
||||
// src/bridge/windowBridge.js
|
||||
const { ipcMain, BrowserWindow } = require('electron');
|
||||
const windowManager = require('../window/windowManager');
|
||||
|
||||
module.exports = {
|
||||
initialize() {
|
||||
ipcMain.handle('toggle-content-protection', () => windowManager.toggleContentProtection());
|
||||
ipcMain.handle('resize-header-window', (event, args) => windowManager.resizeHeaderWindow(args));
|
||||
ipcMain.handle('get-content-protection-status', () => windowManager.getContentProtectionStatus());
|
||||
ipcMain.handle('open-shortcut-editor', () => windowManager.openShortcutEditor());
|
||||
ipcMain.on('show-settings-window', (event, bounds) => windowManager.showSettingsWindow(bounds));
|
||||
ipcMain.on('hide-settings-window', () => windowManager.hideSettingsWindow());
|
||||
ipcMain.on('cancel-hide-settings-window', () => windowManager.cancelHideSettingsWindow());
|
||||
ipcMain.handle('open-login-page', () => windowManager.openLoginPage());
|
||||
ipcMain.handle('move-window-step', (event, direction) => windowManager.moveWindowStep(direction));
|
||||
ipcMain.on('close-shortcut-editor', () => windowManager.closeWindow('shortcut-settings'));
|
||||
|
||||
// Newly moved handlers from windowManager
|
||||
ipcMain.on('header-state-changed', (event, state) => windowManager.handleHeaderStateChanged(state));
|
||||
ipcMain.on('header-animation-finished', (event, state) => windowManager.handleHeaderAnimationFinished(state));
|
||||
ipcMain.handle('get-header-position', () => windowManager.getHeaderPosition());
|
||||
ipcMain.handle('move-header', (event, newX, newY) => windowManager.moveHeader(newX, newY));
|
||||
ipcMain.handle('move-header-to', (event, newX, newY) => windowManager.moveHeaderTo(newX, newY));
|
||||
ipcMain.handle('adjust-window-height', (event, targetHeight) => windowManager.adjustWindowHeight(event.sender, targetHeight));
|
||||
ipcMain.handle('toggle-all-windows-visibility', () => windowManager.toggleAllWindowsVisibility());
|
||||
ipcMain.on('animation-finished', (event) => windowManager.handleAnimationFinished(event.sender));
|
||||
ipcMain.handle('ask:closeAskWindow', () => windowManager.closeAskWindow());
|
||||
},
|
||||
|
||||
notifyFocusChange(win, isFocused) {
|
||||
win.webContents.send('window:focus-change', isFocused);
|
||||
}
|
||||
};
|
@ -1,150 +1,373 @@
|
||||
const { ipcMain, BrowserWindow } = require('electron');
|
||||
const { createStreamingLLM } = require('../../common/ai/factory');
|
||||
const { getStoredApiKey, getStoredProvider, getCurrentModelInfo, windowPool, captureScreenshot } = require('../../electron/windowManager');
|
||||
const authService = require('../../common/services/authService');
|
||||
const sessionRepository = require('../../common/repositories/session');
|
||||
const { BrowserWindow } = require('electron');
|
||||
const { createStreamingLLM } = require('../common/ai/factory');
|
||||
// Lazy require helper to avoid circular dependency issues
|
||||
const getWindowManager = () => require('../../window/windowManager');
|
||||
|
||||
const getWindowPool = () => {
|
||||
try {
|
||||
return getWindowManager().windowPool;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const updateLayout = () => getWindowManager().updateLayout();
|
||||
const ensureAskWindowVisible = () => getWindowManager().ensureAskWindowVisible();
|
||||
|
||||
const sessionRepository = require('../common/repositories/session');
|
||||
const askRepository = require('./repositories');
|
||||
const { getSystemPrompt } = require('../../common/prompts/promptBuilder');
|
||||
const { getSystemPrompt } = require('../common/prompts/promptBuilder');
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs');
|
||||
const os = require('os');
|
||||
const util = require('util');
|
||||
const execFile = util.promisify(require('child_process').execFile);
|
||||
const { desktopCapturer } = require('electron');
|
||||
const modelStateService = require('../common/services/modelStateService');
|
||||
|
||||
function formatConversationForPrompt(conversationTexts) {
|
||||
if (!conversationTexts || conversationTexts.length === 0) return 'No conversation history available.';
|
||||
return conversationTexts.slice(-30).join('\n');
|
||||
// Try to load sharp, but don't fail if it's not available
|
||||
let sharp;
|
||||
try {
|
||||
sharp = require('sharp');
|
||||
console.log('[AskService] Sharp module loaded successfully');
|
||||
} catch (error) {
|
||||
console.warn('[AskService] Sharp module not available:', error.message);
|
||||
console.warn('[AskService] Screenshot functionality will work with reduced image processing capabilities');
|
||||
sharp = null;
|
||||
}
|
||||
let lastScreenshot = null;
|
||||
|
||||
// Access conversation history via the global listenService instance created in index.js
|
||||
function getConversationHistory() {
|
||||
const listenService = global.listenService;
|
||||
return listenService ? listenService.getConversationHistory() : [];
|
||||
}
|
||||
async function captureScreenshot(options = {}) {
|
||||
if (process.platform === 'darwin') {
|
||||
try {
|
||||
const tempPath = path.join(os.tmpdir(), `screenshot-${Date.now()}.jpg`);
|
||||
|
||||
async function sendMessage(userPrompt) {
|
||||
if (!userPrompt || userPrompt.trim().length === 0) {
|
||||
console.warn('[AskService] Cannot process empty message');
|
||||
return { success: false, error: 'Empty message' };
|
||||
await execFile('screencapture', ['-x', '-t', 'jpg', tempPath]);
|
||||
|
||||
const imageBuffer = await fs.promises.readFile(tempPath);
|
||||
await fs.promises.unlink(tempPath);
|
||||
|
||||
if (sharp) {
|
||||
try {
|
||||
// Try using sharp for optimal image processing
|
||||
const resizedBuffer = await sharp(imageBuffer)
|
||||
.resize({ height: 384 })
|
||||
.jpeg({ quality: 80 })
|
||||
.toBuffer();
|
||||
|
||||
const base64 = resizedBuffer.toString('base64');
|
||||
const metadata = await sharp(resizedBuffer).metadata();
|
||||
|
||||
lastScreenshot = {
|
||||
base64,
|
||||
width: metadata.width,
|
||||
height: metadata.height,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
return { success: true, base64, width: metadata.width, height: metadata.height };
|
||||
} catch (sharpError) {
|
||||
console.warn('Sharp module failed, falling back to basic image processing:', sharpError.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Return the original image without resizing
|
||||
console.log('[AskService] Using fallback image processing (no resize/compression)');
|
||||
const base64 = imageBuffer.toString('base64');
|
||||
|
||||
lastScreenshot = {
|
||||
base64,
|
||||
width: null, // We don't have metadata without sharp
|
||||
height: null,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
return { success: true, base64, width: null, height: null };
|
||||
} catch (error) {
|
||||
console.error('Failed to capture screenshot:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
const askWindow = windowPool.get('ask');
|
||||
if (askWindow && !askWindow.isDestroyed()) {
|
||||
askWindow.webContents.send('hide-text-input');
|
||||
}
|
||||
|
||||
let sessionId;
|
||||
|
||||
try {
|
||||
console.log(`[AskService] 🤖 Processing message: ${userPrompt.substring(0, 50)}...`);
|
||||
|
||||
// --- Save user's message immediately ---
|
||||
// This ensures the user message is always timestamped before the assistant's response.
|
||||
sessionId = await sessionRepository.getOrCreateActive('ask');
|
||||
await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() });
|
||||
console.log(`[AskService] DB: Saved user prompt to session ${sessionId}`);
|
||||
// --- End of user message saving ---
|
||||
|
||||
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;
|
||||
|
||||
const conversationHistoryRaw = getConversationHistory();
|
||||
const conversationHistory = formatConversationForPrompt(conversationHistoryRaw);
|
||||
|
||||
const systemPrompt = getSystemPrompt('pickle_glass_analysis', conversationHistory, false);
|
||||
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: `User Request: ${userPrompt.trim()}` },
|
||||
],
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ['screen'],
|
||||
thumbnailSize: {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
},
|
||||
];
|
||||
|
||||
if (screenshotBase64) {
|
||||
messages[1].content.push({
|
||||
type: 'image_url',
|
||||
image_url: { url: `data:image/jpeg;base64,${screenshotBase64}` },
|
||||
});
|
||||
}
|
||||
|
||||
const streamingLLM = createStreamingLLM(modelInfo.provider, {
|
||||
apiKey: modelInfo.apiKey,
|
||||
model: modelInfo.model,
|
||||
temperature: 0.7,
|
||||
maxTokens: 2048,
|
||||
usePortkey: modelInfo.provider === 'openai-glass',
|
||||
portkeyVirtualKey: modelInfo.provider === 'openai-glass' ? modelInfo.apiKey : undefined,
|
||||
});
|
||||
|
||||
const response = await streamingLLM.streamChat(messages);
|
||||
if (sources.length === 0) {
|
||||
throw new Error('No screen sources available');
|
||||
}
|
||||
const source = sources[0];
|
||||
const buffer = source.thumbnail.toJPEG(70);
|
||||
const base64 = buffer.toString('base64');
|
||||
const size = source.thumbnail.getSize();
|
||||
|
||||
// --- Stream Processing ---
|
||||
const reader = response.body.getReader();
|
||||
return {
|
||||
success: true,
|
||||
base64,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to capture screenshot using desktopCapturer:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class
|
||||
* @description
|
||||
*/
|
||||
class AskService {
|
||||
constructor() {
|
||||
this.abortController = null;
|
||||
this.state = {
|
||||
isVisible: false,
|
||||
isLoading: false,
|
||||
isStreaming: false,
|
||||
currentQuestion: '',
|
||||
currentResponse: '',
|
||||
showTextInput: true,
|
||||
};
|
||||
console.log('[AskService] Service instance created.');
|
||||
}
|
||||
|
||||
_broadcastState() {
|
||||
const askWindow = getWindowPool()?.get('ask');
|
||||
if (askWindow && !askWindow.isDestroyed()) {
|
||||
askWindow.webContents.send('ask:stateUpdate', this.state);
|
||||
}
|
||||
}
|
||||
|
||||
async toggleAskButton() {
|
||||
const askWindow = getWindowPool()?.get('ask');
|
||||
|
||||
// 답변이 있거나 스트리밍 중일 때
|
||||
const hasContent = this.state.isStreaming || (this.state.currentResponse && this.state.currentResponse.length > 0);
|
||||
|
||||
if (askWindow && askWindow.isVisible() && hasContent) {
|
||||
// 창을 닫는 대신, 텍스트 입력창만 토글합니다.
|
||||
this.state.showTextInput = !this.state.showTextInput;
|
||||
this._broadcastState(); // 변경된 상태 전파
|
||||
} else {
|
||||
// 기존의 창 보이기/숨기기 로직
|
||||
if (askWindow && askWindow.isVisible()) {
|
||||
askWindow.webContents.send('window-hide-animation');
|
||||
this.state.isVisible = false;
|
||||
} else {
|
||||
console.log('[AskService] Showing hidden Ask window');
|
||||
this.state.isVisible = true;
|
||||
askWindow?.show();
|
||||
updateLayout();
|
||||
askWindow?.webContents.send('window-show-animation');
|
||||
}
|
||||
// 창이 다시 열릴 때를 대비해 상태를 초기화하고 전파합니다.
|
||||
if (this.state.isVisible) {
|
||||
this.state.showTextInput = true;
|
||||
this._broadcastState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string[]} conversationTexts
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
_formatConversationForPrompt(conversationTexts) {
|
||||
if (!conversationTexts || conversationTexts.length === 0) {
|
||||
return 'No conversation history available.';
|
||||
}
|
||||
return conversationTexts.slice(-30).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} userPrompt
|
||||
* @returns {Promise<{success: boolean, response?: string, error?: string}>}
|
||||
*/
|
||||
async sendMessage(userPrompt, conversationHistoryRaw=[]) {
|
||||
ensureAskWindowVisible();
|
||||
|
||||
if (this.abortController) {
|
||||
this.abortController.abort('New request received.');
|
||||
}
|
||||
this.abortController = new AbortController();
|
||||
const { signal } = this.abortController;
|
||||
|
||||
if (!userPrompt || userPrompt.trim().length === 0) {
|
||||
console.warn('[AskService] Cannot process empty message');
|
||||
return { success: false, error: 'Empty message' };
|
||||
}
|
||||
|
||||
let sessionId;
|
||||
|
||||
try {
|
||||
console.log(`[AskService] 🤖 Processing message: ${userPrompt.substring(0, 50)}...`);
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
isLoading: true,
|
||||
isStreaming: false,
|
||||
currentQuestion: userPrompt,
|
||||
currentResponse: '',
|
||||
showTextInput: false,
|
||||
};
|
||||
this._broadcastState();
|
||||
|
||||
sessionId = await sessionRepository.getOrCreateActive('ask');
|
||||
await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() });
|
||||
console.log(`[AskService] DB: Saved user prompt to session ${sessionId}`);
|
||||
|
||||
const modelInfo = modelStateService.getCurrentModelInfo('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;
|
||||
|
||||
const conversationHistory = this._formatConversationForPrompt(conversationHistoryRaw);
|
||||
|
||||
const systemPrompt = getSystemPrompt('pickle_glass_analysis', conversationHistory, false);
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: `User Request: ${userPrompt.trim()}` },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
if (screenshotBase64) {
|
||||
messages[1].content.push({
|
||||
type: 'image_url',
|
||||
image_url: { url: `data:image/jpeg;base64,${screenshotBase64}` },
|
||||
});
|
||||
}
|
||||
|
||||
const streamingLLM = createStreamingLLM(modelInfo.provider, {
|
||||
apiKey: modelInfo.apiKey,
|
||||
model: modelInfo.model,
|
||||
temperature: 0.7,
|
||||
maxTokens: 2048,
|
||||
usePortkey: modelInfo.provider === 'openai-glass',
|
||||
portkeyVirtualKey: modelInfo.provider === 'openai-glass' ? modelInfo.apiKey : undefined,
|
||||
});
|
||||
|
||||
const response = await streamingLLM.streamChat(messages);
|
||||
const askWin = getWindowPool()?.get('ask');
|
||||
|
||||
if (!askWin || askWin.isDestroyed()) {
|
||||
console.error("[AskService] Ask window is not available to send stream to.");
|
||||
response.body.getReader().cancel();
|
||||
return { success: false, error: 'Ask window is not available.' };
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
signal.addEventListener('abort', () => {
|
||||
console.log(`[AskService] Aborting stream reader. Reason: ${signal.reason}`);
|
||||
reader.cancel(signal.reason).catch(() => { /* 이미 취소된 경우의 오류는 무시 */ });
|
||||
});
|
||||
|
||||
await this._processStream(reader, askWin, sessionId, signal);
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('[AskService] SendMessage operation was successfully aborted.');
|
||||
return { success: true, response: 'Cancelled' };
|
||||
}
|
||||
|
||||
console.error('[AskService] Error processing message:', error);
|
||||
this.state.isLoading = false;
|
||||
this.state.error = error.message;
|
||||
this._broadcastState();
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ReadableStreamDefaultReader} reader
|
||||
* @param {BrowserWindow} askWin
|
||||
* @param {number} sessionId
|
||||
* @param {AbortSignal} signal
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
async _processStream(reader, askWin, sessionId, signal) {
|
||||
const decoder = new TextDecoder();
|
||||
let fullResponse = '';
|
||||
|
||||
const askWin = windowPool.get('ask');
|
||||
if (!askWin || askWin.isDestroyed()) {
|
||||
console.error("[AskService] Ask window is not available to send stream to.");
|
||||
reader.cancel();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.state.isLoading = false;
|
||||
this.state.isStreaming = true;
|
||||
this._broadcastState();
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const chunk = decoder.decode(value);
|
||||
const lines = chunk.split('\n').filter(line => line.trim() !== '');
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
const lines = chunk.split('\n').filter(line => line.trim() !== '');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.substring(6);
|
||||
if (data === '[DONE]') {
|
||||
askWin.webContents.send('ask-response-stream-end');
|
||||
|
||||
// Save assistant's message to DB
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.substring(6);
|
||||
if (data === '[DONE]') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// sessionId is already available from when we saved the user prompt
|
||||
await askRepository.addAiMessage({ sessionId, role: 'assistant', content: fullResponse });
|
||||
console.log(`[AskService] DB: Saved assistant response to session ${sessionId}`);
|
||||
} catch(dbError) {
|
||||
console.error("[AskService] DB: Failed to save assistant response:", dbError);
|
||||
const json = JSON.parse(data);
|
||||
const token = json.choices[0]?.delta?.content || '';
|
||||
if (token) {
|
||||
fullResponse += token;
|
||||
this.state.currentResponse = fullResponse;
|
||||
this._broadcastState();
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
|
||||
return { success: true, response: fullResponse };
|
||||
}
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const token = json.choices[0]?.delta?.content || '';
|
||||
if (token) {
|
||||
fullResponse += token;
|
||||
askWin.webContents.send('ask-response-chunk', { token });
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore parsing errors for now
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (streamError) {
|
||||
if (signal.aborted) {
|
||||
console.log(`[AskService] Stream reading was intentionally cancelled. Reason: ${signal.reason}`);
|
||||
} else {
|
||||
console.error('[AskService] Error while processing stream:', streamError);
|
||||
if (askWin && !askWin.isDestroyed()) {
|
||||
askWin.webContents.send('ask-response-stream-error', { error: streamError.message });
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.state.isStreaming = false;
|
||||
this.state.currentResponse = fullResponse;
|
||||
this._broadcastState();
|
||||
if (fullResponse) {
|
||||
try {
|
||||
await askRepository.addAiMessage({ sessionId, role: 'assistant', content: fullResponse });
|
||||
console.log(`[AskService] DB: Saved partial or full assistant response to session ${sessionId} after stream ended.`);
|
||||
} catch(dbError) {
|
||||
console.error("[AskService] DB: Failed to save assistant response after stream ended:", dbError);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[AskService] Error processing message:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
ipcMain.handle('ask:sendMessage', async (event, userPrompt) => {
|
||||
return sendMessage(userPrompt);
|
||||
});
|
||||
console.log('[AskService] Initialized and ready.');
|
||||
}
|
||||
const askService = new AskService();
|
||||
|
||||
module.exports = {
|
||||
initialize,
|
||||
};
|
||||
module.exports = askService;
|
@ -1,6 +1,6 @@
|
||||
const { collection, addDoc, query, getDocs, orderBy, Timestamp } = require('firebase/firestore');
|
||||
const { getFirestoreInstance } = require('../../../common/services/firebaseClient');
|
||||
const { createEncryptedConverter } = require('../../../common/repositories/firestoreConverter');
|
||||
const { getFirestoreInstance } = require('../../common/services/firebaseClient');
|
||||
const { createEncryptedConverter } = require('../../common/repositories/firestoreConverter');
|
||||
|
||||
const aiMessageConverter = createEncryptedConverter(['content']);
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
const sqliteRepository = require('./sqlite.repository');
|
||||
const firebaseRepository = require('./firebase.repository');
|
||||
const authService = require('../../../common/services/authService');
|
||||
const authService = require('../../common/services/authService');
|
||||
|
||||
function getBaseRepository() {
|
||||
const user = authService.getCurrentUser();
|
||||
|
@ -1,4 +1,4 @@
|
||||
const sqliteClient = require('../../../common/services/sqliteClient');
|
||||
const sqliteClient = require('../../common/services/sqliteClient');
|
||||
|
||||
function addAiMessage({ uid, sessionId, role, content, model = 'unknown' }) {
|
||||
// uid is ignored in the SQLite implementation
|
||||
|
@ -82,7 +82,7 @@ async function createSTT({ apiKey, language = 'en', callbacks = {}, usePortkey =
|
||||
silence_duration_ms: 100,
|
||||
},
|
||||
input_audio_noise_reduction: {
|
||||
type: 'far_field'
|
||||
type: 'near_field'
|
||||
}
|
||||
}
|
||||
};
|
@ -110,6 +110,13 @@ const LATEST_SCHEMA = {
|
||||
{ name: 'selected_stt_model', type: 'TEXT' },
|
||||
{ name: 'updated_at', type: 'INTEGER' }
|
||||
]
|
||||
},
|
||||
shortcuts: {
|
||||
columns: [
|
||||
{ name: 'action', type: 'TEXT PRIMARY KEY' },
|
||||
{ name: 'accelerator', type: 'TEXT NOT NULL' },
|
||||
{ name: 'created_at', type: 'INTEGER' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
const sqliteRepository = require('./sqlite.repository');
|
||||
const firebaseRepository = require('./firebase.repository');
|
||||
const authService = require('../../../common/services/authService');
|
||||
const authService = require('../../services/authService');
|
||||
|
||||
function getBaseRepository() {
|
||||
const user = authService.getCurrentUser();
|
@ -1,5 +1,5 @@
|
||||
const { onAuthStateChanged, signInWithCustomToken, signOut } = require('firebase/auth');
|
||||
const { BrowserWindow } = require('electron');
|
||||
const { BrowserWindow, shell } = require('electron');
|
||||
const { getFirebaseAuth } = require('./firebaseClient');
|
||||
const fetch = require('node-fetch');
|
||||
const encryptionService = require('./encryptionService');
|
||||
@ -131,6 +131,19 @@ class AuthService {
|
||||
return this.initializationPromise;
|
||||
}
|
||||
|
||||
async startFirebaseAuthFlow() {
|
||||
try {
|
||||
const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000';
|
||||
const authUrl = `${webUrl}/login?mode=electron`;
|
||||
console.log(`[AuthService] Opening Firebase auth URL in browser: ${authUrl}`);
|
||||
await shell.openExternal(authUrl);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[AuthService] Failed to open Firebase auth URL:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async signInWithCustomToken(token) {
|
||||
const auth = getFirebaseAuth();
|
||||
try {
|
@ -2,10 +2,10 @@ export class LocalProgressTracker {
|
||||
constructor(serviceName) {
|
||||
this.serviceName = serviceName;
|
||||
this.activeOperations = new Map(); // operationId -> { controller, onProgress }
|
||||
this.ipcRenderer = window.require?.('electron')?.ipcRenderer;
|
||||
|
||||
if (!this.ipcRenderer) {
|
||||
throw new Error(`${serviceName} requires Electron environment`);
|
||||
// Check if we're in renderer process with window.api available
|
||||
if (!window.api) {
|
||||
throw new Error(`${serviceName} requires Electron environment with contextBridge`);
|
||||
}
|
||||
|
||||
this.globalProgressHandler = (event, data) => {
|
||||
@ -15,14 +15,14 @@ export class LocalProgressTracker {
|
||||
}
|
||||
};
|
||||
|
||||
const progressEvents = {
|
||||
'ollama': 'ollama:pull-progress',
|
||||
'whisper': 'whisper:download-progress'
|
||||
};
|
||||
// Set up progress listeners based on service name
|
||||
if (serviceName.toLowerCase() === 'ollama') {
|
||||
window.api.settingsView.onOllamaPullProgress(this.globalProgressHandler);
|
||||
} else if (serviceName.toLowerCase() === 'whisper') {
|
||||
window.api.settingsView.onWhisperDownloadProgress(this.globalProgressHandler);
|
||||
}
|
||||
|
||||
const eventName = progressEvents[serviceName.toLowerCase()] || `${serviceName}:progress`;
|
||||
this.progressEvent = eventName;
|
||||
this.ipcRenderer.on(eventName, this.globalProgressHandler);
|
||||
this.progressEvent = serviceName.toLowerCase();
|
||||
}
|
||||
|
||||
async trackOperation(operationId, operationType, onProgress) {
|
||||
@ -35,15 +35,16 @@ export class LocalProgressTracker {
|
||||
this.activeOperations.set(operationId, operation);
|
||||
|
||||
try {
|
||||
const ipcChannels = {
|
||||
'ollama': { install: 'ollama:pull-model' },
|
||||
'whisper': { download: 'whisper:download-model' }
|
||||
};
|
||||
let result;
|
||||
|
||||
const channel = ipcChannels[this.serviceName.toLowerCase()]?.[operationType] ||
|
||||
`${this.serviceName}:${operationType}`;
|
||||
|
||||
const result = await this.ipcRenderer.invoke(channel, operationId);
|
||||
// Use appropriate API call based on service and operation
|
||||
if (this.serviceName.toLowerCase() === 'ollama' && operationType === 'install') {
|
||||
result = await window.api.settingsView.pullOllamaModel(operationId);
|
||||
} else if (this.serviceName.toLowerCase() === 'whisper' && operationType === 'download') {
|
||||
result = await window.api.settingsView.downloadWhisperModel(operationId);
|
||||
} else {
|
||||
throw new Error(`Unsupported operation: ${this.serviceName}:${operationType}`);
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || `${operationType} failed`);
|
||||
@ -93,8 +94,12 @@ export class LocalProgressTracker {
|
||||
|
||||
destroy() {
|
||||
this.cancelAllOperations();
|
||||
if (this.ipcRenderer) {
|
||||
this.ipcRenderer.removeListener(this.progressEvent, this.globalProgressHandler);
|
||||
|
||||
// Remove progress listeners based on service name
|
||||
if (this.progressEvent === 'ollama') {
|
||||
window.api.settingsView.removeOnOllamaPullProgress(this.globalProgressHandler);
|
||||
} else if (this.progressEvent === 'whisper') {
|
||||
window.api.settingsView.removeOnWhisperDownloadProgress(this.globalProgressHandler);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,9 +5,9 @@ const encryptionService = require('../services/encryptionService');
|
||||
const sqliteSessionRepo = require('../repositories/session/sqlite.repository');
|
||||
const sqlitePresetRepo = require('../repositories/preset/sqlite.repository');
|
||||
const sqliteUserRepo = require('../repositories/user/sqlite.repository');
|
||||
const sqliteSttRepo = require('../../features/listen/stt/repositories/sqlite.repository');
|
||||
const sqliteSummaryRepo = require('../../features/listen/summary/repositories/sqlite.repository');
|
||||
const sqliteAiMessageRepo = require('../../features/ask/repositories/sqlite.repository');
|
||||
const sqliteSttRepo = require('../../listen/stt/repositories/sqlite.repository');
|
||||
const sqliteSummaryRepo = require('../../listen/summary/repositories/sqlite.repository');
|
||||
const sqliteAiMessageRepo = require('../../ask/repositories/sqlite.repository');
|
||||
|
||||
const MAX_BATCH_OPERATIONS = 500;
|
||||
|
@ -6,8 +6,11 @@ const encryptionService = require('./encryptionService');
|
||||
const providerSettingsRepository = require('../repositories/providerSettings');
|
||||
const userModelSelectionsRepository = require('../repositories/userModelSelections');
|
||||
|
||||
// Import authService directly (singleton)
|
||||
const authService = require('./authService');
|
||||
|
||||
class ModelStateService {
|
||||
constructor(authService) {
|
||||
constructor() {
|
||||
this.authService = authService;
|
||||
this.store = new Store({ name: 'pickle-glass-model-state' });
|
||||
this.state = {};
|
||||
@ -21,7 +24,6 @@ class ModelStateService {
|
||||
async initialize() {
|
||||
console.log('[ModelStateService] Initializing...');
|
||||
await this._loadStateForCurrentUser();
|
||||
this.setupIpcHandlers();
|
||||
console.log('[ModelStateService] Initialization complete');
|
||||
}
|
||||
|
||||
@ -34,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
|
||||
@ -52,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
|
||||
@ -326,10 +330,20 @@ class ModelStateService {
|
||||
this._logCurrentSelection();
|
||||
}
|
||||
|
||||
setApiKey(provider, key) {
|
||||
async setApiKey(provider, key) {
|
||||
if (provider in this.state.apiKeys) {
|
||||
this.state.apiKeys[provider] = key;
|
||||
this._saveState();
|
||||
|
||||
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;
|
||||
}
|
||||
return false;
|
||||
@ -345,6 +359,7 @@ class ModelStateService {
|
||||
}
|
||||
|
||||
removeApiKey(provider) {
|
||||
console.log(`[ModelStateService] Removing API key for provider: ${provider}`);
|
||||
if (provider in this.state.apiKeys) {
|
||||
this.state.apiKeys[provider] = null;
|
||||
const llmProvider = this.getProviderForModel('llm', this.state.selectedModels.llm);
|
||||
@ -392,6 +407,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') {
|
||||
@ -506,6 +523,43 @@ class ModelStateService {
|
||||
}
|
||||
}
|
||||
|
||||
getProviderConfig() {
|
||||
const serializableProviders = {};
|
||||
for (const key in PROVIDERS) {
|
||||
const { handler, ...rest } = PROVIDERS[key];
|
||||
serializableProviders[key] = rest;
|
||||
}
|
||||
return serializableProviders;
|
||||
}
|
||||
|
||||
async handleValidateKey(provider, key) {
|
||||
const result = await this.validateApiKey(provider, key);
|
||||
if (result.success) {
|
||||
// Use 'local' as placeholder for local services
|
||||
const finalKey = (provider === 'ollama' || provider === 'whisper') ? 'local' : key;
|
||||
await this.setApiKey(provider, finalKey);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async handleRemoveApiKey(provider) {
|
||||
console.log(`[ModelStateService] handleRemoveApiKey: ${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;
|
||||
}
|
||||
|
||||
async handleSetSelectedModel(type, modelId) {
|
||||
return this.setSelectedModel(type, modelId);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {('llm' | 'stt')} type
|
||||
@ -527,55 +581,8 @@ class ModelStateService {
|
||||
return { provider, model, apiKey };
|
||||
}
|
||||
|
||||
setupIpcHandlers() {
|
||||
ipcMain.handle('model:validate-key', async (e, { provider, key }) => {
|
||||
const result = await this.validateApiKey(provider, key);
|
||||
if (result.success) {
|
||||
// Use 'local' as placeholder for local services
|
||||
const finalKey = (provider === 'ollama' || provider === 'whisper') ? 'local' : key;
|
||||
this.setApiKey(provider, finalKey);
|
||||
// After setting the key, auto-select models
|
||||
this._autoSelectAvailableModels();
|
||||
this._saveState(); // Ensure state is saved after model selection
|
||||
}
|
||||
return result;
|
||||
});
|
||||
ipcMain.handle('model:get-all-keys', () => this.getAllApiKeys());
|
||||
ipcMain.handle('model:set-api-key', async (e, { provider, key }) => {
|
||||
const success = this.setApiKey(provider, key);
|
||||
if (success) {
|
||||
this._autoSelectAvailableModels();
|
||||
await this._saveState();
|
||||
}
|
||||
return success;
|
||||
});
|
||||
ipcMain.handle('model:remove-api-key', async (e, { provider }) => {
|
||||
const success = this.removeApiKey(provider);
|
||||
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', async (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;
|
||||
// Export singleton instance
|
||||
const modelStateService = new ModelStateService();
|
||||
module.exports = modelStateService;
|
@ -7,6 +7,7 @@ const { app } = require('electron');
|
||||
const LocalAIServiceBase = require('./localAIServiceBase');
|
||||
const { spawnAsync } = require('../utils/spawnHelper');
|
||||
const { DOWNLOAD_CHECKSUMS } = require('../config/checksums');
|
||||
const ollamaModelRepository = require('../repositories/ollamaModel');
|
||||
|
||||
class OllamaService extends LocalAIServiceBase {
|
||||
constructor() {
|
||||
@ -39,6 +40,26 @@ class OllamaService extends LocalAIServiceBase {
|
||||
this._startHealthMonitoring();
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
try {
|
||||
const installed = await this.isInstalled();
|
||||
if (!installed) {
|
||||
return { success: true, installed: false, running: false, models: [] };
|
||||
}
|
||||
|
||||
const running = await this.isServiceRunning();
|
||||
if (!running) {
|
||||
return { success: true, installed: true, running: false, models: [] };
|
||||
}
|
||||
|
||||
const models = await this.getInstalledModels();
|
||||
return { success: true, installed: true, running: true, models };
|
||||
} catch (error) {
|
||||
console.error('[OllamaService] Error getting status:', error);
|
||||
return { success: false, error: error.message, installed: false, running: false, models: [] };
|
||||
}
|
||||
}
|
||||
|
||||
getOllamaCliPath() {
|
||||
if (this.getPlatform() === 'darwin') {
|
||||
return '/Applications/Ollama.app/Contents/Resources/ollama';
|
||||
@ -802,6 +823,183 @@ class OllamaService extends LocalAIServiceBase {
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
async handleGetStatus() {
|
||||
try {
|
||||
const installed = await this.isInstalled();
|
||||
if (!installed) {
|
||||
return { success: true, installed: false, running: false, models: [] };
|
||||
}
|
||||
|
||||
const running = await this.isServiceRunning();
|
||||
if (!running) {
|
||||
return { success: true, installed: true, running: false, models: [] };
|
||||
}
|
||||
|
||||
const models = await this.getAllModelsWithStatus();
|
||||
return { success: true, installed: true, running: true, models };
|
||||
} catch (error) {
|
||||
console.error('[OllamaService] Error getting status:', error);
|
||||
return { success: false, error: error.message, installed: false, running: false, models: [] };
|
||||
}
|
||||
}
|
||||
|
||||
async handleInstall(event) {
|
||||
try {
|
||||
const onProgress = (data) => {
|
||||
event.sender.send('ollama:install-progress', data);
|
||||
};
|
||||
|
||||
await this.autoInstall(onProgress);
|
||||
|
||||
if (!await this.isServiceRunning()) {
|
||||
onProgress({ stage: 'starting', message: 'Starting Ollama service...', progress: 0 });
|
||||
await this.startService();
|
||||
onProgress({ stage: 'starting', message: 'Ollama service started.', progress: 100 });
|
||||
}
|
||||
event.sender.send('ollama:install-complete', { success: true });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[OllamaService] Failed to install:', error);
|
||||
event.sender.send('ollama:install-complete', { success: false, error: error.message });
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async handleStartService(event) {
|
||||
try {
|
||||
if (!await this.isServiceRunning()) {
|
||||
console.log('[OllamaService] Starting Ollama service...');
|
||||
await this.startService();
|
||||
}
|
||||
event.sender.send('ollama:install-complete', { success: true });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[OllamaService] Failed to start service:', error);
|
||||
event.sender.send('ollama:install-complete', { success: false, error: error.message });
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async handleEnsureReady() {
|
||||
try {
|
||||
if (await this.isInstalled() && !await this.isServiceRunning()) {
|
||||
console.log('[OllamaService] Ollama installed but not running, starting service...');
|
||||
await this.startService();
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[OllamaService] Failed to ensure ready:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async handleGetModels() {
|
||||
try {
|
||||
const models = await this.getAllModelsWithStatus();
|
||||
return { success: true, models };
|
||||
} catch (error) {
|
||||
console.error('[OllamaService] Failed to get models:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async handleGetModelSuggestions() {
|
||||
try {
|
||||
const suggestions = await this.getModelSuggestions();
|
||||
return { success: true, suggestions };
|
||||
} catch (error) {
|
||||
console.error('[OllamaService] Failed to get model suggestions:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async handlePullModel(event, modelName) {
|
||||
try {
|
||||
console.log(`[OllamaService] Starting model pull: ${modelName}`);
|
||||
|
||||
await ollamaModelRepository.updateInstallStatus(modelName, false, true);
|
||||
|
||||
const progressHandler = (data) => {
|
||||
if (data.model === modelName) {
|
||||
event.sender.send('ollama:pull-progress', data);
|
||||
}
|
||||
};
|
||||
|
||||
const completeHandler = (data) => {
|
||||
if (data.model === modelName) {
|
||||
console.log(`[OllamaService] Model ${modelName} pull completed`);
|
||||
this.removeListener('pull-progress', progressHandler);
|
||||
this.removeListener('pull-complete', completeHandler);
|
||||
}
|
||||
};
|
||||
|
||||
this.on('pull-progress', progressHandler);
|
||||
this.on('pull-complete', completeHandler);
|
||||
|
||||
await this.pullModel(modelName);
|
||||
|
||||
await ollamaModelRepository.updateInstallStatus(modelName, true, false);
|
||||
|
||||
console.log(`[OllamaService] Model ${modelName} pull successful`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[OllamaService] Failed to pull model:', error);
|
||||
await ollamaModelRepository.updateInstallStatus(modelName, false, false);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async handleIsModelInstalled(modelName) {
|
||||
try {
|
||||
const installed = await this.isModelInstalled(modelName);
|
||||
return { success: true, installed };
|
||||
} catch (error) {
|
||||
console.error('[OllamaService] Failed to check model installation:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async handleWarmUpModel(modelName) {
|
||||
try {
|
||||
const success = await this.warmUpModel(modelName);
|
||||
return { success };
|
||||
} catch (error) {
|
||||
console.error('[OllamaService] Failed to warm up model:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async handleAutoWarmUp() {
|
||||
try {
|
||||
const success = await this.autoWarmUpSelectedModel();
|
||||
return { success };
|
||||
} catch (error) {
|
||||
console.error('[OllamaService] Failed to auto warm-up:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async handleGetWarmUpStatus() {
|
||||
try {
|
||||
const status = this.getWarmUpStatus();
|
||||
return { success: true, status };
|
||||
} catch (error) {
|
||||
console.error('[OllamaService] Failed to get warm-up status:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async handleShutdown(event, force = false) {
|
||||
try {
|
||||
console.log(`[OllamaService] Manual shutdown requested (force: ${force})`);
|
||||
const success = await this.shutdown(force);
|
||||
return { success };
|
||||
} catch (error) {
|
||||
console.error('[OllamaService] Failed to shutdown Ollama:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
119
src/features/common/services/permissionService.js
Normal file
@ -0,0 +1,119 @@
|
||||
const { systemPreferences, shell, desktopCapturer } = require('electron');
|
||||
const permissionRepository = require('../repositories/permission');
|
||||
|
||||
class PermissionService {
|
||||
async checkSystemPermissions() {
|
||||
const permissions = {
|
||||
microphone: 'unknown',
|
||||
screen: 'unknown',
|
||||
needsSetup: true
|
||||
};
|
||||
|
||||
try {
|
||||
if (process.platform === 'darwin') {
|
||||
const micStatus = systemPreferences.getMediaAccessStatus('microphone');
|
||||
console.log('[Permissions] Microphone status:', micStatus);
|
||||
permissions.microphone = micStatus;
|
||||
|
||||
const screenStatus = systemPreferences.getMediaAccessStatus('screen');
|
||||
console.log('[Permissions] Screen status:', screenStatus);
|
||||
permissions.screen = screenStatus;
|
||||
|
||||
permissions.needsSetup = micStatus !== 'granted' || screenStatus !== 'granted';
|
||||
} else {
|
||||
permissions.microphone = 'granted';
|
||||
permissions.screen = 'granted';
|
||||
permissions.needsSetup = false;
|
||||
}
|
||||
|
||||
console.log('[Permissions] System permissions status:', permissions);
|
||||
return permissions;
|
||||
} catch (error) {
|
||||
console.error('[Permissions] Error checking permissions:', error);
|
||||
return {
|
||||
microphone: 'unknown',
|
||||
screen: 'unknown',
|
||||
needsSetup: true,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async requestMicrophonePermission() {
|
||||
if (process.platform !== 'darwin') {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const status = systemPreferences.getMediaAccessStatus('microphone');
|
||||
console.log('[Permissions] Microphone status:', status);
|
||||
if (status === 'granted') {
|
||||
return { success: true, status: 'granted' };
|
||||
}
|
||||
|
||||
const granted = await systemPreferences.askForMediaAccess('microphone');
|
||||
return {
|
||||
success: granted,
|
||||
status: granted ? 'granted' : 'denied'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Permissions] Error requesting microphone permission:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async openSystemPreferences(section) {
|
||||
if (process.platform !== 'darwin') {
|
||||
return { success: false, error: 'Not supported on this platform' };
|
||||
}
|
||||
|
||||
try {
|
||||
if (section === 'screen-recording') {
|
||||
try {
|
||||
console.log('[Permissions] Triggering screen capture request to register app...');
|
||||
await desktopCapturer.getSources({
|
||||
types: ['screen'],
|
||||
thumbnailSize: { width: 1, height: 1 }
|
||||
});
|
||||
console.log('[Permissions] App registered for screen recording');
|
||||
} catch (captureError) {
|
||||
console.log('[Permissions] Screen capture request triggered (expected to fail):', captureError.message);
|
||||
}
|
||||
|
||||
// await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture');
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[Permissions] Error opening system preferences:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async markPermissionsAsCompleted() {
|
||||
try {
|
||||
await permissionRepository.markPermissionsAsCompleted();
|
||||
console.log('[Permissions] Marked permissions as completed');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[Permissions] Error marking permissions as completed:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async checkPermissionsCompleted() {
|
||||
try {
|
||||
const completed = await permissionRepository.checkPermissionsCompleted();
|
||||
console.log('[Permissions] Permissions completed status:', completed);
|
||||
return completed;
|
||||
} catch (error) {
|
||||
console.error('[Permissions] Error checking permissions completed status:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const permissionService = new PermissionService();
|
||||
module.exports = permissionService;
|
@ -169,6 +169,47 @@ class WhisperService extends LocalAIServiceBase {
|
||||
console.log(`[WhisperService] Model ${modelId} downloaded successfully`);
|
||||
}
|
||||
|
||||
async handleDownloadModel(event, modelId) {
|
||||
try {
|
||||
console.log(`[WhisperService] Handling download for model: ${modelId}`);
|
||||
|
||||
if (!this.isInitialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
const progressHandler = (data) => {
|
||||
if (data.modelId === modelId && event && event.sender) {
|
||||
event.sender.send('whisper:download-progress', data);
|
||||
}
|
||||
};
|
||||
|
||||
this.on('downloadProgress', progressHandler);
|
||||
|
||||
try {
|
||||
await this.ensureModelAvailable(modelId);
|
||||
} finally {
|
||||
this.removeListener('downloadProgress', progressHandler);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error(`[WhisperService] Failed to handle download for model ${modelId}:`, error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async handleGetInstalledModels() {
|
||||
try {
|
||||
if (!this.isInitialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
const models = await this.getInstalledModels();
|
||||
return { success: true, models };
|
||||
} catch (error) {
|
||||
console.error('[WhisperService] Failed to get installed models:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async getModelPath(modelId) {
|
||||
if (!this.isInitialized || !this.modelsDir) {
|
||||
@ -448,4 +489,6 @@ class WhisperService extends LocalAIServiceBase {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { WhisperService };
|
||||
// Export singleton instance
|
||||
const whisperService = new WhisperService();
|
||||
module.exports = whisperService;
|
@ -1,8 +1,8 @@
|
||||
const { BrowserWindow, app } = require('electron');
|
||||
const { BrowserWindow } = require('electron');
|
||||
const SttService = require('./stt/sttService');
|
||||
const SummaryService = require('./summary/summaryService');
|
||||
const authService = require('../../common/services/authService');
|
||||
const sessionRepository = require('../../common/repositories/session');
|
||||
const authService = require('../common/services/authService');
|
||||
const sessionRepository = require('../common/repositories/session');
|
||||
const sttRepository = require('./stt/repositories');
|
||||
|
||||
class ListenService {
|
||||
@ -11,8 +11,9 @@ class ListenService {
|
||||
this.summaryService = new SummaryService();
|
||||
this.currentSessionId = null;
|
||||
this.isInitializingSession = false;
|
||||
|
||||
|
||||
this.setupServiceCallbacks();
|
||||
console.log('[ListenService] Service instance created.');
|
||||
}
|
||||
|
||||
setupServiceCallbacks() {
|
||||
@ -45,6 +46,52 @@ class ListenService {
|
||||
});
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.setupIpcHandlers();
|
||||
console.log('[ListenService] Initialized and ready.');
|
||||
}
|
||||
|
||||
async handleListenRequest(listenButtonText) {
|
||||
const { windowPool, updateLayout } = require('../../window/windowManager');
|
||||
const listenWindow = windowPool.get('listen');
|
||||
const header = windowPool.get('header');
|
||||
|
||||
try {
|
||||
switch (listenButtonText) {
|
||||
case 'Listen':
|
||||
console.log('[ListenService] changeSession to "Listen"');
|
||||
listenWindow.show();
|
||||
updateLayout();
|
||||
listenWindow.webContents.send('window-show-animation');
|
||||
await this.initializeSession();
|
||||
listenWindow.webContents.send('session-state-changed', { isActive: true });
|
||||
break;
|
||||
|
||||
case 'Stop':
|
||||
console.log('[ListenService] changeSession to "Stop"');
|
||||
await this.closeSession();
|
||||
listenWindow.webContents.send('session-state-changed', { isActive: false });
|
||||
break;
|
||||
|
||||
case 'Done':
|
||||
console.log('[ListenService] changeSession to "Done"');
|
||||
listenWindow.webContents.send('window-hide-animation');
|
||||
listenWindow.webContents.send('session-state-changed', { isActive: false });
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`[ListenService] unknown listenButtonText: ${listenButtonText}`);
|
||||
}
|
||||
|
||||
header.webContents.send('listen:changeSessionResult', { success: true });
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ListenService] error in handleListenRequest:', error);
|
||||
header.webContents.send('listen:changeSessionResult', { success: false });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async handleTranscriptionComplete(speaker, text) {
|
||||
console.log(`[ListenService] Transcription complete: ${speaker} - ${text}`);
|
||||
|
||||
@ -158,8 +205,8 @@ class ListenService {
|
||||
}
|
||||
}
|
||||
|
||||
async sendAudioContent(data, mimeType) {
|
||||
return await this.sttService.sendAudioContent(data, mimeType);
|
||||
async sendMicAudioContent(data, mimeType) {
|
||||
return await this.sttService.sendMicAudioContent(data, mimeType);
|
||||
}
|
||||
|
||||
async startMacOSAudioCapture() {
|
||||
@ -183,6 +230,8 @@ class ListenService {
|
||||
// Close STT sessions
|
||||
await this.sttService.closeSessions();
|
||||
|
||||
await this.stopMacOSAudioCapture();
|
||||
|
||||
// End database session
|
||||
if (this.currentSessionId) {
|
||||
await sessionRepository.end(this.currentSessionId);
|
||||
@ -193,8 +242,6 @@ class ListenService {
|
||||
this.currentSessionId = null;
|
||||
this.summaryService.resetConversationHistory();
|
||||
|
||||
this.sendToRenderer('session-did-close');
|
||||
|
||||
console.log('Listen service session closed.');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@ -216,88 +263,58 @@ class ListenService {
|
||||
return this.summaryService.getConversationHistory();
|
||||
}
|
||||
|
||||
setupIpcHandlers() {
|
||||
const { ipcMain } = require('electron');
|
||||
|
||||
ipcMain.handle('is-session-active', async () => {
|
||||
const isActive = this.isSessionActive();
|
||||
console.log(`Checking session status. Active: ${isActive}`);
|
||||
return isActive;
|
||||
});
|
||||
|
||||
ipcMain.handle('initialize-openai', async (event, profile = 'interview', language = 'en') => {
|
||||
console.log(`Received initialize-openai request with profile: ${profile}, language: ${language}`);
|
||||
const success = await this.initializeSession(language);
|
||||
return success;
|
||||
});
|
||||
|
||||
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`를 사용하여 핸들러들을 동적으로 생성합니다.
|
||||
handleSendMicAudioContent = this._createHandler(
|
||||
this.sendMicAudioContent,
|
||||
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('close-session', async () => {
|
||||
// return await this.closeSession();
|
||||
// });
|
||||
|
||||
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:'
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = ListenService;
|
||||
const listenService = new ListenService();
|
||||
module.exports = listenService;
|
@ -1,6 +1,6 @@
|
||||
const { collection, addDoc, query, getDocs, orderBy, Timestamp } = require('firebase/firestore');
|
||||
const { getFirestoreInstance } = require('../../../../common/services/firebaseClient');
|
||||
const { createEncryptedConverter } = require('../../../../common/repositories/firestoreConverter');
|
||||
const { getFirestoreInstance } = require('../../../common/services/firebaseClient');
|
||||
const { createEncryptedConverter } = require('../../../common/repositories/firestoreConverter');
|
||||
|
||||
const transcriptConverter = createEncryptedConverter(['text']);
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
const sqliteRepository = require('./sqlite.repository');
|
||||
const firebaseRepository = require('./firebase.repository');
|
||||
const authService = require('../../../../common/services/authService');
|
||||
const authService = require('../../../common/services/authService');
|
||||
|
||||
function getBaseRepository() {
|
||||
const user = authService.getCurrentUser();
|
||||
|
@ -1,4 +1,4 @@
|
||||
const sqliteClient = require('../../../../common/services/sqliteClient');
|
||||
const sqliteClient = require('../../../common/services/sqliteClient');
|
||||
|
||||
function addTranscript({ uid, sessionId, speaker, text }) {
|
||||
// uid is ignored in the SQLite implementation
|
||||
|
@ -1,7 +1,8 @@
|
||||
const { BrowserWindow } = require('electron');
|
||||
const { spawn } = require('child_process');
|
||||
const { createSTT } = require('../../../common/ai/factory');
|
||||
const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../electron/windowManager');
|
||||
const { createSTT } = require('../../common/ai/factory');
|
||||
const modelStateService = require('../../common/services/modelStateService');
|
||||
// const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager');
|
||||
|
||||
const COMPLETION_DEBOUNCE_MS = 2000;
|
||||
|
||||
@ -41,6 +42,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;
|
||||
@ -120,7 +132,7 @@ class SttService {
|
||||
async initializeSttSessions(language = 'en') {
|
||||
const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || language || 'en';
|
||||
|
||||
const modelInfo = await getCurrentModelInfo(null, { type: 'stt' });
|
||||
const modelInfo = modelStateService.getCurrentModelInfo('stt');
|
||||
if (!modelInfo || !modelInfo.apiKey) {
|
||||
throw new Error('AI model or API key is not configured.');
|
||||
}
|
||||
@ -132,6 +144,7 @@ class SttService {
|
||||
console.log('[SttService] Ignoring message - session already closed');
|
||||
return;
|
||||
}
|
||||
console.log('[SttService] handleMyMessage', message);
|
||||
|
||||
if (this.modelInfo.provider === 'whisper') {
|
||||
// Whisper STT emits 'transcription' events with different structure
|
||||
@ -367,11 +380,6 @@ class SttService {
|
||||
onclose: event => console.log('Their STT session closed:', event.reason),
|
||||
},
|
||||
};
|
||||
|
||||
// Determine auth options for providers that support it
|
||||
// const authService = require('../../../common/services/authService');
|
||||
// const userState = authService.getCurrentUser();
|
||||
// const loggedIn = userState.isLoggedIn;
|
||||
|
||||
const sttOptions = {
|
||||
apiKey: this.modelInfo.apiKey,
|
||||
@ -393,7 +401,7 @@ class SttService {
|
||||
return true;
|
||||
}
|
||||
|
||||
async sendAudioContent(data, mimeType) {
|
||||
async sendMicAudioContent(data, mimeType) {
|
||||
// const provider = await this.getAiProvider();
|
||||
// const isGemini = provider === 'gemini';
|
||||
|
||||
@ -404,7 +412,7 @@ class SttService {
|
||||
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' });
|
||||
modelInfo = modelStateService.getCurrentModelInfo('stt');
|
||||
}
|
||||
if (!modelInfo) {
|
||||
throw new Error('STT model info could not be retrieved.');
|
||||
@ -425,7 +433,7 @@ class SttService {
|
||||
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' });
|
||||
modelInfo = modelStateService.getCurrentModelInfo('stt');
|
||||
}
|
||||
if (!modelInfo) {
|
||||
throw new Error('STT model info could not be retrieved.');
|
||||
@ -476,8 +484,8 @@ class SttService {
|
||||
const { app } = require('electron');
|
||||
const path = require('path');
|
||||
const systemAudioPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, 'app.asar.unpacked', 'src', 'assets', 'SystemAudioDump')
|
||||
: path.join(app.getAppPath(), 'src', 'assets', 'SystemAudioDump');
|
||||
? path.join(process.resourcesPath, 'app.asar.unpacked', 'src', 'ui', 'assets', 'SystemAudioDump')
|
||||
: path.join(app.getAppPath(), 'src', 'ui', 'assets', 'SystemAudioDump');
|
||||
|
||||
console.log('SystemAudioDump path:', systemAudioPath);
|
||||
|
||||
@ -506,7 +514,7 @@ class SttService {
|
||||
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' });
|
||||
modelInfo = modelStateService.getCurrentModelInfo('stt');
|
||||
}
|
||||
if (!modelInfo) {
|
||||
throw new Error('STT model info could not be retrieved.');
|
||||
|
@ -1,7 +1,7 @@
|
||||
const { collection, doc, setDoc, getDoc, Timestamp } = require('firebase/firestore');
|
||||
const { getFirestoreInstance } = require('../../../../common/services/firebaseClient');
|
||||
const { createEncryptedConverter } = require('../../../../common/repositories/firestoreConverter');
|
||||
const encryptionService = require('../../../../common/services/encryptionService');
|
||||
const { getFirestoreInstance } = require('../../../common/services/firebaseClient');
|
||||
const { createEncryptedConverter } = require('../../../common/repositories/firestoreConverter');
|
||||
const encryptionService = require('../../../common/services/encryptionService');
|
||||
|
||||
const fieldsToEncrypt = ['tldr', 'text', 'bullet_json', 'action_json'];
|
||||
const summaryConverter = createEncryptedConverter(fieldsToEncrypt);
|
||||
|
@ -1,6 +1,6 @@
|
||||
const sqliteRepository = require('./sqlite.repository');
|
||||
const firebaseRepository = require('./firebase.repository');
|
||||
const authService = require('../../../../common/services/authService');
|
||||
const authService = require('../../../common/services/authService');
|
||||
|
||||
function getBaseRepository() {
|
||||
const user = authService.getCurrentUser();
|
||||
|
@ -1,4 +1,4 @@
|
||||
const sqliteClient = require('../../../../common/services/sqliteClient');
|
||||
const sqliteClient = require('../../../common/services/sqliteClient');
|
||||
|
||||
function saveSummary({ uid, sessionId, tldr, text, bullet_json, action_json, model = 'unknown' }) {
|
||||
// uid is ignored in the SQLite implementation
|
||||
|
@ -1,10 +1,10 @@
|
||||
const { BrowserWindow } = require('electron');
|
||||
const { getSystemPrompt } = require('../../../common/prompts/promptBuilder.js');
|
||||
const { createLLM } = require('../../../common/ai/factory');
|
||||
const authService = require('../../../common/services/authService');
|
||||
const sessionRepository = require('../../../common/repositories/session');
|
||||
const { getSystemPrompt } = require('../../common/prompts/promptBuilder.js');
|
||||
const { createLLM } = require('../../common/ai/factory');
|
||||
const sessionRepository = require('../../common/repositories/session');
|
||||
const summaryRepository = require('./repositories');
|
||||
const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../electron/windowManager');
|
||||
const modelStateService = require('../../common/services/modelStateService');
|
||||
// const { getStoredApiKey, getStoredProvider, getCurrentModelInfo } = require('../../../window/windowManager.js');
|
||||
|
||||
class SummaryService {
|
||||
constructor() {
|
||||
@ -98,7 +98,7 @@ Please build upon this context while analyzing the new conversation segments.
|
||||
await sessionRepository.touch(this.currentSessionId);
|
||||
}
|
||||
|
||||
const modelInfo = await getCurrentModelInfo(null, { type: 'llm' });
|
||||
const modelInfo = modelStateService.getCurrentModelInfo('llm');
|
||||
if (!modelInfo || !modelInfo.apiKey) {
|
||||
throw new Error('AI model or API key is not configured.');
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
const { collection, doc, addDoc, getDoc, getDocs, updateDoc, deleteDoc, query, where, orderBy } = require('firebase/firestore');
|
||||
const { getFirestoreInstance } = require('../../../common/services/firebaseClient');
|
||||
const { createEncryptedConverter } = require('../../../common/repositories/firestoreConverter');
|
||||
const encryptionService = require('../../../common/services/encryptionService');
|
||||
const { getFirestoreInstance } = require('../../common/services/firebaseClient');
|
||||
const { createEncryptedConverter } = require('../../common/repositories/firestoreConverter');
|
||||
const encryptionService = require('../../common/services/encryptionService');
|
||||
|
||||
const userPresetConverter = createEncryptedConverter(['prompt', 'title']);
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
const sqliteRepository = require('./sqlite.repository');
|
||||
const firebaseRepository = require('./firebase.repository');
|
||||
const authService = require('../../../common/services/authService');
|
||||
const authService = require('../../common/services/authService');
|
||||
|
||||
function getBaseRepository() {
|
||||
const user = authService.getCurrentUser();
|
||||
|
@ -1,4 +1,4 @@
|
||||
const sqliteClient = require('../../../common/services/sqliteClient');
|
||||
const sqliteClient = require('../../common/services/sqliteClient');
|
||||
|
||||
function getPresets(uid) {
|
||||
const db = sqliteClient.getDb();
|
||||
|
@ -1,8 +1,13 @@
|
||||
const { ipcMain, BrowserWindow } = require('electron');
|
||||
const Store = require('electron-store');
|
||||
const authService = require('../../common/services/authService');
|
||||
const authService = require('../common/services/authService');
|
||||
const settingsRepository = require('./repositories');
|
||||
const { getStoredApiKey, getStoredProvider, windowPool } = require('../../electron/windowManager');
|
||||
const { getStoredApiKey, getStoredProvider, windowPool } = require('../../window/windowManager');
|
||||
|
||||
// New imports for common services
|
||||
const modelStateService = require('../common/services/modelStateService');
|
||||
const ollamaService = require('../common/services/ollamaService');
|
||||
const whisperService = require('../common/services/whisperService');
|
||||
|
||||
const store = new Store({
|
||||
name: 'pickle-glass-settings',
|
||||
@ -19,6 +24,51 @@ const NOTIFICATION_CONFIG = {
|
||||
RETRY_BASE_DELAY: 1000, // exponential backoff base (ms)
|
||||
};
|
||||
|
||||
// New facade functions for model state management
|
||||
async function getModelSettings() {
|
||||
try {
|
||||
const [config, storedKeys, availableLlm, availableStt, selectedModels] = await Promise.all([
|
||||
modelStateService.getProviderConfig(),
|
||||
modelStateService.getAllApiKeys(),
|
||||
modelStateService.getAvailableModels('llm'),
|
||||
modelStateService.getAvailableModels('stt'),
|
||||
modelStateService.getSelectedModels(),
|
||||
]);
|
||||
return { success: true, data: { config, storedKeys, availableLlm, availableStt, selectedModels } };
|
||||
} catch (error) {
|
||||
console.error('[SettingsService] Error getting model settings:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function validateAndSaveKey(provider, key) {
|
||||
return modelStateService.handleValidateKey(provider, key);
|
||||
}
|
||||
|
||||
async function clearApiKey(provider) {
|
||||
const success = await modelStateService.handleRemoveApiKey(provider);
|
||||
return { success };
|
||||
}
|
||||
|
||||
async function setSelectedModel(type, modelId) {
|
||||
const success = await modelStateService.handleSetSelectedModel(type, modelId);
|
||||
return { success };
|
||||
}
|
||||
|
||||
// Ollama facade functions
|
||||
async function getOllamaStatus() {
|
||||
return ollamaService.getStatus();
|
||||
}
|
||||
|
||||
async function ensureOllamaReady() {
|
||||
return ollamaService.ensureReady();
|
||||
}
|
||||
|
||||
async function shutdownOllama() {
|
||||
return ollamaService.shutdown(false); // false for graceful shutdown
|
||||
}
|
||||
|
||||
|
||||
// window targeting system
|
||||
class WindowNotificationManager {
|
||||
constructor() {
|
||||
@ -324,6 +374,7 @@ async function removeApiKey() {
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[SettingsService] API key removed for all providers');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[SettingsService] Error removing API key:', error);
|
||||
@ -373,57 +424,6 @@ function initialize() {
|
||||
// cleanup
|
||||
windowNotificationManager.cleanup();
|
||||
|
||||
// IPC handlers for settings
|
||||
ipcMain.handle('settings:getSettings', async () => {
|
||||
return await getSettings();
|
||||
});
|
||||
|
||||
ipcMain.handle('settings:saveSettings', async (event, settings) => {
|
||||
return await saveSettings(settings);
|
||||
});
|
||||
|
||||
// IPC handlers for presets
|
||||
ipcMain.handle('settings:getPresets', async () => {
|
||||
return await getPresets();
|
||||
});
|
||||
|
||||
ipcMain.handle('settings:getPresetTemplates', async () => {
|
||||
return await getPresetTemplates();
|
||||
});
|
||||
|
||||
ipcMain.handle('settings:createPreset', async (event, title, prompt) => {
|
||||
return await createPreset(title, prompt);
|
||||
});
|
||||
|
||||
ipcMain.handle('settings:updatePreset', async (event, id, title, prompt) => {
|
||||
return await updatePreset(id, title, prompt);
|
||||
});
|
||||
|
||||
ipcMain.handle('settings:deletePreset', async (event, id) => {
|
||||
return await deletePreset(id);
|
||||
});
|
||||
|
||||
ipcMain.handle('settings:saveApiKey', async (event, apiKey, provider) => {
|
||||
return await saveApiKey(apiKey, provider);
|
||||
});
|
||||
|
||||
ipcMain.handle('settings:removeApiKey', async () => {
|
||||
return await removeApiKey();
|
||||
});
|
||||
|
||||
ipcMain.handle('settings:updateContentProtection', async (event, enabled) => {
|
||||
return await updateContentProtection(enabled);
|
||||
});
|
||||
|
||||
ipcMain.handle('settings:get-auto-update', async () => {
|
||||
return await getAutoUpdateSetting();
|
||||
});
|
||||
|
||||
ipcMain.handle('settings:set-auto-update', async (event, isEnabled) => {
|
||||
console.log('[SettingsService] Setting auto update setting:', isEnabled);
|
||||
return await setAutoUpdateSetting(isEnabled);
|
||||
});
|
||||
|
||||
console.log('[SettingsService] Initialized and ready.');
|
||||
}
|
||||
|
||||
@ -455,4 +455,14 @@ module.exports = {
|
||||
removeApiKey,
|
||||
updateContentProtection,
|
||||
getAutoUpdateSetting,
|
||||
setAutoUpdateSetting,
|
||||
// Model settings facade
|
||||
getModelSettings,
|
||||
validateAndSaveKey,
|
||||
clearApiKey,
|
||||
setSelectedModel,
|
||||
// Ollama facade
|
||||
getOllamaStatus,
|
||||
ensureOllamaReady,
|
||||
shutdownOllama
|
||||
};
|
1
src/features/shortcuts/repositories/index.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./sqlite.repository');
|
48
src/features/shortcuts/repositories/sqlite.repository.js
Normal file
@ -0,0 +1,48 @@
|
||||
const sqliteClient = require('../../common/services/sqliteClient');
|
||||
const crypto = require('crypto');
|
||||
|
||||
function getAllKeybinds() {
|
||||
const db = sqliteClient.getDb();
|
||||
const query = 'SELECT * FROM shortcuts';
|
||||
try {
|
||||
return db.prepare(query).all();
|
||||
} catch (error) {
|
||||
console.error(`[DB] Failed to get keybinds:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function upsertKeybinds(keybinds) {
|
||||
if (!keybinds || keybinds.length === 0) return;
|
||||
|
||||
const db = sqliteClient.getDb();
|
||||
const upsert = db.transaction((items) => {
|
||||
const query = `
|
||||
INSERT INTO shortcuts (action, accelerator, created_at)
|
||||
VALUES (@action, @accelerator, @created_at)
|
||||
ON CONFLICT(action) DO UPDATE SET
|
||||
accelerator = excluded.accelerator;
|
||||
`;
|
||||
const insert = db.prepare(query);
|
||||
|
||||
for (const item of items) {
|
||||
insert.run({
|
||||
action: item.action,
|
||||
accelerator: item.accelerator,
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
upsert(keybinds);
|
||||
} catch (error) {
|
||||
console.error('[DB] Failed to upsert keybinds:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAllKeybinds,
|
||||
upsertKeybinds
|
||||
};
|
285
src/features/shortcuts/shortcutsService.js
Normal file
@ -0,0 +1,285 @@
|
||||
const { globalShortcut, screen } = require('electron');
|
||||
const shortcutsRepository = require('./repositories');
|
||||
const internalBridge = require('../../bridge/internalBridge');
|
||||
const askService = require('../ask/askService');
|
||||
|
||||
|
||||
class ShortcutsService {
|
||||
constructor() {
|
||||
this.lastVisibleWindows = new Set(['header']);
|
||||
this.mouseEventsIgnored = false;
|
||||
this.movementManager = null;
|
||||
this.windowPool = null;
|
||||
}
|
||||
|
||||
initialize(movementManager, windowPool) {
|
||||
this.movementManager = movementManager;
|
||||
this.windowPool = windowPool;
|
||||
internalBridge.on('reregister-shortcuts', () => {
|
||||
console.log('[ShortcutsService] Reregistering shortcuts due to header state change.');
|
||||
this.registerShortcuts();
|
||||
});
|
||||
console.log('[ShortcutsService] Initialized with dependencies and event listener.');
|
||||
}
|
||||
|
||||
getDefaultKeybinds() {
|
||||
const isMac = process.platform === 'darwin';
|
||||
return {
|
||||
moveUp: isMac ? 'Cmd+Up' : 'Ctrl+Up',
|
||||
moveDown: isMac ? 'Cmd+Down' : 'Ctrl+Down',
|
||||
moveLeft: isMac ? 'Cmd+Left' : 'Ctrl+Left',
|
||||
moveRight: isMac ? 'Cmd+Right' : 'Ctrl+Right',
|
||||
toggleVisibility: isMac ? 'Cmd+\\' : 'Ctrl+\\',
|
||||
toggleClickThrough: isMac ? 'Cmd+M' : 'Ctrl+M',
|
||||
nextStep: isMac ? 'Cmd+Enter' : 'Ctrl+Enter',
|
||||
manualScreenshot: isMac ? 'Cmd+Shift+S' : 'Ctrl+Shift+S',
|
||||
previousResponse: isMac ? 'Cmd+[' : 'Ctrl+[',
|
||||
nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]',
|
||||
scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up',
|
||||
scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down',
|
||||
};
|
||||
}
|
||||
|
||||
async loadKeybinds() {
|
||||
let keybindsArray = await shortcutsRepository.getAllKeybinds();
|
||||
|
||||
if (!keybindsArray || keybindsArray.length === 0) {
|
||||
console.log(`[Shortcuts] No keybinds found. Loading defaults.`);
|
||||
const defaults = this.getDefaultKeybinds();
|
||||
await this.saveKeybinds(defaults);
|
||||
return defaults;
|
||||
}
|
||||
|
||||
const keybinds = {};
|
||||
keybindsArray.forEach(k => {
|
||||
keybinds[k.action] = k.accelerator;
|
||||
});
|
||||
|
||||
const defaults = this.getDefaultKeybinds();
|
||||
let needsUpdate = false;
|
||||
for (const action in defaults) {
|
||||
if (!keybinds[action]) {
|
||||
keybinds[action] = defaults[action];
|
||||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
console.log('[Shortcuts] Updating missing keybinds with defaults.');
|
||||
await this.saveKeybinds(keybinds);
|
||||
}
|
||||
|
||||
return keybinds;
|
||||
}
|
||||
|
||||
async handleSaveShortcuts(newKeybinds) {
|
||||
try {
|
||||
await this.saveKeybinds(newKeybinds);
|
||||
const shortcutEditor = this.windowPool.get('shortcut-settings');
|
||||
if (shortcutEditor && !shortcutEditor.isDestroyed()) {
|
||||
shortcutEditor.close(); // This will trigger re-registration on 'closed' event in windowManager
|
||||
} else {
|
||||
// If editor wasn't open, re-register immediately
|
||||
await this.registerShortcuts();
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Failed to save shortcuts:", error);
|
||||
// On failure, re-register old shortcuts to be safe
|
||||
await this.registerShortcuts();
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async handleRestoreDefaults() {
|
||||
const defaults = this.getDefaultKeybinds();
|
||||
await this.saveKeybinds(defaults);
|
||||
await this.registerShortcuts();
|
||||
return defaults;
|
||||
}
|
||||
|
||||
async saveKeybinds(newKeybinds) {
|
||||
const keybindsToSave = [];
|
||||
for (const action in newKeybinds) {
|
||||
if (Object.prototype.hasOwnProperty.call(newKeybinds, action)) {
|
||||
keybindsToSave.push({
|
||||
action: action,
|
||||
accelerator: newKeybinds[action],
|
||||
});
|
||||
}
|
||||
}
|
||||
await shortcutsRepository.upsertKeybinds(keybindsToSave);
|
||||
console.log(`[Shortcuts] Saved keybinds.`);
|
||||
}
|
||||
|
||||
toggleAllWindowsVisibility(windowPool) {
|
||||
const header = windowPool.get('header');
|
||||
if (!header) return;
|
||||
|
||||
if (header.isVisible()) {
|
||||
this.lastVisibleWindows.clear();
|
||||
|
||||
windowPool.forEach((win, name) => {
|
||||
if (win && !win.isDestroyed() && win.isVisible()) {
|
||||
this.lastVisibleWindows.add(name);
|
||||
}
|
||||
});
|
||||
|
||||
this.lastVisibleWindows.forEach(name => {
|
||||
if (name === 'header') return;
|
||||
const win = windowPool.get(name);
|
||||
if (win && !win.isDestroyed()) win.hide();
|
||||
});
|
||||
header.hide();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastVisibleWindows.forEach(name => {
|
||||
const win = windowPool.get(name);
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async registerShortcuts() {
|
||||
if (!this.movementManager || !this.windowPool) {
|
||||
console.error('[Shortcuts] Service not initialized. Cannot register shortcuts.');
|
||||
return;
|
||||
}
|
||||
const keybinds = await this.loadKeybinds();
|
||||
globalShortcut.unregisterAll();
|
||||
|
||||
const header = this.windowPool.get('header');
|
||||
const mainWindow = header;
|
||||
|
||||
const sendToRenderer = (channel, ...args) => {
|
||||
this.windowPool.forEach(win => {
|
||||
if (win && !win.isDestroyed()) {
|
||||
try {
|
||||
win.webContents.send(channel, ...args);
|
||||
} catch (e) {
|
||||
// Ignore errors for destroyed windows
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
sendToRenderer('shortcuts-updated', keybinds);
|
||||
|
||||
// --- Hardcoded shortcuts ---
|
||||
const isMac = process.platform === 'darwin';
|
||||
const modifier = isMac ? 'Cmd' : 'Ctrl';
|
||||
|
||||
// Monitor switching
|
||||
const displays = screen.getAllDisplays();
|
||||
if (displays.length > 1) {
|
||||
displays.forEach((display, index) => {
|
||||
const key = `${modifier}+Shift+${index + 1}`;
|
||||
globalShortcut.register(key, () => this.movementManager.moveToDisplay(display.id));
|
||||
});
|
||||
}
|
||||
|
||||
// Edge snapping
|
||||
const edgeDirections = [
|
||||
{ key: `${modifier}+Shift+Left`, direction: 'left' },
|
||||
{ key: `${modifier}+Shift+Right`, direction: 'right' },
|
||||
];
|
||||
edgeDirections.forEach(({ key, direction }) => {
|
||||
globalShortcut.register(key, () => {
|
||||
if (header && header.isVisible()) this.movementManager.moveToEdge(direction);
|
||||
});
|
||||
});
|
||||
|
||||
// --- User-configurable shortcuts ---
|
||||
if (header?.currentHeaderState === 'apikey') {
|
||||
if (keybinds.toggleVisibility) {
|
||||
globalShortcut.register(keybinds.toggleVisibility, () => this.toggleAllWindowsVisibility(this.windowPool));
|
||||
}
|
||||
console.log('[Shortcuts] ApiKeyHeader is active, only toggleVisibility shortcut is registered.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const action in keybinds) {
|
||||
const accelerator = keybinds[action];
|
||||
if (!accelerator) continue;
|
||||
|
||||
let callback;
|
||||
switch(action) {
|
||||
case 'toggleVisibility':
|
||||
callback = () => this.toggleAllWindowsVisibility(this.windowPool);
|
||||
break;
|
||||
case 'nextStep':
|
||||
callback = () => askService.toggleAskButton();
|
||||
break;
|
||||
case 'scrollUp':
|
||||
callback = () => {
|
||||
const askWindow = this.windowPool.get('ask');
|
||||
if (askWindow && !askWindow.isDestroyed() && askWindow.isVisible()) {
|
||||
askWindow.webContents.send('scroll-response-up');
|
||||
}
|
||||
};
|
||||
break;
|
||||
case 'scrollDown':
|
||||
callback = () => {
|
||||
const askWindow = this.windowPool.get('ask');
|
||||
if (askWindow && !askWindow.isDestroyed() && askWindow.isVisible()) {
|
||||
askWindow.webContents.send('scroll-response-down');
|
||||
}
|
||||
};
|
||||
break;
|
||||
case 'moveUp':
|
||||
callback = () => { if (header && header.isVisible()) this.movementManager.moveStep('up'); };
|
||||
break;
|
||||
case 'moveDown':
|
||||
callback = () => { if (header && header.isVisible()) this.movementManager.moveStep('down'); };
|
||||
break;
|
||||
case 'moveLeft':
|
||||
callback = () => { if (header && header.isVisible()) this.movementManager.moveStep('left'); };
|
||||
break;
|
||||
case 'moveRight':
|
||||
callback = () => { if (header && header.isVisible()) this.movementManager.moveStep('right'); };
|
||||
break;
|
||||
case 'toggleClickThrough':
|
||||
callback = () => {
|
||||
this.mouseEventsIgnored = !this.mouseEventsIgnored;
|
||||
if(mainWindow && !mainWindow.isDestroyed()){
|
||||
mainWindow.setIgnoreMouseEvents(this.mouseEventsIgnored, { forward: true });
|
||||
mainWindow.webContents.send('click-through-toggled', this.mouseEventsIgnored);
|
||||
}
|
||||
};
|
||||
break;
|
||||
case 'manualScreenshot':
|
||||
callback = () => {
|
||||
if(mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.executeJavaScript('window.captureManualScreenshot && window.captureManualScreenshot();');
|
||||
}
|
||||
};
|
||||
break;
|
||||
case 'previousResponse':
|
||||
callback = () => sendToRenderer('navigate-previous-response');
|
||||
break;
|
||||
case 'nextResponse':
|
||||
callback = () => sendToRenderer('navigate-next-response');
|
||||
break;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
try {
|
||||
globalShortcut.register(accelerator, callback);
|
||||
} catch(e) {
|
||||
console.error(`[Shortcuts] Failed to register shortcut for "${action}" (${accelerator}):`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('[Shortcuts] All shortcuts have been registered.');
|
||||
}
|
||||
|
||||
unregisterAll() {
|
||||
globalShortcut.unregisterAll();
|
||||
console.log('[Shortcuts] All shortcuts have been unregistered.');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ShortcutsService();
|
358
src/index.js
@ -12,11 +12,11 @@ if (require('electron-squirrel-startup')) {
|
||||
}
|
||||
|
||||
const { app, BrowserWindow, shell, ipcMain, dialog, desktopCapturer, session } = require('electron');
|
||||
const { createWindows } = require('./electron/windowManager.js');
|
||||
const ListenService = require('./features/listen/listenService');
|
||||
const { initializeFirebase } = require('./common/services/firebaseClient');
|
||||
const databaseInitializer = require('./common/services/databaseInitializer');
|
||||
const authService = require('./common/services/authService');
|
||||
const { createWindows } = require('./window/windowManager.js');
|
||||
const listenService = require('./features/listen/listenService');
|
||||
const { initializeFirebase } = require('./features/common/services/firebaseClient');
|
||||
const databaseInitializer = require('./features/common/services/databaseInitializer');
|
||||
const authService = require('./features/common/services/authService');
|
||||
const path = require('node:path');
|
||||
const express = require('express');
|
||||
const fetch = require('node-fetch');
|
||||
@ -24,27 +24,23 @@ const { autoUpdater } = require('electron-updater');
|
||||
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 sqliteClient = require('./common/services/sqliteClient');
|
||||
const sessionRepository = require('./features/common/repositories/session');
|
||||
const modelStateService = require('./features/common/services/modelStateService');
|
||||
const featureBridge = require('./bridge/featureBridge');
|
||||
const windowBridge = require('./bridge/windowBridge');
|
||||
|
||||
// Global variables
|
||||
const eventBridge = new EventEmitter();
|
||||
let WEB_PORT = 3000;
|
||||
let isShuttingDown = false; // Flag to prevent infinite shutdown loop
|
||||
|
||||
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 ////////
|
||||
|
||||
// Import and initialize OllamaService
|
||||
const ollamaService = require('./common/services/ollamaService');
|
||||
const ollamaModelRepository = require('./common/repositories/ollamaModel');
|
||||
const ollamaService = require('./features/common/services/ollamaService');
|
||||
const ollamaModelRepository = require('./features/common/repositories/ollamaModel');
|
||||
|
||||
// Native deep link handling - cross-platform compatible
|
||||
let pendingDeepLinkUrl = null;
|
||||
@ -123,7 +119,7 @@ function setupProtocolHandling() {
|
||||
}
|
||||
|
||||
function focusMainWindow() {
|
||||
const { windowPool } = require('./electron/windowManager');
|
||||
const { windowPool } = require('./window/windowManager.js');
|
||||
if (windowPool) {
|
||||
const header = windowPool.get('header');
|
||||
if (header && !header.isDestroyed()) {
|
||||
@ -202,12 +198,9 @@ app.whenReady().then(async () => {
|
||||
await modelStateService.initialize();
|
||||
//////// after_modelStateService ////////
|
||||
|
||||
listenService.setupIpcHandlers();
|
||||
askService.initialize();
|
||||
settingsService.initialize();
|
||||
setupGeneralIpcHandlers();
|
||||
setupOllamaIpcHandlers();
|
||||
setupWhisperIpcHandlers();
|
||||
featureBridge.initialize(); // 추가: featureBridge 초기화
|
||||
windowBridge.initialize();
|
||||
setupWebDataHandlers();
|
||||
|
||||
// Initialize Ollama models in database
|
||||
await ollamaModelRepository.initializeDefaultModels();
|
||||
@ -248,13 +241,6 @@ app.whenReady().then(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
listenService.stopMacOSAudioCapture();
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('before-quit', async (event) => {
|
||||
// Prevent infinite loop by checking if shutdown is already in progress
|
||||
if (isShuttingDown) {
|
||||
@ -272,7 +258,7 @@ app.on('before-quit', async (event) => {
|
||||
|
||||
try {
|
||||
// 1. Stop audio capture first (immediate)
|
||||
listenService.stopMacOSAudioCapture();
|
||||
await listenService.closeSession();
|
||||
console.log('[Shutdown] Audio capture stopped');
|
||||
|
||||
// 2. End all active sessions (database operations) - with error handling
|
||||
@ -328,309 +314,13 @@ app.on('activate', () => {
|
||||
}
|
||||
});
|
||||
|
||||
function setupWhisperIpcHandlers() {
|
||||
const { WhisperService } = require('./common/services/whisperService');
|
||||
const whisperService = new WhisperService();
|
||||
|
||||
// Forward download progress events to renderer
|
||||
whisperService.on('downloadProgress', (data) => {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
windows.forEach(window => {
|
||||
window.webContents.send('whisper:download-progress', data);
|
||||
});
|
||||
});
|
||||
|
||||
// IPC handlers for Whisper operations
|
||||
ipcMain.handle('whisper:download-model', async (event, modelId) => {
|
||||
try {
|
||||
console.log(`[Whisper IPC] Starting download for model: ${modelId}`);
|
||||
|
||||
// Ensure WhisperService is initialized first
|
||||
if (!whisperService.isInitialized) {
|
||||
console.log('[Whisper IPC] Initializing WhisperService...');
|
||||
await whisperService.initialize();
|
||||
}
|
||||
|
||||
// Set up progress listener
|
||||
const progressHandler = (data) => {
|
||||
if (data.modelId === modelId) {
|
||||
event.sender.send('whisper:download-progress', data);
|
||||
}
|
||||
};
|
||||
|
||||
whisperService.on('downloadProgress', progressHandler);
|
||||
|
||||
try {
|
||||
await whisperService.ensureModelAvailable(modelId);
|
||||
console.log(`[Whisper IPC] Model ${modelId} download completed successfully`);
|
||||
} finally {
|
||||
// Cleanup listener
|
||||
whisperService.removeListener('downloadProgress', progressHandler);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error(`[Whisper IPC] Failed to download model ${modelId}:`, error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('whisper:get-installed-models', async () => {
|
||||
try {
|
||||
// Ensure WhisperService is initialized first
|
||||
if (!whisperService.isInitialized) {
|
||||
console.log('[Whisper IPC] Initializing WhisperService for model list...');
|
||||
await whisperService.initialize();
|
||||
}
|
||||
|
||||
const models = await whisperService.getInstalledModels();
|
||||
return { success: true, models };
|
||||
} catch (error) {
|
||||
console.error('[Whisper IPC] Failed to get installed models:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupGeneralIpcHandlers() {
|
||||
const userRepository = require('./common/repositories/user');
|
||||
const presetRepository = require('./common/repositories/preset');
|
||||
|
||||
ipcMain.handle('get-user-presets', () => {
|
||||
// The adapter injects the UID.
|
||||
return presetRepository.getPresets();
|
||||
});
|
||||
|
||||
ipcMain.handle('get-preset-templates', () => {
|
||||
return presetRepository.getPresetTemplates();
|
||||
});
|
||||
|
||||
ipcMain.handle('start-firebase-auth', async () => {
|
||||
try {
|
||||
const authUrl = `http://localhost:${WEB_PORT}/login?mode=electron`;
|
||||
console.log(`[Auth] Opening Firebase auth URL in browser: ${authUrl}`);
|
||||
await shell.openExternal(authUrl);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[Auth] Failed to open Firebase auth URL:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-web-url', () => {
|
||||
return process.env.pickleglass_WEB_URL || 'http://localhost:3000';
|
||||
});
|
||||
|
||||
ipcMain.handle('get-current-user', () => {
|
||||
return authService.getCurrentUser();
|
||||
});
|
||||
|
||||
// --- Web UI Data Handlers (New) ---
|
||||
setupWebDataHandlers();
|
||||
}
|
||||
|
||||
function setupOllamaIpcHandlers() {
|
||||
// Ollama status and installation
|
||||
ipcMain.handle('ollama:get-status', async () => {
|
||||
try {
|
||||
const installed = await ollamaService.isInstalled();
|
||||
const running = installed ? await ollamaService.isServiceRunning() : false;
|
||||
const models = await ollamaService.getAllModelsWithStatus();
|
||||
|
||||
return {
|
||||
installed,
|
||||
running,
|
||||
models,
|
||||
success: true
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Ollama IPC] Failed to get status:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('ollama:install', async (event) => {
|
||||
try {
|
||||
const onProgress = (data) => {
|
||||
event.sender.send('ollama:install-progress', data);
|
||||
};
|
||||
|
||||
await ollamaService.autoInstall(onProgress);
|
||||
|
||||
if (!await ollamaService.isServiceRunning()) {
|
||||
onProgress({ stage: 'starting', message: 'Starting Ollama service...', progress: 0 });
|
||||
await ollamaService.startService();
|
||||
onProgress({ stage: 'starting', message: 'Ollama service started.', progress: 100 });
|
||||
}
|
||||
event.sender.send('ollama:install-complete', { success: true });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[Ollama IPC] Failed to install:', error);
|
||||
event.sender.send('ollama:install-complete', { success: false, error: error.message });
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('ollama:start-service', async (event) => {
|
||||
try {
|
||||
if (!await ollamaService.isServiceRunning()) {
|
||||
console.log('[Ollama IPC] Starting Ollama service...');
|
||||
await ollamaService.startService();
|
||||
}
|
||||
event.sender.send('ollama:install-complete', { success: true });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[Ollama IPC] Failed to start service:', error);
|
||||
event.sender.send('ollama:install-complete', { success: false, error: error.message });
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure Ollama is ready (starts service if installed but not running)
|
||||
ipcMain.handle('ollama:ensure-ready', async () => {
|
||||
try {
|
||||
if (await ollamaService.isInstalled() && !await ollamaService.isServiceRunning()) {
|
||||
console.log('[Ollama IPC] Ollama installed but not running, starting service...');
|
||||
await ollamaService.startService();
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[Ollama IPC] Failed to ensure ready:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Get all models with their status
|
||||
ipcMain.handle('ollama:get-models', async () => {
|
||||
try {
|
||||
const models = await ollamaService.getAllModelsWithStatus();
|
||||
return { success: true, models };
|
||||
} catch (error) {
|
||||
console.error('[Ollama IPC] Failed to get models:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Get model suggestions for autocomplete
|
||||
ipcMain.handle('ollama:get-model-suggestions', async () => {
|
||||
try {
|
||||
const suggestions = await ollamaService.getModelSuggestions();
|
||||
return { success: true, suggestions };
|
||||
} catch (error) {
|
||||
console.error('[Ollama IPC] Failed to get model suggestions:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Pull/install a specific model
|
||||
ipcMain.handle('ollama:pull-model', async (event, modelName) => {
|
||||
try {
|
||||
console.log(`[Ollama IPC] Starting model pull: ${modelName}`);
|
||||
|
||||
// Update DB status to installing
|
||||
await ollamaModelRepository.updateInstallStatus(modelName, false, true);
|
||||
|
||||
// Set up progress listener for real-time updates
|
||||
const progressHandler = (data) => {
|
||||
if (data.model === modelName) {
|
||||
event.sender.send('ollama:pull-progress', data);
|
||||
}
|
||||
};
|
||||
|
||||
const completeHandler = (data) => {
|
||||
if (data.model === modelName) {
|
||||
console.log(`[Ollama IPC] Model ${modelName} pull completed`);
|
||||
// Clean up listeners
|
||||
ollamaService.removeListener('pull-progress', progressHandler);
|
||||
ollamaService.removeListener('pull-complete', completeHandler);
|
||||
}
|
||||
};
|
||||
|
||||
ollamaService.on('pull-progress', progressHandler);
|
||||
ollamaService.on('pull-complete', completeHandler);
|
||||
|
||||
// Pull the model using REST API
|
||||
await ollamaService.pullModel(modelName);
|
||||
|
||||
// Update DB status to installed
|
||||
await ollamaModelRepository.updateInstallStatus(modelName, true, false);
|
||||
|
||||
console.log(`[Ollama IPC] Model ${modelName} pull successful`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[Ollama IPC] Failed to pull model:', error);
|
||||
// Reset status on error
|
||||
await ollamaModelRepository.updateInstallStatus(modelName, false, false);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Check if a specific model is installed
|
||||
ipcMain.handle('ollama:is-model-installed', async (event, modelName) => {
|
||||
try {
|
||||
const installed = await ollamaService.isModelInstalled(modelName);
|
||||
return { success: true, installed };
|
||||
} catch (error) {
|
||||
console.error('[Ollama IPC] Failed to check model installation:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Warm up a specific model
|
||||
ipcMain.handle('ollama:warm-up-model', async (event, modelName) => {
|
||||
try {
|
||||
const success = await ollamaService.warmUpModel(modelName);
|
||||
return { success };
|
||||
} catch (error) {
|
||||
console.error('[Ollama IPC] Failed to warm up model:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Auto warm-up currently selected model
|
||||
ipcMain.handle('ollama:auto-warm-up', async () => {
|
||||
try {
|
||||
const success = await ollamaService.autoWarmUpSelectedModel();
|
||||
return { success };
|
||||
} catch (error) {
|
||||
console.error('[Ollama IPC] Failed to auto warm-up:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Get warm-up status for debugging
|
||||
ipcMain.handle('ollama:get-warm-up-status', async () => {
|
||||
try {
|
||||
const status = ollamaService.getWarmUpStatus();
|
||||
return { success: true, status };
|
||||
} catch (error) {
|
||||
console.error('[Ollama IPC] Failed to get warm-up status:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Shutdown Ollama service manually
|
||||
ipcMain.handle('ollama:shutdown', async (event, force = false) => {
|
||||
try {
|
||||
console.log(`[Ollama IPC] Manual shutdown requested (force: ${force})`);
|
||||
const success = await ollamaService.shutdown(force);
|
||||
return { success };
|
||||
} catch (error) {
|
||||
console.error('[Ollama IPC] Failed to shutdown Ollama:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[Ollama IPC] Handlers registered');
|
||||
}
|
||||
|
||||
function setupWebDataHandlers() {
|
||||
const sessionRepository = require('./common/repositories/session');
|
||||
const sessionRepository = require('./features/common/repositories/session');
|
||||
const sttRepository = require('./features/listen/stt/repositories');
|
||||
const summaryRepository = require('./features/listen/summary/repositories');
|
||||
const askRepository = require('./features/ask/repositories');
|
||||
const userRepository = require('./common/repositories/user');
|
||||
const presetRepository = require('./common/repositories/preset');
|
||||
const userRepository = require('./features/common/repositories/user');
|
||||
const presetRepository = require('./features/common/repositories/preset');
|
||||
|
||||
const handleRequest = async (channel, responseChannel, payload) => {
|
||||
let result;
|
||||
@ -788,7 +478,7 @@ async function handleCustomUrl(url) {
|
||||
handlePersonalizeFromUrl(params);
|
||||
break;
|
||||
default:
|
||||
const { windowPool } = require('./electron/windowManager');
|
||||
const { windowPool } = require('./window/windowManager.js');
|
||||
const header = windowPool.get('header');
|
||||
if (header) {
|
||||
if (header.isMinimized()) header.restore();
|
||||
@ -806,7 +496,7 @@ async function handleCustomUrl(url) {
|
||||
}
|
||||
|
||||
async function handleFirebaseAuthCallback(params) {
|
||||
const userRepository = require('./common/repositories/user');
|
||||
const userRepository = require('./features/common/repositories/user');
|
||||
const { token: idToken } = params;
|
||||
|
||||
if (!idToken) {
|
||||
@ -850,7 +540,7 @@ async function handleFirebaseAuthCallback(params) {
|
||||
console.log('[Auth] Main process sign-in initiated. Waiting for onAuthStateChanged...');
|
||||
|
||||
// 3. Focus the app window
|
||||
const { windowPool } = require('./electron/windowManager');
|
||||
const { windowPool } = require('./window/windowManager.js');
|
||||
const header = windowPool.get('header');
|
||||
if (header) {
|
||||
if (header.isMinimized()) header.restore();
|
||||
@ -863,7 +553,7 @@ async function handleFirebaseAuthCallback(params) {
|
||||
console.error('[Auth] Error during custom token exchange or sign-in:', error);
|
||||
// The UI will not change, and the user can try again.
|
||||
// Optionally, send a generic error event to the renderer.
|
||||
const { windowPool } = require('./electron/windowManager');
|
||||
const { windowPool } = require('./window/windowManager.js');
|
||||
const header = windowPool.get('header');
|
||||
if (header) {
|
||||
header.webContents.send('auth-failed', { message: error.message });
|
||||
@ -874,7 +564,7 @@ async function handleFirebaseAuthCallback(params) {
|
||||
function handlePersonalizeFromUrl(params) {
|
||||
console.log('[Custom URL] Personalize params:', params);
|
||||
|
||||
const { windowPool } = require('./electron/windowManager');
|
||||
const { windowPool } = require('./window/windowManager.js');
|
||||
const header = windowPool.get('header');
|
||||
|
||||
if (header) {
|
||||
|
295
src/preload.js
@ -1,2 +1,293 @@
|
||||
// See the Electron documentation for details on how to use preload scripts:
|
||||
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
|
||||
// src/preload.js
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('api', {
|
||||
// Platform information for renderer processes
|
||||
platform: {
|
||||
isLinux: process.platform === 'linux',
|
||||
isMacOS: process.platform === 'darwin',
|
||||
isWindows: process.platform === 'win32',
|
||||
platform: process.platform
|
||||
},
|
||||
|
||||
// Common utilities used across multiple components
|
||||
common: {
|
||||
// User & Auth
|
||||
getCurrentUser: () => ipcRenderer.invoke('get-current-user'),
|
||||
startFirebaseAuth: () => ipcRenderer.invoke('start-firebase-auth'),
|
||||
firebaseLogout: () => ipcRenderer.invoke('firebase-logout'),
|
||||
|
||||
// App Control
|
||||
quitApplication: () => ipcRenderer.invoke('quit-application'),
|
||||
|
||||
// User state listener (used by multiple components)
|
||||
onUserStateChanged: (callback) => ipcRenderer.on('user-state-changed', callback),
|
||||
removeOnUserStateChanged: (callback) => ipcRenderer.removeListener('user-state-changed', callback),
|
||||
},
|
||||
|
||||
// UI Component specific namespaces
|
||||
// src/ui/app/ApiKeyHeader.js
|
||||
apiKeyHeader: {
|
||||
// Model & Provider Management
|
||||
getProviderConfig: () => ipcRenderer.invoke('model:get-provider-config'),
|
||||
getOllamaStatus: () => ipcRenderer.invoke('ollama:get-status'),
|
||||
getModelSuggestions: () => ipcRenderer.invoke('ollama:get-model-suggestions'),
|
||||
ensureOllamaReady: () => ipcRenderer.invoke('ollama:ensure-ready'),
|
||||
installOllama: () => ipcRenderer.invoke('ollama:install'),
|
||||
startOllamaService: () => ipcRenderer.invoke('ollama:start-service'),
|
||||
pullOllamaModel: (modelName) => ipcRenderer.invoke('ollama:pull-model', modelName),
|
||||
downloadWhisperModel: (modelId) => ipcRenderer.invoke('whisper:download-model', modelId),
|
||||
validateKey: (data) => ipcRenderer.invoke('model:validate-key', data),
|
||||
setSelectedModel: (data) => ipcRenderer.invoke('model:set-selected-model', data),
|
||||
areProvidersConfigured: () => ipcRenderer.invoke('model:are-providers-configured'),
|
||||
|
||||
// Window Management
|
||||
getHeaderPosition: () => ipcRenderer.invoke('get-header-position'),
|
||||
moveHeaderTo: (x, y) => ipcRenderer.invoke('move-header-to', x, y),
|
||||
|
||||
// Listeners
|
||||
onOllamaInstallProgress: (callback) => ipcRenderer.on('ollama:install-progress', callback),
|
||||
removeOnOllamaInstallProgress: (callback) => ipcRenderer.removeListener('ollama:install-progress', callback),
|
||||
onceOllamaInstallComplete: (callback) => ipcRenderer.once('ollama:install-complete', callback),
|
||||
removeOnceOllamaInstallComplete: (callback) => ipcRenderer.removeListener('ollama:install-complete', callback),
|
||||
onOllamaPullProgress: (callback) => ipcRenderer.on('ollama:pull-progress', callback),
|
||||
removeOnOllamaPullProgress: (callback) => ipcRenderer.removeListener('ollama:pull-progress', callback),
|
||||
onWhisperDownloadProgress: (callback) => ipcRenderer.on('whisper:download-progress', callback),
|
||||
removeOnWhisperDownloadProgress: (callback) => ipcRenderer.removeListener('whisper:download-progress', callback),
|
||||
|
||||
// Remove all listeners (for cleanup)
|
||||
removeAllListeners: () => {
|
||||
ipcRenderer.removeAllListeners('whisper:download-progress');
|
||||
ipcRenderer.removeAllListeners('ollama:install-progress');
|
||||
ipcRenderer.removeAllListeners('ollama:pull-progress');
|
||||
ipcRenderer.removeAllListeners('ollama:install-complete');
|
||||
}
|
||||
},
|
||||
|
||||
// src/ui/app/HeaderController.js
|
||||
headerController: {
|
||||
// State Management
|
||||
sendHeaderStateChanged: (state) => ipcRenderer.send('header-state-changed', state),
|
||||
|
||||
// Window Management
|
||||
resizeHeaderWindow: (dimensions) => ipcRenderer.invoke('resize-header-window', dimensions),
|
||||
|
||||
// Permissions
|
||||
checkSystemPermissions: () => ipcRenderer.invoke('check-system-permissions'),
|
||||
checkPermissionsCompleted: () => ipcRenderer.invoke('check-permissions-completed'),
|
||||
|
||||
// Listeners
|
||||
onUserStateChanged: (callback) => ipcRenderer.on('user-state-changed', callback),
|
||||
removeOnUserStateChanged: (callback) => ipcRenderer.removeListener('user-state-changed', callback),
|
||||
onAuthFailed: (callback) => ipcRenderer.on('auth-failed', callback),
|
||||
removeOnAuthFailed: (callback) => ipcRenderer.removeListener('auth-failed', callback),
|
||||
onForceShowApiKeyHeader: (callback) => ipcRenderer.on('force-show-apikey-header', callback),
|
||||
removeOnForceShowApiKeyHeader: (callback) => ipcRenderer.removeListener('force-show-apikey-header', callback)
|
||||
},
|
||||
|
||||
// src/ui/app/MainHeader.js
|
||||
mainHeader: {
|
||||
// Window Management
|
||||
getHeaderPosition: () => ipcRenderer.invoke('get-header-position'),
|
||||
moveHeaderTo: (x, y) => ipcRenderer.invoke('move-header-to', x, y),
|
||||
sendHeaderAnimationFinished: (state) => ipcRenderer.send('header-animation-finished', state),
|
||||
|
||||
// Settings Window Management
|
||||
cancelHideSettingsWindow: () => ipcRenderer.send('cancel-hide-settings-window'),
|
||||
showSettingsWindow: (bounds) => ipcRenderer.send('show-settings-window', bounds),
|
||||
hideSettingsWindow: () => ipcRenderer.send('hide-settings-window'),
|
||||
|
||||
// Generic invoke (for dynamic channel names)
|
||||
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
|
||||
|
||||
// Listeners
|
||||
onListenChangeSessionResult: (callback) => ipcRenderer.on('listen:changeSessionResult', callback),
|
||||
removeOnListenChangeSessionResult: (callback) => ipcRenderer.removeListener('listen:changeSessionResult', callback),
|
||||
onShortcutsUpdated: (callback) => ipcRenderer.on('shortcuts-updated', callback),
|
||||
removeOnShortcutsUpdated: (callback) => ipcRenderer.removeListener('shortcuts-updated', callback)
|
||||
},
|
||||
|
||||
// src/ui/app/PermissionHeader.js
|
||||
permissionHeader: {
|
||||
// Permission Management
|
||||
checkSystemPermissions: () => ipcRenderer.invoke('check-system-permissions'),
|
||||
requestMicrophonePermission: () => ipcRenderer.invoke('request-microphone-permission'),
|
||||
openSystemPreferences: (preference) => ipcRenderer.invoke('open-system-preferences', preference),
|
||||
markPermissionsCompleted: () => ipcRenderer.invoke('mark-permissions-completed')
|
||||
},
|
||||
|
||||
// src/ui/app/PickleGlassApp.js
|
||||
pickleGlassApp: {
|
||||
// Listeners
|
||||
onClickThroughToggled: (callback) => ipcRenderer.on('click-through-toggled', callback),
|
||||
removeOnClickThroughToggled: (callback) => ipcRenderer.removeListener('click-through-toggled', callback),
|
||||
removeAllClickThroughListeners: () => ipcRenderer.removeAllListeners('click-through-toggled')
|
||||
},
|
||||
|
||||
// src/ui/ask/AskView.js
|
||||
askView: {
|
||||
// Window Management
|
||||
closeAskWindow: () => ipcRenderer.invoke('ask:closeAskWindow'),
|
||||
adjustWindowHeight: (height) => ipcRenderer.invoke('adjust-window-height', height),
|
||||
|
||||
// Message Handling
|
||||
sendMessage: (text) => ipcRenderer.invoke('ask:sendQuestionFromAsk', text),
|
||||
|
||||
// Listeners
|
||||
onAskStateUpdate: (callback) => ipcRenderer.on('ask:stateUpdate', callback),
|
||||
removeOnAskStateUpdate: (callback) => ipcRenderer.removeListener('ask:stateUpdate', callback),
|
||||
|
||||
// Listeners
|
||||
onShowTextInput: (callback) => ipcRenderer.on('ask:showTextInput', callback),
|
||||
removeOnShowTextInput: (callback) => ipcRenderer.removeListener('ask:showTextInput', callback),
|
||||
|
||||
onScrollResponseUp: (callback) => ipcRenderer.on('aks:scrollResponseUp', callback),
|
||||
removeOnScrollResponseUp: (callback) => ipcRenderer.removeListener('aks:scrollResponseUp', callback),
|
||||
onScrollResponseDown: (callback) => ipcRenderer.on('aks:scrollResponseDown', callback),
|
||||
removeOnScrollResponseDown: (callback) => ipcRenderer.removeListener('aks:scrollResponseDown', callback)
|
||||
},
|
||||
|
||||
// src/ui/listen/ListenView.js
|
||||
listenView: {
|
||||
// Window Management
|
||||
adjustWindowHeight: (height) => ipcRenderer.invoke('adjust-window-height', height),
|
||||
|
||||
// Listeners
|
||||
onSessionStateChanged: (callback) => ipcRenderer.on('session-state-changed', callback),
|
||||
removeOnSessionStateChanged: (callback) => ipcRenderer.removeListener('session-state-changed', callback)
|
||||
},
|
||||
|
||||
// src/ui/listen/stt/SttView.js
|
||||
sttView: {
|
||||
// Listeners
|
||||
onSttUpdate: (callback) => ipcRenderer.on('stt-update', callback),
|
||||
removeOnSttUpdate: (callback) => ipcRenderer.removeListener('stt-update', callback)
|
||||
},
|
||||
|
||||
// src/ui/listen/summary/SummaryView.js
|
||||
summaryView: {
|
||||
// Message Handling
|
||||
sendQuestionFromSummary: (text) => ipcRenderer.invoke('ask:sendQuestionFromSummary', text),
|
||||
|
||||
// Listeners
|
||||
onSummaryUpdate: (callback) => ipcRenderer.on('summary-update', callback),
|
||||
removeOnSummaryUpdate: (callback) => ipcRenderer.removeListener('summary-update', callback),
|
||||
removeAllSummaryUpdateListeners: () => ipcRenderer.removeAllListeners('summary-update')
|
||||
},
|
||||
|
||||
// src/ui/settings/SettingsView.js
|
||||
settingsView: {
|
||||
// User & Auth
|
||||
getCurrentUser: () => ipcRenderer.invoke('get-current-user'),
|
||||
openPersonalizePage: () => ipcRenderer.invoke('open-personalize-page'),
|
||||
firebaseLogout: () => ipcRenderer.invoke('firebase-logout'),
|
||||
startFirebaseAuth: () => ipcRenderer.invoke('start-firebase-auth'),
|
||||
|
||||
// Model & Provider Management
|
||||
getModelSettings: () => ipcRenderer.invoke('settings:get-model-settings'), // Facade call
|
||||
getProviderConfig: () => ipcRenderer.invoke('model:get-provider-config'),
|
||||
getAllKeys: () => ipcRenderer.invoke('model:get-all-keys'),
|
||||
getAvailableModels: (type) => ipcRenderer.invoke('model:get-available-models', type),
|
||||
getSelectedModels: () => ipcRenderer.invoke('model:get-selected-models'),
|
||||
validateKey: (data) => ipcRenderer.invoke('model:validate-key', data),
|
||||
saveApiKey: (key) => ipcRenderer.invoke('model:save-api-key', key),
|
||||
removeApiKey: (provider) => ipcRenderer.invoke('model:remove-api-key', provider),
|
||||
setSelectedModel: (data) => ipcRenderer.invoke('model:set-selected-model', data),
|
||||
|
||||
// Ollama Management
|
||||
getOllamaStatus: () => ipcRenderer.invoke('ollama:get-status'),
|
||||
ensureOllamaReady: () => ipcRenderer.invoke('ollama:ensure-ready'),
|
||||
shutdownOllama: (graceful) => ipcRenderer.invoke('ollama:shutdown', graceful),
|
||||
|
||||
// Whisper Management
|
||||
getWhisperInstalledModels: () => ipcRenderer.invoke('whisper:get-installed-models'),
|
||||
downloadWhisperModel: (modelId) => ipcRenderer.invoke('whisper:download-model', modelId),
|
||||
|
||||
// Settings Management
|
||||
getPresets: () => ipcRenderer.invoke('settings:getPresets'),
|
||||
getAutoUpdate: () => ipcRenderer.invoke('settings:get-auto-update'),
|
||||
setAutoUpdate: (isEnabled) => ipcRenderer.invoke('settings:set-auto-update', isEnabled),
|
||||
getContentProtectionStatus: () => ipcRenderer.invoke('get-content-protection-status'),
|
||||
toggleContentProtection: () => ipcRenderer.invoke('toggle-content-protection'),
|
||||
getCurrentShortcuts: () => ipcRenderer.invoke('get-current-shortcuts'),
|
||||
openShortcutEditor: () => ipcRenderer.invoke('open-shortcut-editor'),
|
||||
|
||||
// Window Management
|
||||
moveWindowStep: (direction) => ipcRenderer.invoke('move-window-step', direction),
|
||||
cancelHideSettingsWindow: () => ipcRenderer.send('cancel-hide-settings-window'),
|
||||
hideSettingsWindow: () => ipcRenderer.send('hide-settings-window'),
|
||||
|
||||
// App Control
|
||||
quitApplication: () => ipcRenderer.invoke('quit-application'),
|
||||
|
||||
// Progress Tracking
|
||||
pullOllamaModel: (modelName) => ipcRenderer.invoke('ollama:pull-model', modelName),
|
||||
|
||||
// Listeners
|
||||
onUserStateChanged: (callback) => ipcRenderer.on('user-state-changed', callback),
|
||||
removeOnUserStateChanged: (callback) => ipcRenderer.removeListener('user-state-changed', callback),
|
||||
onSettingsUpdated: (callback) => ipcRenderer.on('settings-updated', callback),
|
||||
removeOnSettingsUpdated: (callback) => ipcRenderer.removeListener('settings-updated', callback),
|
||||
onPresetsUpdated: (callback) => ipcRenderer.on('presets-updated', callback),
|
||||
removeOnPresetsUpdated: (callback) => ipcRenderer.removeListener('presets-updated', callback),
|
||||
onShortcutsUpdated: (callback) => ipcRenderer.on('shortcuts-updated', callback),
|
||||
removeOnShortcutsUpdated: (callback) => ipcRenderer.removeListener('shortcuts-updated', callback),
|
||||
onWhisperDownloadProgress: (callback) => ipcRenderer.on('whisper:download-progress', callback),
|
||||
removeOnWhisperDownloadProgress: (callback) => ipcRenderer.removeListener('whisper:download-progress', callback),
|
||||
onOllamaPullProgress: (callback) => ipcRenderer.on('ollama:pull-progress', callback),
|
||||
removeOnOllamaPullProgress: (callback) => ipcRenderer.removeListener('ollama:pull-progress', callback)
|
||||
},
|
||||
|
||||
// src/ui/settings/ShortCutSettingsView.js
|
||||
shortcutSettingsView: {
|
||||
// Shortcut Management
|
||||
saveShortcuts: (shortcuts) => ipcRenderer.invoke('save-shortcuts', shortcuts),
|
||||
getDefaultShortcuts: () => ipcRenderer.invoke('get-default-shortcuts'),
|
||||
closeShortcutEditor: () => ipcRenderer.send('close-shortcut-editor'),
|
||||
|
||||
// Listeners
|
||||
onLoadShortcuts: (callback) => ipcRenderer.on('load-shortcuts', callback),
|
||||
removeOnLoadShortcuts: (callback) => ipcRenderer.removeListener('load-shortcuts', callback)
|
||||
},
|
||||
|
||||
// src/ui/app/content.html inline scripts
|
||||
content: {
|
||||
// Animation Management
|
||||
sendAnimationFinished: () => ipcRenderer.send('animation-finished'),
|
||||
|
||||
// Listeners
|
||||
onWindowShowAnimation: (callback) => ipcRenderer.on('window-show-animation', callback),
|
||||
removeOnWindowShowAnimation: (callback) => ipcRenderer.removeListener('window-show-animation', callback),
|
||||
onWindowHideAnimation: (callback) => ipcRenderer.on('window-hide-animation', callback),
|
||||
removeOnWindowHideAnimation: (callback) => ipcRenderer.removeListener('window-hide-animation', callback),
|
||||
onSettingsWindowHideAnimation: (callback) => ipcRenderer.on('settings-window-hide-animation', callback),
|
||||
removeOnSettingsWindowHideAnimation: (callback) => ipcRenderer.removeListener('settings-window-hide-animation', callback),
|
||||
onListenWindowMoveToCenter: (callback) => ipcRenderer.on('listen-window-move-to-center', callback),
|
||||
removeOnListenWindowMoveToCenter: (callback) => ipcRenderer.removeListener('listen-window-move-to-center', callback),
|
||||
onListenWindowMoveToLeft: (callback) => ipcRenderer.on('listen-window-move-to-left', callback),
|
||||
removeOnListenWindowMoveToLeft: (callback) => ipcRenderer.removeListener('listen-window-move-to-left', callback)
|
||||
},
|
||||
|
||||
// src/ui/listen/audioCore/listenCapture.js
|
||||
listenCapture: {
|
||||
// Audio Management
|
||||
sendMicAudioContent: (data) => ipcRenderer.invoke('listen:sendMicAudio', data),
|
||||
sendSystemAudioContent: (data) => ipcRenderer.invoke('listen:sendSystemAudio', data),
|
||||
startMacosSystemAudio: () => ipcRenderer.invoke('listen:startMacosSystemAudio'),
|
||||
stopMacosSystemAudio: () => ipcRenderer.invoke('listen:stopMacosSystemAudio'),
|
||||
|
||||
// Session Management
|
||||
isSessionActive: () => ipcRenderer.invoke('is-session-active'),
|
||||
|
||||
// Listeners
|
||||
onSystemAudioData: (callback) => ipcRenderer.on('system-audio-data', callback),
|
||||
removeOnSystemAudioData: (callback) => ipcRenderer.removeListener('system-audio-data', callback)
|
||||
},
|
||||
|
||||
// src/ui/listen/audioCore/renderer.js
|
||||
renderer: {
|
||||
// Listeners
|
||||
onChangeListenCaptureState: (callback) => ipcRenderer.on('change-listen-capture-state', callback),
|
||||
removeOnChangeListenCaptureState: (callback) => ipcRenderer.removeListener('change-listen-capture-state', callback)
|
||||
}
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
import { html, css, LitElement } from "../assets/lit-core-2.7.4.min.js"
|
||||
import { getOllamaProgressTracker } from "../common/services/localProgressTracker.js"
|
||||
import { getOllamaProgressTracker } from "../../features/common/services/localProgressTracker.js"
|
||||
|
||||
export class ApiKeyHeader extends LitElement {
|
||||
//////// after_modelStateService ////////
|
||||
@ -370,13 +370,12 @@ export class ApiKeyHeader extends LitElement {
|
||||
}
|
||||
|
||||
async loadProviderConfig() {
|
||||
if (!window.require) return;
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
if (!window.api) return;
|
||||
|
||||
try {
|
||||
const [config, ollamaStatus] = await Promise.all([
|
||||
ipcRenderer.invoke('model:get-provider-config'),
|
||||
ipcRenderer.invoke('ollama:get-status')
|
||||
window.api.apiKeyHeader.getProviderConfig(),
|
||||
window.api.apiKeyHeader.getOllamaStatus()
|
||||
]);
|
||||
|
||||
const llmProviders = [];
|
||||
@ -428,8 +427,7 @@ export class ApiKeyHeader extends LitElement {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
const { ipcRenderer } = window.require("electron")
|
||||
const initialPosition = await ipcRenderer.invoke("get-header-position")
|
||||
const initialPosition = await window.api.apiKeyHeader.getHeaderPosition()
|
||||
|
||||
this.dragState = {
|
||||
initialMouseX: e.screenX,
|
||||
@ -456,8 +454,7 @@ export class ApiKeyHeader extends LitElement {
|
||||
const newWindowX = this.dragState.initialWindowX + (e.screenX - this.dragState.initialMouseX)
|
||||
const newWindowY = this.dragState.initialWindowY + (e.screenY - this.dragState.initialMouseY)
|
||||
|
||||
const { ipcRenderer } = window.require("electron")
|
||||
ipcRenderer.invoke("move-header-to", newWindowX, newWindowY)
|
||||
window.api.apiKeyHeader.moveHeaderTo(newWindowX, newWindowY)
|
||||
}
|
||||
|
||||
handleMouseUp(e) {
|
||||
@ -652,9 +649,8 @@ export class ApiKeyHeader extends LitElement {
|
||||
try {
|
||||
// Lightweight health check - just ping the service
|
||||
const isHealthy = await this._executeOperation('health_check', async () => {
|
||||
if (!window.require) return false;
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
const result = await ipcRenderer.invoke('ollama:get-status');
|
||||
if (!window.api) return false;
|
||||
const result = await window.api.apiKeyHeader.getOllamaStatus();
|
||||
return result?.success && result?.running;
|
||||
}, { timeout: 5000, priority: 'low' });
|
||||
|
||||
@ -928,14 +924,13 @@ export class ApiKeyHeader extends LitElement {
|
||||
}
|
||||
|
||||
async refreshOllamaStatus() {
|
||||
if (!window.require) return;
|
||||
if (!window.api) return;
|
||||
|
||||
try {
|
||||
this._updateConnectionState('connecting', 'Checking Ollama status');
|
||||
|
||||
const result = await this._executeOperation('ollama_status', async () => {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
return await ipcRenderer.invoke('ollama:get-status');
|
||||
return await window.api.apiKeyHeader.getOllamaStatus();
|
||||
});
|
||||
|
||||
if (result?.success) {
|
||||
@ -960,12 +955,11 @@ export class ApiKeyHeader extends LitElement {
|
||||
}
|
||||
|
||||
async loadModelSuggestions() {
|
||||
if (!window.require) return;
|
||||
if (!window.api) return;
|
||||
|
||||
try {
|
||||
const result = await this._executeOperation('model_suggestions', async () => {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
return await ipcRenderer.invoke('ollama:get-model-suggestions');
|
||||
return await window.api.apiKeyHeader.getModelSuggestions();
|
||||
});
|
||||
|
||||
if (result?.success) {
|
||||
@ -988,14 +982,13 @@ export class ApiKeyHeader extends LitElement {
|
||||
}
|
||||
|
||||
async ensureOllamaReady() {
|
||||
if (!window.require) return false;
|
||||
if (!window.api) return false;
|
||||
|
||||
try {
|
||||
this._updateConnectionState('connecting', 'Ensuring Ollama is ready');
|
||||
|
||||
const result = await this._executeOperation('ollama_ensure_ready', async () => {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
return await ipcRenderer.invoke('ollama:ensure-ready');
|
||||
return await window.api.apiKeyHeader.ensureOllamaReady();
|
||||
}, { timeout: this.operationTimeout });
|
||||
|
||||
if (result?.success) {
|
||||
@ -1015,8 +1008,7 @@ export class ApiKeyHeader extends LitElement {
|
||||
}
|
||||
|
||||
async ensureOllamaReadyWithUI() {
|
||||
if (!window.require) return false;
|
||||
const { ipcRenderer } = window.require("electron");
|
||||
if (!window.api) return false;
|
||||
|
||||
this.installingModel = "Setting up Ollama";
|
||||
this.installProgress = 0;
|
||||
@ -1074,21 +1066,21 @@ export class ApiKeyHeader extends LitElement {
|
||||
operationCompleted = true;
|
||||
clearTimeout(completionTimeout);
|
||||
|
||||
ipcRenderer.removeListener("ollama:install-progress", progressHandler);
|
||||
window.api.apiKeyHeader.removeOnOllamaInstallProgress(progressHandler);
|
||||
await this._handleOllamaSetupCompletion(result.success, result.error);
|
||||
};
|
||||
|
||||
ipcRenderer.once("ollama:install-complete", completionHandler);
|
||||
ipcRenderer.on("ollama:install-progress", progressHandler);
|
||||
window.api.apiKeyHeader.onceOllamaInstallComplete(completionHandler);
|
||||
window.api.apiKeyHeader.onOllamaInstallProgress(progressHandler);
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (!this.ollamaStatus.installed) {
|
||||
console.log("[ApiKeyHeader] Ollama not installed. Starting installation.");
|
||||
result = await ipcRenderer.invoke("ollama:install");
|
||||
result = await window.api.apiKeyHeader.installOllama();
|
||||
} else {
|
||||
console.log("[ApiKeyHeader] Ollama installed. Starting service.");
|
||||
result = await ipcRenderer.invoke("ollama:start-service");
|
||||
result = await window.api.apiKeyHeader.startOllamaService();
|
||||
}
|
||||
|
||||
// If IPC call succeeds but no event received, handle completion manually
|
||||
@ -1106,8 +1098,8 @@ export class ApiKeyHeader extends LitElement {
|
||||
operationCompleted = true;
|
||||
clearTimeout(completionTimeout);
|
||||
console.error("[ApiKeyHeader] Ollama setup failed:", error);
|
||||
ipcRenderer.removeListener("ollama:install-progress", progressHandler);
|
||||
ipcRenderer.removeListener("ollama:install-complete", completionHandler);
|
||||
window.api.apiKeyHeader.removeOnOllamaInstallProgress(progressHandler);
|
||||
window.api.apiKeyHeader.removeOnceOllamaInstallComplete(completionHandler);
|
||||
await this._handleOllamaSetupCompletion(false, error.message);
|
||||
}
|
||||
}
|
||||
@ -1229,7 +1221,6 @@ export class ApiKeyHeader extends LitElement {
|
||||
this.clearMessages();
|
||||
this.requestUpdate();
|
||||
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
let progressHandler = null;
|
||||
|
||||
try {
|
||||
@ -1249,10 +1240,10 @@ export class ApiKeyHeader extends LitElement {
|
||||
};
|
||||
|
||||
// Set up progress tracking
|
||||
ipcRenderer.on('ollama:pull-progress', progressHandler);
|
||||
window.api.apiKeyHeader.onOllamaPullProgress(progressHandler);
|
||||
|
||||
// Execute the model pull with timeout
|
||||
const installPromise = ipcRenderer.invoke('ollama:pull-model', modelName);
|
||||
const installPromise = window.api.apiKeyHeader.pullOllamaModel(modelName);
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Installation timeout after 10 minutes')), 600000)
|
||||
);
|
||||
@ -1281,7 +1272,7 @@ export class ApiKeyHeader extends LitElement {
|
||||
} finally {
|
||||
// Comprehensive cleanup
|
||||
if (progressHandler) {
|
||||
ipcRenderer.removeListener('ollama:pull-progress', progressHandler);
|
||||
window.api.apiKeyHeader.removeOnOllamaPullProgress(progressHandler);
|
||||
}
|
||||
|
||||
this.installingModel = null;
|
||||
@ -1307,7 +1298,6 @@ export class ApiKeyHeader extends LitElement {
|
||||
this.clearMessages();
|
||||
this.requestUpdate();
|
||||
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
let progressHandler = null;
|
||||
|
||||
try {
|
||||
@ -1321,10 +1311,10 @@ export class ApiKeyHeader extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
ipcRenderer.on('whisper:download-progress', progressHandler);
|
||||
window.api.apiKeyHeader.onWhisperDownloadProgress(progressHandler);
|
||||
|
||||
// Start download with timeout protection
|
||||
const downloadPromise = ipcRenderer.invoke('whisper:download-model', modelId);
|
||||
const downloadPromise = window.api.apiKeyHeader.downloadWhisperModel(modelId);
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Download timeout after 10 minutes')), 600000)
|
||||
);
|
||||
@ -1351,7 +1341,7 @@ export class ApiKeyHeader extends LitElement {
|
||||
} finally {
|
||||
// Cleanup
|
||||
if (progressHandler) {
|
||||
ipcRenderer.removeListener('whisper:download-progress', progressHandler);
|
||||
window.api.apiKeyHeader.removeOnWhisperDownloadProgress(progressHandler);
|
||||
}
|
||||
delete this.whisperInstallingModels[modelId];
|
||||
this.requestUpdate();
|
||||
@ -1411,8 +1401,6 @@ export class ApiKeyHeader extends LitElement {
|
||||
this.isLoading = true;
|
||||
this.clearMessages();
|
||||
this.requestUpdate();
|
||||
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
try {
|
||||
// Handle LLM provider
|
||||
@ -1436,14 +1424,14 @@ export class ApiKeyHeader extends LitElement {
|
||||
}
|
||||
|
||||
// Validate Ollama is working
|
||||
llmResult = await ipcRenderer.invoke('model:validate-key', {
|
||||
llmResult = await window.api.apiKeyHeader.validateKey({
|
||||
provider: 'ollama',
|
||||
key: 'local'
|
||||
});
|
||||
|
||||
if (llmResult.success) {
|
||||
// Set the selected model
|
||||
await ipcRenderer.invoke('model:set-selected-model', {
|
||||
await window.api.apiKeyHeader.setSelectedModel({
|
||||
type: 'llm',
|
||||
modelId: this.selectedLlmModel
|
||||
});
|
||||
@ -1454,7 +1442,7 @@ export class ApiKeyHeader extends LitElement {
|
||||
throw new Error('Please enter LLM API key');
|
||||
}
|
||||
|
||||
llmResult = await ipcRenderer.invoke('model:validate-key', {
|
||||
llmResult = await window.api.apiKeyHeader.validateKey({
|
||||
provider: this.llmProvider,
|
||||
key: this.llmApiKey.trim()
|
||||
});
|
||||
@ -1467,14 +1455,14 @@ export class ApiKeyHeader extends LitElement {
|
||||
sttResult = { success: true };
|
||||
} else if (this.sttProvider === 'whisper') {
|
||||
// For Whisper, just validate it's enabled (model download already handled in handleSttModelChange)
|
||||
sttResult = await ipcRenderer.invoke('model:validate-key', {
|
||||
sttResult = await window.api.apiKeyHeader.validateKey({
|
||||
provider: 'whisper',
|
||||
key: 'local'
|
||||
});
|
||||
|
||||
if (sttResult.success && this.selectedSttModel) {
|
||||
// Set the selected model
|
||||
await ipcRenderer.invoke('model:set-selected-model', {
|
||||
await window.api.apiKeyHeader.setSelectedModel({
|
||||
type: 'stt',
|
||||
modelId: this.selectedSttModel
|
||||
});
|
||||
@ -1485,7 +1473,7 @@ export class ApiKeyHeader extends LitElement {
|
||||
throw new Error('Please enter STT API key');
|
||||
}
|
||||
|
||||
sttResult = await ipcRenderer.invoke('model:validate-key', {
|
||||
sttResult = await window.api.apiKeyHeader.validateKey({
|
||||
provider: this.sttProvider,
|
||||
key: this.sttApiKey.trim()
|
||||
});
|
||||
@ -1522,15 +1510,15 @@ export class ApiKeyHeader extends LitElement {
|
||||
e.preventDefault()
|
||||
|
||||
console.log("Requesting Firebase authentication from main process...")
|
||||
if (window.require) {
|
||||
window.require("electron").ipcRenderer.invoke("start-firebase-auth")
|
||||
if (window.api) {
|
||||
window.api.common.startFirebaseAuth()
|
||||
}
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
console.log("Close button clicked")
|
||||
if (window.require) {
|
||||
window.require("electron").ipcRenderer.invoke("quit-application")
|
||||
if (window.api) {
|
||||
window.api.common.quitApplication()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1541,10 +1529,10 @@ 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');
|
||||
if (!window.api) {
|
||||
console.error('[ApiKeyHeader] handleAnimationEnd: window.api not available');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1553,14 +1541,12 @@ export class ApiKeyHeader extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
ipcRenderer.invoke('get-current-user')
|
||||
window.api.common.getCurrentUser()
|
||||
.then(userState => {
|
||||
console.log('[ApiKeyHeader] handleAnimationEnd: User state retrieved:', userState);
|
||||
|
||||
// Additional validation for local providers
|
||||
return ipcRenderer.invoke('model:are-providers-configured').then(isConfigured => {
|
||||
return window.api.apiKeyHeader.areProvidersConfigured().then(isConfigured => {
|
||||
console.log('[ApiKeyHeader] handleAnimationEnd: Providers configured check:', isConfigured);
|
||||
|
||||
if (!isConfigured) {
|
||||
@ -1585,7 +1571,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 +1590,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();
|
||||
}
|
||||
@ -1624,12 +1611,8 @@ export class ApiKeyHeader extends LitElement {
|
||||
}
|
||||
|
||||
// Cleanup event listeners
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.removeAllListeners('whisper:download-progress');
|
||||
ipcRenderer.removeAllListeners('ollama:install-progress');
|
||||
ipcRenderer.removeAllListeners('ollama:pull-progress');
|
||||
ipcRenderer.removeAllListeners('ollama:install-complete');
|
||||
if (window.api) {
|
||||
window.api.apiKeyHeader.removeAllListeners();
|
||||
}
|
||||
|
||||
// Cancel any ongoing downloads
|
@ -32,6 +32,7 @@ class HeaderTransitionManager {
|
||||
this.apiKeyHeader = document.createElement('apikey-header');
|
||||
this.apiKeyHeader.stateUpdateCallback = (userState) => this.handleStateUpdate(userState);
|
||||
this.headerContainer.appendChild(this.apiKeyHeader);
|
||||
console.log('[HeaderController] ensureHeader: Header of type:', type, 'created.');
|
||||
} else if (type === 'permission') {
|
||||
this.permissionHeader = document.createElement('permission-setup');
|
||||
this.permissionHeader.continueCallback = () => this.transitionToMainHeader();
|
||||
@ -50,41 +51,39 @@ class HeaderTransitionManager {
|
||||
|
||||
this._bootstrap();
|
||||
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
ipcRenderer.on('user-state-changed', (event, userState) => {
|
||||
if (window.api) {
|
||||
window.api.headerController.onUserStateChanged((event, userState) => {
|
||||
console.log('[HeaderController] Received user state change:', userState);
|
||||
this.handleStateUpdate(userState);
|
||||
});
|
||||
|
||||
ipcRenderer.on('auth-failed', (event, { message }) => {
|
||||
window.api.headerController.onAuthFailed((event, { message }) => {
|
||||
console.error('[HeaderController] Received auth failure from main process:', message);
|
||||
if (this.apiKeyHeader) {
|
||||
this.apiKeyHeader.errorMessage = 'Authentication failed. Please try again.';
|
||||
this.apiKeyHeader.isLoading = false;
|
||||
}
|
||||
});
|
||||
ipcRenderer.on('force-show-apikey-header', async () => {
|
||||
window.api.headerController.onForceShowApiKeyHeader(async () => {
|
||||
console.log('[HeaderController] Received broadcast to show apikey header. Switching now.');
|
||||
await this._resizeForApiKey();
|
||||
this.ensureHeader('apikey');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
notifyHeaderState(stateOverride) {
|
||||
const state = stateOverride || this.currentHeaderType || 'apikey';
|
||||
if (window.require) {
|
||||
window.require('electron').ipcRenderer.send('header-state-changed', state);
|
||||
if (window.api) {
|
||||
window.api.headerController.sendHeaderStateChanged(state);
|
||||
}
|
||||
}
|
||||
|
||||
async _bootstrap() {
|
||||
// The initial state will be sent by the main process via 'user-state-changed'
|
||||
// We just need to request it.
|
||||
if (window.require) {
|
||||
const userState = await window.require('electron').ipcRenderer.invoke('get-current-user');
|
||||
if (window.api) {
|
||||
const userState = await window.api.common.getCurrentUser();
|
||||
console.log('[HeaderController] Bootstrapping with initial user state:', userState);
|
||||
this.handleStateUpdate(userState);
|
||||
} else {
|
||||
@ -96,8 +95,7 @@ class HeaderTransitionManager {
|
||||
|
||||
//////// after_modelStateService ////////
|
||||
async handleStateUpdate(userState) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
const isConfigured = await ipcRenderer.invoke('model:are-providers-configured');
|
||||
const isConfigured = await window.api.apiKeyHeader.areProvidersConfigured();
|
||||
|
||||
if (isConfigured) {
|
||||
const { isLoggedIn } = userState;
|
||||
@ -126,10 +124,9 @@ class HeaderTransitionManager {
|
||||
}
|
||||
|
||||
// Check if permissions were previously completed
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
if (window.api) {
|
||||
try {
|
||||
const permissionsCompleted = await ipcRenderer.invoke('check-permissions-completed');
|
||||
const permissionsCompleted = await window.api.headerController.checkPermissionsCompleted();
|
||||
if (permissionsCompleted) {
|
||||
console.log('[HeaderController] Permissions were previously completed, checking current status...');
|
||||
|
||||
@ -161,39 +158,33 @@ class HeaderTransitionManager {
|
||||
this.ensureHeader('main');
|
||||
}
|
||||
|
||||
_resizeForMain() {
|
||||
if (!window.require) return;
|
||||
return window
|
||||
.require('electron')
|
||||
.ipcRenderer.invoke('resize-header-window', { width: 353, height: 47 })
|
||||
async _resizeForMain() {
|
||||
if (!window.api) return;
|
||||
console.log('[HeaderController] _resizeForMain: Resizing window to 353x47');
|
||||
return window.api.headerController.resizeHeaderWindow({ width: 353, height: 47 })
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
async _resizeForApiKey() {
|
||||
if (!window.require) return;
|
||||
return window
|
||||
.require('electron')
|
||||
.ipcRenderer.invoke('resize-header-window', { width: 350, height: 300 })
|
||||
if (!window.api) return;
|
||||
console.log('[HeaderController] _resizeForApiKey: Resizing window to 350x300');
|
||||
return window.api.headerController.resizeHeaderWindow({ width: 350, height: 300 })
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
async _resizeForPermissionHeader() {
|
||||
if (!window.require) return;
|
||||
return window
|
||||
.require('electron')
|
||||
.ipcRenderer.invoke('resize-header-window', { width: 285, height: 220 })
|
||||
if (!window.api) return;
|
||||
return window.api.headerController.resizeHeaderWindow({ width: 285, height: 220 })
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
async checkPermissions() {
|
||||
if (!window.require) {
|
||||
if (!window.api) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
try {
|
||||
const permissions = await ipcRenderer.invoke('check-system-permissions');
|
||||
const permissions = await window.api.headerController.checkSystemPermissions();
|
||||
console.log('[HeaderController] Current permissions:', permissions);
|
||||
|
||||
if (!permissions.needsSetup) {
|
@ -4,8 +4,8 @@ export class MainHeader extends LitElement {
|
||||
static properties = {
|
||||
// isSessionActive: { type: Boolean, state: true },
|
||||
isTogglingSession: { type: Boolean, state: true },
|
||||
actionText: { type: String, state: true },
|
||||
shortcuts: { type: Object, state: true },
|
||||
listenSessionStatus: { type: String, state: true },
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
@ -348,9 +348,8 @@ export class MainHeader extends LitElement {
|
||||
this.isAnimating = false;
|
||||
this.hasSlidIn = false;
|
||||
this.settingsHideTimer = null;
|
||||
// this.isSessionActive = false;
|
||||
this.isTogglingSession = false;
|
||||
this.actionText = 'Listen';
|
||||
this.listenSessionStatus = 'beforeSession';
|
||||
this.animationEndTimer = null;
|
||||
this.handleAnimationEnd = this.handleAnimationEnd.bind(this);
|
||||
this.handleMouseMove = this.handleMouseMove.bind(this);
|
||||
@ -359,11 +358,19 @@ export class MainHeader extends LitElement {
|
||||
this.wasJustDragged = false;
|
||||
}
|
||||
|
||||
_getListenButtonText(status) {
|
||||
switch (status) {
|
||||
case 'beforeSession': return 'Listen';
|
||||
case 'inSession' : return 'Stop';
|
||||
case 'afterSession': return 'Done';
|
||||
default : return 'Listen';
|
||||
}
|
||||
}
|
||||
|
||||
async handleMouseDown(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
const initialPosition = await ipcRenderer.invoke('get-header-position');
|
||||
const initialPosition = await window.api.mainHeader.getHeaderPosition();
|
||||
|
||||
this.dragState = {
|
||||
initialMouseX: e.screenX,
|
||||
@ -390,8 +397,7 @@ export class MainHeader extends LitElement {
|
||||
const newWindowX = this.dragState.initialWindowX + (e.screenX - this.dragState.initialMouseX);
|
||||
const newWindowY = this.dragState.initialWindowY + (e.screenY - this.dragState.initialMouseY);
|
||||
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.invoke('move-header-to', newWindowX, newWindowY);
|
||||
window.api.mainHeader.moveHeaderTo(newWindowX, newWindowY);
|
||||
}
|
||||
|
||||
handleMouseUp(e) {
|
||||
@ -447,12 +453,12 @@ export class MainHeader extends LitElement {
|
||||
|
||||
if (this.classList.contains('hiding')) {
|
||||
this.classList.add('hidden');
|
||||
if (window.require) {
|
||||
window.require('electron').ipcRenderer.send('header-animation-finished', 'hidden');
|
||||
if (window.api) {
|
||||
window.api.mainHeader.sendHeaderAnimationFinished('hidden');
|
||||
}
|
||||
} else if (this.classList.contains('showing')) {
|
||||
if (window.require) {
|
||||
window.require('electron').ipcRenderer.send('header-animation-finished', 'visible');
|
||||
if (window.api) {
|
||||
window.api.mainHeader.sendHeaderAnimationFinished('visible');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -466,26 +472,27 @@ export class MainHeader extends LitElement {
|
||||
super.connectedCallback();
|
||||
this.addEventListener('animationend', this.handleAnimationEnd);
|
||||
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
if (window.api) {
|
||||
|
||||
this._sessionStateTextListener = (event, text) => {
|
||||
this.actionText = text;
|
||||
this.isTogglingSession = false;
|
||||
this._sessionStateTextListener = (event, { success }) => {
|
||||
if (success) {
|
||||
this.listenSessionStatus = ({
|
||||
beforeSession: 'inSession',
|
||||
inSession: 'afterSession',
|
||||
afterSession: 'beforeSession',
|
||||
})[this.listenSessionStatus] || 'beforeSession';
|
||||
} else {
|
||||
this.listenSessionStatus = 'beforeSession';
|
||||
}
|
||||
this.isTogglingSession = false; // ✨ 로딩 상태만 해제
|
||||
};
|
||||
ipcRenderer.on('session-state-text', this._sessionStateTextListener);
|
||||
window.api.mainHeader.onListenChangeSessionResult(this._sessionStateTextListener);
|
||||
|
||||
|
||||
// this._sessionStateListener = (event, { isActive }) => {
|
||||
// this.isSessionActive = isActive;
|
||||
// this.isTogglingSession = false;
|
||||
// };
|
||||
// ipcRenderer.on('session-state-changed', this._sessionStateListener);
|
||||
this._shortcutListener = (event, keybinds) => {
|
||||
console.log('[MainHeader] Received updated shortcuts:', keybinds);
|
||||
this.shortcuts = keybinds;
|
||||
};
|
||||
ipcRenderer.on('shortcuts-updated', this._shortcutListener);
|
||||
window.api.mainHeader.onShortcutsUpdated(this._shortcutListener);
|
||||
}
|
||||
}
|
||||
|
||||
@ -498,39 +505,34 @@ export class MainHeader extends LitElement {
|
||||
this.animationEndTimer = null;
|
||||
}
|
||||
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
if (window.api) {
|
||||
if (this._sessionStateTextListener) {
|
||||
ipcRenderer.removeListener('session-state-text', this._sessionStateTextListener);
|
||||
window.api.mainHeader.removeOnListenChangeSessionResult(this._sessionStateTextListener);
|
||||
}
|
||||
// if (this._sessionStateListener) {
|
||||
// ipcRenderer.removeListener('session-state-changed', this._sessionStateListener);
|
||||
// }
|
||||
if (this._shortcutListener) {
|
||||
ipcRenderer.removeListener('shortcuts-updated', this._shortcutListener);
|
||||
window.api.mainHeader.removeOnShortcutsUpdated(this._shortcutListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invoke(channel, ...args) {
|
||||
if (this.wasJustDragged) return;
|
||||
if (window.require) {
|
||||
window.require('electron').ipcRenderer.invoke(channel, ...args);
|
||||
if (window.api) {
|
||||
window.api.mainHeader.invoke(channel, ...args);
|
||||
}
|
||||
// return Promise.resolve();
|
||||
}
|
||||
|
||||
showSettingsWindow(element) {
|
||||
if (this.wasJustDragged) return;
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
if (window.api) {
|
||||
console.log(`[MainHeader] showSettingsWindow called at ${Date.now()}`);
|
||||
|
||||
ipcRenderer.send('cancel-hide-settings-window');
|
||||
window.api.mainHeader.cancelHideSettingsWindow();
|
||||
|
||||
if (element) {
|
||||
const { left, top, width, height } = element.getBoundingClientRect();
|
||||
ipcRenderer.send('show-settings-window', {
|
||||
window.api.mainHeader.showSettingsWindow({
|
||||
x: left,
|
||||
y: top,
|
||||
width,
|
||||
@ -542,9 +544,9 @@ export class MainHeader extends LitElement {
|
||||
|
||||
hideSettingsWindow() {
|
||||
if (this.wasJustDragged) return;
|
||||
if (window.require) {
|
||||
if (window.api) {
|
||||
console.log(`[MainHeader] hideSettingsWindow called at ${Date.now()}`);
|
||||
window.require('electron').ipcRenderer.send('hide-settings-window');
|
||||
window.api.mainHeader.hideSettingsWindow();
|
||||
}
|
||||
}
|
||||
|
||||
@ -557,15 +559,26 @@ export class MainHeader extends LitElement {
|
||||
this.isTogglingSession = true;
|
||||
|
||||
try {
|
||||
const channel = 'toggle-feature';
|
||||
const args = ['listen'];
|
||||
await this.invoke(channel, ...args);
|
||||
const channel = 'listen:changeSession';
|
||||
const listenButtonText = this._getListenButtonText(this.listenSessionStatus);
|
||||
await this.invoke(channel, listenButtonText);
|
||||
} catch (error) {
|
||||
console.error('IPC invoke for session toggle failed:', error);
|
||||
console.error('IPC invoke for session change failed:', error);
|
||||
this.isTogglingSession = false;
|
||||
}
|
||||
}
|
||||
|
||||
async _handleAskClick() {
|
||||
if (this.wasJustDragged) return;
|
||||
|
||||
try {
|
||||
const channel = 'ask:toggleAskButton';
|
||||
await this.invoke(channel);
|
||||
} catch (error) {
|
||||
console.error('IPC invoke for ask button failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
renderShortcut(accelerator) {
|
||||
if (!accelerator) return html``;
|
||||
@ -591,11 +604,13 @@ export class MainHeader extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const listenButtonText = this._getListenButtonText(this.listenSessionStatus);
|
||||
|
||||
const buttonClasses = {
|
||||
active: this.actionText === 'Stop',
|
||||
done: this.actionText === 'Done',
|
||||
active: listenButtonText === 'Stop',
|
||||
done: listenButtonText === 'Done',
|
||||
};
|
||||
const showStopIcon = this.actionText === 'Stop' || this.actionText === 'Done';
|
||||
const showStopIcon = listenButtonText === 'Stop' || listenButtonText === 'Done';
|
||||
|
||||
return html`
|
||||
<div class="header" @mousedown=${this.handleMouseDown}>
|
||||
@ -612,7 +627,7 @@ export class MainHeader extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<div class="action-text">
|
||||
<div class="action-text-content">${this.actionText}</div>
|
||||
<div class="action-text-content">${listenButtonText}</div>
|
||||
</div>
|
||||
<div class="listen-icon">
|
||||
${showStopIcon
|
||||
@ -632,7 +647,7 @@ export class MainHeader extends LitElement {
|
||||
`}
|
||||
</button>
|
||||
|
||||
<div class="header-actions ask-action" @click=${() => this.invoke('toggle-feature', 'ask')}>
|
||||
<div class="header-actions ask-action" @click=${() => this._handleAskClick()}>
|
||||
<div class="action-text">
|
||||
<div class="action-text-content">Ask</div>
|
||||
</div>
|
@ -288,13 +288,12 @@ export class PermissionHeader extends LitElement {
|
||||
}
|
||||
|
||||
async checkPermissions() {
|
||||
if (!window.require || this.isChecking) return;
|
||||
if (!window.api || this.isChecking) return;
|
||||
|
||||
this.isChecking = true;
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
try {
|
||||
const permissions = await ipcRenderer.invoke('check-system-permissions');
|
||||
const permissions = await window.api.permissionHeader.checkSystemPermissions();
|
||||
console.log('[PermissionHeader] Permission check result:', permissions);
|
||||
|
||||
const prevMic = this.microphoneGranted;
|
||||
@ -324,13 +323,12 @@ export class PermissionHeader extends LitElement {
|
||||
}
|
||||
|
||||
async handleMicrophoneClick() {
|
||||
if (!window.require || this.microphoneGranted === 'granted') return;
|
||||
if (!window.api || this.microphoneGranted === 'granted') return;
|
||||
|
||||
console.log('[PermissionHeader] Requesting microphone permission...');
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
try {
|
||||
const result = await ipcRenderer.invoke('check-system-permissions');
|
||||
const result = await window.api.permissionHeader.checkSystemPermissions();
|
||||
console.log('[PermissionHeader] Microphone permission result:', result);
|
||||
|
||||
if (result.microphone === 'granted') {
|
||||
@ -340,7 +338,7 @@ export class PermissionHeader extends LitElement {
|
||||
}
|
||||
|
||||
if (result.microphone === 'not-determined' || result.microphone === 'denied' || result.microphone === 'unknown' || result.microphone === 'restricted') {
|
||||
const res = await ipcRenderer.invoke('request-microphone-permission');
|
||||
const res = await window.api.permissionHeader.requestMicrophonePermission();
|
||||
if (res.status === 'granted' || res.success === true) {
|
||||
this.microphoneGranted = 'granted';
|
||||
this.requestUpdate();
|
||||
@ -357,13 +355,12 @@ export class PermissionHeader extends LitElement {
|
||||
}
|
||||
|
||||
async handleScreenClick() {
|
||||
if (!window.require || this.screenGranted === 'granted') return;
|
||||
if (!window.api || this.screenGranted === 'granted') return;
|
||||
|
||||
console.log('[PermissionHeader] Checking screen recording permission...');
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
try {
|
||||
const permissions = await ipcRenderer.invoke('check-system-permissions');
|
||||
const permissions = await window.api.permissionHeader.checkSystemPermissions();
|
||||
console.log('[PermissionHeader] Screen permission check result:', permissions);
|
||||
|
||||
if (permissions.screen === 'granted') {
|
||||
@ -373,7 +370,7 @@ export class PermissionHeader extends LitElement {
|
||||
}
|
||||
if (permissions.screen === 'not-determined' || permissions.screen === 'denied' || permissions.screen === 'unknown' || permissions.screen === 'restricted') {
|
||||
console.log('[PermissionHeader] Opening screen recording preferences...');
|
||||
await ipcRenderer.invoke('open-system-preferences', 'screen-recording');
|
||||
await window.api.permissionHeader.openSystemPreferences('screen-recording');
|
||||
}
|
||||
|
||||
// Check permissions again after a delay
|
||||
@ -389,10 +386,9 @@ export class PermissionHeader extends LitElement {
|
||||
this.microphoneGranted === 'granted' &&
|
||||
this.screenGranted === 'granted') {
|
||||
// Mark permissions as completed
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
if (window.api) {
|
||||
try {
|
||||
await ipcRenderer.invoke('mark-permissions-completed');
|
||||
await window.api.permissionHeader.markPermissionsCompleted();
|
||||
console.log('[PermissionHeader] Marked permissions as completed');
|
||||
} catch (error) {
|
||||
console.error('[PermissionHeader] Error marking permissions as completed:', error);
|
||||
@ -405,8 +401,8 @@ export class PermissionHeader extends LitElement {
|
||||
|
||||
handleClose() {
|
||||
console.log('Close button clicked');
|
||||
if (window.require) {
|
||||
window.require('electron').ipcRenderer.invoke('quit-application');
|
||||
if (window.api) {
|
||||
window.api.common.quitApplication();
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';
|
||||
import { SettingsView } from '../features/settings/SettingsView.js';
|
||||
import { AssistantView } from '../features/listen/AssistantView.js';
|
||||
import { AskView } from '../features/ask/AskView.js';
|
||||
import { ShortcutSettingsView } from '../features/settings/ShortCutSettingsView.js';
|
||||
import { SettingsView } from '../settings/SettingsView.js';
|
||||
import { ListenView } from '../listen/ListenView.js';
|
||||
import { AskView } from '../ask/AskView.js';
|
||||
import { ShortcutSettingsView } from '../settings/ShortCutSettingsView.js';
|
||||
|
||||
import '../features/listen/renderer/renderer.js';
|
||||
import '../listen/audioCore/renderer.js';
|
||||
|
||||
export class PickleGlassApp extends LitElement {
|
||||
static styles = css`
|
||||
@ -17,7 +17,7 @@ export class PickleGlassApp extends LitElement {
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
assistant-view {
|
||||
listen-view {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -74,33 +74,21 @@ export class PickleGlassApp extends LitElement {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
ipcRenderer.on('click-through-toggled', (_, isEnabled) => {
|
||||
if (window.api) {
|
||||
window.api.pickleGlassApp.onClickThroughToggled((_, isEnabled) => {
|
||||
this._isClickThrough = isEnabled;
|
||||
});
|
||||
// ipcRenderer.on('start-listening-session', () => {
|
||||
// console.log('Received start-listening-session command, calling handleListenClick.');
|
||||
// this.handleListenClick();
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.removeAllListeners('click-through-toggled');
|
||||
// ipcRenderer.removeAllListeners('start-listening-session');
|
||||
if (window.api) {
|
||||
window.api.pickleGlassApp.removeAllClickThroughListeners();
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
// if (changedProperties.has('isMainViewVisible') || changedProperties.has('currentView')) {
|
||||
// this.requestWindowResize();
|
||||
// }
|
||||
|
||||
if (changedProperties.has('currentView')) {
|
||||
const viewContainer = this.shadowRoot?.querySelector('.view-container');
|
||||
if (viewContainer) {
|
||||
@ -129,40 +117,9 @@ export class PickleGlassApp extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// async handleListenClick() {
|
||||
// if (window.require) {
|
||||
// const { ipcRenderer } = window.require('electron');
|
||||
// const isActive = await ipcRenderer.invoke('is-session-active');
|
||||
// // if (isActive) {
|
||||
// // console.log('Session is already active. No action needed.');
|
||||
// // return;
|
||||
// // }
|
||||
// }
|
||||
|
||||
// if (window.pickleGlass) {
|
||||
// // await window.pickleGlass.initializeopenai(this.selectedProfile, this.selectedLanguage);
|
||||
// window.pickleGlass.startCapture(this.selectedScreenshotInterval, this.selectedImageQuality);
|
||||
// }
|
||||
|
||||
// // 🔄 Clear previous summary/analysis when a new listening session begins
|
||||
// this.structuredData = {
|
||||
// summary: [],
|
||||
// topic: { header: '', bullets: [] },
|
||||
// actions: [],
|
||||
// followUps: [],
|
||||
// };
|
||||
|
||||
// this.currentResponseIndex = -1;
|
||||
// this.startTime = Date.now();
|
||||
// this.currentView = 'listen';
|
||||
// this.isMainViewVisible = true;
|
||||
// }
|
||||
|
||||
async handleClose() {
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
await ipcRenderer.invoke('quit-application');
|
||||
if (window.api) {
|
||||
await window.api.common.quitApplication();
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,12 +129,12 @@ export class PickleGlassApp extends LitElement {
|
||||
render() {
|
||||
switch (this.currentView) {
|
||||
case 'listen':
|
||||
return html`<assistant-view
|
||||
return html`<listen-view
|
||||
.currentResponseIndex=${this.currentResponseIndex}
|
||||
.selectedProfile=${this.selectedProfile}
|
||||
.structuredData=${this.structuredData}
|
||||
@response-index-changed=${e => (this.currentResponseIndex = e.detail.index)}
|
||||
></assistant-view>`;
|
||||
></listen-view>`;
|
||||
case 'ask':
|
||||
return html`<ask-view></ask-view>`;
|
||||
case 'settings':
|
@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-security-policy" content="script-src 'self' 'unsafe-inline'" />
|
||||
<meta http-equiv="content-security-policy" content="script-src 'self' 'unsafe-inline' 'unsafe-eval'" />
|
||||
<title>Pickle Glass Content</title>
|
||||
<style>
|
||||
:root {
|
||||
@ -230,7 +230,7 @@
|
||||
<body>
|
||||
<script src="../assets/marked-4.3.0.min.js"></script>
|
||||
|
||||
<script type="module" src="../../public/build/content.js"></script>
|
||||
<script type="module" src="../../../public/build/content.js"></script>
|
||||
|
||||
<pickle-glass-app id="pickle-glass"></pickle-glass-app>
|
||||
|
||||
@ -238,15 +238,13 @@
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const app = document.getElementById('pickle-glass');
|
||||
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
if (window.api) {
|
||||
// --- REFACTORED: Event-driven animation handling ---
|
||||
app.addEventListener('animationend', (event) => {
|
||||
// 숨김 애니메이션이 끝나면 main 프로세스에 알려 창을 실제로 숨깁니다.
|
||||
if (event.animationName === 'slideUpToHeader' || event.animationName === 'settingsCollapseToButton') {
|
||||
console.log(`Animation finished: ${event.animationName}. Notifying main process.`);
|
||||
ipcRenderer.send('animation-finished');
|
||||
window.api.content.sendAnimationFinished();
|
||||
|
||||
// 완료 후 애니메이션 클래스 정리
|
||||
app.classList.remove('window-sliding-up', 'settings-window-hide');
|
||||
@ -257,26 +255,26 @@
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('window-show-animation', () => {
|
||||
window.api.content.onWindowShowAnimation(() => {
|
||||
console.log('Starting window show animation');
|
||||
app.classList.remove('window-hidden', 'window-sliding-up', 'settings-window-hide');
|
||||
app.classList.add('window-sliding-down');
|
||||
});
|
||||
|
||||
ipcRenderer.on('window-hide-animation', () => {
|
||||
window.api.content.onWindowHideAnimation(() => {
|
||||
console.log('Starting window hide animation');
|
||||
app.classList.remove('window-sliding-down', 'settings-window-show');
|
||||
app.classList.add('window-sliding-up');
|
||||
});
|
||||
|
||||
ipcRenderer.on('settings-window-hide-animation', () => {
|
||||
window.api.content.onSettingsWindowHideAnimation(() => {
|
||||
console.log('Starting settings window hide animation');
|
||||
app.classList.remove('window-sliding-down', 'settings-window-show');
|
||||
app.classList.add('settings-window-hide');
|
||||
});
|
||||
|
||||
// --- UNCHANGED: Existing logic for listen window movement ---
|
||||
ipcRenderer.on('listen-window-move-to-center', () => {
|
||||
window.api.content.onListenWindowMoveToCenter(() => {
|
||||
console.log('Moving listen window to center');
|
||||
app.classList.add('listen-window-moving');
|
||||
app.classList.remove('listen-window-left');
|
||||
@ -287,7 +285,7 @@
|
||||
}, 350);
|
||||
});
|
||||
|
||||
ipcRenderer.on('listen-window-move-to-left', () => {
|
||||
window.api.content.onListenWindowMoveToLeft(() => {
|
||||
console.log('Moving listen window to left');
|
||||
app.classList.add('listen-window-moving');
|
||||
app.classList.remove('listen-window-center');
|
@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-security-policy" content="script-src 'self' 'unsafe-inline'" />
|
||||
<meta http-equiv="content-security-policy" content="script-src 'self' 'unsafe-inline' 'unsafe-eval'" />
|
||||
<title>Pickle Glass Header</title>
|
||||
<style>
|
||||
html,
|
||||
@ -17,7 +17,7 @@
|
||||
<div id="header-container" tabindex="0" style="outline: none;">
|
||||
</div>
|
||||
|
||||
<script type="module" src="../../public/build/header.js"></script>
|
||||
<script type="module" src="../../../public/build/header.js"></script>
|
||||
<script>
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('glass') === 'true') {
|
@ -1,4 +1,4 @@
|
||||
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
|
||||
import { html, css, LitElement } from '../../ui/assets/lit-core-2.7.4.min.js';
|
||||
|
||||
export class AskView extends LitElement {
|
||||
static properties = {
|
||||
@ -719,28 +719,21 @@ export class AskView extends LitElement {
|
||||
this.headerText = 'AI Response';
|
||||
this.headerAnimating = false;
|
||||
this.isStreaming = false;
|
||||
this.accumulatedResponse = '';
|
||||
|
||||
this.marked = null;
|
||||
this.hljs = null;
|
||||
this.DOMPurify = null;
|
||||
this.isLibrariesLoaded = false;
|
||||
|
||||
this.handleStreamChunk = this.handleStreamChunk.bind(this);
|
||||
this.handleStreamEnd = this.handleStreamEnd.bind(this);
|
||||
|
||||
this.handleSendText = this.handleSendText.bind(this);
|
||||
this.handleGlobalSendRequest = this.handleGlobalSendRequest.bind(this);
|
||||
this.handleTextKeydown = this.handleTextKeydown.bind(this);
|
||||
this.closeResponsePanel = this.closeResponsePanel.bind(this);
|
||||
this.handleCopy = this.handleCopy.bind(this);
|
||||
this.clearResponseContent = this.clearResponseContent.bind(this);
|
||||
this.processAssistantQuestion = this.processAssistantQuestion.bind(this);
|
||||
this.handleToggleTextInput = this.handleToggleTextInput.bind(this);
|
||||
this.handleEscKey = this.handleEscKey.bind(this);
|
||||
this.handleDocumentClick = this.handleDocumentClick.bind(this);
|
||||
this.handleWindowBlur = this.handleWindowBlur.bind(this);
|
||||
|
||||
this.handleScroll = this.handleScroll.bind(this);
|
||||
this.handleCloseAskWindow = this.handleCloseAskWindow.bind(this);
|
||||
this.handleCloseIfNoContent = this.handleCloseIfNoContent.bind(this);
|
||||
|
||||
this.loadLibraries();
|
||||
|
||||
@ -748,6 +741,86 @@ export class AskView extends LitElement {
|
||||
this.isThrottled = false;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
console.log('📱 AskView connectedCallback - IPC 이벤트 리스너 설정');
|
||||
|
||||
document.addEventListener('keydown', this.handleEscKey);
|
||||
|
||||
this.resizeObserver = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
const needed = entry.contentRect.height;
|
||||
const current = window.innerHeight;
|
||||
|
||||
if (needed > current - 4) {
|
||||
this.requestWindowResize(Math.ceil(needed));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const container = this.shadowRoot?.querySelector('.ask-container');
|
||||
if (container) this.resizeObserver.observe(container);
|
||||
|
||||
this.handleQuestionFromAssistant = (event, question) => {
|
||||
console.log('📨 AskView: Received question from ListenView:', question);
|
||||
this.handleSendText(null, question);
|
||||
};
|
||||
|
||||
if (window.api) {
|
||||
window.api.askView.onShowTextInput(() => {
|
||||
console.log('📤 Show text input signal received');
|
||||
if (!this.showTextInput) {
|
||||
this.showTextInput = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
window.api.askView.onScrollResponseUp(() => this.handleScroll('up'));
|
||||
window.api.askView.onScrollResponseDown(() => this.handleScroll('down'));
|
||||
window.api.askView.onAskStateUpdate((event, newState) => {
|
||||
this.currentResponse = newState.currentResponse;
|
||||
this.currentQuestion = newState.currentQuestion;
|
||||
this.isLoading = newState.isLoading;
|
||||
this.isStreaming = newState.isStreaming;
|
||||
this.showTextInput = newState.showTextInput;
|
||||
});
|
||||
console.log('✅ AskView: IPC 이벤트 리스너 등록 완료');
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.resizeObserver?.disconnect();
|
||||
|
||||
console.log('📱 AskView disconnectedCallback - IPC 이벤트 리스너 제거');
|
||||
|
||||
document.removeEventListener('keydown', this.handleEscKey);
|
||||
|
||||
if (this.copyTimeout) {
|
||||
clearTimeout(this.copyTimeout);
|
||||
}
|
||||
|
||||
if (this.headerAnimationTimeout) {
|
||||
clearTimeout(this.headerAnimationTimeout);
|
||||
}
|
||||
|
||||
if (this.streamingTimeout) {
|
||||
clearTimeout(this.streamingTimeout);
|
||||
}
|
||||
|
||||
Object.values(this.lineCopyTimeouts).forEach(timeout => clearTimeout(timeout));
|
||||
|
||||
if (window.api) {
|
||||
window.api.askView.removeOnAskStateUpdate(this.handleAskStateUpdate);
|
||||
window.api.askView.removeOnShowTextInput(this.handleShowTextInput);
|
||||
window.api.askView.removeOnScrollResponseUp(this.handleScroll);
|
||||
window.api.askView.removeOnScrollResponseDown(this.handleScroll);
|
||||
console.log('✅ AskView: IPC 이벤트 리스너 제거 필요');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async loadLibraries() {
|
||||
try {
|
||||
if (!window.marked) {
|
||||
@ -804,38 +877,46 @@ export class AskView extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
handleDocumentClick(e) {
|
||||
handleCloseAskWindow() {
|
||||
this.clearResponseContent();
|
||||
window.api.askView.closeAskWindow();
|
||||
}
|
||||
|
||||
handleCloseIfNoContent() {
|
||||
if (!this.currentResponse && !this.isLoading && !this.isStreaming) {
|
||||
const askContainer = this.shadowRoot?.querySelector('.ask-container');
|
||||
if (askContainer && !e.composedPath().includes(askContainer)) {
|
||||
this.closeIfNoContent();
|
||||
}
|
||||
this.handleCloseAskWindow();
|
||||
}
|
||||
}
|
||||
|
||||
handleEscKey(e) {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
this.closeResponsePanel();
|
||||
this.handleCloseIfNoContent();
|
||||
}
|
||||
}
|
||||
|
||||
handleWindowBlur() {
|
||||
if (!this.currentResponse && !this.isLoading && !this.isStreaming) {
|
||||
// If there's no active content, ask the main process to close this window.
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.invoke('close-ask-window-if-empty');
|
||||
clearResponseContent() {
|
||||
this.currentResponse = '';
|
||||
this.currentQuestion = '';
|
||||
this.isLoading = false;
|
||||
this.isStreaming = false;
|
||||
this.headerText = 'AI Response';
|
||||
this.showTextInput = true;
|
||||
}
|
||||
|
||||
handleInputFocus() {
|
||||
this.isInputFocused = true;
|
||||
}
|
||||
|
||||
focusTextInput() {
|
||||
requestAnimationFrame(() => {
|
||||
const textInput = this.shadowRoot?.getElementById('textInput');
|
||||
if (textInput) {
|
||||
textInput.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeIfNoContent() {
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.invoke('force-close-window', 'ask');
|
||||
}
|
||||
}
|
||||
|
||||
loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -875,125 +956,6 @@ export class AskView extends LitElement {
|
||||
return text;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
console.log('📱 AskView connectedCallback - IPC 이벤트 리스너 설정');
|
||||
|
||||
document.addEventListener('click', this.handleDocumentClick, true);
|
||||
document.addEventListener('keydown', this.handleEscKey);
|
||||
|
||||
this.resizeObserver = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
const needed = entry.contentRect.height;
|
||||
const current = window.innerHeight;
|
||||
|
||||
if (needed > current - 4) {
|
||||
this.requestWindowResize(Math.ceil(needed));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const container = this.shadowRoot?.querySelector('.ask-container');
|
||||
if (container) this.resizeObserver.observe(container);
|
||||
|
||||
this.handleQuestionFromAssistant = (event, question) => {
|
||||
console.log('📨 AskView: Received question from AssistantView:', question);
|
||||
this.currentResponse = '';
|
||||
this.isStreaming = false;
|
||||
this.requestUpdate();
|
||||
|
||||
this.currentQuestion = question;
|
||||
this.isLoading = true;
|
||||
this.showTextInput = false;
|
||||
this.headerText = 'analyzing screen...';
|
||||
this.startHeaderAnimation();
|
||||
this.requestUpdate();
|
||||
|
||||
this.processAssistantQuestion(question);
|
||||
};
|
||||
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.on('ask-global-send', this.handleGlobalSendRequest);
|
||||
ipcRenderer.on('toggle-text-input', this.handleToggleTextInput);
|
||||
ipcRenderer.on('receive-question-from-assistant', this.handleQuestionFromAssistant);
|
||||
ipcRenderer.on('hide-text-input', () => {
|
||||
console.log('📤 Hide text input signal received');
|
||||
this.showTextInput = false;
|
||||
this.requestUpdate();
|
||||
});
|
||||
ipcRenderer.on('clear-ask-response', () => {
|
||||
console.log('📤 Clear response signal received');
|
||||
this.currentResponse = '';
|
||||
this.isStreaming = false;
|
||||
this.isLoading = false;
|
||||
this.headerText = 'AI Response';
|
||||
this.requestUpdate();
|
||||
});
|
||||
ipcRenderer.on('window-hide-animation', () => {
|
||||
console.log('📤 Ask window hiding - clearing response content');
|
||||
setTimeout(() => {
|
||||
this.clearResponseContent();
|
||||
}, 250);
|
||||
});
|
||||
ipcRenderer.on('window-blur', this.handleWindowBlur);
|
||||
ipcRenderer.on('window-did-show', () => {
|
||||
if (!this.currentResponse && !this.isLoading && !this.isStreaming) {
|
||||
this.focusTextInput();
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on('ask-response-chunk', this.handleStreamChunk);
|
||||
ipcRenderer.on('ask-response-stream-end', this.handleStreamEnd);
|
||||
|
||||
ipcRenderer.on('scroll-response-up', () => this.handleScroll('up'));
|
||||
ipcRenderer.on('scroll-response-down', () => this.handleScroll('down'));
|
||||
console.log('✅ AskView: IPC 이벤트 리스너 등록 완료');
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.resizeObserver?.disconnect();
|
||||
|
||||
console.log('📱 AskView disconnectedCallback - IPC 이벤트 리스너 제거');
|
||||
|
||||
document.removeEventListener('click', this.handleDocumentClick, true);
|
||||
document.removeEventListener('keydown', this.handleEscKey);
|
||||
|
||||
if (this.copyTimeout) {
|
||||
clearTimeout(this.copyTimeout);
|
||||
}
|
||||
|
||||
if (this.headerAnimationTimeout) {
|
||||
clearTimeout(this.headerAnimationTimeout);
|
||||
}
|
||||
|
||||
if (this.streamingTimeout) {
|
||||
clearTimeout(this.streamingTimeout);
|
||||
}
|
||||
|
||||
Object.values(this.lineCopyTimeouts).forEach(timeout => clearTimeout(timeout));
|
||||
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.removeListener('ask-global-send', this.handleGlobalSendRequest);
|
||||
ipcRenderer.removeListener('toggle-text-input', this.handleToggleTextInput);
|
||||
ipcRenderer.removeListener('clear-ask-response', () => { });
|
||||
ipcRenderer.removeListener('hide-text-input', () => { });
|
||||
ipcRenderer.removeListener('window-hide-animation', () => { });
|
||||
ipcRenderer.removeListener('window-blur', this.handleWindowBlur);
|
||||
|
||||
ipcRenderer.removeListener('ask-response-chunk', this.handleStreamChunk);
|
||||
ipcRenderer.removeListener('ask-response-stream-end', this.handleStreamEnd);
|
||||
|
||||
ipcRenderer.removeListener('scroll-response-up', () => this.handleScroll('up'));
|
||||
ipcRenderer.removeListener('scroll-response-down', () => this.handleScroll('down'));
|
||||
console.log('✅ AskView: IPC 이벤트 리스너 제거 완료');
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll(direction) {
|
||||
const scrollableElement = this.shadowRoot.querySelector('#responseContainer');
|
||||
if (scrollableElement) {
|
||||
@ -1006,56 +968,26 @@ export class AskView extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
// --- 스트리밍 처리 핸들러 ---
|
||||
handleStreamChunk(event, { token }) {
|
||||
if (!this.isStreaming) {
|
||||
this.isStreaming = true;
|
||||
this.isLoading = false;
|
||||
this.accumulatedResponse = '';
|
||||
const container = this.shadowRoot.getElementById('responseContainer');
|
||||
if (container) container.innerHTML = '';
|
||||
this.headerText = 'AI Response';
|
||||
this.headerAnimating = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
this.accumulatedResponse += token;
|
||||
this.renderContent();
|
||||
}
|
||||
|
||||
handleStreamEnd() {
|
||||
this.isStreaming = false;
|
||||
this.currentResponse = this.accumulatedResponse;
|
||||
if (this.headerText !== 'AI Response') {
|
||||
this.headerText = 'AI Response';
|
||||
this.requestUpdate();
|
||||
}
|
||||
this.renderContent();
|
||||
}
|
||||
|
||||
// ✨ 렌더링 로직 통합
|
||||
renderContent() {
|
||||
if (!this.isLoading && !this.isStreaming && !this.currentResponse) {
|
||||
const responseContainer = this.shadowRoot.getElementById('responseContainer');
|
||||
if (responseContainer) responseContainer.innerHTML = '<div class="empty-state">Ask a question to see the response here</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const responseContainer = this.shadowRoot.getElementById('responseContainer');
|
||||
if (!responseContainer) return;
|
||||
|
||||
|
||||
// ✨ 로딩 상태를 먼저 확인
|
||||
if (this.isLoading) {
|
||||
responseContainer.innerHTML = `
|
||||
<div class="loading-dots">
|
||||
<div class="loading-dot"></div><div class="loading-dot"></div><div class="loading-dot"></div>
|
||||
</div>`;
|
||||
responseContainer.innerHTML = `<div class="loading-dots">...</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let textToRender = this.isStreaming ? this.accumulatedResponse : this.currentResponse;
|
||||
|
||||
// 불완전한 마크다운 수정
|
||||
textToRender = this.fixIncompleteMarkdown(textToRender);
|
||||
|
||||
// ✨ 응답이 없을 때의 처리
|
||||
if (!this.currentResponse) {
|
||||
responseContainer.innerHTML = `<div class="empty-state">...</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let textToRender = this.fixIncompleteMarkdown(this.currentResponse);
|
||||
textToRender = this.fixIncompleteCodeBlocks(textToRender);
|
||||
|
||||
|
||||
if (this.isLibrariesLoaded && this.marked && this.DOMPurify) {
|
||||
try {
|
||||
@ -1136,27 +1068,10 @@ export class AskView extends LitElement {
|
||||
this.adjustWindowHeightThrottled();
|
||||
}
|
||||
|
||||
clearResponseContent() {
|
||||
this.currentResponse = '';
|
||||
this.currentQuestion = '';
|
||||
this.isLoading = false;
|
||||
this.isStreaming = false;
|
||||
this.headerText = 'AI Response';
|
||||
this.showTextInput = true;
|
||||
this.accumulatedResponse = '';
|
||||
this.requestUpdate();
|
||||
this.renderContent(); // 👈 updateResponseContent() 대신 renderContent() 호출
|
||||
}
|
||||
|
||||
handleToggleTextInput() {
|
||||
this.showTextInput = !this.showTextInput;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
requestWindowResize(targetHeight) {
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.invoke('adjust-window-height', targetHeight);
|
||||
if (window.api) {
|
||||
window.api.askView.adjustWindowHeight(targetHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1196,13 +1111,6 @@ export class AskView extends LitElement {
|
||||
.replace(/`(.*?)`/g, '<code>$1</code>');
|
||||
}
|
||||
|
||||
closeResponsePanel() {
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.invoke('force-close-window', 'ask');
|
||||
}
|
||||
}
|
||||
|
||||
fixIncompleteMarkdown(text) {
|
||||
if (!text) return text;
|
||||
|
||||
@ -1240,29 +1148,6 @@ export class AskView extends LitElement {
|
||||
return text;
|
||||
}
|
||||
|
||||
// ✨ processAssistantQuestion 수정
|
||||
async processAssistantQuestion(question) {
|
||||
this.currentQuestion = question;
|
||||
this.showTextInput = false;
|
||||
this.isLoading = true;
|
||||
this.isStreaming = false;
|
||||
this.currentResponse = '';
|
||||
this.accumulatedResponse = '';
|
||||
this.startHeaderAnimation();
|
||||
this.requestUpdate();
|
||||
this.renderContent();
|
||||
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.invoke('ask:sendMessage', question).catch(error => {
|
||||
console.error('Error processing assistant question:', error);
|
||||
this.isLoading = false;
|
||||
this.isStreaming = false;
|
||||
this.currentResponse = `Error: ${error.message}`;
|
||||
this.renderContent();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleCopy() {
|
||||
if (this.copyState === 'copied') return;
|
||||
@ -1332,33 +1217,16 @@ export class AskView extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
async handleSendText() {
|
||||
async handleSendText(e, overridingText = '') {
|
||||
const textInput = this.shadowRoot?.getElementById('textInput');
|
||||
if (!textInput) return;
|
||||
const text = textInput.value.trim();
|
||||
const text = (overridingText || textInput?.value || '').trim();
|
||||
if (!text) return;
|
||||
|
||||
textInput.value = '';
|
||||
|
||||
this.currentQuestion = text;
|
||||
this.lineCopyState = {};
|
||||
this.showTextInput = false;
|
||||
this.isLoading = true;
|
||||
this.isStreaming = false;
|
||||
this.currentResponse = '';
|
||||
this.accumulatedResponse = '';
|
||||
this.startHeaderAnimation();
|
||||
this.requestUpdate();
|
||||
this.renderContent();
|
||||
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.invoke('ask:sendMessage', text).catch(error => {
|
||||
if (window.api) {
|
||||
window.api.askView.sendMessage(text).catch(error => {
|
||||
console.error('Error sending text:', error);
|
||||
this.isLoading = false;
|
||||
this.isStreaming = false;
|
||||
this.currentResponse = `Error: ${error.message}`;
|
||||
this.renderContent();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1380,50 +1248,25 @@ export class AskView extends LitElement {
|
||||
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('isLoading')) {
|
||||
|
||||
// ✨ isLoading 또는 currentResponse가 변경될 때마다 뷰를 다시 그립니다.
|
||||
if (changedProperties.has('isLoading') || changedProperties.has('currentResponse')) {
|
||||
this.renderContent();
|
||||
}
|
||||
|
||||
if (changedProperties.has('showTextInput') || changedProperties.has('isLoading')) {
|
||||
|
||||
if (changedProperties.has('showTextInput') || changedProperties.has('isLoading') || changedProperties.has('currentResponse')) {
|
||||
this.adjustWindowHeightThrottled();
|
||||
}
|
||||
|
||||
|
||||
if (changedProperties.has('showTextInput') && this.showTextInput) {
|
||||
this.focusTextInput();
|
||||
}
|
||||
}
|
||||
|
||||
focusTextInput() {
|
||||
requestAnimationFrame(() => {
|
||||
const textInput = this.shadowRoot?.getElementById('textInput');
|
||||
if (textInput) {
|
||||
textInput.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
setTimeout(() => this.adjustWindowHeight(), 200);
|
||||
}
|
||||
|
||||
handleGlobalSendRequest() {
|
||||
const textInput = this.shadowRoot?.getElementById('textInput');
|
||||
|
||||
if (!this.showTextInput) {
|
||||
this.showTextInput = true;
|
||||
this.requestUpdate();
|
||||
this.focusTextInput();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!textInput) return;
|
||||
|
||||
textInput.focus();
|
||||
|
||||
if (!textInput.value.trim()) return;
|
||||
|
||||
this.handleSendText();
|
||||
}
|
||||
|
||||
getTruncatedQuestion(question, maxLength = 30) {
|
||||
if (!question) return '';
|
||||
@ -1431,27 +1274,11 @@ export class AskView extends LitElement {
|
||||
return question.substring(0, maxLength) + '...';
|
||||
}
|
||||
|
||||
handleInputFocus() {
|
||||
this.isInputFocused = true;
|
||||
}
|
||||
|
||||
handleInputBlur(e) {
|
||||
this.isInputFocused = false;
|
||||
|
||||
// 잠시 후 포커스가 다른 곳으로 갔는지 확인
|
||||
setTimeout(() => {
|
||||
const activeElement = this.shadowRoot?.activeElement || document.activeElement;
|
||||
const textInput = this.shadowRoot?.getElementById('textInput');
|
||||
|
||||
// 포커스가 AskView 내부가 아니고, 응답이 없는 경우
|
||||
if (!this.currentResponse && !this.isLoading && !this.isStreaming && activeElement !== textInput && !this.isInputFocused) {
|
||||
this.closeIfNoContent();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
render() {
|
||||
const hasResponse = this.isLoading || this.currentResponse || this.isStreaming;
|
||||
const headerText = this.isLoading ? 'Thinking...' : 'AI Response';
|
||||
|
||||
return html`
|
||||
<div class="ask-container">
|
||||
@ -1464,7 +1291,7 @@ export class AskView extends LitElement {
|
||||
<path d="M8 12l2 2 4-4" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="response-label ${this.headerAnimating ? 'animating' : ''}">${this.headerText}</span>
|
||||
<span class="response-label">${headerText}</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="question-text">${this.getTruncatedQuestion(this.currentQuestion)}</span>
|
||||
@ -1486,7 +1313,7 @@ export class AskView extends LitElement {
|
||||
<path d="M20 6L9 17l-5-5" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="close-button" @click=${this.closeResponsePanel}>
|
||||
<button class="close-button" @click=${this.handleCloseAskWindow}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
@ -1509,7 +1336,6 @@ export class AskView extends LitElement {
|
||||
placeholder="Ask about your screen or audio"
|
||||
@keydown=${this.handleTextKeydown}
|
||||
@focus=${this.handleInputFocus}
|
||||
@blur=${this.handleInputBlur}
|
||||
/>
|
||||
<button
|
||||
class="submit-btn"
|
||||
@ -1527,7 +1353,7 @@ export class AskView extends LitElement {
|
||||
|
||||
// Dynamically resize the BrowserWindow to fit current content
|
||||
adjustWindowHeight() {
|
||||
if (!window.require) return;
|
||||
if (!window.api) return;
|
||||
|
||||
this.updateComplete.then(() => {
|
||||
const headerEl = this.shadowRoot.querySelector('.response-header');
|
||||
@ -1544,8 +1370,7 @@ export class AskView extends LitElement {
|
||||
|
||||
const targetHeight = Math.min(700, idealHeight);
|
||||
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.invoke('adjust-window-height', targetHeight);
|
||||
window.api.askView.adjustWindowHeight(targetHeight);
|
||||
|
||||
}).catch(err => console.error('AskView adjustWindowHeight error:', err));
|
||||
}
|
Before Width: | Height: | Size: 877 B After Width: | Height: | Size: 877 B |
Before Width: | Height: | Size: 226 B After Width: | Height: | Size: 226 B |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 125 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@ -1,8 +1,8 @@
|
||||
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
|
||||
import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';
|
||||
import './stt/SttView.js';
|
||||
import './summary/SummaryView.js';
|
||||
|
||||
export class AssistantView extends LitElement {
|
||||
export class ListenView extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
@ -453,9 +453,8 @@ export class AssistantView extends LitElement {
|
||||
if (this.isSessionActive) {
|
||||
this.startTimer();
|
||||
}
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.on('session-state-changed', (event, { isActive }) => {
|
||||
if (window.api) {
|
||||
window.api.listenView.onSessionStateChanged((event, { isActive }) => {
|
||||
const wasActive = this.isSessionActive;
|
||||
this.isSessionActive = isActive;
|
||||
|
||||
@ -514,7 +513,7 @@ export class AssistantView extends LitElement {
|
||||
}
|
||||
|
||||
adjustWindowHeight() {
|
||||
if (!window.require) return;
|
||||
if (!window.api) return;
|
||||
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
@ -537,8 +536,7 @@ export class AssistantView extends LitElement {
|
||||
`[Height Adjusted] Mode: ${this.viewMode}, TopBar: ${topBarHeight}px, Content: ${contentHeight}px, Ideal: ${idealHeight}px, Target: ${targetHeight}px`
|
||||
);
|
||||
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.invoke('adjust-window-height', targetHeight);
|
||||
window.api.listenView.adjustWindowHeight(targetHeight);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error in adjustWindowHeight:', error);
|
||||
@ -689,4 +687,4 @@ export class AssistantView extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('assistant-view', AssistantView);
|
||||
customElements.define('listen-view', ListenView);
|
@ -1,5 +1,4 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
const createAecModule = require('../../../assets/aec.js');
|
||||
const createAecModule = require('./aec.js');
|
||||
|
||||
let aecModPromise = null; // 한 번만 로드
|
||||
let aecMod = null;
|
||||
@ -34,18 +33,15 @@ const SAMPLE_RATE = 24000;
|
||||
const AUDIO_CHUNK_DURATION = 0.1;
|
||||
const BUFFER_SIZE = 4096;
|
||||
|
||||
const isLinux = process.platform === 'linux';
|
||||
const isMacOS = process.platform === 'darwin';
|
||||
const isLinux = window.api.platform.isLinux;
|
||||
const isMacOS = window.api.platform.isMacOS;
|
||||
|
||||
let mediaStream = null;
|
||||
let micMediaStream = null;
|
||||
let screenshotInterval = null;
|
||||
let audioContext = null;
|
||||
let audioProcessor = null;
|
||||
let systemAudioContext = null;
|
||||
let systemAudioProcessor = null;
|
||||
let currentImageQuality = 'medium';
|
||||
let lastScreenshotBase64 = null;
|
||||
|
||||
let systemAudioBuffer = [];
|
||||
const MAX_SYSTEM_BUFFER_SIZE = 10;
|
||||
@ -141,10 +137,6 @@ function runAecSync(micF32, sysF32) {
|
||||
return micF32;
|
||||
}
|
||||
|
||||
// ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
|
||||
// 새로운 프레임 단위 처리 로직
|
||||
// ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
|
||||
|
||||
const frameSize = 160; // AEC 모듈 초기화 시 설정한 프레임 크기
|
||||
const numFrames = Math.floor(micF32.length / frameSize);
|
||||
|
||||
@ -198,7 +190,7 @@ function runAecSync(micF32, sysF32) {
|
||||
|
||||
|
||||
// System audio data handler
|
||||
ipcRenderer.on('system-audio-data', (event, { data }) => {
|
||||
window.api.listenCapture.onSystemAudioData((event, { data }) => {
|
||||
systemAudioBuffer.push({
|
||||
data: data,
|
||||
timestamp: Date.now(),
|
||||
@ -336,7 +328,7 @@ async function setupMicProcessing(micStream) {
|
||||
const pcm16 = convertFloat32ToInt16(processedChunk);
|
||||
const b64 = arrayBufferToBase64(pcm16.buffer);
|
||||
|
||||
ipcRenderer.invoke('send-audio-content', {
|
||||
window.api.listenCapture.sendMicAudioContent({
|
||||
data: b64,
|
||||
mimeType: 'audio/pcm;rate=24000',
|
||||
});
|
||||
@ -369,7 +361,7 @@ function setupLinuxMicProcessing(micStream) {
|
||||
const pcmData16 = convertFloat32ToInt16(chunk);
|
||||
const base64Data = arrayBufferToBase64(pcmData16.buffer);
|
||||
|
||||
await ipcRenderer.invoke('send-audio-content', {
|
||||
await window.api.listenCapture.sendMicAudioContent({
|
||||
data: base64Data,
|
||||
mimeType: 'audio/pcm;rate=24000',
|
||||
});
|
||||
@ -403,7 +395,7 @@ function setupSystemAudioProcessing(systemStream) {
|
||||
const base64Data = arrayBufferToBase64(pcmData16.buffer);
|
||||
|
||||
try {
|
||||
await ipcRenderer.invoke('send-system-audio-content', {
|
||||
await window.api.listenCapture.sendSystemAudioContent({
|
||||
data: base64Data,
|
||||
mimeType: 'audio/pcm;rate=24000',
|
||||
});
|
||||
@ -419,94 +411,10 @@ function setupSystemAudioProcessing(systemStream) {
|
||||
return { context: systemAudioContext, processor: systemProcessor };
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Screenshot functions (exact from renderer.js)
|
||||
// ---------------------------
|
||||
async function captureScreenshot(imageQuality = 'medium', isManual = false) {
|
||||
console.log(`Capturing ${isManual ? 'manual' : 'automated'} screenshot...`);
|
||||
|
||||
// Check rate limiting for automated screenshots only
|
||||
if (!isManual && tokenTracker.shouldThrottle()) {
|
||||
console.log('⚠️ Automated screenshot skipped due to rate limiting');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Request screenshot from main process
|
||||
const result = await ipcRenderer.invoke('capture-screenshot', {
|
||||
quality: imageQuality,
|
||||
});
|
||||
|
||||
if (result.success && result.base64) {
|
||||
// Store the latest screenshot
|
||||
lastScreenshotBase64 = result.base64;
|
||||
|
||||
// Note: sendResult is not defined in the original, this was likely an error
|
||||
// Commenting out this section as it references undefined variable
|
||||
/*
|
||||
if (sendResult.success) {
|
||||
// Track image tokens after successful send
|
||||
const imageTokens = tokenTracker.calculateImageTokens(result.width || 1920, result.height || 1080);
|
||||
tokenTracker.addTokens(imageTokens, 'image');
|
||||
console.log(`📊 Image sent successfully - ${imageTokens} tokens used (${result.width}x${result.height})`);
|
||||
} else {
|
||||
console.error('Failed to send image:', sendResult.error);
|
||||
}
|
||||
*/
|
||||
} else {
|
||||
console.error('Failed to capture screenshot:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error capturing screenshot:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function captureManualScreenshot(imageQuality = null) {
|
||||
console.log('Manual screenshot triggered');
|
||||
const quality = imageQuality || currentImageQuality;
|
||||
await captureScreenshot(quality, true);
|
||||
}
|
||||
|
||||
async function getCurrentScreenshot() {
|
||||
try {
|
||||
// First try to get a fresh screenshot from main process
|
||||
const result = await ipcRenderer.invoke('get-current-screenshot');
|
||||
|
||||
if (result.success && result.base64) {
|
||||
console.log('📸 Got fresh screenshot from main process');
|
||||
return result.base64;
|
||||
}
|
||||
|
||||
// If no screenshot available, capture one now
|
||||
console.log('📸 No screenshot available, capturing new one');
|
||||
const captureResult = await ipcRenderer.invoke('capture-screenshot', {
|
||||
quality: currentImageQuality,
|
||||
});
|
||||
|
||||
if (captureResult.success && captureResult.base64) {
|
||||
lastScreenshotBase64 = captureResult.base64;
|
||||
return captureResult.base64;
|
||||
}
|
||||
|
||||
// Fallback to last stored screenshot
|
||||
if (lastScreenshotBase64) {
|
||||
console.log('📸 Using cached screenshot');
|
||||
return lastScreenshotBase64;
|
||||
}
|
||||
|
||||
throw new Error('Failed to get screenshot');
|
||||
} catch (error) {
|
||||
console.error('Error getting current screenshot:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Main capture functions (exact from renderer.js)
|
||||
// ---------------------------
|
||||
async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'medium') {
|
||||
// Store the image quality for manual screenshots
|
||||
currentImageQuality = imageQuality;
|
||||
|
||||
// Reset token tracker when starting new capture session
|
||||
tokenTracker.reset();
|
||||
@ -518,15 +426,15 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
||||
console.log('Starting macOS capture with SystemAudioDump...');
|
||||
|
||||
// Start macOS audio capture
|
||||
const audioResult = await ipcRenderer.invoke('start-macos-audio');
|
||||
const audioResult = await window.api.listenCapture.startMacosSystemAudio();
|
||||
if (!audioResult.success) {
|
||||
console.warn('[listenCapture] macOS audio start failed:', audioResult.error);
|
||||
|
||||
// 이미 실행 중 → stop 후 재시도
|
||||
if (audioResult.error === 'already_running') {
|
||||
await ipcRenderer.invoke('stop-macos-audio');
|
||||
await window.api.listenCapture.stopMacosSystemAudio();
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
const retry = await ipcRenderer.invoke('start-macos-audio');
|
||||
const retry = await window.api.listenCapture.startMacosSystemAudio();
|
||||
if (!retry.success) {
|
||||
throw new Error('Retry failed: ' + retry.error);
|
||||
}
|
||||
@ -535,13 +443,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize screen capture in main process
|
||||
const screenResult = await ipcRenderer.invoke('start-screen-capture');
|
||||
if (!screenResult.success) {
|
||||
throw new Error('Failed to start screen capture: ' + screenResult.error);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
micMediaStream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
@ -603,14 +504,8 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
||||
// Windows - capture mic and system audio separately using native loopback
|
||||
console.log('Starting Windows capture with native loopback audio...');
|
||||
|
||||
// Start screen capture in main process for screenshots
|
||||
const screenResult = await ipcRenderer.invoke('start-screen-capture');
|
||||
if (!screenResult.success) {
|
||||
throw new Error('Failed to start screen capture: ' + screenResult.error);
|
||||
}
|
||||
|
||||
// Ensure STT sessions are initialized before starting audio capture
|
||||
const sessionActive = await ipcRenderer.invoke('is-session-active');
|
||||
const sessionActive = await window.api.listenCapture.isSessionActive();
|
||||
if (!sessionActive) {
|
||||
throw new Error('STT sessions not initialized - please wait for initialization to complete');
|
||||
}
|
||||
@ -657,20 +552,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
||||
// Continue without system audio
|
||||
}
|
||||
}
|
||||
|
||||
// Start capturing screenshots - check if manual mode
|
||||
if (screenshotIntervalSeconds === 'manual' || screenshotIntervalSeconds === 'Manual') {
|
||||
console.log('Manual mode enabled - screenshots will be captured on demand only');
|
||||
// Don't start automatic capture in manual mode
|
||||
} else {
|
||||
// 스크린샷 기능 활성화 (chatModel에서 사용)
|
||||
const intervalMilliseconds = parseInt(screenshotIntervalSeconds) * 1000;
|
||||
screenshotInterval = setInterval(() => captureScreenshot(imageQuality), intervalMilliseconds);
|
||||
|
||||
// Capture first screenshot immediately
|
||||
setTimeout(() => captureScreenshot(imageQuality), 100);
|
||||
console.log(`📸 Screenshot capture enabled with ${screenshotIntervalSeconds}s interval`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error starting capture:', err);
|
||||
// Note: pickleGlass.e() is not available in this context, commenting out
|
||||
@ -679,11 +560,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
||||
}
|
||||
|
||||
function stopCapture() {
|
||||
if (screenshotInterval) {
|
||||
clearInterval(screenshotInterval);
|
||||
screenshotInterval = null;
|
||||
}
|
||||
|
||||
// Clean up microphone resources
|
||||
if (audioProcessor) {
|
||||
audioProcessor.disconnect();
|
||||
@ -714,14 +590,9 @@ function stopCapture() {
|
||||
micMediaStream = null;
|
||||
}
|
||||
|
||||
// Stop screen capture in main process
|
||||
ipcRenderer.invoke('stop-screen-capture').catch(err => {
|
||||
console.error('Error stopping screen capture:', err);
|
||||
});
|
||||
|
||||
// Stop macOS audio capture if running
|
||||
if (isMacOS) {
|
||||
ipcRenderer.invoke('stop-macos-audio').catch(err => {
|
||||
window.api.listenCapture.stopMacosSystemAudio().catch(err => {
|
||||
console.error('Error stopping macOS audio:', err);
|
||||
});
|
||||
}
|
||||
@ -736,19 +607,14 @@ module.exports = {
|
||||
disposeAec, // 필요시 Rust 객체 파괴
|
||||
startCapture,
|
||||
stopCapture,
|
||||
captureManualScreenshot,
|
||||
getCurrentScreenshot,
|
||||
isLinux,
|
||||
isMacOS,
|
||||
};
|
||||
|
||||
// Expose functions to global scope for external access (exact from renderer.js)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.captureManualScreenshot = captureManualScreenshot;
|
||||
window.listenCapture = module.exports;
|
||||
window.pickleGlass = window.pickleGlass || {};
|
||||
window.pickleGlass.startCapture = startCapture;
|
||||
window.pickleGlass.stopCapture = stopCapture;
|
||||
window.pickleGlass.captureManualScreenshot = captureManualScreenshot;
|
||||
window.pickleGlass.getCurrentScreenshot = getCurrentScreenshot;
|
||||
}
|