From 093f233f5abe67b7c23cf08abb0fb246c4085218 Mon Sep 17 00:00:00 2001 From: sanio Date: Sun, 13 Jul 2025 10:01:13 +0900 Subject: [PATCH] transfer roles from askview to askservice --- aec | 2 +- src/features/ask/askService.js | 68 ++++++++++--- src/preload.js | 6 +- src/ui/ask/AskView.js | 171 ++++++++++++++++++++------------- 4 files changed, 161 insertions(+), 86 deletions(-) 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 = '
Ask a question to see the response here
'; - return; - } - const responseContainer = this.shadowRoot.getElementById('responseContainer'); if (!responseContainer) return; - + + // ✨ 로딩 상태를 먼저 확인 if (this.isLoading) { - responseContainer.innerHTML = ` -
-
-
`; + responseContainer.innerHTML = `
...
`; return; } - - let textToRender = this.isStreaming ? this.accumulatedResponse : this.currentResponse; - - // 불완전한 마크다운 수정 - textToRender = this.fixIncompleteMarkdown(textToRender); + + // ✨ 응답이 없을 때의 처리 + if (!this.currentResponse) { + responseContainer.innerHTML = `
...
`; + return; + } + + // ✨ isStreaming이나 accumulatedResponse 대신 currentResponse를 직접 사용 + let textToRender = this.fixIncompleteMarkdown(this.currentResponse); textToRender = this.fixIncompleteCodeBlocks(textToRender); + if (this.isLibrariesLoaded && this.marked && this.DOMPurify) { try { @@ -1260,26 +1269,32 @@ export class AskView extends LitElement { 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(); + // 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.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(); }); } + + // 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(); + // }); + // } } handleTextKeydown(e) { @@ -1297,16 +1312,33 @@ export class AskView extends LitElement { } } + // updated(changedProperties) { + // super.updated(changedProperties); + // if (changedProperties.has('isLoading')) { + // this.renderContent(); + // } + + // if (changedProperties.has('showTextInput') || changedProperties.has('isLoading')) { + // this.adjustWindowHeightThrottled(); + // } + + // if (changedProperties.has('showTextInput') && this.showTextInput) { + // this.focusTextInput(); + // } + // } + 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(); } @@ -1327,6 +1359,7 @@ export class AskView extends LitElement { render() { const hasResponse = this.isLoading || this.currentResponse || this.isStreaming; + const headerText = this.isLoading ? 'Thinking...' : 'AI Response'; return html`
@@ -1339,7 +1372,7 @@ export class AskView extends LitElement {
- ${this.headerText} + ${headerText}
${this.getTruncatedQuestion(this.currentQuestion)}