refactor ask
This commit is contained in:
parent
0992cd4668
commit
bf13a865ba
@ -57,8 +57,10 @@ module.exports = {
|
||||
ipcMain.handle('ollama:shutdown', async (event, force = false) => await ollamaService.handleShutdown(event, force));
|
||||
|
||||
// Ask
|
||||
ipcMain.handle('ask:sendMessage', async (event, userPrompt, conversationHistoryRaw = []) => await askService.sendMessage(userPrompt, conversationHistoryRaw));
|
||||
|
||||
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());
|
||||
|
||||
// Listen
|
||||
ipcMain.handle('send-audio-content', async (event, { data, mimeType }) => await listenService.handleSendAudioContent(data, mimeType));
|
||||
ipcMain.handle('send-system-audio-content', async (event, { data, mimeType }) => {
|
||||
@ -71,22 +73,6 @@ module.exports = {
|
||||
ipcMain.handle('start-macos-audio', async () => await listenService.handleStartMacosAudio());
|
||||
ipcMain.handle('stop-macos-audio', async () => await listenService.handleStopMacosAudio());
|
||||
ipcMain.handle('update-google-search-setting', async (event, enabled) => await listenService.handleUpdateGoogleSearchSetting(enabled));
|
||||
|
||||
// ModelStateService
|
||||
ipcMain.handle('model:validate-key', async (e, { provider, key }) => await modelStateService.handleValidateKey(provider, key));
|
||||
ipcMain.handle('model:get-all-keys', () => modelStateService.getAllApiKeys());
|
||||
ipcMain.handle('model:set-api-key', async (e, { provider, key }) => await modelStateService.setApiKey(provider, key));
|
||||
ipcMain.handle('model:remove-api-key', async (e, { provider }) => await modelStateService.handleRemoveApiKey(provider));
|
||||
ipcMain.handle('model:get-selected-models', () => modelStateService.getSelectedModels());
|
||||
ipcMain.handle('model:set-selected-model', async (e, { type, modelId }) => await modelStateService.handleSetSelectedModel(type, modelId));
|
||||
ipcMain.handle('model:get-available-models', (e, { type }) => modelStateService.getAvailableModels(type));
|
||||
ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured());
|
||||
ipcMain.handle('model:get-current-model-info', (e, { type }) => modelStateService.getCurrentModelInfo(type));
|
||||
ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
|
||||
|
||||
console.log('[FeatureBridge] Initialized with all feature handlers.');
|
||||
|
||||
|
||||
ipcMain.handle('listen:changeSession', async (event, listenButtonText) => {
|
||||
console.log('[FeatureBridge] listen:changeSession from mainheader', listenButtonText);
|
||||
try {
|
||||
@ -100,6 +86,21 @@ module.exports = {
|
||||
|
||||
|
||||
|
||||
// ModelStateService
|
||||
ipcMain.handle('model:validate-key', async (e, { provider, key }) => await modelStateService.handleValidateKey(provider, key));
|
||||
ipcMain.handle('model:get-all-keys', () => modelStateService.getAllApiKeys());
|
||||
ipcMain.handle('model:set-api-key', async (e, { provider, key }) => await modelStateService.setApiKey(provider, key));
|
||||
ipcMain.handle('model:remove-api-key', async (e, { provider }) => await modelStateService.handleRemoveApiKey(provider));
|
||||
ipcMain.handle('model:get-selected-models', () => modelStateService.getSelectedModels());
|
||||
ipcMain.handle('model:set-selected-model', async (e, { type, modelId }) => await modelStateService.handleSetSelectedModel(type, modelId));
|
||||
ipcMain.handle('model:get-available-models', (e, { type }) => modelStateService.getAvailableModels(type));
|
||||
ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured());
|
||||
ipcMain.handle('model:get-current-model-info', (e, { type }) => modelStateService.getCurrentModelInfo(type));
|
||||
ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
|
||||
|
||||
|
||||
|
||||
console.log('[FeatureBridge] Initialized with all feature handlers.');
|
||||
},
|
||||
|
||||
// Renderer로 상태를 전송
|
||||
|
@ -6,37 +6,60 @@ const askRepository = require('./repositories');
|
||||
const { getSystemPrompt } = require('../common/prompts/promptBuilder');
|
||||
|
||||
/**
|
||||
* @class AskService
|
||||
* @description 사용자의 질문을 처리하고 AI 모델과 통신하여 응답을 스트리밍하는 모든 로직을 캡슐화합니다.
|
||||
* @class
|
||||
* @description
|
||||
*/
|
||||
class AskService {
|
||||
/**
|
||||
* AskService의 인스턴스를 생성합니다.
|
||||
*/
|
||||
constructor() {
|
||||
this.abortController = null;
|
||||
console.log('[AskService] Service instance created.');
|
||||
}
|
||||
|
||||
async toggleAskButton() {
|
||||
const { windowPool, updateLayout } = require('../../window/windowManager');
|
||||
const askWindow = windowPool.get('ask');
|
||||
const header = windowPool.get('header');
|
||||
try {
|
||||
if (askWindow.isVisible()) {
|
||||
askWindow.webContents.send('window-hide-animation');
|
||||
} else {
|
||||
console.log('[AskService] Showing hidden Ask window');
|
||||
askWindow.show();
|
||||
updateLayout();
|
||||
askWindow.webContents.send('window-show-animation');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[AskService] error in toggleAskButton:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 대화 기록 배열을 프롬프트에 적합한 단일 문자열로 변환합니다.
|
||||
* @param {string[]} conversationTexts - 대화 내용 문자열의 배열
|
||||
* @returns {string} 프롬프트에 사용될 형식의 대화 기록
|
||||
*
|
||||
* @param {string[]} conversationTexts
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
_formatConversationForPrompt(conversationTexts) {
|
||||
if (!conversationTexts || conversationTexts.length === 0) {
|
||||
return 'No conversation history available.';
|
||||
}
|
||||
// 최근 30개의 대화만 사용
|
||||
return conversationTexts.slice(-30).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 프롬프트를 받아 AI 모델에 전송하고, 응답을 스트리밍으로 처리합니다.
|
||||
* @param {string} userPrompt - 사용자가 입력한 질문 또는 메시지
|
||||
* @returns {Promise<{success: boolean, response?: string, error?: string}>} 처리 결과 객체
|
||||
*
|
||||
* @param {string} userPrompt
|
||||
* @returns {Promise<{success: boolean, response?: string, error?: string}>}
|
||||
*/
|
||||
async sendMessage(userPrompt, conversationHistoryRaw=[]) {
|
||||
if (this.abortController) {
|
||||
this.abortController.abort('New request received.');
|
||||
}
|
||||
this.abortController = new AbortController();
|
||||
const { signal } = this.abortController;
|
||||
|
||||
if (!userPrompt || userPrompt.trim().length === 0) {
|
||||
console.warn('[AskService] Cannot process empty message');
|
||||
return { success: false, error: 'Empty message' };
|
||||
@ -47,7 +70,6 @@ class AskService {
|
||||
try {
|
||||
console.log(`[AskService] 🤖 Processing message: ${userPrompt.substring(0, 50)}...`);
|
||||
|
||||
// --- 사용자 메시지 저장 ---
|
||||
sessionId = await sessionRepository.getOrCreateActive('ask');
|
||||
await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() });
|
||||
console.log(`[AskService] DB: Saved user prompt to session ${sessionId}`);
|
||||
@ -61,7 +83,6 @@ class AskService {
|
||||
const screenshotResult = await captureScreenshot({ quality: 'medium' });
|
||||
const screenshotBase64 = screenshotResult.success ? screenshotResult.base64 : null;
|
||||
|
||||
// const conversationHistoryRaw = this._getConversationHistory();
|
||||
const conversationHistory = this._formatConversationForPrompt(conversationHistoryRaw);
|
||||
|
||||
const systemPrompt = getSystemPrompt('pickle_glass_analysis', conversationHistory, false);
|
||||
@ -101,14 +122,22 @@ class AskService {
|
||||
return { success: false, error: 'Ask window is not available.' };
|
||||
}
|
||||
|
||||
// --- 스트림 처리 ---
|
||||
await this._processStream(response.body, askWin, sessionId);
|
||||
const reader = response.body.getReader();
|
||||
signal.addEventListener('abort', () => {
|
||||
console.log(`[AskService] Aborting stream reader. Reason: ${signal.reason}`);
|
||||
reader.cancel(signal.reason).catch(() => { /* 이미 취소된 경우의 오류는 무시 */ });
|
||||
});
|
||||
|
||||
// _processStream 내부에서 전체 응답이 완료되면 반환됩니다.
|
||||
// 하지만 비동기 스트림의 특성상 이 지점에서는 직접 반환 값을 알기 어렵습니다.
|
||||
// 성공/실패 여부는 스트림 처리 로직 내에서 결정됩니다.
|
||||
await this._processStream(reader, askWin, sessionId, signal);
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('[AskService] SendMessage operation was successfully aborted.');
|
||||
return { success: true, response: 'Cancelled' };
|
||||
}
|
||||
|
||||
console.error('[AskService] Error processing message:', error);
|
||||
const askWin = windowPool.get('ask');
|
||||
if (askWin && !askWin.isDestroyed()) {
|
||||
@ -119,18 +148,17 @@ class AskService {
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 모델로부터 받은 응답 스트림을 처리합니다.
|
||||
* @param {ReadableStream} body - 스트리밍 응답의 body
|
||||
* @param {BrowserWindow} askWin - 응답을 보낼 대상 창
|
||||
* @param {number} sessionId - 현재 세션 ID
|
||||
*
|
||||
* @param {ReadableStreamDefaultReader} reader
|
||||
* @param {BrowserWindow} askWin
|
||||
* @param {number} sessionId
|
||||
* @param {AbortSignal} signal
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
async _processStream(body, askWin, sessionId) {
|
||||
const reader = body.getReader();
|
||||
async _processStream(reader, askWin, sessionId, signal) {
|
||||
const decoder = new TextDecoder();
|
||||
let fullResponse = '';
|
||||
let finalResult = { success: false }; // 최종 결과 저장을 위한 변수
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
@ -144,13 +172,9 @@ class AskService {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.substring(6);
|
||||
if (data === '[DONE]') {
|
||||
askWin.webContents.send('ask-response-stream-end');
|
||||
|
||||
await askRepository.addAiMessage({ sessionId, role: 'assistant', content: fullResponse });
|
||||
console.log(`[AskService] DB: Saved assistant response to session ${sessionId}`);
|
||||
|
||||
// 스트림이 성공적으로 완료되었으므로, 최종 결과를 성공으로 설정합니다.
|
||||
// 실제 반환은 sendMessage에서 이루어지지만, 로직상의 완료를 의미합니다.
|
||||
if (askWin && !askWin.isDestroyed()) {
|
||||
askWin.webContents.send('ask-response-stream-end');
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@ -158,35 +182,37 @@ class AskService {
|
||||
const token = json.choices[0]?.delta?.content || '';
|
||||
if (token) {
|
||||
fullResponse += token;
|
||||
askWin.webContents.send('ask-response-chunk', { token });
|
||||
if (askWin && !askWin.isDestroyed()) {
|
||||
askWin.webContents.send('ask-response-chunk', { token });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// JSON 파싱 오류는 무시하고 계속 진행
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (streamError) {
|
||||
console.error('[AskService] Error while processing stream:', streamError);
|
||||
askWin.webContents.send('ask-response-stream-error', { error: streamError.message });
|
||||
// 스트림 처리 중 에러가 발생했음을 기록
|
||||
if (signal.aborted) {
|
||||
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 {
|
||||
// 스트림이 정상적으로 [DONE]을 받지 못하고 종료된 경우에도
|
||||
// 현재까지의 응답이라도 저장 시도
|
||||
if (fullResponse) {
|
||||
try {
|
||||
await askRepository.addAiMessage({ sessionId, role: 'assistant', content: fullResponse });
|
||||
console.log(`[AskService] DB: Saved partial assistant response to session ${sessionId} after stream interruption.`);
|
||||
console.log(`[AskService] DB: Saved partial or full assistant response to session ${sessionId} after stream ended.`);
|
||||
} catch(dbError) {
|
||||
console.error("[AskService] DB: Failed to save assistant response after stream interruption:", dbError);
|
||||
console.error("[AskService] DB: Failed to save assistant response after stream ended:", dbError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AskService 클래스의 단일 인스턴스를 생성하여 내보냅니다.
|
||||
// 이렇게 하면 애플리케이션 전체에서 동일한 서비스 인스턴스를 공유하게 됩니다.
|
||||
const askService = new AskService();
|
||||
|
||||
module.exports = askService;
|
@ -131,7 +131,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
adjustWindowHeight: (height) => ipcRenderer.invoke('adjust-window-height', height),
|
||||
|
||||
// Message Handling
|
||||
sendMessage: (text) => ipcRenderer.invoke('ask:sendMessage', text),
|
||||
sendMessage: (text) => ipcRenderer.invoke('ask:sendQuestionFromAsk', text),
|
||||
|
||||
// Listeners
|
||||
onSendQuestionToRenderer: (callback) => ipcRenderer.on('ask:sendQuestionToRenderer', callback),
|
||||
@ -170,7 +170,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
// src/ui/listen/summary/SummaryView.js
|
||||
summaryView: {
|
||||
// Message Handling
|
||||
sendQuestionToMain: (text) => ipcRenderer.invoke('ask:sendQuestionToMain', text),
|
||||
sendQuestionFromSummary: (text) => ipcRenderer.invoke('ask:sendQuestionFromSummary', text),
|
||||
|
||||
// Listeners
|
||||
onSummaryUpdate: (callback) => ipcRenderer.on('summary-update', callback),
|
||||
|
@ -568,6 +568,17 @@ export class MainHeader extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
async _handleAskClick() {
|
||||
if (this.wasJustDragged) return;
|
||||
|
||||
try {
|
||||
const channel = 'ask:toggleAskButton';
|
||||
await this.invoke(channel);
|
||||
} catch (error) {
|
||||
console.error('IPC invoke for ask button failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
renderShortcut(accelerator) {
|
||||
if (!accelerator) return html``;
|
||||
@ -636,7 +647,7 @@ export class MainHeader extends LitElement {
|
||||
`}
|
||||
</button>
|
||||
|
||||
<div class="header-actions ask-action" @click=${() => this.invoke('toggle-feature', 'ask')}>
|
||||
<div class="header-actions ask-action" @click=${() => this._handleAskClick()}>
|
||||
<div class="action-text">
|
||||
<div class="action-text-content">Ask</div>
|
||||
</div>
|
||||
|
@ -408,7 +408,7 @@ export class SummaryView extends LitElement {
|
||||
|
||||
if (window.api) {
|
||||
try {
|
||||
const result = await window.api.summaryView.sendQuestionToMain(requestText);
|
||||
const result = await window.api.summaryView.sendQuestionFromSummary(requestText);
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ Question sent to AskView successfully');
|
||||
|
@ -887,52 +887,13 @@ function setupIpcHandlers(movementManager) {
|
||||
});
|
||||
|
||||
|
||||
ipcMain.handle('ask:sendQuestionToMain', (event, question) => {
|
||||
console.log('📨 Main process: Sending question to AskView', question);
|
||||
toggleFeature('ask', {ask: { questionText: question }});
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// ipcMain.handle('listen:changeSession', async (event, actionText) => {
|
||||
// console.log('📨 Main process: Received actionText', actionText);
|
||||
// const header = windowPool.get('header');
|
||||
// const listenWindow = windowPool.get('listen');
|
||||
|
||||
// try {
|
||||
// if (listenService && listenService.isSessionActive()) {
|
||||
// console.log('[WindowManager] Listen session is active, closing it.');
|
||||
// // ✨ closeSession도 비동기일 수 있으므로 await 처리 (만약 동기 함수라면 await는 무시됨)
|
||||
// await listenService.closeSession();
|
||||
// listenWindow.webContents.send('session-state-changed', { isActive: false });
|
||||
// } else {
|
||||
// if (listenWindow.isVisible()) {
|
||||
// listenWindow.webContents.send('window-hide-animation');
|
||||
// listenWindow.webContents.send('session-state-changed', { isActive: false });
|
||||
// } else {
|
||||
// listenWindow.show();
|
||||
// updateLayout();
|
||||
// listenWindow.webContents.send('window-show-animation');
|
||||
|
||||
// // ✨ 핵심: initializeSession 작업이 끝날 때까지 기다림
|
||||
// await listenService.initializeSession();
|
||||
|
||||
// listenWindow.webContents.send('session-state-changed', { isActive: true });
|
||||
// }
|
||||
// }
|
||||
|
||||
// // ✨ 모든 비동기 작업이 성공적으로 끝난 후 결과 전송
|
||||
// header.webContents.send('listen:changeSessionResult', { success: true });
|
||||
// return { success: true };
|
||||
|
||||
// } catch (error) {
|
||||
// console.error('[WindowManager] Failed to change listen session:', error);
|
||||
|
||||
// // ✨ 작업 실패 시 UI에 실패 결과를 알려 로딩 상태를 해제하도록 함
|
||||
// header.webContents.send('listen:changeSessionResult', { success: false });
|
||||
// return { success: false, error: error.message };
|
||||
// }
|
||||
// ipcMain.handle('ask:sendQuestionToMain', (event, question) => {
|
||||
// console.log('📨 Main process: Sending question to AskView', question);
|
||||
// toggleFeature('ask', {ask: { questionText: question }});
|
||||
// return { success: true };
|
||||
// });
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -950,33 +911,6 @@ async function toggleFeature(featureName, options = {}) {
|
||||
createFeatureWindows(windowPool.get('header'));
|
||||
}
|
||||
|
||||
const header = windowPool.get('header');
|
||||
// if (featureName === 'listen') {
|
||||
// console.log(`[WindowManager] Toggling feature: ${featureName}`);
|
||||
// const listenWindow = windowPool.get(featureName);
|
||||
// // const listenService = global.listenService;
|
||||
// if (listenService && listenService.isSessionActive()) {
|
||||
// console.log('[WindowManager] Listen session is active, closing it via toggle.');
|
||||
// await listenService.closeSession();
|
||||
// listenWindow.webContents.send('session-state-changed', { isActive: false });
|
||||
// header.webContents.send('session-state-text', 'Done');
|
||||
// // return;
|
||||
// } else {
|
||||
// if (listenWindow.isVisible()) {
|
||||
// listenWindow.webContents.send('window-hide-animation');
|
||||
// listenWindow.webContents.send('session-state-changed', { isActive: false });
|
||||
// header.webContents.send('session-state-text', 'Listen');
|
||||
// } else {
|
||||
// listenWindow.show();
|
||||
// updateLayout();
|
||||
// listenWindow.webContents.send('window-show-animation');
|
||||
// await listenService.initializeSession();
|
||||
// listenWindow.webContents.send('session-state-changed', { isActive: true });
|
||||
// header.webContents.send('session-state-text', 'Stop');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
if (featureName === 'ask') {
|
||||
let askWindow = windowPool.get('ask');
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user