Merge remote-tracking branch 'origin/main' into pr-82
This commit is contained in:
commit
338eef7112
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@ -41,4 +41,5 @@ jobs:
|
||||
SLACK_TITLE: "🚨 Build Failed"
|
||||
SLACK_MESSAGE: "😭 Build failed for `${{ github.repository }}` repo on main branch."
|
||||
SLACK_COLOR: 'danger'
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "aec"]
|
||||
path = aec
|
||||
url = https://github.com/samtiz/aec.git
|
@ -14,8 +14,6 @@
|
||||
|
||||
> This project is a fork of [CheatingDaddy](https://github.com/sohzm/cheating-daddy) with modifications and enhancements. Thanks to [Soham](https://x.com/soham_btw) and all the open-source contributors who made this possible!
|
||||
|
||||
> Currently, we're working on a full code refactor and modularization. Once that's completed, we'll jump into addressing the major issues. You can find WIP issues & changelog below this document.
|
||||
|
||||
🤖 **Fast, light & open-source**—Glass lives on your desktop, sees what you see, listens in real time, understands your context, and turns every moment into structured knowledge.
|
||||
|
||||
💬 **Proactive in meetings**—it surfaces action items, summaries, and answers the instant you need them.
|
||||
@ -117,15 +115,17 @@ We have a list of [help wanted](https://github.com/pickle-com/glass/issues?q=is%
|
||||
|
||||
| Status | Issue | Description |
|
||||
|--------|--------------------------------|---------------------------------------------------|
|
||||
| 🚧 WIP | Windows Build | Make Glass buildable & runnable in Windows |
|
||||
| 🚧 WIP | Local LLM Support | Supporting Local LLM to power AI answers |
|
||||
| 🚧 WIP | AEC Improvement | Transcription is not working occasionally |
|
||||
| 🚧 WIP | Firebase Data Storage Issue | Session & ask should be saved in firebase for signup users |
|
||||
| 🚧 WIP | Liquid Glass | Liquid Glass UI for MacOS 26 |
|
||||
|
||||
### Changelog
|
||||
|
||||
- Jul 5: Now support Gemini, Intel Mac supported
|
||||
- Jul 6: Full code refactoring has done.
|
||||
- Jul 7: Now support Claude, LLM/STT model selection
|
||||
- Jul 8: Now support Windows(beta), Improved AEC by Rust(to seperate mic/system audio), shortcut editing(beta)
|
||||
|
||||
|
||||
|
||||
## About Pickle
|
||||
|
1
aec
Submodule
1
aec
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit f00bb1fb948053c752b916adfee19f90644a0b2f
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pickle-glass",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pickle-glass",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.3",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
|
@ -1,7 +1,9 @@
|
||||
{
|
||||
"name": "pickle-glass",
|
||||
"productName": "Glass",
|
||||
"version": "0.2.2",
|
||||
|
||||
"version": "0.2.3",
|
||||
|
||||
"description": "Cl*ely for Free",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
@ -10,7 +12,7 @@
|
||||
"package": "npm run build:renderer && electron-forge package",
|
||||
"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",
|
||||
"build:win": "npm run build:all && electron-builder --win --x64 --publish never",
|
||||
"publish": "npm run build:all && electron-builder --config electron-builder.yml --publish always",
|
||||
"lint": "eslint --ext .ts,.tsx,.js .",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
@ -39,7 +41,6 @@
|
||||
"better-sqlite3": "^9.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.0.0",
|
||||
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.6.2",
|
||||
@ -74,4 +75,4 @@
|
||||
"optionalDependencies": {
|
||||
"electron-liquid-glass": "^1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,12 @@ 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 },
|
||||
shortcuts: { type: Object, state: true },
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
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;
|
||||
@ -99,7 +100,7 @@ export class MainHeader extends LitElement {
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
width: max-content;
|
||||
height: 47px;
|
||||
padding: 2px 10px 2px 13px;
|
||||
background: transparent;
|
||||
@ -212,16 +213,6 @@ export class MainHeader extends LitElement {
|
||||
}
|
||||
|
||||
.action-button,
|
||||
.settings-button {
|
||||
background: transparent;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
padding-bottom: 1px;
|
||||
justify-content: center;
|
||||
@ -275,9 +266,16 @@ export class MainHeader extends LitElement {
|
||||
.settings-button {
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
transition: background 0.15s ease;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
|
||||
.settings-button:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
@ -286,6 +284,7 @@ export class MainHeader extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.settings-icon svg {
|
||||
@ -346,6 +345,7 @@ export class MainHeader extends LitElement {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.shortcuts = {};
|
||||
this.dragState = null;
|
||||
this.wasJustDragged = false;
|
||||
this.isVisible = true;
|
||||
@ -501,6 +501,11 @@ export class MainHeader extends LitElement {
|
||||
this.isSessionActive = isActive;
|
||||
};
|
||||
ipcRenderer.on('session-state-changed', this._sessionStateListener);
|
||||
this._shortcutListener = (event, keybinds) => {
|
||||
console.log('[MainHeader] Received updated shortcuts:', keybinds);
|
||||
this.shortcuts = keybinds;
|
||||
};
|
||||
ipcRenderer.on('shortcuts-updated', this._shortcutListener);
|
||||
}
|
||||
}
|
||||
|
||||
@ -518,6 +523,9 @@ export class MainHeader extends LitElement {
|
||||
if (this._sessionStateListener) {
|
||||
ipcRenderer.removeListener('session-state-changed', this._sessionStateListener);
|
||||
}
|
||||
if (this._shortcutListener) {
|
||||
ipcRenderer.removeListener('shortcuts-updated', this._shortcutListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -567,6 +575,29 @@ export class MainHeader extends LitElement {
|
||||
|
||||
}
|
||||
|
||||
renderShortcut(accelerator) {
|
||||
if (!accelerator) return html``;
|
||||
|
||||
const keyMap = {
|
||||
'Cmd': '⌘', 'Command': '⌘',
|
||||
'Ctrl': '⌃', 'Control': '⌃',
|
||||
'Alt': '⌥', 'Option': '⌥',
|
||||
'Shift': '⇧',
|
||||
'Enter': '↵',
|
||||
'Backspace': '⌫',
|
||||
'Delete': '⌦',
|
||||
'Tab': '⇥',
|
||||
'Escape': '⎋',
|
||||
'Up': '↑', 'Down': '↓', 'Left': '←', 'Right': '→',
|
||||
'\\': html`<svg viewBox="0 0 6 12" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:6px; height:12px;"><path d="M1.5 1.3L5.1 10.6" stroke="white" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
||||
};
|
||||
|
||||
const keys = accelerator.split('+');
|
||||
return html`${keys.map(key => html`
|
||||
<div class="icon-box">${keyMap[key] || key}</div>
|
||||
`)}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="header" @mousedown=${this.handleMouseDown}>
|
||||
@ -599,14 +630,8 @@ export class MainHeader extends LitElement {
|
||||
<div class="action-text">
|
||||
<div class="action-text-content">Ask</div>
|
||||
</div>
|
||||
<div class="icon-container ask-icons">
|
||||
<div class="icon-box">⌘</div>
|
||||
<div class="icon-box">
|
||||
<svg viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.41797 8.16406C2.41797 8.00935 2.47943 7.86098 2.58882 7.75158C2.69822 7.64219 2.84659 7.58073 3.0013 7.58073H10.0013C10.4654 7.58073 10.9106 7.39636 11.2387 7.06817C11.5669 6.73998 11.7513 6.29486 11.7513 5.83073V3.4974C11.7513 3.34269 11.8128 3.19431 11.9222 3.08492C12.0316 2.97552 12.1799 2.91406 12.3346 2.91406C12.4893 2.91406 12.6377 2.97552 12.7471 3.08492C12.8565 3.19431 12.918 3.34269 12.918 3.4974V5.83073C12.918 6.60428 12.6107 7.34614 12.0637 7.89312C11.5167 8.44011 10.7748 8.7474 10.0013 8.7474H3.0013C2.84659 8.7474 2.69822 8.68594 2.58882 8.57654C2.47943 8.46715 2.41797 8.31877 2.41797 8.16406Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.58876 8.57973C2.4794 8.47034 2.41797 8.32199 2.41797 8.16731C2.41797 8.01263 2.4794 7.86429 2.58876 7.75489L4.92209 5.42156C5.03211 5.3153 5.17946 5.25651 5.33241 5.25783C5.48536 5.25916 5.63167 5.32051 5.73982 5.42867C5.84798 5.53682 5.90932 5.68313 5.91065 5.83608C5.91198 5.98903 5.85319 6.13638 5.74693 6.24639L3.82601 8.16731L5.74693 10.0882C5.80264 10.142 5.84708 10.2064 5.87765 10.2776C5.90823 10.3487 5.92432 10.4253 5.92499 10.5027C5.92566 10.5802 5.9109 10.657 5.88157 10.7287C5.85224 10.8004 5.80893 10.8655 5.75416 10.9203C5.69939 10.9751 5.63426 11.0184 5.56257 11.0477C5.49088 11.077 5.41406 11.0918 5.33661 11.0911C5.25916 11.0905 5.18261 11.0744 5.11144 11.0438C5.04027 11.0132 4.9759 10.9688 4.92209 10.9131L2.58876 8.57973Z" fill="white"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="icon-container">
|
||||
${this.renderShortcut(this.shortcuts.nextStep)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -614,13 +639,8 @@ export class MainHeader extends LitElement {
|
||||
<div class="action-text">
|
||||
<div class="action-text-content">Show/Hide</div>
|
||||
</div>
|
||||
<div class="icon-container showhide-icons">
|
||||
<div class="icon-box">⌘</div>
|
||||
<div class="icon-box">
|
||||
<svg viewBox="0 0 6 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.50391 1.32812L5.16391 10.673" stroke="white" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="icon-container">
|
||||
${this.renderShortcut(this.shortcuts.toggleVisibility)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -2,6 +2,7 @@ import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';
|
||||
import { SettingsView } from '../features/settings/SettingsView.js';
|
||||
import { AssistantView } from '../features/listen/AssistantView.js';
|
||||
import { AskView } from '../features/ask/AskView.js';
|
||||
import { ShortcutSettingsView } from '../features/settings/ShortCutSettingsView.js';
|
||||
|
||||
import '../features/listen/renderer/renderer.js';
|
||||
|
||||
@ -268,6 +269,8 @@ export class PickleGlassApp extends LitElement {
|
||||
.onProfileChange=${profile => (this.selectedProfile = profile)}
|
||||
.onLanguageChange=${lang => (this.selectedLanguage = lang)}
|
||||
></settings-view>`;
|
||||
case 'shortcut-settings':
|
||||
return html`<shortcut-settings-view></shortcut-settings-view>`;
|
||||
case 'history':
|
||||
return html`<history-view></history-view>`;
|
||||
case 'help':
|
||||
|
20
src/assets/aec.js
Normal file
20
src/assets/aec.js
Normal file
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
||||
const { GoogleGenerativeAI } = require('@google/generative-ai');
|
||||
const { GoogleGenAI } = require('@google/genai');
|
||||
const { GoogleGenerativeAI } = require("@google/generative-ai")
|
||||
const { GoogleGenAI } = require("@google/genai")
|
||||
|
||||
/**
|
||||
* Creates a Gemini STT session
|
||||
@ -9,322 +9,294 @@ const { GoogleGenAI } = require('@google/genai');
|
||||
* @param {object} [opts.callbacks] - Event callbacks
|
||||
* @returns {Promise<object>} STT session
|
||||
*/
|
||||
async function createSTT({ apiKey, language = 'en-US', callbacks = {}, ...config }) {
|
||||
const liveClient = new GoogleGenAI({ vertexai: false, apiKey });
|
||||
async function createSTT({ apiKey, language = "en-US", callbacks = {}, ...config }) {
|
||||
const liveClient = new GoogleGenAI({ vertexai: false, apiKey })
|
||||
|
||||
// Language code BCP-47 conversion
|
||||
const lang = language.includes('-') ? language : `${language}-US`;
|
||||
const lang = language.includes("-") ? language : `${language}-US`
|
||||
|
||||
const session = await liveClient.live.connect({
|
||||
|
||||
model: 'gemini-live-2.5-flash-preview',
|
||||
callbacks,
|
||||
callbacks: {
|
||||
...callbacks,
|
||||
onMessage: (msg) => {
|
||||
if (!msg || typeof msg !== 'object') return;
|
||||
msg.provider = 'gemini';
|
||||
callbacks.onmessage?.(msg);
|
||||
}
|
||||
},
|
||||
|
||||
config: {
|
||||
inputAudioTranscription: {},
|
||||
speechConfig: { languageCode: lang },
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
return {
|
||||
sendRealtimeInput: async payload => session.sendRealtimeInput(payload),
|
||||
sendRealtimeInput: async (payload) => session.sendRealtimeInput(payload),
|
||||
close: async () => session.close(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Gemini LLM instance
|
||||
* @param {object} opts - Configuration options
|
||||
* @param {string} opts.apiKey - Gemini API key
|
||||
* @param {string} [opts.model='gemini-2.5-flash'] - Model name
|
||||
* @param {number} [opts.temperature=0.7] - Temperature
|
||||
* @param {number} [opts.maxTokens=8192] - Max tokens
|
||||
* @returns {object} LLM instance
|
||||
* Creates a Gemini LLM instance with proper text response handling
|
||||
*/
|
||||
function createLLM({ apiKey, model = 'gemini-2.5-flash', temperature = 0.7, maxTokens = 8192, ...config }) {
|
||||
const client = new GoogleGenerativeAI(apiKey);
|
||||
|
||||
function createLLM({ apiKey, model = "gemini-2.5-flash", temperature = 0.7, maxTokens = 8192, ...config }) {
|
||||
const client = new GoogleGenerativeAI(apiKey)
|
||||
|
||||
return {
|
||||
generateContent: async (parts) => {
|
||||
const geminiModel = client.getGenerativeModel({ model: model });
|
||||
|
||||
let systemPrompt = '';
|
||||
let userContent = [];
|
||||
|
||||
const geminiModel = client.getGenerativeModel({
|
||||
model: model,
|
||||
generationConfig: {
|
||||
temperature,
|
||||
maxOutputTokens: maxTokens,
|
||||
// Ensure we get text responses, not JSON
|
||||
responseMimeType: "text/plain",
|
||||
},
|
||||
})
|
||||
|
||||
const systemPrompt = ""
|
||||
const userContent = []
|
||||
|
||||
for (const part of parts) {
|
||||
if (typeof part === 'string') {
|
||||
if (systemPrompt === '' && part.includes('You are')) {
|
||||
systemPrompt = part;
|
||||
} else {
|
||||
userContent.push(part);
|
||||
}
|
||||
if (typeof part === "string") {
|
||||
// Don't automatically assume strings starting with "You are" are system prompts
|
||||
// Check if it's explicitly marked as a system instruction
|
||||
userContent.push(part)
|
||||
} else if (part.inlineData) {
|
||||
// Convert base64 image data to Gemini format
|
||||
userContent.push({
|
||||
inlineData: {
|
||||
mimeType: part.inlineData.mimeType,
|
||||
data: part.inlineData.data
|
||||
}
|
||||
});
|
||||
data: part.inlineData.data,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare content array
|
||||
const content = [];
|
||||
|
||||
// Add system instruction if present
|
||||
if (systemPrompt) {
|
||||
// For Gemini, we'll prepend system prompt to user content
|
||||
content.push(systemPrompt + '\n\n' + userContent[0]);
|
||||
content.push(...userContent.slice(1));
|
||||
} else {
|
||||
content.push(...userContent);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const result = await geminiModel.generateContent(content);
|
||||
const response = await result.response;
|
||||
|
||||
const result = await geminiModel.generateContent(userContent)
|
||||
const response = await result.response
|
||||
|
||||
// Return plain text, not wrapped in JSON structure
|
||||
return {
|
||||
response: {
|
||||
text: () => response.text()
|
||||
}
|
||||
};
|
||||
text: () => response.text(),
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Gemini API error:', error);
|
||||
throw error;
|
||||
console.error("Gemini API error:", error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// For compatibility with chat-style interfaces
|
||||
|
||||
chat: async (messages) => {
|
||||
// Extract system instruction if present
|
||||
let systemInstruction = '';
|
||||
const history = [];
|
||||
let lastMessage;
|
||||
// Filter out any system prompts that might be causing JSON responses
|
||||
let systemInstruction = ""
|
||||
const history = []
|
||||
let lastMessage
|
||||
|
||||
messages.forEach((msg, index) => {
|
||||
if (msg.role === 'system') {
|
||||
systemInstruction = msg.content;
|
||||
return;
|
||||
if (msg.role === "system") {
|
||||
// Clean system instruction - avoid JSON formatting requests
|
||||
systemInstruction = msg.content
|
||||
.replace(/respond in json/gi, "")
|
||||
.replace(/format.*json/gi, "")
|
||||
.replace(/return.*json/gi, "")
|
||||
|
||||
// Add explicit instruction for natural text
|
||||
if (!systemInstruction.includes("respond naturally")) {
|
||||
systemInstruction += "\n\nRespond naturally in plain text, not in JSON or structured format."
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Gemini's history format
|
||||
const role = msg.role === 'user' ? 'user' : 'model';
|
||||
|
||||
const role = msg.role === "user" ? "user" : "model"
|
||||
|
||||
if (index === messages.length - 1) {
|
||||
lastMessage = msg;
|
||||
lastMessage = msg
|
||||
} else {
|
||||
history.push({ role, parts: [{ text: msg.content }] });
|
||||
history.push({ role, parts: [{ text: msg.content }] })
|
||||
}
|
||||
});
|
||||
|
||||
const geminiModel = client.getGenerativeModel({
|
||||
})
|
||||
|
||||
const geminiModel = client.getGenerativeModel({
|
||||
model: model,
|
||||
systemInstruction: systemInstruction
|
||||
});
|
||||
|
||||
const chat = geminiModel.startChat({
|
||||
history: history,
|
||||
systemInstruction:
|
||||
systemInstruction ||
|
||||
"Respond naturally in plain text format. Do not use JSON or structured responses unless specifically requested.",
|
||||
generationConfig: {
|
||||
temperature: temperature,
|
||||
maxOutputTokens: maxTokens,
|
||||
}
|
||||
});
|
||||
|
||||
// Get the last user message content
|
||||
let content = lastMessage.content;
|
||||
|
||||
// Handle multimodal content for the last message
|
||||
// Force plain text responses
|
||||
responseMimeType: "text/plain",
|
||||
},
|
||||
})
|
||||
|
||||
const chat = geminiModel.startChat({
|
||||
history: history,
|
||||
})
|
||||
|
||||
let content = lastMessage.content
|
||||
|
||||
// Handle multimodal content
|
||||
if (Array.isArray(content)) {
|
||||
const geminiContent = [];
|
||||
const geminiContent = []
|
||||
for (const part of content) {
|
||||
if (typeof part === 'string') {
|
||||
geminiContent.push(part);
|
||||
} else if (part.type === 'text') {
|
||||
geminiContent.push(part.text);
|
||||
} else if (part.type === 'image_url' && part.image_url) {
|
||||
// Convert base64 image to Gemini format
|
||||
const base64Data = part.image_url.url.split(',')[1];
|
||||
if (typeof part === "string") {
|
||||
geminiContent.push(part)
|
||||
} else if (part.type === "text") {
|
||||
geminiContent.push(part.text)
|
||||
} else if (part.type === "image_url" && part.image_url) {
|
||||
const base64Data = part.image_url.url.split(",")[1]
|
||||
geminiContent.push({
|
||||
inlineData: {
|
||||
mimeType: 'image/png',
|
||||
data: base64Data
|
||||
}
|
||||
});
|
||||
mimeType: "image/png",
|
||||
data: base64Data,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
content = geminiContent;
|
||||
content = geminiContent
|
||||
}
|
||||
|
||||
const result = await chat.sendMessage(content);
|
||||
const response = await result.response;
|
||||
|
||||
const result = await chat.sendMessage(content)
|
||||
const response = await result.response
|
||||
|
||||
// Return plain text content
|
||||
return {
|
||||
content: response.text(),
|
||||
raw: result
|
||||
};
|
||||
}
|
||||
};
|
||||
raw: result,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Gemini streaming LLM instance
|
||||
* @param {object} opts - Configuration options
|
||||
* @param {string} opts.apiKey - Gemini API key
|
||||
* @param {string} [opts.model='gemini-2.5-flash'] - Model name
|
||||
* @param {number} [opts.temperature=0.7] - Temperature
|
||||
* @param {number} [opts.maxTokens=8192] - Max tokens
|
||||
* @returns {object} Streaming LLM instance
|
||||
* Creates a Gemini streaming LLM instance with text response fix
|
||||
*/
|
||||
function createStreamingLLM({ apiKey, model = 'gemini-2.5-flash', temperature = 0.7, maxTokens = 8192, ...config }) {
|
||||
const client = new GoogleGenerativeAI(apiKey);
|
||||
|
||||
function createStreamingLLM({ apiKey, model = "gemini-2.5-flash", temperature = 0.7, maxTokens = 8192, ...config }) {
|
||||
const client = new GoogleGenerativeAI(apiKey)
|
||||
|
||||
return {
|
||||
streamChat: async (messages) => {
|
||||
console.log('[Gemini Provider] Starting streaming request');
|
||||
|
||||
// Extract system instruction if present
|
||||
let systemInstruction = '';
|
||||
const nonSystemMessages = [];
|
||||
|
||||
console.log("[Gemini Provider] Starting streaming request")
|
||||
|
||||
let systemInstruction = ""
|
||||
const nonSystemMessages = []
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role === 'system') {
|
||||
systemInstruction = msg.content;
|
||||
if (msg.role === "system") {
|
||||
// Clean and modify system instruction
|
||||
systemInstruction = msg.content
|
||||
.replace(/respond in json/gi, "")
|
||||
.replace(/format.*json/gi, "")
|
||||
.replace(/return.*json/gi, "")
|
||||
|
||||
if (!systemInstruction.includes("respond naturally")) {
|
||||
systemInstruction += "\n\nRespond naturally in plain text, not in JSON or structured format."
|
||||
}
|
||||
} else {
|
||||
nonSystemMessages.push(msg);
|
||||
nonSystemMessages.push(msg)
|
||||
}
|
||||
}
|
||||
|
||||
const geminiModel = client.getGenerativeModel({
|
||||
|
||||
const geminiModel = client.getGenerativeModel({
|
||||
model: model,
|
||||
systemInstruction: systemInstruction || undefined
|
||||
});
|
||||
|
||||
const chat = geminiModel.startChat({
|
||||
history: [],
|
||||
systemInstruction:
|
||||
systemInstruction ||
|
||||
"Respond naturally in plain text format. Do not use JSON or structured responses unless specifically requested.",
|
||||
generationConfig: {
|
||||
temperature,
|
||||
maxOutputTokens: maxTokens || 8192,
|
||||
}
|
||||
});
|
||||
|
||||
// Create a ReadableStream to handle Gemini's streaming
|
||||
// Force plain text responses
|
||||
responseMimeType: "text/plain",
|
||||
},
|
||||
})
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
console.log('[Gemini Provider] Processing messages:', nonSystemMessages.length, 'messages (excluding system)');
|
||||
|
||||
// Get the last user message
|
||||
const lastMessage = nonSystemMessages[nonSystemMessages.length - 1];
|
||||
let lastUserMessage = lastMessage.content;
|
||||
|
||||
// Handle case where content might be an array (multimodal)
|
||||
if (Array.isArray(lastUserMessage)) {
|
||||
// Extract text content from array
|
||||
const textParts = lastUserMessage.filter(part =>
|
||||
typeof part === 'string' || (part && part.type === 'text')
|
||||
);
|
||||
lastUserMessage = textParts.map(part =>
|
||||
typeof part === 'string' ? part : part.text
|
||||
).join(' ');
|
||||
}
|
||||
|
||||
console.log('[Gemini Provider] Sending message to Gemini:',
|
||||
typeof lastUserMessage === 'string' ? lastUserMessage.substring(0, 100) + '...' : 'multimodal content');
|
||||
|
||||
// Prepare the message content for Gemini
|
||||
let geminiContent = [];
|
||||
|
||||
// Handle multimodal content properly
|
||||
const lastMessage = nonSystemMessages[nonSystemMessages.length - 1]
|
||||
let geminiContent = []
|
||||
|
||||
if (Array.isArray(lastMessage.content)) {
|
||||
for (const part of lastMessage.content) {
|
||||
if (typeof part === 'string') {
|
||||
geminiContent.push(part);
|
||||
} else if (part.type === 'text') {
|
||||
geminiContent.push(part.text);
|
||||
} else if (part.type === 'image_url' && part.image_url) {
|
||||
// Convert base64 image to Gemini format
|
||||
const base64Data = part.image_url.url.split(',')[1];
|
||||
if (typeof part === "string") {
|
||||
geminiContent.push(part)
|
||||
} else if (part.type === "text") {
|
||||
geminiContent.push(part.text)
|
||||
} else if (part.type === "image_url" && part.image_url) {
|
||||
const base64Data = part.image_url.url.split(",")[1]
|
||||
geminiContent.push({
|
||||
inlineData: {
|
||||
mimeType: 'image/png',
|
||||
data: base64Data
|
||||
}
|
||||
});
|
||||
mimeType: "image/png",
|
||||
data: base64Data,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
geminiContent = [lastUserMessage];
|
||||
geminiContent = [lastMessage.content]
|
||||
}
|
||||
|
||||
console.log('[Gemini Provider] Prepared Gemini content:',
|
||||
geminiContent.length, 'parts');
|
||||
|
||||
// Stream the response
|
||||
let chunkCount = 0;
|
||||
let totalContent = '';
|
||||
|
||||
const contentParts = geminiContent.map(part => {
|
||||
if (typeof part === 'string') {
|
||||
return { text: part };
|
||||
|
||||
const contentParts = geminiContent.map((part) => {
|
||||
if (typeof part === "string") {
|
||||
return { text: part }
|
||||
} else if (part.inlineData) {
|
||||
return { inlineData: part.inlineData };
|
||||
return { inlineData: part.inlineData }
|
||||
}
|
||||
return part;
|
||||
});
|
||||
return part
|
||||
})
|
||||
|
||||
const result = await geminiModel.generateContentStream({
|
||||
contents: [{
|
||||
role: 'user',
|
||||
parts: contentParts
|
||||
}],
|
||||
generationConfig: {
|
||||
temperature,
|
||||
maxOutputTokens: maxTokens || 8192,
|
||||
}
|
||||
});
|
||||
|
||||
contents: [
|
||||
{
|
||||
role: "user",
|
||||
parts: contentParts,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
for await (const chunk of result.stream) {
|
||||
chunkCount++;
|
||||
const chunkText = chunk.text() || '';
|
||||
totalContent += chunkText;
|
||||
|
||||
// Format as SSE data
|
||||
const chunkText = chunk.text() || ""
|
||||
|
||||
// Format as SSE data - this should now be plain text
|
||||
const data = JSON.stringify({
|
||||
choices: [{
|
||||
delta: {
|
||||
content: chunkText
|
||||
}
|
||||
}]
|
||||
});
|
||||
controller.enqueue(new TextEncoder().encode(`data: ${data}\n\n`));
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
content: chunkText,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
controller.enqueue(new TextEncoder().encode(`data: ${data}\n\n`))
|
||||
}
|
||||
|
||||
console.log(`[Gemini Provider] Streamed ${chunkCount} chunks, total length: ${totalContent.length} chars`);
|
||||
|
||||
// Send the final done message
|
||||
controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n'));
|
||||
controller.close();
|
||||
console.log('[Gemini Provider] Streaming completed successfully');
|
||||
|
||||
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
|
||||
controller.close()
|
||||
} catch (error) {
|
||||
console.error('[Gemini Provider] Streaming error:', error);
|
||||
controller.error(error);
|
||||
console.error("[Gemini Provider] Streaming error:", error)
|
||||
controller.error(error)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create a Response object with the stream
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createSTT,
|
||||
createLLM,
|
||||
createStreamingLLM
|
||||
};
|
||||
createStreamingLLM,
|
||||
}
|
||||
|
@ -48,11 +48,11 @@ async function createSTT({ apiKey, language = 'en', callbacks = {}, usePortkey =
|
||||
turn_detection: {
|
||||
type: 'server_vad',
|
||||
threshold: 0.5,
|
||||
prefix_padding_ms: 50,
|
||||
silence_duration_ms: 25,
|
||||
prefix_padding_ms: 200,
|
||||
silence_duration_ms: 100,
|
||||
},
|
||||
input_audio_noise_reduction: {
|
||||
type: 'near_field'
|
||||
type: 'far_field'
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -72,6 +72,7 @@ async function createSTT({ apiKey, language = 'en', callbacks = {}, usePortkey =
|
||||
close: () => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'session.close' }));
|
||||
ws.onmessage = ws.onerror = () => {}; // 핸들러 제거
|
||||
ws.close(1000, 'Client initiated close.');
|
||||
}
|
||||
}
|
||||
@ -79,10 +80,17 @@ async function createSTT({ apiKey, language = 'en', callbacks = {}, usePortkey =
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
if (callbacks && callbacks.onmessage) {
|
||||
callbacks.onmessage(message);
|
||||
}
|
||||
// ── 종료·하트비트 패킷 필터링 ──────────────────────────────
|
||||
if (!event.data || event.data === 'null' || event.data === '[DONE]') return;
|
||||
|
||||
let msg;
|
||||
try { msg = JSON.parse(event.data); }
|
||||
catch { return; } // JSON 파싱 실패 무시
|
||||
|
||||
if (!msg || typeof msg !== 'object') return;
|
||||
|
||||
msg.provider = 'openai'; // ← 항상 명시
|
||||
callbacks.onmessage?.(msg);
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
|
@ -6,7 +6,8 @@ const LATEST_SCHEMA = {
|
||||
{ name: 'email', type: 'TEXT NOT NULL' },
|
||||
{ name: 'created_at', type: 'INTEGER' },
|
||||
{ name: 'api_key', type: 'TEXT' },
|
||||
{ name: 'provider', type: 'TEXT DEFAULT \'openai\'' }
|
||||
{ name: 'provider', type: 'TEXT DEFAULT \'openai\'' },
|
||||
{ name: 'auto_update_enabled', type: 'INTEGER DEFAULT 1' }
|
||||
]
|
||||
},
|
||||
sessions: {
|
||||
|
@ -37,56 +37,64 @@ class AuthService {
|
||||
this.currentUserMode = 'local'; // 'local' or 'firebase'
|
||||
this.currentUser = null;
|
||||
this.isInitialized = false;
|
||||
this.initializationPromise = null;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if (this.isInitialized) return;
|
||||
if (this.isInitialized) return this.initializationPromise;
|
||||
|
||||
const auth = getFirebaseAuth();
|
||||
onAuthStateChanged(auth, async (user) => {
|
||||
const previousUser = this.currentUser;
|
||||
this.initializationPromise = new Promise((resolve) => {
|
||||
const auth = getFirebaseAuth();
|
||||
onAuthStateChanged(auth, async (user) => {
|
||||
const previousUser = this.currentUser;
|
||||
|
||||
if (user) {
|
||||
// User signed IN
|
||||
console.log(`[AuthService] Firebase user signed in:`, user.uid);
|
||||
this.currentUser = user;
|
||||
this.currentUserId = user.uid;
|
||||
this.currentUserMode = 'firebase';
|
||||
if (user) {
|
||||
// User signed IN
|
||||
console.log(`[AuthService] Firebase user signed in:`, user.uid);
|
||||
this.currentUser = user;
|
||||
this.currentUserId = user.uid;
|
||||
this.currentUserMode = 'firebase';
|
||||
|
||||
// Start background task to fetch and save virtual key
|
||||
(async () => {
|
||||
try {
|
||||
const idToken = await user.getIdToken(true);
|
||||
const virtualKey = await getVirtualKeyByEmail(user.email, idToken);
|
||||
// Start background task to fetch and save virtual key
|
||||
(async () => {
|
||||
try {
|
||||
const idToken = await user.getIdToken(true);
|
||||
const virtualKey = await getVirtualKeyByEmail(user.email, idToken);
|
||||
|
||||
if (global.modelStateService) {
|
||||
global.modelStateService.setFirebaseVirtualKey(virtualKey);
|
||||
if (global.modelStateService) {
|
||||
global.modelStateService.setFirebaseVirtualKey(virtualKey);
|
||||
}
|
||||
console.log(`[AuthService] BG: Virtual key for ${user.email} has been processed.`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[AuthService] BG: Failed to fetch or save virtual key:', error);
|
||||
}
|
||||
console.log(`[AuthService] BG: Virtual key for ${user.email} has been processed.`);
|
||||
})();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[AuthService] BG: Failed to fetch or save virtual key:', error);
|
||||
}
|
||||
})();
|
||||
|
||||
} else {
|
||||
// User signed OUT
|
||||
console.log(`[AuthService] No Firebase user.`);
|
||||
if (previousUser) {
|
||||
console.log(`[AuthService] Clearing API key for logged-out user: ${previousUser.uid}`);
|
||||
if (global.modelStateService) {
|
||||
global.modelStateService.setFirebaseVirtualKey(null);
|
||||
} else {
|
||||
// User signed OUT
|
||||
console.log(`[AuthService] No Firebase user.`);
|
||||
if (previousUser) {
|
||||
console.log(`[AuthService] Clearing API key for logged-out user: ${previousUser.uid}`);
|
||||
if (global.modelStateService) {
|
||||
global.modelStateService.setFirebaseVirtualKey(null);
|
||||
}
|
||||
}
|
||||
this.currentUser = null;
|
||||
this.currentUserId = 'default_user';
|
||||
this.currentUserMode = 'local';
|
||||
}
|
||||
this.currentUser = null;
|
||||
this.currentUserId = 'default_user';
|
||||
this.currentUserMode = 'local';
|
||||
}
|
||||
this.broadcastUserState();
|
||||
this.broadcastUserState();
|
||||
|
||||
if (!this.isInitialized) {
|
||||
this.isInitialized = true;
|
||||
console.log('[AuthService] Initialized and resolved initialization promise.');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log('[AuthService] Initialized and attached to Firebase Auth state.');
|
||||
return this.initializationPromise;
|
||||
}
|
||||
|
||||
async signInWithCustomToken(token) {
|
||||
|
@ -67,6 +67,11 @@ class ModelStateService {
|
||||
};
|
||||
this.state = this.store.get(`users.${userId}`, defaultState);
|
||||
console.log(`[ModelStateService] State loaded for user: ${userId}`);
|
||||
for (const p of Object.keys(PROVIDERS)) {
|
||||
if (!(p in this.state.apiKeys)) {
|
||||
this.state.apiKeys[p] = null;
|
||||
}
|
||||
}
|
||||
this._autoSelectAvailableModels();
|
||||
this._saveState();
|
||||
this._logCurrentSelection();
|
||||
|
@ -11,7 +11,13 @@ const authService = require('../common/services/authService');
|
||||
const systemSettingsRepository = require('../common/repositories/systemSettings');
|
||||
const userRepository = require('../common/repositories/user');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const Store = require('electron-store');
|
||||
const shortCutStore = new Store({
|
||||
name: 'user-preferences',
|
||||
defaults: {
|
||||
customKeybinds: {}
|
||||
}
|
||||
});
|
||||
|
||||
/* ────────────────[ GLASS BYPASS ]─────────────── */
|
||||
let liquidGlass;
|
||||
@ -51,6 +57,7 @@ let settingsHideTimer = null;
|
||||
|
||||
let selectedCaptureSourceId = null;
|
||||
|
||||
// let shortcutEditorWindow = null;
|
||||
let layoutManager = null;
|
||||
function updateLayout() {
|
||||
if (layoutManager) {
|
||||
@ -60,16 +67,16 @@ function updateLayout() {
|
||||
|
||||
let movementManager = null;
|
||||
|
||||
let storedProvider = 'openai';
|
||||
|
||||
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) {
|
||||
if (windowPool.has('listen')) return;
|
||||
function createFeatureWindows(header, namesToCreate) {
|
||||
// if (windowPool.has('listen')) return;
|
||||
|
||||
const commonChildOptions = {
|
||||
parent: header,
|
||||
@ -84,106 +91,207 @@ function createFeatureWindows(header) {
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false },
|
||||
};
|
||||
|
||||
// listen
|
||||
const listen = new BrowserWindow({
|
||||
...commonChildOptions, width:400,minWidth:400,maxWidth:400,
|
||||
maxHeight:700,
|
||||
});
|
||||
listen.setContentProtection(isContentProtectionOn);
|
||||
listen.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
||||
if (process.platform === 'darwin') {
|
||||
listen.setWindowButtonVisibility(false);
|
||||
}
|
||||
const listenLoadOptions = { query: { view: 'listen' } };
|
||||
if (!shouldUseLiquidGlass) {
|
||||
listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions);
|
||||
}
|
||||
else {
|
||||
listenLoadOptions.query.glass = 'true';
|
||||
listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions);
|
||||
listen.webContents.once('did-finish-load', () => {
|
||||
const viewId = liquidGlass.addView(listen.getNativeWindowHandle(), {
|
||||
cornerRadius: 12,
|
||||
tintColor: '#FF00001A', // Red tint
|
||||
opaque: false,
|
||||
});
|
||||
if (viewId !== -1) {
|
||||
liquidGlass.unstable_setVariant(viewId, 2);
|
||||
// liquidGlass.unstable_setScrim(viewId, 1);
|
||||
// liquidGlass.unstable_setSubdued(viewId, 1);
|
||||
const createFeatureWindow = (name) => {
|
||||
if (windowPool.has(name)) return;
|
||||
|
||||
switch (name) {
|
||||
case 'listen': {
|
||||
const listen = new BrowserWindow({
|
||||
...commonChildOptions, width:400,minWidth:400,maxWidth:400,
|
||||
maxHeight:700,
|
||||
});
|
||||
listen.setContentProtection(isContentProtectionOn);
|
||||
listen.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
||||
if (process.platform === 'darwin') {
|
||||
listen.setWindowButtonVisibility(false);
|
||||
}
|
||||
const listenLoadOptions = { query: { view: 'listen' } };
|
||||
if (!shouldUseLiquidGlass) {
|
||||
listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions);
|
||||
}
|
||||
else {
|
||||
listenLoadOptions.query.glass = 'true';
|
||||
listen.loadFile(path.join(__dirname, '../app/content.html'), listenLoadOptions);
|
||||
listen.webContents.once('did-finish-load', () => {
|
||||
const viewId = liquidGlass.addView(listen.getNativeWindowHandle(), {
|
||||
cornerRadius: 12,
|
||||
tintColor: '#FF00001A', // Red tint
|
||||
opaque: false,
|
||||
});
|
||||
if (viewId !== -1) {
|
||||
liquidGlass.unstable_setVariant(viewId, 2);
|
||||
// liquidGlass.unstable_setScrim(viewId, 1);
|
||||
// liquidGlass.unstable_setSubdued(viewId, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
windowPool.set('listen', listen);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ask
|
||||
case 'ask': {
|
||||
const ask = new BrowserWindow({ ...commonChildOptions, width:600 });
|
||||
ask.setContentProtection(isContentProtectionOn);
|
||||
ask.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
||||
if (process.platform === 'darwin') {
|
||||
ask.setWindowButtonVisibility(false);
|
||||
}
|
||||
const askLoadOptions = { query: { view: 'ask' } };
|
||||
if (!shouldUseLiquidGlass) {
|
||||
ask.loadFile(path.join(__dirname, '../app/content.html'), askLoadOptions);
|
||||
}
|
||||
else {
|
||||
askLoadOptions.query.glass = 'true';
|
||||
ask.loadFile(path.join(__dirname, '../app/content.html'), askLoadOptions);
|
||||
ask.webContents.once('did-finish-load', () => {
|
||||
const viewId = liquidGlass.addView(ask.getNativeWindowHandle(), {
|
||||
cornerRadius: 12,
|
||||
tintColor: '#FF00001A', // Red tint
|
||||
opaque: false,
|
||||
});
|
||||
if (viewId !== -1) {
|
||||
liquidGlass.unstable_setVariant(viewId, 2);
|
||||
// liquidGlass.unstable_setScrim(viewId, 1);
|
||||
// liquidGlass.unstable_setSubdued(viewId, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
windowPool.set('listen', listen);
|
||||
|
||||
// ask
|
||||
const ask = new BrowserWindow({ ...commonChildOptions, width:600 });
|
||||
ask.setContentProtection(isContentProtectionOn);
|
||||
ask.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
||||
if (process.platform === 'darwin') {
|
||||
ask.setWindowButtonVisibility(false);
|
||||
}
|
||||
const askLoadOptions = { query: { view: 'ask' } };
|
||||
if (!shouldUseLiquidGlass) {
|
||||
ask.loadFile(path.join(__dirname, '../app/content.html'), askLoadOptions);
|
||||
}
|
||||
else {
|
||||
askLoadOptions.query.glass = 'true';
|
||||
ask.loadFile(path.join(__dirname, '../app/content.html'), askLoadOptions);
|
||||
ask.webContents.once('did-finish-load', () => {
|
||||
const viewId = liquidGlass.addView(ask.getNativeWindowHandle(), {
|
||||
cornerRadius: 12,
|
||||
tintColor: '#FF00001A', // Red tint
|
||||
opaque: false,
|
||||
});
|
||||
if (viewId !== -1) {
|
||||
liquidGlass.unstable_setVariant(viewId, 2);
|
||||
// liquidGlass.unstable_setScrim(viewId, 1);
|
||||
// liquidGlass.unstable_setSubdued(viewId, 1);
|
||||
ask.on('blur',()=>ask.webContents.send('window-blur'));
|
||||
|
||||
// Open DevTools in development
|
||||
if (!app.isPackaged) {
|
||||
ask.webContents.openDevTools({ mode: 'detach' });
|
||||
}
|
||||
windowPool.set('ask', ask);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ask.on('blur',()=>ask.webContents.send('window-blur'));
|
||||
|
||||
// Open DevTools in development
|
||||
if (!app.isPackaged) {
|
||||
ask.webContents.openDevTools({ mode: 'detach' });
|
||||
}
|
||||
windowPool.set('ask', ask);
|
||||
|
||||
// settings
|
||||
const settings = new BrowserWindow({ ...commonChildOptions, width:240, maxHeight:400, parent:undefined });
|
||||
settings.setContentProtection(isContentProtectionOn);
|
||||
settings.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
||||
if (process.platform === 'darwin') {
|
||||
settings.setWindowButtonVisibility(false);
|
||||
}
|
||||
const settingsLoadOptions = { query: { view: 'settings' } };
|
||||
if (!shouldUseLiquidGlass) {
|
||||
settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions)
|
||||
.catch(console.error);
|
||||
}
|
||||
else {
|
||||
settingsLoadOptions.query.glass = 'true';
|
||||
settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions)
|
||||
.catch(console.error);
|
||||
settings.webContents.once('did-finish-load', () => {
|
||||
const viewId = liquidGlass.addView(settings.getNativeWindowHandle(), {
|
||||
cornerRadius: 12,
|
||||
tintColor: '#FF00001A', // Red tint
|
||||
opaque: false,
|
||||
});
|
||||
if (viewId !== -1) {
|
||||
liquidGlass.unstable_setVariant(viewId, 2);
|
||||
// liquidGlass.unstable_setScrim(viewId, 1);
|
||||
// liquidGlass.unstable_setSubdued(viewId, 1);
|
||||
// settings
|
||||
case 'settings': {
|
||||
const settings = new BrowserWindow({ ...commonChildOptions, width:240, maxHeight:400, parent:undefined });
|
||||
settings.setContentProtection(isContentProtectionOn);
|
||||
settings.setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true});
|
||||
if (process.platform === 'darwin') {
|
||||
settings.setWindowButtonVisibility(false);
|
||||
}
|
||||
const settingsLoadOptions = { query: { view: 'settings' } };
|
||||
if (!shouldUseLiquidGlass) {
|
||||
settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions)
|
||||
.catch(console.error);
|
||||
}
|
||||
else {
|
||||
settingsLoadOptions.query.glass = 'true';
|
||||
settings.loadFile(path.join(__dirname,'../app/content.html'), settingsLoadOptions)
|
||||
.catch(console.error);
|
||||
settings.webContents.once('did-finish-load', () => {
|
||||
const viewId = liquidGlass.addView(settings.getNativeWindowHandle(), {
|
||||
cornerRadius: 12,
|
||||
tintColor: '#FF00001A', // Red tint
|
||||
opaque: false,
|
||||
});
|
||||
if (viewId !== -1) {
|
||||
liquidGlass.unstable_setVariant(viewId, 2);
|
||||
// liquidGlass.unstable_setScrim(viewId, 1);
|
||||
// liquidGlass.unstable_setSubdued(viewId, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
windowPool.set('settings', settings);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
case 'shortcut-settings': {
|
||||
const shortcutEditor = new BrowserWindow({
|
||||
...commonChildOptions,
|
||||
width: 420,
|
||||
height: 720,
|
||||
modal: false,
|
||||
parent: undefined,
|
||||
alwaysOnTop: true,
|
||||
titleBarOverlay: false,
|
||||
});
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
shortcutEditor.setAlwaysOnTop(true, 'screen-saver');
|
||||
} else {
|
||||
shortcutEditor.setAlwaysOnTop(true);
|
||||
}
|
||||
|
||||
/* ──────────[ ① 다른 창 클릭 차단 ]────────── */
|
||||
const disableClicks = () => {
|
||||
for (const [name, win] of windowPool) {
|
||||
if (win !== shortcutEditor && !win.isDestroyed()) {
|
||||
win.setIgnoreMouseEvents(true, { forward: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
const restoreClicks = () => {
|
||||
for (const [, win] of windowPool) {
|
||||
if (!win.isDestroyed()) win.setIgnoreMouseEvents(false);
|
||||
}
|
||||
};
|
||||
|
||||
const header = windowPool.get('header');
|
||||
if (header && !header.isDestroyed()) {
|
||||
const { x, y, width } = header.getBounds();
|
||||
shortcutEditor.setBounds({ x, y, width });
|
||||
}
|
||||
|
||||
shortcutEditor.once('ready-to-show', () => {
|
||||
disableClicks();
|
||||
shortcutEditor.show();
|
||||
});
|
||||
|
||||
const loadOptions = { query: { view: 'shortcut-settings' } };
|
||||
if (!shouldUseLiquidGlass) {
|
||||
shortcutEditor.loadFile(path.join(__dirname, '../app/content.html'), loadOptions);
|
||||
} else {
|
||||
loadOptions.query.glass = 'true';
|
||||
shortcutEditor.loadFile(path.join(__dirname, '../app/content.html'), loadOptions);
|
||||
shortcutEditor.webContents.once('did-finish-load', () => {
|
||||
const viewId = liquidGlass.addView(shortcutEditor.getNativeWindowHandle(), {
|
||||
cornerRadius: 12, tintColor: '#FF00001A', opaque: false,
|
||||
});
|
||||
if (viewId !== -1) {
|
||||
liquidGlass.unstable_setVariant(viewId, 2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
shortcutEditor.on('closed', () => {
|
||||
restoreClicks();
|
||||
windowPool.delete('shortcut-settings');
|
||||
console.log('[Shortcuts] Re-enabled after editing.');
|
||||
loadAndRegisterShortcuts(movementManager);
|
||||
});
|
||||
|
||||
shortcutEditor.webContents.once('dom-ready', async () => {
|
||||
const savedKeybinds = shortCutStore.get('customKeybinds', {});
|
||||
const defaultKeybinds = getDefaultKeybinds();
|
||||
const keybinds = { ...defaultKeybinds, ...savedKeybinds };
|
||||
shortcutEditor.webContents.send('load-shortcuts', keybinds);
|
||||
});
|
||||
|
||||
if (!app.isPackaged) {
|
||||
shortcutEditor.webContents.openDevTools({ mode: 'detach' });
|
||||
}
|
||||
windowPool.set('shortcut-settings', shortcutEditor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (Array.isArray(namesToCreate)) {
|
||||
namesToCreate.forEach(name => createFeatureWindow(name));
|
||||
} else if (typeof namesToCreate === 'string') {
|
||||
createFeatureWindow(namesToCreate);
|
||||
} else {
|
||||
createFeatureWindow('listen');
|
||||
createFeatureWindow('ask');
|
||||
createFeatureWindow('settings');
|
||||
}
|
||||
windowPool.set('settings', settings);
|
||||
}
|
||||
|
||||
function destroyFeatureWindows() {
|
||||
@ -199,6 +307,7 @@ function destroyFeatureWindows() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getCurrentDisplay(window) {
|
||||
if (!window || window.isDestroyed()) return screen.getPrimaryDisplay();
|
||||
|
||||
@ -354,7 +463,7 @@ function createWindows() {
|
||||
setupIpcHandlers(movementManager);
|
||||
|
||||
if (currentHeaderState === 'main') {
|
||||
createFeatureWindows(header);
|
||||
createFeatureWindows(header, ['listen', 'ask', 'settings', 'shortcut-settings']);
|
||||
}
|
||||
|
||||
header.setContentProtection(isContentProtectionOn);
|
||||
@ -385,10 +494,6 @@ function createWindows() {
|
||||
|
||||
header.on('resize', updateLayout);
|
||||
|
||||
// header.webContents.once('dom-ready', () => {
|
||||
// loadAndRegisterShortcuts();
|
||||
// });
|
||||
|
||||
ipcMain.handle('toggle-all-windows-visibility', () => toggleAllWindowsVisibility(movementManager));
|
||||
|
||||
ipcMain.handle('toggle-feature', async (event, featureName) => {
|
||||
@ -584,37 +689,32 @@ function createWindows() {
|
||||
}
|
||||
});
|
||||
|
||||
// setupIpcHandlers();
|
||||
|
||||
return windowPool;
|
||||
}
|
||||
|
||||
function loadAndRegisterShortcuts(movementManager) {
|
||||
if (windowPool.has('shortcut-settings')) {
|
||||
console.log('[Shortcuts] Editing in progress, skipping registration.');
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultKeybinds = getDefaultKeybinds();
|
||||
const header = windowPool.get('header');
|
||||
const savedKeybinds = shortCutStore.get('customKeybinds', {});
|
||||
const keybinds = { ...defaultKeybinds, ...savedKeybinds };
|
||||
|
||||
const sendToRenderer = (channel, ...args) => {
|
||||
windowPool.forEach(win => {
|
||||
try {
|
||||
if (win && !win.isDestroyed()) {
|
||||
if (win && !win.isDestroyed()) {
|
||||
try {
|
||||
win.webContents.send(channel, ...args);
|
||||
} catch (e) {
|
||||
// 창이 이미 닫혔을 수 있으므로 오류를 무시합니다.
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
if (!header) {
|
||||
return updateGlobalShortcuts(defaultKeybinds, undefined, sendToRenderer, movementManager);
|
||||
}
|
||||
|
||||
header.webContents
|
||||
.executeJavaScript(`(() => localStorage.getItem('customKeybinds'))()`)
|
||||
.then(saved => (saved ? JSON.parse(saved) : {}))
|
||||
.then(savedKeybinds => {
|
||||
const keybinds = { ...defaultKeybinds, ...savedKeybinds };
|
||||
updateGlobalShortcuts(keybinds, header, sendToRenderer, movementManager);
|
||||
})
|
||||
.catch(() => updateGlobalShortcuts(defaultKeybinds, header, sendToRenderer, movementManager));
|
||||
updateGlobalShortcuts(keybinds, windowPool.get('header'), sendToRenderer, movementManager);
|
||||
}
|
||||
|
||||
|
||||
@ -768,6 +868,7 @@ function setupIpcHandlers(movementManager) {
|
||||
} else { // 'apikey' | 'permission'
|
||||
destroyFeatureWindows();
|
||||
}
|
||||
loadAndRegisterShortcuts(movementManager);
|
||||
|
||||
for (const [name, win] of windowPool) {
|
||||
if (!isAllowed(name) && !win.isDestroyed()) {
|
||||
@ -777,36 +878,69 @@ function setupIpcHandlers(movementManager) {
|
||||
win.show();
|
||||
}
|
||||
}
|
||||
|
||||
const header = windowPool.get('header');
|
||||
if (header && !header.isDestroyed()) {
|
||||
header.webContents
|
||||
.executeJavaScript(`(() => localStorage.getItem('customKeybinds'))()`)
|
||||
.then(saved => {
|
||||
const defaultKeybinds = getDefaultKeybinds();
|
||||
const savedKeybinds = saved ? JSON.parse(saved) : {};
|
||||
const keybinds = { ...defaultKeybinds, ...savedKeybinds };
|
||||
|
||||
const sendToRenderer = (channel, ...args) => {
|
||||
windowPool.forEach(win => {
|
||||
try {
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send(channel, ...args);
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
};
|
||||
|
||||
updateGlobalShortcuts(keybinds, header, sendToRenderer, movementManager);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('update-keybinds', (event, newKeybinds) => {
|
||||
updateGlobalShortcuts(newKeybinds);
|
||||
});
|
||||
|
||||
ipcMain.handle('get-current-shortcuts', () => {
|
||||
const defaultKeybinds = getDefaultKeybinds();
|
||||
const savedKeybinds = shortCutStore.get('customKeybinds', {});
|
||||
return { ...defaultKeybinds, ...savedKeybinds };
|
||||
});
|
||||
|
||||
ipcMain.handle('open-shortcut-editor', () => {
|
||||
const header = windowPool.get('header');
|
||||
if (!header) return;
|
||||
|
||||
// 편집기 열기 전 모든 단축키 비활성화
|
||||
globalShortcut.unregisterAll();
|
||||
console.log('[Shortcuts] Disabled for editing.');
|
||||
|
||||
createFeatureWindows(header, 'shortcut-settings');
|
||||
});
|
||||
|
||||
ipcMain.handle('get-default-shortcuts', () => {
|
||||
shortCutStore.set('customKeybinds', {});
|
||||
return getDefaultKeybinds();
|
||||
});
|
||||
|
||||
ipcMain.handle('save-shortcuts', async (event, newKeybinds) => {
|
||||
try {
|
||||
const defaultKeybinds = getDefaultKeybinds();
|
||||
const customKeybinds = {};
|
||||
for (const key in newKeybinds) {
|
||||
if (newKeybinds[key] && newKeybinds[key] !== defaultKeybinds[key]) {
|
||||
customKeybinds[key] = newKeybinds[key];
|
||||
}
|
||||
}
|
||||
|
||||
shortCutStore.set('customKeybinds', customKeybinds);
|
||||
console.log('[Shortcuts] Custom keybinds saved to store:', customKeybinds);
|
||||
|
||||
const editor = windowPool.get('shortcut-settings');
|
||||
if (editor && !editor.isDestroyed()) {
|
||||
editor.close();
|
||||
} else {
|
||||
loadAndRegisterShortcuts(movementManager);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Failed to save shortcuts:", error);
|
||||
loadAndRegisterShortcuts(movementManager);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('close-shortcut-editor', () => {
|
||||
const editor = windowPool.get('shortcut-settings');
|
||||
if (editor && !editor.isDestroyed()) {
|
||||
editor.close();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('open-login-page', () => {
|
||||
const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000';
|
||||
const personalizeUrl = `${webUrl}/personalize?desktop=true`;
|
||||
@ -971,15 +1105,6 @@ function setupIpcHandlers(movementManager) {
|
||||
console.log('[WindowManager] Received request to log out.');
|
||||
|
||||
await authService.signOut();
|
||||
//////// before_modelStateService ////////
|
||||
// await setApiKey(null);
|
||||
|
||||
// windowPool.forEach(win => {
|
||||
// if (win && !win.isDestroyed()) {
|
||||
// win.webContents.send('api-key-removed');
|
||||
// }
|
||||
// });
|
||||
//////// before_modelStateService ////////
|
||||
});
|
||||
|
||||
ipcMain.handle('check-system-permissions', async () => {
|
||||
@ -1114,101 +1239,6 @@ function setupIpcHandlers(movementManager) {
|
||||
}
|
||||
|
||||
|
||||
//////// before_modelStateService ////////
|
||||
// async function setApiKey(apiKey, provider = 'openai') {
|
||||
// console.log('[WindowManager] Persisting API key and provider to DB');
|
||||
|
||||
// try {
|
||||
// await userRepository.saveApiKey(apiKey, authService.getCurrentUserId(), provider);
|
||||
// console.log('[WindowManager] API key and provider saved to SQLite');
|
||||
|
||||
// // Notify authService that the key status may have changed
|
||||
// await authService.updateApiKeyStatus();
|
||||
|
||||
// } catch (err) {
|
||||
// console.error('[WindowManager] Failed to save API key to SQLite:', err);
|
||||
// }
|
||||
|
||||
// windowPool.forEach(win => {
|
||||
// if (win && !win.isDestroyed()) {
|
||||
// const js = apiKey ? `
|
||||
// localStorage.setItem('openai_api_key', ${JSON.stringify(apiKey)});
|
||||
// localStorage.setItem('ai_provider', ${JSON.stringify(provider)});
|
||||
// ` : `
|
||||
// localStorage.removeItem('openai_api_key');
|
||||
// localStorage.removeItem('ai_provider');
|
||||
// `;
|
||||
// win.webContents.executeJavaScript(js).catch(() => {});
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
|
||||
// async function getStoredApiKey() {
|
||||
// const userId = authService.getCurrentUserId();
|
||||
// if (!userId) return null;
|
||||
// const user = await userRepository.getById(userId);
|
||||
// return user?.api_key || null;
|
||||
// }
|
||||
|
||||
// async function getStoredProvider() {
|
||||
// const userId = authService.getCurrentUserId();
|
||||
// if (!userId) return 'openai';
|
||||
// const user = await userRepository.getById(userId);
|
||||
// return user?.provider || 'openai';
|
||||
// }
|
||||
|
||||
// function setupApiKeyIPC() {
|
||||
// const { ipcMain } = require('electron');
|
||||
|
||||
// // Both handlers now do the same thing: fetch the key from the source of truth.
|
||||
// ipcMain.handle('get-stored-api-key', getStoredApiKey);
|
||||
|
||||
// ipcMain.handle('api-key-validated', async (event, data) => {
|
||||
// console.log('[WindowManager] API key validation completed, saving...');
|
||||
|
||||
// // Support both old format (string) and new format (object)
|
||||
// const apiKey = typeof data === 'string' ? data : data.apiKey;
|
||||
// const provider = typeof data === 'string' ? 'openai' : (data.provider || 'openai');
|
||||
|
||||
// await setApiKey(apiKey, provider);
|
||||
|
||||
// windowPool.forEach((win, name) => {
|
||||
// if (win && !win.isDestroyed()) {
|
||||
// win.webContents.send('api-key-validated', { apiKey, provider });
|
||||
// }
|
||||
// });
|
||||
|
||||
// return { success: true };
|
||||
// });
|
||||
|
||||
// ipcMain.handle('remove-api-key', async () => {
|
||||
// console.log('[WindowManager] API key removal requested');
|
||||
// await setApiKey(null);
|
||||
|
||||
// windowPool.forEach((win, name) => {
|
||||
// if (win && !win.isDestroyed()) {
|
||||
// win.webContents.send('api-key-removed');
|
||||
// }
|
||||
// });
|
||||
|
||||
// const settingsWindow = windowPool.get('settings');
|
||||
// if (settingsWindow && settingsWindow.isVisible()) {
|
||||
// settingsWindow.hide();
|
||||
// console.log('[WindowManager] Settings window hidden after clearing API key.');
|
||||
// }
|
||||
|
||||
// return { success: true };
|
||||
// });
|
||||
|
||||
// ipcMain.handle('get-ai-provider', getStoredProvider);
|
||||
|
||||
// console.log('[WindowManager] API key related IPC handlers registered (SQLite-backed)');
|
||||
// }
|
||||
//////// before_modelStateService ////////
|
||||
|
||||
|
||||
|
||||
|
||||
//////// after_modelStateService ////////
|
||||
async function getStoredApiKey() {
|
||||
@ -1227,15 +1257,15 @@ async function getStoredProvider() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 렌더러에서 요청한 타입('llm' 또는 'stt')에 대한 모델 정보를 반환합니다.
|
||||
* @param {IpcMainInvokeEvent} event - 일렉트론 IPC 이벤트 객체
|
||||
* @param {{type: 'llm' | 'stt'}} { type } - 요청할 모델 타입
|
||||
*
|
||||
* @param {IpcMainInvokeEvent} event
|
||||
* @param {{type: 'llm' | 'stt'}}
|
||||
*/
|
||||
async function getCurrentModelInfo(event, { type }) {
|
||||
if (global.modelStateService && (type === 'llm' || type === 'stt')) {
|
||||
return global.modelStateService.getCurrentModelInfo(type);
|
||||
}
|
||||
return null; // 서비스가 없거나 유효하지 않은 타입일 경우 null 반환
|
||||
return null;
|
||||
}
|
||||
|
||||
function setupApiKeyIPC() {
|
||||
@ -1279,33 +1309,26 @@ function getDefaultKeybinds() {
|
||||
}
|
||||
|
||||
function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementManager) {
|
||||
// console.log('Updating global shortcuts with:', keybinds);
|
||||
|
||||
// Unregister all existing shortcuts
|
||||
globalShortcut.unregisterAll();
|
||||
|
||||
let toggleVisibilityDebounceTimer = null;
|
||||
|
||||
if (sendToRenderer) {
|
||||
sendToRenderer('shortcuts-updated', keybinds);
|
||||
console.log('[Shortcuts] Broadcasted updated shortcuts to all windows.');
|
||||
}
|
||||
|
||||
// ✨ 하드코딩된 단축키 등록을 위해 변수 유지
|
||||
const isMac = process.platform === 'darwin';
|
||||
const modifier = isMac ? 'Cmd' : 'Ctrl';
|
||||
const header = windowPool.get('header');
|
||||
const state = header?.currentHeaderState || currentHeaderState;
|
||||
|
||||
if (keybinds.toggleVisibility) {
|
||||
try {
|
||||
globalShortcut.register(keybinds.toggleVisibility, () => toggleAllWindowsVisibility(movementManager));
|
||||
console.log(`Registered toggleVisibility: ${keybinds.toggleVisibility}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// ✨ 기능 1: 사용자가 설정할 수 없는 '모니터 이동' 단축키 (기존 로직 유지)
|
||||
const displays = screen.getAllDisplays();
|
||||
if (displays.length > 1) {
|
||||
displays.forEach((display, index) => {
|
||||
const key = `${modifier}+Shift+${index + 1}`;
|
||||
try {
|
||||
globalShortcut.register(key, () => {
|
||||
movementManager.moveToDisplay(display.id);
|
||||
});
|
||||
globalShortcut.register(key, () => movementManager.moveToDisplay(display.id));
|
||||
console.log(`Registered display switch shortcut: ${key} -> Display ${index + 1}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to register display switch ${key}:`, error);
|
||||
@ -1313,171 +1336,122 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, movementMan
|
||||
});
|
||||
}
|
||||
|
||||
if (currentHeaderState === 'apikey') {
|
||||
// API 키 입력 상태에서는 필수 단축키(toggleVisibility) 외에는 아무것도 등록하지 않음
|
||||
if (state === 'apikey') {
|
||||
if (keybinds.toggleVisibility) {
|
||||
try {
|
||||
globalShortcut.register(keybinds.toggleVisibility, () => toggleAllWindowsVisibility(movementManager));
|
||||
} catch (error) {
|
||||
console.error(`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`, error);
|
||||
}
|
||||
}
|
||||
console.log('ApiKeyHeader is active, skipping conditional shortcuts');
|
||||
return;
|
||||
}
|
||||
|
||||
const directions = [
|
||||
{ key: `${modifier}+Left`, direction: 'left' },
|
||||
{ key: `${modifier}+Right`, direction: 'right' },
|
||||
{ key: `${modifier}+Up`, direction: 'up' },
|
||||
{ key: `${modifier}+Down`, direction: 'down' },
|
||||
];
|
||||
|
||||
directions.forEach(({ key, direction }) => {
|
||||
try {
|
||||
globalShortcut.register(key, () => {
|
||||
const header = windowPool.get('header');
|
||||
if (header && header.isVisible()) {
|
||||
movementManager.moveStep(direction);
|
||||
}
|
||||
});
|
||||
// console.log(`Registered global shortcut: ${key} -> ${direction}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to register ${key}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
// ✨ 기능 2: 사용자가 설정할 수 없는 '화면 가장자리 이동' 단축키 (기존 로직 유지)
|
||||
const edgeDirections = [
|
||||
{ key: `${modifier}+Shift+Left`, direction: 'left' },
|
||||
{ key: `${modifier}+Shift+Right`, direction: 'right' },
|
||||
{ key: `${modifier}+Shift+Up`, direction: 'up' },
|
||||
{ key: `${modifier}+Shift+Down`, direction: 'down' },
|
||||
// { key: `${modifier}+Shift+Up`, direction: 'up' },
|
||||
// { key: `${modifier}+Shift+Down`, direction: 'down' },
|
||||
];
|
||||
|
||||
edgeDirections.forEach(({ key, direction }) => {
|
||||
try {
|
||||
globalShortcut.register(key, () => {
|
||||
const header = windowPool.get('header');
|
||||
if (header && header.isVisible()) {
|
||||
movementManager.moveToEdge(direction);
|
||||
}
|
||||
if (header && header.isVisible()) movementManager.moveToEdge(direction);
|
||||
});
|
||||
console.log(`Registered global shortcut: ${key} -> edge ${direction}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to register ${key}:`, error);
|
||||
console.error(`Failed to register edge move for ${key}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
if (keybinds.toggleClickThrough) {
|
||||
|
||||
// ✨ 기능 3: 사용자가 설정 가능한 모든 단축키를 동적으로 등록 (새로운 방식 적용)
|
||||
for (const action in keybinds) {
|
||||
const accelerator = keybinds[action];
|
||||
if (!accelerator) continue;
|
||||
|
||||
try {
|
||||
globalShortcut.register(keybinds.toggleClickThrough, () => {
|
||||
mouseEventsIgnored = !mouseEventsIgnored;
|
||||
if (mouseEventsIgnored) {
|
||||
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||
console.log('Mouse events ignored');
|
||||
} else {
|
||||
mainWindow.setIgnoreMouseEvents(false);
|
||||
console.log('Mouse events enabled');
|
||||
}
|
||||
mainWindow.webContents.send('click-through-toggled', mouseEventsIgnored);
|
||||
});
|
||||
// console.log(`Registered toggleClickThrough: ${keybinds.toggleClickThrough}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to register toggleClickThrough (${keybinds.toggleClickThrough}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (keybinds.nextStep) {
|
||||
try {
|
||||
globalShortcut.register(keybinds.nextStep, () => {
|
||||
console.log('⌘/Ctrl+Enter Ask shortcut triggered');
|
||||
|
||||
const askWindow = windowPool.get('ask');
|
||||
if (!askWindow || askWindow.isDestroyed()) {
|
||||
console.error('Ask window not found or destroyed');
|
||||
return;
|
||||
}
|
||||
|
||||
if (askWindow.isVisible()) {
|
||||
askWindow.webContents.send('ask-global-send');
|
||||
} else {
|
||||
try {
|
||||
askWindow.show();
|
||||
|
||||
const header = windowPool.get('header');
|
||||
if (header) {
|
||||
const currentHeaderPosition = header.getBounds();
|
||||
let callback;
|
||||
switch(action) {
|
||||
case 'toggleVisibility':
|
||||
callback = () => toggleAllWindowsVisibility(movementManager);
|
||||
break;
|
||||
case 'nextStep':
|
||||
callback = () => {
|
||||
const askWindow = windowPool.get('ask');
|
||||
if (!askWindow || askWindow.isDestroyed()) return;
|
||||
if (askWindow.isVisible()) {
|
||||
askWindow.webContents.send('ask-global-send');
|
||||
} else {
|
||||
askWindow.show();
|
||||
updateLayout();
|
||||
header.setPosition(currentHeaderPosition.x, currentHeaderPosition.y, false);
|
||||
askWindow.webContents.send('window-show-animation');
|
||||
}
|
||||
|
||||
askWindow.webContents.send('window-show-animation');
|
||||
} catch (e) {
|
||||
console.error('Error showing Ask window:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
// console.log(`Registered Ask shortcut (nextStep): ${keybinds.nextStep}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to register Ask shortcut (${keybinds.nextStep}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (keybinds.manualScreenshot) {
|
||||
try {
|
||||
globalShortcut.register(keybinds.manualScreenshot, () => {
|
||||
console.log('Manual screenshot shortcut triggered');
|
||||
mainWindow.webContents.executeJavaScript(`
|
||||
if (window.captureManualScreenshot) {
|
||||
window.captureManualScreenshot();
|
||||
} else {
|
||||
console.log('Manual screenshot function not available');
|
||||
}
|
||||
`);
|
||||
});
|
||||
// console.log(`Registered manualScreenshot: ${keybinds.manualScreenshot}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to register manualScreenshot (${keybinds.manualScreenshot}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (keybinds.previousResponse) {
|
||||
try {
|
||||
globalShortcut.register(keybinds.previousResponse, () => {
|
||||
console.log('Previous response shortcut triggered');
|
||||
sendToRenderer('navigate-previous-response');
|
||||
});
|
||||
// console.log(`Registered previousResponse: ${keybinds.previousResponse}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to register previousResponse (${keybinds.previousResponse}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (keybinds.nextResponse) {
|
||||
try {
|
||||
globalShortcut.register(keybinds.nextResponse, () => {
|
||||
console.log('Next response shortcut triggered');
|
||||
sendToRenderer('navigate-next-response');
|
||||
});
|
||||
// console.log(`Registered nextResponse: ${keybinds.nextResponse}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to register nextResponse (${keybinds.nextResponse}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (keybinds.scrollUp) {
|
||||
try {
|
||||
globalShortcut.register(keybinds.scrollUp, () => {
|
||||
console.log('Scroll up shortcut triggered');
|
||||
sendToRenderer('scroll-response-up');
|
||||
});
|
||||
// console.log(`Registered scrollUp: ${keybinds.scrollUp}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to register scrollUp (${keybinds.scrollUp}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (keybinds.scrollDown) {
|
||||
try {
|
||||
globalShortcut.register(keybinds.scrollDown, () => {
|
||||
console.log('Scroll down shortcut triggered');
|
||||
sendToRenderer('scroll-response-down');
|
||||
});
|
||||
// console.log(`Registered scrollDown: ${keybinds.scrollDown}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to register scrollDown (${keybinds.scrollDown}):`, error);
|
||||
};
|
||||
break;
|
||||
case 'scrollUp':
|
||||
callback = () => {
|
||||
// 'ask' 창을 명시적으로 가져옵니다.
|
||||
const askWindow = windowPool.get('ask');
|
||||
// 'ask' 창이 존재하고, 파괴되지 않았으며, 보이는 경우에만 이벤트를 전송합니다.
|
||||
if (askWindow && !askWindow.isDestroyed() && askWindow.isVisible()) {
|
||||
askWindow.webContents.send('scroll-response-up');
|
||||
}
|
||||
};
|
||||
break;
|
||||
case 'scrollDown':
|
||||
callback = () => {
|
||||
// 'ask' 창을 명시적으로 가져옵니다.
|
||||
const askWindow = windowPool.get('ask');
|
||||
// 'ask' 창이 존재하고, 파괴되지 않았으며, 보이는 경우에만 이벤트를 전송합니다.
|
||||
if (askWindow && !askWindow.isDestroyed() && askWindow.isVisible()) {
|
||||
askWindow.webContents.send('scroll-response-down');
|
||||
}
|
||||
};
|
||||
break;
|
||||
case 'moveUp':
|
||||
callback = () => { if (header && header.isVisible()) movementManager.moveStep('up'); };
|
||||
break;
|
||||
case 'moveDown':
|
||||
callback = () => { if (header && header.isVisible()) movementManager.moveStep('down'); };
|
||||
break;
|
||||
case 'moveLeft':
|
||||
callback = () => { if (header && header.isVisible()) movementManager.moveStep('left'); };
|
||||
break;
|
||||
case 'moveRight':
|
||||
callback = () => { if (header && header.isVisible()) movementManager.moveStep('right'); };
|
||||
break;
|
||||
case 'toggleClickThrough':
|
||||
callback = () => {
|
||||
mouseEventsIgnored = !mouseEventsIgnored;
|
||||
if(mainWindow && !mainWindow.isDestroyed()){
|
||||
mainWindow.setIgnoreMouseEvents(mouseEventsIgnored, { forward: true });
|
||||
mainWindow.webContents.send('click-through-toggled', mouseEventsIgnored);
|
||||
}
|
||||
};
|
||||
break;
|
||||
case 'manualScreenshot':
|
||||
callback = () => {
|
||||
if(mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.executeJavaScript('window.captureManualScreenshot && window.captureManualScreenshot();');
|
||||
}
|
||||
};
|
||||
break;
|
||||
case 'previousResponse':
|
||||
callback = () => sendToRenderer('navigate-previous-response');
|
||||
break;
|
||||
case 'nextResponse':
|
||||
callback = () => sendToRenderer('navigate-next-response');
|
||||
break;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
globalShortcut.register(accelerator, callback);
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(`Failed to register shortcut for "${action}" (${accelerator}):`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -658,6 +658,8 @@ export class AskView extends LitElement {
|
||||
this.handleDocumentClick = this.handleDocumentClick.bind(this);
|
||||
this.handleWindowBlur = this.handleWindowBlur.bind(this);
|
||||
|
||||
this.handleScroll = this.handleScroll.bind(this);
|
||||
|
||||
this.loadLibraries();
|
||||
|
||||
// --- Resize helpers ---
|
||||
@ -863,6 +865,9 @@ export class AskView extends LitElement {
|
||||
|
||||
ipcRenderer.on('ask-response-chunk', this.handleStreamChunk);
|
||||
ipcRenderer.on('ask-response-stream-end', this.handleStreamEnd);
|
||||
|
||||
ipcRenderer.on('scroll-response-up', () => this.handleScroll('up'));
|
||||
ipcRenderer.on('scroll-response-down', () => this.handleScroll('down'));
|
||||
console.log('✅ AskView: IPC 이벤트 리스너 등록 완료');
|
||||
}
|
||||
}
|
||||
@ -901,9 +906,24 @@ export class AskView extends LitElement {
|
||||
|
||||
ipcRenderer.removeListener('ask-response-chunk', this.handleStreamChunk);
|
||||
ipcRenderer.removeListener('ask-response-stream-end', this.handleStreamEnd);
|
||||
|
||||
ipcRenderer.removeListener('scroll-response-up', () => this.handleScroll('up'));
|
||||
ipcRenderer.removeListener('scroll-response-down', () => this.handleScroll('down'));
|
||||
console.log('✅ AskView: IPC 이벤트 리스너 제거 완료');
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll(direction) {
|
||||
const scrollableElement = this.shadowRoot.querySelector('#responseContainer');
|
||||
if (scrollableElement) {
|
||||
const scrollAmount = 100; // 한 번에 스크롤할 양 (px)
|
||||
if (direction === 'up') {
|
||||
scrollableElement.scrollTop -= scrollAmount;
|
||||
} else {
|
||||
scrollableElement.scrollTop += scrollAmount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 스트리밍 처리 핸들러 ---
|
||||
handleStreamChunk(event, { token }) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
const { BrowserWindow } = require('electron');
|
||||
const { BrowserWindow, app } = require('electron');
|
||||
const SttService = require('./stt/sttService');
|
||||
const SummaryService = require('./summary/summaryService');
|
||||
const authService = require('../../common/services/authService');
|
||||
@ -117,8 +117,27 @@ class ListenService {
|
||||
throw new Error('Failed to initialize database session');
|
||||
}
|
||||
|
||||
// Initialize STT sessions
|
||||
await this.sttService.initializeSttSessions(language);
|
||||
/* ---------- STT Initialization Retry Logic ---------- */
|
||||
const MAX_RETRY = 10;
|
||||
const RETRY_DELAY_MS = 300; // 0.3 seconds
|
||||
|
||||
let sttReady = false;
|
||||
for (let attempt = 1; attempt <= MAX_RETRY; attempt++) {
|
||||
try {
|
||||
await this.sttService.initializeSttSessions(language);
|
||||
sttReady = true;
|
||||
break; // Exit on success
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`[ListenService] STT init attempt ${attempt} failed: ${err.message}`
|
||||
);
|
||||
if (attempt < MAX_RETRY) {
|
||||
await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!sttReady) throw new Error('STT init failed after retries');
|
||||
/* ------------------------------------------- */
|
||||
|
||||
console.log('✅ Listen service initialized successfully.');
|
||||
|
||||
@ -213,9 +232,9 @@ class ListenService {
|
||||
try {
|
||||
await this.sendAudioContent(data, mimeType);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error sending user audio:', error);
|
||||
return { success: false, error: error.message };
|
||||
} catch (e) {
|
||||
console.error('Error sending user audio:', e);
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
});
|
||||
|
||||
@ -237,9 +256,13 @@ class ListenService {
|
||||
if (process.platform !== 'darwin') {
|
||||
return { success: false, error: 'macOS audio capture only available on macOS' };
|
||||
}
|
||||
if (this.sttService.isMacOSAudioRunning?.()) {
|
||||
return { success: false, error: 'already_running' };
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await this.startMacOSAudioCapture();
|
||||
return { success };
|
||||
return { success, error: null };
|
||||
} catch (error) {
|
||||
console.error('Error starting macOS audio capture:', error);
|
||||
return { success: false, error: error.message };
|
||||
@ -274,4 +297,4 @@ class ListenService {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ListenService;
|
||||
module.exports = ListenService;
|
@ -1,5 +1,30 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
const createAecModule = require('../../../assets/aec.js');
|
||||
|
||||
let aecModPromise = null; // 한 번만 로드
|
||||
let aecMod = null;
|
||||
let aecPtr = 0; // Rust Aec* 1개만 재사용
|
||||
|
||||
/** WASM 모듈 가져오고 1회 초기화 */
|
||||
async function getAec () {
|
||||
if (aecModPromise) return aecModPromise; // 캐시
|
||||
|
||||
aecModPromise = createAecModule().then((M) => {
|
||||
aecMod = M;
|
||||
// C 심볼 → JS 래퍼 바인딩 (딱 1번)
|
||||
M.newPtr = M.cwrap('AecNew', 'number',
|
||||
['number','number','number','number']);
|
||||
M.cancel = M.cwrap('AecCancelEcho', null,
|
||||
['number','number','number','number','number']);
|
||||
M.destroy = M.cwrap('AecDestroy', null, ['number']);
|
||||
return M;
|
||||
});
|
||||
|
||||
return aecModPromise;
|
||||
}
|
||||
|
||||
// 바로 로드-실패 로그를 보기 위해
|
||||
getAec().catch(console.error);
|
||||
// ---------------------------
|
||||
// Constants & Globals
|
||||
// ---------------------------
|
||||
@ -80,128 +105,49 @@ function arrayBufferToBase64(buffer) {
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Complete SimpleAEC implementation (exact from renderer.js)
|
||||
// ---------------------------
|
||||
class SimpleAEC {
|
||||
constructor() {
|
||||
this.adaptiveFilter = new Float32Array(1024);
|
||||
this.mu = 0.2;
|
||||
this.echoDelay = 100;
|
||||
this.sampleRate = 24000;
|
||||
this.delaySamples = Math.floor((this.echoDelay / 1000) * this.sampleRate);
|
||||
|
||||
this.echoGain = 0.5;
|
||||
this.noiseFloor = 0.01;
|
||||
|
||||
// 🔧 Adaptive-gain parameters (User-tuned, very aggressive)
|
||||
this.targetErr = 0.002;
|
||||
this.adaptRate = 0.1;
|
||||
|
||||
console.log('🎯 AEC initialized (hyper-aggressive)');
|
||||
}
|
||||
|
||||
process(micData, systemData) {
|
||||
if (!systemData || systemData.length === 0) {
|
||||
return micData;
|
||||
}
|
||||
|
||||
for (let i = 0; i < systemData.length; i++) {
|
||||
if (systemData[i] > 0.98) systemData[i] = 0.98;
|
||||
else if (systemData[i] < -0.98) systemData[i] = -0.98;
|
||||
|
||||
systemData[i] = Math.tanh(systemData[i] * 4);
|
||||
}
|
||||
|
||||
let sum2 = 0;
|
||||
for (let i = 0; i < systemData.length; i++) sum2 += systemData[i] * systemData[i];
|
||||
const rms = Math.sqrt(sum2 / systemData.length);
|
||||
const targetRms = 0.08; // 🔧 기준 RMS (기존 0.1)
|
||||
const scale = targetRms / (rms + 1e-6); // 1e-6: 0-division 방지
|
||||
|
||||
const output = new Float32Array(micData.length);
|
||||
|
||||
const optimalDelay = this.findOptimalDelay(micData, systemData);
|
||||
|
||||
for (let i = 0; i < micData.length; i++) {
|
||||
let echoEstimate = 0;
|
||||
|
||||
for (let d = -500; d <= 500; d += 100) {
|
||||
const delayIndex = i - optimalDelay - d;
|
||||
if (delayIndex >= 0 && delayIndex < systemData.length) {
|
||||
const weight = Math.exp(-Math.abs(d) / 1000);
|
||||
echoEstimate += systemData[delayIndex] * scale * this.echoGain * weight;
|
||||
}
|
||||
}
|
||||
|
||||
output[i] = micData[i] - echoEstimate * 0.9;
|
||||
|
||||
if (Math.abs(output[i]) < this.noiseFloor) {
|
||||
output[i] *= 0.5;
|
||||
}
|
||||
|
||||
if (this.isSimilarToSystem(output[i], systemData, i, optimalDelay)) {
|
||||
output[i] *= 0.25;
|
||||
}
|
||||
|
||||
output[i] = Math.max(-1, Math.min(1, output[i]));
|
||||
}
|
||||
|
||||
|
||||
let errSum = 0;
|
||||
for (let i = 0; i < output.length; i++) errSum += output[i] * output[i];
|
||||
const errRms = Math.sqrt(errSum / output.length);
|
||||
|
||||
const err = errRms - this.targetErr;
|
||||
this.echoGain += this.adaptRate * err; // 비례 제어
|
||||
this.echoGain = Math.max(0, Math.min(1, this.echoGain));
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
findOptimalDelay(micData, systemData) {
|
||||
let maxCorr = 0;
|
||||
let optimalDelay = this.delaySamples;
|
||||
|
||||
for (let delay = 0; delay < 5000 && delay < systemData.length; delay += 200) {
|
||||
let corr = 0;
|
||||
let count = 0;
|
||||
|
||||
for (let i = 0; i < Math.min(500, micData.length); i++) {
|
||||
if (i + delay < systemData.length) {
|
||||
corr += micData[i] * systemData[i + delay];
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
corr = Math.abs(corr / count);
|
||||
if (corr > maxCorr) {
|
||||
maxCorr = corr;
|
||||
optimalDelay = delay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return optimalDelay;
|
||||
}
|
||||
|
||||
isSimilarToSystem(sample, systemData, index, delay) {
|
||||
const windowSize = 50;
|
||||
let similarity = 0;
|
||||
|
||||
for (let i = -windowSize; i <= windowSize; i++) {
|
||||
const sysIndex = index - delay + i;
|
||||
if (sysIndex >= 0 && sysIndex < systemData.length) {
|
||||
similarity += Math.abs(sample - systemData[sysIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
return similarity / (2 * windowSize + 1) < 0.15;
|
||||
}
|
||||
/* ───────────────────────── JS ↔︎ WASM 헬퍼 ───────────────────────── */
|
||||
function int16PtrFromFloat32(mod, f32) {
|
||||
const len = f32.length;
|
||||
const bytes = len * 2;
|
||||
const ptr = mod._malloc(bytes);
|
||||
// HEAP16이 없으면 HEAPU8.buffer로 직접 래핑
|
||||
const heapBuf = (mod.HEAP16 ? mod.HEAP16.buffer : mod.HEAPU8.buffer);
|
||||
const i16 = new Int16Array(heapBuf, ptr, len);
|
||||
for (let i = 0; i < len; ++i) {
|
||||
const s = Math.max(-1, Math.min(1, f32[i]));
|
||||
i16[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
||||
}
|
||||
return { ptr, view: i16 };
|
||||
}
|
||||
|
||||
function float32FromInt16View(i16) {
|
||||
const out = new Float32Array(i16.length);
|
||||
for (let i = 0; i < i16.length; ++i) out[i] = i16[i] / 32768;
|
||||
return out;
|
||||
}
|
||||
|
||||
/* 필요하다면 종료 시 */
|
||||
function disposeAec () {
|
||||
getAec().then(mod => { if (aecPtr) mod.destroy(aecPtr); });
|
||||
}
|
||||
|
||||
function runAecSync (micF32, sysF32) {
|
||||
if (!aecMod || !aecPtr || !aecMod.HEAPU8) return micF32; // 아직 모듈 안 뜸 → 패스
|
||||
|
||||
const len = micF32.length;
|
||||
const mic = int16PtrFromFloat32(aecMod, micF32);
|
||||
const echo = int16PtrFromFloat32(aecMod, sysF32);
|
||||
const out = aecMod._malloc(len * 2);
|
||||
|
||||
aecMod.cancel(aecPtr, mic.ptr, echo.ptr, out, len);
|
||||
|
||||
const heapBuf = (aecMod.HEAP16 ? aecMod.HEAP16.buffer : aecMod.HEAPU8.buffer);
|
||||
const outF32 = float32FromInt16View(new Int16Array(heapBuf, out, len));
|
||||
|
||||
aecMod._free(mic.ptr); aecMod._free(echo.ptr); aecMod._free(out);
|
||||
return outF32;
|
||||
}
|
||||
|
||||
let aecProcessor = new SimpleAEC();
|
||||
|
||||
// System audio data handler
|
||||
ipcRenderer.on('system-audio-data', (event, { data }) => {
|
||||
@ -214,8 +160,6 @@ ipcRenderer.on('system-audio-data', (event, { data }) => {
|
||||
if (systemAudioBuffer.length > MAX_SYSTEM_BUFFER_SIZE) {
|
||||
systemAudioBuffer = systemAudioBuffer.slice(-MAX_SYSTEM_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
console.log('📥 Received system audio for AEC reference');
|
||||
});
|
||||
|
||||
// ---------------------------
|
||||
@ -305,39 +249,47 @@ setInterval(() => {
|
||||
// ---------------------------
|
||||
// Audio processing functions (exact from renderer.js)
|
||||
// ---------------------------
|
||||
function setupMicProcessing(micStream) {
|
||||
async function setupMicProcessing(micStream) {
|
||||
/* ── WASM 먼저 로드 ───────────────────────── */
|
||||
const mod = await getAec();
|
||||
if (!aecPtr) aecPtr = mod.newPtr(160, 1600, 24000, 1);
|
||||
|
||||
|
||||
const micAudioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
|
||||
await micAudioContext.resume();
|
||||
const micSource = micAudioContext.createMediaStreamSource(micStream);
|
||||
const micProcessor = micAudioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
|
||||
|
||||
let audioBuffer = [];
|
||||
const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
|
||||
|
||||
micProcessor.onaudioprocess = async e => {
|
||||
micProcessor.onaudioprocess = (e) => {
|
||||
const inputData = e.inputBuffer.getChannelData(0);
|
||||
audioBuffer.push(...inputData);
|
||||
console.log('🎤 micProcessor.onaudioprocess');
|
||||
|
||||
// samplesPerChunk(=2400) 만큼 모이면 전송
|
||||
while (audioBuffer.length >= samplesPerChunk) {
|
||||
let chunk = audioBuffer.splice(0, samplesPerChunk);
|
||||
let processedChunk = new Float32Array(chunk);
|
||||
let processedChunk = new Float32Array(chunk); // 기본값
|
||||
|
||||
// Check for system audio and apply AEC only if voice is active
|
||||
if (aecProcessor && systemAudioBuffer.length > 0) {
|
||||
const latestSystemAudio = systemAudioBuffer[systemAudioBuffer.length - 1];
|
||||
const systemFloat32 = base64ToFloat32Array(latestSystemAudio.data);
|
||||
// ───────────────── WASM AEC ─────────────────
|
||||
if (systemAudioBuffer.length > 0) {
|
||||
const latest = systemAudioBuffer[systemAudioBuffer.length - 1];
|
||||
const sysF32 = base64ToFloat32Array(latest.data);
|
||||
|
||||
// Apply AEC only when system audio has active speech
|
||||
if (isVoiceActive(systemFloat32)) {
|
||||
processedChunk = aecProcessor.process(new Float32Array(chunk), systemFloat32);
|
||||
console.log('🔊 Applied AEC because system audio is active');
|
||||
}
|
||||
// **음성 구간일 때만 런**
|
||||
processedChunk = runAecSync(new Float32Array(chunk), sysF32);
|
||||
console.log('🔊 Applied WASM-AEC (speex)');
|
||||
} else {
|
||||
console.log('🔊 No system audio for AEC reference');
|
||||
}
|
||||
|
||||
const pcmData16 = convertFloat32ToInt16(processedChunk);
|
||||
const base64Data = arrayBufferToBase64(pcmData16.buffer);
|
||||
const pcm16 = convertFloat32ToInt16(processedChunk);
|
||||
const b64 = arrayBufferToBase64(pcm16.buffer);
|
||||
|
||||
await ipcRenderer.invoke('send-audio-content', {
|
||||
data: base64Data,
|
||||
ipcRenderer.invoke('send-audio-content', {
|
||||
data: b64,
|
||||
mimeType: 'audio/pcm;rate=24000',
|
||||
});
|
||||
}
|
||||
@ -520,7 +472,19 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
||||
// Start macOS audio capture
|
||||
const audioResult = await ipcRenderer.invoke('start-macos-audio');
|
||||
if (!audioResult.success) {
|
||||
throw new Error('Failed to start macOS audio capture: ' + audioResult.error);
|
||||
console.warn('[listenCapture] macOS audio start failed:', audioResult.error);
|
||||
|
||||
// 이미 실행 중 → stop 후 재시도
|
||||
if (audioResult.error === 'already_running') {
|
||||
await ipcRenderer.invoke('stop-macos-audio');
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
const retry = await ipcRenderer.invoke('start-macos-audio');
|
||||
if (!retry.success) {
|
||||
throw new Error('Retry failed: ' + retry.error);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Failed to start macOS audio capture: ' + audioResult.error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize screen capture in main process
|
||||
@ -543,7 +507,7 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
||||
});
|
||||
|
||||
console.log('macOS microphone capture started');
|
||||
const { context, processor } = setupMicProcessing(micMediaStream);
|
||||
const { context, processor } = await setupMicProcessing(micMediaStream);
|
||||
audioContext = context;
|
||||
audioProcessor = processor;
|
||||
} catch (micErr) {
|
||||
@ -616,7 +580,7 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
||||
video: false,
|
||||
});
|
||||
console.log('Windows microphone capture started');
|
||||
const { context, processor } = setupMicProcessing(micMediaStream);
|
||||
const { context, processor } = await setupMicProcessing(micMediaStream);
|
||||
audioContext = context;
|
||||
audioProcessor = processor;
|
||||
} catch (micErr) {
|
||||
@ -719,6 +683,9 @@ function stopCapture() {
|
||||
// Exports & global registration
|
||||
// ---------------------------
|
||||
module.exports = {
|
||||
getAec, // 새로 만든 초기화 함수
|
||||
runAecSync, // sync 버전
|
||||
disposeAec, // 필요시 Rust 객체 파괴
|
||||
startCapture,
|
||||
stopCapture,
|
||||
captureManualScreenshot,
|
||||
|
@ -12,11 +12,6 @@ class SttService {
|
||||
this.myCurrentUtterance = '';
|
||||
this.theirCurrentUtterance = '';
|
||||
|
||||
this.myLastPartialText = '';
|
||||
this.theirLastPartialText = '';
|
||||
this.myInactivityTimer = null;
|
||||
this.theirInactivityTimer = null;
|
||||
|
||||
// Turn-completion debouncing
|
||||
this.myCompletionBuffer = '';
|
||||
this.theirCompletionBuffer = '';
|
||||
@ -38,33 +33,6 @@ class SttService {
|
||||
this.onStatusUpdate = onStatusUpdate;
|
||||
}
|
||||
|
||||
// async getApiKey() {
|
||||
// const storedKey = await getStoredApiKey();
|
||||
// if (storedKey) {
|
||||
// console.log('[SttService] Using stored API key');
|
||||
// return storedKey;
|
||||
// }
|
||||
|
||||
// const envKey = process.env.OPENAI_API_KEY;
|
||||
// if (envKey) {
|
||||
// console.log('[SttService] Using environment API key');
|
||||
// return envKey;
|
||||
// }
|
||||
|
||||
// console.error('[SttService] No API key found in storage or environment');
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// async getAiProvider() {
|
||||
// try {
|
||||
// const { ipcRenderer } = require('electron');
|
||||
// const provider = await ipcRenderer.invoke('get-ai-provider');
|
||||
// return provider || 'openai';
|
||||
// } catch (error) {
|
||||
// return getStoredProvider ? getStoredProvider() : 'openai';
|
||||
// }
|
||||
// }
|
||||
|
||||
sendToRenderer(channel, data) {
|
||||
BrowserWindow.getAllWindows().forEach(win => {
|
||||
if (!win.isDestroyed()) {
|
||||
@ -74,10 +42,9 @@ class SttService {
|
||||
}
|
||||
|
||||
flushMyCompletion() {
|
||||
if (!this.modelInfo || !this.myCompletionBuffer.trim()) return;
|
||||
const finalText = (this.myCompletionBuffer + this.myCurrentUtterance).trim();
|
||||
if (!this.modelInfo || !finalText) return;
|
||||
|
||||
const finalText = this.myCompletionBuffer.trim();
|
||||
|
||||
// Notify completion callback
|
||||
if (this.onTranscriptionComplete) {
|
||||
this.onTranscriptionComplete('Me', finalText);
|
||||
@ -102,9 +69,8 @@ class SttService {
|
||||
}
|
||||
|
||||
flushTheirCompletion() {
|
||||
if (!this.modelInfo || !this.theirCompletionBuffer.trim()) return;
|
||||
|
||||
const finalText = this.theirCompletionBuffer.trim();
|
||||
const finalText = (this.theirCompletionBuffer + this.theirCurrentUtterance).trim();
|
||||
if (!this.modelInfo || !finalText) return;
|
||||
|
||||
// Notify completion callback
|
||||
if (this.onTranscriptionComplete) {
|
||||
@ -130,39 +96,29 @@ class SttService {
|
||||
}
|
||||
|
||||
debounceMyCompletion(text) {
|
||||
// 상대방이 말하고 있던 경우, 화자가 변경되었으므로 즉시 상대방의 말풍선을 완성합니다.
|
||||
if (this.theirCompletionTimer) {
|
||||
clearTimeout(this.theirCompletionTimer);
|
||||
this.flushTheirCompletion();
|
||||
if (this.modelInfo?.provider === 'gemini') {
|
||||
this.myCompletionBuffer += text;
|
||||
} else {
|
||||
this.myCompletionBuffer += (this.myCompletionBuffer ? ' ' : '') + text;
|
||||
}
|
||||
|
||||
this.myCompletionBuffer += (this.myCompletionBuffer ? ' ' : '') + text;
|
||||
|
||||
if (this.myCompletionTimer) clearTimeout(this.myCompletionTimer);
|
||||
this.myCompletionTimer = setTimeout(() => this.flushMyCompletion(), COMPLETION_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
debounceTheirCompletion(text) {
|
||||
// 내가 말하고 있던 경우, 화자가 변경되었으므로 즉시 내 말풍선을 완성합니다.
|
||||
if (this.myCompletionTimer) {
|
||||
clearTimeout(this.myCompletionTimer);
|
||||
this.flushMyCompletion();
|
||||
if (this.modelInfo?.provider === 'gemini') {
|
||||
this.theirCompletionBuffer += text;
|
||||
} else {
|
||||
this.theirCompletionBuffer += (this.theirCompletionBuffer ? ' ' : '') + text;
|
||||
}
|
||||
|
||||
this.theirCompletionBuffer += (this.theirCompletionBuffer ? ' ' : '') + text;
|
||||
|
||||
if (this.theirCompletionTimer) clearTimeout(this.theirCompletionTimer);
|
||||
this.theirCompletionTimer = setTimeout(() => this.flushTheirCompletion(), COMPLETION_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
async initializeSttSessions(language = 'en') {
|
||||
const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || language || 'en';
|
||||
|
||||
// const API_KEY = await this.getApiKey();
|
||||
// if (!API_KEY) {
|
||||
// throw new Error('No API key available');
|
||||
// }
|
||||
// const provider = await this.getAiProvider();
|
||||
|
||||
const modelInfo = await getCurrentModelInfo(null, { type: 'stt' });
|
||||
if (!modelInfo || !modelInfo.apiKey) {
|
||||
@ -171,10 +127,6 @@ class SttService {
|
||||
this.modelInfo = modelInfo;
|
||||
console.log(`[SttService] Initializing STT for ${modelInfo.provider} using model ${modelInfo.model}`);
|
||||
|
||||
|
||||
// const isGemini = modelInfo.provider === 'gemini';
|
||||
// console.log(`[SttService] Initializing STT for provider: ${modelInfo.provider}`);
|
||||
|
||||
const handleMyMessage = message => {
|
||||
if (!this.modelInfo) {
|
||||
console.log('[SttService] Ignoring message - session already closed');
|
||||
@ -182,13 +134,35 @@ class SttService {
|
||||
}
|
||||
|
||||
if (this.modelInfo.provider === 'gemini') {
|
||||
const text = message.serverContent?.inputTranscription?.text || '';
|
||||
if (text && text.trim()) {
|
||||
const finalUtteranceText = text.trim().replace(/<noise>/g, '').trim();
|
||||
if (finalUtteranceText && finalUtteranceText !== '.') {
|
||||
this.debounceMyCompletion(finalUtteranceText);
|
||||
}
|
||||
if (!message.serverContent?.modelTurn) {
|
||||
console.log('[Gemini STT - Me]', JSON.stringify(message, null, 2));
|
||||
}
|
||||
|
||||
if (message.serverContent?.turnComplete) {
|
||||
if (this.myCompletionTimer) {
|
||||
clearTimeout(this.myCompletionTimer);
|
||||
this.flushMyCompletion();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const transcription = message.serverContent?.inputTranscription;
|
||||
if (!transcription || !transcription.text) return;
|
||||
|
||||
const textChunk = transcription.text;
|
||||
if (!textChunk.trim() || textChunk.trim() === '<noise>') {
|
||||
return; // 1. Ignore whitespace-only chunks or noise
|
||||
}
|
||||
|
||||
this.debounceMyCompletion(textChunk);
|
||||
|
||||
this.sendToRenderer('stt-update', {
|
||||
speaker: 'Me',
|
||||
text: this.myCompletionBuffer,
|
||||
isPartial: true,
|
||||
isFinal: false,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else {
|
||||
const type = message.type;
|
||||
const text = message.transcript || message.delta || (message.alternatives && message.alternatives[0]?.transcript) || '';
|
||||
@ -222,19 +196,43 @@ class SttService {
|
||||
};
|
||||
|
||||
const handleTheirMessage = message => {
|
||||
if (!message || typeof message !== 'object') return;
|
||||
|
||||
if (!this.modelInfo) {
|
||||
console.log('[SttService] Ignoring message - session already closed');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modelInfo.provider === 'gemini') {
|
||||
const text = message.serverContent?.inputTranscription?.text || '';
|
||||
if (text && text.trim()) {
|
||||
const finalUtteranceText = text.trim().replace(/<noise>/g, '').trim();
|
||||
if (finalUtteranceText && finalUtteranceText !== '.') {
|
||||
this.debounceTheirCompletion(finalUtteranceText);
|
||||
}
|
||||
if (!message.serverContent?.modelTurn) {
|
||||
console.log('[Gemini STT - Them]', JSON.stringify(message, null, 2));
|
||||
}
|
||||
|
||||
if (message.serverContent?.turnComplete) {
|
||||
if (this.theirCompletionTimer) {
|
||||
clearTimeout(this.theirCompletionTimer);
|
||||
this.flushTheirCompletion();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const transcription = message.serverContent?.inputTranscription;
|
||||
if (!transcription || !transcription.text) return;
|
||||
|
||||
const textChunk = transcription.text;
|
||||
if (!textChunk.trim() || textChunk.trim() === '<noise>') {
|
||||
return; // 1. Ignore whitespace-only chunks or noise
|
||||
}
|
||||
|
||||
this.debounceTheirCompletion(textChunk);
|
||||
|
||||
this.sendToRenderer('stt-update', {
|
||||
speaker: 'Them',
|
||||
text: this.theirCompletionBuffer,
|
||||
isPartial: true,
|
||||
isFinal: false,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else {
|
||||
const type = message.type;
|
||||
const text = message.transcript || message.delta || (message.alternatives && message.alternatives[0]?.transcript) || '';
|
||||
@ -494,14 +492,6 @@ class SttService {
|
||||
this.stopMacOSAudioCapture();
|
||||
|
||||
// Clear timers
|
||||
if (this.myInactivityTimer) {
|
||||
clearTimeout(this.myInactivityTimer);
|
||||
this.myInactivityTimer = null;
|
||||
}
|
||||
if (this.theirInactivityTimer) {
|
||||
clearTimeout(this.theirInactivityTimer);
|
||||
this.theirInactivityTimer = null;
|
||||
}
|
||||
if (this.myCompletionTimer) {
|
||||
clearTimeout(this.myCompletionTimer);
|
||||
this.myCompletionTimer = null;
|
||||
@ -527,8 +517,6 @@ class SttService {
|
||||
// Reset state
|
||||
this.myCurrentUtterance = '';
|
||||
this.theirCurrentUtterance = '';
|
||||
this.myLastPartialText = '';
|
||||
this.theirLastPartialText = '';
|
||||
this.myCompletionBuffer = '';
|
||||
this.theirCompletionBuffer = '';
|
||||
this.modelInfo = null;
|
||||
|
@ -437,22 +437,10 @@ export class SettingsView extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
//////// before_modelStateService ////////
|
||||
// static properties = {
|
||||
// firebaseUser: { type: Object, state: true },
|
||||
// apiKey: { type: String, state: true },
|
||||
// isLoading: { type: Boolean, state: true },
|
||||
// isContentProtectionOn: { type: Boolean, state: true },
|
||||
// settings: { type: Object, state: true },
|
||||
// presets: { type: Array, state: true },
|
||||
// selectedPreset: { type: Object, state: true },
|
||||
// showPresets: { type: Boolean, state: true },
|
||||
// saving: { type: Boolean, state: true },
|
||||
// };
|
||||
//////// before_modelStateService ////////
|
||||
|
||||
//////// after_modelStateService ////////
|
||||
static properties = {
|
||||
shortcuts: { type: Object, state: true },
|
||||
firebaseUser: { type: Object, state: true },
|
||||
isLoading: { type: Boolean, state: true },
|
||||
isContentProtectionOn: { type: Boolean, state: true },
|
||||
@ -468,25 +456,15 @@ export class SettingsView extends LitElement {
|
||||
presets: { type: Array, state: true },
|
||||
selectedPreset: { type: Object, state: true },
|
||||
showPresets: { type: Boolean, state: true },
|
||||
autoUpdateEnabled: { type: Boolean, state: true },
|
||||
autoUpdateLoading: { type: Boolean, state: true },
|
||||
};
|
||||
//////// after_modelStateService ////////
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
//////// before_modelStateService ////////
|
||||
// this.firebaseUser = null;
|
||||
// this.apiKey = null;
|
||||
// this.isLoading = false;
|
||||
// this.isContentProtectionOn = true;
|
||||
// this.settings = null;
|
||||
// this.presets = [];
|
||||
// this.selectedPreset = null;
|
||||
// this.showPresets = false;
|
||||
// this.saving = false;
|
||||
// this.loadInitialData();
|
||||
//////// before_modelStateService ////////
|
||||
|
||||
//////// after_modelStateService ////////
|
||||
this.shortcuts = {};
|
||||
this.firebaseUser = null;
|
||||
this.apiKeys = { openai: '', gemini: '', anthropic: '' };
|
||||
this.providerConfig = {};
|
||||
@ -503,51 +481,47 @@ export class SettingsView extends LitElement {
|
||||
this.selectedPreset = null;
|
||||
this.showPresets = false;
|
||||
this.handleUsePicklesKey = this.handleUsePicklesKey.bind(this)
|
||||
this.autoUpdateEnabled = true;
|
||||
this.autoUpdateLoading = true;
|
||||
this.loadInitialData();
|
||||
//////// after_modelStateService ////////
|
||||
}
|
||||
|
||||
async loadAutoUpdateSetting() {
|
||||
if (!window.require) return;
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
this.autoUpdateLoading = true;
|
||||
try {
|
||||
const enabled = await ipcRenderer.invoke('settings:get-auto-update');
|
||||
this.autoUpdateEnabled = enabled;
|
||||
console.log('Auto-update setting loaded:', enabled);
|
||||
} catch (e) {
|
||||
console.error('Error loading auto-update setting:', e);
|
||||
this.autoUpdateEnabled = true; // fallback
|
||||
}
|
||||
this.autoUpdateLoading = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
//////// before_modelStateService ////////
|
||||
// async loadInitialData() {
|
||||
// if (!window.require) return;
|
||||
|
||||
// try {
|
||||
// this.isLoading = true;
|
||||
// const { ipcRenderer } = window.require('electron');
|
||||
|
||||
// // Load all data in parallel
|
||||
// const [settings, presets, apiKey, contentProtection, userState] = await Promise.all([
|
||||
// ipcRenderer.invoke('settings:getSettings'),
|
||||
// ipcRenderer.invoke('settings:getPresets'),
|
||||
// ipcRenderer.invoke('get-stored-api-key'),
|
||||
// ipcRenderer.invoke('get-content-protection-status'),
|
||||
// ipcRenderer.invoke('get-current-user')
|
||||
// ]);
|
||||
|
||||
// this.settings = settings;
|
||||
// this.presets = presets || [];
|
||||
// this.apiKey = apiKey;
|
||||
// this.isContentProtectionOn = contentProtection;
|
||||
|
||||
// // Set first user preset as selected
|
||||
// if (this.presets.length > 0) {
|
||||
// const firstUserPreset = this.presets.find(p => p.is_default === 0);
|
||||
// if (firstUserPreset) {
|
||||
// this.selectedPreset = firstUserPreset;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (userState && userState.isLoggedIn) {
|
||||
// this.firebaseUser = userState.user;
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Error loading initial data:', error);
|
||||
// } finally {
|
||||
// this.isLoading = false;
|
||||
// }
|
||||
// }
|
||||
//////// before_modelStateService ////////
|
||||
async handleToggleAutoUpdate() {
|
||||
if (!window.require || this.autoUpdateLoading) return;
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
this.autoUpdateLoading = true;
|
||||
this.requestUpdate();
|
||||
try {
|
||||
const newValue = !this.autoUpdateEnabled;
|
||||
const result = await ipcRenderer.invoke('settings:set-auto-update', newValue);
|
||||
if (result && result.success) {
|
||||
this.autoUpdateEnabled = newValue;
|
||||
} else {
|
||||
console.error('Failed to update auto-update setting');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error toggling auto-update:', e);
|
||||
}
|
||||
this.autoUpdateLoading = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
//////// after_modelStateService ////////
|
||||
async loadInitialData() {
|
||||
@ -555,7 +529,7 @@ export class SettingsView extends LitElement {
|
||||
this.isLoading = true;
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
try {
|
||||
const [userState, config, storedKeys, availableLlm, availableStt, selectedModels, presets, contentProtection] = await Promise.all([
|
||||
const [userState, config, storedKeys, availableLlm, availableStt, selectedModels, presets, contentProtection, shortcuts] = await Promise.all([
|
||||
ipcRenderer.invoke('get-current-user'),
|
||||
ipcRenderer.invoke('model:get-provider-config'), // Provider 설정 로드
|
||||
ipcRenderer.invoke('model:get-all-keys'),
|
||||
@ -563,7 +537,8 @@ export class SettingsView extends LitElement {
|
||||
ipcRenderer.invoke('model:get-available-models', { type: 'stt' }),
|
||||
ipcRenderer.invoke('model:get-selected-models'),
|
||||
ipcRenderer.invoke('settings:getPresets'),
|
||||
ipcRenderer.invoke('get-content-protection-status')
|
||||
ipcRenderer.invoke('get-content-protection-status'),
|
||||
ipcRenderer.invoke('get-current-shortcuts')
|
||||
]);
|
||||
|
||||
if (userState && userState.isLoggedIn) this.firebaseUser = userState;
|
||||
@ -575,6 +550,7 @@ export class SettingsView extends LitElement {
|
||||
this.selectedStt = selectedModels.stt;
|
||||
this.presets = presets || [];
|
||||
this.isContentProtectionOn = contentProtection;
|
||||
this.shortcuts = shortcuts || {};
|
||||
if (this.presets.length > 0) {
|
||||
const firstUserPreset = this.presets.find(p => p.is_default === 0);
|
||||
if (firstUserPreset) this.selectedPreset = firstUserPreset;
|
||||
@ -668,12 +644,20 @@ export class SettingsView extends LitElement {
|
||||
}
|
||||
//////// after_modelStateService ////////
|
||||
|
||||
openShortcutEditor() {
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.invoke('open-shortcut-editor');
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.setupEventListeners();
|
||||
this.setupIpcListeners();
|
||||
this.setupWindowResize();
|
||||
this.loadAutoUpdateSetting();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@ -705,6 +689,7 @@ export class SettingsView extends LitElement {
|
||||
} else {
|
||||
this.firebaseUser = null;
|
||||
}
|
||||
this.loadAutoUpdateSetting();
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
@ -732,10 +717,15 @@ export class SettingsView extends LitElement {
|
||||
console.error('[SettingsView] Failed to refresh presets:', error);
|
||||
}
|
||||
};
|
||||
this._shortcutListener = (event, keybinds) => {
|
||||
console.log('[SettingsView] Received updated shortcuts:', keybinds);
|
||||
this.shortcuts = keybinds;
|
||||
};
|
||||
|
||||
ipcRenderer.on('user-state-changed', this._userStateListener);
|
||||
ipcRenderer.on('settings-updated', this._settingsUpdatedListener);
|
||||
ipcRenderer.on('presets-updated', this._presetsUpdatedListener);
|
||||
ipcRenderer.on('shortcuts-updated', this._shortcutListener);
|
||||
}
|
||||
|
||||
cleanupIpcListeners() {
|
||||
@ -752,6 +742,9 @@ export class SettingsView extends LitElement {
|
||||
if (this._presetsUpdatedListener) {
|
||||
ipcRenderer.removeListener('presets-updated', this._presetsUpdatedListener);
|
||||
}
|
||||
if (this._shortcutListener) {
|
||||
ipcRenderer.removeListener('shortcuts-updated', this._shortcutListener);
|
||||
}
|
||||
}
|
||||
|
||||
setupWindowResize() {
|
||||
@ -797,14 +790,41 @@ export class SettingsView extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
// getMainShortcuts() {
|
||||
// return [
|
||||
// { name: 'Show / Hide', key: '\\' },
|
||||
// { name: 'Ask Anything', key: '↵' },
|
||||
// { name: 'Scroll AI Response', key: '↕' }
|
||||
// ];
|
||||
// }
|
||||
getMainShortcuts() {
|
||||
return [
|
||||
{ name: 'Show / Hide', key: '\\' },
|
||||
{ name: 'Ask Anything', key: '↵' },
|
||||
{ name: 'Scroll AI Response', key: '↕' }
|
||||
{ name: 'Show / Hide', accelerator: this.shortcuts.toggleVisibility },
|
||||
{ name: 'Ask Anything', accelerator: this.shortcuts.nextStep },
|
||||
{ name: 'Scroll Up Response', accelerator: this.shortcuts.scrollUp },
|
||||
{ name: 'Scroll Down Response', accelerator: this.shortcuts.scrollDown },
|
||||
];
|
||||
}
|
||||
|
||||
renderShortcutKeys(accelerator) {
|
||||
if (!accelerator) return html`N/A`;
|
||||
|
||||
const keyMap = {
|
||||
'Cmd': '⌘', 'Command': '⌘', 'Ctrl': '⌃', 'Alt': '⌥', 'Shift': '⇧', 'Enter': '↵',
|
||||
'Up': '↑', 'Down': '↓', 'Left': '←', 'Right': '→'
|
||||
};
|
||||
|
||||
// scrollDown/scrollUp의 특수 처리
|
||||
if (accelerator.includes('↕')) {
|
||||
const keys = accelerator.replace('↕','').split('+');
|
||||
keys.push('↕');
|
||||
return html`${keys.map(key => html`<span class="shortcut-key">${keyMap[key] || key}</span>`)}`;
|
||||
}
|
||||
|
||||
const keys = accelerator.split('+');
|
||||
return html`${keys.map(key => html`<span class="shortcut-key">${keyMap[key] || key}</span>`)}`;
|
||||
}
|
||||
|
||||
togglePresets() {
|
||||
this.showPresets = !this.showPresets;
|
||||
}
|
||||
@ -1131,14 +1151,20 @@ export class SettingsView extends LitElement {
|
||||
|
||||
${apiKeyManagementHTML}
|
||||
${modelSelectionHTML}
|
||||
|
||||
<div class="buttons-section" style="border-top: 1px solid rgba(255, 255, 255, 0.1); padding-top: 6px; margin-top: 6px;">
|
||||
<button class="settings-button full-width" @click=${this.openShortcutEditor}>
|
||||
Edit Shortcuts
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="shortcuts-section">
|
||||
${this.getMainShortcuts().map(shortcut => html`
|
||||
<div class="shortcut-item">
|
||||
<span class="shortcut-name">${shortcut.name}</span>
|
||||
<div class="shortcut-keys">
|
||||
<span class="cmd-key">⌘</span>
|
||||
<span class="shortcut-key">${shortcut.key}</span>
|
||||
${this.renderShortcutKeys(shortcut.accelerator)}
|
||||
</div>
|
||||
</div>
|
||||
`)}
|
||||
@ -1177,6 +1203,9 @@ export class SettingsView extends LitElement {
|
||||
<button class="settings-button full-width" @click=${this.handlePersonalize}>
|
||||
<span>Personalize / Meeting Notes</span>
|
||||
</button>
|
||||
<button class="settings-button full-width" @click=${this.handleToggleAutoUpdate} ?disabled=${this.autoUpdateLoading}>
|
||||
<span>Automatic Updates: ${this.autoUpdateEnabled ? 'On' : 'Off'}</span>
|
||||
</button>
|
||||
|
||||
<div class="move-buttons">
|
||||
<button class="settings-button half-width" @click=${this.handleMoveLeft}>
|
||||
|
235
src/features/settings/ShortCutSettingsView.js
Normal file
235
src/features/settings/ShortCutSettingsView.js
Normal file
@ -0,0 +1,235 @@
|
||||
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
|
||||
|
||||
const commonSystemShortcuts = new Set([
|
||||
'Cmd+Q', 'Cmd+W', 'Cmd+A', 'Cmd+S', 'Cmd+Z', 'Cmd+X', 'Cmd+C', 'Cmd+V', 'Cmd+P', 'Cmd+F', 'Cmd+G', 'Cmd+H', 'Cmd+M', 'Cmd+N', 'Cmd+O', 'Cmd+T',
|
||||
'Ctrl+Q', 'Ctrl+W', 'Ctrl+A', 'Ctrl+S', 'Ctrl+Z', 'Ctrl+X', 'Ctrl+C', 'Ctrl+V', 'Ctrl+P', 'Ctrl+F', 'Ctrl+G', 'Ctrl+H', 'Ctrl+M', 'Ctrl+N', 'Ctrl+O', 'Ctrl+T'
|
||||
]);
|
||||
|
||||
const displayNameMap = {
|
||||
nextStep: 'Ask Anything',
|
||||
moveUp: 'Move Up Window',
|
||||
moveDown: 'Move Down Window',
|
||||
scrollUp: 'Scroll Up Response',
|
||||
scrollDown: 'Scroll Down Response',
|
||||
};
|
||||
|
||||
export class ShortcutSettingsView extends LitElement {
|
||||
static styles = css`
|
||||
* { font-family:'Helvetica Neue',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
||||
cursor:default; user-select:none; box-sizing:border-box; }
|
||||
|
||||
:host { display:flex; width:100%; height:100%; color:white; }
|
||||
|
||||
.container { display:flex; flex-direction:column; height:100%;
|
||||
background:rgba(20,20,20,.9); border-radius:12px;
|
||||
outline:.5px rgba(255,255,255,.2) solid; outline-offset:-1px;
|
||||
position:relative; overflow:hidden; padding:12px; }
|
||||
|
||||
.close-button{position:absolute;top:10px;right:10px;inline-size:14px;block-size:14px;
|
||||
background:rgba(255,255,255,.1);border:none;border-radius:3px;
|
||||
color:rgba(255,255,255,.7);display:grid;place-items:center;
|
||||
font-size:14px;line-height:0;cursor:pointer;transition:.15s;z-index:10;}
|
||||
.close-button:hover{background:rgba(255,255,255,.2);color:rgba(255,255,255,.9);}
|
||||
|
||||
.title{font-size:14px;font-weight:500;margin:0 0 8px;padding-bottom:8px;
|
||||
border-bottom:1px solid rgba(255,255,255,.1);text-align:center;}
|
||||
|
||||
.scroll-area{flex:1 1 auto;overflow-y:auto;margin:0 -4px;padding:4px;}
|
||||
|
||||
.shortcut-entry{display:flex;align-items:center;width:100%;gap:8px;
|
||||
margin-bottom:8px;font-size:12px;padding:4px;}
|
||||
.shortcut-name{flex:1 1 auto;color:rgba(255,255,255,.9);font-weight:300;
|
||||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
|
||||
.action-btn{background:none;border:none;color:rgba(0,122,255,.8);
|
||||
font-size:11px;padding:0 4px;cursor:pointer;transition:.15s;}
|
||||
.action-btn:hover{color:#0a84ff;text-decoration:underline;}
|
||||
|
||||
.shortcut-input{inline-size:120px;background:rgba(0,0,0,.2);
|
||||
border:1px solid rgba(255,255,255,.2);border-radius:4px;
|
||||
padding:4px 6px;font:11px 'SF Mono','Menlo',monospace;
|
||||
color:white;text-align:right;cursor:text;margin-left:auto;}
|
||||
.shortcut-input:focus,.shortcut-input.capturing{
|
||||
outline:none;border-color:rgba(0,122,255,.6);
|
||||
box-shadow:0 0 0 1px rgba(0,122,255,.3);}
|
||||
|
||||
.feedback{font-size:10px;margin-top:2px;min-height:12px;}
|
||||
.feedback.error{color:#ef4444;}
|
||||
.feedback.success{color:#22c55e;}
|
||||
|
||||
.actions{display:flex;gap:4px;padding-top:8px;border-top:1px solid rgba(255,255,255,.1);}
|
||||
.settings-button{flex:1;background:rgba(255,255,255,.1);
|
||||
border:1px solid rgba(255,255,255,.2);border-radius:4px;
|
||||
color:white;padding:5px 10px;font-size:11px;cursor:pointer;transition:.15s;}
|
||||
.settings-button:hover{background:rgba(255,255,255,.15);}
|
||||
.settings-button.primary{background:rgba(0,122,255,.25);border-color:rgba(0,122,255,.6);}
|
||||
.settings-button.primary:hover{background:rgba(0,122,255,.35);}
|
||||
.settings-button.danger{background:rgba(255,59,48,.1);border-color:rgba(255,59,48,.3);
|
||||
color:rgba(255,59,48,.9);}
|
||||
.settings-button.danger:hover{background:rgba(255,59,48,.15);}
|
||||
`;
|
||||
|
||||
static properties = {
|
||||
shortcuts: { type: Object, state: true },
|
||||
isLoading: { type: Boolean, state: true },
|
||||
capturingKey: { type: String, state: true },
|
||||
feedback: { type:Object, state:true }
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.shortcuts = {};
|
||||
this.feedback = {};
|
||||
this.isLoading = true;
|
||||
this.capturingKey = null;
|
||||
this.ipcRenderer = window.require ? window.require('electron').ipcRenderer : null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.ipcRenderer) return;
|
||||
this.loadShortcutsHandler = (event, keybinds) => {
|
||||
this.shortcuts = keybinds;
|
||||
this.isLoading = false;
|
||||
};
|
||||
this.ipcRenderer.on('load-shortcuts', this.loadShortcutsHandler);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.ipcRenderer && this.loadShortcutsHandler) {
|
||||
this.ipcRenderer.removeListener('load-shortcuts', this.loadShortcutsHandler);
|
||||
}
|
||||
}
|
||||
|
||||
handleKeydown(e, shortcutKey){
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
const result = this._parseAccelerator(e);
|
||||
if(!result) return; // modifier키만 누른 상태
|
||||
|
||||
const {accel, error} = result;
|
||||
if(error){
|
||||
this.feedback = {...this.feedback, [shortcutKey]:{type:'error',msg:error}};
|
||||
return;
|
||||
}
|
||||
// 성공
|
||||
this.shortcuts = {...this.shortcuts, [shortcutKey]:accel};
|
||||
this.feedback = {...this.feedback, [shortcutKey]:{type:'success',msg:'Shortcut set'}};
|
||||
this.stopCapture();
|
||||
}
|
||||
|
||||
_parseAccelerator(e){
|
||||
/* returns {accel?, error?} */
|
||||
const parts=[]; if(e.metaKey) parts.push('Cmd');
|
||||
if(e.ctrlKey) parts.push('Ctrl');
|
||||
if(e.altKey) parts.push('Alt');
|
||||
if(e.shiftKey) parts.push('Shift');
|
||||
|
||||
const isModifier=['Meta','Control','Alt','Shift'].includes(e.key);
|
||||
if(isModifier) return null;
|
||||
|
||||
const map={ArrowUp:'Up',ArrowDown:'Down',ArrowLeft:'Left',ArrowRight:'Right',' ':'Space'};
|
||||
parts.push(e.key.length===1? e.key.toUpperCase() : (map[e.key]||e.key));
|
||||
const accel=parts.join('+');
|
||||
|
||||
/* ---- validation ---- */
|
||||
if(parts.length===1) return {error:'Invalid shortcut: needs a modifier'};
|
||||
if(parts.length>4) return {error:'Invalid shortcut: max 4 keys'};
|
||||
if(commonSystemShortcuts.has(accel)) return {error:'Invalid shortcut: system reserved'};
|
||||
return {accel};
|
||||
}
|
||||
|
||||
startCapture(key){ this.capturingKey = key; this.feedback = {...this.feedback, [key]:undefined}; }
|
||||
|
||||
disableShortcut(key){
|
||||
this.shortcuts = {...this.shortcuts, [key]:''}; // 공백 => 작동 X
|
||||
this.feedback = {...this.feedback, [key]:{type:'success',msg:'Shortcut disabled'}};
|
||||
}
|
||||
|
||||
stopCapture() {
|
||||
this.capturingKey = null;
|
||||
}
|
||||
|
||||
async handleSave() {
|
||||
if (!this.ipcRenderer) return;
|
||||
const result = await this.ipcRenderer.invoke('save-shortcuts', this.shortcuts);
|
||||
if (!result.success) {
|
||||
alert('Failed to save shortcuts: ' + result.error);
|
||||
}
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
if (!this.ipcRenderer) return;
|
||||
this.ipcRenderer.send('close-shortcut-editor');
|
||||
}
|
||||
|
||||
async handleResetToDefault() {
|
||||
if (!this.ipcRenderer) return;
|
||||
const confirmation = confirm("Are you sure you want to reset all shortcuts to their default values?");
|
||||
if (!confirmation) return;
|
||||
|
||||
try {
|
||||
const defaultShortcuts = await this.ipcRenderer.invoke('get-default-shortcuts');
|
||||
this.shortcuts = defaultShortcuts;
|
||||
} catch (error) {
|
||||
alert('Failed to load default settings.');
|
||||
}
|
||||
}
|
||||
|
||||
formatShortcutName(name) {
|
||||
if (displayNameMap[name]) {
|
||||
return displayNameMap[name];
|
||||
}
|
||||
const result = name.replace(/([A-Z])/g, " $1");
|
||||
return result.charAt(0).toUpperCase() + result.slice(1);
|
||||
}
|
||||
|
||||
render(){
|
||||
if(this.isLoading){
|
||||
return html`<div class="container"><div class="loading-state">Loading Shortcuts...</div></div>`;
|
||||
}
|
||||
return html`
|
||||
<div class="container">
|
||||
<button class="close-button" @click=${this.handleClose} title="Close">×</button>
|
||||
<h1 class="title">Edit Shortcuts</h1>
|
||||
|
||||
<div class="scroll-area">
|
||||
${Object.keys(this.shortcuts).map(key=>html`
|
||||
<div>
|
||||
<div class="shortcut-entry">
|
||||
<span class="shortcut-name">${this.formatShortcutName(key)}</span>
|
||||
|
||||
<!-- Edit & Disable 버튼 -->
|
||||
<button class="action-btn" @click=${()=>this.startCapture(key)}>Edit</button>
|
||||
<button class="action-btn" @click=${()=>this.disableShortcut(key)}>Disable</button>
|
||||
|
||||
<input readonly
|
||||
class="shortcut-input ${this.capturingKey===key?'capturing':''}"
|
||||
.value=${this.shortcuts[key]||''}
|
||||
placeholder=${this.capturingKey===key?'Press new shortcut…':'Click to edit'}
|
||||
@click=${()=>this.startCapture(key)}
|
||||
@keydown=${e=>this.handleKeydown(e,key)}
|
||||
@blur=${()=>this.stopCapture()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
${this.feedback[key] ? html`
|
||||
<div class="feedback ${this.feedback[key].type}">
|
||||
${this.feedback[key].msg}
|
||||
</div>` : html`<div class="feedback"></div>`
|
||||
}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="settings-button" @click=${this.handleClose}>Cancel</button>
|
||||
<button class="settings-button danger" @click=${this.handleResetToDefault}>Reset to Default</button>
|
||||
<button class="settings-button primary" @click=${this.handleSave}>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('shortcut-settings-view', ShortcutSettingsView);
|
@ -18,4 +18,6 @@ module.exports = {
|
||||
createPreset: (...args) => getRepository().createPreset(...args),
|
||||
updatePreset: (...args) => getRepository().updatePreset(...args),
|
||||
deletePreset: (...args) => getRepository().deletePreset(...args),
|
||||
getAutoUpdate: (...args) => getRepository().getAutoUpdate(...args),
|
||||
setAutoUpdate: (...args) => getRepository().setAutoUpdate(...args),
|
||||
};
|
@ -90,10 +90,57 @@ function deletePreset(id, uid) {
|
||||
}
|
||||
}
|
||||
|
||||
function getAutoUpdate(uid) {
|
||||
const db = sqliteClient.getDb();
|
||||
const targetUid = uid;
|
||||
|
||||
try {
|
||||
const row = db.prepare('SELECT auto_update_enabled FROM users WHERE uid = ?').get(targetUid);
|
||||
|
||||
if (row) {
|
||||
console.log('SQLite: Auto update setting found:', row.auto_update_enabled);
|
||||
return row.auto_update_enabled !== 0;
|
||||
} else {
|
||||
// User doesn't exist, create them with default settings
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const stmt = db.prepare(
|
||||
'INSERT OR REPLACE INTO users (uid, display_name, email, created_at, auto_update_enabled) VALUES (?, ?, ?, ?, ?)');
|
||||
stmt.run(targetUid, 'User', 'user@example.com', now, 1);
|
||||
return true; // default to enabled
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SQLite: Error getting auto_update_enabled setting:', error);
|
||||
return true; // fallback to enabled
|
||||
}
|
||||
}
|
||||
|
||||
function setAutoUpdate(uid, isEnabled) {
|
||||
const db = sqliteClient.getDb();
|
||||
const targetUid = uid || sqliteClient.defaultUserId;
|
||||
|
||||
try {
|
||||
const result = db.prepare('UPDATE users SET auto_update_enabled = ? WHERE uid = ?').run(isEnabled ? 1 : 0, targetUid);
|
||||
|
||||
// If no rows were updated, the user might not exist, so create them
|
||||
if (result.changes === 0) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const stmt = db.prepare('INSERT OR REPLACE INTO users (uid, display_name, email, created_at, auto_update_enabled) VALUES (?, ?, ?, ?, ?)');
|
||||
stmt.run(targetUid, 'User', 'user@example.com', now, isEnabled ? 1 : 0);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('SQLite: Error setting auto-update:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPresets,
|
||||
getPresetTemplates,
|
||||
createPreset,
|
||||
updatePreset,
|
||||
deletePreset
|
||||
deletePreset,
|
||||
getAutoUpdate,
|
||||
setAutoUpdate
|
||||
};
|
@ -383,6 +383,29 @@ async function updateContentProtection(enabled) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getAutoUpdateSetting() {
|
||||
try {
|
||||
const uid = authService.getCurrentUserId();
|
||||
// This can be awaited if the repository returns a promise.
|
||||
// Assuming it's synchronous for now based on original structure.
|
||||
return settingsRepository.getAutoUpdate(uid);
|
||||
} catch (error) {
|
||||
console.error('[SettingsService] Error getting auto update setting:', error);
|
||||
return true; // Fallback to enabled
|
||||
}
|
||||
}
|
||||
|
||||
async function setAutoUpdateSetting(isEnabled) {
|
||||
try {
|
||||
const uid = authService.getCurrentUserId();
|
||||
await settingsRepository.setAutoUpdate(uid, isEnabled);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[SettingsService] Error setting auto update setting:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
// cleanup
|
||||
windowNotificationManager.cleanup();
|
||||
@ -428,6 +451,15 @@ function initialize() {
|
||||
ipcMain.handle('settings:updateContentProtection', async (event, enabled) => {
|
||||
return await updateContentProtection(enabled);
|
||||
});
|
||||
|
||||
ipcMain.handle('settings:get-auto-update', async () => {
|
||||
return await getAutoUpdateSetting();
|
||||
});
|
||||
|
||||
ipcMain.handle('settings:set-auto-update', async (event, isEnabled) => {
|
||||
console.log('[SettingsService] Setting auto update setting:', isEnabled);
|
||||
return await setAutoUpdateSetting(isEnabled);
|
||||
});
|
||||
|
||||
console.log('[SettingsService] Initialized and ready.');
|
||||
}
|
||||
@ -459,4 +491,5 @@ module.exports = {
|
||||
saveApiKey,
|
||||
removeApiKey,
|
||||
updateContentProtection,
|
||||
getAutoUpdateSetting,
|
||||
};
|
13
src/index.js
13
src/index.js
@ -26,6 +26,7 @@ const askService = require('./features/ask/askService');
|
||||
const settingsService = require('./features/settings/settingsService');
|
||||
const sessionRepository = require('./common/repositories/session');
|
||||
const ModelStateService = require('./common/services/modelStateService');
|
||||
const sqliteClient = require('./common/services/sqliteClient');
|
||||
|
||||
const eventBridge = new EventEmitter();
|
||||
let WEB_PORT = 3000;
|
||||
@ -189,10 +190,12 @@ app.whenReady().then(async () => {
|
||||
// Clean up zombie sessions from previous runs first
|
||||
sessionRepository.endAllActiveSessions();
|
||||
|
||||
authService.initialize();
|
||||
await authService.initialize();
|
||||
|
||||
//////// after_modelStateService ////////
|
||||
modelStateService.initialize();
|
||||
//////// after_modelStateService ////////
|
||||
|
||||
listenService.setupIpcHandlers();
|
||||
askService.initialize();
|
||||
settingsService.initialize();
|
||||
@ -213,6 +216,7 @@ app.whenReady().then(async () => {
|
||||
);
|
||||
}
|
||||
|
||||
// initAutoUpdater should be called after auth is initialized
|
||||
initAutoUpdater();
|
||||
|
||||
// Process any pending deep link after everything is initialized
|
||||
@ -649,8 +653,13 @@ async function startWebStack() {
|
||||
}
|
||||
|
||||
// Auto-update initialization
|
||||
function initAutoUpdater() {
|
||||
async function initAutoUpdater() {
|
||||
try {
|
||||
const autoUpdateEnabled = await settingsService.getAutoUpdateSetting();
|
||||
if (!autoUpdateEnabled) {
|
||||
console.log('[AutoUpdater] Skipped because auto-updates are disabled in settings');
|
||||
return;
|
||||
}
|
||||
// Skip auto-updater in development mode
|
||||
if (!app.isPackaged) {
|
||||
console.log('[AutoUpdater] Skipped in development (app is not packaged)');
|
||||
|
Loading…
x
Reference in New Issue
Block a user