diff --git a/aec b/aec index f00bb1f..9e11f4f 160000 --- a/aec +++ b/aec @@ -1 +1 @@ -Subproject commit f00bb1fb948053c752b916adfee19f90644a0b2f +Subproject commit 9e11f4f95707714464194bdfc9db0222ec5c6163 diff --git a/src/features/ask/askService.js b/src/features/ask/askService.js index c0d086f..81c1439 100644 --- a/src/features/ask/askService.js +++ b/src/features/ask/askService.js @@ -12,25 +12,52 @@ const { getSystemPrompt } = require('../common/prompts/promptBuilder'); 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 = windowPool.get('ask'); + if (askWindow && !askWindow.isDestroyed()) { + askWindow.webContents.send('ask:stateUpdate', this.state); + } + } + async toggleAskButton() { const { windowPool, updateLayout } = require('../../window/windowManager'); const askWindow = windowPool.get('ask'); - const header = windowPool.get('header'); - try { + + // 답변이 있거나 스트리밍 중일 때 + 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(); // 변경된 상태 전파 + } else { + // 기존의 창 보이기/숨기기 로직 if (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'); } - } catch (error) { - console.error('[AskService] error in toggleAskButton:', error); - throw error; + // 창이 다시 열릴 때를 대비해 상태를 초기화하고 전파합니다. + if (this.state.isVisible) { + this.state.showTextInput = true; + this._broadcastState(); + } } } @@ -69,6 +96,16 @@ class AskService { 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() }); @@ -139,10 +176,9 @@ class AskService { } console.error('[AskService] Error processing message:', error); - const askWin = windowPool.get('ask'); - if (askWin && !askWin.isDestroyed()) { - askWin.webContents.send('ask-response-stream-error', { error: error.message }); - } + this.state.isLoading = false; + this.state.error = error.message; + this._broadcastState(); return { success: false, error: error.message }; } } @@ -161,6 +197,9 @@ class AskService { let fullResponse = ''; try { + this.state.isLoading = false; + this.state.isStreaming = true; + this._broadcastState(); while (true) { const { done, value } = await reader.read(); if (done) break; @@ -172,9 +211,6 @@ class AskService { if (line.startsWith('data: ')) { const data = line.substring(6); if (data === '[DONE]') { - if (askWin && !askWin.isDestroyed()) { - askWin.webContents.send('ask-response-stream-end'); - } return; } try { @@ -182,9 +218,8 @@ class AskService { const token = json.choices[0]?.delta?.content || ''; if (token) { fullResponse += token; - if (askWin && !askWin.isDestroyed()) { - askWin.webContents.send('ask-response-chunk', { token }); - } + this.state.currentResponse = fullResponse; + this._broadcastState(); } } catch (error) { } @@ -201,6 +236,9 @@ class AskService { } } } finally { + this.state.isStreaming = false; + this.state.currentResponse = fullResponse; + this._broadcastState(); if (fullResponse) { try { await askRepository.addAiMessage({ sessionId, role: 'assistant', content: fullResponse }); diff --git a/src/preload.js b/src/preload.js index 9ea0ac2..b2b1d62 100644 --- a/src/preload.js +++ b/src/preload.js @@ -132,7 +132,11 @@ contextBridge.exposeInMainWorld('api', { // 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 onSendQuestionToRenderer: (callback) => ipcRenderer.on('ask:sendQuestionToRenderer', callback), removeOnSendQuestionToRenderer: (callback) => ipcRenderer.removeListener('ask:sendQuestionToRenderer', callback), diff --git a/src/ui/ask/AskView.js b/src/ui/ask/AskView.js index ac9b6e9..a3ae7e9 100644 --- a/src/ui/ask/AskView.js +++ b/src/ui/ask/AskView.js @@ -719,15 +719,15 @@ export class AskView extends LitElement { this.headerText = 'AI Response'; this.headerAnimating = false; this.isStreaming = false; - this.accumulatedResponse = ''; + // 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.handleStreamChunk = this.handleStreamChunk.bind(this); + // this.handleStreamEnd = this.handleStreamEnd.bind(this); this.handleSendText = this.handleSendText.bind(this); this.handleTextKeydown = this.handleTextKeydown.bind(this); this.handleCopy = this.handleCopy.bind(this); @@ -784,11 +784,18 @@ export class AskView extends LitElement { } }); - window.api.askView.onResponseChunk(this.handleStreamChunk); - window.api.askView.onResponseStreamEnd(this.handleStreamEnd); + // window.api.askView.onResponseChunk(this.handleStreamChunk); + // window.api.askView.onResponseStreamEnd(this.handleStreamEnd); 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 이벤트 리스너 등록 완료'); } } @@ -816,9 +823,14 @@ export class AskView extends LitElement { Object.values(this.lineCopyTimeouts).forEach(timeout => clearTimeout(timeout)); if (window.api) { - // Note: We need to keep references to the actual callbacks used in connectedCallback - // For now, we'll just log that removal is needed - // TODO: Store callback references for proper removal + window.api.askView.removeOnAskStateUpdate(this.handleAskStateUpdate); + window.api.askView.removeOnSendQuestionToRenderer(this.handleQuestionFromAssistant); + window.api.askView.removeOnHideTextInput(this.handleHideTextInput); + window.api.askView.removeOnShowTextInput(this.handleShowTextInput); + window.api.askView.removeOnResponseChunk(this.handleStreamChunk); + window.api.askView.removeOnResponseStreamEnd(this.handleStreamEnd); + window.api.askView.removeOnScrollResponseUp(this.handleScroll); + window.api.askView.removeOnScrollResponseDown(this.handleScroll); console.log('✅ AskView: IPC 이벤트 리스너 제거 필요'); } } @@ -905,9 +917,9 @@ export class AskView extends LitElement { this.isStreaming = false; this.headerText = 'AI Response'; this.showTextInput = true; - this.accumulatedResponse = ''; - this.requestUpdate(); - this.renderContent(); + // this.accumulatedResponse = ''; + // this.requestUpdate(); + // this.renderContent(); } handleInputFocus() { @@ -975,55 +987,52 @@ 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(); - } + // 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(); - } + // 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 = '