Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbad79875c | ||
|
|
7f15b65eb1 | ||
|
|
d6dbaa3141 | ||
|
|
2ebde60dcd | ||
|
|
0d56e06724 | ||
|
|
526bc4e877 | ||
|
|
684b61755c | ||
|
|
1b74968006 | ||
|
|
4cf48ee0af | ||
|
|
494e692738 | ||
|
|
8b216bbb33 | ||
|
|
bd62cf5524 | ||
|
|
bfd76dc0c1 |
+78
-73
@@ -1,78 +1,83 @@
|
|||||||
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
|
const { FusesPlugin } = require("@electron-forge/plugin-fuses");
|
||||||
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
|
const { FuseV1Options, FuseVersion } = require("@electron/fuses");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
packagerConfig: {
|
packagerConfig: {
|
||||||
asar: {
|
asar: {
|
||||||
unpack: '**/{onnxruntime-node,onnxruntime-common,@huggingface/transformers,sharp,@img}/**',
|
unpack:
|
||||||
},
|
"**/{onnxruntime-node,onnxruntime-common,@huggingface/transformers,sharp,@img}/**",
|
||||||
extraResource: ['./src/assets/SystemAudioDump'],
|
|
||||||
name: 'Cheating Daddy',
|
|
||||||
icon: 'src/assets/logo',
|
|
||||||
// use `security find-identity -v -p codesigning` to find your identity
|
|
||||||
// for macos signing
|
|
||||||
// also fuck apple
|
|
||||||
// osxSign: {
|
|
||||||
// identity: '<paste your identity here>',
|
|
||||||
// optionsForFile: (filePath) => {
|
|
||||||
// return {
|
|
||||||
// entitlements: 'entitlements.plist',
|
|
||||||
// };
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// notarize if off cuz i ran this for 6 hours and it still didnt finish
|
|
||||||
// osxNotarize: {
|
|
||||||
// appleId: 'your apple id',
|
|
||||||
// appleIdPassword: 'app specific password',
|
|
||||||
// teamId: 'your team id',
|
|
||||||
// },
|
|
||||||
},
|
},
|
||||||
rebuildConfig: {},
|
extraResource: ["./src/assets/SystemAudioDump"],
|
||||||
makers: [
|
name: "Mastermind",
|
||||||
{
|
icon: "src/assets/logo",
|
||||||
name: '@electron-forge/maker-squirrel',
|
// use `security find-identity -v -p codesigning` to find your identity
|
||||||
config: {
|
// for macos signing
|
||||||
name: 'cheating-daddy',
|
// also fuck apple
|
||||||
productName: 'Cheating Daddy',
|
// osxSign: {
|
||||||
shortcutName: 'Cheating Daddy',
|
// identity: '<paste your identity here>',
|
||||||
createDesktopShortcut: true,
|
// optionsForFile: (filePath) => {
|
||||||
createStartMenuShortcut: true,
|
// return {
|
||||||
},
|
// entitlements: 'entitlements.plist',
|
||||||
|
// };
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// notarize if off cuz i ran this for 6 hours and it still didnt finish
|
||||||
|
// osxNotarize: {
|
||||||
|
// appleId: 'your apple id',
|
||||||
|
// appleIdPassword: 'app specific password',
|
||||||
|
// teamId: 'your team id',
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
rebuildConfig: {
|
||||||
|
// Ensure onnxruntime-node is rebuilt against Electron's Node.js headers
|
||||||
|
// so the native binding matches the ABI used in packaged builds.
|
||||||
|
onlyModules: ["onnxruntime-node", "sharp"],
|
||||||
|
},
|
||||||
|
makers: [
|
||||||
|
{
|
||||||
|
name: "@electron-forge/maker-squirrel",
|
||||||
|
config: {
|
||||||
|
name: "mastermind",
|
||||||
|
productName: "Mastermind",
|
||||||
|
shortcutName: "Mastermind",
|
||||||
|
createDesktopShortcut: true,
|
||||||
|
createStartMenuShortcut: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "@electron-forge/maker-dmg",
|
||||||
|
platforms: ["darwin"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "@reforged/maker-appimage",
|
||||||
|
platforms: ["linux"],
|
||||||
|
config: {
|
||||||
|
options: {
|
||||||
|
name: "Mastermind",
|
||||||
|
productName: "Mastermind",
|
||||||
|
genericName: "AI Assistant",
|
||||||
|
description: "AI assistant for interviews and learning",
|
||||||
|
categories: ["Development", "Education"],
|
||||||
|
icon: "src/assets/logo.png",
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
name: '@electron-forge/maker-dmg',
|
},
|
||||||
platforms: ['darwin'],
|
],
|
||||||
},
|
plugins: [
|
||||||
{
|
{
|
||||||
name: '@reforged/maker-appimage',
|
name: "@electron-forge/plugin-auto-unpack-natives",
|
||||||
platforms: ['linux'],
|
config: {},
|
||||||
config: {
|
},
|
||||||
options: {
|
// Fuses are used to enable/disable various Electron functionality
|
||||||
name: 'Cheating Daddy',
|
// at package time, before code signing the application
|
||||||
productName: 'Cheating Daddy',
|
new FusesPlugin({
|
||||||
genericName: 'AI Assistant',
|
version: FuseVersion.V1,
|
||||||
description: 'AI assistant for interviews and learning',
|
[FuseV1Options.RunAsNode]: false,
|
||||||
categories: ['Development', 'Education'],
|
[FuseV1Options.EnableCookieEncryption]: true,
|
||||||
icon: 'src/assets/logo.png'
|
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
||||||
}
|
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
||||||
},
|
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
|
||||||
},
|
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
||||||
],
|
}),
|
||||||
plugins: [
|
],
|
||||||
{
|
|
||||||
name: '@electron-forge/plugin-auto-unpack-natives',
|
|
||||||
config: {},
|
|
||||||
},
|
|
||||||
// Fuses are used to enable/disable various Electron functionality
|
|
||||||
// at package time, before code signing the application
|
|
||||||
new FusesPlugin({
|
|
||||||
version: FuseVersion.V1,
|
|
||||||
[FuseV1Options.RunAsNode]: false,
|
|
||||||
[FuseV1Options.EnableCookieEncryption]: true,
|
|
||||||
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
|
||||||
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
|
||||||
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
|
|
||||||
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|||||||
+52
-49
@@ -1,52 +1,55 @@
|
|||||||
{
|
{
|
||||||
"name": "cheating-daddy",
|
"name": "mastermind",
|
||||||
"productName": "cheating-daddy",
|
"productName": "Mastermind",
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"description": "cheating daddy",
|
"description": "Mastermind AI assistant",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron-forge start",
|
"start": "electron-forge start",
|
||||||
"package": "electron-forge package",
|
"package": "electron-forge package",
|
||||||
"make": "electron-forge make",
|
"make": "electron-forge make",
|
||||||
"publish": "electron-forge publish",
|
"publish": "electron-forge publish",
|
||||||
"lint": "echo \"No linting configured\""
|
"lint": "echo \"No linting configured\"",
|
||||||
},
|
"postinstall": "electron-rebuild -f -w onnxruntime-node"
|
||||||
"keywords": [
|
},
|
||||||
"cheating daddy",
|
"keywords": [
|
||||||
"cheating daddy ai",
|
"mastermind",
|
||||||
"cheating daddy ai assistant",
|
"mastermind ai",
|
||||||
"cheating daddy ai assistant for interviews",
|
"mastermind ai assistant",
|
||||||
"cheating daddy ai assistant for interviews"
|
"mastermind ai assistant for interviews",
|
||||||
],
|
"mastermind ai assistant for interviews"
|
||||||
"author": {
|
],
|
||||||
"name": "sohzm",
|
"author": {
|
||||||
"email": "sohambharambe9@gmail.com"
|
"name": "ShiftyX1",
|
||||||
},
|
"email": "lead@pyserve.org"
|
||||||
"license": "GPL-3.0",
|
},
|
||||||
"dependencies": {
|
"license": "GPL-3.0",
|
||||||
"@google/genai": "^1.41.0",
|
"dependencies": {
|
||||||
"@huggingface/transformers": "^3.8.1",
|
"@google/genai": "^1.41.0",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"@huggingface/transformers": "^3.8.1",
|
||||||
"ollama": "^0.6.3",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"p-retry": "^4.6.2",
|
"ollama": "^0.6.3",
|
||||||
"ws": "^8.19.0"
|
"openai": "^6.22.0",
|
||||||
},
|
"p-retry": "^4.6.2",
|
||||||
"devDependencies": {
|
"ws": "^8.19.0"
|
||||||
"@electron-forge/cli": "^7.8.1",
|
},
|
||||||
"@electron-forge/maker-deb": "^7.8.1",
|
"devDependencies": {
|
||||||
"@electron-forge/maker-dmg": "^7.8.1",
|
"@electron/rebuild": "^3.7.1",
|
||||||
"@electron-forge/maker-rpm": "^7.8.1",
|
"@electron-forge/cli": "^7.8.1",
|
||||||
"@electron-forge/maker-squirrel": "^7.8.1",
|
"@electron-forge/maker-deb": "^7.8.1",
|
||||||
"@electron-forge/maker-zip": "^7.8.1",
|
"@electron-forge/maker-dmg": "^7.8.1",
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
|
"@electron-forge/maker-rpm": "^7.8.1",
|
||||||
"@electron-forge/plugin-fuses": "^7.8.1",
|
"@electron-forge/maker-squirrel": "^7.8.1",
|
||||||
"@electron/fuses": "^1.8.0",
|
"@electron-forge/maker-zip": "^7.8.1",
|
||||||
"@reforged/maker-appimage": "^5.0.0",
|
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
|
||||||
"electron": "^30.0.5"
|
"@electron-forge/plugin-fuses": "^7.8.1",
|
||||||
},
|
"@electron/fuses": "^1.8.0",
|
||||||
"pnpm": {
|
"@reforged/maker-appimage": "^5.0.0",
|
||||||
"overrides": {
|
"electron": "^30.0.5"
|
||||||
"p-retry": "4.6.2"
|
},
|
||||||
}
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"p-retry": "4.6.2"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+19
@@ -23,6 +23,9 @@ importers:
|
|||||||
ollama:
|
ollama:
|
||||||
specifier: ^0.6.3
|
specifier: ^0.6.3
|
||||||
version: 0.6.3
|
version: 0.6.3
|
||||||
|
openai:
|
||||||
|
specifier: ^6.22.0
|
||||||
|
version: 6.22.0(ws@8.19.0)
|
||||||
p-retry:
|
p-retry:
|
||||||
specifier: 4.6.2
|
specifier: 4.6.2
|
||||||
version: 4.6.2
|
version: 4.6.2
|
||||||
@@ -1750,6 +1753,18 @@ packages:
|
|||||||
onnxruntime-web@1.22.0-dev.20250409-89f8206ba4:
|
onnxruntime-web@1.22.0-dev.20250409-89f8206ba4:
|
||||||
resolution: {integrity: sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==}
|
resolution: {integrity: sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==}
|
||||||
|
|
||||||
|
openai@6.22.0:
|
||||||
|
resolution: {integrity: sha512-7Yvy17F33Bi9RutWbsaYt5hJEEJ/krRPOrwan+f9aCPuMat1WVsb2VNSII5W1EksKT6fF69TG/xj4XzodK3JZw==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
ws: ^8.18.0
|
||||||
|
zod: ^3.25 || ^4.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
ws:
|
||||||
|
optional: true
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
ora@5.4.1:
|
ora@5.4.1:
|
||||||
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
|
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -4522,6 +4537,10 @@ snapshots:
|
|||||||
platform: 1.3.6
|
platform: 1.3.6
|
||||||
protobufjs: 7.5.4
|
protobufjs: 7.5.4
|
||||||
|
|
||||||
|
openai@6.22.0(ws@8.19.0):
|
||||||
|
optionalDependencies:
|
||||||
|
ws: 8.19.0
|
||||||
|
|
||||||
ora@5.4.1:
|
ora@5.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
bl: 4.1.0
|
bl: 4.1.0
|
||||||
|
|||||||
@@ -242,15 +242,15 @@ export class AppHeader extends LitElement {
|
|||||||
|
|
||||||
getViewTitle() {
|
getViewTitle() {
|
||||||
const titles = {
|
const titles = {
|
||||||
onboarding: 'Welcome to Cheating Daddy',
|
onboarding: 'Welcome to Mastermind',
|
||||||
main: 'Cheating Daddy',
|
main: 'Mastermind',
|
||||||
customize: 'Customize',
|
customize: 'Customize',
|
||||||
help: 'Help & Shortcuts',
|
help: 'Help & Shortcuts',
|
||||||
history: 'Conversation History',
|
history: 'Conversation History',
|
||||||
advanced: 'Advanced Tools',
|
advanced: 'Advanced Tools',
|
||||||
assistant: 'Cheating Daddy',
|
assistant: 'Mastermind',
|
||||||
};
|
};
|
||||||
return titles[this.currentView] || 'Cheating Daddy';
|
return titles[this.currentView] || 'Mastermind';
|
||||||
}
|
}
|
||||||
|
|
||||||
getElapsedTime() {
|
getElapsedTime() {
|
||||||
|
|||||||
+1083
-836
File diff suppressed because it is too large
Load Diff
+1143
-666
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1480
-823
File diff suppressed because it is too large
Load Diff
@@ -312,7 +312,7 @@ export class OnboardingView extends LitElement {
|
|||||||
if (this.currentSlide === 0) {
|
if (this.currentSlide === 0) {
|
||||||
return html`
|
return html`
|
||||||
<div class="slide">
|
<div class="slide">
|
||||||
<div class="slide-title">Cheating Daddy</div>
|
<div class="slide-title">Mastermind</div>
|
||||||
<div class="slide-text">Real-time AI that listens, watches, and helps during interviews, meetings, and exams.</div>
|
<div class="slide-text">Real-time AI that listens, watches, and helps during interviews, meetings, and exams.</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn-primary" @click=${() => { this.currentSlide = 1; }}>Continue</button>
|
<button class="btn-primary" @click=${() => { this.currentSlide = 1; }}>Continue</button>
|
||||||
|
|||||||
+292
-255
@@ -1,299 +1,336 @@
|
|||||||
if (require('electron-squirrel-startup')) {
|
if (require("electron-squirrel-startup")) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { app, BrowserWindow, shell, ipcMain } = require('electron');
|
// ── Global crash handlers to prevent silent process termination ──
|
||||||
const { createWindow, updateGlobalShortcuts } = require('./utils/window');
|
process.on("uncaughtException", (error) => {
|
||||||
const { setupGeminiIpcHandlers, stopMacOSAudioCapture, sendToRenderer } = require('./utils/gemini');
|
console.error("[FATAL] Uncaught exception:", error);
|
||||||
const storage = require('./storage');
|
try {
|
||||||
|
const { sendToRenderer } = require("./utils/gemini");
|
||||||
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Fatal error: " + (error?.message || "unknown"),
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
// sendToRenderer may not be available yet
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (reason) => {
|
||||||
|
console.error("[FATAL] Unhandled promise rejection:", reason);
|
||||||
|
try {
|
||||||
|
const { sendToRenderer } = require("./utils/gemini");
|
||||||
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Unhandled error: " +
|
||||||
|
(reason instanceof Error ? reason.message : String(reason)),
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
// sendToRenderer may not be available yet
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { app, BrowserWindow, shell, ipcMain } = require("electron");
|
||||||
|
const { createWindow, updateGlobalShortcuts } = require("./utils/window");
|
||||||
|
const {
|
||||||
|
setupGeminiIpcHandlers,
|
||||||
|
stopMacOSAudioCapture,
|
||||||
|
sendToRenderer,
|
||||||
|
} = require("./utils/gemini");
|
||||||
|
const storage = require("./storage");
|
||||||
|
|
||||||
const geminiSessionRef = { current: null };
|
const geminiSessionRef = { current: null };
|
||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
|
|
||||||
function createMainWindow() {
|
function createMainWindow() {
|
||||||
mainWindow = createWindow(sendToRenderer, geminiSessionRef);
|
mainWindow = createWindow(sendToRenderer, geminiSessionRef);
|
||||||
return mainWindow;
|
return mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
// Initialize storage (checks version, resets if needed)
|
// Initialize storage (checks version, resets if needed)
|
||||||
storage.initializeStorage();
|
storage.initializeStorage();
|
||||||
|
|
||||||
// Trigger screen recording permission prompt on macOS if not already granted
|
// Trigger screen recording permission prompt on macOS if not already granted
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === "darwin") {
|
||||||
const { desktopCapturer } = require('electron');
|
const { desktopCapturer } = require("electron");
|
||||||
desktopCapturer.getSources({ types: ['screen'] }).catch(() => {});
|
desktopCapturer.getSources({ types: ["screen"] }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createMainWindow();
|
||||||
|
setupGeminiIpcHandlers(geminiSessionRef);
|
||||||
|
setupStorageIpcHandlers();
|
||||||
|
setupGeneralIpcHandlers();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("window-all-closed", () => {
|
||||||
|
stopMacOSAudioCapture();
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("before-quit", () => {
|
||||||
|
stopMacOSAudioCapture();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("activate", () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
setupGeminiIpcHandlers(geminiSessionRef);
|
}
|
||||||
setupStorageIpcHandlers();
|
|
||||||
setupGeneralIpcHandlers();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
stopMacOSAudioCapture();
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('before-quit', () => {
|
|
||||||
stopMacOSAudioCapture();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
createMainWindow();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function setupStorageIpcHandlers() {
|
function setupStorageIpcHandlers() {
|
||||||
// ============ CONFIG ============
|
// ============ CONFIG ============
|
||||||
ipcMain.handle('storage:get-config', async () => {
|
ipcMain.handle("storage:get-config", async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getConfig() };
|
return { success: true, data: storage.getConfig() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting config:', error);
|
console.error("Error getting config:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:set-config', async (event, config) => {
|
ipcMain.handle("storage:set-config", async (event, config) => {
|
||||||
try {
|
try {
|
||||||
storage.setConfig(config);
|
storage.setConfig(config);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting config:', error);
|
console.error("Error setting config:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:update-config', async (event, key, value) => {
|
ipcMain.handle("storage:update-config", async (event, key, value) => {
|
||||||
try {
|
try {
|
||||||
storage.updateConfig(key, value);
|
storage.updateConfig(key, value);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating config:', error);
|
console.error("Error updating config:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ CREDENTIALS ============
|
// ============ CREDENTIALS ============
|
||||||
ipcMain.handle('storage:get-credentials', async () => {
|
ipcMain.handle("storage:get-credentials", async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getCredentials() };
|
return { success: true, data: storage.getCredentials() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting credentials:', error);
|
console.error("Error getting credentials:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:set-credentials', async (event, credentials) => {
|
ipcMain.handle("storage:set-credentials", async (event, credentials) => {
|
||||||
try {
|
try {
|
||||||
storage.setCredentials(credentials);
|
storage.setCredentials(credentials);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting credentials:', error);
|
console.error("Error setting credentials:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:get-api-key', async () => {
|
ipcMain.handle("storage:get-api-key", async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getApiKey() };
|
return { success: true, data: storage.getApiKey() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting API key:', error);
|
console.error("Error getting API key:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:set-api-key', async (event, apiKey) => {
|
ipcMain.handle("storage:set-api-key", async (event, apiKey) => {
|
||||||
try {
|
try {
|
||||||
storage.setApiKey(apiKey);
|
storage.setApiKey(apiKey);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting API key:', error);
|
console.error("Error setting API key:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:get-groq-api-key', async () => {
|
ipcMain.handle("storage:get-groq-api-key", async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getGroqApiKey() };
|
return { success: true, data: storage.getGroqApiKey() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting Groq API key:', error);
|
console.error("Error getting Groq API key:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:set-groq-api-key', async (event, groqApiKey) => {
|
ipcMain.handle("storage:set-groq-api-key", async (event, groqApiKey) => {
|
||||||
try {
|
try {
|
||||||
storage.setGroqApiKey(groqApiKey);
|
storage.setGroqApiKey(groqApiKey);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting Groq API key:', error);
|
console.error("Error setting Groq API key:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ PREFERENCES ============
|
// ============ PREFERENCES ============
|
||||||
ipcMain.handle('storage:get-preferences', async () => {
|
ipcMain.handle("storage:get-preferences", async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getPreferences() };
|
return { success: true, data: storage.getPreferences() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting preferences:', error);
|
console.error("Error getting preferences:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:set-preferences', async (event, preferences) => {
|
ipcMain.handle("storage:set-preferences", async (event, preferences) => {
|
||||||
try {
|
try {
|
||||||
storage.setPreferences(preferences);
|
storage.setPreferences(preferences);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting preferences:', error);
|
console.error("Error setting preferences:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:update-preference', async (event, key, value) => {
|
ipcMain.handle("storage:update-preference", async (event, key, value) => {
|
||||||
try {
|
try {
|
||||||
storage.updatePreference(key, value);
|
storage.updatePreference(key, value);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating preference:', error);
|
console.error("Error updating preference:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ KEYBINDS ============
|
// ============ KEYBINDS ============
|
||||||
ipcMain.handle('storage:get-keybinds', async () => {
|
ipcMain.handle("storage:get-keybinds", async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getKeybinds() };
|
return { success: true, data: storage.getKeybinds() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting keybinds:', error);
|
console.error("Error getting keybinds:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:set-keybinds', async (event, keybinds) => {
|
ipcMain.handle("storage:set-keybinds", async (event, keybinds) => {
|
||||||
try {
|
try {
|
||||||
storage.setKeybinds(keybinds);
|
storage.setKeybinds(keybinds);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting keybinds:', error);
|
console.error("Error setting keybinds:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ HISTORY ============
|
// ============ HISTORY ============
|
||||||
ipcMain.handle('storage:get-all-sessions', async () => {
|
ipcMain.handle("storage:get-all-sessions", async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getAllSessions() };
|
return { success: true, data: storage.getAllSessions() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting sessions:', error);
|
console.error("Error getting sessions:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:get-session', async (event, sessionId) => {
|
ipcMain.handle("storage:get-session", async (event, sessionId) => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getSession(sessionId) };
|
return { success: true, data: storage.getSession(sessionId) };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting session:', error);
|
console.error("Error getting session:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:save-session', async (event, sessionId, data) => {
|
ipcMain.handle("storage:save-session", async (event, sessionId, data) => {
|
||||||
try {
|
try {
|
||||||
storage.saveSession(sessionId, data);
|
storage.saveSession(sessionId, data);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving session:', error);
|
console.error("Error saving session:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:delete-session', async (event, sessionId) => {
|
ipcMain.handle("storage:delete-session", async (event, sessionId) => {
|
||||||
try {
|
try {
|
||||||
storage.deleteSession(sessionId);
|
storage.deleteSession(sessionId);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting session:', error);
|
console.error("Error deleting session:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:delete-all-sessions', async () => {
|
ipcMain.handle("storage:delete-all-sessions", async () => {
|
||||||
try {
|
try {
|
||||||
storage.deleteAllSessions();
|
storage.deleteAllSessions();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting all sessions:', error);
|
console.error("Error deleting all sessions:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ LIMITS ============
|
// ============ LIMITS ============
|
||||||
ipcMain.handle('storage:get-today-limits', async () => {
|
ipcMain.handle("storage:get-today-limits", async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getTodayLimits() };
|
return { success: true, data: storage.getTodayLimits() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting today limits:', error);
|
console.error("Error getting today limits:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ CLEAR ALL ============
|
// ============ CLEAR ALL ============
|
||||||
ipcMain.handle('storage:clear-all', async () => {
|
ipcMain.handle("storage:clear-all", async () => {
|
||||||
try {
|
try {
|
||||||
storage.clearAllData();
|
storage.clearAllData();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error clearing all data:', error);
|
console.error("Error clearing all data:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupGeneralIpcHandlers() {
|
function setupGeneralIpcHandlers() {
|
||||||
ipcMain.handle('get-app-version', async () => {
|
ipcMain.handle("get-app-version", async () => {
|
||||||
return app.getVersion();
|
return app.getVersion();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('quit-application', async event => {
|
ipcMain.handle("quit-application", async (event) => {
|
||||||
try {
|
try {
|
||||||
stopMacOSAudioCapture();
|
stopMacOSAudioCapture();
|
||||||
app.quit();
|
app.quit();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error quitting application:', error);
|
console.error("Error quitting application:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('open-external', async (event, url) => {
|
ipcMain.handle("open-external", async (event, url) => {
|
||||||
try {
|
try {
|
||||||
await shell.openExternal(url);
|
await shell.openExternal(url);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error opening external URL:', error);
|
console.error("Error opening external URL:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('update-keybinds', (event, newKeybinds) => {
|
ipcMain.on("update-keybinds", (event, newKeybinds) => {
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
// Also save to storage
|
// Also save to storage
|
||||||
storage.setKeybinds(newKeybinds);
|
storage.setKeybinds(newKeybinds);
|
||||||
updateGlobalShortcuts(newKeybinds, mainWindow, sendToRenderer, geminiSessionRef);
|
updateGlobalShortcuts(
|
||||||
}
|
newKeybinds,
|
||||||
});
|
mainWindow,
|
||||||
|
sendToRenderer,
|
||||||
|
geminiSessionRef,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Debug logging from renderer
|
// Debug logging from renderer
|
||||||
ipcMain.on('log-message', (event, msg) => {
|
ipcMain.on("log-message", (event, msg) => {
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+370
-327
@@ -1,531 +1,574 @@
|
|||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const os = require('os');
|
const os = require("os");
|
||||||
|
|
||||||
const CONFIG_VERSION = 1;
|
const CONFIG_VERSION = 1;
|
||||||
|
|
||||||
// Default values
|
// Default values
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
configVersion: CONFIG_VERSION,
|
configVersion: CONFIG_VERSION,
|
||||||
onboarded: false,
|
onboarded: false,
|
||||||
layout: 'normal'
|
layout: "normal",
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_CREDENTIALS = {
|
const DEFAULT_CREDENTIALS = {
|
||||||
apiKey: '',
|
apiKey: "",
|
||||||
groqApiKey: ''
|
groqApiKey: "",
|
||||||
|
openaiCompatibleApiKey: "",
|
||||||
|
openaiCompatibleBaseUrl: "",
|
||||||
|
openaiCompatibleModel: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_PREFERENCES = {
|
const DEFAULT_PREFERENCES = {
|
||||||
customPrompt: '',
|
customPrompt: "",
|
||||||
selectedProfile: 'interview',
|
selectedProfile: "interview",
|
||||||
selectedLanguage: 'en-US',
|
selectedLanguage: "en-US",
|
||||||
selectedScreenshotInterval: '5',
|
selectedScreenshotInterval: "5",
|
||||||
selectedImageQuality: 'medium',
|
selectedImageQuality: "medium",
|
||||||
advancedMode: false,
|
advancedMode: false,
|
||||||
audioMode: 'speaker_only',
|
audioMode: "speaker_only",
|
||||||
fontSize: 'medium',
|
fontSize: "medium",
|
||||||
backgroundTransparency: 0.8,
|
backgroundTransparency: 0.8,
|
||||||
googleSearchEnabled: false,
|
googleSearchEnabled: false,
|
||||||
ollamaHost: 'http://127.0.0.1:11434',
|
responseProvider: "gemini",
|
||||||
ollamaModel: 'llama3.1',
|
ollamaHost: "http://127.0.0.1:11434",
|
||||||
whisperModel: 'Xenova/whisper-small',
|
ollamaModel: "llama3.1",
|
||||||
|
whisperModel: "Xenova/whisper-small",
|
||||||
|
whisperDevice: "", // '' = auto-detect, 'cpu' = native, 'wasm' = compatible
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_KEYBINDS = null; // null means use system defaults
|
const DEFAULT_KEYBINDS = null; // null means use system defaults
|
||||||
|
|
||||||
const DEFAULT_LIMITS = {
|
const DEFAULT_LIMITS = {
|
||||||
data: [] // Array of { date: 'YYYY-MM-DD', flash: { count }, flashLite: { count }, groq: { 'qwen3-32b': { chars, limit }, 'gpt-oss-120b': { chars, limit }, 'gpt-oss-20b': { chars, limit } }, gemini: { 'gemma-3-27b-it': { chars } } }
|
data: [], // Array of { date: 'YYYY-MM-DD', flash: { count }, flashLite: { count }, groq: { 'qwen3-32b': { chars, limit }, 'gpt-oss-120b': { chars, limit }, 'gpt-oss-20b': { chars, limit } }, gemini: { 'gemma-3-27b-it': { chars } } }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the config directory path based on OS
|
// Get the config directory path based on OS
|
||||||
function getConfigDir() {
|
function getConfigDir() {
|
||||||
const platform = os.platform();
|
const platform = os.platform();
|
||||||
let configDir;
|
let configDir;
|
||||||
|
|
||||||
if (platform === 'win32') {
|
if (platform === "win32") {
|
||||||
configDir = path.join(os.homedir(), 'AppData', 'Roaming', 'cheating-daddy-config');
|
configDir = path.join(
|
||||||
} else if (platform === 'darwin') {
|
os.homedir(),
|
||||||
configDir = path.join(os.homedir(), 'Library', 'Application Support', 'cheating-daddy-config');
|
"AppData",
|
||||||
} else {
|
"Roaming",
|
||||||
configDir = path.join(os.homedir(), '.config', 'cheating-daddy-config');
|
"cheating-daddy-config",
|
||||||
}
|
);
|
||||||
|
} else if (platform === "darwin") {
|
||||||
|
configDir = path.join(
|
||||||
|
os.homedir(),
|
||||||
|
"Library",
|
||||||
|
"Application Support",
|
||||||
|
"cheating-daddy-config",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
configDir = path.join(os.homedir(), ".config", "cheating-daddy-config");
|
||||||
|
}
|
||||||
|
|
||||||
return configDir;
|
return configDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
// File paths
|
// File paths
|
||||||
function getConfigPath() {
|
function getConfigPath() {
|
||||||
return path.join(getConfigDir(), 'config.json');
|
return path.join(getConfigDir(), "config.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCredentialsPath() {
|
function getCredentialsPath() {
|
||||||
return path.join(getConfigDir(), 'credentials.json');
|
return path.join(getConfigDir(), "credentials.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPreferencesPath() {
|
function getPreferencesPath() {
|
||||||
return path.join(getConfigDir(), 'preferences.json');
|
return path.join(getConfigDir(), "preferences.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getKeybindsPath() {
|
function getKeybindsPath() {
|
||||||
return path.join(getConfigDir(), 'keybinds.json');
|
return path.join(getConfigDir(), "keybinds.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLimitsPath() {
|
function getLimitsPath() {
|
||||||
return path.join(getConfigDir(), 'limits.json');
|
return path.join(getConfigDir(), "limits.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHistoryDir() {
|
function getHistoryDir() {
|
||||||
return path.join(getConfigDir(), 'history');
|
return path.join(getConfigDir(), "history");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to read JSON file safely
|
// Helper to read JSON file safely
|
||||||
function readJsonFile(filePath, defaultValue) {
|
function readJsonFile(filePath, defaultValue) {
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
const data = fs.readFileSync(filePath, 'utf8');
|
const data = fs.readFileSync(filePath, "utf8");
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Error reading ${filePath}:`, error.message);
|
|
||||||
}
|
}
|
||||||
return defaultValue;
|
} catch (error) {
|
||||||
|
console.warn(`Error reading ${filePath}:`, error.message);
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to write JSON file safely
|
// Helper to write JSON file safely
|
||||||
function writeJsonFile(filePath, data) {
|
function writeJsonFile(filePath, data) {
|
||||||
try {
|
try {
|
||||||
const dir = path.dirname(filePath);
|
const dir = path.dirname(filePath);
|
||||||
if (!fs.existsSync(dir)) {
|
if (!fs.existsSync(dir)) {
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
}
|
|
||||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error writing ${filePath}:`, error.message);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error writing ${filePath}:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we need to reset (no configVersion or wrong version)
|
// Check if we need to reset (no configVersion or wrong version)
|
||||||
function needsReset() {
|
function needsReset() {
|
||||||
const configPath = getConfigPath();
|
const configPath = getConfigPath();
|
||||||
if (!fs.existsSync(configPath)) {
|
if (!fs.existsSync(configPath)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
||||||
return !config.configVersion || config.configVersion !== CONFIG_VERSION;
|
return !config.configVersion || config.configVersion !== CONFIG_VERSION;
|
||||||
} catch {
|
} catch {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wipe and reinitialize the config directory
|
// Wipe and reinitialize the config directory
|
||||||
function resetConfigDir() {
|
function resetConfigDir() {
|
||||||
const configDir = getConfigDir();
|
const configDir = getConfigDir();
|
||||||
|
|
||||||
console.log('Resetting config directory...');
|
console.log("Resetting config directory...");
|
||||||
|
|
||||||
// Remove existing directory if it exists
|
// Remove existing directory if it exists
|
||||||
if (fs.existsSync(configDir)) {
|
if (fs.existsSync(configDir)) {
|
||||||
fs.rmSync(configDir, { recursive: true, force: true });
|
fs.rmSync(configDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create fresh directory structure
|
// Create fresh directory structure
|
||||||
fs.mkdirSync(configDir, { recursive: true });
|
fs.mkdirSync(configDir, { recursive: true });
|
||||||
fs.mkdirSync(getHistoryDir(), { recursive: true });
|
fs.mkdirSync(getHistoryDir(), { recursive: true });
|
||||||
|
|
||||||
// Initialize with defaults
|
// Initialize with defaults
|
||||||
writeJsonFile(getConfigPath(), DEFAULT_CONFIG);
|
writeJsonFile(getConfigPath(), DEFAULT_CONFIG);
|
||||||
writeJsonFile(getCredentialsPath(), DEFAULT_CREDENTIALS);
|
writeJsonFile(getCredentialsPath(), DEFAULT_CREDENTIALS);
|
||||||
writeJsonFile(getPreferencesPath(), DEFAULT_PREFERENCES);
|
writeJsonFile(getPreferencesPath(), DEFAULT_PREFERENCES);
|
||||||
|
|
||||||
console.log('Config directory initialized with defaults');
|
console.log("Config directory initialized with defaults");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize storage - call this on app startup
|
// Initialize storage - call this on app startup
|
||||||
function initializeStorage() {
|
function initializeStorage() {
|
||||||
if (needsReset()) {
|
if (needsReset()) {
|
||||||
resetConfigDir();
|
resetConfigDir();
|
||||||
} else {
|
} else {
|
||||||
// Ensure history directory exists
|
// Ensure history directory exists
|
||||||
const historyDir = getHistoryDir();
|
const historyDir = getHistoryDir();
|
||||||
if (!fs.existsSync(historyDir)) {
|
if (!fs.existsSync(historyDir)) {
|
||||||
fs.mkdirSync(historyDir, { recursive: true });
|
fs.mkdirSync(historyDir, { recursive: true });
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ CONFIG ============
|
// ============ CONFIG ============
|
||||||
|
|
||||||
function getConfig() {
|
function getConfig() {
|
||||||
return readJsonFile(getConfigPath(), DEFAULT_CONFIG);
|
return readJsonFile(getConfigPath(), DEFAULT_CONFIG);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setConfig(config) {
|
function setConfig(config) {
|
||||||
const current = getConfig();
|
const current = getConfig();
|
||||||
const updated = { ...current, ...config, configVersion: CONFIG_VERSION };
|
const updated = { ...current, ...config, configVersion: CONFIG_VERSION };
|
||||||
return writeJsonFile(getConfigPath(), updated);
|
return writeJsonFile(getConfigPath(), updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateConfig(key, value) {
|
function updateConfig(key, value) {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
config[key] = value;
|
config[key] = value;
|
||||||
return writeJsonFile(getConfigPath(), config);
|
return writeJsonFile(getConfigPath(), config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ CREDENTIALS ============
|
// ============ CREDENTIALS ============
|
||||||
|
|
||||||
function getCredentials() {
|
function getCredentials() {
|
||||||
return readJsonFile(getCredentialsPath(), DEFAULT_CREDENTIALS);
|
return readJsonFile(getCredentialsPath(), DEFAULT_CREDENTIALS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCredentials(credentials) {
|
function setCredentials(credentials) {
|
||||||
const current = getCredentials();
|
const current = getCredentials();
|
||||||
const updated = { ...current, ...credentials };
|
const updated = { ...current, ...credentials };
|
||||||
return writeJsonFile(getCredentialsPath(), updated);
|
return writeJsonFile(getCredentialsPath(), updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getApiKey() {
|
function getApiKey() {
|
||||||
return getCredentials().apiKey || '';
|
return getCredentials().apiKey || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function setApiKey(apiKey) {
|
function setApiKey(apiKey) {
|
||||||
return setCredentials({ apiKey });
|
return setCredentials({ apiKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGroqApiKey() {
|
function getGroqApiKey() {
|
||||||
return getCredentials().groqApiKey || '';
|
return getCredentials().groqApiKey || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function setGroqApiKey(groqApiKey) {
|
function setGroqApiKey(groqApiKey) {
|
||||||
return setCredentials({ groqApiKey });
|
return setCredentials({ groqApiKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpenAICompatibleConfig() {
|
||||||
|
const creds = getCredentials();
|
||||||
|
return {
|
||||||
|
apiKey: creds.openaiCompatibleApiKey || "",
|
||||||
|
baseUrl: creds.openaiCompatibleBaseUrl || "",
|
||||||
|
model: creds.openaiCompatibleModel || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOpenAICompatibleConfig(apiKey, baseUrl, model) {
|
||||||
|
return setCredentials({
|
||||||
|
openaiCompatibleApiKey: apiKey,
|
||||||
|
openaiCompatibleBaseUrl: baseUrl,
|
||||||
|
openaiCompatibleModel: model,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ PREFERENCES ============
|
// ============ PREFERENCES ============
|
||||||
|
|
||||||
function getPreferences() {
|
function getPreferences() {
|
||||||
const saved = readJsonFile(getPreferencesPath(), {});
|
const saved = readJsonFile(getPreferencesPath(), {});
|
||||||
return { ...DEFAULT_PREFERENCES, ...saved };
|
return { ...DEFAULT_PREFERENCES, ...saved };
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPreferences(preferences) {
|
function setPreferences(preferences) {
|
||||||
const current = getPreferences();
|
const current = getPreferences();
|
||||||
const updated = { ...current, ...preferences };
|
const updated = { ...current, ...preferences };
|
||||||
return writeJsonFile(getPreferencesPath(), updated);
|
return writeJsonFile(getPreferencesPath(), updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePreference(key, value) {
|
function updatePreference(key, value) {
|
||||||
const preferences = getPreferences();
|
const preferences = getPreferences();
|
||||||
preferences[key] = value;
|
preferences[key] = value;
|
||||||
return writeJsonFile(getPreferencesPath(), preferences);
|
return writeJsonFile(getPreferencesPath(), preferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ KEYBINDS ============
|
// ============ KEYBINDS ============
|
||||||
|
|
||||||
function getKeybinds() {
|
function getKeybinds() {
|
||||||
return readJsonFile(getKeybindsPath(), DEFAULT_KEYBINDS);
|
return readJsonFile(getKeybindsPath(), DEFAULT_KEYBINDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setKeybinds(keybinds) {
|
function setKeybinds(keybinds) {
|
||||||
return writeJsonFile(getKeybindsPath(), keybinds);
|
return writeJsonFile(getKeybindsPath(), keybinds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ LIMITS (Rate Limiting) ============
|
// ============ LIMITS (Rate Limiting) ============
|
||||||
|
|
||||||
function getLimits() {
|
function getLimits() {
|
||||||
return readJsonFile(getLimitsPath(), DEFAULT_LIMITS);
|
return readJsonFile(getLimitsPath(), DEFAULT_LIMITS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLimits(limits) {
|
function setLimits(limits) {
|
||||||
return writeJsonFile(getLimitsPath(), limits);
|
return writeJsonFile(getLimitsPath(), limits);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTodayDateString() {
|
function getTodayDateString() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
return now.toISOString().split('T')[0]; // YYYY-MM-DD
|
return now.toISOString().split("T")[0]; // YYYY-MM-DD
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTodayLimits() {
|
function getTodayLimits() {
|
||||||
const limits = getLimits();
|
const limits = getLimits();
|
||||||
const today = getTodayDateString();
|
const today = getTodayDateString();
|
||||||
|
|
||||||
// Find today's entry
|
// Find today's entry
|
||||||
const todayEntry = limits.data.find(entry => entry.date === today);
|
const todayEntry = limits.data.find((entry) => entry.date === today);
|
||||||
|
|
||||||
if (todayEntry) {
|
if (todayEntry) {
|
||||||
// ensure new fields exist
|
// ensure new fields exist
|
||||||
if(!todayEntry.groq) {
|
if (!todayEntry.groq) {
|
||||||
todayEntry.groq = {
|
todayEntry.groq = {
|
||||||
'qwen3-32b': { chars: 0, limit: 1500000 },
|
"qwen3-32b": { chars: 0, limit: 1500000 },
|
||||||
'gpt-oss-120b': { chars: 0, limit: 600000 },
|
"gpt-oss-120b": { chars: 0, limit: 600000 },
|
||||||
'gpt-oss-20b': { chars: 0, limit: 600000 },
|
"gpt-oss-20b": { chars: 0, limit: 600000 },
|
||||||
'kimi-k2-instruct': { chars: 0, limit: 600000 }
|
"kimi-k2-instruct": { chars: 0, limit: 600000 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if(!todayEntry.gemini) {
|
if (!todayEntry.gemini) {
|
||||||
todayEntry.gemini = {
|
todayEntry.gemini = {
|
||||||
'gemma-3-27b-it': { chars: 0 }
|
"gemma-3-27b-it": { chars: 0 },
|
||||||
};
|
};
|
||||||
}
|
|
||||||
setLimits(limits);
|
|
||||||
return todayEntry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// No entry for today - clean old entries and create new one
|
|
||||||
limits.data = limits.data.filter(entry => entry.date === today);
|
|
||||||
const newEntry = {
|
|
||||||
date: today,
|
|
||||||
flash: { count: 0 },
|
|
||||||
flashLite: { count: 0 },
|
|
||||||
groq: {
|
|
||||||
'qwen3-32b': { chars: 0, limit: 1500000 },
|
|
||||||
'gpt-oss-120b': { chars: 0, limit: 600000 },
|
|
||||||
'gpt-oss-20b': { chars: 0, limit: 600000 },
|
|
||||||
'kimi-k2-instruct': { chars: 0, limit: 600000 }
|
|
||||||
},
|
|
||||||
gemini: {
|
|
||||||
'gemma-3-27b-it': { chars: 0 }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
limits.data.push(newEntry);
|
|
||||||
setLimits(limits);
|
setLimits(limits);
|
||||||
|
return todayEntry;
|
||||||
|
}
|
||||||
|
|
||||||
return newEntry;
|
// No entry for today - clean old entries and create new one
|
||||||
|
limits.data = limits.data.filter((entry) => entry.date === today);
|
||||||
|
const newEntry = {
|
||||||
|
date: today,
|
||||||
|
flash: { count: 0 },
|
||||||
|
flashLite: { count: 0 },
|
||||||
|
groq: {
|
||||||
|
"qwen3-32b": { chars: 0, limit: 1500000 },
|
||||||
|
"gpt-oss-120b": { chars: 0, limit: 600000 },
|
||||||
|
"gpt-oss-20b": { chars: 0, limit: 600000 },
|
||||||
|
"kimi-k2-instruct": { chars: 0, limit: 600000 },
|
||||||
|
},
|
||||||
|
gemini: {
|
||||||
|
"gemma-3-27b-it": { chars: 0 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
limits.data.push(newEntry);
|
||||||
|
setLimits(limits);
|
||||||
|
|
||||||
|
return newEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
function incrementLimitCount(model) {
|
function incrementLimitCount(model) {
|
||||||
const limits = getLimits();
|
const limits = getLimits();
|
||||||
const today = getTodayDateString();
|
const today = getTodayDateString();
|
||||||
|
|
||||||
// Find or create today's entry
|
// Find or create today's entry
|
||||||
let todayEntry = limits.data.find(entry => entry.date === today);
|
let todayEntry = limits.data.find((entry) => entry.date === today);
|
||||||
|
|
||||||
if (!todayEntry) {
|
if (!todayEntry) {
|
||||||
// Clean old entries and create new one
|
// Clean old entries and create new one
|
||||||
limits.data = [];
|
limits.data = [];
|
||||||
todayEntry = {
|
todayEntry = {
|
||||||
date: today,
|
date: today,
|
||||||
flash: { count: 0 },
|
flash: { count: 0 },
|
||||||
flashLite: { count: 0 }
|
flashLite: { count: 0 },
|
||||||
};
|
};
|
||||||
limits.data.push(todayEntry);
|
limits.data.push(todayEntry);
|
||||||
} else {
|
} else {
|
||||||
// Clean old entries, keep only today
|
// Clean old entries, keep only today
|
||||||
limits.data = limits.data.filter(entry => entry.date === today);
|
limits.data = limits.data.filter((entry) => entry.date === today);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment the appropriate model count
|
// Increment the appropriate model count
|
||||||
if (model === 'gemini-2.5-flash') {
|
if (model === "gemini-2.5-flash") {
|
||||||
todayEntry.flash.count++;
|
todayEntry.flash.count++;
|
||||||
} else if (model === 'gemini-2.5-flash-lite') {
|
} else if (model === "gemini-2.5-flash-lite") {
|
||||||
todayEntry.flashLite.count++;
|
todayEntry.flashLite.count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLimits(limits);
|
setLimits(limits);
|
||||||
return todayEntry;
|
return todayEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
function incrementCharUsage(provider, model, charCount) {
|
function incrementCharUsage(provider, model, charCount) {
|
||||||
getTodayLimits();
|
getTodayLimits();
|
||||||
|
|
||||||
const limits = getLimits();
|
const limits = getLimits();
|
||||||
const today = getTodayDateString();
|
const today = getTodayDateString();
|
||||||
const todayEntry = limits.data.find(entry => entry.date === today);
|
const todayEntry = limits.data.find((entry) => entry.date === today);
|
||||||
|
|
||||||
if(todayEntry[provider] && todayEntry[provider][model]) {
|
if (todayEntry[provider] && todayEntry[provider][model]) {
|
||||||
todayEntry[provider][model].chars += charCount;
|
todayEntry[provider][model].chars += charCount;
|
||||||
setLimits(limits);
|
setLimits(limits);
|
||||||
}
|
}
|
||||||
|
|
||||||
return todayEntry;
|
return todayEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAvailableModel() {
|
function getAvailableModel() {
|
||||||
const todayLimits = getTodayLimits();
|
const todayLimits = getTodayLimits();
|
||||||
|
|
||||||
// RPD limits: flash = 20, flash-lite = 20
|
// RPD limits: flash = 20, flash-lite = 20
|
||||||
// After both exhausted, fall back to flash (for paid API users)
|
// After both exhausted, fall back to flash (for paid API users)
|
||||||
if (todayLimits.flash.count < 20) {
|
if (todayLimits.flash.count < 20) {
|
||||||
return 'gemini-2.5-flash';
|
return "gemini-2.5-flash";
|
||||||
} else if (todayLimits.flashLite.count < 20) {
|
} else if (todayLimits.flashLite.count < 20) {
|
||||||
return 'gemini-2.5-flash-lite';
|
return "gemini-2.5-flash-lite";
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'gemini-2.5-flash'; // Default to flash for paid API users
|
return "gemini-2.5-flash"; // Default to flash for paid API users
|
||||||
}
|
}
|
||||||
|
|
||||||
function getModelForToday() {
|
function getModelForToday() {
|
||||||
const todayEntry = getTodayLimits();
|
const todayEntry = getTodayLimits();
|
||||||
const groq = todayEntry.groq;
|
const groq = todayEntry.groq;
|
||||||
|
|
||||||
if (groq['qwen3-32b'].chars < groq['qwen3-32b'].limit) {
|
if (groq["qwen3-32b"].chars < groq["qwen3-32b"].limit) {
|
||||||
return 'qwen/qwen3-32b';
|
return "qwen/qwen3-32b";
|
||||||
}
|
}
|
||||||
if (groq['gpt-oss-120b'].chars < groq['gpt-oss-120b'].limit) {
|
if (groq["gpt-oss-120b"].chars < groq["gpt-oss-120b"].limit) {
|
||||||
return 'openai/gpt-oss-120b';
|
return "openai/gpt-oss-120b";
|
||||||
}
|
}
|
||||||
if (groq['gpt-oss-20b'].chars < groq['gpt-oss-20b'].limit) {
|
if (groq["gpt-oss-20b"].chars < groq["gpt-oss-20b"].limit) {
|
||||||
return 'openai/gpt-oss-20b';
|
return "openai/gpt-oss-20b";
|
||||||
}
|
}
|
||||||
if (groq['kimi-k2-instruct'].chars < groq['kimi-k2-instruct'].limit) {
|
if (groq["kimi-k2-instruct"].chars < groq["kimi-k2-instruct"].limit) {
|
||||||
return 'moonshotai/kimi-k2-instruct';
|
return "moonshotai/kimi-k2-instruct";
|
||||||
}
|
}
|
||||||
|
|
||||||
// All limits exhausted
|
// All limits exhausted
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ HISTORY ============
|
// ============ HISTORY ============
|
||||||
|
|
||||||
function getSessionPath(sessionId) {
|
function getSessionPath(sessionId) {
|
||||||
return path.join(getHistoryDir(), `${sessionId}.json`);
|
return path.join(getHistoryDir(), `${sessionId}.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSession(sessionId, data) {
|
function saveSession(sessionId, data) {
|
||||||
const sessionPath = getSessionPath(sessionId);
|
const sessionPath = getSessionPath(sessionId);
|
||||||
|
|
||||||
// Load existing session to preserve metadata
|
// Load existing session to preserve metadata
|
||||||
const existingSession = readJsonFile(sessionPath, null);
|
const existingSession = readJsonFile(sessionPath, null);
|
||||||
|
|
||||||
const sessionData = {
|
const sessionData = {
|
||||||
sessionId,
|
sessionId,
|
||||||
createdAt: existingSession?.createdAt || parseInt(sessionId),
|
createdAt: existingSession?.createdAt || parseInt(sessionId),
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
// Profile context - set once when session starts
|
// Profile context - set once when session starts
|
||||||
profile: data.profile || existingSession?.profile || null,
|
profile: data.profile || existingSession?.profile || null,
|
||||||
customPrompt: data.customPrompt || existingSession?.customPrompt || null,
|
customPrompt: data.customPrompt || existingSession?.customPrompt || null,
|
||||||
// Conversation data
|
// Conversation data
|
||||||
conversationHistory: data.conversationHistory || existingSession?.conversationHistory || [],
|
conversationHistory:
|
||||||
screenAnalysisHistory: data.screenAnalysisHistory || existingSession?.screenAnalysisHistory || []
|
data.conversationHistory || existingSession?.conversationHistory || [],
|
||||||
};
|
screenAnalysisHistory:
|
||||||
return writeJsonFile(sessionPath, sessionData);
|
data.screenAnalysisHistory ||
|
||||||
|
existingSession?.screenAnalysisHistory ||
|
||||||
|
[],
|
||||||
|
};
|
||||||
|
return writeJsonFile(sessionPath, sessionData);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSession(sessionId) {
|
function getSession(sessionId) {
|
||||||
return readJsonFile(getSessionPath(sessionId), null);
|
return readJsonFile(getSessionPath(sessionId), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllSessions() {
|
function getAllSessions() {
|
||||||
const historyDir = getHistoryDir();
|
const historyDir = getHistoryDir();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(historyDir)) {
|
if (!fs.existsSync(historyDir)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
|
||||||
|
|
||||||
const files = fs.readdirSync(historyDir)
|
|
||||||
.filter(f => f.endsWith('.json'))
|
|
||||||
.sort((a, b) => {
|
|
||||||
// Sort by timestamp descending (newest first)
|
|
||||||
const tsA = parseInt(a.replace('.json', ''));
|
|
||||||
const tsB = parseInt(b.replace('.json', ''));
|
|
||||||
return tsB - tsA;
|
|
||||||
});
|
|
||||||
|
|
||||||
return files.map(file => {
|
|
||||||
const sessionId = file.replace('.json', '');
|
|
||||||
const data = readJsonFile(path.join(historyDir, file), null);
|
|
||||||
if (data) {
|
|
||||||
return {
|
|
||||||
sessionId,
|
|
||||||
createdAt: data.createdAt,
|
|
||||||
lastUpdated: data.lastUpdated,
|
|
||||||
messageCount: data.conversationHistory?.length || 0,
|
|
||||||
screenAnalysisCount: data.screenAnalysisHistory?.length || 0,
|
|
||||||
profile: data.profile || null,
|
|
||||||
customPrompt: data.customPrompt || null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}).filter(Boolean);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error reading sessions:', error.message);
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(historyDir)
|
||||||
|
.filter((f) => f.endsWith(".json"))
|
||||||
|
.sort((a, b) => {
|
||||||
|
// Sort by timestamp descending (newest first)
|
||||||
|
const tsA = parseInt(a.replace(".json", ""));
|
||||||
|
const tsB = parseInt(b.replace(".json", ""));
|
||||||
|
return tsB - tsA;
|
||||||
|
});
|
||||||
|
|
||||||
|
return files
|
||||||
|
.map((file) => {
|
||||||
|
const sessionId = file.replace(".json", "");
|
||||||
|
const data = readJsonFile(path.join(historyDir, file), null);
|
||||||
|
if (data) {
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
createdAt: data.createdAt,
|
||||||
|
lastUpdated: data.lastUpdated,
|
||||||
|
messageCount: data.conversationHistory?.length || 0,
|
||||||
|
screenAnalysisCount: data.screenAnalysisHistory?.length || 0,
|
||||||
|
profile: data.profile || null,
|
||||||
|
customPrompt: data.customPrompt || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error reading sessions:", error.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSession(sessionId) {
|
function deleteSession(sessionId) {
|
||||||
const sessionPath = getSessionPath(sessionId);
|
const sessionPath = getSessionPath(sessionId);
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(sessionPath)) {
|
if (fs.existsSync(sessionPath)) {
|
||||||
fs.unlinkSync(sessionPath);
|
fs.unlinkSync(sessionPath);
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting session:', error.message);
|
|
||||||
}
|
}
|
||||||
return false;
|
} catch (error) {
|
||||||
|
console.error("Error deleting session:", error.message);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteAllSessions() {
|
function deleteAllSessions() {
|
||||||
const historyDir = getHistoryDir();
|
const historyDir = getHistoryDir();
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(historyDir)) {
|
if (fs.existsSync(historyDir)) {
|
||||||
const files = fs.readdirSync(historyDir).filter(f => f.endsWith('.json'));
|
const files = fs
|
||||||
files.forEach(file => {
|
.readdirSync(historyDir)
|
||||||
fs.unlinkSync(path.join(historyDir, file));
|
.filter((f) => f.endsWith(".json"));
|
||||||
});
|
files.forEach((file) => {
|
||||||
}
|
fs.unlinkSync(path.join(historyDir, file));
|
||||||
return true;
|
});
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting all sessions:', error.message);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting all sessions:", error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ CLEAR ALL DATA ============
|
// ============ CLEAR ALL DATA ============
|
||||||
|
|
||||||
function clearAllData() {
|
function clearAllData() {
|
||||||
resetConfigDir();
|
resetConfigDir();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// Initialization
|
// Initialization
|
||||||
initializeStorage,
|
initializeStorage,
|
||||||
getConfigDir,
|
getConfigDir,
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
getConfig,
|
getConfig,
|
||||||
setConfig,
|
setConfig,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
|
|
||||||
// Credentials
|
// Credentials
|
||||||
getCredentials,
|
getCredentials,
|
||||||
setCredentials,
|
setCredentials,
|
||||||
getApiKey,
|
getApiKey,
|
||||||
setApiKey,
|
setApiKey,
|
||||||
getGroqApiKey,
|
getGroqApiKey,
|
||||||
setGroqApiKey,
|
setGroqApiKey,
|
||||||
|
getOpenAICompatibleConfig,
|
||||||
|
setOpenAICompatibleConfig,
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
getPreferences,
|
getPreferences,
|
||||||
setPreferences,
|
setPreferences,
|
||||||
updatePreference,
|
updatePreference,
|
||||||
|
|
||||||
// Keybinds
|
// Keybinds
|
||||||
getKeybinds,
|
getKeybinds,
|
||||||
setKeybinds,
|
setKeybinds,
|
||||||
|
|
||||||
// Limits (Rate Limiting)
|
// Limits (Rate Limiting)
|
||||||
getLimits,
|
getLimits,
|
||||||
setLimits,
|
setLimits,
|
||||||
getTodayLimits,
|
getTodayLimits,
|
||||||
incrementLimitCount,
|
incrementLimitCount,
|
||||||
getAvailableModel,
|
getAvailableModel,
|
||||||
incrementCharUsage,
|
incrementCharUsage,
|
||||||
getModelForToday,
|
getModelForToday,
|
||||||
|
|
||||||
// History
|
// History
|
||||||
saveSession,
|
saveSession,
|
||||||
getSession,
|
getSession,
|
||||||
getAllSessions,
|
getAllSessions,
|
||||||
deleteSession,
|
deleteSession,
|
||||||
deleteAllSessions,
|
deleteAllSessions,
|
||||||
|
|
||||||
// Clear all
|
// Clear all
|
||||||
clearAllData
|
clearAllData,
|
||||||
};
|
};
|
||||||
|
|||||||
+1191
-847
File diff suppressed because it is too large
Load Diff
+732
-315
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,177 @@
|
|||||||
|
/**
|
||||||
|
* nodeDetect.js — Locate the system Node.js binary.
|
||||||
|
*
|
||||||
|
* When spawning child processes that rely on native addons compiled against the
|
||||||
|
* system Node.js ABI (e.g. onnxruntime-node), we must NOT run them inside
|
||||||
|
* Electron's embedded Node.js runtime — the ABI mismatch causes SIGTRAP /
|
||||||
|
* SIGSEGV crashes. This module finds the real system `node` binary so we can
|
||||||
|
* pass it as `execPath` to `child_process.fork()`.
|
||||||
|
*
|
||||||
|
* Falls back to `null` when no system Node.js is found, letting the caller
|
||||||
|
* decide on an alternative strategy (e.g. WASM backend).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { execSync } = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const os = require("os");
|
||||||
|
|
||||||
|
/** Well-known Node.js install locations per platform. */
|
||||||
|
const KNOWN_PATHS = {
|
||||||
|
darwin: [
|
||||||
|
"/usr/local/bin/node",
|
||||||
|
"/opt/homebrew/bin/node", // Apple Silicon Homebrew
|
||||||
|
path.join(os.homedir(), ".nvm/versions/node"), // nvm — needs glob
|
||||||
|
path.join(os.homedir(), ".volta/bin/node"), // Volta
|
||||||
|
path.join(os.homedir(), ".fnm/aliases/default/bin/node"), // fnm
|
||||||
|
path.join(os.homedir(), ".mise/shims/node"), // mise (rtx)
|
||||||
|
path.join(os.homedir(), ".asdf/shims/node"), // asdf
|
||||||
|
],
|
||||||
|
linux: [
|
||||||
|
"/usr/bin/node",
|
||||||
|
"/usr/local/bin/node",
|
||||||
|
path.join(os.homedir(), ".nvm/versions/node"),
|
||||||
|
path.join(os.homedir(), ".volta/bin/node"),
|
||||||
|
path.join(os.homedir(), ".fnm/aliases/default/bin/node"),
|
||||||
|
path.join(os.homedir(), ".mise/shims/node"),
|
||||||
|
path.join(os.homedir(), ".asdf/shims/node"),
|
||||||
|
],
|
||||||
|
win32: [
|
||||||
|
"C:\\Program Files\\nodejs\\node.exe",
|
||||||
|
"C:\\Program Files (x86)\\nodejs\\node.exe",
|
||||||
|
path.join(os.homedir(), "AppData", "Roaming", "nvm", "current", "node.exe"),
|
||||||
|
path.join(os.homedir(), ".volta", "bin", "node.exe"),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the latest nvm-installed Node.js binary on macOS / Linux.
|
||||||
|
* Returns the path to the `node` binary or null.
|
||||||
|
*/
|
||||||
|
function findNvmNode() {
|
||||||
|
const nvmDir = path.join(os.homedir(), ".nvm", "versions", "node");
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(nvmDir)) return null;
|
||||||
|
const versions = fs.readdirSync(nvmDir).filter((d) => d.startsWith("v"));
|
||||||
|
if (versions.length === 0) return null;
|
||||||
|
// Sort semver descending (rough but sufficient)
|
||||||
|
versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
||||||
|
const nodeBin = path.join(nvmDir, versions[0], "bin", "node");
|
||||||
|
if (fs.existsSync(nodeBin)) return nodeBin;
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to resolve `node` via the system PATH using `which` (Unix) or
|
||||||
|
* `where` (Windows). Returns the path string or null.
|
||||||
|
*/
|
||||||
|
function whichNode() {
|
||||||
|
try {
|
||||||
|
const cmd = process.platform === "win32" ? "where node" : "which node";
|
||||||
|
const result = execSync(cmd, {
|
||||||
|
encoding: "utf8",
|
||||||
|
timeout: 5000,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
// Ensure common manager shim dirs are on PATH
|
||||||
|
PATH: [
|
||||||
|
process.env.PATH || "",
|
||||||
|
"/usr/local/bin",
|
||||||
|
"/opt/homebrew/bin",
|
||||||
|
path.join(os.homedir(), ".volta", "bin"),
|
||||||
|
path.join(os.homedir(), ".fnm", "aliases", "default", "bin"),
|
||||||
|
path.join(os.homedir(), ".mise", "shims"),
|
||||||
|
path.join(os.homedir(), ".asdf", "shims"),
|
||||||
|
].join(process.platform === "win32" ? ";" : ":"),
|
||||||
|
},
|
||||||
|
stdio: ["ignore", "pipe", "ignore"],
|
||||||
|
}).trim();
|
||||||
|
// `where` on Windows may return multiple lines — take the first
|
||||||
|
const first = result.split(/\r?\n/)[0].trim();
|
||||||
|
if (first && fs.existsSync(first)) return first;
|
||||||
|
} catch (_) {
|
||||||
|
// Command failed
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a given path is a real Node.js binary (not the Electron binary
|
||||||
|
* pretending to be Node via ELECTRON_RUN_AS_NODE).
|
||||||
|
*/
|
||||||
|
function isRealNode(nodePath) {
|
||||||
|
if (!nodePath) return false;
|
||||||
|
try {
|
||||||
|
const out = execSync(
|
||||||
|
`"${nodePath}" -e "process.stdout.write(String(!process.versions.electron))"`,
|
||||||
|
{
|
||||||
|
encoding: "utf8",
|
||||||
|
timeout: 5000,
|
||||||
|
env: { ...process.env, ELECTRON_RUN_AS_NODE: undefined },
|
||||||
|
stdio: ["ignore", "pipe", "ignore"],
|
||||||
|
},
|
||||||
|
).trim();
|
||||||
|
return out === "true";
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the system Node.js binary.
|
||||||
|
*
|
||||||
|
* @returns {{ nodePath: string } | null} The absolute path to system `node`,
|
||||||
|
* or null if none found. The caller should fall back to WASM when null.
|
||||||
|
*/
|
||||||
|
function findSystemNode() {
|
||||||
|
// 1. Try `which node` / `where node` first (respects user's PATH / shims)
|
||||||
|
const fromPath = whichNode();
|
||||||
|
if (fromPath && isRealNode(fromPath)) {
|
||||||
|
return { nodePath: fromPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try nvm (has multiple version dirs)
|
||||||
|
const fromNvm = findNvmNode();
|
||||||
|
if (fromNvm && isRealNode(fromNvm)) {
|
||||||
|
return { nodePath: fromNvm };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Walk the well-known paths for the current platform
|
||||||
|
const platform = process.platform;
|
||||||
|
const candidates = KNOWN_PATHS[platform] || KNOWN_PATHS.linux;
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
// Skip the nvm root — already handled above
|
||||||
|
if (candidate.includes(".nvm/versions/node")) continue;
|
||||||
|
if (fs.existsSync(candidate) && isRealNode(candidate)) {
|
||||||
|
return { nodePath: candidate };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cache so we only search once per process lifetime. */
|
||||||
|
let _cached = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached version of `findSystemNode()`.
|
||||||
|
* @returns {{ nodePath: string } | null}
|
||||||
|
*/
|
||||||
|
function getSystemNode() {
|
||||||
|
if (_cached === undefined) {
|
||||||
|
_cached = findSystemNode();
|
||||||
|
if (_cached) {
|
||||||
|
console.log("[nodeDetect] Found system Node.js:", _cached.nodePath);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"[nodeDetect] No system Node.js found — will fall back to WASM backend",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { findSystemNode, getSystemNode, isRealNode };
|
||||||
+109
-88
@@ -1,21 +1,45 @@
|
|||||||
const profilePrompts = {
|
const responseModeFormats = {
|
||||||
interview: {
|
brief: `**RESPONSE FORMAT REQUIREMENTS:**
|
||||||
intro: `You are an AI-powered interview assistant, designed to act as a discreet on-screen teleprompter. Your mission is to help the user excel in their job interview by providing concise, impactful, and ready-to-speak answers or key talking points. Analyze the ongoing interview dialogue and, crucially, the 'User-provided context' below.`,
|
|
||||||
|
|
||||||
formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
|
|
||||||
- Keep responses SHORT and CONCISE (1-3 sentences max)
|
- Keep responses SHORT and CONCISE (1-3 sentences max)
|
||||||
- Use **markdown formatting** for better readability
|
- Use **markdown formatting** for better readability
|
||||||
- Use **bold** for key points and emphasis
|
- Use **bold** for key points and emphasis
|
||||||
- Use bullet points (-) for lists when appropriate
|
- Use bullet points (-) for lists when appropriate
|
||||||
- Focus on the most essential information only`,
|
- Focus on the most essential information only
|
||||||
|
- EXCEPTION: If a coding/algorithm task is detected, ALWAYS provide the complete working code (see CODING TASKS below)`,
|
||||||
|
|
||||||
searchUsage: `**SEARCH TOOL USAGE:**
|
detailed: `**RESPONSE FORMAT REQUIREMENTS:**
|
||||||
|
- Provide a THOROUGH and COMPREHENSIVE response with full explanations
|
||||||
|
- Use **markdown formatting** for better readability
|
||||||
|
- Use **bold** for key points and emphasis
|
||||||
|
- Use headers (##) to organize sections when appropriate
|
||||||
|
- Use bullet points (-) for lists when appropriate
|
||||||
|
- Include relevant context, edge cases, and reasoning
|
||||||
|
- For technical topics, explain the "why" behind each point
|
||||||
|
- No length restriction — be as detailed as needed to fully answer the question`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const codingAwareness = `**CODING TASKS — CRITICAL INSTRUCTION:**
|
||||||
|
When the interviewer/questioner asks to solve a coding problem, implement an algorithm, debug code, do a live coding exercise, open an IDE and write code, or any task that requires a code solution:
|
||||||
|
- You MUST provide the ACTUAL COMPLETE WORKING CODE SOLUTION
|
||||||
|
- NEVER respond with meta-advice like "now you should write code" or "prepare to implement" or "think about the approach"
|
||||||
|
- NEVER say "open your IDE" or "start coding" — instead, GIVE THE CODE
|
||||||
|
- In brief mode: provide 2-3 bullet approach points, then the FULL working code with comments
|
||||||
|
- In detailed mode: explain approach, time/space complexity, edge cases, then the FULL working code with comments
|
||||||
|
- Include the programming language name in the code fence (e.g. \`\`\`python, \`\`\`javascript)
|
||||||
|
- If the language is not specified, default to Python
|
||||||
|
- The code must be complete, runnable, and correct`;
|
||||||
|
|
||||||
|
const profilePrompts = {
|
||||||
|
interview: {
|
||||||
|
intro: `You are an AI-powered interview assistant, designed to act as a discreet on-screen teleprompter. Your mission is to help the user excel in their job interview by providing concise, impactful, and ready-to-speak answers or key talking points. Analyze the ongoing interview dialogue and, crucially, the 'User-provided context' below.`,
|
||||||
|
|
||||||
|
searchUsage: `**SEARCH TOOL USAGE:**
|
||||||
- If the interviewer mentions **recent events, news, or current trends** (anything from the last 6 months), **ALWAYS use Google search** to get up-to-date information
|
- If the interviewer mentions **recent events, news, or current trends** (anything from the last 6 months), **ALWAYS use Google search** to get up-to-date information
|
||||||
- If they ask about **company-specific information, recent acquisitions, funding, or leadership changes**, use Google search first
|
- If they ask about **company-specific information, recent acquisitions, funding, or leadership changes**, use Google search first
|
||||||
- If they mention **new technologies, frameworks, or industry developments**, search for the latest information
|
- If they mention **new technologies, frameworks, or industry developments**, search for the latest information
|
||||||
- After searching, provide a **concise, informed response** based on the real-time data`,
|
- After searching, provide a **concise, informed response** based on the real-time data`,
|
||||||
|
|
||||||
content: `Focus on delivering the most essential information the user needs. Your suggestions should be direct and immediately usable.
|
content: `Focus on delivering the most essential information the user needs. Your suggestions should be direct and immediately usable.
|
||||||
|
|
||||||
To help the user 'crack' the interview in their specific field:
|
To help the user 'crack' the interview in their specific field:
|
||||||
1. Heavily rely on the 'User-provided context' (e.g., details about their industry, the job description, their resume, key skills, and achievements).
|
1. Heavily rely on the 'User-provided context' (e.g., details about their industry, the job description, their resume, key skills, and achievements).
|
||||||
@@ -32,27 +56,20 @@ You: "I've been working with React for 4 years, building everything from simple
|
|||||||
Interviewer: "Why do you want to work here?"
|
Interviewer: "Why do you want to work here?"
|
||||||
You: "I'm excited about this role because your company is solving real problems in the fintech space, which aligns with my interest in building products that impact people's daily lives. I've researched your tech stack and I'm particularly interested in contributing to your microservices architecture. Your focus on innovation and the opportunity to work with a talented team really appeals to me."`,
|
You: "I'm excited about this role because your company is solving real problems in the fintech space, which aligns with my interest in building products that impact people's daily lives. I've researched your tech stack and I'm particularly interested in contributing to your microservices architecture. Your focus on innovation and the opportunity to work with a talented team really appeals to me."`,
|
||||||
|
|
||||||
outputInstructions: `**OUTPUT INSTRUCTIONS:**
|
outputInstructions: `**OUTPUT INSTRUCTIONS:**
|
||||||
Provide only the exact words to say in **markdown format**. No coaching, no "you should" statements, no explanations - just the direct response the candidate can speak immediately. Keep it **short and impactful**.`,
|
Provide only the exact words to say in **markdown format**. No coaching, no "you should" statements, no explanations - just the direct response the candidate can speak immediately. Keep it **short and impactful**.`,
|
||||||
},
|
},
|
||||||
|
|
||||||
sales: {
|
sales: {
|
||||||
intro: `You are a sales call assistant. Your job is to provide the exact words the salesperson should say to prospects during sales calls. Give direct, ready-to-speak responses that are persuasive and professional.`,
|
intro: `You are a sales call assistant. Your job is to provide the exact words the salesperson should say to prospects during sales calls. Give direct, ready-to-speak responses that are persuasive and professional.`,
|
||||||
|
|
||||||
formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
|
searchUsage: `**SEARCH TOOL USAGE:**
|
||||||
- Keep responses SHORT and CONCISE (1-3 sentences max)
|
|
||||||
- Use **markdown formatting** for better readability
|
|
||||||
- Use **bold** for key points and emphasis
|
|
||||||
- Use bullet points (-) for lists when appropriate
|
|
||||||
- Focus on the most essential information only`,
|
|
||||||
|
|
||||||
searchUsage: `**SEARCH TOOL USAGE:**
|
|
||||||
- If the prospect mentions **recent industry trends, market changes, or current events**, **ALWAYS use Google search** to get up-to-date information
|
- If the prospect mentions **recent industry trends, market changes, or current events**, **ALWAYS use Google search** to get up-to-date information
|
||||||
- If they reference **competitor information, recent funding news, or market data**, search for the latest information first
|
- If they reference **competitor information, recent funding news, or market data**, search for the latest information first
|
||||||
- If they ask about **new regulations, industry reports, or recent developments**, use search to provide accurate data
|
- If they ask about **new regulations, industry reports, or recent developments**, use search to provide accurate data
|
||||||
- After searching, provide a **concise, informed response** that demonstrates current market knowledge`,
|
- After searching, provide a **concise, informed response** that demonstrates current market knowledge`,
|
||||||
|
|
||||||
content: `Examples:
|
content: `Examples:
|
||||||
|
|
||||||
Prospect: "Tell me about your product"
|
Prospect: "Tell me about your product"
|
||||||
You: "Our platform helps companies like yours reduce operational costs by 30% while improving efficiency. We've worked with over 500 businesses in your industry, and they typically see ROI within the first 90 days. What specific operational challenges are you facing right now?"
|
You: "Our platform helps companies like yours reduce operational costs by 30% while improving efficiency. We've worked with over 500 businesses in your industry, and they typically see ROI within the first 90 days. What specific operational challenges are you facing right now?"
|
||||||
@@ -63,27 +80,20 @@ You: "Three key differentiators set us apart: First, our implementation takes ju
|
|||||||
Prospect: "I need to think about it"
|
Prospect: "I need to think about it"
|
||||||
You: "I completely understand this is an important decision. What specific concerns can I address for you today? Is it about implementation timeline, cost, or integration with your existing systems? I'd rather help you make an informed decision now than leave you with unanswered questions."`,
|
You: "I completely understand this is an important decision. What specific concerns can I address for you today? Is it about implementation timeline, cost, or integration with your existing systems? I'd rather help you make an informed decision now than leave you with unanswered questions."`,
|
||||||
|
|
||||||
outputInstructions: `**OUTPUT INSTRUCTIONS:**
|
outputInstructions: `**OUTPUT INSTRUCTIONS:**
|
||||||
Provide only the exact words to say in **markdown format**. Be persuasive but not pushy. Focus on value and addressing objections directly. Keep responses **short and impactful**.`,
|
Provide only the exact words to say in **markdown format**. Be persuasive but not pushy. Focus on value and addressing objections directly. Keep responses **short and impactful**.`,
|
||||||
},
|
},
|
||||||
|
|
||||||
meeting: {
|
meeting: {
|
||||||
intro: `You are a meeting assistant. Your job is to provide the exact words to say during professional meetings, presentations, and discussions. Give direct, ready-to-speak responses that are clear and professional.`,
|
intro: `You are a meeting assistant. Your job is to provide the exact words to say during professional meetings, presentations, and discussions. Give direct, ready-to-speak responses that are clear and professional.`,
|
||||||
|
|
||||||
formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
|
searchUsage: `**SEARCH TOOL USAGE:**
|
||||||
- Keep responses SHORT and CONCISE (1-3 sentences max)
|
|
||||||
- Use **markdown formatting** for better readability
|
|
||||||
- Use **bold** for key points and emphasis
|
|
||||||
- Use bullet points (-) for lists when appropriate
|
|
||||||
- Focus on the most essential information only`,
|
|
||||||
|
|
||||||
searchUsage: `**SEARCH TOOL USAGE:**
|
|
||||||
- If participants mention **recent industry news, regulatory changes, or market updates**, **ALWAYS use Google search** for current information
|
- If participants mention **recent industry news, regulatory changes, or market updates**, **ALWAYS use Google search** for current information
|
||||||
- If they reference **competitor activities, recent reports, or current statistics**, search for the latest data first
|
- If they reference **competitor activities, recent reports, or current statistics**, search for the latest data first
|
||||||
- If they discuss **new technologies, tools, or industry developments**, use search to provide accurate insights
|
- If they discuss **new technologies, tools, or industry developments**, use search to provide accurate insights
|
||||||
- After searching, provide a **concise, informed response** that adds value to the discussion`,
|
- After searching, provide a **concise, informed response** that adds value to the discussion`,
|
||||||
|
|
||||||
content: `Examples:
|
content: `Examples:
|
||||||
|
|
||||||
Participant: "What's the status on the project?"
|
Participant: "What's the status on the project?"
|
||||||
You: "We're currently on track to meet our deadline. We've completed 75% of the deliverables, with the remaining items scheduled for completion by Friday. The main challenge we're facing is the integration testing, but we have a plan in place to address it."
|
You: "We're currently on track to meet our deadline. We've completed 75% of the deliverables, with the remaining items scheduled for completion by Friday. The main challenge we're facing is the integration testing, but we have a plan in place to address it."
|
||||||
@@ -94,27 +104,20 @@ You: "Absolutely. We're currently at 80% of our allocated budget with 20% of the
|
|||||||
Participant: "What are the next steps?"
|
Participant: "What are the next steps?"
|
||||||
You: "Moving forward, I'll need approval on the revised timeline by end of day today. Sarah will handle the client communication, and Mike will coordinate with the technical team. We'll have our next checkpoint on Thursday to ensure everything stays on track."`,
|
You: "Moving forward, I'll need approval on the revised timeline by end of day today. Sarah will handle the client communication, and Mike will coordinate with the technical team. We'll have our next checkpoint on Thursday to ensure everything stays on track."`,
|
||||||
|
|
||||||
outputInstructions: `**OUTPUT INSTRUCTIONS:**
|
outputInstructions: `**OUTPUT INSTRUCTIONS:**
|
||||||
Provide only the exact words to say in **markdown format**. Be clear, concise, and action-oriented in your responses. Keep it **short and impactful**.`,
|
Provide only the exact words to say in **markdown format**. Be clear, concise, and action-oriented in your responses. Keep it **short and impactful**.`,
|
||||||
},
|
},
|
||||||
|
|
||||||
presentation: {
|
presentation: {
|
||||||
intro: `You are a presentation coach. Your job is to provide the exact words the presenter should say during presentations, pitches, and public speaking events. Give direct, ready-to-speak responses that are engaging and confident.`,
|
intro: `You are a presentation coach. Your job is to provide the exact words the presenter should say during presentations, pitches, and public speaking events. Give direct, ready-to-speak responses that are engaging and confident.`,
|
||||||
|
|
||||||
formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
|
searchUsage: `**SEARCH TOOL USAGE:**
|
||||||
- Keep responses SHORT and CONCISE (1-3 sentences max)
|
|
||||||
- Use **markdown formatting** for better readability
|
|
||||||
- Use **bold** for key points and emphasis
|
|
||||||
- Use bullet points (-) for lists when appropriate
|
|
||||||
- Focus on the most essential information only`,
|
|
||||||
|
|
||||||
searchUsage: `**SEARCH TOOL USAGE:**
|
|
||||||
- If the audience asks about **recent market trends, current statistics, or latest industry data**, **ALWAYS use Google search** for up-to-date information
|
- If the audience asks about **recent market trends, current statistics, or latest industry data**, **ALWAYS use Google search** for up-to-date information
|
||||||
- If they reference **recent events, new competitors, or current market conditions**, search for the latest information first
|
- If they reference **recent events, new competitors, or current market conditions**, search for the latest information first
|
||||||
- If they inquire about **recent studies, reports, or breaking news** in your field, use search to provide accurate data
|
- If they inquire about **recent studies, reports, or breaking news** in your field, use search to provide accurate data
|
||||||
- After searching, provide a **concise, credible response** with current facts and figures`,
|
- After searching, provide a **concise, credible response** with current facts and figures`,
|
||||||
|
|
||||||
content: `Examples:
|
content: `Examples:
|
||||||
|
|
||||||
Audience: "Can you explain that slide again?"
|
Audience: "Can you explain that slide again?"
|
||||||
You: "Of course. This slide shows our three-year growth trajectory. The blue line represents revenue, which has grown 150% year over year. The orange bars show our customer acquisition, doubling each year. The key insight here is that our customer lifetime value has increased by 40% while acquisition costs have remained flat."
|
You: "Of course. This slide shows our three-year growth trajectory. The blue line represents revenue, which has grown 150% year over year. The orange bars show our customer acquisition, doubling each year. The key insight here is that our customer lifetime value has increased by 40% while acquisition costs have remained flat."
|
||||||
@@ -125,27 +128,20 @@ You: "Great question. Our competitive advantage comes down to three core strengt
|
|||||||
Audience: "How do you plan to scale?"
|
Audience: "How do you plan to scale?"
|
||||||
You: "Our scaling strategy focuses on three pillars. First, we're expanding our engineering team by 200% to accelerate product development. Second, we're entering three new markets next quarter. Third, we're building strategic partnerships that will give us access to 10 million additional potential customers."`,
|
You: "Our scaling strategy focuses on three pillars. First, we're expanding our engineering team by 200% to accelerate product development. Second, we're entering three new markets next quarter. Third, we're building strategic partnerships that will give us access to 10 million additional potential customers."`,
|
||||||
|
|
||||||
outputInstructions: `**OUTPUT INSTRUCTIONS:**
|
outputInstructions: `**OUTPUT INSTRUCTIONS:**
|
||||||
Provide only the exact words to say in **markdown format**. Be confident, engaging, and back up claims with specific numbers or facts when possible. Keep responses **short and impactful**.`,
|
Provide only the exact words to say in **markdown format**. Be confident, engaging, and back up claims with specific numbers or facts when possible. Keep responses **short and impactful**.`,
|
||||||
},
|
},
|
||||||
|
|
||||||
negotiation: {
|
negotiation: {
|
||||||
intro: `You are a negotiation assistant. Your job is to provide the exact words to say during business negotiations, contract discussions, and deal-making conversations. Give direct, ready-to-speak responses that are strategic and professional.`,
|
intro: `You are a negotiation assistant. Your job is to provide the exact words to say during business negotiations, contract discussions, and deal-making conversations. Give direct, ready-to-speak responses that are strategic and professional.`,
|
||||||
|
|
||||||
formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
|
searchUsage: `**SEARCH TOOL USAGE:**
|
||||||
- Keep responses SHORT and CONCISE (1-3 sentences max)
|
|
||||||
- Use **markdown formatting** for better readability
|
|
||||||
- Use **bold** for key points and emphasis
|
|
||||||
- Use bullet points (-) for lists when appropriate
|
|
||||||
- Focus on the most essential information only`,
|
|
||||||
|
|
||||||
searchUsage: `**SEARCH TOOL USAGE:**
|
|
||||||
- If they mention **recent market pricing, current industry standards, or competitor offers**, **ALWAYS use Google search** for current benchmarks
|
- If they mention **recent market pricing, current industry standards, or competitor offers**, **ALWAYS use Google search** for current benchmarks
|
||||||
- If they reference **recent legal changes, new regulations, or market conditions**, search for the latest information first
|
- If they reference **recent legal changes, new regulations, or market conditions**, search for the latest information first
|
||||||
- If they discuss **recent company news, financial performance, or industry developments**, use search to provide informed responses
|
- If they discuss **recent company news, financial performance, or industry developments**, use search to provide informed responses
|
||||||
- After searching, provide a **strategic, well-informed response** that leverages current market intelligence`,
|
- After searching, provide a **strategic, well-informed response** that leverages current market intelligence`,
|
||||||
|
|
||||||
content: `Examples:
|
content: `Examples:
|
||||||
|
|
||||||
Other party: "That price is too high"
|
Other party: "That price is too high"
|
||||||
You: "I understand your concern about the investment. Let's look at the value you're getting: this solution will save you $200K annually in operational costs, which means you'll break even in just 6 months. Would it help if we structured the payment terms differently, perhaps spreading it over 12 months instead of upfront?"
|
You: "I understand your concern about the investment. Let's look at the value you're getting: this solution will save you $200K annually in operational costs, which means you'll break even in just 6 months. Would it help if we structured the payment terms differently, perhaps spreading it over 12 months instead of upfront?"
|
||||||
@@ -156,27 +152,20 @@ You: "I appreciate your directness. We want this to work for both parties. Our c
|
|||||||
Other party: "We're considering other options"
|
Other party: "We're considering other options"
|
||||||
You: "That's smart business practice. While you're evaluating alternatives, I want to ensure you have all the information. Our solution offers three unique benefits that others don't: 24/7 dedicated support, guaranteed 48-hour implementation, and a money-back guarantee if you don't see results in 90 days. How important are these factors in your decision?"`,
|
You: "That's smart business practice. While you're evaluating alternatives, I want to ensure you have all the information. Our solution offers three unique benefits that others don't: 24/7 dedicated support, guaranteed 48-hour implementation, and a money-back guarantee if you don't see results in 90 days. How important are these factors in your decision?"`,
|
||||||
|
|
||||||
outputInstructions: `**OUTPUT INSTRUCTIONS:**
|
outputInstructions: `**OUTPUT INSTRUCTIONS:**
|
||||||
Provide only the exact words to say in **markdown format**. Focus on finding win-win solutions and addressing underlying concerns. Keep responses **short and impactful**.`,
|
Provide only the exact words to say in **markdown format**. Focus on finding win-win solutions and addressing underlying concerns. Keep responses **short and impactful**.`,
|
||||||
},
|
},
|
||||||
|
|
||||||
exam: {
|
exam: {
|
||||||
intro: `You are an exam assistant designed to help students pass tests efficiently. Your role is to provide direct, accurate answers to exam questions with minimal explanation - just enough to confirm the answer is correct.`,
|
intro: `You are an exam assistant designed to help students pass tests efficiently. Your role is to provide direct, accurate answers to exam questions with minimal explanation - just enough to confirm the answer is correct.`,
|
||||||
|
|
||||||
formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
|
searchUsage: `**SEARCH TOOL USAGE:**
|
||||||
- Keep responses SHORT and CONCISE (1-2 sentences max)
|
|
||||||
- Use **markdown formatting** for better readability
|
|
||||||
- Use **bold** for the answer choice/result
|
|
||||||
- Focus on the most essential information only
|
|
||||||
- Provide only brief justification for correctness`,
|
|
||||||
|
|
||||||
searchUsage: `**SEARCH TOOL USAGE:**
|
|
||||||
- If the question involves **recent information, current events, or updated facts**, **ALWAYS use Google search** for the latest data
|
- If the question involves **recent information, current events, or updated facts**, **ALWAYS use Google search** for the latest data
|
||||||
- If they reference **specific dates, statistics, or factual information** that might be outdated, search for current information
|
- If they reference **specific dates, statistics, or factual information** that might be outdated, search for current information
|
||||||
- If they ask about **recent research, new theories, or updated methodologies**, search for the latest information
|
- If they ask about **recent research, new theories, or updated methodologies**, search for the latest information
|
||||||
- After searching, provide **direct, accurate answers** with minimal explanation`,
|
- After searching, provide **direct, accurate answers** with minimal explanation`,
|
||||||
|
|
||||||
content: `Focus on providing efficient exam assistance that helps students pass tests quickly.
|
content: `Focus on providing efficient exam assistance that helps students pass tests quickly.
|
||||||
|
|
||||||
**Key Principles:**
|
**Key Principles:**
|
||||||
1. **Answer the question directly** - no unnecessary explanations
|
1. **Answer the question directly** - no unnecessary explanations
|
||||||
@@ -196,30 +185,62 @@ You: "**Question**: Which of the following is a primary color? A) Green B) Red C
|
|||||||
Question: "Solve for x: 2x + 5 = 13"
|
Question: "Solve for x: 2x + 5 = 13"
|
||||||
You: "**Question**: Solve for x: 2x + 5 = 13 **Answer**: x = 4 **Why**: Subtract 5 from both sides: 2x = 8, then divide by 2: x = 4."`,
|
You: "**Question**: Solve for x: 2x + 5 = 13 **Answer**: x = 4 **Why**: Subtract 5 from both sides: 2x = 8, then divide by 2: x = 4."`,
|
||||||
|
|
||||||
outputInstructions: `**OUTPUT INSTRUCTIONS:**
|
outputInstructions: `**OUTPUT INSTRUCTIONS:**
|
||||||
Provide direct exam answers in **markdown format**. Include the question text, the correct answer choice, and a brief justification. Focus on efficiency and accuracy. Keep responses **short and to the point**.`,
|
Provide direct exam answers in **markdown format**. Include the question text, the correct answer choice, and a brief justification. Focus on efficiency and accuracy. Keep responses **short and to the point**.`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildSystemPrompt(promptParts, customPrompt = '', googleSearchEnabled = true) {
|
function buildSystemPrompt(
|
||||||
const sections = [promptParts.intro, '\n\n', promptParts.formatRequirements];
|
promptParts,
|
||||||
|
customPrompt = "",
|
||||||
|
googleSearchEnabled = true,
|
||||||
|
responseMode = "brief",
|
||||||
|
) {
|
||||||
|
const formatReqs =
|
||||||
|
responseModeFormats[responseMode] || responseModeFormats.brief;
|
||||||
|
const sections = [
|
||||||
|
promptParts.intro,
|
||||||
|
"\n\n",
|
||||||
|
formatReqs,
|
||||||
|
"\n\n",
|
||||||
|
codingAwareness,
|
||||||
|
];
|
||||||
|
|
||||||
// Only add search usage section if Google Search is enabled
|
// Only add search usage section if Google Search is enabled
|
||||||
if (googleSearchEnabled) {
|
if (googleSearchEnabled) {
|
||||||
sections.push('\n\n', promptParts.searchUsage);
|
sections.push("\n\n", promptParts.searchUsage);
|
||||||
}
|
}
|
||||||
|
|
||||||
sections.push('\n\n', promptParts.content, '\n\nUser-provided context\n-----\n', customPrompt, '\n-----\n\n', promptParts.outputInstructions);
|
sections.push(
|
||||||
|
"\n\n",
|
||||||
|
promptParts.content,
|
||||||
|
"\n\nUser-provided context\n-----\n",
|
||||||
|
customPrompt,
|
||||||
|
"\n-----\n\n",
|
||||||
|
promptParts.outputInstructions,
|
||||||
|
);
|
||||||
|
|
||||||
return sections.join('');
|
return sections.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSystemPrompt(profile, customPrompt = '', googleSearchEnabled = true) {
|
function getSystemPrompt(
|
||||||
const promptParts = profilePrompts[profile] || profilePrompts.interview;
|
profile,
|
||||||
return buildSystemPrompt(promptParts, customPrompt, googleSearchEnabled);
|
customPrompt = "",
|
||||||
|
googleSearchEnabled = true,
|
||||||
|
responseMode = "brief",
|
||||||
|
) {
|
||||||
|
const promptParts = profilePrompts[profile] || profilePrompts.interview;
|
||||||
|
return buildSystemPrompt(
|
||||||
|
promptParts,
|
||||||
|
customPrompt,
|
||||||
|
googleSearchEnabled,
|
||||||
|
responseMode,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
profilePrompts,
|
profilePrompts,
|
||||||
getSystemPrompt,
|
responseModeFormats,
|
||||||
|
codingAwareness,
|
||||||
|
getSystemPrompt,
|
||||||
};
|
};
|
||||||
|
|||||||
+1018
-854
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,332 @@
|
|||||||
|
/**
|
||||||
|
* Whisper Worker — runs ONNX Runtime in an isolated child process.
|
||||||
|
*
|
||||||
|
* The main Electron process forks this file and communicates via IPC messages.
|
||||||
|
* If ONNX Runtime crashes (SIGSEGV/SIGABRT inside the native Metal or CPU
|
||||||
|
* execution provider), only this worker dies — the main process survives and
|
||||||
|
* can respawn the worker automatically.
|
||||||
|
*
|
||||||
|
* Protocol (parent ↔ worker):
|
||||||
|
* parent → worker:
|
||||||
|
* { type: 'load', modelName, cacheDir, device? }
|
||||||
|
* { type: 'transcribe', audioBase64, language? } // PCM 16-bit 16kHz as base64
|
||||||
|
* { type: 'shutdown' }
|
||||||
|
*
|
||||||
|
* worker → parent:
|
||||||
|
* { type: 'load-result', success, error?, device? }
|
||||||
|
* { type: 'transcribe-result', success, text?, error? }
|
||||||
|
* { type: 'status', message }
|
||||||
|
* { type: 'ready' }
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Crash handlers — report fatal errors before the process dies ──
|
||||||
|
|
||||||
|
process.on("uncaughtException", (err) => {
|
||||||
|
try {
|
||||||
|
send({
|
||||||
|
type: "status",
|
||||||
|
message: `[Worker] Uncaught exception: ${err.message || err}`,
|
||||||
|
});
|
||||||
|
console.error("[WhisperWorker] Uncaught exception:", err);
|
||||||
|
} catch (_) {
|
||||||
|
// Cannot communicate with parent anymore
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (reason) => {
|
||||||
|
try {
|
||||||
|
send({
|
||||||
|
type: "status",
|
||||||
|
message: `[Worker] Unhandled rejection: ${reason?.message || reason}`,
|
||||||
|
});
|
||||||
|
console.error("[WhisperWorker] Unhandled rejection:", reason);
|
||||||
|
} catch (_) {
|
||||||
|
// Cannot communicate with parent anymore
|
||||||
|
}
|
||||||
|
// Don't exit — let it be caught by the pipeline's own handlers
|
||||||
|
});
|
||||||
|
|
||||||
|
let whisperPipeline = null;
|
||||||
|
/** Which ONNX backend is actually active: "cpu" | "wasm" */
|
||||||
|
let activeDevice = null;
|
||||||
|
|
||||||
|
function pcm16ToFloat32(pcm16Buffer) {
|
||||||
|
if (!pcm16Buffer || pcm16Buffer.length === 0) {
|
||||||
|
return new Float32Array(0);
|
||||||
|
}
|
||||||
|
const alignedLength =
|
||||||
|
pcm16Buffer.length % 2 === 0 ? pcm16Buffer.length : pcm16Buffer.length - 1;
|
||||||
|
const samples = alignedLength / 2;
|
||||||
|
const float32 = new Float32Array(samples);
|
||||||
|
for (let i = 0; i < samples; i++) {
|
||||||
|
float32[i] = pcm16Buffer.readInt16LE(i * 2) / 32768;
|
||||||
|
}
|
||||||
|
return float32;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the Whisper model.
|
||||||
|
*
|
||||||
|
* @param {string} modelName HuggingFace model id, e.g. "Xenova/whisper-small"
|
||||||
|
* @param {string} cacheDir Directory for cached model files
|
||||||
|
* @param {string} [device] "cpu" (onnxruntime-node) or "wasm" (onnxruntime-web).
|
||||||
|
* When "cpu" is requested we try native first and fall
|
||||||
|
* back to "wasm" on failure (ABI mismatch, etc.).
|
||||||
|
*/
|
||||||
|
async function loadModel(modelName, cacheDir, device = "cpu") {
|
||||||
|
if (whisperPipeline) {
|
||||||
|
send({ type: "load-result", success: true, device: activeDevice });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
send({
|
||||||
|
type: "status",
|
||||||
|
message: "Loading Whisper model (first time may take a while)...",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate / create cache directory
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
if (cacheDir) {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(cacheDir)) {
|
||||||
|
fs.mkdirSync(cacheDir, { recursive: true });
|
||||||
|
console.log("[WhisperWorker] Created cache directory:", cacheDir);
|
||||||
|
}
|
||||||
|
} catch (mkdirErr) {
|
||||||
|
console.warn(
|
||||||
|
"[WhisperWorker] Cannot create cache dir:",
|
||||||
|
mkdirErr.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for corrupted partial downloads — if an onnx file exists but
|
||||||
|
// is suspiciously small (< 1 KB), delete it so the library re-downloads.
|
||||||
|
try {
|
||||||
|
const modelDir = path.join(cacheDir, modelName.replace("/", path.sep));
|
||||||
|
if (fs.existsSync(modelDir)) {
|
||||||
|
const walk = (dir) => {
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const full = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walk(full);
|
||||||
|
} else if (
|
||||||
|
entry.name.endsWith(".onnx") &&
|
||||||
|
fs.statSync(full).size < 1024
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
"[WhisperWorker] Removing likely-corrupt file:",
|
||||||
|
full,
|
||||||
|
);
|
||||||
|
fs.unlinkSync(full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(modelDir);
|
||||||
|
}
|
||||||
|
} catch (cleanErr) {
|
||||||
|
console.warn("[WhisperWorker] Cache cleanup error:", cleanErr.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pipeline, env } = await import("@huggingface/transformers");
|
||||||
|
env.cacheDir = cacheDir;
|
||||||
|
|
||||||
|
// Attempt to load with the requested device
|
||||||
|
const devicesToTry = device === "wasm" ? ["wasm"] : ["cpu", "wasm"];
|
||||||
|
|
||||||
|
let lastError = null;
|
||||||
|
|
||||||
|
for (const dev of devicesToTry) {
|
||||||
|
try {
|
||||||
|
send({
|
||||||
|
type: "status",
|
||||||
|
message: `Loading Whisper (${dev} backend)...`,
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
`[WhisperWorker] Trying device: ${dev}, model: ${modelName}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
whisperPipeline = await pipeline(
|
||||||
|
"automatic-speech-recognition",
|
||||||
|
modelName,
|
||||||
|
{
|
||||||
|
dtype: "q8",
|
||||||
|
device: dev,
|
||||||
|
progress_callback: (progress) => {
|
||||||
|
// progress: { status, name?, file?, progress?, loaded?, total? }
|
||||||
|
if (
|
||||||
|
progress.status === "download" ||
|
||||||
|
progress.status === "progress"
|
||||||
|
) {
|
||||||
|
send({
|
||||||
|
type: "progress",
|
||||||
|
file: progress.file || progress.name || "",
|
||||||
|
progress: progress.progress ?? 0,
|
||||||
|
loaded: progress.loaded ?? 0,
|
||||||
|
total: progress.total ?? 0,
|
||||||
|
status: progress.status,
|
||||||
|
});
|
||||||
|
} else if (progress.status === "done") {
|
||||||
|
send({
|
||||||
|
type: "progress",
|
||||||
|
file: progress.file || progress.name || "",
|
||||||
|
progress: 100,
|
||||||
|
loaded: progress.total ?? 0,
|
||||||
|
total: progress.total ?? 0,
|
||||||
|
status: "done",
|
||||||
|
});
|
||||||
|
} else if (progress.status === "initiate") {
|
||||||
|
send({
|
||||||
|
type: "progress",
|
||||||
|
file: progress.file || progress.name || "",
|
||||||
|
progress: 0,
|
||||||
|
loaded: 0,
|
||||||
|
total: 0,
|
||||||
|
status: "initiate",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
activeDevice = dev;
|
||||||
|
console.log(
|
||||||
|
`[WhisperWorker] Model loaded successfully (device: ${dev})`,
|
||||||
|
);
|
||||||
|
send({ type: "load-result", success: true, device: dev });
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
lastError = err;
|
||||||
|
console.error(
|
||||||
|
`[WhisperWorker] Failed to load with device "${dev}":`,
|
||||||
|
err.message || err,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dev === "cpu" && devicesToTry.includes("wasm")) {
|
||||||
|
send({
|
||||||
|
type: "status",
|
||||||
|
message: `Native CPU backend failed (${err.message}). Trying WASM fallback...`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset pipeline state before retry
|
||||||
|
whisperPipeline = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All devices failed
|
||||||
|
throw lastError || new Error("All ONNX backends failed");
|
||||||
|
} catch (error) {
|
||||||
|
send({ type: "load-result", success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transcribe(audioBase64, language) {
|
||||||
|
if (!whisperPipeline) {
|
||||||
|
send({
|
||||||
|
type: "transcribe-result",
|
||||||
|
success: false,
|
||||||
|
error: "Whisper pipeline not loaded",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pcm16Buffer = Buffer.from(audioBase64, "base64");
|
||||||
|
|
||||||
|
if (pcm16Buffer.length < 2) {
|
||||||
|
send({
|
||||||
|
type: "transcribe-result",
|
||||||
|
success: false,
|
||||||
|
error: "Audio buffer too small",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap at ~30 seconds (16kHz, 16-bit mono)
|
||||||
|
const maxBytes = 16000 * 2 * 30;
|
||||||
|
const audioData =
|
||||||
|
pcm16Buffer.length > maxBytes
|
||||||
|
? pcm16Buffer.slice(0, maxBytes)
|
||||||
|
: pcm16Buffer;
|
||||||
|
|
||||||
|
const float32Audio = pcm16ToFloat32(audioData);
|
||||||
|
if (float32Audio.length === 0) {
|
||||||
|
send({
|
||||||
|
type: "transcribe-result",
|
||||||
|
success: false,
|
||||||
|
error: "Empty audio after conversion",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build pipeline options with the requested language
|
||||||
|
const pipelineOpts = {
|
||||||
|
sampling_rate: 16000,
|
||||||
|
task: "transcribe",
|
||||||
|
};
|
||||||
|
if (language && language !== "auto") {
|
||||||
|
pipelineOpts.language = language;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await whisperPipeline(float32Audio, pipelineOpts);
|
||||||
|
|
||||||
|
const text = result.text?.trim() || "";
|
||||||
|
send({ type: "transcribe-result", success: true, text });
|
||||||
|
} catch (error) {
|
||||||
|
send({
|
||||||
|
type: "transcribe-result",
|
||||||
|
success: false,
|
||||||
|
error: error.message || String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function send(msg) {
|
||||||
|
try {
|
||||||
|
if (process.send) {
|
||||||
|
process.send(msg);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Parent may have disconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on("message", (msg) => {
|
||||||
|
switch (msg.type) {
|
||||||
|
case "load":
|
||||||
|
loadModel(msg.modelName, msg.cacheDir, msg.device).catch((err) => {
|
||||||
|
send({ type: "load-result", success: false, error: err.message });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "transcribe":
|
||||||
|
transcribe(msg.audioBase64, msg.language).catch((err) => {
|
||||||
|
send({ type: "transcribe-result", success: false, error: err.message });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "shutdown":
|
||||||
|
// Dispose the ONNX session gracefully before exiting to avoid
|
||||||
|
// native cleanup race conditions (SIGABRT on mutex destroy).
|
||||||
|
(async () => {
|
||||||
|
if (whisperPipeline) {
|
||||||
|
try {
|
||||||
|
if (typeof whisperPipeline.dispose === "function") {
|
||||||
|
await whisperPipeline.dispose();
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Best-effort cleanup
|
||||||
|
}
|
||||||
|
whisperPipeline = null;
|
||||||
|
}
|
||||||
|
// Small delay to let native threads wind down
|
||||||
|
setTimeout(() => process.exit(0), 200);
|
||||||
|
})();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Signal readiness to parent
|
||||||
|
send({ type: "ready" });
|
||||||
+362
-301
@@ -1,368 +1,429 @@
|
|||||||
const { BrowserWindow, globalShortcut, ipcMain, screen } = require('electron');
|
const { BrowserWindow, globalShortcut, ipcMain, screen } = require("electron");
|
||||||
const path = require('node:path');
|
const path = require("node:path");
|
||||||
const storage = require('../storage');
|
const storage = require("../storage");
|
||||||
|
|
||||||
let mouseEventsIgnored = false;
|
let mouseEventsIgnored = false;
|
||||||
|
|
||||||
function createWindow(sendToRenderer, geminiSessionRef) {
|
function createWindow(sendToRenderer, geminiSessionRef) {
|
||||||
// Get layout preference (default to 'normal')
|
// Get layout preference (default to 'normal')
|
||||||
let windowWidth = 1100;
|
let windowWidth = 1100;
|
||||||
let windowHeight = 800;
|
let windowHeight = 800;
|
||||||
|
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: windowWidth,
|
width: windowWidth,
|
||||||
height: windowHeight,
|
height: windowHeight,
|
||||||
frame: false,
|
frame: false,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
hasShadow: false,
|
hasShadow: false,
|
||||||
alwaysOnTop: true,
|
alwaysOnTop: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
contextIsolation: false, // TODO: change to true
|
contextIsolation: false, // TODO: change to true
|
||||||
backgroundThrottling: false,
|
backgroundThrottling: false,
|
||||||
enableBlinkFeatures: 'GetDisplayMedia',
|
enableBlinkFeatures: "GetDisplayMedia",
|
||||||
webSecurity: true,
|
webSecurity: true,
|
||||||
allowRunningInsecureContent: false,
|
allowRunningInsecureContent: false,
|
||||||
},
|
},
|
||||||
backgroundColor: '#00000000',
|
backgroundColor: "#00000000",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { session, desktopCapturer } = require('electron');
|
const { session, desktopCapturer } = require("electron");
|
||||||
session.defaultSession.setDisplayMediaRequestHandler(
|
session.defaultSession.setDisplayMediaRequestHandler(
|
||||||
(request, callback) => {
|
(request, callback) => {
|
||||||
desktopCapturer.getSources({ types: ['screen'] }).then(sources => {
|
desktopCapturer.getSources({ types: ["screen"] }).then((sources) => {
|
||||||
callback({ video: sources[0], audio: 'loopback' });
|
callback({ video: sources[0], audio: "loopback" });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ useSystemPicker: true }
|
{ useSystemPicker: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
mainWindow.setResizable(false);
|
mainWindow.setResizable(false);
|
||||||
mainWindow.setContentProtection(true);
|
mainWindow.setContentProtection(true);
|
||||||
mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
||||||
|
|
||||||
// Hide from Windows taskbar
|
// Hide from Windows taskbar
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === "win32") {
|
||||||
try {
|
try {
|
||||||
mainWindow.setSkipTaskbar(true);
|
mainWindow.setSkipTaskbar(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Could not hide from taskbar:', error.message);
|
console.warn("Could not hide from taskbar:", error.message);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Hide from Mission Control on macOS
|
// Hide from Mission Control on macOS
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === "darwin") {
|
||||||
try {
|
try {
|
||||||
mainWindow.setHiddenInMissionControl(true);
|
mainWindow.setHiddenInMissionControl(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Could not hide from Mission Control:', error.message);
|
console.warn("Could not hide from Mission Control:", error.message);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Center window at the top of the screen
|
// Center window at the top of the screen
|
||||||
const primaryDisplay = screen.getPrimaryDisplay();
|
const primaryDisplay = screen.getPrimaryDisplay();
|
||||||
const { width: screenWidth } = primaryDisplay.workAreaSize;
|
const { width: screenWidth } = primaryDisplay.workAreaSize;
|
||||||
const x = Math.floor((screenWidth - windowWidth) / 2);
|
const x = Math.floor((screenWidth - windowWidth) / 2);
|
||||||
const y = 0;
|
const y = 0;
|
||||||
mainWindow.setPosition(x, y);
|
mainWindow.setPosition(x, y);
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === "win32") {
|
||||||
mainWindow.setAlwaysOnTop(true, 'screen-saver', 1);
|
mainWindow.setAlwaysOnTop(true, "screen-saver", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
mainWindow.loadFile(path.join(__dirname, '../index.html'));
|
mainWindow.loadFile(path.join(__dirname, "../index.html"));
|
||||||
|
|
||||||
// After window is created, initialize keybinds
|
// After window is created, initialize keybinds
|
||||||
mainWindow.webContents.once('dom-ready', () => {
|
mainWindow.webContents.once("dom-ready", () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const defaultKeybinds = getDefaultKeybinds();
|
const defaultKeybinds = getDefaultKeybinds();
|
||||||
let keybinds = defaultKeybinds;
|
let keybinds = defaultKeybinds;
|
||||||
|
|
||||||
// Load keybinds from storage
|
// Load keybinds from storage
|
||||||
const savedKeybinds = storage.getKeybinds();
|
const savedKeybinds = storage.getKeybinds();
|
||||||
if (savedKeybinds) {
|
if (savedKeybinds) {
|
||||||
keybinds = { ...defaultKeybinds, ...savedKeybinds };
|
keybinds = { ...defaultKeybinds, ...savedKeybinds };
|
||||||
}
|
}
|
||||||
|
|
||||||
updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessionRef);
|
updateGlobalShortcuts(
|
||||||
}, 150);
|
keybinds,
|
||||||
});
|
mainWindow,
|
||||||
|
sendToRenderer,
|
||||||
|
geminiSessionRef,
|
||||||
|
);
|
||||||
|
}, 150);
|
||||||
|
});
|
||||||
|
|
||||||
setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef);
|
setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef);
|
||||||
|
|
||||||
return mainWindow;
|
return mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultKeybinds() {
|
function getDefaultKeybinds() {
|
||||||
const isMac = process.platform === 'darwin';
|
const isMac = process.platform === "darwin";
|
||||||
return {
|
return {
|
||||||
moveUp: isMac ? 'Alt+Up' : 'Ctrl+Up',
|
moveUp: isMac ? "Alt+Up" : "Ctrl+Up",
|
||||||
moveDown: isMac ? 'Alt+Down' : 'Ctrl+Down',
|
moveDown: isMac ? "Alt+Down" : "Ctrl+Down",
|
||||||
moveLeft: isMac ? 'Alt+Left' : 'Ctrl+Left',
|
moveLeft: isMac ? "Alt+Left" : "Ctrl+Left",
|
||||||
moveRight: isMac ? 'Alt+Right' : 'Ctrl+Right',
|
moveRight: isMac ? "Alt+Right" : "Ctrl+Right",
|
||||||
toggleVisibility: isMac ? 'Cmd+\\' : 'Ctrl+\\',
|
toggleVisibility: isMac ? "Cmd+\\" : "Ctrl+\\",
|
||||||
toggleClickThrough: isMac ? 'Cmd+M' : 'Ctrl+M',
|
toggleClickThrough: isMac ? "Cmd+M" : "Ctrl+M",
|
||||||
nextStep: isMac ? 'Cmd+Enter' : 'Ctrl+Enter',
|
nextStep: isMac ? "Cmd+Enter" : "Ctrl+Enter",
|
||||||
previousResponse: isMac ? 'Cmd+[' : 'Ctrl+[',
|
previousResponse: isMac ? "Cmd+[" : "Ctrl+[",
|
||||||
nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]',
|
nextResponse: isMac ? "Cmd+]" : "Ctrl+]",
|
||||||
scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up',
|
scrollUp: isMac ? "Cmd+Shift+Up" : "Ctrl+Shift+Up",
|
||||||
scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down',
|
scrollDown: isMac ? "Cmd+Shift+Down" : "Ctrl+Shift+Down",
|
||||||
emergencyErase: isMac ? 'Cmd+Shift+E' : 'Ctrl+Shift+E',
|
expandResponse: isMac ? "Cmd+E" : "Ctrl+E",
|
||||||
};
|
emergencyErase: isMac ? "Cmd+Shift+E" : "Ctrl+Shift+E",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessionRef) {
|
function updateGlobalShortcuts(
|
||||||
console.log('Updating global shortcuts with:', keybinds);
|
keybinds,
|
||||||
|
mainWindow,
|
||||||
|
sendToRenderer,
|
||||||
|
geminiSessionRef,
|
||||||
|
) {
|
||||||
|
console.log("Updating global shortcuts with:", keybinds);
|
||||||
|
|
||||||
// Unregister all existing shortcuts
|
// Unregister all existing shortcuts
|
||||||
globalShortcut.unregisterAll();
|
globalShortcut.unregisterAll();
|
||||||
|
|
||||||
const primaryDisplay = screen.getPrimaryDisplay();
|
const primaryDisplay = screen.getPrimaryDisplay();
|
||||||
const { width, height } = primaryDisplay.workAreaSize;
|
const { width, height } = primaryDisplay.workAreaSize;
|
||||||
const moveIncrement = Math.floor(Math.min(width, height) * 0.1);
|
const moveIncrement = Math.floor(Math.min(width, height) * 0.1);
|
||||||
|
|
||||||
// Register window movement shortcuts
|
// Register window movement shortcuts
|
||||||
const movementActions = {
|
const movementActions = {
|
||||||
moveUp: () => {
|
moveUp: () => {
|
||||||
if (!mainWindow.isVisible()) return;
|
if (!mainWindow.isVisible()) return;
|
||||||
const [currentX, currentY] = mainWindow.getPosition();
|
const [currentX, currentY] = mainWindow.getPosition();
|
||||||
mainWindow.setPosition(currentX, currentY - moveIncrement);
|
mainWindow.setPosition(currentX, currentY - moveIncrement);
|
||||||
},
|
},
|
||||||
moveDown: () => {
|
moveDown: () => {
|
||||||
if (!mainWindow.isVisible()) return;
|
if (!mainWindow.isVisible()) return;
|
||||||
const [currentX, currentY] = mainWindow.getPosition();
|
const [currentX, currentY] = mainWindow.getPosition();
|
||||||
mainWindow.setPosition(currentX, currentY + moveIncrement);
|
mainWindow.setPosition(currentX, currentY + moveIncrement);
|
||||||
},
|
},
|
||||||
moveLeft: () => {
|
moveLeft: () => {
|
||||||
if (!mainWindow.isVisible()) return;
|
if (!mainWindow.isVisible()) return;
|
||||||
const [currentX, currentY] = mainWindow.getPosition();
|
const [currentX, currentY] = mainWindow.getPosition();
|
||||||
mainWindow.setPosition(currentX - moveIncrement, currentY);
|
mainWindow.setPosition(currentX - moveIncrement, currentY);
|
||||||
},
|
},
|
||||||
moveRight: () => {
|
moveRight: () => {
|
||||||
if (!mainWindow.isVisible()) return;
|
if (!mainWindow.isVisible()) return;
|
||||||
const [currentX, currentY] = mainWindow.getPosition();
|
const [currentX, currentY] = mainWindow.getPosition();
|
||||||
mainWindow.setPosition(currentX + moveIncrement, currentY);
|
mainWindow.setPosition(currentX + moveIncrement, currentY);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register each movement shortcut
|
// Register each movement shortcut
|
||||||
Object.keys(movementActions).forEach(action => {
|
Object.keys(movementActions).forEach((action) => {
|
||||||
const keybind = keybinds[action];
|
const keybind = keybinds[action];
|
||||||
if (keybind) {
|
if (keybind) {
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(keybind, movementActions[action]);
|
globalShortcut.register(keybind, movementActions[action]);
|
||||||
console.log(`Registered ${action}: ${keybind}`);
|
console.log(`Registered ${action}: ${keybind}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register ${action} (${keybind}):`, error);
|
console.error(`Failed to register ${action} (${keybind}):`, error);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register toggle visibility shortcut
|
|
||||||
if (keybinds.toggleVisibility) {
|
|
||||||
try {
|
|
||||||
globalShortcut.register(keybinds.toggleVisibility, () => {
|
|
||||||
if (mainWindow.isVisible()) {
|
|
||||||
mainWindow.hide();
|
|
||||||
} else {
|
|
||||||
mainWindow.showInactive();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log(`Registered toggleVisibility: ${keybinds.toggleVisibility}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`, error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Register toggle click-through shortcut
|
// Register toggle visibility shortcut
|
||||||
if (keybinds.toggleClickThrough) {
|
if (keybinds.toggleVisibility) {
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(keybinds.toggleClickThrough, () => {
|
globalShortcut.register(keybinds.toggleVisibility, () => {
|
||||||
mouseEventsIgnored = !mouseEventsIgnored;
|
if (mainWindow.isVisible()) {
|
||||||
if (mouseEventsIgnored) {
|
mainWindow.hide();
|
||||||
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
} else {
|
||||||
console.log('Mouse events ignored');
|
mainWindow.showInactive();
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
console.log(`Registered toggleVisibility: ${keybinds.toggleVisibility}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Register next step shortcut (either starts session or takes screenshot based on view)
|
// Register toggle click-through shortcut
|
||||||
if (keybinds.nextStep) {
|
if (keybinds.toggleClickThrough) {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register next step shortcut (either starts session or takes screenshot based on view)
|
||||||
|
if (keybinds.nextStep) {
|
||||||
|
try {
|
||||||
|
globalShortcut.register(keybinds.nextStep, async () => {
|
||||||
|
console.log("Next step shortcut triggered");
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(keybinds.nextStep, async () => {
|
// Determine the shortcut key format
|
||||||
console.log('Next step shortcut triggered');
|
const isMac = process.platform === "darwin";
|
||||||
try {
|
const shortcutKey = isMac ? "cmd+enter" : "ctrl+enter";
|
||||||
// Determine the shortcut key format
|
|
||||||
const isMac = process.platform === 'darwin';
|
|
||||||
const shortcutKey = isMac ? 'cmd+enter' : 'ctrl+enter';
|
|
||||||
|
|
||||||
// Use the new handleShortcut function
|
// Use the new handleShortcut function
|
||||||
mainWindow.webContents.executeJavaScript(`
|
mainWindow.webContents.executeJavaScript(`
|
||||||
cheatingDaddy.handleShortcut('${shortcutKey}');
|
cheatingDaddy.handleShortcut('${shortcutKey}');
|
||||||
`);
|
`);
|
||||||
} catch (error) {
|
|
||||||
console.error('Error handling next step shortcut:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log(`Registered nextStep: ${keybinds.nextStep}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register nextStep (${keybinds.nextStep}):`, error);
|
console.error("Error handling next step shortcut:", error);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
console.log(`Registered nextStep: ${keybinds.nextStep}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to register nextStep (${keybinds.nextStep}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Register previous response shortcut
|
// Register previous response shortcut
|
||||||
if (keybinds.previousResponse) {
|
if (keybinds.previousResponse) {
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(keybinds.previousResponse, () => {
|
globalShortcut.register(keybinds.previousResponse, () => {
|
||||||
console.log('Previous response shortcut triggered');
|
console.log("Previous response shortcut triggered");
|
||||||
sendToRenderer('navigate-previous-response');
|
sendToRenderer("navigate-previous-response");
|
||||||
});
|
});
|
||||||
console.log(`Registered previousResponse: ${keybinds.previousResponse}`);
|
console.log(`Registered previousResponse: ${keybinds.previousResponse}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register previousResponse (${keybinds.previousResponse}):`, error);
|
console.error(
|
||||||
}
|
`Failed to register previousResponse (${keybinds.previousResponse}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Register next response shortcut
|
// Register next response shortcut
|
||||||
if (keybinds.nextResponse) {
|
if (keybinds.nextResponse) {
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(keybinds.nextResponse, () => {
|
globalShortcut.register(keybinds.nextResponse, () => {
|
||||||
console.log('Next response shortcut triggered');
|
console.log("Next response shortcut triggered");
|
||||||
sendToRenderer('navigate-next-response');
|
sendToRenderer("navigate-next-response");
|
||||||
});
|
});
|
||||||
console.log(`Registered nextResponse: ${keybinds.nextResponse}`);
|
console.log(`Registered nextResponse: ${keybinds.nextResponse}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register nextResponse (${keybinds.nextResponse}):`, error);
|
console.error(
|
||||||
}
|
`Failed to register nextResponse (${keybinds.nextResponse}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Register scroll up shortcut
|
// Register scroll up shortcut
|
||||||
if (keybinds.scrollUp) {
|
if (keybinds.scrollUp) {
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(keybinds.scrollUp, () => {
|
globalShortcut.register(keybinds.scrollUp, () => {
|
||||||
console.log('Scroll up shortcut triggered');
|
console.log("Scroll up shortcut triggered");
|
||||||
sendToRenderer('scroll-response-up');
|
sendToRenderer("scroll-response-up");
|
||||||
});
|
});
|
||||||
console.log(`Registered scrollUp: ${keybinds.scrollUp}`);
|
console.log(`Registered scrollUp: ${keybinds.scrollUp}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register scrollUp (${keybinds.scrollUp}):`, error);
|
console.error(
|
||||||
}
|
`Failed to register scrollUp (${keybinds.scrollUp}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Register scroll down shortcut
|
// Register scroll down shortcut
|
||||||
if (keybinds.scrollDown) {
|
if (keybinds.scrollDown) {
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(keybinds.scrollDown, () => {
|
globalShortcut.register(keybinds.scrollDown, () => {
|
||||||
console.log('Scroll down shortcut triggered');
|
console.log("Scroll down shortcut triggered");
|
||||||
sendToRenderer('scroll-response-down');
|
sendToRenderer("scroll-response-down");
|
||||||
});
|
});
|
||||||
console.log(`Registered scrollDown: ${keybinds.scrollDown}`);
|
console.log(`Registered scrollDown: ${keybinds.scrollDown}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register scrollDown (${keybinds.scrollDown}):`, error);
|
console.error(
|
||||||
}
|
`Failed to register scrollDown (${keybinds.scrollDown}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Register emergency erase shortcut
|
// Register expand response shortcut
|
||||||
if (keybinds.emergencyErase) {
|
if (keybinds.expandResponse) {
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(keybinds.emergencyErase, () => {
|
globalShortcut.register(keybinds.expandResponse, () => {
|
||||||
console.log('Emergency Erase triggered!');
|
console.log("Expand response shortcut triggered");
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
sendToRenderer("expand-response");
|
||||||
mainWindow.hide();
|
});
|
||||||
|
console.log(`Registered expandResponse: ${keybinds.expandResponse}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to register expandResponse (${keybinds.expandResponse}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register emergency erase shortcut
|
||||||
|
if (keybinds.emergencyErase) {
|
||||||
|
try {
|
||||||
|
globalShortcut.register(keybinds.emergencyErase, () => {
|
||||||
|
console.log("Emergency Erase triggered!");
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.hide();
|
||||||
|
|
||||||
if (geminiSessionRef.current) {
|
if (geminiSessionRef.current) {
|
||||||
geminiSessionRef.current.close();
|
geminiSessionRef.current.close();
|
||||||
geminiSessionRef.current = null;
|
geminiSessionRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToRenderer('clear-sensitive-data');
|
sendToRenderer("clear-sensitive-data");
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const { app } = require('electron');
|
const { app } = require("electron");
|
||||||
app.quit();
|
app.quit();
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log(`Registered emergencyErase: ${keybinds.emergencyErase}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to register emergencyErase (${keybinds.emergencyErase}):`, error);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
console.log(`Registered emergencyErase: ${keybinds.emergencyErase}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to register emergencyErase (${keybinds.emergencyErase}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
|
function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
|
||||||
ipcMain.on('view-changed', (event, view) => {
|
ipcMain.on("view-changed", (event, view) => {
|
||||||
if (!mainWindow.isDestroyed()) {
|
if (!mainWindow.isDestroyed()) {
|
||||||
const primaryDisplay = screen.getPrimaryDisplay();
|
const primaryDisplay = screen.getPrimaryDisplay();
|
||||||
const { width: screenWidth } = primaryDisplay.workAreaSize;
|
const { width: screenWidth } = primaryDisplay.workAreaSize;
|
||||||
|
|
||||||
if (view === 'assistant') {
|
if (view === "assistant") {
|
||||||
// Shrink window for live view
|
// Shrink window for live view
|
||||||
const liveWidth = 850;
|
const liveWidth = 850;
|
||||||
const liveHeight = 400;
|
const liveHeight = 400;
|
||||||
const x = Math.floor((screenWidth - liveWidth) / 2);
|
const x = Math.floor((screenWidth - liveWidth) / 2);
|
||||||
mainWindow.setSize(liveWidth, liveHeight);
|
mainWindow.setSize(liveWidth, liveHeight);
|
||||||
mainWindow.setPosition(x, 0);
|
mainWindow.setPosition(x, 0);
|
||||||
} else {
|
} else {
|
||||||
// Restore full size
|
// Restore full size
|
||||||
const fullWidth = 1100;
|
const fullWidth = 1100;
|
||||||
const fullHeight = 800;
|
const fullHeight = 800;
|
||||||
const x = Math.floor((screenWidth - fullWidth) / 2);
|
const x = Math.floor((screenWidth - fullWidth) / 2);
|
||||||
mainWindow.setSize(fullWidth, fullHeight);
|
mainWindow.setSize(fullWidth, fullHeight);
|
||||||
mainWindow.setPosition(x, 0);
|
mainWindow.setPosition(x, 0);
|
||||||
mainWindow.setIgnoreMouseEvents(false);
|
mainWindow.setIgnoreMouseEvents(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('window-minimize', () => {
|
ipcMain.handle("window-minimize", () => {
|
||||||
if (!mainWindow.isDestroyed()) {
|
if (!mainWindow.isDestroyed()) {
|
||||||
mainWindow.minimize();
|
mainWindow.minimize();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('update-keybinds', (event, newKeybinds) => {
|
ipcMain.on("update-keybinds", (event, newKeybinds) => {
|
||||||
if (!mainWindow.isDestroyed()) {
|
if (!mainWindow.isDestroyed()) {
|
||||||
updateGlobalShortcuts(newKeybinds, mainWindow, sendToRenderer, geminiSessionRef);
|
updateGlobalShortcuts(
|
||||||
}
|
newKeybinds,
|
||||||
});
|
mainWindow,
|
||||||
|
sendToRenderer,
|
||||||
|
geminiSessionRef,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('toggle-window-visibility', async event => {
|
ipcMain.handle("toggle-window-visibility", async (event) => {
|
||||||
try {
|
try {
|
||||||
if (mainWindow.isDestroyed()) {
|
if (mainWindow.isDestroyed()) {
|
||||||
return { success: false, error: 'Window has been destroyed' };
|
return { success: false, error: "Window has been destroyed" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mainWindow.isVisible()) {
|
if (mainWindow.isVisible()) {
|
||||||
mainWindow.hide();
|
mainWindow.hide();
|
||||||
} else {
|
} else {
|
||||||
mainWindow.showInactive();
|
mainWindow.showInactive();
|
||||||
}
|
}
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling window visibility:', error);
|
console.error("Error toggling window visibility:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('update-sizes', async event => {
|
ipcMain.handle("update-sizes", async (event) => {
|
||||||
// With the sidebar layout, the window size is user-controlled.
|
// With the sidebar layout, the window size is user-controlled.
|
||||||
// This handler is kept for compatibility but is a no-op now.
|
// This handler is kept for compatibility but is a no-op now.
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createWindow,
|
createWindow,
|
||||||
getDefaultKeybinds,
|
getDefaultKeybinds,
|
||||||
updateGlobalShortcuts,
|
updateGlobalShortcuts,
|
||||||
setupWindowIpcHandlers,
|
setupWindowIpcHandlers,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user