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 {
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 });

View File

@ -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),

View File

@ -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 = '<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;
}
// ✨ 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`
<div class="ask-container">
@ -1339,7 +1372,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>