diff --git a/src/bridge/featureBridge.js b/src/bridge/featureBridge.js index 5f3da4f..422ba40 100644 --- a/src/bridge/featureBridge.js +++ b/src/bridge/featureBridge.js @@ -67,6 +67,7 @@ module.exports = { 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()); + ipcMain.handle('stop-screen-capture', async () => askService.handleStopScreenCapture()); // Listen ipcMain.handle('send-audio-content', async (event, { data, mimeType }) => await listenService.handleSendAudioContent(data, mimeType)); diff --git a/src/features/ask/askService.js b/src/features/ask/askService.js index 8fbd7e9..dfa815e 100644 --- a/src/features/ask/askService.js +++ b/src/features/ask/askService.js @@ -1,10 +1,111 @@ const { BrowserWindow } = require('electron'); const { createStreamingLLM } = require('../common/ai/factory'); -const { getCurrentModelInfo, windowPool, captureScreenshot } = require('../../window/windowManager'); +const { getCurrentModelInfo, windowPool, updateLayout } = require('../../window/windowManager'); const sessionRepository = require('../common/repositories/session'); const askRepository = require('./repositories'); const { getSystemPrompt } = require('../common/prompts/promptBuilder'); -const listenService = require('../listen/listenService'); +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'); + +// 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; + +async function captureScreenshot(options = {}) { + if (process.platform === 'darwin') { + try { + const tempPath = path.join(os.tmpdir(), `screenshot-${Date.now()}.jpg`); + + 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 }; + } + } + + try { + const sources = await desktopCapturer.getSources({ + types: ['screen'], + thumbnailSize: { + width: 1920, + height: 1080, + }, + }); + + 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(); + + 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 @@ -32,15 +133,17 @@ class AskService { } async toggleAskButton() { - const { windowPool, updateLayout } = require('../../window/windowManager'); const askWindow = windowPool.get('ask'); + // 답변이 있거나 스트리밍 중일 때 const hasContent = this.state.isStreaming || (this.state.currentResponse && this.state.currentResponse.length > 0); if (askWindow.isVisible() && hasContent) { + // 창을 닫는 대신, 텍스트 입력창만 토글합니다. this.state.showTextInput = !this.state.showTextInput; - this._broadcastState(); + this._broadcastState(); // 변경된 상태 전파 } else { + // 기존의 창 보이기/숨기기 로직 if (askWindow.isVisible()) { askWindow.webContents.send('window-hide-animation'); this.state.isVisible = false; @@ -51,6 +154,7 @@ class AskService { updateLayout(); askWindow.webContents.send('window-show-animation'); } + // 창이 다시 열릴 때를 대비해 상태를 초기화하고 전파합니다. if (this.state.isVisible) { this.state.showTextInput = true; this._broadcastState(); @@ -77,7 +181,7 @@ class AskService { * @param {string} userPrompt * @returns {Promise<{success: boolean, response?: string, error?: string}>} */ - async sendMessage(userPrompt) { + async sendMessage(userPrompt, conversationHistoryRaw=[]) { if (this.abortController) { this.abortController.abort('New request received.'); } @@ -117,20 +221,7 @@ class AskService { const screenshotResult = await captureScreenshot({ quality: 'medium' }); const screenshotBase64 = screenshotResult.success ? screenshotResult.base64 : null; - let conversationHistoryRaw = []; - try { - const history = listenService.getConversationHistory(); - if (history && Array.isArray(history) && history.length > 0) { - console.log(`[AskService] Using conversation history from ListenService ${history.length} items).`); - conversationHistoryRaw = history; - } else { - console.log('[AskService] No active conversation history found in ListenService.'); - } - } catch (error) { - console.error('[AskService] Failed to get conversation history from ListenService:', error); - } const conversationHistory = this._formatConversationForPrompt(conversationHistoryRaw); - console.log(`[AskService] Using conversation history (${conversationHistory}`); const systemPrompt = getSystemPrompt('pickle_glass_analysis', conversationHistory, false); @@ -241,6 +332,9 @@ class AskService { 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; @@ -256,6 +350,12 @@ class AskService { } } } + + handleStopScreenCapture() { + lastScreenshot = null; + console.log('[AskService] Stopped screen capture and cleared cache.'); + return { success: true }; + } } const askService = new AskService(); diff --git a/src/window/windowManager.js b/src/window/windowManager.js index 4551a87..ae18e76 100644 --- a/src/window/windowManager.js +++ b/src/window/windowManager.js @@ -2,23 +2,9 @@ const { BrowserWindow, globalShortcut, ipcMain, screen, app, shell, desktopCaptu const WindowLayoutManager = require('./windowLayoutManager'); const SmoothMovementManager = require('./smoothMovementManager'); 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 shortcutsService = require('../features/shortcuts/shortcutsService'); const internalBridge = require('../bridge/internalBridge'); - -// Try to load sharp, but don't fail if it's not available -let sharp; -try { - sharp = require('sharp'); - console.log('[WindowManager] Sharp module loaded successfully'); -} catch (error) { - console.warn('[WindowManager] Sharp module not available:', error.message); - console.warn('[WindowManager] Screenshot functionality will work with reduced image processing capabilities'); - sharp = null; -} const permissionRepository = require('../features/common/repositories/permission'); /* ────────────────[ GLASS BYPASS ]─────────────── */ @@ -53,7 +39,6 @@ const DEFAULT_WINDOW_WIDTH = 353; let currentHeaderState = 'apikey'; const windowPool = new Map(); let fixedYPosition = 0; -let lastScreenshot = null; let settingsHideTimer = null; @@ -719,59 +704,6 @@ function setupIpcHandlers(movementManager) { } }); - ipcMain.handle('start-screen-capture', async () => { - try { - isCapturing = true; - console.log('Starting screen capture in main process'); - return { success: true }; - } catch (error) { - console.error('Failed to start screen capture:', error); - return { success: false, error: error.message }; - } - }); - - ipcMain.handle('stop-screen-capture', async () => { - try { - isCapturing = false; - lastScreenshot = null; - console.log('Stopped screen capture in main process'); - return { success: true }; - } catch (error) { - console.error('Failed to stop screen capture:', error); - return { success: false, error: error.message }; - } - }); - - ipcMain.handle('capture-screenshot', async (event, options = {}) => { - return captureScreenshot(options); - }); - - ipcMain.handle('get-current-screenshot', async event => { - try { - if (lastScreenshot && Date.now() - lastScreenshot.timestamp < 1000) { - console.log('Returning cached screenshot'); - return { - success: true, - base64: lastScreenshot.base64, - width: lastScreenshot.width, - height: lastScreenshot.height, - }; - } - return { - success: false, - error: 'No screenshot available', - }; - } catch (error) { - console.error('Failed to get current screenshot:', error); - return { - success: false, - error: error.message, - }; - } - }); - - // firebase-logout handler moved to windowBridge.js to avoid duplication - ipcMain.handle('check-system-permissions', async () => { const { systemPreferences } = require('electron'); const permissions = { @@ -1041,91 +973,6 @@ function setupApiKeyIPC() { //////// after_modelStateService //////// -async function captureScreenshot(options = {}) { - if (process.platform === 'darwin') { - try { - const tempPath = path.join(os.tmpdir(), `screenshot-${Date.now()}.jpg`); - - 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: 1080 }) - .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('[WindowManager] 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 }; - } - } - - try { - const sources = await desktopCapturer.getSources({ - types: ['screen'], - thumbnailSize: { - width: 1920, - height: 1080, - }, - }); - - 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(); - - 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, - }; - } -} - const closeWindow = (windowName) => { const win = windowPool.get(windowName); if (win && !win.isDestroyed()) { @@ -1141,7 +988,6 @@ module.exports = { getStoredApiKey, getStoredProvider, getCurrentModelInfo, - captureScreenshot, toggleFeature, // Export toggleFeature so shortcutsService can use it toggleContentProtection, resizeHeaderWindow,