Merge branch 'main' into pr-107

This commit is contained in:
sanio 2025-07-11 17:06:18 +09:00
commit 6ec45e138f
25 changed files with 566 additions and 3855 deletions

View File

@ -8,7 +8,7 @@ jobs:
# Job 1: Any contributor can self-assign
self-assign:
# Only run if the comment is exactly '/assign'
if: github.event.comment.body == '/assign'
if: startsWith(github.event.comment.body, '/assign') && !contains(github.event.comment.body, '@')
runs-on: ubuntu-latest
permissions:
issues: write

View File

@ -46,8 +46,11 @@ win:
- target: portable
arch: x64
requestedExecutionLevel: asInvoker
# Disable code signing to avoid symbolic link issues on Windows
signAndEditExecutable: false
signAndEditExecutable: true
cscLink: build\certs\glass-dev.pfx
cscKeyPassword: "${env.CSC_KEY_PASSWORD}"
signtoolOptions:
certificateSubjectName: "Glass Dev Code Signing"
# NSIS installer configuration for Windows
nsis:

View File

@ -1,87 +0,0 @@
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
const { notarizeApp } = require('./notarize');
module.exports = {
packagerConfig: {
asar: {
unpack: '**/*.node,**/*.dylib,' + '**/node_modules/{sharp,@img}/**/*',
},
extraResource: ['./src/assets/SystemAudioDump', './pickleglass_web/out'],
name: 'Glass',
icon: 'src/assets/logo',
appBundleId: 'com.pickle.glass',
arch: 'universal',
protocols: [
{
name: 'PickleGlass Protocol',
schemes: ['pickleglass'],
},
],
asarUnpack: [
'**/*.node',
'**/*.dylib',
'node_modules/@img/sharp-darwin-x64/**',
'node_modules/@img/sharp-libvips-darwin-x64/**',
'node_modules/@img/sharp-darwin-arm64/**',
'node_modules/@img/sharp-libvips-darwin-arm64/**',
],
osxSign: {
identity: process.env.APPLE_SIGNING_IDENTITY,
'hardened-runtime': true,
entitlements: 'entitlements.plist',
'entitlements-inherit': 'entitlements.plist',
},
osxNotarize: {
tool: 'notarytool',
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
teamId: process.env.APPLE_TEAM_ID,
},
},
rebuildConfig: {},
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {
name: 'pickle-glass',
productName: 'Glass',
shortcutName: 'Glass',
createDesktopShortcut: true,
createStartMenuShortcut: true,
},
},
{
name: '@electron-forge/maker-dmg',
platforms: ['darwin'],
},
{
name: '@electron-forge/maker-deb',
config: {},
},
{
name: '@electron-forge/maker-rpm',
config: {},
},
],
hooks: {
afterSign: async (context, forgeConfig, platform, arch, appPath) => {
await notarizeApp(context, forgeConfig, platform, arch, appPath);
},
},
plugins: [
{
name: '@electron-forge/plugin-auto-unpack-natives',
config: {},
},
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: false,
}),
],
};

2389
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,13 @@
{
"name": "pickle-glass",
"productName": "Glass",
"version": "0.2.4",
"description": "Cl*ely for Free",
"main": "src/index.js",
"scripts": {
"setup": "npm install && cd pickleglass_web && npm install && npm run build && cd .. && npm start",
"start": "npm run build:renderer && electron-forge start",
"package": "npm run build:renderer && electron-forge package",
"start": "npm run build:renderer && electron .",
"package": "npm run build:all && electron-builder --dir",
"make": "npm run build:renderer && electron-forge make",
"build": "npm run build:all && electron-builder --config electron-builder.yml --publish never",
"build:win": "npm run build:all && electron-builder --win --x64 --publish never",
@ -58,14 +56,6 @@
"ws": "^8.18.0"
},
"devDependencies": {
"@electron-forge/cli": "^7.8.1",
"@electron-forge/maker-deb": "^7.8.1",
"@electron-forge/maker-dmg": "^7.8.1",
"@electron-forge/maker-rpm": "^7.8.1",
"@electron-forge/maker-squirrel": "^7.8.1",
"@electron-forge/maker-zip": "^7.8.1",
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
"@electron-forge/plugin-fuses": "^7.8.1",
"@electron/fuses": "^1.8.0",
"@electron/notarize": "^2.5.0",
"electron": "^30.5.1",
@ -77,4 +67,4 @@
"optionalDependencies": {
"electron-liquid-glass": "^1.0.1"
}
}
}

View File

