centralize mainheader listen session logic

This commit is contained in:
sanio 2025-07-11 05:46:11 +09:00
parent e86c2db464
commit bcefa75154
4 changed files with 155 additions and 67 deletions

View File

@ -2,7 +2,9 @@ import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';
export class MainHeader extends LitElement {
static properties = {
isSessionActive: { type: Boolean, state: true },
// isSessionActive: { type: Boolean, state: true },
isTogglingSession: { type: Boolean, state: true },
actionText: { type: String, state: true },
shortcuts: { type: Object, state: true },
};
@ -95,6 +97,11 @@ export class MainHeader extends LitElement {
position: relative;
}
.listen-button:disabled {
cursor: default;
opacity: 0.8;
}
.listen-button.active::before {
background: rgba(215, 0, 0, 0.5);
}
@ -103,6 +110,24 @@ export class MainHeader extends LitElement {
background: rgba(255, 20, 20, 0.6);
}
.listen-button.done {
background-color: rgba(255, 255, 255, 0.6);
transition: background-color 0.15s ease;
}
.listen-button.done .action-text-content {
color: black;
}
.listen-button.done .listen-icon svg rect,
.listen-button.done .listen-icon svg path {
fill: black;
}
.listen-button.done:hover {
background-color: #f0f0f0;
}
.listen-button:hover::before {
background: rgba(255, 255, 255, 0.18);
}
@ -132,6 +157,38 @@ export class MainHeader extends LitElement {
pointer-events: none;
}
.listen-button.done::after {
display: none;
}
.loading-dots {
display: flex;
align-items: center;
gap: 5px;
}
.loading-dots span {
width: 6px;
height: 6px;
background-color: white;
border-radius: 50%;
animation: pulse 1.4s infinite ease-in-out both;
}
.loading-dots span:nth-of-type(1) {
animation-delay: -0.32s;
}
.loading-dots span:nth-of-type(2) {
animation-delay: -0.16s;
}
@keyframes pulse {
0%, 80%, 100% {
opacity: 0.2;
}
40% {
opacity: 1.0;
}
}
.header-actions {
-webkit-app-region: no-drag;
height: 26px;
@ -242,7 +299,9 @@ export class MainHeader extends LitElement {
this.isAnimating = false;
this.hasSlidIn = false;
this.settingsHideTimer = null;
this.isSessionActive = false;
// this.isSessionActive = false;
this.isTogglingSession = false;
this.actionText = 'Listen';
this.animationEndTimer = null;
this.handleAnimationEnd = this.handleAnimationEnd.bind(this);
}
@ -305,10 +364,19 @@ export class MainHeader extends LitElement {
if (window.require) {
const { ipcRenderer } = window.require('electron');
this._sessionStateListener = (event, { isActive }) => {
this.isSessionActive = isActive;
this._sessionStateTextListener = (event, text) => {
this.actionText = text;
this.isTogglingSession = false;
};
ipcRenderer.on('session-state-changed', this._sessionStateListener);
ipcRenderer.on('session-state-text', this._sessionStateTextListener);
// this._sessionStateListener = (event, { isActive }) => {
// this.isSessionActive = isActive;
// this.isTogglingSession = false;
// };
// ipcRenderer.on('session-state-changed', this._sessionStateListener);
this._shortcutListener = (event, keybinds) => {
console.log('[MainHeader] Received updated shortcuts:', keybinds);
this.shortcuts = keybinds;
@ -328,9 +396,12 @@ export class MainHeader extends LitElement {
if (window.require) {
const { ipcRenderer } = window.require('electron');
if (this._sessionStateListener) {
ipcRenderer.removeListener('session-state-changed', this._sessionStateListener);
if (this._sessionStateTextListener) {
ipcRenderer.removeListener('session-state-text', this._sessionStateTextListener);
}
// if (this._sessionStateListener) {
// ipcRenderer.removeListener('session-state-changed', this._sessionStateListener);
// }
if (this._shortcutListener) {
ipcRenderer.removeListener('shortcuts-updated', this._shortcutListener);
}
@ -341,6 +412,7 @@ export class MainHeader extends LitElement {
if (window.require) {
window.require('electron').ipcRenderer.invoke(channel, ...args);
}
// return Promise.resolve();
}
showSettingsWindow(element) {
@ -369,6 +441,23 @@ export class MainHeader extends LitElement {
}
}
async _handleListenClick() {
if (this.isTogglingSession) {
return;
}
this.isTogglingSession = true;
try {
const channel = 'toggle-feature';
const args = ['listen'];
await this.invoke(channel, ...args);
} catch (error) {
console.error('IPC invoke for session toggle failed:', error);
this.isTogglingSession = false;
}
}
renderShortcut(accelerator) {
if (!accelerator) return html``;
@ -394,31 +483,45 @@ export class MainHeader extends LitElement {
}
render() {
const buttonClasses = {
active: this.actionText === 'Stop',
done: this.actionText === 'Done',
};
const showStopIcon = this.actionText === 'Stop' || this.actionText === 'Done';
return html`
<div class="header">
<button
class="listen-button ${this.isSessionActive ? 'active' : ''}"
@click=${() => this.invoke(this.isSessionActive ? 'close-session' : 'toggle-feature', 'listen')}
class="listen-button ${Object.keys(buttonClasses).filter(k => buttonClasses[k]).join(' ')}"
@click=${this._handleListenClick}
?disabled=${this.isTogglingSession}
>
<div class="action-text">
<div class="action-text-content">${this.isSessionActive ? 'Stop' : 'Listen'}</div>
</div>
<div class="listen-icon">
${this.isSessionActive
? html`
<svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="9" height="9" rx="1" fill="white"/>
</svg>
`
: html`
<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.69922 2.7515C1.69922 2.37153 2.00725 2.0635 2.38722 2.0635H2.73122C3.11119 2.0635 3.41922 2.37153 3.41922 2.7515V8.2555C3.41922 8.63547 3.11119 8.9435 2.73122 8.9435H2.38722C2.00725 8.9435 1.69922 8.63547 1.69922 8.2555V2.7515Z" fill="white"/>
<path d="M5.13922 1.3755C5.13922 0.995528 5.44725 0.6875 5.82722 0.6875H6.17122C6.55119 0.6875 6.85922 0.995528 6.85922 1.3755V9.6315C6.85922 10.0115 6.55119 10.3195 6.17122 10.3195H5.82722C5.44725 10.3195 5.13922 10.0115 5.13922 9.6315V1.3755Z" fill="white"/>
<path d="M8.57922 3.0955C8.57922 2.71553 8.88725 2.4075 9.26722 2.4075H9.61122C9.99119 2.4075 10.2992 2.71553 10.2992 3.0955V7.9115C10.2992 8.29147 9.99119 8.5995 9.61122 8.5995H9.26722C8.88725 8.5995 8.57922 8.29147 8.57922 7.9115V3.0955Z" fill="white"/>
</svg>
`}
</div>
${this.isTogglingSession
? html`
<div class="loading-dots">
<span></span><span></span><span></span>
</div>
`
: html`
<div class="action-text">
<div class="action-text-content">${this.actionText}</div>
</div>
<div class="listen-icon">
${showStopIcon
? html`
<svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="9" height="9" rx="1" fill="white"/>
</svg>
`
: html`
<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.69922 2.7515C1.69922 2.37153 2.00725 2.0635 2.38722 2.0635H2.73122C3.11119 2.0635 3.41922 2.37153 3.41922 2.7515V8.2555C3.41922 8.63547 3.11119 8.9435 2.73122 8.9435H2.38722C2.00725 8.9435 1.69922 8.63547 1.69922 8.2555V2.7515Z" fill="white"/>
<path d="M5.13922 1.3755C5.13922 0.995528 5.44725 0.6875 5.82722 0.6875H6.17122C6.55119 0.6875 6.85922 0.995528 6.85922 1.3755V9.6315C6.85922 10.0115 6.55119 10.3195 6.17122 10.3195H5.82722C5.44725 10.3195 5.13922 10.0115 5.13922 9.6315V1.3755Z" fill="white"/>
<path d="M8.57922 3.0955C8.57922 2.71553 8.88725 2.4075 9.26722 2.4075H9.61122C9.99119 2.4075 10.2992 2.71553 10.2992 3.0955V7.9115C10.2992 8.29147 9.99119 8.5995 9.61122 8.5995H9.26722C8.88725 8.5995 8.57922 8.29147 8.57922 7.9115V3.0955Z" fill="white"/>
</svg>
`}
</div>
`}
</button>
<div class="header-actions ask-action" @click=${() => this.invoke('toggle-feature', 'ask')}>

View File

@ -472,7 +472,7 @@ function createWindows() {
createFeatureWindows(windowPool.get('header'));
}
const header = windowPool.get('header');
if (featureName === 'listen') {
console.log(`[WindowManager] Toggling feature: ${featureName}`);
const listenWindow = windowPool.get(featureName);
@ -480,18 +480,22 @@ function createWindows() {
if (listenService && listenService.isSessionActive()) {
console.log('[WindowManager] Listen session is active, closing it via toggle.');
await listenService.closeSession();
return;
}
if (listenWindow.isVisible()) {
listenWindow.webContents.send('window-hide-animation');
listenWindow.webContents.send('session-state-changed', { isActive: false });
header.webContents.send('session-state-text', 'Done');
// return;
} else {
listenWindow.show();
updateLayout();
// listenWindow.webContents.send('start-listening-session');
listenWindow.webContents.send('window-show-animation');
await listenService.initializeSession();
// listenWindow.webContents.send('start-listening-session');
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');
}
}
}

View File

@ -144,9 +144,7 @@ class ListenService {
console.log('✅ Listen service initialized successfully.');
this.sendToRenderer('session-state-changed', { isActive: true });
this.sendToRenderer('update-status', 'Connected. Ready to listen.');
// this.sendToRenderer('change-listen-capture-state', { status: "start" });
return true;
} catch (error) {
@ -181,6 +179,7 @@ class ListenService {
async closeSession() {
try {
this.sendToRenderer('change-listen-capture-state', { status: "stop" });
// Close STT sessions
await this.sttService.closeSessions();
@ -194,9 +193,7 @@ class ListenService {
this.currentSessionId = null;
this.summaryService.resetConversationHistory();
this.sendToRenderer('session-state-changed', { isActive: false });
this.sendToRenderer('session-did-close');
this.sendToRenderer('change-listen-capture-state', { status: "stop" });
console.log('Listen service session closed.');
return { success: true };
@ -285,9 +282,9 @@ class ListenService {
}
});
ipcMain.handle('close-session', async () => {
return await this.closeSession();
});
// ipcMain.handle('close-session', async () => {
// return await this.closeSession();
// });
ipcMain.handle('update-google-search-setting', async (event, enabled) => {
try {

View File

@ -7,15 +7,9 @@ let aecPtr = 0; // Rust Aec* 1개만 재사용
/** WASM 모듈 가져오고 1회 초기화 */
async function getAec () {
if (aecModPromise) {
console.log('[AEC] getAec: 캐시=있음(재사용)');
return aecModPromise; // 캐시
}
console.log('[AEC] getAec: 캐시=없음 → 모듈 로드 시작');
if (aecModPromise) return aecModPromise; // 캐시
aecModPromise = createAecModule().then((M) => {
console.log('[AEC] WASM 모듈 로드 완료');
aecMod = M;
// C 심볼 → JS 래퍼 바인딩 (딱 1번)
M.newPtr = M.cwrap('AecNew', 'number',
@ -24,12 +18,7 @@ async function getAec () {
['number','number','number','number','number']);
M.destroy = M.cwrap('AecDestroy', null, ['number']);
return M;
})
.catch(err => {
console.error('[AEC] WASM 모듈 로드 실패:', err);
throw err; // 상위에서도 잡을 수 있게
});
});
return aecModPromise;
}
@ -143,10 +132,6 @@ function disposeAec () {
}
function runAecSync (micF32, sysF32) {
const modStat = aecMod?.HEAPU8 ? '있음' : '없음'; // aecMod가 초기화되었고 HEAP 접근 가능?
const ptrStat = aecPtr ? '있음' : '없음'; // newPtr 호출 여부
const heapStat = aecMod?.HEAPU8 ? '있음' : '없음'; // HEAPU8 생성 여부
console.log(`[AEC] mod:${modStat} ptr:${ptrStat} heap:${heapStat}`);
if (!aecMod || !aecPtr || !aecMod.HEAPU8) return micF32; // 아직 모듈 안 뜸 → 패스
const len = micF32.length;
@ -160,7 +145,6 @@ function runAecSync (micF32, sysF32) {
const outF32 = float32FromInt16View(new Int16Array(heapBuf, out, len));
aecMod._free(mic.ptr); aecMod._free(echo.ptr); aecMod._free(out);
console.log(`[AEC] 적용 완료`);
return outF32;
}
@ -282,7 +266,7 @@ async function setupMicProcessing(micStream) {
micProcessor.onaudioprocess = (e) => {
const inputData = e.inputBuffer.getChannelData(0);
audioBuffer.push(...inputData);
// console.log('🎤 micProcessor.onaudioprocess');
console.log('🎤 micProcessor.onaudioprocess');
// samplesPerChunk(=2400) 만큼 모이면 전송
while (audioBuffer.length >= samplesPerChunk) {
@ -296,7 +280,7 @@ async function setupMicProcessing(micStream) {
// **음성 구간일 때만 런**
processedChunk = runAecSync(new Float32Array(chunk), sysF32);
// console.log('🔊 Applied WASM-AEC (speex)');
console.log('🔊 Applied WASM-AEC (speex)');
} else {
console.log('🔊 No system audio for AEC reference');
}