transfer roles from askview to askservice

This commit is contained in:
sanio 2025-07-13 10:01:13 +09:00
parent c0edcfb0f9
commit 093f233f5a
4 changed files with 161 additions and 86 deletions

2
aec

@ -1 +1 @@
Subproject commit f00bb1fb948053c752b916adfee19f90644a0b2f Subproject commit 9e11f4f95707714464194bdfc9db0222ec5c6163

View File

@ -12,25 +12,52 @@ const { getSystemPrompt } = require('../common/prompts/promptBuilder');
class AskService { class AskService {
constructor() { constructor() {
this.abortController = null; this.abortController = null;
this.state = {
isVisible: false,
isLoading: false,
isStreaming: false,
currentQuestion: '',
currentResponse: '',
showTextInput: true,
};
console.log('[AskService] Service instance created.'); 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() { async toggleAskButton() {
const { windowPool, updateLayout } = require('../../window/windowManager'); const { windowPool, updateLayout } = require('../../window/windowManager');
const askWindow = windowPool.get('ask'); 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()) { if (askWindow.isVisible()) {
askWindow.webContents.send('window-hide-animation'); askWindow.webContents.send('window-hide-animation');
this.state.isVisible = false;
} else { } else {
console.log('[AskService] Showing hidden Ask window'); console.log('[AskService] Showing hidden Ask window');
this.state.isVisible = true;
askWindow.show(); askWindow.show();
updateLayout(); updateLayout();
askWindow.webContents.send('window-show-animation'); askWindow.webContents.send('window-show-animation');
} }
} catch (error) { // 창이 다시 열릴 때를 대비해 상태를 초기화하고 전파합니다.
console.error('[AskService] error in toggleAskButton:', error); if (this.state.isVisible) {
throw error; this.state.showTextInput = true;
this._broadcastState();
}
} }
} }
@ -69,6 +96,16 @@ class AskService {
try { try {
console.log(`[AskService] 🤖 Processing message: ${userPrompt.substring(0, 50)}...`); 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'); sessionId = await sessionRepository.getOrCreateActive('ask');
await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() }); await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() });
@ -139,10 +176,9 @@ class AskService {
} }
console.error('[AskService] Error processing message:', error); console.error('[AskService] Error processing message:', error);
const askWin = windowPool.get('ask'); this.state.isLoading = false;
if (askWin && !askWin.isDestroyed()) { this.state.error = error.message;
askWin.webContents.send('ask-response-stream-error', { error: error.message }); this._broadcastState();
}
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
} }
@ -161,6 +197,9 @@ class AskService {
let fullResponse = ''; let fullResponse = '';
try { try {
this.state.isLoading = false;
this.state.isStreaming = true;
this._broadcastState();
while (true) { while (true) {
const { done, value } = await reader.read(); const { done, value } = await reader.read();
if (done) break; if (done) break;
@ -172,9 +211,6 @@ class AskService {
if (line.startsWith('data: ')) { if (line.startsWith('data: ')) {
const data = line.substring(6); const data = line.substring(6);
if (data === '[DONE]') { if (data === '[DONE]') {
if (askWin && !askWin.isDestroyed()) {
askWin.webContents.send('ask-response-stream-end');
}
return; return;
} }
try { try {
@ -182,9 +218,8 @@ class AskService {
const token = json.choices[0]?.delta?.content || ''; const token = json.choices[0]?.delta?.content || '';
if (token) { if (token) {
fullResponse += token; fullResponse += token;
if (askWin && !askWin.isDestroyed()) { this.state.currentResponse = fullResponse;
askWin.webContents.send('ask-response-chunk', { token }); this._broadcastState();
}
} }
} catch (error) { } catch (error) {
} }
@ -201,6 +236,9 @@ class AskService {
} }
} }
} finally { } finally {
this.state.isStreaming = false;
this.state.currentResponse = fullResponse;
this._broadcastState();
if (fullResponse) { if (fullResponse) {
try { try {
await askRepository.addAiMessage({ sessionId, role: 'assistant', content: fullResponse }); await askRepository.addAiMessage({ sessionId, role: 'assistant', content: fullResponse });

View File

@ -132,7 +132,11 @@ contextBridge.exposeInMainWorld('api', {
// Message Handling // Message Handling
sendMessage: (text) => ipcRenderer.invoke('ask:sendQuestionFromAsk', text), sendMessage: (text) => ipcRenderer.invoke('ask:sendQuestionFromAsk', text),
// Listeners
onAskStateUpdate: (callback) => ipcRenderer.on('ask:stateUpdate', callback),
removeOnAskStateUpdate: (callback) => ipcRenderer.removeListener('ask:stateUpdate', callback),
// Listeners // Listeners
onSendQuestionToRenderer: (callback) => ipcRenderer.on('ask:sendQuestionToRenderer', callback), onSendQuestionToRenderer: (callback) => ipcRenderer.on('ask:sendQuestionToRenderer', callback),
removeOnSendQuestionToRenderer: (callback) => ipcRenderer.removeListener('ask:sendQuestionToRenderer', callback), removeOnSendQuestionToRenderer: (callback) => ipcRenderer.removeListener('ask:sendQuestionToRenderer', callback),

View File

@ -719,15 +719,15 @@ export class AskView extends LitElement {
this.headerText = 'AI Response'; this.headerText = 'AI Response';
this.headerAnimating = false; this.headerAnimating = false;
this.isStreaming = false; this.isStreaming = false;
this.accumulatedResponse = ''; // this.accumulatedResponse = '';
this.marked = null; this.marked = null;
this.hljs = null; this.hljs = null;
this.DOMPurify = null; this.DOMPurify = null;
this.isLibrariesLoaded = false; this.isLibrariesLoaded = false;
this.handleStreamChunk = this.handleStreamChunk.bind(this); // this.handleStreamChunk = this.handleStreamChunk.bind(this);
this.handleStreamEnd = this.handleStreamEnd.bind(this); // this.handleStreamEnd = this.handleStreamEnd.bind(this);
this.handleSendText = this.handleSendText.bind(this); this.handleSendText = this.handleSendText.bind(this);
this.handleTextKeydown = this.handleTextKeydown.bind(this); this.handleTextKeydown = this.handleTextKeydown.bind(this);
this.handleCopy = this.handleCopy.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.onResponseChunk(this.handleStreamChunk);
window.api.askView.onResponseStreamEnd(this.handleStreamEnd); // window.api.askView.onResponseStreamEnd(this.handleStreamEnd);
window.api.askView.onScrollResponseUp(() => this.handleScroll('up')); window.api.askView.onScrollResponseUp(() => this.handleScroll('up'));
window.api.askView.onScrollResponseDown(() => this.handleScroll('down')); 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 이벤트 리스너 등록 완료'); console.log('✅ AskView: IPC 이벤트 리스너 등록 완료');
} }
} }
@ -816,9 +823,14 @@ export class AskView extends LitElement {
Object.values(this.lineCopyTimeouts).forEach(timeout => clearTimeout(timeout)); Object.values(this.lineCopyTimeouts).forEach(timeout => clearTimeout(timeout));
if (window.api) { if (window.api) {
// Note: We need to keep references to the actual callbacks used in connectedCallback window.api.askView.removeOnAskStateUpdate(this.handleAskStateUpdate);
// For now, we'll just log that removal is needed window.api.askView.removeOnSendQuestionToRenderer(this.handleQuestionFromAssistant);
// TODO: Store callback references for proper removal 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 이벤트 리스너 제거 필요'); console.log('✅ AskView: IPC 이벤트 리스너 제거 필요');
} }
} }
@ -905,9 +917,9 @@ export class AskView extends LitElement {
this.isStreaming = false; this.isStreaming = false;
this.headerText = 'AI Response'; this.headerText = 'AI Response';
this.showTextInput = true; this.showTextInput = true;
this.accumulatedResponse = ''; // this.accumulatedResponse = '';
this.requestUpdate(); // this.requestUpdate();
this.renderContent(); // this.renderContent();
} }
handleInputFocus() { handleInputFocus() {
@ -975,55 +987,52 @@ export class AskView extends LitElement {
} }
// --- 스트리밍 처리 핸들러 --- // --- 스트리밍 처리 핸들러 ---
handleStreamChunk(event, { token }) { // handleStreamChunk(event, { token }) {
if (!this.isStreaming) { // if (!this.isStreaming) {
this.isStreaming = true; // this.isStreaming = true;
this.isLoading = false; // this.isLoading = false;
this.accumulatedResponse = ''; // this.accumulatedResponse = '';
const container = this.shadowRoot.getElementById('responseContainer'); // const container = this.shadowRoot.getElementById('responseContainer');
if (container) container.innerHTML = ''; // if (container) container.innerHTML = '';
this.headerText = 'AI Response'; // this.headerText = 'AI Response';
this.headerAnimating = false; // this.headerAnimating = false;
this.requestUpdate(); // this.requestUpdate();
} // }
this.accumulatedResponse += token; // this.accumulatedResponse += token;
this.renderContent(); // this.renderContent();
} // }
handleStreamEnd() { // handleStreamEnd() {
this.isStreaming = false; // this.isStreaming = false;
this.currentResponse = this.accumulatedResponse; // this.currentResponse = this.accumulatedResponse;
if (this.headerText !== 'AI Response') { // if (this.headerText !== 'AI Response') {
this.headerText = 'AI Response'; // this.headerText = 'AI Response';
this.requestUpdate(); // this.requestUpdate();
} // }
this.renderContent(); // this.renderContent();
} // }
// ✨ 렌더링 로직 통합 // ✨ 렌더링 로직 통합
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'); const responseContainer = this.shadowRoot.getElementById('responseContainer');
if (!responseContainer) return; if (!responseContainer) return;
// ✨ 로딩 상태를 먼저 확인
if (this.isLoading) { if (this.isLoading) {
responseContainer.innerHTML = ` responseContainer.innerHTML = `<div class="loading-dots">...</div>`;
<div class="loading-dots">
<div class="loading-dot"></div><div class="loading-dot"></div><div class="loading-dot"></div>
</div>`;
return; return;
} }
let textToRender = this.isStreaming ? this.accumulatedResponse : this.currentResponse; // ✨ 응답이 없을 때의 처리
if (!this.currentResponse) {
// 불완전한 마크다운 수정 responseContainer.innerHTML = `<div class="empty-state">...</div>`;
textToRender = this.fixIncompleteMarkdown(textToRender); return;
}
// ✨ isStreaming이나 accumulatedResponse 대신 currentResponse를 직접 사용
let textToRender = this.fixIncompleteMarkdown(this.currentResponse);
textToRender = this.fixIncompleteCodeBlocks(textToRender); textToRender = this.fixIncompleteCodeBlocks(textToRender);
if (this.isLibrariesLoaded && this.marked && this.DOMPurify) { if (this.isLibrariesLoaded && this.marked && this.DOMPurify) {
try { try {
@ -1260,26 +1269,32 @@ export class AskView extends LitElement {
textInput.value = ''; textInput.value = '';
this.currentQuestion = text; // this.currentQuestion = text;
this.lineCopyState = {}; // this.lineCopyState = {};
this.showTextInput = false; // this.showTextInput = false;
this.isLoading = true; // this.isLoading = true;
this.isStreaming = false; // this.isStreaming = false;
this.currentResponse = ''; // this.currentResponse = '';
this.accumulatedResponse = ''; // this.accumulatedResponse = '';
this.startHeaderAnimation(); // this.startHeaderAnimation();
this.requestUpdate(); // this.requestUpdate();
this.renderContent(); // this.renderContent();
if (window.api) { if (window.api) {
window.api.askView.sendMessage(text).catch(error => { window.api.askView.sendMessage(text).catch(error => {
console.error('Error sending text:', 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) { 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) { updated(changedProperties) {
super.updated(changedProperties); super.updated(changedProperties);
if (changedProperties.has('isLoading')) {
// ✨ isLoading 또는 currentResponse가 변경될 때마다 뷰를 다시 그립니다.
if (changedProperties.has('isLoading') || changedProperties.has('currentResponse')) {
this.renderContent(); this.renderContent();
} }
if (changedProperties.has('showTextInput') || changedProperties.has('isLoading')) { if (changedProperties.has('showTextInput') || changedProperties.has('isLoading') || changedProperties.has('currentResponse')) {
this.adjustWindowHeightThrottled(); this.adjustWindowHeightThrottled();
} }
if (changedProperties.has('showTextInput') && this.showTextInput) { if (changedProperties.has('showTextInput') && this.showTextInput) {
this.focusTextInput(); this.focusTextInput();
} }
@ -1327,6 +1359,7 @@ export class AskView extends LitElement {
render() { render() {
const hasResponse = this.isLoading || this.currentResponse || this.isStreaming; const hasResponse = this.isLoading || this.currentResponse || this.isStreaming;
const headerText = this.isLoading ? 'Thinking...' : 'AI Response';
return html` return html`
<div class="ask-container"> <div class="ask-container">
@ -1339,7 +1372,7 @@ export class AskView extends LitElement {
<path d="M8 12l2 2 4-4" /> <path d="M8 12l2 2 4-4" />
</svg> </svg>
</div> </div>
<span class="response-label ${this.headerAnimating ? 'animating' : ''}">${this.headerText}</span> <span class="response-label">${headerText}</span>
</div> </div>
<div class="header-right"> <div class="header-right">
<span class="question-text">${this.getTruncatedQuestion(this.currentQuestion)}</span> <span class="question-text">${this.getTruncatedQuestion(this.currentQuestion)}</span>