@ -43,9 +43,10 @@ export default function LoginPage() {
window.location.href = deepLinkUrl
setTimeout(() => {
alert('Login completed. Please return to Pickle Glass app.')
}, 1000)
// Maybe we don't need this
// setTimeout(() => {
// alert('Login completed. Please return to Pickle Glass app.')
// }, 1000)
} catch (error) {
console.error('❌ Deep link processing failed:', error)

View File

@ -25,15 +25,14 @@ export class ApiKeyHeader extends LitElement {
static styles = css`
:host {
display: block;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
transition: opacity 0.25s ease-out;
display: block;
transition: opacity 0.3s ease-in, transform 0.3s ease-in;
will-change: opacity, transform;
}
:host(.sliding-out) {
animation: slideOutUp 0.3s ease-in forwards;
will-change: opacity, transform;
opacity: 0;
transform: translateY(-20px);
}
:host(.hidden) {
@ -41,17 +40,6 @@ export class ApiKeyHeader extends LitElement {
pointer-events: none;
}
@keyframes slideOutUp {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-20px);
}
}
* {
font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
cursor: default;
@ -60,6 +48,7 @@ export class ApiKeyHeader extends LitElement {
}
.container {
-webkit-app-region: drag;
width: 350px;
min-height: 260px;
padding: 18px 20px;
@ -89,6 +78,7 @@ export class ApiKeyHeader extends LitElement {
}
.close-button {
-webkit-app-region: no-drag;
position: absolute;
top: 10px;
right: 10px;
@ -168,6 +158,7 @@ export class ApiKeyHeader extends LitElement {
}
.api-input {
-webkit-app-region: no-drag;
width: 100%;
height: 34px;
background: rgba(255, 255, 255, 0.1);
@ -195,6 +186,7 @@ export class ApiKeyHeader extends LitElement {
.provider-column { flex: 1; display: flex; flex-direction: column; align-items: center; }
.provider-label { color: rgba(255, 255, 255, 0.7); font-size: 11px; font-weight: 500; margin-bottom: 6px; }
.api-input, .provider-select {
-webkit-app-region: no-drag;
width: 100%;
height: 34px;
text-align: center;
@ -221,6 +213,7 @@ export class ApiKeyHeader extends LitElement {
.action-button {
-webkit-app-region: no-drag;
width: 100%;
height: 34px;
background: rgba(255, 255, 255, 0.2);
@ -266,37 +259,10 @@ export class ApiKeyHeader extends LitElement {
font-weight: 500; /* Medium */
margin: 10px 0;
}
/* ────────────────[ GLASS BYPASS ]─────────────── */
:host-context(body.has-glass) .container,
:host-context(body.has-glass) .api-input,
:host-context(body.has-glass) .provider-select,
:host-context(body.has-glass) .action-button,
:host-context(body.has-glass) .close-button {
background: transparent !important;
border: none !important;
box-shadow: none !important;
filter: none !important;
backdrop-filter: none !important;
}
:host-context(body.has-glass) .container::after,
:host-context(body.has-glass) .action-button::after {
display: none !important;
}
:host-context(body.has-glass) .action-button:hover,
:host-context(body.has-glass) .provider-select:hover,
:host-context(body.has-glass) .close-button:hover {
background: transparent !important;
}
`
constructor() {
super()
this.dragState = null
this.wasJustDragged = false
this.isLoading = false
this.errorMessage = ""
this.successMessage = ""
@ -358,8 +324,6 @@ export class ApiKeyHeader extends LitElement {
this.loadProviderConfig();
//////// after_modelStateService ////////
this.handleMouseMove = this.handleMouseMove.bind(this)
this.handleMouseUp = this.handleMouseUp.bind(this)
this.handleKeyPress = this.handleKeyPress.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
this.handleInput = this.handleInput.bind(this)
@ -1533,7 +1497,6 @@ export class ApiKeyHeader extends LitElement {
handleUsePicklesKey(e) {
e.preventDefault()
if (this.wasJustDragged) return
console.log("Requesting Firebase authentication from main process...")
if (window.require) {

View File

@ -2,17 +2,16 @@ 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 },
};
static styles = css`
:host {
display: flex;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
transition: transform 0.2s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.2s ease-out;
will-change: transform, opacity;
}
:host(.hiding) {
@ -33,65 +32,6 @@ export class MainHeader extends LitElement {
pointer-events: none;
}
@keyframes slideUp {
0% {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0px);
}
30% {
opacity: 0.7;
transform: translateY(-20%) scale(0.98);
filter: blur(0.5px);
}
70% {
opacity: 0.3;
transform: translateY(-80%) scale(0.92);
filter: blur(1.5px);
}
100% {
opacity: 0;
transform: translateY(-150%) scale(0.85);
filter: blur(2px);
}
}
@keyframes slideDown {
0% {
opacity: 0;
transform: translateY(-150%) scale(0.85);
filter: blur(2px);
}
30% {
opacity: 0.5;
transform: translateY(-50%) scale(0.92);
filter: blur(1px);
}
65% {
opacity: 0.9;
transform: translateY(-5%) scale(0.99);
filter: blur(0.2px);
}
85% {
opacity: 0.98;
transform: translateY(2%) scale(1.005);
filter: blur(0px);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0px);
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
* {
font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
@ -100,6 +40,7 @@ export class MainHeader extends LitElement {
}
.header {
-webkit-app-region: drag;
width: max-content;
height: 47px;
padding: 2px 10px 2px 13px;
@ -141,6 +82,7 @@ export class MainHeader extends LitElement {
}
.listen-button {
-webkit-app-region: no-drag;
height: 26px;
padding: 0 13px;
background: transparent;
@ -155,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);
}
@ -163,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);
}
@ -192,7 +157,40 @@ 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;
box-sizing: border-box;
justify-content: flex-start;
@ -264,6 +262,7 @@ export class MainHeader extends LitElement {
}
.settings-button {
-webkit-app-region: no-drag;
padding: 5px;
border-radius: 50%;
background: transparent;
@ -291,125 +290,22 @@ export class MainHeader extends LitElement {
width: 16px;
height: 16px;
}
/* ────────────────[ GLASS BYPASS ]─────────────── */
:host-context(body.has-glass) .header,
:host-context(body.has-glass) .listen-button,
:host-context(body.has-glass) .header-actions,
:host-context(body.has-glass) .settings-button {
background: transparent !important;
filter: none !important;
box-shadow: none !important;
backdrop-filter: none !important;
}
:host-context(body.has-glass) .icon-box {
background: transparent !important;
border: none !important;
}
:host-context(body.has-glass) .header::before,
:host-context(body.has-glass) .header::after,
:host-context(body.has-glass) .listen-button::before,
:host-context(body.has-glass) .listen-button::after {
display: none !important;
}
:host-context(body.has-glass) .header-actions:hover,
:host-context(body.has-glass) .settings-button:hover,
:host-context(body.has-glass) .listen-button:hover::before {
background: transparent !important;
}
:host-context(body.has-glass) * {
animation: none !important;
transition: none !important;
transform: none !important;
filter: none !important;
backdrop-filter: none !important;
box-shadow: none !important;
}
:host-context(body.has-glass) .header,
:host-context(body.has-glass) .listen-button,
:host-context(body.has-glass) .header-actions,
:host-context(body.has-glass) .settings-button,
:host-context(body.has-glass) .icon-box {
border-radius: 0 !important;
}
:host-context(body.has-glass) {
animation: none !important;
transition: none !important;
transform: none !important;
will-change: auto !important;
}
`;
constructor() {
super();
this.shortcuts = {};
this.dragState = null;
this.wasJustDragged = false;
this.isVisible = true;
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.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleAnimationEnd = this.handleAnimationEnd.bind(this);
}
async handleMouseDown(e) {
e.preventDefault();
const { ipcRenderer } = window.require('electron');
const initialPosition = await ipcRenderer.invoke('get-header-position');
this.dragState = {
initialMouseX: e.screenX,
initialMouseY: e.screenY,
initialWindowX: initialPosition.x,
initialWindowY: initialPosition.y,
moved: false,
};
window.addEventListener('mousemove', this.handleMouseMove, { capture: true });
window.addEventListener('mouseup', this.handleMouseUp, { once: true, capture: true });
}
handleMouseMove(e) {
if (!this.dragState) return;
const deltaX = Math.abs(e.screenX - this.dragState.initialMouseX);
const deltaY = Math.abs(e.screenY - this.dragState.initialMouseY);
if (deltaX > 3 || deltaY > 3) {
this.dragState.moved = true;
}
const newWindowX = this.dragState.initialWindowX + (e.screenX - this.dragState.initialMouseX);
const newWindowY = this.dragState.initialWindowY + (e.screenY - this.dragState.initialMouseY);
const { ipcRenderer } = window.require('electron');
ipcRenderer.invoke('move-header-to', newWindowX, newWindowY);
}
handleMouseUp(e) {
if (!this.dragState) return;
const wasDragged = this.dragState.moved;
window.removeEventListener('mousemove', this.handleMouseMove, { capture: true });
this.dragState = null;
if (wasDragged) {
this.wasJustDragged = true;
setTimeout(() => {
this.wasJustDragged = false;
}, 0);
}
}
toggleVisibility() {
if (this.isAnimating) {
console.log('[MainHeader] Animation already in progress, ignoring toggle');
@ -431,58 +327,29 @@ export class MainHeader extends LitElement {
}
hide() {
this.classList.remove('showing', 'hidden');
this.classList.remove('showing');
this.classList.add('hiding');
this.isVisible = false;
this.animationEndTimer = setTimeout(() => {
if (this.classList.contains('hiding')) {
this.handleAnimationEnd({ target: this });
}
}, 350);
}
show() {
this.classList.remove('hiding', 'hidden');
this.classList.add('showing');
this.isVisible = true;
this.animationEndTimer = setTimeout(() => {
if (this.classList.contains('showing')) {
this.handleAnimationEnd({ target: this });
}
}, 400);
}
handleAnimationEnd(e) {
if (e.target !== this) return;
if (this.animationEndTimer) {
clearTimeout(this.animationEndTimer);
this.animationEndTimer = null;
}
this.isAnimating = false;
if (this.classList.contains('hiding')) {
this.classList.remove('hiding');
this.classList.add('hidden');
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.send('header-animation-complete', 'hidden');
window.require('electron').ipcRenderer.send('header-animation-finished', 'hidden');
}
} else if (this.classList.contains('showing')) {
this.classList.remove('showing');
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.send('header-animation-complete', 'visible');
window.require('electron').ipcRenderer.send('header-animation-finished', 'visible');
}
} else if (this.classList.contains('sliding-in')) {
this.classList.remove('sliding-in');
this.hasSlidIn = true;
console.log('[MainHeader] Slide-in animation completed');
}
}
@ -497,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;
@ -520,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);
}
@ -530,51 +409,56 @@ export class MainHeader extends LitElement {
}
invoke(channel, ...args) {
if (this.wasJustDragged) {
return;
}
if (window.require) {
window.require('electron').ipcRenderer.invoke(channel, ...args);
}
// return Promise.resolve();
}
showWindow(name, element) {
if (this.wasJustDragged) return;
showSettingsWindow(element) {
if (window.require) {
const { ipcRenderer } = window.require('electron');
console.log(`[MainHeader] showWindow('${name}') called at ${Date.now()}`);
console.log(`[MainHeader] showSettingsWindow called at ${Date.now()}`);
ipcRenderer.send('cancel-hide-window', name);
ipcRenderer.send('cancel-hide-settings-window');
if (name === 'settings' && element) {
const rect = element.getBoundingClientRect();
ipcRenderer.send('show-window', {
name: 'settings',
bounds: {
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height
}
if (element) {
const { left, top, width, height } = element.getBoundingClientRect();
ipcRenderer.send('show-settings-window', {
x: left,
y: top,
width,
height,
});
} else {
ipcRenderer.send('show-window', name);
}
}
}
hideWindow(name) {
if (this.wasJustDragged) return;
hideSettingsWindow() {
if (window.require) {
console.log(`[MainHeader] hideWindow('${name}') called at ${Date.now()}`);
window.require('electron').ipcRenderer.send('hide-window', name);
console.log(`[MainHeader] hideSettingsWindow called at ${Date.now()}`);
window.require('electron').ipcRenderer.send('hide-settings-window');
}
}
cancelHideWindow(name) {
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``;
@ -599,31 +483,45 @@ export class MainHeader extends LitElement {
}
render() {
return html`
<div class="header" @mousedown=${this.handleMouseDown}>
<button
class="listen-button ${this.isSessionActive ? 'active' : ''}"
@click=${() => this.invoke(this.isSessionActive ? 'close-session' : 'toggle-feature', 'listen')}
>
<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>
const buttonClasses = {
active: this.actionText === 'Stop',
done: this.actionText === 'Done',
};
const showStopIcon = this.actionText === 'Stop' || this.actionText === 'Done';
`
: 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>
return html`
<div class="header">
<button
class="listen-button ${Object.keys(buttonClasses).filter(k => buttonClasses[k]).join(' ')}"
@click=${this._handleListenClick}
?disabled=${this.isTogglingSession}
>
${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')}>
@ -646,8 +544,8 @@ export class MainHeader extends LitElement {
<button
class="settings-button"
@mouseenter=${(e) => this.showWindow('settings', e.currentTarget)}
@mouseleave=${() => this.hideWindow('settings')}
@mouseenter=${(e) => this.showSettingsWindow(e.currentTarget)}
@mouseleave=${() => this.hideSettingsWindow()}
>
<div class="settings-icon">
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">

View File

@ -4,14 +4,13 @@ export class PermissionHeader extends LitElement {
static styles = css`
:host {
display: block;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
transition: opacity 0.25s ease-out;
transition: opacity 0.3s ease-in, transform 0.3s ease-in;
will-change: opacity, transform;
}
:host(.sliding-out) {
animation: slideOutUp 0.3s ease-in forwards;
will-change: opacity, transform;
opacity: 0;
transform: translateY(-20px);
}
:host(.hidden) {
@ -19,17 +18,6 @@ export class PermissionHeader extends LitElement {
pointer-events: none;
}
@keyframes slideOutUp {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-20px);
}
}
* {
font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
cursor: default;
@ -38,6 +26,7 @@ export class PermissionHeader extends LitElement {
}
.container {
-webkit-app-region: drag;
width: 285px;
height: 220px;
padding: 18px 20px;
@ -67,6 +56,7 @@ export class PermissionHeader extends LitElement {
}
.close-button {
-webkit-app-region: no-drag;
position: absolute;
top: 10px;
right: 10px;
@ -157,6 +147,7 @@ export class PermissionHeader extends LitElement {
}
.action-button {
-webkit-app-region: no-drag;
width: 100%;
height: 34px;
background: rgba(255, 255, 255, 0.2);
@ -198,6 +189,7 @@ export class PermissionHeader extends LitElement {
}
.continue-button {
-webkit-app-region: no-drag;
width: 100%;
height: 34px;
background: rgba(34, 197, 94, 0.8);
@ -237,30 +229,6 @@ export class PermissionHeader extends LitElement {
background: rgba(255, 255, 255, 0.2);
cursor: not-allowed;
}
/* ────────────────[ GLASS BYPASS ]─────────────── */
:host-context(body.has-glass) .container,
:host-context(body.has-glass) .action-button,
:host-context(body.has-glass) .continue-button,
:host-context(body.has-glass) .close-button {
background: transparent !important;
border: none !important;
box-shadow: none !important;
filter: none !important;
backdrop-filter: none !important;
}
:host-context(body.has-glass) .container::after,
:host-context(body.has-glass) .action-button::after,
:host-context(body.has-glass) .continue-button::after {
display: none !important;
}
:host-context(body.has-glass) .action-button:hover,
:host-context(body.has-glass) .continue-button:hover,
:host-context(body.has-glass) .close-button:hover {
background: transparent !important;
}
`;
static properties = {
@ -276,9 +244,6 @@ export class PermissionHeader extends LitElement {
this.screenGranted = 'unknown';
this.isChecking = false;
this.continueCallback = null;
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
}
async connectedCallback() {
@ -298,61 +263,6 @@ export class PermissionHeader extends LitElement {
}
}
async handleMouseDown(e) {
if (e.target.tagName === 'BUTTON') {
return;
}
e.preventDefault();
const { ipcRenderer } = window.require('electron');
const initialPosition = await ipcRenderer.invoke('get-header-position');
this.dragState = {
initialMouseX: e.screenX,
initialMouseY: e.screenY,
initialWindowX: initialPosition.x,
initialWindowY: initialPosition.y,
moved: false,
};
window.addEventListener('mousemove', this.handleMouseMove);
window.addEventListener('mouseup', this.handleMouseUp, { once: true });
}
handleMouseMove(e) {
if (!this.dragState) return;
const deltaX = Math.abs(e.screenX - this.dragState.initialMouseX);
const deltaY = Math.abs(e.screenY - this.dragState.initialMouseY);
if (deltaX > 3 || deltaY > 3) {
this.dragState.moved = true;
}
const newWindowX = this.dragState.initialWindowX + (e.screenX - this.dragState.initialMouseX);
const newWindowY = this.dragState.initialWindowY + (e.screenY - this.dragState.initialMouseY);
const { ipcRenderer } = window.require('electron');
ipcRenderer.invoke('move-header-to', newWindowX, newWindowY);
}
handleMouseUp(e) {
if (!this.dragState) return;
const wasDragged = this.dragState.moved;
window.removeEventListener('mousemove', this.handleMouseMove);
this.dragState = null;
if (wasDragged) {
this.wasJustDragged = true;
setTimeout(() => {
this.wasJustDragged = false;
}, 200);
}
}
async checkPermissions() {
if (!window.require || this.isChecking) return;
@ -390,7 +300,7 @@ export class PermissionHeader extends LitElement {
}
async handleMicrophoneClick() {
if (!window.require || this.microphoneGranted === 'granted' || this.wasJustDragged) return;
if (!window.require || this.microphoneGranted === 'granted') return;
console.log('[PermissionHeader] Requesting microphone permission...');
const { ipcRenderer } = window.require('electron');
@ -423,7 +333,7 @@ export class PermissionHeader extends LitElement {
}
async handleScreenClick() {
if (!window.require || this.screenGranted === 'granted' || this.wasJustDragged) return;
if (!window.require || this.screenGranted === 'granted') return;
console.log('[PermissionHeader] Checking screen recording permission...');
const { ipcRenderer } = window.require('electron');
@ -453,8 +363,7 @@ export class PermissionHeader extends LitElement {
async handleContinue() {
if (this.continueCallback &&
this.microphoneGranted === 'granted' &&
this.screenGranted === 'granted' &&
!this.wasJustDragged) {
this.screenGranted === 'granted') {
// Mark permissions as completed
if (window.require) {
const { ipcRenderer } = window.require('electron');
@ -481,7 +390,7 @@ export class PermissionHeader extends LitElement {
const allGranted = this.microphoneGranted === 'granted' && this.screenGranted === 'granted';
return html`
<div class="container" @mousedown=${this.handleMouseDown}>
<div class="container">
<button class="close-button" @click=${this.handleClose} title="Close application">
<svg width="8" height="8" viewBox="0 0 10 10" fill="currentColor">
<path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.2" />

View File

@ -68,12 +68,7 @@ export class PickleGlassApp extends LitElement {
this.selectedScreenshotInterval = localStorage.getItem('selectedScreenshotInterval') || '5';
this.selectedImageQuality = localStorage.getItem('selectedImageQuality') || 'medium';
this._isClickThrough = false;
this.outlines = [];
this.analysisRequests = [];
window.pickleGlass.setStructuredData = data => {
this.updateStructuredData(data);
};
}
connectedCallback() {
@ -82,14 +77,13 @@ export class PickleGlassApp extends LitElement {
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.on('update-status', (_, status) => this.setStatus(status));
ipcRenderer.on('click-through-toggled', (_, isEnabled) => {
this._isClickThrough = isEnabled;
});
ipcRenderer.on('start-listening-session', () => {
console.log('Received start-listening-session command, calling handleListenClick.');
this.handleListenClick();
});
// ipcRenderer.on('start-listening-session', () => {
// console.log('Received start-listening-session command, calling handleListenClick.');
// this.handleListenClick();
// });
}
}
@ -97,16 +91,15 @@ export class PickleGlassApp extends LitElement {
super.disconnectedCallback();
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.removeAllListeners('update-status');
ipcRenderer.removeAllListeners('click-through-toggled');
ipcRenderer.removeAllListeners('start-listening-session');
// ipcRenderer.removeAllListeners('start-listening-session');
}
}
updated(changedProperties) {
if (changedProperties.has('isMainViewVisible') || changedProperties.has('currentView')) {
this.requestWindowResize();
}
// if (changedProperties.has('isMainViewVisible') || changedProperties.has('currentView')) {
// this.requestWindowResize();
// }
if (changedProperties.has('currentView')) {
const viewContainer = this.shadowRoot?.querySelector('.view-container');
@ -136,67 +129,35 @@ export class PickleGlassApp extends LitElement {
}
}
requestWindowResize() {
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.invoke('resize-window', {
isMainViewVisible: this.isMainViewVisible,
view: this.currentView,
});
}
}
setStatus(text) {
this.statusText = text;
}
// async handleListenClick() {
// if (window.require) {
// const { ipcRenderer } = window.require('electron');
// const isActive = await ipcRenderer.invoke('is-session-active');
// // if (isActive) {
// // console.log('Session is already active. No action needed.');
// // return;
// // }
// }
async handleListenClick() {
if (window.require) {
const { ipcRenderer } = window.require('electron');
const isActive = await ipcRenderer.invoke('is-session-active');
if (isActive) {
console.log('Session is already active. No action needed.');
return;
}
}
// if (window.pickleGlass) {
// // await window.pickleGlass.initializeopenai(this.selectedProfile, this.selectedLanguage);
// window.pickleGlass.startCapture(this.selectedScreenshotInterval, this.selectedImageQuality);
// }
if (window.pickleGlass) {
await window.pickleGlass.initializeopenai(this.selectedProfile, this.selectedLanguage);
window.pickleGlass.startCapture(this.selectedScreenshotInterval, this.selectedImageQuality);
}
// // 🔄 Clear previous summary/analysis when a new listening session begins
// this.structuredData = {
// summary: [],
// topic: { header: '', bullets: [] },
// actions: [],
// followUps: [],
// };
// 🔄 Clear previous summary/analysis when a new listening session begins
this.structuredData = {
summary: [],
topic: { header: '', bullets: [] },
actions: [],
followUps: [],
};
this.currentResponseIndex = -1;
this.startTime = Date.now();
this.currentView = 'listen';
this.isMainViewVisible = true;
}
handleShowHideClick() {
this.isMainViewVisible = !this.isMainViewVisible;
}
handleSettingsClick() {
this.currentView = 'settings';
this.isMainViewVisible = true;
}
handleHelpClick() {
this.currentView = 'help';
this.isMainViewVisible = true;
}
handleHistoryClick() {
this.currentView = 'history';
this.isMainViewVisible = true;
}
// this.currentResponseIndex = -1;
// this.startTime = Date.now();
// this.currentView = 'listen';
// this.isMainViewVisible = true;
// }
async handleClose() {
if (window.require) {
@ -205,50 +166,8 @@ export class PickleGlassApp extends LitElement {
}
}
handleBackClick() {
this.currentView = 'listen';
}
async handleSendText(message) {
if (window.pickleGlass) {
const result = await window.pickleGlass.sendTextMessage(message);
if (!result.success) {
console.error('Failed to send message:', result.error);
this.setStatus('Error sending message: ' + result.error);
} else {
this.setStatus('Message sent...');
}
}
}
// updateOutline(outline) {
// console.log('📝 PickleGlassApp updateOutline:', outline);
// this.outlines = [...outline];
// this.requestUpdate();
// }
// updateAnalysisRequests(requests) {
// console.log('📝 PickleGlassApp updateAnalysisRequests:', requests);
// this.analysisRequests = [...requests];
// this.requestUpdate();
// }
updateStructuredData(data) {
console.log('📝 PickleGlassApp updateStructuredData:', data);
this.structuredData = data;
this.requestUpdate();
const assistantView = this.shadowRoot?.querySelector('assistant-view');
if (assistantView) {
assistantView.structuredData = data;
console.log('✅ Structured data passed to AssistantView');
}
}
handleResponseIndexChanged(e) {
this.currentResponseIndex = e.detail.index;
}
render() {
switch (this.currentView) {
@ -257,7 +176,6 @@ export class PickleGlassApp extends LitElement {
.currentResponseIndex=${this.currentResponseIndex}
.selectedProfile=${this.selectedProfile}
.structuredData=${this.structuredData}
.onSendText=${message => this.handleSendText(message)}
@response-index-changed=${e => (this.currentResponseIndex = e.detail.index)}
></assistant-view>`;
case 'ask':

View File

@ -237,63 +237,62 @@
<script>
window.addEventListener('DOMContentLoaded', () => {
const app = document.getElementById('pickle-glass');
let animationTimeout = null;
if (window.require) {
const { ipcRenderer } = window.require('electron');
// --- REFACTORED: Event-driven animation handling ---
app.addEventListener('animationend', (event) => {
// 숨김 애니메이션이 끝나면 main 프로세스에 알려 창을 실제로 숨깁니다.
if (event.animationName === 'slideUpToHeader' || event.animationName === 'settingsCollapseToButton') {
console.log(`Animation finished: ${event.animationName}. Notifying main process.`);
ipcRenderer.send('animation-finished');
// 완료 후 애니메이션 클래스 정리
app.classList.remove('window-sliding-up', 'settings-window-hide');
app.classList.add('window-hidden');
} else if (event.animationName === 'slideDownFromHeader' || event.animationName === 'settingsPopFromButton') {
// 보이기 애니메이션 완료 후 클래스 정리
app.classList.remove('window-sliding-down', 'settings-window-show');
}
});
ipcRenderer.on('window-show-animation', () => {
console.log('Starting window show animation');
app.classList.remove('window-hidden', 'window-sliding-up', 'settings-window-hide');
app.classList.add('window-sliding-down');
if (animationTimeout) clearTimeout(animationTimeout);
animationTimeout = setTimeout(() => {
app.classList.remove('window-sliding-down');
}, 120);
});
ipcRenderer.on('window-hide-animation', () => {
console.log('Starting window hide animation');
app.classList.remove('window-sliding-down', 'settings-window-show');
app.classList.add('window-sliding-up');
if (animationTimeout) clearTimeout(animationTimeout);
animationTimeout = setTimeout(() => {
app.classList.remove('window-sliding-up');
app.classList.add('window-hidden');
}, 100);
});
ipcRenderer.on('settings-window-hide-animation', () => {
console.log('Starting settings window hide animation');
app.classList.remove('window-sliding-down', 'settings-window-show');
app.classList.add('settings-window-hide');
if (animationTimeout) clearTimeout(animationTimeout);
animationTimeout = setTimeout(() => {
app.classList.remove('settings-window-hide');
app.classList.add('window-hidden');
}, 100);
});
// --- UNCHANGED: Existing logic for listen window movement ---
ipcRenderer.on('listen-window-move-to-center', () => {
console.log('Moving listen window to center');
app.classList.add('listen-window-moving');
app.classList.remove('listen-window-left');
app.classList.add('listen-window-center');
setTimeout(() => {
app.classList.remove('listen-window-moving');
}, 350);
});
ipcRenderer.on('listen-window-move-to-left', () => {
console.log('Moving listen window to left');
app.classList.add('listen-window-moving');
app.classList.remove('listen-window-center');
app.classList.add('listen-window-left');
setTimeout(() => {
app.classList.remove('listen-window-moving');
}, 350);
@ -305,6 +304,11 @@
const params = new URLSearchParams(window.location.search);
if (params.get('glass') === 'true') {
document.body.classList.add('has-glass');
// --- ADDED: Link to centralized glass-bypass styles ---
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '../common/styles/glass-bypass.css';
document.head.appendChild(link);
}
</script>
</body>

View File

@ -22,6 +22,11 @@
const params = new URLSearchParams(window.location.search);
if (params.get('glass') === 'true') {
document.body.classList.add('has-glass');
// --- ADDED: Link to centralized glass-bypass styles ---
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '../common/styles/glass-bypass.css';
document.head.appendChild(link);
}
</script>
</body>

View File

@ -1,89 +0,0 @@
const crypto = require('crypto');
const { app } = require('electron');
const os = require('os');
class CryptoService {
constructor() {
this.algorithm = 'aes-256-gcm';
this.saltLength = 32;
this.tagLength = 16;
this.ivLength = 16;
this.iterations = 100000;
this.keyLength = 32;
this._derivedKey = null;
}
_getMachineId() {
const machineInfo = `${os.hostname()}-${os.platform()}-${os.arch()}`;
const appPath = app.getPath('userData');
return crypto.createHash('sha256').update(machineInfo + appPath).digest('hex');
}
_deriveKey() {
if (this._derivedKey) return this._derivedKey;
const machineId = this._getMachineId();
const salt = crypto.createHash('sha256').update('pickle-glass-salt').digest();
this._derivedKey = crypto.pbkdf2Sync(machineId, salt, this.iterations, this.keyLength, 'sha256');
return this._derivedKey;
}
encrypt(text) {
if (!text) return null;
try {
const iv = crypto.randomBytes(this.ivLength);
const salt = crypto.randomBytes(this.saltLength);
const key = this._deriveKey();
const cipher = crypto.createCipheriv(this.algorithm, key, iv);
const encrypted = Buffer.concat([
cipher.update(text, 'utf8'),
cipher.final()
]);
const tag = cipher.getAuthTag();
const combined = Buffer.concat([salt, iv, tag, encrypted]);
return combined.toString('base64');
} catch (error) {
console.error('[CryptoService] Encryption failed:', error.message);
throw new Error('Encryption failed');
}
}
decrypt(encryptedData) {
if (!encryptedData) return null;
try {
const combined = Buffer.from(encryptedData, 'base64');
const salt = combined.slice(0, this.saltLength);
const iv = combined.slice(this.saltLength, this.saltLength + this.ivLength);
const tag = combined.slice(this.saltLength + this.ivLength, this.saltLength + this.ivLength + this.tagLength);
const encrypted = combined.slice(this.saltLength + this.ivLength + this.tagLength);
const key = this._deriveKey();
const decipher = crypto.createDecipheriv(this.algorithm, key, iv);
decipher.setAuthTag(tag);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final()
]);
return decrypted.toString('utf8');
} catch (error) {
console.error('[CryptoService] Decryption failed:', error.message);
throw new Error('Decryption failed');
}
}
clearCache() {
this._derivedKey = null;
}
}
module.exports = new CryptoService();

View File

@ -2,7 +2,7 @@ const Store = require('electron-store');
const fetch = require('node-fetch');
const { ipcMain, webContents } = require('electron');
const { PROVIDERS } = require('../ai/factory');
const cryptoService = require('./cryptoService');
const encryptionService = require('./encryptionService');
class ModelStateService {
constructor(authService) {
@ -11,8 +11,8 @@ class ModelStateService {
this.state = {};
}
initialize() {
this._loadStateForCurrentUser();
async initialize() {
await this._loadStateForCurrentUser();
this.setupIpcHandlers();
console.log('[ModelStateService] Initialized.');
@ -64,8 +64,12 @@ class ModelStateService {
});
}
_loadStateForCurrentUser() {
async _loadStateForCurrentUser() {
const userId = this.authService.getCurrentUserId();
// Initialize encryption service for current user
await encryptionService.initializeKey(userId);
const initialApiKeys = Object.keys(PROVIDERS).reduce((acc, key) => {
acc[key] = null;
return acc;
@ -83,7 +87,7 @@ class ModelStateService {
this.state.apiKeys[p] = null;
} else if (this.state.apiKeys[p] && p !== 'ollama' && p !== 'whisper') {
try {
this.state.apiKeys[p] = cryptoService.decrypt(this.state.apiKeys[p]);
this.state.apiKeys[p] = encryptionService.decrypt(this.state.apiKeys[p]);
} catch (error) {
console.error(`[ModelStateService] Failed to decrypt API key for ${p}, resetting`);
this.state.apiKeys[p] = null;
@ -107,7 +111,7 @@ class ModelStateService {
for (const [provider, key] of Object.entries(stateToSave.apiKeys)) {
if (key && provider !== 'ollama' && provider !== 'whisper') {
try {
stateToSave.apiKeys[provider] = cryptoService.encrypt(key);
stateToSave.apiKeys[provider] = encryptionService.encrypt(key);
} catch (error) {
console.error(`[ModelStateService] Failed to encrypt API key for ${provider}`);
stateToSave.apiKeys[provider] = null;
@ -213,13 +217,21 @@ class ModelStateService {
const llmModels = PROVIDERS['openai-glass']?.llmModels;
const sttModels = PROVIDERS['openai-glass']?.sttModels;
if (!this.state.selectedModels.llm && llmModels?.length > 0) {
// When logging in with Pickle, prioritize Pickle's models over existing selections
if (virtualKey && llmModels?.length > 0) {
this.state.selectedModels.llm = llmModels[0].id;
console.log(`[ModelStateService] Prioritized Pickle LLM model: ${llmModels[0].id}`);
}
if (!this.state.selectedModels.stt && sttModels?.length > 0) {
if (virtualKey && sttModels?.length > 0) {
this.state.selectedModels.stt = sttModels[0].id;
console.log(`[ModelStateService] Prioritized Pickle STT model: ${sttModels[0].id}`);
}
this._autoSelectAvailableModels();
// If logging out (virtualKey is null), run auto-selection to find alternatives
if (!virtualKey) {
this._autoSelectAvailableModels();
}
this._saveState();
this._logCurrentSelection();
}

View File

@ -48,7 +48,11 @@ class WhisperService extends LocalAIServiceBase {
this.modelsDir = path.join(whisperDir, 'models');
this.tempDir = path.join(whisperDir, 'temp');
this.whisperPath = path.join(whisperDir, 'bin', 'whisper');
// Windows에서는 .exe 확장자 필요
const platform = this.getPlatform();
const whisperExecutable = platform === 'win32' ? 'whisper.exe' : 'whisper';
this.whisperPath = path.join(whisperDir, 'bin', whisperExecutable);
await this.ensureDirectories();
await this.ensureWhisperBinary();
@ -304,17 +308,112 @@ class WhisperService extends LocalAIServiceBase {
const tempFile = path.join(this.tempDir, 'whisper-binary.zip');
try {
console.log('[WhisperService] Step 1: Downloading Whisper binary...');
await this.downloadWithRetry(binaryUrl, tempFile);
const extractDir = path.dirname(this.whisperPath);
await spawnAsync('powershell', ['-command', `Expand-Archive -Path '${tempFile}' -DestinationPath '${extractDir}' -Force`]);
await fsPromises.unlink(tempFile);
console.log('[WhisperService] Step 2: Extracting archive...');
const extractDir = path.join(this.tempDir, 'extracted');
// 임시 압축 해제 디렉토리 생성
await fsPromises.mkdir(extractDir, { recursive: true });
// PowerShell 명령에서 경로를 올바르게 인용
const expandCommand = `Expand-Archive -Path "${tempFile}" -DestinationPath "${extractDir}" -Force`;
await spawnAsync('powershell', ['-command', expandCommand]);
console.log('[WhisperService] Step 3: Finding and moving whisper executable...');
// 압축 해제된 디렉토리에서 whisper.exe 파일 찾기
const whisperExecutables = await this.findWhisperExecutables(extractDir);
if (whisperExecutables.length === 0) {
throw new Error('whisper.exe not found in extracted files');
}
// 첫 번째로 찾은 whisper.exe를 목표 위치로 복사
const sourceExecutable = whisperExecutables[0];
const targetDir = path.dirname(this.whisperPath);
await fsPromises.mkdir(targetDir, { recursive: true });
await fsPromises.copyFile(sourceExecutable, this.whisperPath);
console.log('[WhisperService] Step 4: Verifying installation...');
// 설치 검증
await fsPromises.access(this.whisperPath, fs.constants.F_OK);
// whisper.exe 실행 테스트
try {
await spawnAsync(this.whisperPath, ['--help']);
console.log('[WhisperService] Whisper executable verified successfully');
} catch (testError) {
console.warn('[WhisperService] Whisper executable test failed, but file exists:', testError.message);
}
console.log('[WhisperService] Step 5: Cleanup...');
// 임시 파일 정리
await fsPromises.unlink(tempFile).catch(() => {});
await this.removeDirectory(extractDir).catch(() => {});
console.log('[WhisperService] Whisper installed successfully on Windows');
return true;
} catch (error) {
console.error('[WhisperService] Windows installation failed:', error);
// 실패 시 임시 파일 정리
await fsPromises.unlink(tempFile).catch(() => {});
await this.removeDirectory(path.join(this.tempDir, 'extracted')).catch(() => {});
throw new Error(`Failed to install Whisper on Windows: ${error.message}`);
}
}
// 압축 해제된 디렉토리에서 whisper.exe 파일들을 재귀적으로 찾기
async findWhisperExecutables(dir) {
const executables = [];
try {
const items = await fsPromises.readdir(dir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dir, item.name);
if (item.isDirectory()) {
const subExecutables = await this.findWhisperExecutables(fullPath);
executables.push(...subExecutables);
} else if (item.isFile() && (item.name === 'whisper.exe' || item.name === 'main.exe')) {
// main.exe도 포함 (일부 빌드에서 whisper 실행파일이 main.exe로 명명됨)
executables.push(fullPath);
}
}
} catch (error) {
console.warn('[WhisperService] Error reading directory:', dir, error.message);
}
return executables;
}
// 디렉토리 재귀적 삭제
async removeDirectory(dir) {
try {
const items = await fsPromises.readdir(dir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dir, item.name);
if (item.isDirectory()) {
await this.removeDirectory(fullPath);
} else {
await fsPromises.unlink(fullPath);
}
}
await fsPromises.rmdir(dir);
} catch (error) {
console.warn('[WhisperService] Error removing directory:', dir, error.message);
}
}
async installLinux() {
console.log('[WhisperService] Installing Whisper on Linux...');

View File

@ -0,0 +1,12 @@
/*
파일은 body.has-glass 클래스가 적용되었을 모든 애니메이션, 트랜지션,
배경, 테두리 등을 비활성화하여 깨끗한 투명 효과(Glass) 보장합니다.
*/
body.has-glass * {
animation: none !important;
transition: none !important;
background: transparent !important;
border: none !important;
box-shadow: none !important;
backdrop-filter: none !important;
}

View File

@ -78,13 +78,6 @@ function updateLayout() {
let movementManager = null;
const featureWindows = ['listen','ask','settings'];
// const featureWindows = ['listen','ask','settings','shortcut-settings'];
function isAllowed(name) {
if (name === 'header') return true;
return featureWindows.includes(name) && currentHeaderState === 'main';
}
function createFeatureWindows(header, namesToCreate) {
// if (windowPool.has('listen')) return;
@ -97,7 +90,7 @@ function createFeatureWindows(header, namesToCreate) {
hasShadow: false,
skipTaskbar: true,
hiddenInMissionControl: true,
resizable: false,
resizable: true,
webPreferences: { nodeIntegration: true, contextIsolation: false },
};
@ -107,8 +100,8 @@ function createFeatureWindows(header, namesToCreate) {
switch (name) {
case 'listen': {
const listen = new BrowserWindow({
...commonChildOptions, width:400,minWidth:400,maxWidth:400,
maxHeight:700,
...commonChildOptions, width:400,minWidth:400,maxWidth:900,
maxHeight:900,
});
listen.setContentProtection(isContentProtectionOn);
listen.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
@ -305,6 +298,7 @@ function createFeatureWindows(header, namesToCreate) {
}
function destroyFeatureWindows() {
const featureWindows = ['listen','ask','settings','shortcut-settings'];
if (settingsHideTimer) {
clearTimeout(settingsHideTimer);
settingsHideTimer = null;
@ -337,78 +331,35 @@ function getDisplayById(displayId) {
function toggleAllWindowsVisibility(movementManager) {
function toggleAllWindowsVisibility() {
const header = windowPool.get('header');
if (!header) return;
if (header.isVisible()) {
console.log('[Visibility] Smart hiding - calculating nearest edge');
const headerBounds = header.getBounds();
const display = screen.getPrimaryDisplay();
const { width: screenWidth, height: screenHeight } = display.workAreaSize;
const centerX = headerBounds.x + headerBounds.width / 2;
const centerY = headerBounds.y + headerBounds.height / 2;
const distances = {
top: centerY,
bottom: screenHeight - centerY,
left: centerX,
right: screenWidth - centerX,
};
const nearestEdge = Object.keys(distances).reduce((nearest, edge) => (distances[edge] < distances[nearest] ? edge : nearest));
console.log(`[Visibility] Nearest edge: ${nearestEdge} (distance: ${distances[nearestEdge].toFixed(1)}px)`);
lastVisibleWindows.clear();
lastVisibleWindows.add('header');
windowPool.forEach((win, name) => {
if (win.isVisible()) {
lastVisibleWindows.add(name);
if (name !== 'header') {
// win.webContents.send('window-hide-animation');
// setTimeout(() => {
// if (!win.isDestroyed()) {
// win.hide();
// }
// }, 200);
win.hide();
}
}
});
console.log('[Visibility] Visible windows before hide:', Array.from(lastVisibleWindows));
movementManager.hideToEdge(nearestEdge, () => {
header.hide();
console.log('[Visibility] Smart hide completed');
}, { instant: true });
} else {
console.log('[Visibility] Smart showing from hidden position');
console.log('[Visibility] Restoring windows:', Array.from(lastVisibleWindows));
header.show();
movementManager.showFromEdge(() => {
lastVisibleWindows.forEach(name => {
if (name === 'header') return;
const win = windowPool.get(name);
if (win && !win.isDestroyed()) {
win.show();
win.webContents.send('window-show-animation');
}
});
setImmediate(updateLayout);
setTimeout(updateLayout, 120);
console.log('[Visibility] Smart show completed');
});
lastVisibleWindows.clear();
windowPool.forEach((win, name) => {
if (win && !win.isDestroyed() && win.isVisible()) {
lastVisibleWindows.add(name);
}
});
lastVisibleWindows.forEach(name => {
if (name === 'header') return;
const win = windowPool.get(name);
if (win && !win.isDestroyed()) win.hide();
});
header.hide();
return;
}
}
lastVisibleWindows.forEach(name => {
const win = windowPool.get(name);
if (win && !win.isDestroyed())
win.show();
});
}
function createWindows() {
@ -464,6 +415,7 @@ function createWindows() {
});
}
windowPool.set('header', header);
header.on('moved', updateLayout);
layoutManager = new WindowLayoutManager(windowPool);
header.webContents.once('dom-ready', () => {
@ -507,25 +459,38 @@ function createWindows() {
updateLayout();
});
ipcMain.handle('toggle-all-windows-visibility', () => toggleAllWindowsVisibility(movementManager));
ipcMain.handle('toggle-all-windows-visibility', () => toggleAllWindowsVisibility());
ipcMain.handle('toggle-feature', async (event, featureName) => {
if (!windowPool.get(featureName) && currentHeaderState === 'main') {
createFeatureWindows(windowPool.get('header'));
}
const windowToToggle = windowPool.get(featureName);
if (windowToToggle) {
if (featureName === 'listen') {
const listenService = global.listenService;
if (listenService && listenService.isSessionActive()) {
console.log('[WindowManager] Listen session is active, closing it via toggle.');
await listenService.closeSession();
return;
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');
}
}
console.log(`[WindowManager] Toggling feature: ${featureName}`);
}
if (featureName === 'ask') {
@ -590,13 +555,6 @@ function createWindows() {
} else {
console.log('[WindowManager] No response found, closing window');
askWindow.webContents.send('window-hide-animation');
setTimeout(() => {
if (!askWindow.isDestroyed()) {
askWindow.hide();
updateLayout();
}
}, 250);
}
} catch (error) {
console.error('[WindowManager] Error checking Ask window state:', error);
@ -610,38 +568,29 @@ function createWindows() {
askWindow.webContents.send('window-show-animation');
askWindow.webContents.send('window-did-show');
}
} else {
const windowToToggle = windowPool.get(featureName);
}
if (windowToToggle) {
if (windowToToggle.isDestroyed()) {
if (featureName === 'settings') {
const settingsWindow = windowPool.get(featureName);
if (settingsWindow) {
if (settingsWindow.isDestroyed()) {
console.error(`Window ${featureName} is destroyed, cannot toggle`);
return;
}
if (windowToToggle.isVisible()) {
if (settingsWindow.isVisible()) {
if (featureName === 'settings') {
windowToToggle.webContents.send('settings-window-hide-animation');
settingsWindow.webContents.send('settings-window-hide-animation');
} else {
windowToToggle.webContents.send('window-hide-animation');
settingsWindow.webContents.send('window-hide-animation');
}
setTimeout(() => {
if (!windowToToggle.isDestroyed()) {
windowToToggle.hide();
updateLayout();
}
}, 250);
} else {
try {
windowToToggle.show();
settingsWindow.show();
updateLayout();
if (featureName === 'listen') {
windowToToggle.webContents.send('start-listening-session');
}
windowToToggle.webContents.send('window-show-animation');
settingsWindow.webContents.send('window-show-animation');
} catch (e) {
console.error('Error showing window:', e);
}
@ -767,9 +716,17 @@ function setupIpcHandlers(movementManager) {
}
});
ipcMain.on('show-window', (event, args) => {
const { name, bounds } = typeof args === 'object' && args !== null ? args : { name: args, bounds: null };
const win = windowPool.get(name);
ipcMain.on('animation-finished', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win && !win.isDestroyed()) {
console.log(`[WindowManager] Hiding window after animation.`);
win.hide();
}
});
ipcMain.on('show-settings-window', (event, bounds) => {
if (!bounds) return;
const win = windowPool.get('settings');
if (win && !win.isDestroyed()) {
if (settingsHideTimer) {
@ -777,78 +734,60 @@ function setupIpcHandlers(movementManager) {
settingsHideTimer = null;
}
if (name === 'settings') {
// Adjust position based on button bounds
const header = windowPool.get('header');
const headerBounds = header?.getBounds() ?? { x: 0, y: 0 };
const settingsBounds = win.getBounds();
// Adjust position based on button bounds
const header = windowPool.get('header');
const headerBounds = header?.getBounds() ?? { x: 0, y: 0 };
const settingsBounds = win.getBounds();
const disp = getCurrentDisplay(header);
const { x: waX, y: waY, width: waW, height: waH } = disp.workArea;
const disp = getCurrentDisplay(header);
const { x: waX, y: waY, width: waW, height: waH } = disp.workArea;
let x = Math.round(headerBounds.x + (bounds?.x ?? 0) + (bounds?.width ?? 0) / 2 - settingsBounds.width / 2);
let y = Math.round(headerBounds.y + (bounds?.y ?? 0) + (bounds?.height ?? 0) + 31);
let x = Math.round(headerBounds.x + (bounds?.x ?? 0) + (bounds?.width ?? 0) / 2 - settingsBounds.width / 2);
let y = Math.round(headerBounds.y + (bounds?.y ?? 0) + (bounds?.height ?? 0) + 31);
x = Math.max(waX + 10, Math.min(waX + waW - settingsBounds.width - 10, x));
y = Math.max(waY + 10, Math.min(waY + waH - settingsBounds.height - 10, y));
win.setBounds({ x, y });
win.__lockedByButton = true;
console.log(`[WindowManager] Positioning settings window at (${x}, ${y}) based on button bounds.`);
}
x = Math.max(waX + 10, Math.min(waX + waW - settingsBounds.width - 10, x));
y = Math.max(waY + 10, Math.min(waY + waH - settingsBounds.height - 10, y));
win.setBounds({ x, y });
win.__lockedByButton = true;
console.log(`[WindowManager] Positioning settings window at (${x}, ${y}) based on button bounds.`);
win.show();
win.moveTop();
if (name === 'settings') {
win.setAlwaysOnTop(true);
}
// updateLayout();
win.setAlwaysOnTop(true);
}
});
ipcMain.on('hide-window', (event, name) => {
const window = windowPool.get(name);
ipcMain.on('hide-settings-window', (event) => {
const window = windowPool.get("settings");
if (window && !window.isDestroyed()) {
if (name === 'settings') {
if (settingsHideTimer) {
clearTimeout(settingsHideTimer);
}
settingsHideTimer = setTimeout(() => {
// window.setAlwaysOnTop(false);
// window.hide();
if (window && !window.isDestroyed()) {
window.setAlwaysOnTop(false);
window.hide();
}
settingsHideTimer = null;
}, 200);
} else {
window.hide();
if (settingsHideTimer) {
clearTimeout(settingsHideTimer);
}
settingsHideTimer = setTimeout(() => {
if (window && !window.isDestroyed()) {
window.setAlwaysOnTop(false);
window.hide();
}
settingsHideTimer = null;
}, 200);
window.__lockedByButton = false;
}
});
ipcMain.on('cancel-hide-window', (event, name) => {
if (name === 'settings' && settingsHideTimer) {
ipcMain.on('cancel-hide-settings-window', (event) => {
if (settingsHideTimer) {
clearTimeout(settingsHideTimer);
settingsHideTimer = null;
}
});
ipcMain.handle('hide-all', () => {
windowPool.forEach(win => {
if (win.isFocused()) return;
win.hide();
});
});
ipcMain.handle('quit-application', () => {
app.quit();
});
ipcMain.handle('is-window-visible', (event, windowName) => {
ipcMain.handle('is-ask-window-visible', (event, windowName) => {
const window = windowPool.get(windowName);
if (window && !window.isDestroyed()) {
return window.isVisible();
@ -882,15 +821,6 @@ function setupIpcHandlers(movementManager) {
destroyFeatureWindows();
}
loadAndRegisterShortcuts(movementManager);
for (const [name, win] of windowPool) {
if (!isAllowed(name) && !win.isDestroyed()) {
win.hide();
}
if (isAllowed(name) && win.isVisible()) {
win.show();
}
}
});
ipcMain.on('update-keybinds', (event, newKeybinds) => {
@ -963,9 +893,6 @@ function setupIpcHandlers(movementManager) {
setupApiKeyIPC();
ipcMain.handle('resize-window', () => {});
ipcMain.handle('resize-for-view', () => {});
ipcMain.handle('resize-header-window', (event, { width, height }) => {
const header = windowPool.get('header');
@ -1018,74 +945,19 @@ function setupIpcHandlers(movementManager) {
return { success: false, error: 'Header window not found' };
});
ipcMain.on('header-animation-complete', (event, state) => {
ipcMain.on('header-animation-finished', (event, state) => {
const header = windowPool.get('header');
if (!header) return;
if (!header || header.isDestroyed()) return;
if (state === 'hidden') {
header.hide();
console.log('[WindowManager] Header hidden after animation.');
} else if (state === 'visible') {
lastVisibleWindows.forEach(name => {
if (name === 'header') return;
const win = windowPool.get(name);
if (win) win.show();
});
setImmediate(updateLayout);
setTimeout(updateLayout, 120);
}
});
ipcMain.handle('get-header-position', () => {
const header = windowPool.get('header');
if (header) {
const [x, y] = header.getPosition();
return { x, y };
}
return { x: 0, y: 0 };
});
ipcMain.handle('move-header', (event, newX, newY) => {
const header = windowPool.get('header');
if (header) {
const currentY = newY !== undefined ? newY : header.getBounds().y;
header.setPosition(newX, currentY, false);
console.log('[WindowManager] Header shown after animation.');
updateLayout();
}
});
ipcMain.handle('move-header-to', (event, newX, newY) => {
const header = windowPool.get('header');
if (header) {
const targetDisplay = screen.getDisplayNearestPoint({ x: newX, y: newY });
const { x: workAreaX, y: workAreaY, width, height } = targetDisplay.workArea;
const headerBounds = header.getBounds();
// Only clamp if the new position would actually go out of bounds
// This prevents progressive restriction of movement
let clampedX = newX;
let clampedY = newY;
// Check if we need to clamp X position
if (newX < workAreaX) {
clampedX = workAreaX;
} else if (newX + headerBounds.width > workAreaX + width) {
clampedX = workAreaX + width - headerBounds.width;
}
// Check if we need to clamp Y position
if (newY < workAreaY) {
clampedY = workAreaY;
} else if (newY + headerBounds.height > workAreaY + height) {
clampedY = workAreaY + height - headerBounds.height;
}
header.setPosition(clampedX, clampedY, false);
updateLayout();
}
});
ipcMain.handle('move-window-step', (event, direction) => {
if (movementManager) {
@ -1099,13 +971,6 @@ function setupIpcHandlers(movementManager) {
console.log(`[WindowManager] Force closing window: ${windowName}`);
window.webContents.send('window-hide-animation');
setTimeout(() => {
if (!window.isDestroyed()) {
window.hide();
updateLayout();
}
}, 250);
}
});
@ -1399,7 +1264,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan
if (state === 'apikey') {
if (keybinds.toggleVisibility) {
try {
globalShortcut.register(keybinds.toggleVisibility, () => toggleAllWindowsVisibility(movementManager));
globalShortcut.register(keybinds.toggleVisibility, () => toggleAllWindowsVisibility());
} catch (error) {
console.error(`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`, error);
}
@ -1435,7 +1300,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan
let callback;
switch(action) {
case 'toggleVisibility':
callback = () => toggleAllWindowsVisibility(movementManager);
callback = () => toggleAllWindowsVisibility();
break;
case 'nextStep':
callback = () => {
@ -1605,9 +1470,6 @@ module.exports = {
createWindows,
windowPool,
fixedYPosition,
//////// before_modelStateService ////////
// setApiKey,
//////// before_modelStateService ////////
getStoredApiKey,
getStoredProvider,
getCurrentModelInfo,

View File

@ -41,56 +41,6 @@ export class AskView extends LitElement {
pointer-events: none;
}
@keyframes slideUp {
0% {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0px);
}
30% {
opacity: 0.7;
transform: translateY(-20%) scale(0.98);
filter: blur(0.5px);
}
70% {
opacity: 0.3;
transform: translateY(-80%) scale(0.92);
filter: blur(1.5px);
}
100% {
opacity: 0;
transform: translateY(-150%) scale(0.85);
filter: blur(2px);
}
}
@keyframes slideDown {
0% {
opacity: 0;
transform: translateY(-150%) scale(0.85);
filter: blur(2px);
}
30% {
opacity: 0.5;
transform: translateY(-50%) scale(0.92);
filter: blur(1px);
}
65% {
opacity: 0.9;
transform: translateY(-5%) scale(0.99);
filter: blur(0.2px);
}
85% {
opacity: 0.98;
transform: translateY(2%) scale(1.005);
filter: blur(0px);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0px);
}
}
* {
font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
@ -252,20 +202,6 @@ export class AskView extends LitElement {
animation: fadeInOut 0.3s ease-in-out;
}
@keyframes fadeInOut {
0% {
opacity: 1;
transform: translateY(0);
}
50% {
opacity: 0;
transform: translateY(-10px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.header-right {
display: flex;
@ -422,19 +358,6 @@ export class AskView extends LitElement {
animation-delay: 0.4s;
}
@keyframes pulse {
0%,
80%,
100% {
opacity: 0.3;
transform: scale(0.8);
}
40% {
opacity: 1;
transform: scale(1.2);
}
}
.response-line {
position: relative;
padding: 2px 0;
@ -492,7 +415,7 @@ export class AskView extends LitElement {
background: rgba(0, 0, 0, 0.1);
border-top: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
transition: all 0.3s ease-in-out;
transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out;
transform-origin: bottom;
}
@ -596,42 +519,6 @@ export class AskView extends LitElement {
color: rgba(255, 255, 255, 0.5);
font-size: 14px;
}
/* ────────────────[ GLASS BYPASS ]─────────────── */
:host-context(body.has-glass) .ask-container,
:host-context(body.has-glass) .response-header,
:host-context(body.has-glass) .response-icon,
:host-context(body.has-glass) .copy-button,
:host-context(body.has-glass) .close-button,
:host-context(body.has-glass) .line-copy-button,
:host-context(body.has-glass) .text-input-container,
:host-context(body.has-glass) .response-container pre,
:host-context(body.has-glass) .response-container p code,
:host-context(body.has-glass) .response-container pre code {
background: transparent !important;
border: none !important;
outline: none !important;
box-shadow: none !important;
filter: none !important;
backdrop-filter: none !important;
}
:host-context(body.has-glass) .ask-container::before {
display: none !important;
}
:host-context(body.has-glass) .copy-button:hover,
:host-context(body.has-glass) .close-button:hover,
:host-context(body.has-glass) .line-copy-button,
:host-context(body.has-glass) .line-copy-button:hover,
:host-context(body.has-glass) .response-line:hover {
background: transparent !important;
}
:host-context(body.has-glass) .response-container::-webkit-scrollbar-track,
:host-context(body.has-glass) .response-container::-webkit-scrollbar-thumb {
background: transparent !important;
}
`;
constructor() {
@ -654,6 +541,7 @@ export class AskView extends LitElement {
this.handleStreamChunk = this.handleStreamChunk.bind(this);
this.handleStreamEnd = this.handleStreamEnd.bind(this);
this.handleSendText = this.handleSendText.bind(this);
this.handleGlobalSendRequest = this.handleGlobalSendRequest.bind(this);
this.handleTextKeydown = this.handleTextKeydown.bind(this);
this.closeResponsePanel = this.closeResponsePanel.bind(this);
this.handleCopy = this.handleCopy.bind(this);
@ -669,7 +557,6 @@ export class AskView extends LitElement {
this.loadLibraries();
// --- Resize helpers ---
this.adjustHeightThrottle = null;
this.isThrottled = false;
}
@ -1333,6 +1220,14 @@ export class AskView extends LitElement {
handleGlobalSendRequest() {
const textInput = this.shadowRoot?.getElementById('textInput');
if (!this.showTextInput) {
this.showTextInput = true;
this.requestUpdate();
this.focusTextInput();
return;
}
if (!textInput) return;
textInput.focus();
@ -1462,12 +1357,11 @@ export class AskView extends LitElement {
adjustWindowHeightThrottled() {
if (this.isThrottled) return;
this.adjustWindowHeight();
this.isThrottled = true;
this.adjustHeightThrottle = setTimeout(() => {
requestAnimationFrame(() => {
this.adjustWindowHeight();
this.isThrottled = false;
}, 16);
});
}
}

View File

@ -27,56 +27,6 @@ export class AssistantView extends LitElement {
pointer-events: none;
}
@keyframes slideUp {
0% {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0px);
}
30% {
opacity: 0.7;
transform: translateY(-20%) scale(0.98);
filter: blur(0.5px);
}
70% {
opacity: 0.3;
transform: translateY(-80%) scale(0.92);
filter: blur(1.5px);
}
100% {
opacity: 0;
transform: translateY(-150%) scale(0.85);
filter: blur(2px);
}
}
@keyframes slideDown {
0% {
opacity: 0;
transform: translateY(-150%) scale(0.85);
filter: blur(2px);
}
30% {
opacity: 0.5;
transform: translateY(-50%) scale(0.92);
filter: blur(1px);
}
65% {
opacity: 0.9;
transform: translateY(-5%) scale(0.99);
filter: blur(0.2px);
}
85% {
opacity: 0.98;
transform: translateY(2%) scale(1.005);
filter: blur(0px);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0px);
}
}
* {
font-family: 'Helvetica Neue', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
@ -245,17 +195,6 @@ export class AssistantView extends LitElement {
animation: slideIn 0.3s ease forwards;
}
@keyframes slideIn {
from {
transform: translateX(10%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.bar-controls {
display: flex;
gap: 4px;
@ -349,134 +288,6 @@ export class AssistantView extends LitElement {
font-size: 10px;
color: rgba(255, 255, 255, 0.7);
}
/* ────────────────[ GLASS BYPASS ]─────────────── */
:host-context(body.has-glass) .assistant-container,
:host-context(body.has-glass) .top-bar,
:host-context(body.has-glass) .toggle-button,
:host-context(body.has-glass) .copy-button,
:host-context(body.has-glass) .transcription-container,
:host-context(body.has-glass) .insights-container,
:host-context(body.has-glass) .stt-message,
:host-context(body.has-glass) .outline-item,
:host-context(body.has-glass) .request-item,
:host-context(body.has-glass) .markdown-content,
:host-context(body.has-glass) .insights-container pre,
:host-context(body.has-glass) .insights-container p code,
:host-context(body.has-glass) .insights-container pre code {
background: transparent !important;
border: none !important;
outline: none !important;
box-shadow: none !important;
filter: none !important;
backdrop-filter: none !important;
}
:host-context(body.has-glass) .assistant-container::before,
:host-context(body.has-glass) .assistant-container::after {
display: none !important;
}
:host-context(body.has-glass) .toggle-button:hover,
:host-context(body.has-glass) .copy-button:hover,
:host-context(body.has-glass) .outline-item:hover,
:host-context(body.has-glass) .request-item.clickable:hover,
:host-context(body.has-glass) .markdown-content:hover {
background: transparent !important;
transform: none !important;
}
:host-context(body.has-glass) .transcription-container::-webkit-scrollbar-track,
:host-context(body.has-glass) .transcription-container::-webkit-scrollbar-thumb,
:host-context(body.has-glass) .insights-container::-webkit-scrollbar-track,
:host-context(body.has-glass) .insights-container::-webkit-scrollbar-thumb {
background: transparent !important;
}
:host-context(body.has-glass) * {
animation: none !important;
transition: none !important;
transform: none !important;
filter: none !important;
backdrop-filter: none !important;
box-shadow: none !important;
}
:host-context(body.has-glass) .assistant-container,
:host-context(body.has-glass) .stt-message,
:host-context(body.has-glass) .toggle-button,
:host-context(body.has-glass) .copy-button {
border-radius: 0 !important;
}
:host-context(body.has-glass) ::-webkit-scrollbar,
:host-context(body.has-glass) ::-webkit-scrollbar-track,
:host-context(body.has-glass) ::-webkit-scrollbar-thumb {
background: transparent !important;
width: 0 !important; /* 스크롤바 자체 숨기기 */
}
:host-context(body.has-glass) .assistant-container,
:host-context(body.has-glass) .top-bar,
:host-context(body.has-glass) .toggle-button,
:host-context(body.has-glass) .copy-button,
:host-context(body.has-glass) .transcription-container,
:host-context(body.has-glass) .insights-container,
:host-context(body.has-glass) .stt-message,
:host-context(body.has-glass) .outline-item,
:host-context(body.has-glass) .request-item,
:host-context(body.has-glass) .markdown-content,
:host-context(body.has-glass) .insights-container pre,
:host-context(body.has-glass) .insights-container p code,
:host-context(body.has-glass) .insights-container pre code {
background: transparent !important;
border: none !important;
outline: none !important;
box-shadow: none !important;
filter: none !important;
backdrop-filter: none !important;
}
:host-context(body.has-glass) .assistant-container::before,
:host-context(body.has-glass) .assistant-container::after {
display: none !important;
}
:host-context(body.has-glass) .toggle-button:hover,
:host-context(body.has-glass) .copy-button:hover,
:host-context(body.has-glass) .outline-item:hover,
:host-context(body.has-glass) .request-item.clickable:hover,
:host-context(body.has-glass) .markdown-content:hover {
background: transparent !important;
transform: none !important;
}
:host-context(body.has-glass) .transcription-container::-webkit-scrollbar-track,
:host-context(body.has-glass) .transcription-container::-webkit-scrollbar-thumb,
:host-context(body.has-glass) .insights-container::-webkit-scrollbar-track,
:host-context(body.has-glass) .insights-container::-webkit-scrollbar-thumb {
background: transparent !important;
}
:host-context(body.has-glass) * {
animation: none !important;
transition: none !important;
transform: none !important;
filter: none !important;
backdrop-filter: none !important;
box-shadow: none !important;
}
:host-context(body.has-glass) .assistant-container,
:host-context(body.has-glass) .stt-message,
:host-context(body.has-glass) .toggle-button,
:host-context(body.has-glass) .copy-button {
border-radius: 0 !important;
}
:host-context(body.has-glass) ::-webkit-scrollbar,
:host-context(body.has-glass) ::-webkit-scrollbar-track,
:host-context(body.has-glass) ::-webkit-scrollbar-thumb {
background: transparent !important;
width: 0 !important;
}
`;
static properties = {

View File

@ -144,7 +144,6 @@ class ListenService {
console.log('✅ Listen service initialized successfully.');
this.sendToRenderer('session-state-changed', { isActive: true });
this.sendToRenderer('update-status', 'Connected. Ready to listen.');
return true;
@ -155,6 +154,7 @@ class ListenService {
} finally {
this.isInitializingSession = false;
this.sendToRenderer('session-initializing', false);
this.sendToRenderer('change-listen-capture-state', { status: "start" });
}
}
@ -179,6 +179,7 @@ class ListenService {
async closeSession() {
try {
this.sendToRenderer('change-listen-capture-state', { status: "stop" });
// Close STT sessions
await this.sttService.closeSessions();
@ -192,7 +193,6 @@ class ListenService {
this.currentSessionId = null;
this.summaryService.resetConversationHistory();
this.sendToRenderer('session-state-changed', { isActive: false });
this.sendToRenderer('session-did-close');
console.log('Listen service session closed.');
@ -282,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

@ -1,138 +1,30 @@
// renderer.js
const { ipcRenderer } = require('electron');
const listenCapture = require('./listenCapture.js');
const params = new URLSearchParams(window.location.search);
const isListenView = params.get('view') === 'listen';
let realtimeConversationHistory = [];
async function queryLoginState() {
const userState = await ipcRenderer.invoke('get-current-user');
return userState;
}
function pickleGlassElement() {
return document.getElementById('pickle-glass');
}
async function initializeopenai(profile = 'interview', language = 'en') {
// The API key is now handled in the main process from .env file.
// We just need to trigger the initialization.
try {
console.log(`Requesting OpenAI initialization with profile: ${profile}, language: ${language}`);
const success = await ipcRenderer.invoke('initialize-openai', profile, language);
if (success) {
// The status will be updated via 'update-status' event from the main process.
console.log('OpenAI initialization successful.');
} else {
console.error('OpenAI initialization failed.');
const appElement = pickleGlassElement();
if (appElement && typeof appElement.setStatus === 'function') {
appElement.setStatus('Initialization Failed');
}
}
} catch (error) {
console.error('Error during OpenAI initialization IPC call:', error);
const appElement = pickleGlassElement();
if (appElement && typeof appElement.setStatus === 'function') {
appElement.setStatus('Error');
}
}
}
// Listen for status updates
ipcRenderer.on('update-status', (event, status) => {
console.log('Status update:', status);
pickleGlass.e().setStatus(status);
});
// Listen for real-time STT updates
ipcRenderer.on('stt-update', (event, data) => {
console.log('Renderer.js stt-update', data);
const { speaker, text, isFinal, isPartial, timestamp } = data;
if (isPartial) {
console.log(`🔄 [${speaker} - partial]: ${text}`);
} else if (isFinal) {
console.log(`✅ [${speaker} - final]: ${text}`);
const speakerText = speaker.toLowerCase();
const conversationText = `${speakerText}: ${text.trim()}`;
realtimeConversationHistory.push(conversationText);
if (realtimeConversationHistory.length > 30) {
realtimeConversationHistory = realtimeConversationHistory.slice(-30);
}
console.log(`📝 Updated realtime conversation history: ${realtimeConversationHistory.length} texts`);
console.log(`📋 Latest text: ${conversationText}`);
}
if (pickleGlass.e() && typeof pickleGlass.e().updateRealtimeTranscription === 'function') {
pickleGlass.e().updateRealtimeTranscription({
speaker,
text,
isFinal,
isPartial,
timestamp,
});
}
});
ipcRenderer.on('update-structured-data', (_, structuredData) => {
console.log('📥 Received structured data update:', structuredData);
window.pickleGlass.structuredData = structuredData;
window.pickleGlass.setStructuredData(structuredData);
});
window.pickleGlass.structuredData = {
summary: [],
topic: { header: '', bullets: [] },
actions: [],
};
window.pickleGlass.setStructuredData = data => {
window.pickleGlass.structuredData = data;
pickleGlass.e()?.updateStructuredData?.(data);
};
function formatRealtimeConversationHistory() {
if (realtimeConversationHistory.length === 0) return 'No conversation history available.';
return realtimeConversationHistory.slice(-30).join('\n');
}
window.pickleGlass = {
initializeopenai,
startCapture: listenCapture.startCapture,
stopCapture: listenCapture.stopCapture,
isLinux: listenCapture.isLinux,
isMacOS: listenCapture.isMacOS,
captureManualScreenshot: listenCapture.captureManualScreenshot,
getCurrentScreenshot: listenCapture.getCurrentScreenshot,
e: pickleGlassElement,
};
// -------------------------------------------------------
// 🔔 React to session state changes from the main process
// When the session ends (isActive === false), ensure we stop
// all local capture pipelines (mic, screen, etc.).
// -------------------------------------------------------
ipcRenderer.on('session-state-changed', (_event, { isActive }) => {
if (!isActive) {
ipcRenderer.on('change-listen-capture-state', (_event, { status }) => {
if (!isListenView) {
console.log('[Renderer] Non-listen view: ignoring capture-state change');
return;
}
if (status === "stop") {
console.log('[Renderer] Session ended stopping local capture');
listenCapture.stopCapture();
} else {
console.log('[Renderer] New session started clearing in-memory history and summaries');
// Reset live conversation & analysis caches
realtimeConversationHistory = [];
const blankData = {
summary: [],
topic: { header: '', bullets: [] },
actions: [],
followUps: [],
};
window.pickleGlass.setStructuredData(blankData);
console.log('[Renderer] Session initialized starting local capture');
listenCapture.startCapture();
}
});

View File

@ -264,7 +264,7 @@ export class SummaryView extends LitElement {
super.connectedCallback();
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.on('update-structured-data', (event, data) => {
ipcRenderer.on('summary-update', (event, data) => {
this.structuredData = data;
this.requestUpdate();
});
@ -275,7 +275,7 @@ export class SummaryView extends LitElement {
super.disconnectedCallback();
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.removeAllListeners('update-structured-data');
ipcRenderer.removeAllListeners('summary-update');
}
}
@ -412,7 +412,7 @@ export class SummaryView extends LitElement {
const { ipcRenderer } = window.require('electron');
try {
const isAskViewVisible = await ipcRenderer.invoke('is-window-visible', 'ask');
const isAskViewVisible = await ipcRenderer.invoke('is-ask-window-visible', 'ask');
if (!isAskViewVisible) {
await ipcRenderer.invoke('toggle-feature', 'ask');

View File

@ -27,23 +27,6 @@ class SummaryService {
this.currentSessionId = sessionId;
}
// async getApiKey() {
// const storedKey = await getStoredApiKey();
// if (storedKey) {
// console.log('[SummaryService] Using stored API key');
// return storedKey;
// }
// const envKey = process.env.OPENAI_API_KEY;
// if (envKey) {
// console.log('[SummaryService] Using environment API key');
// return envKey;
// }
// console.error('[SummaryService] No API key found in storage or environment');
// return null;
// }
sendToRenderer(channel, data) {
BrowserWindow.getAllWindows().forEach(win => {
if (!win.isDestroyed()) {
@ -327,7 +310,7 @@ Keep all points concise and build upon previous analysis if provided.`,
.then(data => {
if (data) {
console.log('📤 Sending structured data to renderer');
this.sendToRenderer('update-structured-data', data);
this.sendToRenderer('summary-update', data);
// Notify callback
if (this.onAnalysisComplete) {

View File

@ -367,11 +367,6 @@ export class SettingsView extends LitElement {
margin-right: 6px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.hidden {
display: none;
}
@ -1030,14 +1025,14 @@ export class SettingsView extends LitElement {
handleMouseEnter = () => {
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.send('cancel-hide-window', 'settings');
ipcRenderer.send('cancel-hide-settings-window');
}
}
handleMouseLeave = () => {
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.send('hide-window', 'settings');
ipcRenderer.send('hide-settings-window');
}
}

View File

@ -199,7 +199,7 @@ app.whenReady().then(async () => {
await authService.initialize();
//////// after_modelStateService ////////
modelStateService.initialize();
await modelStateService.initialize();
//////// after_modelStateService ////////
listenService.setupIpcHandlers();
@ -396,21 +396,6 @@ function setupGeneralIpcHandlers() {
const userRepository = require('./common/repositories/user');
const presetRepository = require('./common/repositories/preset');
ipcMain.handle('save-api-key', (event, apiKey) => {
try {
// The adapter injects the UID and handles local/firebase logic.
// Assuming a default provider if not specified.
userRepository.saveApiKey(apiKey, 'openai');
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send('api-key-updated');
});
return { success: true };
} catch (error) {
console.error('IPC: Failed to save API key:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-user-presets', () => {
// The adapter injects the UID.
return presetRepository.getPresets();