Compare commits
13
Commits
bd62cf5524
...
v0.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31d50c9713 | ||
|
|
bbad79875c | ||
|
|
7f15b65eb1 | ||
|
|
d6dbaa3141 | ||
|
|
2ebde60dcd | ||
|
|
0d56e06724 | ||
|
|
526bc4e877 | ||
|
|
684b61755c | ||
|
|
1b74968006 | ||
|
|
4cf48ee0af | ||
|
|
494e692738 | ||
|
|
8b216bbb33 | ||
|
|
430895d9ab |
+28
-23
@@ -1,14 +1,15 @@
|
|||||||
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'],
|
extraResource: ["./src/assets/SystemAudioDump"],
|
||||||
name: 'Cheating Daddy',
|
name: "Mastermind",
|
||||||
icon: 'src/assets/logo',
|
icon: "src/assets/logo",
|
||||||
// use `security find-identity -v -p codesigning` to find your identity
|
// use `security find-identity -v -p codesigning` to find your identity
|
||||||
// for macos signing
|
// for macos signing
|
||||||
// also fuck apple
|
// also fuck apple
|
||||||
@@ -27,40 +28,44 @@ module.exports = {
|
|||||||
// teamId: 'your team id',
|
// teamId: 'your team id',
|
||||||
// },
|
// },
|
||||||
},
|
},
|
||||||
rebuildConfig: {},
|
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: [
|
makers: [
|
||||||
{
|
{
|
||||||
name: '@electron-forge/maker-squirrel',
|
name: "@electron-forge/maker-squirrel",
|
||||||
config: {
|
config: {
|
||||||
name: 'cheating-daddy',
|
name: "mastermind",
|
||||||
productName: 'Cheating Daddy',
|
productName: "Mastermind",
|
||||||
shortcutName: 'Cheating Daddy',
|
shortcutName: "Mastermind",
|
||||||
createDesktopShortcut: true,
|
createDesktopShortcut: true,
|
||||||
createStartMenuShortcut: true,
|
createStartMenuShortcut: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '@electron-forge/maker-dmg',
|
name: "@electron-forge/maker-dmg",
|
||||||
platforms: ['darwin'],
|
platforms: ["darwin"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '@reforged/maker-appimage',
|
name: "@reforged/maker-appimage",
|
||||||
platforms: ['linux'],
|
platforms: ["linux"],
|
||||||
config: {
|
config: {
|
||||||
options: {
|
options: {
|
||||||
name: 'Cheating Daddy',
|
name: "Mastermind",
|
||||||
productName: 'Cheating Daddy',
|
productName: "Mastermind",
|
||||||
genericName: 'AI Assistant',
|
genericName: "AI Assistant",
|
||||||
description: 'AI assistant for interviews and learning',
|
description: "AI assistant for interviews and learning",
|
||||||
categories: ['Development', 'Education'],
|
categories: ["Development", "Education"],
|
||||||
icon: 'src/assets/logo.png'
|
icon: "src/assets/logo.png",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
{
|
{
|
||||||
name: '@electron-forge/plugin-auto-unpack-natives',
|
name: "@electron-forge/plugin-auto-unpack-natives",
|
||||||
config: {},
|
config: {},
|
||||||
},
|
},
|
||||||
// Fuses are used to enable/disable various Electron functionality
|
// Fuses are used to enable/disable various Electron functionality
|
||||||
|
|||||||
+14
-11
@@ -1,26 +1,27 @@
|
|||||||
{
|
{
|
||||||
"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": [
|
"keywords": [
|
||||||
"cheating daddy",
|
"mastermind",
|
||||||
"cheating daddy ai",
|
"mastermind ai",
|
||||||
"cheating daddy ai assistant",
|
"mastermind ai assistant",
|
||||||
"cheating daddy ai assistant for interviews",
|
"mastermind ai assistant for interviews",
|
||||||
"cheating daddy ai assistant for interviews"
|
"mastermind ai assistant for interviews"
|
||||||
],
|
],
|
||||||
"author": {
|
"author": {
|
||||||
"name": "sohzm",
|
"name": "ShiftyX1",
|
||||||
"email": "sohambharambe9@gmail.com"
|
"email": "lead@pyserve.org"
|
||||||
},
|
},
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -28,10 +29,12 @@
|
|||||||
"@huggingface/transformers": "^3.8.1",
|
"@huggingface/transformers": "^3.8.1",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"ollama": "^0.6.3",
|
"ollama": "^0.6.3",
|
||||||
|
"openai": "^6.22.0",
|
||||||
"p-retry": "^4.6.2",
|
"p-retry": "^4.6.2",
|
||||||
"ws": "^8.19.0"
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@electron/rebuild": "^3.7.1",
|
||||||
"@electron-forge/cli": "^7.8.1",
|
"@electron-forge/cli": "^7.8.1",
|
||||||
"@electron-forge/maker-deb": "^7.8.1",
|
"@electron-forge/maker-deb": "^7.8.1",
|
||||||
"@electron-forge/maker-dmg": "^7.8.1",
|
"@electron-forge/maker-dmg": "^7.8.1",
|
||||||
|
|||||||
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() {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
|
import { html, css, LitElement } from "../../assets/lit-core-2.7.4.min.js";
|
||||||
import { MainView } from '../views/MainView.js';
|
import { MainView } from "../views/MainView.js";
|
||||||
import { CustomizeView } from '../views/CustomizeView.js';
|
import { CustomizeView } from "../views/CustomizeView.js";
|
||||||
import { HelpView } from '../views/HelpView.js';
|
import { HelpView } from "../views/HelpView.js";
|
||||||
import { HistoryView } from '../views/HistoryView.js';
|
import { HistoryView } from "../views/HistoryView.js";
|
||||||
import { AssistantView } from '../views/AssistantView.js';
|
import { AssistantView } from "../views/AssistantView.js";
|
||||||
import { OnboardingView } from '../views/OnboardingView.js';
|
import { OnboardingView } from "../views/OnboardingView.js";
|
||||||
import { AICustomizeView } from '../views/AICustomizeView.js';
|
import { AICustomizeView } from "../views/AICustomizeView.js";
|
||||||
import { FeedbackView } from '../views/FeedbackView.js';
|
import { FeedbackView } from "../views/FeedbackView.js";
|
||||||
|
|
||||||
export class CheatingDaddyApp extends LitElement {
|
export class CheatingDaddyApp extends LitElement {
|
||||||
static styles = css`
|
static styles = css`
|
||||||
@@ -81,15 +81,15 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.traffic-light.close {
|
.traffic-light.close {
|
||||||
background: #FF5F57;
|
background: #ff5f57;
|
||||||
}
|
}
|
||||||
|
|
||||||
.traffic-light.minimize {
|
.traffic-light.minimize {
|
||||||
background: #FEBC2E;
|
background: #febc2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.traffic-light.maximize {
|
.traffic-light.maximize {
|
||||||
background: #28C840;
|
background: #28c840;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
@@ -100,7 +100,10 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 42px 0 var(--space-md) 0;
|
padding: 42px 0 var(--space-md) 0;
|
||||||
transition: width var(--transition), min-width var(--transition), opacity var(--transition);
|
transition:
|
||||||
|
width var(--transition),
|
||||||
|
min-width var(--transition),
|
||||||
|
opacity var(--transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.hidden {
|
.sidebar.hidden {
|
||||||
@@ -144,7 +147,9 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color var(--transition), background var(--transition);
|
transition:
|
||||||
|
color var(--transition),
|
||||||
|
background var(--transition);
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -187,7 +192,9 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
transition: background var(--transition), border-color var(--transition);
|
transition:
|
||||||
|
background var(--transition),
|
||||||
|
border-color var(--transition);
|
||||||
animation: update-wobble 5s ease-in-out infinite;
|
animation: update-wobble 5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,11 +204,23 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes update-wobble {
|
@keyframes update-wobble {
|
||||||
0%, 90%, 100% { transform: rotate(0deg); }
|
0%,
|
||||||
92% { transform: rotate(-2deg); }
|
90%,
|
||||||
94% { transform: rotate(2deg); }
|
100% {
|
||||||
96% { transform: rotate(-1.5deg); }
|
transform: rotate(0deg);
|
||||||
98% { transform: rotate(1.5deg); }
|
}
|
||||||
|
92% {
|
||||||
|
transform: rotate(-2deg);
|
||||||
|
}
|
||||||
|
94% {
|
||||||
|
transform: rotate(2deg);
|
||||||
|
}
|
||||||
|
96% {
|
||||||
|
transform: rotate(-1.5deg);
|
||||||
|
}
|
||||||
|
98% {
|
||||||
|
transform: rotate(1.5deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-btn svg {
|
.update-btn svg {
|
||||||
@@ -363,20 +382,21 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
_storageLoaded: { state: true },
|
_storageLoaded: { state: true },
|
||||||
_updateAvailable: { state: true },
|
_updateAvailable: { state: true },
|
||||||
_whisperDownloading: { state: true },
|
_whisperDownloading: { state: true },
|
||||||
|
_whisperProgress: { state: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.currentView = 'main';
|
this.currentView = "main";
|
||||||
this.statusText = '';
|
this.statusText = "";
|
||||||
this.startTime = null;
|
this.startTime = null;
|
||||||
this.isRecording = false;
|
this.isRecording = false;
|
||||||
this.sessionActive = false;
|
this.sessionActive = false;
|
||||||
this.selectedProfile = 'interview';
|
this.selectedProfile = "interview";
|
||||||
this.selectedLanguage = 'en-US';
|
this.selectedLanguage = "en-US";
|
||||||
this.selectedScreenshotInterval = '5';
|
this.selectedScreenshotInterval = "5";
|
||||||
this.selectedImageQuality = 'medium';
|
this.selectedImageQuality = "medium";
|
||||||
this.layoutMode = 'normal';
|
this.layoutMode = "normal";
|
||||||
this.responses = [];
|
this.responses = [];
|
||||||
this.currentResponseIndex = -1;
|
this.currentResponseIndex = -1;
|
||||||
this._viewInstances = new Map();
|
this._viewInstances = new Map();
|
||||||
@@ -388,7 +408,8 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
this._timerInterval = null;
|
this._timerInterval = null;
|
||||||
this._updateAvailable = false;
|
this._updateAvailable = false;
|
||||||
this._whisperDownloading = false;
|
this._whisperDownloading = false;
|
||||||
this._localVersion = '';
|
this._whisperProgress = null;
|
||||||
|
this._localVersion = "";
|
||||||
|
|
||||||
this._loadFromStorage();
|
this._loadFromStorage();
|
||||||
this._checkForUpdates();
|
this._checkForUpdates();
|
||||||
@@ -399,16 +420,22 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
this._localVersion = await cheatingDaddy.getVersion();
|
this._localVersion = await cheatingDaddy.getVersion();
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
|
|
||||||
const res = await fetch('https://raw.githubusercontent.com/sohzm/cheating-daddy/refs/heads/master/package.json');
|
const res = await fetch(
|
||||||
|
"https://raw.githubusercontent.com/ShiftyX1/Mastermind/refs/heads/master/package.json",
|
||||||
|
);
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const remote = await res.json();
|
const remote = await res.json();
|
||||||
const remoteVersion = remote.version;
|
const remoteVersion = remote.version;
|
||||||
|
|
||||||
const toNum = v => v.split('.').map(Number);
|
const toNum = (v) => v.split(".").map(Number);
|
||||||
const [rMaj, rMin, rPatch] = toNum(remoteVersion);
|
const [rMaj, rMin, rPatch] = toNum(remoteVersion);
|
||||||
const [lMaj, lMin, lPatch] = toNum(this._localVersion);
|
const [lMaj, lMin, lPatch] = toNum(this._localVersion);
|
||||||
|
|
||||||
if (rMaj > lMaj || (rMaj === lMaj && rMin > lMin) || (rMaj === lMaj && rMin === lMin && rPatch > lPatch)) {
|
if (
|
||||||
|
rMaj > lMaj ||
|
||||||
|
(rMaj === lMaj && rMin > lMin) ||
|
||||||
|
(rMaj === lMaj && rMin === lMin && rPatch > lPatch)
|
||||||
|
) {
|
||||||
this._updateAvailable = true;
|
this._updateAvailable = true;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
@@ -421,20 +448,20 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
try {
|
try {
|
||||||
const [config, prefs] = await Promise.all([
|
const [config, prefs] = await Promise.all([
|
||||||
cheatingDaddy.storage.getConfig(),
|
cheatingDaddy.storage.getConfig(),
|
||||||
cheatingDaddy.storage.getPreferences()
|
cheatingDaddy.storage.getPreferences(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.currentView = config.onboarded ? 'main' : 'onboarding';
|
this.currentView = config.onboarded ? "main" : "onboarding";
|
||||||
this.selectedProfile = prefs.selectedProfile || 'interview';
|
this.selectedProfile = prefs.selectedProfile || "interview";
|
||||||
this.selectedLanguage = prefs.selectedLanguage || 'en-US';
|
this.selectedLanguage = prefs.selectedLanguage || "en-US";
|
||||||
this.selectedScreenshotInterval = prefs.selectedScreenshotInterval || '5';
|
this.selectedScreenshotInterval = prefs.selectedScreenshotInterval || "5";
|
||||||
this.selectedImageQuality = prefs.selectedImageQuality || 'medium';
|
this.selectedImageQuality = prefs.selectedImageQuality || "medium";
|
||||||
this.layoutMode = config.layout || 'normal';
|
this.layoutMode = config.layout || "normal";
|
||||||
|
|
||||||
this._storageLoaded = true;
|
this._storageLoaded = true;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading from storage:', error);
|
console.error("Error loading from storage:", error);
|
||||||
this._storageLoaded = true;
|
this._storageLoaded = true;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
@@ -444,13 +471,27 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
ipcRenderer.on('new-response', (_, response) => this.addNewResponse(response));
|
ipcRenderer.on("new-response", (_, response) =>
|
||||||
ipcRenderer.on('update-response', (_, response) => this.updateCurrentResponse(response));
|
this.addNewResponse(response),
|
||||||
ipcRenderer.on('update-status', (_, status) => this.setStatus(status));
|
);
|
||||||
ipcRenderer.on('click-through-toggled', (_, isEnabled) => { this._isClickThrough = isEnabled; });
|
ipcRenderer.on("update-response", (_, response) =>
|
||||||
ipcRenderer.on('reconnect-failed', (_, data) => this.addNewResponse(data.message));
|
this.updateCurrentResponse(response),
|
||||||
ipcRenderer.on('whisper-downloading', (_, downloading) => { this._whisperDownloading = downloading; });
|
);
|
||||||
|
ipcRenderer.on("update-status", (_, status) => this.setStatus(status));
|
||||||
|
ipcRenderer.on("click-through-toggled", (_, isEnabled) => {
|
||||||
|
this._isClickThrough = isEnabled;
|
||||||
|
});
|
||||||
|
ipcRenderer.on("reconnect-failed", (_, data) =>
|
||||||
|
this.addNewResponse(data.message),
|
||||||
|
);
|
||||||
|
ipcRenderer.on("whisper-downloading", (_, downloading) => {
|
||||||
|
this._whisperDownloading = downloading;
|
||||||
|
if (!downloading) this._whisperProgress = null;
|
||||||
|
});
|
||||||
|
ipcRenderer.on("whisper-progress", (_, progress) => {
|
||||||
|
this._whisperProgress = progress;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,13 +499,14 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this._stopTimer();
|
this._stopTimer();
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
ipcRenderer.removeAllListeners('new-response');
|
ipcRenderer.removeAllListeners("new-response");
|
||||||
ipcRenderer.removeAllListeners('update-response');
|
ipcRenderer.removeAllListeners("update-response");
|
||||||
ipcRenderer.removeAllListeners('update-status');
|
ipcRenderer.removeAllListeners("update-status");
|
||||||
ipcRenderer.removeAllListeners('click-through-toggled');
|
ipcRenderer.removeAllListeners("click-through-toggled");
|
||||||
ipcRenderer.removeAllListeners('reconnect-failed');
|
ipcRenderer.removeAllListeners("reconnect-failed");
|
||||||
ipcRenderer.removeAllListeners('whisper-downloading');
|
ipcRenderer.removeAllListeners("whisper-downloading");
|
||||||
|
ipcRenderer.removeAllListeners("whisper-progress");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,12 +527,12 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getElapsedTime() {
|
getElapsedTime() {
|
||||||
if (!this.startTime) return '0:00';
|
if (!this.startTime) return "0:00";
|
||||||
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
||||||
const h = Math.floor(elapsed / 3600);
|
const h = Math.floor(elapsed / 3600);
|
||||||
const m = Math.floor((elapsed % 3600) / 60);
|
const m = Math.floor((elapsed % 3600) / 60);
|
||||||
const s = elapsed % 60;
|
const s = elapsed % 60;
|
||||||
const pad = n => String(n).padStart(2, '0');
|
const pad = (n) => String(n).padStart(2, "0");
|
||||||
if (h > 0) return `${h}:${pad(m)}:${pad(s)}`;
|
if (h > 0) return `${h}:${pad(m)}:${pad(s)}`;
|
||||||
return `${m}:${pad(s)}`;
|
return `${m}:${pad(s)}`;
|
||||||
}
|
}
|
||||||
@@ -499,7 +541,11 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
|
|
||||||
setStatus(text) {
|
setStatus(text) {
|
||||||
this.statusText = text;
|
this.statusText = text;
|
||||||
if (text.includes('Ready') || text.includes('Listening') || text.includes('Error')) {
|
if (
|
||||||
|
text.includes("Ready") ||
|
||||||
|
text.includes("Listening") ||
|
||||||
|
text.includes("Error")
|
||||||
|
) {
|
||||||
this._currentResponseIsComplete = true;
|
this._currentResponseIsComplete = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -531,34 +577,34 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleClose() {
|
async handleClose() {
|
||||||
if (this.currentView === 'assistant') {
|
if (this.currentView === "assistant") {
|
||||||
cheatingDaddy.stopCapture();
|
cheatingDaddy.stopCapture();
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('close-session');
|
await ipcRenderer.invoke("close-session");
|
||||||
}
|
}
|
||||||
this.sessionActive = false;
|
this.sessionActive = false;
|
||||||
this._stopTimer();
|
this._stopTimer();
|
||||||
this.currentView = 'main';
|
this.currentView = "main";
|
||||||
} else {
|
} else {
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('quit-application');
|
await ipcRenderer.invoke("quit-application");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _handleMinimize() {
|
async _handleMinimize() {
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('window-minimize');
|
await ipcRenderer.invoke("window-minimize");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleHideToggle() {
|
async handleHideToggle() {
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('toggle-window-visibility');
|
await ipcRenderer.invoke("toggle-window-visibility");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,12 +612,12 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
|
|
||||||
async handleStart() {
|
async handleStart() {
|
||||||
const prefs = await cheatingDaddy.storage.getPreferences();
|
const prefs = await cheatingDaddy.storage.getPreferences();
|
||||||
const providerMode = prefs.providerMode || 'byok';
|
const providerMode = prefs.providerMode || "byok";
|
||||||
|
|
||||||
if (providerMode === 'local') {
|
if (providerMode === "local") {
|
||||||
const success = await cheatingDaddy.initializeLocal(this.selectedProfile);
|
const success = await cheatingDaddy.initializeLocal(this.selectedProfile);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
const mainView = this.shadowRoot.querySelector('main-view');
|
const mainView = this.shadowRoot.querySelector("main-view");
|
||||||
if (mainView && mainView.triggerApiKeyError) {
|
if (mainView && mainView.triggerApiKeyError) {
|
||||||
mainView.triggerApiKeyError();
|
mainView.triggerApiKeyError();
|
||||||
}
|
}
|
||||||
@@ -579,37 +625,49 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const apiKey = await cheatingDaddy.storage.getApiKey();
|
const apiKey = await cheatingDaddy.storage.getApiKey();
|
||||||
if (!apiKey || apiKey === '') {
|
if (!apiKey || apiKey === "") {
|
||||||
const mainView = this.shadowRoot.querySelector('main-view');
|
const mainView = this.shadowRoot.querySelector("main-view");
|
||||||
if (mainView && mainView.triggerApiKeyError) {
|
if (mainView && mainView.triggerApiKeyError) {
|
||||||
mainView.triggerApiKeyError();
|
mainView.triggerApiKeyError();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await cheatingDaddy.initializeGemini(this.selectedProfile, this.selectedLanguage);
|
await cheatingDaddy.initializeGemini(
|
||||||
|
this.selectedProfile,
|
||||||
|
this.selectedLanguage,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
cheatingDaddy.startCapture(this.selectedScreenshotInterval, this.selectedImageQuality);
|
cheatingDaddy.startCapture(
|
||||||
|
this.selectedScreenshotInterval,
|
||||||
|
this.selectedImageQuality,
|
||||||
|
);
|
||||||
this.responses = [];
|
this.responses = [];
|
||||||
this.currentResponseIndex = -1;
|
this.currentResponseIndex = -1;
|
||||||
this.startTime = Date.now();
|
this.startTime = Date.now();
|
||||||
this.sessionActive = true;
|
this.sessionActive = true;
|
||||||
this.currentView = 'assistant';
|
this.currentView = "assistant";
|
||||||
this._startTimer();
|
this._startTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleAPIKeyHelp() {
|
async handleAPIKeyHelp() {
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('open-external', 'https://cheatingdaddy.com/help/api-key');
|
await ipcRenderer.invoke(
|
||||||
|
"open-external",
|
||||||
|
"https://cheatingdaddy.com/help/api-key",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleGroqAPIKeyHelp() {
|
async handleGroqAPIKeyHelp() {
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('open-external', 'https://console.groq.com/keys');
|
await ipcRenderer.invoke(
|
||||||
|
"open-external",
|
||||||
|
"https://console.groq.com/keys",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,33 +675,39 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
|
|
||||||
async handleProfileChange(profile) {
|
async handleProfileChange(profile) {
|
||||||
this.selectedProfile = profile;
|
this.selectedProfile = profile;
|
||||||
await cheatingDaddy.storage.updatePreference('selectedProfile', profile);
|
await cheatingDaddy.storage.updatePreference("selectedProfile", profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleLanguageChange(language) {
|
async handleLanguageChange(language) {
|
||||||
this.selectedLanguage = language;
|
this.selectedLanguage = language;
|
||||||
await cheatingDaddy.storage.updatePreference('selectedLanguage', language);
|
await cheatingDaddy.storage.updatePreference("selectedLanguage", language);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleScreenshotIntervalChange(interval) {
|
async handleScreenshotIntervalChange(interval) {
|
||||||
this.selectedScreenshotInterval = interval;
|
this.selectedScreenshotInterval = interval;
|
||||||
await cheatingDaddy.storage.updatePreference('selectedScreenshotInterval', interval);
|
await cheatingDaddy.storage.updatePreference(
|
||||||
|
"selectedScreenshotInterval",
|
||||||
|
interval,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleImageQualityChange(quality) {
|
async handleImageQualityChange(quality) {
|
||||||
this.selectedImageQuality = quality;
|
this.selectedImageQuality = quality;
|
||||||
await cheatingDaddy.storage.updatePreference('selectedImageQuality', quality);
|
await cheatingDaddy.storage.updatePreference(
|
||||||
|
"selectedImageQuality",
|
||||||
|
quality,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleLayoutModeChange(layoutMode) {
|
async handleLayoutModeChange(layoutMode) {
|
||||||
this.layoutMode = layoutMode;
|
this.layoutMode = layoutMode;
|
||||||
await cheatingDaddy.storage.updateConfig('layout', layoutMode);
|
await cheatingDaddy.storage.updateConfig("layout", layoutMode);
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
try {
|
try {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('update-sizes');
|
await ipcRenderer.invoke("update-sizes");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update sizes:', error);
|
console.error("Failed to update sizes:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
@@ -651,17 +715,27 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
|
|
||||||
async handleExternalLinkClick(url) {
|
async handleExternalLinkClick(url) {
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('open-external', url);
|
await ipcRenderer.invoke("open-external", url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSendText(message) {
|
async handleSendText(message) {
|
||||||
const result = await window.cheatingDaddy.sendTextMessage(message);
|
const result = await window.cheatingDaddy.sendTextMessage(message);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
this.setStatus('Error sending message: ' + result.error);
|
this.setStatus("Error sending message: " + result.error);
|
||||||
} else {
|
} else {
|
||||||
this.setStatus('Message sent...');
|
this.setStatus("Message sent...");
|
||||||
|
this._awaitingNewResponse = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleExpandResponse() {
|
||||||
|
const result = await window.cheatingDaddy.expandLastResponse();
|
||||||
|
if (!result.success) {
|
||||||
|
this.setStatus("Error expanding: " + (result.error || "Unknown error"));
|
||||||
|
} else {
|
||||||
|
this.setStatus("Expanding response...");
|
||||||
this._awaitingNewResponse = true;
|
this._awaitingNewResponse = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -673,29 +747,29 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleOnboardingComplete() {
|
handleOnboardingComplete() {
|
||||||
this.currentView = 'main';
|
this.currentView = "main";
|
||||||
}
|
}
|
||||||
|
|
||||||
updated(changedProperties) {
|
updated(changedProperties) {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
if (changedProperties.has('currentView') && window.require) {
|
if (changedProperties.has("currentView") && window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
ipcRenderer.send('view-changed', this.currentView);
|
ipcRenderer.send("view-changed", this.currentView);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ──
|
// ── Helpers ──
|
||||||
|
|
||||||
_isLiveMode() {
|
_isLiveMode() {
|
||||||
return this.currentView === 'assistant';
|
return this.currentView === "assistant";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Render ──
|
// ── Render ──
|
||||||
|
|
||||||
renderCurrentView() {
|
renderCurrentView() {
|
||||||
switch (this.currentView) {
|
switch (this.currentView) {
|
||||||
case 'onboarding':
|
case "onboarding":
|
||||||
return html`
|
return html`
|
||||||
<onboarding-view
|
<onboarding-view
|
||||||
.onComplete=${() => this.handleOnboardingComplete()}
|
.onComplete=${() => this.handleOnboardingComplete()}
|
||||||
@@ -703,26 +777,27 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
></onboarding-view>
|
></onboarding-view>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
case 'main':
|
case "main":
|
||||||
return html`
|
return html`
|
||||||
<main-view
|
<main-view
|
||||||
.selectedProfile=${this.selectedProfile}
|
.selectedProfile=${this.selectedProfile}
|
||||||
.onProfileChange=${p => this.handleProfileChange(p)}
|
.onProfileChange=${(p) => this.handleProfileChange(p)}
|
||||||
.onStart=${() => this.handleStart()}
|
.onStart=${() => this.handleStart()}
|
||||||
.onExternalLink=${url => this.handleExternalLinkClick(url)}
|
.onExternalLink=${(url) => this.handleExternalLinkClick(url)}
|
||||||
.whisperDownloading=${this._whisperDownloading}
|
.whisperDownloading=${this._whisperDownloading}
|
||||||
|
.whisperProgress=${this._whisperProgress}
|
||||||
></main-view>
|
></main-view>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
case 'ai-customize':
|
case "ai-customize":
|
||||||
return html`
|
return html`
|
||||||
<ai-customize-view
|
<ai-customize-view
|
||||||
.selectedProfile=${this.selectedProfile}
|
.selectedProfile=${this.selectedProfile}
|
||||||
.onProfileChange=${p => this.handleProfileChange(p)}
|
.onProfileChange=${(p) => this.handleProfileChange(p)}
|
||||||
></ai-customize-view>
|
></ai-customize-view>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
case 'customize':
|
case "customize":
|
||||||
return html`
|
return html`
|
||||||
<customize-view
|
<customize-view
|
||||||
.selectedProfile=${this.selectedProfile}
|
.selectedProfile=${this.selectedProfile}
|
||||||
@@ -730,30 +805,34 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
.selectedScreenshotInterval=${this.selectedScreenshotInterval}
|
.selectedScreenshotInterval=${this.selectedScreenshotInterval}
|
||||||
.selectedImageQuality=${this.selectedImageQuality}
|
.selectedImageQuality=${this.selectedImageQuality}
|
||||||
.layoutMode=${this.layoutMode}
|
.layoutMode=${this.layoutMode}
|
||||||
.onProfileChange=${p => this.handleProfileChange(p)}
|
.onProfileChange=${(p) => this.handleProfileChange(p)}
|
||||||
.onLanguageChange=${l => this.handleLanguageChange(l)}
|
.onLanguageChange=${(l) => this.handleLanguageChange(l)}
|
||||||
.onScreenshotIntervalChange=${i => this.handleScreenshotIntervalChange(i)}
|
.onScreenshotIntervalChange=${(i) =>
|
||||||
.onImageQualityChange=${q => this.handleImageQualityChange(q)}
|
this.handleScreenshotIntervalChange(i)}
|
||||||
.onLayoutModeChange=${lm => this.handleLayoutModeChange(lm)}
|
.onImageQualityChange=${(q) => this.handleImageQualityChange(q)}
|
||||||
|
.onLayoutModeChange=${(lm) => this.handleLayoutModeChange(lm)}
|
||||||
></customize-view>
|
></customize-view>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
case 'feedback':
|
case "feedback":
|
||||||
return html`<feedback-view></feedback-view>`;
|
return html`<feedback-view></feedback-view>`;
|
||||||
|
|
||||||
case 'help':
|
case "help":
|
||||||
return html`<help-view .onExternalLinkClick=${url => this.handleExternalLinkClick(url)}></help-view>`;
|
return html`<help-view
|
||||||
|
.onExternalLinkClick=${(url) => this.handleExternalLinkClick(url)}
|
||||||
|
></help-view>`;
|
||||||
|
|
||||||
case 'history':
|
case "history":
|
||||||
return html`<history-view></history-view>`;
|
return html`<history-view></history-view>`;
|
||||||
|
|
||||||
case 'assistant':
|
case "assistant":
|
||||||
return html`
|
return html`
|
||||||
<assistant-view
|
<assistant-view
|
||||||
.responses=${this.responses}
|
.responses=${this.responses}
|
||||||
.currentResponseIndex=${this.currentResponseIndex}
|
.currentResponseIndex=${this.currentResponseIndex}
|
||||||
.selectedProfile=${this.selectedProfile}
|
.selectedProfile=${this.selectedProfile}
|
||||||
.onSendText=${msg => this.handleSendText(msg)}
|
.onSendText=${(msg) => this.handleSendText(msg)}
|
||||||
|
.onExpandResponse=${() => this.handleExpandResponse()}
|
||||||
.shouldAnimateResponse=${this.shouldAnimateResponse}
|
.shouldAnimateResponse=${this.shouldAnimateResponse}
|
||||||
@response-index-changed=${this.handleResponseIndexChanged}
|
@response-index-changed=${this.handleResponseIndexChanged}
|
||||||
@response-animation-complete=${() => {
|
@response-animation-complete=${() => {
|
||||||
@@ -771,74 +850,238 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
|
|
||||||
renderSidebar() {
|
renderSidebar() {
|
||||||
const items = [
|
const items = [
|
||||||
{ id: 'main', label: 'Home', icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="m19 8.71l-5.333-4.148a2.666 2.666 0 0 0-3.274 0L5.059 8.71a2.67 2.67 0 0 0-1.029 2.105v7.2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.2c0-.823-.38-1.6-1.03-2.105"/><path d="M16 15c-2.21 1.333-5.792 1.333-8 0"/></g></svg>` },
|
{
|
||||||
{ id: 'ai-customize', label: 'AI Customization', icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 3v7h6l-8 11v-7H5z" /></svg>` },
|
id: "main",
|
||||||
{ id: 'history', label: 'History', icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M10 20.777a9 9 0 0 1-2.48-.969M14 3.223a9.003 9.003 0 0 1 0 17.554m-9.421-3.684a9 9 0 0 1-1.227-2.592M3.124 10.5c.16-.95.468-1.85.9-2.675l.169-.305m2.714-2.941A9 9 0 0 1 10 3.223"/><path d="M12 8v4l3 3"/></g></svg>` },
|
label: "Home",
|
||||||
{ id: 'customize', label: 'Settings', icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M19.875 6.27A2.23 2.23 0 0 1 21 8.218v7.284c0 .809-.443 1.555-1.158 1.948l-6.75 4.27a2.27 2.27 0 0 1-2.184 0l-6.75-4.27A2.23 2.23 0 0 1 3 15.502V8.217c0-.809.443-1.554 1.158-1.947l6.75-3.98a2.33 2.33 0 0 1 2.25 0l6.75 3.98z"/><path d="M9 12a3 3 0 1 0 6 0a3 3 0 1 0-6 0"/></g></svg>` },
|
icon: html`<svg
|
||||||
{ id: 'feedback', label: 'Feedback', icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-5l-5 3v-3H6a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3zM9.5 9h.01m4.99 0h.01"/><path d="M9.5 13a3.5 3.5 0 0 0 5 0"/></g></svg>` },
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{ id: 'help', label: 'Help', icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M12 3c7.2 0 9 1.8 9 9s-1.8 9-9 9s-9-1.8-9-9s1.8-9 9-9m0 13v.01"/><path d="M12 13a2 2 0 0 0 .914-3.782a1.98 1.98 0 0 0-2.414.483"/></g></svg>` },
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m19 8.71l-5.333-4.148a2.666 2.666 0 0 0-3.274 0L5.059 8.71a2.67 2.67 0 0 0-1.029 2.105v7.2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.2c0-.823-.38-1.6-1.03-2.105"
|
||||||
|
/>
|
||||||
|
<path d="M16 15c-2.21 1.333-5.792 1.333-8 0" />
|
||||||
|
</g>
|
||||||
|
</svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ai-customize",
|
||||||
|
label: "AI Customization",
|
||||||
|
icon: html`<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 3v7h6l-8 11v-7H5z"
|
||||||
|
/>
|
||||||
|
</svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "history",
|
||||||
|
label: "History",
|
||||||
|
icon: html`<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10 20.777a9 9 0 0 1-2.48-.969M14 3.223a9.003 9.003 0 0 1 0 17.554m-9.421-3.684a9 9 0 0 1-1.227-2.592M3.124 10.5c.16-.95.468-1.85.9-2.675l.169-.305m2.714-2.941A9 9 0 0 1 10 3.223"
|
||||||
|
/>
|
||||||
|
<path d="M12 8v4l3 3" />
|
||||||
|
</g>
|
||||||
|
</svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "customize",
|
||||||
|
label: "Settings",
|
||||||
|
icon: html`<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M19.875 6.27A2.23 2.23 0 0 1 21 8.218v7.284c0 .809-.443 1.555-1.158 1.948l-6.75 4.27a2.27 2.27 0 0 1-2.184 0l-6.75-4.27A2.23 2.23 0 0 1 3 15.502V8.217c0-.809.443-1.554 1.158-1.947l6.75-3.98a2.33 2.33 0 0 1 2.25 0l6.75 3.98z"
|
||||||
|
/>
|
||||||
|
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 1 0-6 0" />
|
||||||
|
</g>
|
||||||
|
</svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "feedback",
|
||||||
|
label: "Feedback",
|
||||||
|
icon: html`<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-5l-5 3v-3H6a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3zM9.5 9h.01m4.99 0h.01"
|
||||||
|
/>
|
||||||
|
<path d="M9.5 13a3.5 3.5 0 0 0 5 0" />
|
||||||
|
</g>
|
||||||
|
</svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "help",
|
||||||
|
label: "Help",
|
||||||
|
icon: html`<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 3c7.2 0 9 1.8 9 9s-1.8 9-9 9s-9-1.8-9-9s1.8-9 9-9m0 13v.01"
|
||||||
|
/>
|
||||||
|
<path d="M12 13a2 2 0 0 0 .914-3.782a1.98 1.98 0 0 0-2.414.483" />
|
||||||
|
</g>
|
||||||
|
</svg>`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="sidebar ${this._isLiveMode() ? 'hidden' : ''}">
|
<div class="sidebar ${this._isLiveMode() ? "hidden" : ""}">
|
||||||
<div class="sidebar-brand">
|
<div class="sidebar-brand">
|
||||||
<h1>Cheating Daddy</h1>
|
<h1>Mastermind</h1>
|
||||||
</div>
|
</div>
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
${items.map(item => html`
|
${items.map(
|
||||||
|
(item) => html`
|
||||||
<button
|
<button
|
||||||
class="nav-item ${this.currentView === item.id ? 'active' : ''}"
|
class="nav-item ${this.currentView === item.id ? "active" : ""}"
|
||||||
@click=${() => this.navigate(item.id)}
|
@click=${() => this.navigate(item.id)}
|
||||||
title=${item.label}
|
title=${item.label}
|
||||||
>
|
>
|
||||||
${item.icon}
|
${item.icon} ${item.label}
|
||||||
${item.label}
|
|
||||||
</button>
|
</button>
|
||||||
`)}
|
`,
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
${this._updateAvailable ? html`
|
${this._updateAvailable
|
||||||
<button class="update-btn" @click=${() => this.handleExternalLinkClick('https://cheatingdaddy.com/download')}>
|
? html`
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2M7 11l5 5l5-5m-5-7v12" /></svg>
|
<button
|
||||||
|
class="update-btn"
|
||||||
|
@click=${() =>
|
||||||
|
this.handleExternalLinkClick(
|
||||||
|
"https://cheatingdaddy.com/download",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2M7 11l5 5l5-5m-5-7v12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
Update available
|
Update available
|
||||||
</button>
|
</button>
|
||||||
` : html`
|
`
|
||||||
<div class="version-text">v${this._localVersion}</div>
|
: html` <div class="version-text">v${this._localVersion}</div> `}
|
||||||
`}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLiveBar() {
|
renderLiveBar() {
|
||||||
if (!this._isLiveMode()) return '';
|
if (!this._isLiveMode()) return "";
|
||||||
|
|
||||||
const profileLabels = {
|
const profileLabels = {
|
||||||
interview: 'Interview',
|
interview: "Interview",
|
||||||
sales: 'Sales Call',
|
sales: "Sales Call",
|
||||||
meeting: 'Meeting',
|
meeting: "Meeting",
|
||||||
presentation: 'Presentation',
|
presentation: "Presentation",
|
||||||
negotiation: 'Negotiation',
|
negotiation: "Negotiation",
|
||||||
exam: 'Exam',
|
exam: "Exam",
|
||||||
};
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="live-bar">
|
<div class="live-bar">
|
||||||
<div class="live-bar-left">
|
<div class="live-bar-left">
|
||||||
<button class="live-bar-back" @click=${() => this.handleClose()} title="End session">
|
<button
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
class="live-bar-back"
|
||||||
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 0 1-.02 1.06L8.832 10l3.938 3.71a.75.75 0 1 1-1.04 1.08l-4.5-4.25a.75.75 0 0 1 0-1.08l4.5-4.25a.75.75 0 0 1 1.06.02Z" clip-rule="evenodd" />
|
@click=${() => this.handleClose()}
|
||||||
|
title="End session"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M12.79 5.23a.75.75 0 0 1-.02 1.06L8.832 10l3.938 3.71a.75.75 0 1 1-1.04 1.08l-4.5-4.25a.75.75 0 0 1 0-1.08l4.5-4.25a.75.75 0 0 1 1.06.02Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="live-bar-center">
|
<div class="live-bar-center">
|
||||||
${profileLabels[this.selectedProfile] || 'Session'}
|
${profileLabels[this.selectedProfile] || "Session"}
|
||||||
</div>
|
</div>
|
||||||
<div class="live-bar-right">
|
<div class="live-bar-right">
|
||||||
${this.statusText ? html`<span class="live-bar-text">${this.statusText}</span>` : ''}
|
${this.statusText
|
||||||
|
? html`<span class="live-bar-text">${this.statusText}</span>`
|
||||||
|
: ""}
|
||||||
<span class="live-bar-text">${this.getElapsedTime()}</span>
|
<span class="live-bar-text">${this.getElapsedTime()}</span>
|
||||||
${this._isClickThrough ? html`<span class="live-bar-text">[click through]</span>` : ''}
|
${this._isClickThrough
|
||||||
<span class="live-bar-text clickable" @click=${() => this.handleHideToggle()}>[hide]</span>
|
? html`<span class="live-bar-text">[click through]</span>`
|
||||||
|
: ""}
|
||||||
|
<span
|
||||||
|
class="live-bar-text clickable"
|
||||||
|
@click=${() => this.handleHideToggle()}
|
||||||
|
>[hide]</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -846,30 +1089,34 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
// Onboarding is fullscreen, no sidebar
|
// Onboarding is fullscreen, no sidebar
|
||||||
if (this.currentView === 'onboarding') {
|
if (this.currentView === "onboarding") {
|
||||||
return html`
|
return html` <div class="fullscreen">${this.renderCurrentView()}</div> `;
|
||||||
<div class="fullscreen">
|
|
||||||
${this.renderCurrentView()}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLive = this._isLiveMode();
|
const isLive = this._isLiveMode();
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
<div class="top-drag-bar ${isLive ? 'hidden' : ''}">
|
<div class="top-drag-bar ${isLive ? "hidden" : ""}">
|
||||||
<div class="traffic-lights">
|
<div class="traffic-lights">
|
||||||
<button class="traffic-light close" @click=${() => this.handleClose()} title="Close"></button>
|
<button
|
||||||
<button class="traffic-light minimize" @click=${() => this._handleMinimize()} title="Minimize"></button>
|
class="traffic-light close"
|
||||||
|
@click=${() => this.handleClose()}
|
||||||
|
title="Close"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
class="traffic-light minimize"
|
||||||
|
@click=${() => this._handleMinimize()}
|
||||||
|
title="Minimize"
|
||||||
|
></button>
|
||||||
<button class="traffic-light maximize" title="Maximize"></button>
|
<button class="traffic-light maximize" title="Maximize"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="drag-region"></div>
|
<div class="drag-region"></div>
|
||||||
</div>
|
</div>
|
||||||
${this.renderSidebar()}
|
${this.renderSidebar()}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
${isLive ? this.renderLiveBar() : ''}
|
${isLive ? this.renderLiveBar() : ""}
|
||||||
<div class="content-inner ${isLive ? 'live' : ''}">
|
<div class="content-inner ${isLive ? "live" : ""}">
|
||||||
${this.renderCurrentView()}
|
${this.renderCurrentView()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -878,4 +1125,4 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('cheating-daddy-app', CheatingDaddyApp);
|
customElements.define("cheating-daddy-app", CheatingDaddyApp);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
|
import { html, css, LitElement } from "../../assets/lit-core-2.7.4.min.js";
|
||||||
|
|
||||||
export class AssistantView extends LitElement {
|
export class AssistantView extends LitElement {
|
||||||
static styles = css`
|
static styles = css`
|
||||||
@@ -54,12 +54,22 @@ export class AssistantView extends LitElement {
|
|||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.response-container h1 { font-size: 1.5em; }
|
.response-container h1 {
|
||||||
.response-container h2 { font-size: 1.3em; }
|
font-size: 1.5em;
|
||||||
.response-container h3 { font-size: 1.15em; }
|
}
|
||||||
.response-container h4 { font-size: 1.05em; }
|
.response-container h2 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
.response-container h3 {
|
||||||
|
font-size: 1.15em;
|
||||||
|
}
|
||||||
|
.response-container h4 {
|
||||||
|
font-size: 1.05em;
|
||||||
|
}
|
||||||
.response-container h5,
|
.response-container h5,
|
||||||
.response-container h6 { font-size: 1em; }
|
.response-container h6 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.response-container p {
|
.response-container p {
|
||||||
margin: 0.6em 0;
|
margin: 0.6em 0;
|
||||||
@@ -100,11 +110,220 @@ export class AssistantView extends LitElement {
|
|||||||
padding: var(--space-md);
|
padding: var(--space-md);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
margin: 0.8em 0;
|
margin: 0.8em 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container pre::before {
|
||||||
|
content: attr(data-language);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-top: none;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom-left-radius: var(--radius-sm);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.response-container pre code {
|
.response-container pre code {
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Syntax highlighting for code blocks ── */
|
||||||
|
/* Default (Dark theme) */
|
||||||
|
.response-container .hljs {
|
||||||
|
color: #c9d1d9;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-doctag,
|
||||||
|
.response-container .hljs-keyword,
|
||||||
|
.response-container .hljs-meta .hljs-keyword,
|
||||||
|
.response-container .hljs-template-tag,
|
||||||
|
.response-container .hljs-template-variable,
|
||||||
|
.response-container .hljs-type,
|
||||||
|
.response-container .hljs-variable.language_ {
|
||||||
|
color: #ff7b72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-title,
|
||||||
|
.response-container .hljs-title.class_,
|
||||||
|
.response-container .hljs-title.class_.inherited__,
|
||||||
|
.response-container .hljs-title.function_ {
|
||||||
|
color: #d2a8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-attr,
|
||||||
|
.response-container .hljs-attribute,
|
||||||
|
.response-container .hljs-literal,
|
||||||
|
.response-container .hljs-meta,
|
||||||
|
.response-container .hljs-number,
|
||||||
|
.response-container .hljs-operator,
|
||||||
|
.response-container .hljs-selector-attr,
|
||||||
|
.response-container .hljs-selector-class,
|
||||||
|
.response-container .hljs-selector-id,
|
||||||
|
.response-container .hljs-variable {
|
||||||
|
color: #79c0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-meta .hljs-string,
|
||||||
|
.response-container .hljs-regexp,
|
||||||
|
.response-container .hljs-string {
|
||||||
|
color: #a5d6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-built_in,
|
||||||
|
.response-container .hljs-symbol {
|
||||||
|
color: #ffa657;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-code,
|
||||||
|
.response-container .hljs-comment,
|
||||||
|
.response-container .hljs-formula {
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-name,
|
||||||
|
.response-container .hljs-quote,
|
||||||
|
.response-container .hljs-selector-pseudo,
|
||||||
|
.response-container .hljs-selector-tag {
|
||||||
|
color: #7ee787;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-subst {
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-section {
|
||||||
|
color: #1f6feb;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-bullet {
|
||||||
|
color: #f2cc60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-emphasis {
|
||||||
|
color: #c9d1d9;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-strong {
|
||||||
|
color: #c9d1d9;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-addition {
|
||||||
|
color: #aff5b4;
|
||||||
|
background-color: #033a16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-deletion {
|
||||||
|
color: #ffdcd7;
|
||||||
|
background-color: #67060c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light theme syntax highlighting */
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs {
|
||||||
|
color: #24292f;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-doctag,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-keyword,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-meta .hljs-keyword,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-template-tag,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-template-variable,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-type,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-variable.language_ {
|
||||||
|
color: #cf222e;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-title,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-title.class_,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-title.class_.inherited__,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-title.function_ {
|
||||||
|
color: #8250df;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-attr,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-attribute,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-literal,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-meta,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-number,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-operator,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-selector-attr,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-selector-class,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-selector-id,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-variable {
|
||||||
|
color: #0550ae;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-meta .hljs-string,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-regexp,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-string {
|
||||||
|
color: #0a3069;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-built_in,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-symbol {
|
||||||
|
color: #953800;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-code,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-comment,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-formula {
|
||||||
|
color: #6e7781;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-name,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-quote,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-selector-pseudo,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-selector-tag {
|
||||||
|
color: #116329;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-subst {
|
||||||
|
color: #24292f;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-section {
|
||||||
|
color: #0969da;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-bullet {
|
||||||
|
color: #953800;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-emphasis {
|
||||||
|
color: #24292f;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-strong {
|
||||||
|
color: #24292f;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-addition {
|
||||||
|
color: #116329;
|
||||||
|
background-color: #dafbe1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-deletion {
|
||||||
|
color: #82071e;
|
||||||
|
background-color: #ffebe9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.response-container a {
|
.response-container a {
|
||||||
@@ -263,7 +482,9 @@ export class AssistantView extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
transition: border-color 0.4s ease, background var(--transition);
|
transition:
|
||||||
|
border-color 0.4s ease,
|
||||||
|
background var(--transition);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -298,6 +519,52 @@ export class AssistantView extends LitElement {
|
|||||||
height: calc(100% + 2px);
|
height: calc(100% + 2px);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Expand button ── */
|
||||||
|
|
||||||
|
.expand-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--space-xs) var(--space-md);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
background: var(--bg-app);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
padding: var(--space-xs) var(--space-md);
|
||||||
|
border-radius: 100px;
|
||||||
|
height: 26px;
|
||||||
|
transition:
|
||||||
|
color var(--transition),
|
||||||
|
border-color var(--transition),
|
||||||
|
background var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-btn:hover:not(:disabled) {
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-btn svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
static properties = {
|
static properties = {
|
||||||
@@ -305,28 +572,32 @@ export class AssistantView extends LitElement {
|
|||||||
currentResponseIndex: { type: Number },
|
currentResponseIndex: { type: Number },
|
||||||
selectedProfile: { type: String },
|
selectedProfile: { type: String },
|
||||||
onSendText: { type: Function },
|
onSendText: { type: Function },
|
||||||
|
onExpandResponse: { type: Function },
|
||||||
shouldAnimateResponse: { type: Boolean },
|
shouldAnimateResponse: { type: Boolean },
|
||||||
isAnalyzing: { type: Boolean, state: true },
|
isAnalyzing: { type: Boolean, state: true },
|
||||||
|
isExpanding: { type: Boolean, state: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.responses = [];
|
this.responses = [];
|
||||||
this.currentResponseIndex = -1;
|
this.currentResponseIndex = -1;
|
||||||
this.selectedProfile = 'interview';
|
this.selectedProfile = "interview";
|
||||||
this.onSendText = () => {};
|
this.onSendText = () => {};
|
||||||
|
this.onExpandResponse = () => {};
|
||||||
this.isAnalyzing = false;
|
this.isAnalyzing = false;
|
||||||
|
this.isExpanding = false;
|
||||||
this._animFrame = null;
|
this._animFrame = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getProfileNames() {
|
getProfileNames() {
|
||||||
return {
|
return {
|
||||||
interview: 'Job Interview',
|
interview: "Job Interview",
|
||||||
sales: 'Sales Call',
|
sales: "Sales Call",
|
||||||
meeting: 'Business Meeting',
|
meeting: "Business Meeting",
|
||||||
presentation: 'Presentation',
|
presentation: "Presentation",
|
||||||
negotiation: 'Negotiation',
|
negotiation: "Negotiation",
|
||||||
exam: 'Exam Assistant',
|
exam: "Exam Assistant",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,22 +605,45 @@ export class AssistantView extends LitElement {
|
|||||||
const profileNames = this.getProfileNames();
|
const profileNames = this.getProfileNames();
|
||||||
return this.responses.length > 0 && this.currentResponseIndex >= 0
|
return this.responses.length > 0 && this.currentResponseIndex >= 0
|
||||||
? this.responses[this.currentResponseIndex]
|
? this.responses[this.currentResponseIndex]
|
||||||
: `Listening to your ${profileNames[this.selectedProfile] || 'session'}...`;
|
: `Listening to your ${profileNames[this.selectedProfile] || "session"}...`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMarkdown(content) {
|
renderMarkdown(content) {
|
||||||
if (typeof window !== 'undefined' && window.marked) {
|
if (typeof window !== "undefined" && window.marked) {
|
||||||
try {
|
try {
|
||||||
|
// Configure marked to use highlight.js for syntax highlighting
|
||||||
window.marked.setOptions({
|
window.marked.setOptions({
|
||||||
breaks: true,
|
breaks: true,
|
||||||
gfm: true,
|
gfm: true,
|
||||||
sanitize: false,
|
sanitize: false,
|
||||||
|
highlight: (code, lang) => {
|
||||||
|
if (window.hljs && lang) {
|
||||||
|
try {
|
||||||
|
return window.hljs.highlight(code, { language: lang }).value;
|
||||||
|
} catch (e) {
|
||||||
|
// If language is not recognized, try auto-detection
|
||||||
|
try {
|
||||||
|
return window.hljs.highlightAuto(code).value;
|
||||||
|
} catch (err) {
|
||||||
|
return window.hljs.escapeHtml(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (window.hljs) {
|
||||||
|
// Auto-detect language if not specified
|
||||||
|
try {
|
||||||
|
return window.hljs.highlightAuto(code).value;
|
||||||
|
} catch (e) {
|
||||||
|
return window.hljs.escapeHtml(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
let rendered = window.marked.parse(content);
|
let rendered = window.marked.parse(content);
|
||||||
rendered = this.wrapWordsInSpans(rendered);
|
rendered = this.wrapWordsInSpans(rendered);
|
||||||
return rendered;
|
return rendered;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error parsing markdown:', error);
|
console.warn("Error parsing markdown:", error);
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,17 +652,21 @@ export class AssistantView extends LitElement {
|
|||||||
|
|
||||||
wrapWordsInSpans(html) {
|
wrapWordsInSpans(html) {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
const doc = parser.parseFromString(html, "text/html");
|
||||||
const tagsToSkip = ['PRE'];
|
const tagsToSkip = ["PRE", "CODE"];
|
||||||
|
|
||||||
function wrap(node) {
|
function wrap(node) {
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() && !tagsToSkip.includes(node.parentNode.tagName)) {
|
if (
|
||||||
|
node.nodeType === Node.TEXT_NODE &&
|
||||||
|
node.textContent.trim() &&
|
||||||
|
!tagsToSkip.includes(node.parentNode.tagName)
|
||||||
|
) {
|
||||||
const words = node.textContent.split(/(\s+)/);
|
const words = node.textContent.split(/(\s+)/);
|
||||||
const frag = document.createDocumentFragment();
|
const frag = document.createDocumentFragment();
|
||||||
words.forEach(word => {
|
words.forEach((word) => {
|
||||||
if (word.trim()) {
|
if (word.trim()) {
|
||||||
const span = document.createElement('span');
|
const span = document.createElement("span");
|
||||||
span.setAttribute('data-word', '');
|
span.setAttribute("data-word", "");
|
||||||
span.textContent = word;
|
span.textContent = word;
|
||||||
frag.appendChild(span);
|
frag.appendChild(span);
|
||||||
} else {
|
} else {
|
||||||
@@ -376,7 +674,10 @@ export class AssistantView extends LitElement {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
node.parentNode.replaceChild(frag, node);
|
node.parentNode.replaceChild(frag, node);
|
||||||
} else if (node.nodeType === Node.ELEMENT_NODE && !tagsToSkip.includes(node.tagName)) {
|
} else if (
|
||||||
|
node.nodeType === Node.ELEMENT_NODE &&
|
||||||
|
!tagsToSkip.includes(node.tagName)
|
||||||
|
) {
|
||||||
Array.from(node.childNodes).forEach(wrap);
|
Array.from(node.childNodes).forEach(wrap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,13 +685,56 @@ export class AssistantView extends LitElement {
|
|||||||
return doc.body.innerHTML;
|
return doc.body.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyCodeHighlighting(container) {
|
||||||
|
if (!window.hljs) return;
|
||||||
|
|
||||||
|
// Find all code blocks in the rendered content
|
||||||
|
const codeBlocks = container.querySelectorAll("pre code");
|
||||||
|
|
||||||
|
codeBlocks.forEach((block) => {
|
||||||
|
const pre = block.parentElement;
|
||||||
|
if (!pre || pre.tagName !== "PRE") return;
|
||||||
|
|
||||||
|
// Skip if already highlighted
|
||||||
|
if (block.classList.contains("hljs")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = block.textContent;
|
||||||
|
let lang = block.className.replace(/language-|lang-/, "") || "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (lang && window.hljs.getLanguage(lang)) {
|
||||||
|
block.innerHTML = window.hljs.highlight(code, { language: lang }).value;
|
||||||
|
} else {
|
||||||
|
// Auto-detect language
|
||||||
|
const result = window.hljs.highlightAuto(code);
|
||||||
|
block.innerHTML = result.value;
|
||||||
|
if (result.language && !lang) {
|
||||||
|
lang = result.language;
|
||||||
|
block.className = `language-${lang}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
block.classList.add("hljs");
|
||||||
|
|
||||||
|
// Set data-language attribute on pre tag for display
|
||||||
|
if (lang) {
|
||||||
|
pre.setAttribute("data-language", lang);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error highlighting code block:", e);
|
||||||
|
// Leave block as-is if highlighting fails
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
navigateToPreviousResponse() {
|
navigateToPreviousResponse() {
|
||||||
if (this.currentResponseIndex > 0) {
|
if (this.currentResponseIndex > 0) {
|
||||||
this.currentResponseIndex--;
|
this.currentResponseIndex--;
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('response-index-changed', {
|
new CustomEvent("response-index-changed", {
|
||||||
detail: { index: this.currentResponseIndex },
|
detail: { index: this.currentResponseIndex },
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
@@ -400,16 +744,16 @@ export class AssistantView extends LitElement {
|
|||||||
if (this.currentResponseIndex < this.responses.length - 1) {
|
if (this.currentResponseIndex < this.responses.length - 1) {
|
||||||
this.currentResponseIndex++;
|
this.currentResponseIndex++;
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('response-index-changed', {
|
new CustomEvent("response-index-changed", {
|
||||||
detail: { index: this.currentResponseIndex },
|
detail: { index: this.currentResponseIndex },
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollResponseUp() {
|
scrollResponseUp() {
|
||||||
const container = this.shadowRoot.querySelector('.response-container');
|
const container = this.shadowRoot.querySelector(".response-container");
|
||||||
if (container) {
|
if (container) {
|
||||||
const scrollAmount = container.clientHeight * 0.3;
|
const scrollAmount = container.clientHeight * 0.3;
|
||||||
container.scrollTop = Math.max(0, container.scrollTop - scrollAmount);
|
container.scrollTop = Math.max(0, container.scrollTop - scrollAmount);
|
||||||
@@ -417,10 +761,13 @@ export class AssistantView extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollResponseDown() {
|
scrollResponseDown() {
|
||||||
const container = this.shadowRoot.querySelector('.response-container');
|
const container = this.shadowRoot.querySelector(".response-container");
|
||||||
if (container) {
|
if (container) {
|
||||||
const scrollAmount = container.clientHeight * 0.3;
|
const scrollAmount = container.clientHeight * 0.3;
|
||||||
container.scrollTop = Math.min(container.scrollHeight - container.clientHeight, container.scrollTop + scrollAmount);
|
container.scrollTop = Math.min(
|
||||||
|
container.scrollHeight - container.clientHeight,
|
||||||
|
container.scrollTop + scrollAmount,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,17 +775,19 @@ export class AssistantView extends LitElement {
|
|||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
|
|
||||||
this.handlePreviousResponse = () => this.navigateToPreviousResponse();
|
this.handlePreviousResponse = () => this.navigateToPreviousResponse();
|
||||||
this.handleNextResponse = () => this.navigateToNextResponse();
|
this.handleNextResponse = () => this.navigateToNextResponse();
|
||||||
this.handleScrollUp = () => this.scrollResponseUp();
|
this.handleScrollUp = () => this.scrollResponseUp();
|
||||||
this.handleScrollDown = () => this.scrollResponseDown();
|
this.handleScrollDown = () => this.scrollResponseDown();
|
||||||
|
this.handleExpandHotkey = () => this.handleExpandResponse();
|
||||||
|
|
||||||
ipcRenderer.on('navigate-previous-response', this.handlePreviousResponse);
|
ipcRenderer.on("navigate-previous-response", this.handlePreviousResponse);
|
||||||
ipcRenderer.on('navigate-next-response', this.handleNextResponse);
|
ipcRenderer.on("navigate-next-response", this.handleNextResponse);
|
||||||
ipcRenderer.on('scroll-response-up', this.handleScrollUp);
|
ipcRenderer.on("scroll-response-up", this.handleScrollUp);
|
||||||
ipcRenderer.on('scroll-response-down', this.handleScrollDown);
|
ipcRenderer.on("scroll-response-down", this.handleScrollDown);
|
||||||
|
ipcRenderer.on("expand-response", this.handleExpandHotkey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,25 +796,40 @@ export class AssistantView extends LitElement {
|
|||||||
this._stopWaveformAnimation();
|
this._stopWaveformAnimation();
|
||||||
|
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
if (this.handlePreviousResponse) ipcRenderer.removeListener('navigate-previous-response', this.handlePreviousResponse);
|
if (this.handlePreviousResponse)
|
||||||
if (this.handleNextResponse) ipcRenderer.removeListener('navigate-next-response', this.handleNextResponse);
|
ipcRenderer.removeListener(
|
||||||
if (this.handleScrollUp) ipcRenderer.removeListener('scroll-response-up', this.handleScrollUp);
|
"navigate-previous-response",
|
||||||
if (this.handleScrollDown) ipcRenderer.removeListener('scroll-response-down', this.handleScrollDown);
|
this.handlePreviousResponse,
|
||||||
|
);
|
||||||
|
if (this.handleNextResponse)
|
||||||
|
ipcRenderer.removeListener(
|
||||||
|
"navigate-next-response",
|
||||||
|
this.handleNextResponse,
|
||||||
|
);
|
||||||
|
if (this.handleScrollUp)
|
||||||
|
ipcRenderer.removeListener("scroll-response-up", this.handleScrollUp);
|
||||||
|
if (this.handleScrollDown)
|
||||||
|
ipcRenderer.removeListener(
|
||||||
|
"scroll-response-down",
|
||||||
|
this.handleScrollDown,
|
||||||
|
);
|
||||||
|
if (this.handleExpandHotkey)
|
||||||
|
ipcRenderer.removeListener("expand-response", this.handleExpandHotkey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSendText() {
|
async handleSendText() {
|
||||||
const textInput = this.shadowRoot.querySelector('#textInput');
|
const textInput = this.shadowRoot.querySelector("#textInput");
|
||||||
if (textInput && textInput.value.trim()) {
|
if (textInput && textInput.value.trim()) {
|
||||||
const message = textInput.value.trim();
|
const message = textInput.value.trim();
|
||||||
textInput.value = '';
|
textInput.value = "";
|
||||||
await this.onSendText(message);
|
await this.onSendText(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTextKeydown(e) {
|
handleTextKeydown(e) {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.handleSendText();
|
this.handleSendText();
|
||||||
}
|
}
|
||||||
@@ -480,10 +844,22 @@ export class AssistantView extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleExpandResponse() {
|
||||||
|
if (
|
||||||
|
this.isExpanding ||
|
||||||
|
this.responses.length === 0 ||
|
||||||
|
this.currentResponseIndex < 0
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
this.isExpanding = true;
|
||||||
|
this._responseCountWhenStarted = this.responses.length;
|
||||||
|
await this.onExpandResponse();
|
||||||
|
}
|
||||||
|
|
||||||
_startWaveformAnimation() {
|
_startWaveformAnimation() {
|
||||||
const canvas = this.shadowRoot.querySelector('.analyze-canvas');
|
const canvas = this.shadowRoot.querySelector(".analyze-canvas");
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext("2d");
|
||||||
const dpr = window.devicePixelRatio || 1;
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
@@ -491,7 +867,8 @@ export class AssistantView extends LitElement {
|
|||||||
canvas.height = rect.height * dpr;
|
canvas.height = rect.height * dpr;
|
||||||
ctx.scale(dpr, dpr);
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
const dangerColor = getComputedStyle(this).getPropertyValue('--danger').trim() || '#EF4444';
|
const dangerColor =
|
||||||
|
getComputedStyle(this).getPropertyValue("--danger").trim() || "#EF4444";
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
const FADE_IN = 0.5; // seconds
|
const FADE_IN = 0.5; // seconds
|
||||||
const PARTICLE_SPREAD = 4; // px inward from border
|
const PARTICLE_SPREAD = 4; // px inward from border
|
||||||
@@ -542,7 +919,11 @@ export class AssistantView extends LitElement {
|
|||||||
// Pre-seed random offsets for stable particles
|
// Pre-seed random offsets for stable particles
|
||||||
const seeds = [];
|
const seeds = [];
|
||||||
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
||||||
seeds.push({ pos: Math.random(), drift: Math.random(), depthSeed: Math.random() });
|
seeds.push({
|
||||||
|
pos: Math.random(),
|
||||||
|
drift: Math.random(),
|
||||||
|
depthSeed: Math.random(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const draw = (now) => {
|
const draw = (now) => {
|
||||||
@@ -585,13 +966,17 @@ export class AssistantView extends LitElement {
|
|||||||
ctx.strokeStyle = dangerColor;
|
ctx.strokeStyle = dangerColor;
|
||||||
ctx.globalAlpha = wave.opacity * fade;
|
ctx.globalAlpha = wave.opacity * fade;
|
||||||
ctx.lineWidth = wave.width;
|
ctx.lineWidth = wave.width;
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = "round";
|
||||||
ctx.lineJoin = 'round';
|
ctx.lineJoin = "round";
|
||||||
|
|
||||||
for (let x = 0; x <= w; x++) {
|
for (let x = 0; x <= w; x++) {
|
||||||
const norm = x / w;
|
const norm = x / w;
|
||||||
const envelope = Math.sin(norm * Math.PI);
|
const envelope = Math.sin(norm * Math.PI);
|
||||||
const y = midY + Math.sin(norm * Math.PI * 2 * wave.freq + elapsed * wave.speed) * (midY * wave.amp) * envelope;
|
const y =
|
||||||
|
midY +
|
||||||
|
Math.sin(norm * Math.PI * 2 * wave.freq + elapsed * wave.speed) *
|
||||||
|
(midY * wave.amp) *
|
||||||
|
envelope;
|
||||||
if (x === 0) ctx.moveTo(x, y);
|
if (x === 0) ctx.moveTo(x, y);
|
||||||
else ctx.lineTo(x, y);
|
else ctx.lineTo(x, y);
|
||||||
}
|
}
|
||||||
@@ -610,16 +995,16 @@ export class AssistantView extends LitElement {
|
|||||||
cancelAnimationFrame(this._animFrame);
|
cancelAnimationFrame(this._animFrame);
|
||||||
this._animFrame = null;
|
this._animFrame = null;
|
||||||
}
|
}
|
||||||
const canvas = this.shadowRoot.querySelector('.analyze-canvas');
|
const canvas = this.shadowRoot.querySelector(".analyze-canvas");
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext("2d");
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const container = this.shadowRoot.querySelector('.response-container');
|
const container = this.shadowRoot.querySelector(".response-container");
|
||||||
if (container) {
|
if (container) {
|
||||||
container.scrollTop = container.scrollHeight;
|
container.scrollTop = container.scrollHeight;
|
||||||
}
|
}
|
||||||
@@ -633,11 +1018,14 @@ export class AssistantView extends LitElement {
|
|||||||
|
|
||||||
updated(changedProperties) {
|
updated(changedProperties) {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
if (changedProperties.has('responses') || changedProperties.has('currentResponseIndex')) {
|
if (
|
||||||
|
changedProperties.has("responses") ||
|
||||||
|
changedProperties.has("currentResponseIndex")
|
||||||
|
) {
|
||||||
this.updateResponseContent();
|
this.updateResponseContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProperties.has('isAnalyzing')) {
|
if (changedProperties.has("isAnalyzing")) {
|
||||||
if (this.isAnalyzing) {
|
if (this.isAnalyzing) {
|
||||||
this._startWaveformAnimation();
|
this._startWaveformAnimation();
|
||||||
} else {
|
} else {
|
||||||
@@ -645,46 +1033,120 @@ export class AssistantView extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProperties.has('responses') && this.isAnalyzing) {
|
if (
|
||||||
|
changedProperties.has("responses") &&
|
||||||
|
(this.isAnalyzing || this.isExpanding)
|
||||||
|
) {
|
||||||
if (this.responses.length > this._responseCountWhenStarted) {
|
if (this.responses.length > this._responseCountWhenStarted) {
|
||||||
this.isAnalyzing = false;
|
this.isAnalyzing = false;
|
||||||
|
this.isExpanding = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateResponseContent() {
|
updateResponseContent() {
|
||||||
const container = this.shadowRoot.querySelector('#responseContainer');
|
const container = this.shadowRoot.querySelector("#responseContainer");
|
||||||
if (container) {
|
if (container) {
|
||||||
const currentResponse = this.getCurrentResponse();
|
const currentResponse = this.getCurrentResponse();
|
||||||
const renderedResponse = this.renderMarkdown(currentResponse);
|
const renderedResponse = this.renderMarkdown(currentResponse);
|
||||||
container.innerHTML = renderedResponse;
|
container.innerHTML = renderedResponse;
|
||||||
|
|
||||||
|
// Apply syntax highlighting to code blocks
|
||||||
|
this.applyCodeHighlighting(container);
|
||||||
|
|
||||||
if (this.shouldAnimateResponse) {
|
if (this.shouldAnimateResponse) {
|
||||||
this.dispatchEvent(new CustomEvent('response-animation-complete', { bubbles: true, composed: true }));
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("response-animation-complete", {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const hasMultipleResponses = this.responses.length > 1;
|
const hasMultipleResponses = this.responses.length > 1;
|
||||||
|
const hasResponse =
|
||||||
|
this.responses.length > 0 && this.currentResponseIndex >= 0;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="response-container" id="responseContainer"></div>
|
<div class="response-container" id="responseContainer"></div>
|
||||||
|
|
||||||
${hasMultipleResponses ? html`
|
${hasMultipleResponses || hasResponse
|
||||||
|
? html`
|
||||||
<div class="response-nav">
|
<div class="response-nav">
|
||||||
<button class="nav-btn" @click=${this.navigateToPreviousResponse} ?disabled=${this.currentResponseIndex <= 0} title="Previous response">
|
${hasMultipleResponses
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
? html`
|
||||||
<path fill-rule="evenodd" d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" />
|
<button
|
||||||
|
class="nav-btn"
|
||||||
|
@click=${this.navigateToPreviousResponse}
|
||||||
|
?disabled=${this.currentResponseIndex <= 0}
|
||||||
|
title="Previous response"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<span class="response-counter">${this.currentResponseIndex + 1} of ${this.responses.length}</span>
|
<span class="response-counter"
|
||||||
<button class="nav-btn" @click=${this.navigateToNextResponse} ?disabled=${this.currentResponseIndex >= this.responses.length - 1} title="Next response">
|
>${this.currentResponseIndex + 1} of
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
${this.responses.length}</span
|
||||||
<path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
|
>
|
||||||
|
<button
|
||||||
|
class="nav-btn"
|
||||||
|
@click=${this.navigateToNextResponse}
|
||||||
|
?disabled=${this.currentResponseIndex >=
|
||||||
|
this.responses.length - 1}
|
||||||
|
title="Next response"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${hasResponse
|
||||||
|
? html`
|
||||||
|
<button
|
||||||
|
class="expand-btn"
|
||||||
|
@click=${this.handleExpandResponse}
|
||||||
|
?disabled=${this.isExpanding}
|
||||||
|
title="Expand this response with more detail"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.168l3.71-3.938a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
${this.isExpanding ? "Expanding..." : "Expand"}
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
`
|
||||||
|
: ""}
|
||||||
|
|
||||||
<div class="input-bar">
|
<div class="input-bar">
|
||||||
<div class="input-bar-inner">
|
<div class="input-bar-inner">
|
||||||
@@ -695,11 +1157,26 @@ export class AssistantView extends LitElement {
|
|||||||
@keydown=${this.handleTextKeydown}
|
@keydown=${this.handleTextKeydown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button class="analyze-btn ${this.isAnalyzing ? 'analyzing' : ''}" @click=${this.handleScreenAnswer}>
|
<button
|
||||||
|
class="analyze-btn ${this.isAnalyzing ? "analyzing" : ""}"
|
||||||
|
@click=${this.handleScreenAnswer}
|
||||||
|
>
|
||||||
<canvas class="analyze-canvas"></canvas>
|
<canvas class="analyze-canvas"></canvas>
|
||||||
<span class="analyze-btn-content">
|
<span class="analyze-btn-content">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24">
|
<svg
|
||||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 3v7h6l-8 11v-7H5z" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 3v7h6l-8 11v-7H5z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Analyze Screen
|
Analyze Screen
|
||||||
</span>
|
</span>
|
||||||
@@ -709,4 +1186,4 @@ export class AssistantView extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('assistant-view', AssistantView);
|
customElements.define("assistant-view", AssistantView);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
|
import { html, css, LitElement } from "../../assets/lit-core-2.7.4.min.js";
|
||||||
import { unifiedPageStyles } from './sharedPageStyles.js';
|
import { unifiedPageStyles } from "./sharedPageStyles.js";
|
||||||
|
|
||||||
export class CustomizeView extends LitElement {
|
export class CustomizeView extends LitElement {
|
||||||
static styles = [
|
static styles = [
|
||||||
@@ -22,7 +22,7 @@ export class CustomizeView extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.warning-callout::before {
|
.warning-callout::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -6px;
|
top: -6px;
|
||||||
left: 16px;
|
left: 16px;
|
||||||
@@ -198,26 +198,26 @@ export class CustomizeView extends LitElement {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.selectedProfile = 'interview';
|
this.selectedProfile = "interview";
|
||||||
this.selectedLanguage = 'en-US';
|
this.selectedLanguage = "en-US";
|
||||||
this.selectedImageQuality = 'medium';
|
this.selectedImageQuality = "medium";
|
||||||
this.layoutMode = 'normal';
|
this.layoutMode = "normal";
|
||||||
this.keybinds = this.getDefaultKeybinds();
|
this.keybinds = this.getDefaultKeybinds();
|
||||||
this.onProfileChange = () => {};
|
this.onProfileChange = () => {};
|
||||||
this.onLanguageChange = () => {};
|
this.onLanguageChange = () => {};
|
||||||
this.onImageQualityChange = () => {};
|
this.onImageQualityChange = () => {};
|
||||||
this.onLayoutModeChange = () => {};
|
this.onLayoutModeChange = () => {};
|
||||||
this.googleSearchEnabled = true;
|
this.googleSearchEnabled = true;
|
||||||
this.providerMode = 'byok';
|
this.providerMode = "byok";
|
||||||
this.isClearing = false;
|
this.isClearing = false;
|
||||||
this.isRestoring = false;
|
this.isRestoring = false;
|
||||||
this.clearStatusMessage = '';
|
this.clearStatusMessage = "";
|
||||||
this.clearStatusType = '';
|
this.clearStatusType = "";
|
||||||
this.backgroundTransparency = 0.8;
|
this.backgroundTransparency = 0.8;
|
||||||
this.fontSize = 20;
|
this.fontSize = 20;
|
||||||
this.audioMode = 'speaker_only';
|
this.audioMode = "speaker_only";
|
||||||
this.customPrompt = '';
|
this.customPrompt = "";
|
||||||
this.theme = 'dark';
|
this.theme = "dark";
|
||||||
this._loadFromStorage();
|
this._loadFromStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,14 +227,17 @@ export class CustomizeView extends LitElement {
|
|||||||
|
|
||||||
async _loadFromStorage() {
|
async _loadFromStorage() {
|
||||||
try {
|
try {
|
||||||
const [prefs, keybinds] = await Promise.all([cheatingDaddy.storage.getPreferences(), cheatingDaddy.storage.getKeybinds()]);
|
const [prefs, keybinds] = await Promise.all([
|
||||||
|
cheatingDaddy.storage.getPreferences(),
|
||||||
|
cheatingDaddy.storage.getKeybinds(),
|
||||||
|
]);
|
||||||
this.googleSearchEnabled = prefs.googleSearchEnabled ?? true;
|
this.googleSearchEnabled = prefs.googleSearchEnabled ?? true;
|
||||||
this.providerMode = prefs.providerMode || 'byok';
|
this.providerMode = prefs.providerMode || "byok";
|
||||||
this.backgroundTransparency = prefs.backgroundTransparency ?? 0.8;
|
this.backgroundTransparency = prefs.backgroundTransparency ?? 0.8;
|
||||||
this.fontSize = prefs.fontSize ?? 20;
|
this.fontSize = prefs.fontSize ?? 20;
|
||||||
this.audioMode = prefs.audioMode ?? 'speaker_only';
|
this.audioMode = prefs.audioMode ?? "speaker_only";
|
||||||
this.customPrompt = prefs.customPrompt ?? '';
|
this.customPrompt = prefs.customPrompt ?? "";
|
||||||
this.theme = prefs.theme ?? 'dark';
|
this.theme = prefs.theme ?? "dark";
|
||||||
if (keybinds) {
|
if (keybinds) {
|
||||||
this.keybinds = { ...this.getDefaultKeybinds(), ...keybinds };
|
this.keybinds = { ...this.getDefaultKeybinds(), ...keybinds };
|
||||||
}
|
}
|
||||||
@@ -242,94 +245,145 @@ export class CustomizeView extends LitElement {
|
|||||||
this.updateFontSize();
|
this.updateFontSize();
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading settings:', error);
|
console.error("Error loading settings:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getProfiles() {
|
getProfiles() {
|
||||||
return [
|
return [
|
||||||
{ value: 'interview', name: 'Job Interview' },
|
{ value: "interview", name: "Job Interview" },
|
||||||
{ value: 'sales', name: 'Sales Call' },
|
{ value: "sales", name: "Sales Call" },
|
||||||
{ value: 'meeting', name: 'Business Meeting' },
|
{ value: "meeting", name: "Business Meeting" },
|
||||||
{ value: 'presentation', name: 'Presentation' },
|
{ value: "presentation", name: "Presentation" },
|
||||||
{ value: 'negotiation', name: 'Negotiation' },
|
{ value: "negotiation", name: "Negotiation" },
|
||||||
{ value: 'exam', name: 'Exam Assistant' },
|
{ value: "exam", name: "Exam Assistant" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
getLanguages() {
|
getLanguages() {
|
||||||
return [
|
return [
|
||||||
{ value: 'en-US', name: 'English (US)' },
|
{ value: "auto", name: "Auto (Multilingual)" },
|
||||||
{ value: 'en-GB', name: 'English (UK)' },
|
{ value: "en-US", name: "English (US)" },
|
||||||
{ value: 'en-AU', name: 'English (Australia)' },
|
{ value: "en-GB", name: "English (UK)" },
|
||||||
{ value: 'en-IN', name: 'English (India)' },
|
{ value: "en-AU", name: "English (Australia)" },
|
||||||
{ value: 'de-DE', name: 'German (Germany)' },
|
{ value: "en-IN", name: "English (India)" },
|
||||||
{ value: 'es-US', name: 'Spanish (US)' },
|
{ value: "de-DE", name: "German (Germany)" },
|
||||||
{ value: 'es-ES', name: 'Spanish (Spain)' },
|
{ value: "es-US", name: "Spanish (US)" },
|
||||||
{ value: 'fr-FR', name: 'French (France)' },
|
{ value: "es-ES", name: "Spanish (Spain)" },
|
||||||
{ value: 'fr-CA', name: 'French (Canada)' },
|
{ value: "fr-FR", name: "French (France)" },
|
||||||
{ value: 'hi-IN', name: 'Hindi (India)' },
|
{ value: "fr-CA", name: "French (Canada)" },
|
||||||
{ value: 'pt-BR', name: 'Portuguese (Brazil)' },
|
{ value: "hi-IN", name: "Hindi (India)" },
|
||||||
{ value: 'ar-XA', name: 'Arabic (Generic)' },
|
{ value: "pt-BR", name: "Portuguese (Brazil)" },
|
||||||
{ value: 'id-ID', name: 'Indonesian (Indonesia)' },
|
{ value: "ar-XA", name: "Arabic (Generic)" },
|
||||||
{ value: 'it-IT', name: 'Italian (Italy)' },
|
{ value: "id-ID", name: "Indonesian (Indonesia)" },
|
||||||
{ value: 'ja-JP', name: 'Japanese (Japan)' },
|
{ value: "it-IT", name: "Italian (Italy)" },
|
||||||
{ value: 'tr-TR', name: 'Turkish (Turkey)' },
|
{ value: "ja-JP", name: "Japanese (Japan)" },
|
||||||
{ value: 'vi-VN', name: 'Vietnamese (Vietnam)' },
|
{ value: "tr-TR", name: "Turkish (Turkey)" },
|
||||||
{ value: 'bn-IN', name: 'Bengali (India)' },
|
{ value: "vi-VN", name: "Vietnamese (Vietnam)" },
|
||||||
{ value: 'gu-IN', name: 'Gujarati (India)' },
|
{ value: "bn-IN", name: "Bengali (India)" },
|
||||||
{ value: 'kn-IN', name: 'Kannada (India)' },
|
{ value: "gu-IN", name: "Gujarati (India)" },
|
||||||
{ value: 'ml-IN', name: 'Malayalam (India)' },
|
{ value: "kn-IN", name: "Kannada (India)" },
|
||||||
{ value: 'mr-IN', name: 'Marathi (India)' },
|
{ value: "ml-IN", name: "Malayalam (India)" },
|
||||||
{ value: 'ta-IN', name: 'Tamil (India)' },
|
{ value: "mr-IN", name: "Marathi (India)" },
|
||||||
{ value: 'te-IN', name: 'Telugu (India)' },
|
{ value: "ta-IN", name: "Tamil (India)" },
|
||||||
{ value: 'nl-NL', name: 'Dutch (Netherlands)' },
|
{ value: "te-IN", name: "Telugu (India)" },
|
||||||
{ value: 'ko-KR', name: 'Korean (South Korea)' },
|
{ value: "nl-NL", name: "Dutch (Netherlands)" },
|
||||||
{ value: 'cmn-CN', name: 'Mandarin Chinese (China)' },
|
{ value: "ko-KR", name: "Korean (South Korea)" },
|
||||||
{ value: 'pl-PL', name: 'Polish (Poland)' },
|
{ value: "cmn-CN", name: "Mandarin Chinese (China)" },
|
||||||
{ value: 'ru-RU', name: 'Russian (Russia)' },
|
{ value: "pl-PL", name: "Polish (Poland)" },
|
||||||
{ value: 'th-TH', name: 'Thai (Thailand)' },
|
{ value: "ru-RU", name: "Russian (Russia)" },
|
||||||
|
{ value: "th-TH", name: "Thai (Thailand)" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultKeybinds() {
|
getDefaultKeybinds() {
|
||||||
const isMac = cheatingDaddy.isMacOS || navigator.platform.includes('Mac');
|
const isMac = cheatingDaddy.isMacOS || navigator.platform.includes("Mac");
|
||||||
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",
|
||||||
|
expandResponse: isMac ? "Cmd+E" : "Ctrl+E",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getKeybindActions() {
|
getKeybindActions() {
|
||||||
return [
|
return [
|
||||||
{ key: 'moveUp', name: 'Move Window Up', description: 'Move the app window up' },
|
{
|
||||||
{ key: 'moveDown', name: 'Move Window Down', description: 'Move the app window down' },
|
key: "moveUp",
|
||||||
{ key: 'moveLeft', name: 'Move Window Left', description: 'Move the app window left' },
|
name: "Move Window Up",
|
||||||
{ key: 'moveRight', name: 'Move Window Right', description: 'Move the app window right' },
|
description: "Move the app window up",
|
||||||
{ key: 'toggleVisibility', name: 'Toggle Visibility', description: 'Show or hide the app window' },
|
},
|
||||||
{ key: 'toggleClickThrough', name: 'Toggle Click-through', description: 'Enable or disable click-through mode' },
|
{
|
||||||
{ key: 'nextStep', name: 'Ask Next Step', description: 'Take screenshot and ask for next step' },
|
key: "moveDown",
|
||||||
{ key: 'previousResponse', name: 'Previous Response', description: 'Move to previous AI response' },
|
name: "Move Window Down",
|
||||||
{ key: 'nextResponse', name: 'Next Response', description: 'Move to next AI response' },
|
description: "Move the app window down",
|
||||||
{ key: 'scrollUp', name: 'Scroll Response Up', description: 'Scroll response content upward' },
|
},
|
||||||
{ key: 'scrollDown', name: 'Scroll Response Down', description: 'Scroll response content downward' },
|
{
|
||||||
|
key: "moveLeft",
|
||||||
|
name: "Move Window Left",
|
||||||
|
description: "Move the app window left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "moveRight",
|
||||||
|
name: "Move Window Right",
|
||||||
|
description: "Move the app window right",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "toggleVisibility",
|
||||||
|
name: "Toggle Visibility",
|
||||||
|
description: "Show or hide the app window",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "toggleClickThrough",
|
||||||
|
name: "Toggle Click-through",
|
||||||
|
description: "Enable or disable click-through mode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "nextStep",
|
||||||
|
name: "Ask Next Step",
|
||||||
|
description: "Take screenshot and ask for next step",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "previousResponse",
|
||||||
|
name: "Previous Response",
|
||||||
|
description: "Move to previous AI response",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "nextResponse",
|
||||||
|
name: "Next Response",
|
||||||
|
description: "Move to next AI response",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "scrollUp",
|
||||||
|
name: "Scroll Response Up",
|
||||||
|
description: "Scroll response content upward",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "scrollDown",
|
||||||
|
name: "Scroll Response Down",
|
||||||
|
description: "Scroll response content downward",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "expandResponse",
|
||||||
|
name: "Expand Response",
|
||||||
|
description: "Expand the current response with more detail",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveKeybinds() {
|
async saveKeybinds() {
|
||||||
await cheatingDaddy.storage.setKeybinds(this.keybinds);
|
await cheatingDaddy.storage.setKeybinds(this.keybinds);
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
ipcRenderer.send('update-keybinds', this.keybinds);
|
ipcRenderer.send("update-keybinds", this.keybinds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,18 +409,24 @@ export class CustomizeView extends LitElement {
|
|||||||
|
|
||||||
async handleCustomPromptInput(e) {
|
async handleCustomPromptInput(e) {
|
||||||
this.customPrompt = e.target.value;
|
this.customPrompt = e.target.value;
|
||||||
await cheatingDaddy.storage.updatePreference('customPrompt', this.customPrompt);
|
await cheatingDaddy.storage.updatePreference(
|
||||||
|
"customPrompt",
|
||||||
|
this.customPrompt,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleAudioModeSelect(e) {
|
async handleAudioModeSelect(e) {
|
||||||
this.audioMode = e.target.value;
|
this.audioMode = e.target.value;
|
||||||
await cheatingDaddy.storage.updatePreference('audioMode', this.audioMode);
|
await cheatingDaddy.storage.updatePreference("audioMode", this.audioMode);
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleProviderModeChange(e) {
|
async handleProviderModeChange(e) {
|
||||||
this.providerMode = e.target.value;
|
this.providerMode = e.target.value;
|
||||||
await cheatingDaddy.storage.updatePreference('providerMode', this.providerMode);
|
await cheatingDaddy.storage.updatePreference(
|
||||||
|
"providerMode",
|
||||||
|
this.providerMode,
|
||||||
|
);
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,13 +439,19 @@ export class CustomizeView extends LitElement {
|
|||||||
|
|
||||||
async handleGoogleSearchChange(e) {
|
async handleGoogleSearchChange(e) {
|
||||||
this.googleSearchEnabled = e.target.checked;
|
this.googleSearchEnabled = e.target.checked;
|
||||||
await cheatingDaddy.storage.updatePreference('googleSearchEnabled', this.googleSearchEnabled);
|
await cheatingDaddy.storage.updatePreference(
|
||||||
|
"googleSearchEnabled",
|
||||||
|
this.googleSearchEnabled,
|
||||||
|
);
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
try {
|
try {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('update-google-search-setting', this.googleSearchEnabled);
|
await ipcRenderer.invoke(
|
||||||
|
"update-google-search-setting",
|
||||||
|
this.googleSearchEnabled,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to notify main process:', error);
|
console.error("Failed to notify main process:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
@@ -393,25 +459,34 @@ export class CustomizeView extends LitElement {
|
|||||||
|
|
||||||
async handleBackgroundTransparencyChange(e) {
|
async handleBackgroundTransparencyChange(e) {
|
||||||
this.backgroundTransparency = parseFloat(e.target.value);
|
this.backgroundTransparency = parseFloat(e.target.value);
|
||||||
await cheatingDaddy.storage.updatePreference('backgroundTransparency', this.backgroundTransparency);
|
await cheatingDaddy.storage.updatePreference(
|
||||||
|
"backgroundTransparency",
|
||||||
|
this.backgroundTransparency,
|
||||||
|
);
|
||||||
this.updateBackgroundAppearance();
|
this.updateBackgroundAppearance();
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBackgroundAppearance() {
|
updateBackgroundAppearance() {
|
||||||
const colors = cheatingDaddy.theme.get(this.theme);
|
const colors = cheatingDaddy.theme.get(this.theme);
|
||||||
cheatingDaddy.theme.applyBackgrounds(colors.background, this.backgroundTransparency);
|
cheatingDaddy.theme.applyBackgrounds(
|
||||||
|
colors.background,
|
||||||
|
this.backgroundTransparency,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleFontSizeChange(e) {
|
async handleFontSizeChange(e) {
|
||||||
this.fontSize = parseInt(e.target.value, 10);
|
this.fontSize = parseInt(e.target.value, 10);
|
||||||
await cheatingDaddy.storage.updatePreference('fontSize', this.fontSize);
|
await cheatingDaddy.storage.updatePreference("fontSize", this.fontSize);
|
||||||
this.updateFontSize();
|
this.updateFontSize();
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFontSize() {
|
updateFontSize() {
|
||||||
document.documentElement.style.setProperty('--response-font-size', `${this.fontSize}px`);
|
document.documentElement.style.setProperty(
|
||||||
|
"--response-font-size",
|
||||||
|
`${this.fontSize}px`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeybindChange(action, value) {
|
handleKeybindChange(action, value) {
|
||||||
@@ -421,50 +496,50 @@ export class CustomizeView extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleKeybindFocus(e) {
|
handleKeybindFocus(e) {
|
||||||
e.target.placeholder = 'Press key combination...';
|
e.target.placeholder = "Press key combination...";
|
||||||
e.target.select();
|
e.target.select();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeybindInput(e) {
|
handleKeybindInput(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const modifiers = [];
|
const modifiers = [];
|
||||||
if (e.ctrlKey) modifiers.push('Ctrl');
|
if (e.ctrlKey) modifiers.push("Ctrl");
|
||||||
if (e.metaKey) modifiers.push('Cmd');
|
if (e.metaKey) modifiers.push("Cmd");
|
||||||
if (e.altKey) modifiers.push('Alt');
|
if (e.altKey) modifiers.push("Alt");
|
||||||
if (e.shiftKey) modifiers.push('Shift');
|
if (e.shiftKey) modifiers.push("Shift");
|
||||||
let mainKey = e.key;
|
let mainKey = e.key;
|
||||||
|
|
||||||
switch (e.code) {
|
switch (e.code) {
|
||||||
case 'ArrowUp':
|
case "ArrowUp":
|
||||||
mainKey = 'Up';
|
mainKey = "Up";
|
||||||
break;
|
break;
|
||||||
case 'ArrowDown':
|
case "ArrowDown":
|
||||||
mainKey = 'Down';
|
mainKey = "Down";
|
||||||
break;
|
break;
|
||||||
case 'ArrowLeft':
|
case "ArrowLeft":
|
||||||
mainKey = 'Left';
|
mainKey = "Left";
|
||||||
break;
|
break;
|
||||||
case 'ArrowRight':
|
case "ArrowRight":
|
||||||
mainKey = 'Right';
|
mainKey = "Right";
|
||||||
break;
|
break;
|
||||||
case 'Enter':
|
case "Enter":
|
||||||
mainKey = 'Enter';
|
mainKey = "Enter";
|
||||||
break;
|
break;
|
||||||
case 'Space':
|
case "Space":
|
||||||
mainKey = 'Space';
|
mainKey = "Space";
|
||||||
break;
|
break;
|
||||||
case 'Backslash':
|
case "Backslash":
|
||||||
mainKey = '\\';
|
mainKey = "\\";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (e.key.length === 1) mainKey = e.key.toUpperCase();
|
if (e.key.length === 1) mainKey = e.key.toUpperCase();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['Control', 'Meta', 'Alt', 'Shift'].includes(e.key)) return;
|
if (["Control", "Meta", "Alt", "Shift"].includes(e.key)) return;
|
||||||
|
|
||||||
const action = e.target.dataset.action;
|
const action = e.target.dataset.action;
|
||||||
const keybind = [...modifiers, mainKey].join('+');
|
const keybind = [...modifiers, mainKey].join("+");
|
||||||
this.handleKeybindChange(action, keybind);
|
this.handleKeybindChange(action, keybind);
|
||||||
e.target.value = keybind;
|
e.target.value = keybind;
|
||||||
e.target.blur();
|
e.target.blur();
|
||||||
@@ -474,8 +549,8 @@ export class CustomizeView extends LitElement {
|
|||||||
this.keybinds = this.getDefaultKeybinds();
|
this.keybinds = this.getDefaultKeybinds();
|
||||||
await cheatingDaddy.storage.setKeybinds(null);
|
await cheatingDaddy.storage.setKeybinds(null);
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
ipcRenderer.send('update-keybinds', this.keybinds);
|
ipcRenderer.send("update-keybinds", this.keybinds);
|
||||||
}
|
}
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
@@ -483,22 +558,22 @@ export class CustomizeView extends LitElement {
|
|||||||
async restoreAllSettings() {
|
async restoreAllSettings() {
|
||||||
if (this.isRestoring) return;
|
if (this.isRestoring) return;
|
||||||
this.isRestoring = true;
|
this.isRestoring = true;
|
||||||
this.clearStatusMessage = '';
|
this.clearStatusMessage = "";
|
||||||
this.clearStatusType = '';
|
this.clearStatusType = "";
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
try {
|
try {
|
||||||
// Restore all preferences to defaults
|
// Restore all preferences to defaults
|
||||||
const defaults = {
|
const defaults = {
|
||||||
customPrompt: '',
|
customPrompt: "",
|
||||||
selectedProfile: 'interview',
|
selectedProfile: "interview",
|
||||||
selectedLanguage: 'en-US',
|
selectedLanguage: "en-US",
|
||||||
selectedScreenshotInterval: '5',
|
selectedScreenshotInterval: "5",
|
||||||
selectedImageQuality: 'medium',
|
selectedImageQuality: "medium",
|
||||||
audioMode: 'speaker_only',
|
audioMode: "speaker_only",
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
backgroundTransparency: 0.8,
|
backgroundTransparency: 0.8,
|
||||||
googleSearchEnabled: false,
|
googleSearchEnabled: false,
|
||||||
theme: 'dark',
|
theme: "dark",
|
||||||
};
|
};
|
||||||
for (const [key, value] of Object.entries(defaults)) {
|
for (const [key, value] of Object.entries(defaults)) {
|
||||||
await cheatingDaddy.storage.updatePreference(key, value);
|
await cheatingDaddy.storage.updatePreference(key, value);
|
||||||
@@ -508,8 +583,8 @@ export class CustomizeView extends LitElement {
|
|||||||
this.keybinds = this.getDefaultKeybinds();
|
this.keybinds = this.getDefaultKeybinds();
|
||||||
await cheatingDaddy.storage.setKeybinds(null);
|
await cheatingDaddy.storage.setKeybinds(null);
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
ipcRenderer.send('update-keybinds', this.keybinds);
|
ipcRenderer.send("update-keybinds", this.keybinds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply to local state
|
// Apply to local state
|
||||||
@@ -533,12 +608,12 @@ export class CustomizeView extends LitElement {
|
|||||||
this.updateFontSize();
|
this.updateFontSize();
|
||||||
await cheatingDaddy.theme.save(defaults.theme);
|
await cheatingDaddy.theme.save(defaults.theme);
|
||||||
|
|
||||||
this.clearStatusMessage = 'All settings restored to defaults';
|
this.clearStatusMessage = "All settings restored to defaults";
|
||||||
this.clearStatusType = 'success';
|
this.clearStatusType = "success";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error restoring settings:', error);
|
console.error("Error restoring settings:", error);
|
||||||
this.clearStatusMessage = `Error restoring settings: ${error.message}`;
|
this.clearStatusMessage = `Error restoring settings: ${error.message}`;
|
||||||
this.clearStatusType = 'error';
|
this.clearStatusType = "error";
|
||||||
} finally {
|
} finally {
|
||||||
this.isRestoring = false;
|
this.isRestoring = false;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
@@ -548,28 +623,28 @@ export class CustomizeView extends LitElement {
|
|||||||
async clearLocalData() {
|
async clearLocalData() {
|
||||||
if (this.isClearing) return;
|
if (this.isClearing) return;
|
||||||
this.isClearing = true;
|
this.isClearing = true;
|
||||||
this.clearStatusMessage = '';
|
this.clearStatusMessage = "";
|
||||||
this.clearStatusType = '';
|
this.clearStatusType = "";
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
try {
|
try {
|
||||||
await cheatingDaddy.storage.clearAll();
|
await cheatingDaddy.storage.clearAll();
|
||||||
this.clearStatusMessage = 'Successfully cleared all local data';
|
this.clearStatusMessage = "Successfully cleared all local data";
|
||||||
this.clearStatusType = 'success';
|
this.clearStatusType = "success";
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.clearStatusMessage = 'Closing application...';
|
this.clearStatusMessage = "Closing application...";
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('quit-application');
|
await ipcRenderer.invoke("quit-application");
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error clearing data:', error);
|
console.error("Error clearing data:", error);
|
||||||
this.clearStatusMessage = `Error clearing data: ${error.message}`;
|
this.clearStatusMessage = `Error clearing data: ${error.message}`;
|
||||||
this.clearStatusType = 'error';
|
this.clearStatusType = "error";
|
||||||
} finally {
|
} finally {
|
||||||
this.isClearing = false;
|
this.isClearing = false;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
@@ -583,7 +658,11 @@ export class CustomizeView extends LitElement {
|
|||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Regime</label>
|
<label class="form-label">Regime</label>
|
||||||
<select class="control" .value=${this.providerMode} @change=${this.handleProviderModeChange}>
|
<select
|
||||||
|
class="control"
|
||||||
|
.value=${this.providerMode}
|
||||||
|
@change=${this.handleProviderModeChange}
|
||||||
|
>
|
||||||
<option value="byok">BYOK (API Keys)</option>
|
<option value="byok">BYOK (API Keys)</option>
|
||||||
<option value="local">Local AI (Ollama)</option>
|
<option value="local">Local AI (Ollama)</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -600,18 +679,31 @@ export class CustomizeView extends LitElement {
|
|||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Audio Mode</label>
|
<label class="form-label">Audio Mode</label>
|
||||||
<select class="control" .value=${this.audioMode} @change=${this.handleAudioModeSelect}>
|
<select
|
||||||
|
class="control"
|
||||||
|
.value=${this.audioMode}
|
||||||
|
@change=${this.handleAudioModeSelect}
|
||||||
|
>
|
||||||
<option value="speaker_only">Speaker Only (Interviewer)</option>
|
<option value="speaker_only">Speaker Only (Interviewer)</option>
|
||||||
<option value="mic_only">Microphone Only (Me)</option>
|
<option value="mic_only">Microphone Only (Me)</option>
|
||||||
<option value="both">Both Speaker and Microphone</option>
|
<option value="both">Both Speaker and Microphone</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
${this.audioMode !== 'speaker_only' ? html`
|
${this.audioMode !== "speaker_only"
|
||||||
<div class="warning-callout">May cause unexpected behavior. Only change this if you know what you're doing.</div>
|
? html`
|
||||||
` : ''}
|
<div class="warning-callout">
|
||||||
|
May cause unexpected behavior. Only change this if you know
|
||||||
|
what you're doing.
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Image Quality</label>
|
<label class="form-label">Image Quality</label>
|
||||||
<select class="control" .value=${this.selectedImageQuality} @change=${this.handleImageQualitySelect}>
|
<select
|
||||||
|
class="control"
|
||||||
|
.value=${this.selectedImageQuality}
|
||||||
|
@change=${this.handleImageQualitySelect}
|
||||||
|
>
|
||||||
<option value="high">High Quality</option>
|
<option value="high">High Quality</option>
|
||||||
<option value="medium">Medium Quality</option>
|
<option value="medium">Medium Quality</option>
|
||||||
<option value="low">Low Quality</option>
|
<option value="low">Low Quality</option>
|
||||||
@@ -629,8 +721,20 @@ export class CustomizeView extends LitElement {
|
|||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Speech Language</label>
|
<label class="form-label">Speech Language</label>
|
||||||
<select class="control" .value=${this.selectedLanguage} @change=${this.handleLanguageSelect}>
|
<select
|
||||||
${this.getLanguages().map(language => html`<option value=${language.value}>${language.name}</option>`)}
|
class="control"
|
||||||
|
.value=${this.selectedLanguage}
|
||||||
|
@change=${this.handleLanguageSelect}
|
||||||
|
>
|
||||||
|
${this.getLanguages().map(
|
||||||
|
(language) =>
|
||||||
|
html`<option
|
||||||
|
value=${language.value}
|
||||||
|
?selected=${language.value === this.selectedLanguage}
|
||||||
|
>
|
||||||
|
${language.name}
|
||||||
|
</option>`,
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -645,14 +749,23 @@ export class CustomizeView extends LitElement {
|
|||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Theme</label>
|
<label class="form-label">Theme</label>
|
||||||
<select class="control" .value=${this.theme} @change=${this.handleThemeChange}>
|
<select
|
||||||
${this.getThemes().map(theme => html`<option value=${theme.value}>${theme.name}</option>`)}
|
class="control"
|
||||||
|
.value=${this.theme}
|
||||||
|
@change=${this.handleThemeChange}
|
||||||
|
>
|
||||||
|
${this.getThemes().map(
|
||||||
|
(theme) =>
|
||||||
|
html`<option value=${theme.value}>${theme.name}</option>`,
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group slider-wrap">
|
<div class="form-group slider-wrap">
|
||||||
<div class="slider-header">
|
<div class="slider-header">
|
||||||
<label class="form-label">Background Transparency</label>
|
<label class="form-label">Background Transparency</label>
|
||||||
<span class="slider-value">${Math.round(this.backgroundTransparency * 100)}%</span>
|
<span class="slider-value"
|
||||||
|
>${Math.round(this.backgroundTransparency * 100)}%</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
class="slider-input"
|
class="slider-input"
|
||||||
@@ -688,7 +801,8 @@ export class CustomizeView extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<section class="surface">
|
<section class="surface">
|
||||||
<div class="surface-title">Keyboard Shortcuts</div>
|
<div class="surface-title">Keyboard Shortcuts</div>
|
||||||
${this.getKeybindActions().map(action => html`
|
${this.getKeybindActions().map(
|
||||||
|
(action) => html`
|
||||||
<div class="keybind-row">
|
<div class="keybind-row">
|
||||||
<span class="keybind-name">${action.name}</span>
|
<span class="keybind-name">${action.name}</span>
|
||||||
<input
|
<input
|
||||||
@@ -701,9 +815,16 @@ export class CustomizeView extends LitElement {
|
|||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
`)}
|
`,
|
||||||
|
)}
|
||||||
<div style="margin-top: var(--space-sm);">
|
<div style="margin-top: var(--space-sm);">
|
||||||
<button class="control" style="width:auto;padding:8px 10px;" @click=${this.resetKeybinds}>Reset to defaults</button>
|
<button
|
||||||
|
class="control"
|
||||||
|
style="width:auto;padding:8px 10px;"
|
||||||
|
@click=${this.resetKeybinds}
|
||||||
|
>
|
||||||
|
Reset to defaults
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
@@ -714,16 +835,32 @@ export class CustomizeView extends LitElement {
|
|||||||
<section class="surface danger-surface">
|
<section class="surface danger-surface">
|
||||||
<div class="surface-title danger">Privacy and Data</div>
|
<div class="surface-title danger">Privacy and Data</div>
|
||||||
<div style="display:flex;gap:var(--space-sm);flex-wrap:wrap;">
|
<div style="display:flex;gap:var(--space-sm);flex-wrap:wrap;">
|
||||||
<button class="danger-button" @click=${this.restoreAllSettings} ?disabled=${this.isRestoring}>
|
<button
|
||||||
${this.isRestoring ? 'Restoring...' : 'Restore all settings'}
|
class="danger-button"
|
||||||
|
@click=${this.restoreAllSettings}
|
||||||
|
?disabled=${this.isRestoring}
|
||||||
|
>
|
||||||
|
${this.isRestoring ? "Restoring..." : "Restore all settings"}
|
||||||
</button>
|
</button>
|
||||||
<button class="danger-button" @click=${this.clearLocalData} ?disabled=${this.isClearing}>
|
<button
|
||||||
${this.isClearing ? 'Clearing...' : 'Delete all data'}
|
class="danger-button"
|
||||||
|
@click=${this.clearLocalData}
|
||||||
|
?disabled=${this.isClearing}
|
||||||
|
>
|
||||||
|
${this.isClearing ? "Clearing..." : "Delete all data"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
${this.clearStatusMessage ? html`
|
${this.clearStatusMessage
|
||||||
<div class="status ${this.clearStatusType === 'success' ? 'success' : 'error'}">${this.clearStatusMessage}</div>
|
? html`
|
||||||
` : ''}
|
<div
|
||||||
|
class="status ${this.clearStatusType === "success"
|
||||||
|
? "success"
|
||||||
|
: "error"}"
|
||||||
|
>
|
||||||
|
${this.clearStatusMessage}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -733,16 +870,13 @@ export class CustomizeView extends LitElement {
|
|||||||
<div class="unified-page">
|
<div class="unified-page">
|
||||||
<div class="unified-wrap">
|
<div class="unified-wrap">
|
||||||
<div class="page-title">Settings</div>
|
<div class="page-title">Settings</div>
|
||||||
${this.renderAISection()}
|
${this.renderAISection()} ${this.renderAudioSection()}
|
||||||
${this.renderAudioSection()}
|
${this.renderLanguageSection()} ${this.renderAppearanceSection()}
|
||||||
${this.renderLanguageSection()}
|
${this.renderKeyboardSection()} ${this.renderPrivacySection()}
|
||||||
${this.renderAppearanceSection()}
|
|
||||||
${this.renderKeyboardSection()}
|
|
||||||
${this.renderPrivacySection()}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('customize-view', CustomizeView);
|
customElements.define("customize-view", CustomizeView);
|
||||||
|
|||||||
+686
-128
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>
|
||||||
|
|||||||
+99
-62
@@ -1,11 +1,43 @@
|
|||||||
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;
|
||||||
@@ -20,9 +52,9 @@ app.whenReady().then(async () => {
|
|||||||
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();
|
createMainWindow();
|
||||||
@@ -31,18 +63,18 @@ app.whenReady().then(async () => {
|
|||||||
setupGeneralIpcHandlers();
|
setupGeneralIpcHandlers();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on("window-all-closed", () => {
|
||||||
stopMacOSAudioCapture();
|
stopMacOSAudioCapture();
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== "darwin") {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('before-quit', () => {
|
app.on("before-quit", () => {
|
||||||
stopMacOSAudioCapture();
|
stopMacOSAudioCapture();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on("activate", () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
}
|
}
|
||||||
@@ -50,250 +82,255 @@ app.on('activate', () => {
|
|||||||
|
|
||||||
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+113
-93
@@ -1,6 +1,6 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -8,38 +8,39 @@ const CONFIG_VERSION = 1;
|
|||||||
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: '',
|
openaiCompatibleApiKey: "",
|
||||||
openaiCompatibleBaseUrl: '',
|
openaiCompatibleBaseUrl: "",
|
||||||
openaiCompatibleModel: ''
|
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,
|
||||||
responseProvider: 'gemini',
|
responseProvider: "gemini",
|
||||||
ollamaHost: 'http://127.0.0.1:11434',
|
ollamaHost: "http://127.0.0.1:11434",
|
||||||
ollamaModel: 'llama3.1',
|
ollamaModel: "llama3.1",
|
||||||
whisperModel: 'Xenova/whisper-small',
|
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
|
||||||
@@ -47,12 +48,22 @@ 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",
|
||||||
|
"Roaming",
|
||||||
|
"cheating-daddy-config",
|
||||||
|
);
|
||||||
|
} else if (platform === "darwin") {
|
||||||
|
configDir = path.join(
|
||||||
|
os.homedir(),
|
||||||
|
"Library",
|
||||||
|
"Application Support",
|
||||||
|
"cheating-daddy-config",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
configDir = path.join(os.homedir(), '.config', 'cheating-daddy-config');
|
configDir = path.join(os.homedir(), ".config", "cheating-daddy-config");
|
||||||
}
|
}
|
||||||
|
|
||||||
return configDir;
|
return configDir;
|
||||||
@@ -60,34 +71,34 @@ function getConfigDir() {
|
|||||||
|
|
||||||
// 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) {
|
} catch (error) {
|
||||||
@@ -103,7 +114,7 @@ function writeJsonFile(filePath, data) {
|
|||||||
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');
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error writing ${filePath}:`, error.message);
|
console.error(`Error writing ${filePath}:`, error.message);
|
||||||
@@ -119,7 +130,7 @@ function needsReset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -130,7 +141,7 @@ function needsReset() {
|
|||||||
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)) {
|
||||||
@@ -146,7 +157,7 @@ function resetConfigDir() {
|
|||||||
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
|
||||||
@@ -193,7 +204,7 @@ function setCredentials(credentials) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getApiKey() {
|
function getApiKey() {
|
||||||
return getCredentials().apiKey || '';
|
return getCredentials().apiKey || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function setApiKey(apiKey) {
|
function setApiKey(apiKey) {
|
||||||
@@ -201,7 +212,7 @@ function setApiKey(apiKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getGroqApiKey() {
|
function getGroqApiKey() {
|
||||||
return getCredentials().groqApiKey || '';
|
return getCredentials().groqApiKey || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function setGroqApiKey(groqApiKey) {
|
function setGroqApiKey(groqApiKey) {
|
||||||
@@ -211,9 +222,9 @@ function setGroqApiKey(groqApiKey) {
|
|||||||
function getOpenAICompatibleConfig() {
|
function getOpenAICompatibleConfig() {
|
||||||
const creds = getCredentials();
|
const creds = getCredentials();
|
||||||
return {
|
return {
|
||||||
apiKey: creds.openaiCompatibleApiKey || '',
|
apiKey: creds.openaiCompatibleApiKey || "",
|
||||||
baseUrl: creds.openaiCompatibleBaseUrl || '',
|
baseUrl: creds.openaiCompatibleBaseUrl || "",
|
||||||
model: creds.openaiCompatibleModel || ''
|
model: creds.openaiCompatibleModel || "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +232,7 @@ function setOpenAICompatibleConfig(apiKey, baseUrl, model) {
|
|||||||
return setCredentials({
|
return setCredentials({
|
||||||
openaiCompatibleApiKey: apiKey,
|
openaiCompatibleApiKey: apiKey,
|
||||||
openaiCompatibleBaseUrl: baseUrl,
|
openaiCompatibleBaseUrl: baseUrl,
|
||||||
openaiCompatibleModel: model
|
openaiCompatibleModel: model,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +277,7 @@ function setLimits(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() {
|
||||||
@@ -274,21 +285,21 @@ function getTodayLimits() {
|
|||||||
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);
|
setLimits(limits);
|
||||||
@@ -296,20 +307,20 @@ function getTodayLimits() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No entry for today - clean old entries and create new one
|
// No entry for today - clean old entries and create new one
|
||||||
limits.data = limits.data.filter(entry => entry.date === today);
|
limits.data = limits.data.filter((entry) => entry.date === today);
|
||||||
const newEntry = {
|
const newEntry = {
|
||||||
date: today,
|
date: today,
|
||||||
flash: { count: 0 },
|
flash: { count: 0 },
|
||||||
flashLite: { count: 0 },
|
flashLite: { count: 0 },
|
||||||
groq: {
|
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 },
|
||||||
},
|
},
|
||||||
gemini: {
|
gemini: {
|
||||||
'gemma-3-27b-it': { chars: 0 }
|
"gemma-3-27b-it": { chars: 0 },
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
limits.data.push(newEntry);
|
limits.data.push(newEntry);
|
||||||
setLimits(limits);
|
setLimits(limits);
|
||||||
@@ -322,7 +333,7 @@ function incrementLimitCount(model) {
|
|||||||
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
|
||||||
@@ -330,18 +341,18 @@ function incrementLimitCount(model) {
|
|||||||
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++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,9 +365,9 @@ function incrementCharUsage(provider, model, charCount) {
|
|||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -370,29 +381,29 @@ function getAvailableModel() {
|
|||||||
// 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
|
||||||
@@ -419,8 +430,12 @@ function saveSession(sessionId, data) {
|
|||||||
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:
|
||||||
|
data.screenAnalysisHistory ||
|
||||||
|
existingSession?.screenAnalysisHistory ||
|
||||||
|
[],
|
||||||
};
|
};
|
||||||
return writeJsonFile(sessionPath, sessionData);
|
return writeJsonFile(sessionPath, sessionData);
|
||||||
}
|
}
|
||||||
@@ -437,17 +452,19 @@ function getAllSessions() {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = fs.readdirSync(historyDir)
|
const files = fs
|
||||||
.filter(f => f.endsWith('.json'))
|
.readdirSync(historyDir)
|
||||||
|
.filter((f) => f.endsWith(".json"))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
// Sort by timestamp descending (newest first)
|
// Sort by timestamp descending (newest first)
|
||||||
const tsA = parseInt(a.replace('.json', ''));
|
const tsA = parseInt(a.replace(".json", ""));
|
||||||
const tsB = parseInt(b.replace('.json', ''));
|
const tsB = parseInt(b.replace(".json", ""));
|
||||||
return tsB - tsA;
|
return tsB - tsA;
|
||||||
});
|
});
|
||||||
|
|
||||||
return files.map(file => {
|
return files
|
||||||
const sessionId = file.replace('.json', '');
|
.map((file) => {
|
||||||
|
const sessionId = file.replace(".json", "");
|
||||||
const data = readJsonFile(path.join(historyDir, file), null);
|
const data = readJsonFile(path.join(historyDir, file), null);
|
||||||
if (data) {
|
if (data) {
|
||||||
return {
|
return {
|
||||||
@@ -457,13 +474,14 @@ function getAllSessions() {
|
|||||||
messageCount: data.conversationHistory?.length || 0,
|
messageCount: data.conversationHistory?.length || 0,
|
||||||
screenAnalysisCount: data.screenAnalysisHistory?.length || 0,
|
screenAnalysisCount: data.screenAnalysisHistory?.length || 0,
|
||||||
profile: data.profile || null,
|
profile: data.profile || null,
|
||||||
customPrompt: data.customPrompt || null
|
customPrompt: data.customPrompt || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}).filter(Boolean);
|
})
|
||||||
|
.filter(Boolean);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error reading sessions:', error.message);
|
console.error("Error reading sessions:", error.message);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -476,7 +494,7 @@ function deleteSession(sessionId) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting session:', error.message);
|
console.error("Error deleting session:", error.message);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -485,14 +503,16 @@ 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)
|
||||||
|
.filter((f) => f.endsWith(".json"));
|
||||||
|
files.forEach((file) => {
|
||||||
fs.unlinkSync(path.join(historyDir, file));
|
fs.unlinkSync(path.join(historyDir, file));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting all sessions:', error.message);
|
console.error("Error deleting all sessions:", error.message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -550,5 +570,5 @@ module.exports = {
|
|||||||
deleteAllSessions,
|
deleteAllSessions,
|
||||||
|
|
||||||
// Clear all
|
// Clear all
|
||||||
clearAllData
|
clearAllData,
|
||||||
};
|
};
|
||||||
|
|||||||
+480
-331
File diff suppressed because it is too large
Load Diff
+533
-116
@@ -1,17 +1,31 @@
|
|||||||
const { Ollama } = require('ollama');
|
const { Ollama } = require("ollama");
|
||||||
const { getSystemPrompt } = require('./prompts');
|
const { getSystemPrompt } = require("./prompts");
|
||||||
const { sendToRenderer, initializeNewSession, saveConversationTurn } = require('./gemini');
|
const {
|
||||||
|
sendToRenderer,
|
||||||
|
initializeNewSession,
|
||||||
|
saveConversationTurn,
|
||||||
|
} = require("./gemini");
|
||||||
|
const { fork } = require("child_process");
|
||||||
|
const path = require("path");
|
||||||
|
const { getSystemNode } = require("./nodeDetect");
|
||||||
|
|
||||||
// ── State ──
|
// ── State ──
|
||||||
|
|
||||||
let ollamaClient = null;
|
let ollamaClient = null;
|
||||||
let ollamaModel = null;
|
let ollamaModel = null;
|
||||||
let whisperPipeline = null;
|
let whisperWorker = null;
|
||||||
let isWhisperLoading = false;
|
let isWhisperLoading = false;
|
||||||
|
let whisperReady = false;
|
||||||
let localConversationHistory = [];
|
let localConversationHistory = [];
|
||||||
let currentSystemPrompt = null;
|
let currentSystemPrompt = null;
|
||||||
let isLocalActive = false;
|
let isLocalActive = false;
|
||||||
|
|
||||||
|
// Set when we intentionally kill the worker to suppress crash handling
|
||||||
|
let whisperShuttingDown = false;
|
||||||
|
|
||||||
|
// Pending transcription callback (one at a time)
|
||||||
|
let pendingTranscribe = null;
|
||||||
|
|
||||||
// VAD state
|
// VAD state
|
||||||
let isSpeaking = false;
|
let isSpeaking = false;
|
||||||
let speechBuffers = [];
|
let speechBuffers = [];
|
||||||
@@ -20,13 +34,32 @@ let speechFrameCount = 0;
|
|||||||
|
|
||||||
// VAD configuration
|
// VAD configuration
|
||||||
const VAD_MODES = {
|
const VAD_MODES = {
|
||||||
NORMAL: { energyThreshold: 0.01, speechFramesRequired: 3, silenceFramesRequired: 30 },
|
NORMAL: {
|
||||||
LOW_BITRATE: { energyThreshold: 0.008, speechFramesRequired: 4, silenceFramesRequired: 35 },
|
energyThreshold: 0.01,
|
||||||
AGGRESSIVE: { energyThreshold: 0.015, speechFramesRequired: 2, silenceFramesRequired: 20 },
|
speechFramesRequired: 3,
|
||||||
VERY_AGGRESSIVE: { energyThreshold: 0.02, speechFramesRequired: 2, silenceFramesRequired: 15 },
|
silenceFramesRequired: 30,
|
||||||
|
},
|
||||||
|
LOW_BITRATE: {
|
||||||
|
energyThreshold: 0.008,
|
||||||
|
speechFramesRequired: 4,
|
||||||
|
silenceFramesRequired: 35,
|
||||||
|
},
|
||||||
|
AGGRESSIVE: {
|
||||||
|
energyThreshold: 0.015,
|
||||||
|
speechFramesRequired: 2,
|
||||||
|
silenceFramesRequired: 20,
|
||||||
|
},
|
||||||
|
VERY_AGGRESSIVE: {
|
||||||
|
energyThreshold: 0.02,
|
||||||
|
speechFramesRequired: 2,
|
||||||
|
silenceFramesRequired: 15,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let vadConfig = VAD_MODES.VERY_AGGRESSIVE;
|
let vadConfig = VAD_MODES.VERY_AGGRESSIVE;
|
||||||
|
|
||||||
|
// Maximum speech buffer size: ~30 seconds at 16kHz, 16-bit mono
|
||||||
|
const MAX_SPEECH_BUFFER_BYTES = 16000 * 2 * 30; // 960,000 bytes
|
||||||
|
|
||||||
// Audio resampling buffer
|
// Audio resampling buffer
|
||||||
let resampleRemainder = Buffer.alloc(0);
|
let resampleRemainder = Buffer.alloc(0);
|
||||||
|
|
||||||
@@ -47,15 +80,24 @@ function resample24kTo16k(inputBuffer) {
|
|||||||
const frac = srcPos - srcIndex;
|
const frac = srcPos - srcIndex;
|
||||||
|
|
||||||
const s0 = combined.readInt16LE(srcIndex * 2);
|
const s0 = combined.readInt16LE(srcIndex * 2);
|
||||||
const s1 = srcIndex + 1 < inputSamples ? combined.readInt16LE((srcIndex + 1) * 2) : s0;
|
const s1 =
|
||||||
|
srcIndex + 1 < inputSamples
|
||||||
|
? combined.readInt16LE((srcIndex + 1) * 2)
|
||||||
|
: s0;
|
||||||
const interpolated = Math.round(s0 + frac * (s1 - s0));
|
const interpolated = Math.round(s0 + frac * (s1 - s0));
|
||||||
outputBuffer.writeInt16LE(Math.max(-32768, Math.min(32767, interpolated)), i * 2);
|
outputBuffer.writeInt16LE(
|
||||||
|
Math.max(-32768, Math.min(32767, interpolated)),
|
||||||
|
i * 2,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store remainder for next call
|
// Store remainder for next call
|
||||||
const consumedInputSamples = Math.ceil((outputSamples * 3) / 2);
|
const consumedInputSamples = Math.ceil((outputSamples * 3) / 2);
|
||||||
const remainderStart = consumedInputSamples * 2;
|
const remainderStart = consumedInputSamples * 2;
|
||||||
resampleRemainder = remainderStart < combined.length ? combined.slice(remainderStart) : Buffer.alloc(0);
|
resampleRemainder =
|
||||||
|
remainderStart < combined.length
|
||||||
|
? combined.slice(remainderStart)
|
||||||
|
: Buffer.alloc(0);
|
||||||
|
|
||||||
return outputBuffer;
|
return outputBuffer;
|
||||||
}
|
}
|
||||||
@@ -84,8 +126,8 @@ function processVAD(pcm16kBuffer) {
|
|||||||
if (!isSpeaking && speechFrameCount >= vadConfig.speechFramesRequired) {
|
if (!isSpeaking && speechFrameCount >= vadConfig.speechFramesRequired) {
|
||||||
isSpeaking = true;
|
isSpeaking = true;
|
||||||
speechBuffers = [];
|
speechBuffers = [];
|
||||||
console.log('[LocalAI] Speech started (RMS:', rms.toFixed(4), ')');
|
console.log("[LocalAI] Speech started (RMS:", rms.toFixed(4), ")");
|
||||||
sendToRenderer('update-status', 'Listening... (speech detected)');
|
sendToRenderer("update-status", "Listening... (speech detected)");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
silenceFrameCount++;
|
silenceFrameCount++;
|
||||||
@@ -93,13 +135,23 @@ function processVAD(pcm16kBuffer) {
|
|||||||
|
|
||||||
if (isSpeaking && silenceFrameCount >= vadConfig.silenceFramesRequired) {
|
if (isSpeaking && silenceFrameCount >= vadConfig.silenceFramesRequired) {
|
||||||
isSpeaking = false;
|
isSpeaking = false;
|
||||||
console.log('[LocalAI] Speech ended, accumulated', speechBuffers.length, 'chunks');
|
console.log(
|
||||||
sendToRenderer('update-status', 'Transcribing...');
|
"[LocalAI] Speech ended, accumulated",
|
||||||
|
speechBuffers.length,
|
||||||
|
"chunks",
|
||||||
|
);
|
||||||
|
sendToRenderer("update-status", "Transcribing...");
|
||||||
|
|
||||||
// Trigger transcription with accumulated audio
|
// Trigger transcription with accumulated audio
|
||||||
const audioData = Buffer.concat(speechBuffers);
|
const audioData = Buffer.concat(speechBuffers);
|
||||||
speechBuffers = [];
|
speechBuffers = [];
|
||||||
handleSpeechEnd(audioData);
|
handleSpeechEnd(audioData).catch((err) => {
|
||||||
|
console.error("[LocalAI] handleSpeechEnd crashed:", err);
|
||||||
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Transcription error: " + (err?.message || "unknown"),
|
||||||
|
);
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,76 +159,395 @@ function processVAD(pcm16kBuffer) {
|
|||||||
// Accumulate audio during speech
|
// Accumulate audio during speech
|
||||||
if (isSpeaking) {
|
if (isSpeaking) {
|
||||||
speechBuffers.push(Buffer.from(pcm16kBuffer));
|
speechBuffers.push(Buffer.from(pcm16kBuffer));
|
||||||
|
|
||||||
|
// Cap buffer at ~30 seconds to prevent OOM and ONNX tensor overflow
|
||||||
|
const totalBytes = speechBuffers.reduce((sum, b) => sum + b.length, 0);
|
||||||
|
if (totalBytes >= MAX_SPEECH_BUFFER_BYTES) {
|
||||||
|
isSpeaking = false;
|
||||||
|
console.log(
|
||||||
|
"[LocalAI] Speech buffer limit reached (" +
|
||||||
|
totalBytes +
|
||||||
|
" bytes), forcing transcription",
|
||||||
|
);
|
||||||
|
sendToRenderer("update-status", "Transcribing (max length reached)...");
|
||||||
|
const audioData = Buffer.concat(speechBuffers);
|
||||||
|
speechBuffers = [];
|
||||||
|
silenceFrameCount = 0;
|
||||||
|
speechFrameCount = 0;
|
||||||
|
handleSpeechEnd(audioData).catch((err) => {
|
||||||
|
console.error("[LocalAI] handleSpeechEnd crashed:", err);
|
||||||
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Transcription error: " + (err?.message || "unknown"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Whisper Transcription ──
|
// ── Whisper Worker (isolated child process) ──
|
||||||
|
|
||||||
|
function spawnWhisperWorker() {
|
||||||
|
if (whisperWorker) return;
|
||||||
|
|
||||||
|
const workerPath = path.join(__dirname, "whisperWorker.js");
|
||||||
|
console.log("[LocalAI] Spawning Whisper worker:", workerPath);
|
||||||
|
|
||||||
|
// Determine the best way to spawn the worker:
|
||||||
|
// 1. System Node.js (preferred) — native addons were compiled against this
|
||||||
|
// ABI, so onnxruntime-node works without SIGTRAP / ABI mismatches.
|
||||||
|
// 2. Electron utilityProcess (packaged builds) — proper Node.js child
|
||||||
|
// process API that doesn't require the RunAsNode fuse.
|
||||||
|
// 3. ELECTRON_RUN_AS_NODE (last resort, dev only) — the old approach that
|
||||||
|
// only works when the RunAsNode fuse isn't flipped.
|
||||||
|
|
||||||
|
const systemNode = getSystemNode();
|
||||||
|
|
||||||
|
if (systemNode) {
|
||||||
|
// Spawn with system Node.js — onnxruntime-node native binary matches ABI
|
||||||
|
console.log("[LocalAI] Using system Node.js:", systemNode.nodePath);
|
||||||
|
whisperWorker = fork(workerPath, [], {
|
||||||
|
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
||||||
|
execPath: systemNode.nodePath,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
// Unset ELECTRON_RUN_AS_NODE so the system node doesn't inherit it
|
||||||
|
ELECTRON_RUN_AS_NODE: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No system Node.js found — try utilityProcess (Electron >= 22)
|
||||||
|
// utilityProcess.fork() creates a proper child Node.js process without
|
||||||
|
// needing the RunAsNode fuse. Falls back to ELECTRON_RUN_AS_NODE for
|
||||||
|
// dev mode where fuses aren't applied.
|
||||||
|
try {
|
||||||
|
const { utilityProcess: UP } = require("electron");
|
||||||
|
if (UP && typeof UP.fork === "function") {
|
||||||
|
console.log("[LocalAI] Using Electron utilityProcess");
|
||||||
|
const up = UP.fork(workerPath);
|
||||||
|
// Wrap utilityProcess to look like a ChildProcess for the rest of localai.js
|
||||||
|
whisperWorker = wrapUtilityProcess(up);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// utilityProcess not available (older Electron or renderer context)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
"[LocalAI] No system Node.js — falling back to ELECTRON_RUN_AS_NODE (WASM backend will be used)",
|
||||||
|
);
|
||||||
|
whisperWorker = fork(workerPath, [], {
|
||||||
|
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
||||||
|
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
whisperWorker.stdout.on("data", (data) => {
|
||||||
|
console.log("[WhisperWorker stdout]", data.toString().trim());
|
||||||
|
});
|
||||||
|
whisperWorker.stderr.on("data", (data) => {
|
||||||
|
console.error("[WhisperWorker stderr]", data.toString().trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
whisperWorker.on("message", (msg) => {
|
||||||
|
switch (msg.type) {
|
||||||
|
case "ready":
|
||||||
|
console.log("[LocalAI] Whisper worker ready");
|
||||||
|
break;
|
||||||
|
case "load-result":
|
||||||
|
handleWorkerLoadResult(msg);
|
||||||
|
break;
|
||||||
|
case "transcribe-result":
|
||||||
|
handleWorkerTranscribeResult(msg);
|
||||||
|
break;
|
||||||
|
case "status":
|
||||||
|
sendToRenderer("update-status", msg.message);
|
||||||
|
break;
|
||||||
|
case "progress":
|
||||||
|
sendToRenderer("whisper-progress", {
|
||||||
|
file: msg.file,
|
||||||
|
progress: msg.progress,
|
||||||
|
loaded: msg.loaded,
|
||||||
|
total: msg.total,
|
||||||
|
status: msg.status,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
whisperWorker.on("exit", (code, signal) => {
|
||||||
|
console.error(
|
||||||
|
"[LocalAI] Whisper worker exited — code:",
|
||||||
|
code,
|
||||||
|
"signal:",
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
whisperWorker = null;
|
||||||
|
whisperReady = false;
|
||||||
|
|
||||||
|
// If we intentionally shut down, don't treat as crash
|
||||||
|
if (whisperShuttingDown) {
|
||||||
|
whisperShuttingDown = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject any pending transcription
|
||||||
|
if (pendingTranscribe) {
|
||||||
|
pendingTranscribe.reject(
|
||||||
|
new Error(
|
||||||
|
"Whisper worker crashed (code: " + code + ", signal: " + signal + ")",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
pendingTranscribe = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If session is still active, inform the user and respawn
|
||||||
|
if (isLocalActive) {
|
||||||
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Whisper crashed (signal: " +
|
||||||
|
(signal || code) +
|
||||||
|
"). Respawning worker...",
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isLocalActive) {
|
||||||
|
respawnWhisperWorker();
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
whisperWorker.on("error", (err) => {
|
||||||
|
console.error("[LocalAI] Whisper worker error:", err);
|
||||||
|
whisperWorker = null;
|
||||||
|
whisperReady = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap Electron's utilityProcess to behave like a ChildProcess (duck-typing)
|
||||||
|
* so the rest of localai.js can use the same API.
|
||||||
|
*/
|
||||||
|
function wrapUtilityProcess(up) {
|
||||||
|
const EventEmitter = require("events");
|
||||||
|
const wrapper = new EventEmitter();
|
||||||
|
|
||||||
|
// Forward messages
|
||||||
|
up.on("message", (msg) => wrapper.emit("message", msg));
|
||||||
|
|
||||||
|
// Map utilityProcess exit to ChildProcess-like exit event
|
||||||
|
up.on("exit", (code) => wrapper.emit("exit", code, null));
|
||||||
|
|
||||||
|
// Provide stdout/stderr stubs (utilityProcess pipes to parent console)
|
||||||
|
const { Readable } = require("stream");
|
||||||
|
wrapper.stdout = new Readable({ read() {} });
|
||||||
|
wrapper.stderr = new Readable({ read() {} });
|
||||||
|
|
||||||
|
wrapper.send = (data) => up.postMessage(data);
|
||||||
|
wrapper.kill = (signal) => up.kill();
|
||||||
|
wrapper.removeAllListeners = () => {
|
||||||
|
up.removeAllListeners();
|
||||||
|
EventEmitter.prototype.removeAllListeners.call(wrapper);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup stdout/stderr forwarding
|
||||||
|
wrapper.stdout.on("data", (data) => {
|
||||||
|
console.log("[WhisperWorker stdout]", data.toString().trim());
|
||||||
|
});
|
||||||
|
wrapper.stderr.on("data", (data) => {
|
||||||
|
console.error("[WhisperWorker stderr]", data.toString().trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pendingLoad = null;
|
||||||
|
|
||||||
|
function handleWorkerLoadResult(msg) {
|
||||||
|
if (msg.success) {
|
||||||
|
console.log(
|
||||||
|
"[LocalAI] Whisper model loaded successfully (in worker, device:",
|
||||||
|
msg.device || "unknown",
|
||||||
|
")",
|
||||||
|
);
|
||||||
|
whisperReady = true;
|
||||||
|
sendToRenderer("whisper-downloading", false);
|
||||||
|
isWhisperLoading = false;
|
||||||
|
if (pendingLoad) {
|
||||||
|
pendingLoad.resolve(true);
|
||||||
|
pendingLoad = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("[LocalAI] Whisper worker failed to load model:", msg.error);
|
||||||
|
sendToRenderer("whisper-downloading", false);
|
||||||
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Failed to load Whisper model: " + msg.error,
|
||||||
|
);
|
||||||
|
isWhisperLoading = false;
|
||||||
|
if (pendingLoad) {
|
||||||
|
pendingLoad.resolve(false);
|
||||||
|
pendingLoad = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWorkerTranscribeResult(msg) {
|
||||||
|
if (!pendingTranscribe) return;
|
||||||
|
if (msg.success) {
|
||||||
|
console.log("[LocalAI] Transcription:", msg.text);
|
||||||
|
pendingTranscribe.resolve(msg.text || null);
|
||||||
|
} else {
|
||||||
|
console.error("[LocalAI] Worker transcription error:", msg.error);
|
||||||
|
pendingTranscribe.resolve(null);
|
||||||
|
}
|
||||||
|
pendingTranscribe = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function respawnWhisperWorker() {
|
||||||
|
killWhisperWorker();
|
||||||
|
spawnWhisperWorker();
|
||||||
|
const { app } = require("electron");
|
||||||
|
const cacheDir = path.join(app.getPath("userData"), "whisper-models");
|
||||||
|
const modelName =
|
||||||
|
require("../storage").getPreferences().whisperModel ||
|
||||||
|
"Xenova/whisper-small";
|
||||||
|
sendToRenderer("whisper-downloading", true);
|
||||||
|
isWhisperLoading = true;
|
||||||
|
const device = resolveWhisperDevice();
|
||||||
|
whisperWorker.send({ type: "load", modelName, cacheDir, device });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine which ONNX backend to use for Whisper inference.
|
||||||
|
* - "cpu" → onnxruntime-node (fast, native — requires matching ABI)
|
||||||
|
* - "wasm" → onnxruntime-web (slower but universally compatible)
|
||||||
|
*
|
||||||
|
* When spawned with system Node.js, native CPU backend is safe.
|
||||||
|
* Otherwise default to WASM to prevent native crashes.
|
||||||
|
*/
|
||||||
|
function resolveWhisperDevice() {
|
||||||
|
const prefs = require("../storage").getPreferences();
|
||||||
|
if (prefs.whisperDevice) return prefs.whisperDevice;
|
||||||
|
// Auto-detect: if we're running with system Node.js, native is safe
|
||||||
|
const systemNode = getSystemNode();
|
||||||
|
return systemNode ? "cpu" : "wasm";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map the app's BCP-47 language tag (e.g. "en-US", "ru-RU") to the
|
||||||
|
* ISO 639-1 code that Whisper expects (e.g. "en", "ru").
|
||||||
|
* Returns "auto" when the user selected auto-detect, which tells the
|
||||||
|
* worker to let Whisper detect the language itself.
|
||||||
|
*/
|
||||||
|
function resolveWhisperLanguage() {
|
||||||
|
const prefs = require("../storage").getPreferences();
|
||||||
|
const lang = prefs.selectedLanguage || "en-US";
|
||||||
|
if (lang === "auto") return "auto";
|
||||||
|
// BCP-47: primary subtag is the ISO 639 code
|
||||||
|
// Handle special case: "cmn-CN" → "zh" (Mandarin Chinese → Whisper uses "zh")
|
||||||
|
const primary = lang.split("-")[0].toLowerCase();
|
||||||
|
const WHISPER_LANG_MAP = {
|
||||||
|
cmn: "zh",
|
||||||
|
yue: "zh",
|
||||||
|
};
|
||||||
|
return WHISPER_LANG_MAP[primary] || primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
function killWhisperWorker() {
|
||||||
|
if (whisperWorker) {
|
||||||
|
whisperShuttingDown = true;
|
||||||
|
try {
|
||||||
|
whisperWorker.removeAllListeners();
|
||||||
|
whisperWorker.kill();
|
||||||
|
} catch (_) {
|
||||||
|
// Already dead
|
||||||
|
}
|
||||||
|
whisperWorker = null;
|
||||||
|
whisperReady = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadWhisperPipeline(modelName) {
|
async function loadWhisperPipeline(modelName) {
|
||||||
if (whisperPipeline) return whisperPipeline;
|
if (whisperReady) return true;
|
||||||
if (isWhisperLoading) return null;
|
if (isWhisperLoading) return null;
|
||||||
|
|
||||||
isWhisperLoading = true;
|
isWhisperLoading = true;
|
||||||
console.log('[LocalAI] Loading Whisper model:', modelName);
|
console.log("[LocalAI] Loading Whisper model via worker:", modelName);
|
||||||
sendToRenderer('whisper-downloading', true);
|
sendToRenderer("whisper-downloading", true);
|
||||||
sendToRenderer('update-status', 'Loading Whisper model (first time may take a while)...');
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Loading Whisper model (first time may take a while)...",
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
spawnWhisperWorker();
|
||||||
// Dynamic import for ESM module
|
|
||||||
const { pipeline, env } = await import('@huggingface/transformers');
|
const { app } = require("electron");
|
||||||
// Cache models outside the asar archive so ONNX runtime can load them
|
const cacheDir = path.join(app.getPath("userData"), "whisper-models");
|
||||||
const { app } = require('electron');
|
|
||||||
const path = require('path');
|
const device = resolveWhisperDevice();
|
||||||
env.cacheDir = path.join(app.getPath('userData'), 'whisper-models');
|
console.log("[LocalAI] Whisper device:", device);
|
||||||
whisperPipeline = await pipeline('automatic-speech-recognition', modelName, {
|
|
||||||
dtype: 'q8',
|
return new Promise((resolve) => {
|
||||||
device: 'auto',
|
pendingLoad = { resolve };
|
||||||
|
whisperWorker.send({ type: "load", modelName, cacheDir, device });
|
||||||
});
|
});
|
||||||
console.log('[LocalAI] Whisper model loaded successfully');
|
|
||||||
sendToRenderer('whisper-downloading', false);
|
|
||||||
isWhisperLoading = false;
|
|
||||||
return whisperPipeline;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[LocalAI] Failed to load Whisper model:', error);
|
|
||||||
sendToRenderer('whisper-downloading', false);
|
|
||||||
sendToRenderer('update-status', 'Failed to load Whisper model: ' + error.message);
|
|
||||||
isWhisperLoading = false;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pcm16ToFloat32(pcm16Buffer) {
|
|
||||||
const samples = pcm16Buffer.length / 2;
|
|
||||||
const float32 = new Float32Array(samples);
|
|
||||||
for (let i = 0; i < samples; i++) {
|
|
||||||
float32[i] = pcm16Buffer.readInt16LE(i * 2) / 32768;
|
|
||||||
}
|
|
||||||
return float32;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function transcribeAudio(pcm16kBuffer) {
|
async function transcribeAudio(pcm16kBuffer) {
|
||||||
if (!whisperPipeline) {
|
if (!whisperReady || !whisperWorker) {
|
||||||
console.error('[LocalAI] Whisper pipeline not loaded');
|
console.error("[LocalAI] Whisper worker not ready");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!pcm16kBuffer || pcm16kBuffer.length < 2) {
|
||||||
|
console.error("[LocalAI] Invalid audio buffer:", pcm16kBuffer?.length);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[LocalAI] Starting transcription, audio length:",
|
||||||
|
pcm16kBuffer.length,
|
||||||
|
"bytes",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send audio to worker as base64 (IPC serialization)
|
||||||
|
const audioBase64 = pcm16kBuffer.toString("base64");
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Timeout: if worker takes > 60s, assume it's stuck
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
console.error("[LocalAI] Transcription timed out after 60s");
|
||||||
|
if (pendingTranscribe) {
|
||||||
|
pendingTranscribe = null;
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
pendingTranscribe = {
|
||||||
|
resolve: (val) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve(val);
|
||||||
|
},
|
||||||
|
reject: (err) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const float32Audio = pcm16ToFloat32(pcm16kBuffer);
|
whisperWorker.send({
|
||||||
|
type: "transcribe",
|
||||||
// Whisper expects audio at 16kHz which is what we have
|
audioBase64,
|
||||||
const result = await whisperPipeline(float32Audio, {
|
language: resolveWhisperLanguage(),
|
||||||
sampling_rate: 16000,
|
|
||||||
language: 'en',
|
|
||||||
task: 'transcribe',
|
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
const text = result.text?.trim();
|
clearTimeout(timeout);
|
||||||
console.log('[LocalAI] Transcription:', text);
|
pendingTranscribe = null;
|
||||||
return text;
|
console.error("[LocalAI] Failed to send to worker:", err);
|
||||||
} catch (error) {
|
resolve(null);
|
||||||
console.error('[LocalAI] Transcription error:', error);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Speech End Handler ──
|
// ── Speech End Handler ──
|
||||||
@@ -186,35 +557,52 @@ async function handleSpeechEnd(audioData) {
|
|||||||
|
|
||||||
// Minimum audio length check (~0.5 seconds at 16kHz, 16-bit)
|
// Minimum audio length check (~0.5 seconds at 16kHz, 16-bit)
|
||||||
if (audioData.length < 16000) {
|
if (audioData.length < 16000) {
|
||||||
console.log('[LocalAI] Audio too short, skipping');
|
console.log("[LocalAI] Audio too short, skipping");
|
||||||
sendToRenderer('update-status', 'Listening...');
|
sendToRenderer("update-status", "Listening...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("[LocalAI] Processing audio:", audioData.length, "bytes");
|
||||||
|
|
||||||
|
try {
|
||||||
const transcription = await transcribeAudio(audioData);
|
const transcription = await transcribeAudio(audioData);
|
||||||
|
|
||||||
if (!transcription || transcription.trim() === '' || transcription.trim().length < 2) {
|
if (
|
||||||
console.log('[LocalAI] Empty transcription, skipping');
|
!transcription ||
|
||||||
sendToRenderer('update-status', 'Listening...');
|
transcription.trim() === "" ||
|
||||||
|
transcription.trim().length < 2
|
||||||
|
) {
|
||||||
|
console.log("[LocalAI] Empty transcription, skipping");
|
||||||
|
sendToRenderer("update-status", "Listening...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToRenderer('update-status', 'Generating response...');
|
sendToRenderer("update-status", "Generating response...");
|
||||||
await sendToOllama(transcription);
|
await sendToOllama(transcription);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[LocalAI] handleSpeechEnd error:", error);
|
||||||
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Error: " + (error?.message || "transcription failed"),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Ollama Chat ──
|
// ── Ollama Chat ──
|
||||||
|
|
||||||
async function sendToOllama(transcription) {
|
async function sendToOllama(transcription) {
|
||||||
if (!ollamaClient || !ollamaModel) {
|
if (!ollamaClient || !ollamaModel) {
|
||||||
console.error('[LocalAI] Ollama not configured');
|
console.error("[LocalAI] Ollama not configured");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[LocalAI] Sending to Ollama:', transcription.substring(0, 100) + '...');
|
console.log(
|
||||||
|
"[LocalAI] Sending to Ollama:",
|
||||||
|
transcription.substring(0, 100) + "...",
|
||||||
|
);
|
||||||
|
|
||||||
localConversationHistory.push({
|
localConversationHistory.push({
|
||||||
role: 'user',
|
role: "user",
|
||||||
content: transcription.trim(),
|
content: transcription.trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -225,7 +613,10 @@ async function sendToOllama(transcription) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: 'system', content: currentSystemPrompt || 'You are a helpful assistant.' },
|
{
|
||||||
|
role: "system",
|
||||||
|
content: currentSystemPrompt || "You are a helpful assistant.",
|
||||||
|
},
|
||||||
...localConversationHistory,
|
...localConversationHistory,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -235,41 +626,52 @@ async function sendToOllama(transcription) {
|
|||||||
stream: true,
|
stream: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
let fullText = '';
|
let fullText = "";
|
||||||
let isFirst = true;
|
let isFirst = true;
|
||||||
|
|
||||||
for await (const part of response) {
|
for await (const part of response) {
|
||||||
const token = part.message?.content || '';
|
const token = part.message?.content || "";
|
||||||
if (token) {
|
if (token) {
|
||||||
fullText += token;
|
fullText += token;
|
||||||
sendToRenderer(isFirst ? 'new-response' : 'update-response', fullText);
|
sendToRenderer(isFirst ? "new-response" : "update-response", fullText);
|
||||||
isFirst = false;
|
isFirst = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fullText.trim()) {
|
if (fullText.trim()) {
|
||||||
localConversationHistory.push({
|
localConversationHistory.push({
|
||||||
role: 'assistant',
|
role: "assistant",
|
||||||
content: fullText.trim(),
|
content: fullText.trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
saveConversationTurn(transcription, fullText);
|
saveConversationTurn(transcription, fullText);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[LocalAI] Ollama response completed');
|
console.log("[LocalAI] Ollama response completed");
|
||||||
sendToRenderer('update-status', 'Listening...');
|
sendToRenderer("update-status", "Listening...");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LocalAI] Ollama error:', error);
|
console.error("[LocalAI] Ollama error:", error);
|
||||||
sendToRenderer('update-status', 'Ollama error: ' + error.message);
|
sendToRenderer("update-status", "Ollama error: " + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Public API ──
|
// ── Public API ──
|
||||||
|
|
||||||
async function initializeLocalSession(ollamaHost, model, whisperModel, profile, customPrompt) {
|
async function initializeLocalSession(
|
||||||
console.log('[LocalAI] Initializing local session:', { ollamaHost, model, whisperModel, profile });
|
ollamaHost,
|
||||||
|
model,
|
||||||
|
whisperModel,
|
||||||
|
profile,
|
||||||
|
customPrompt,
|
||||||
|
) {
|
||||||
|
console.log("[LocalAI] Initializing local session:", {
|
||||||
|
ollamaHost,
|
||||||
|
model,
|
||||||
|
whisperModel,
|
||||||
|
profile,
|
||||||
|
});
|
||||||
|
|
||||||
sendToRenderer('session-initializing', true);
|
sendToRenderer("session-initializing", true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Setup system prompt
|
// Setup system prompt
|
||||||
@@ -282,18 +684,26 @@ async function initializeLocalSession(ollamaHost, model, whisperModel, profile,
|
|||||||
// Test Ollama connection
|
// Test Ollama connection
|
||||||
try {
|
try {
|
||||||
await ollamaClient.list();
|
await ollamaClient.list();
|
||||||
console.log('[LocalAI] Ollama connection verified');
|
console.log("[LocalAI] Ollama connection verified");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LocalAI] Cannot connect to Ollama at', ollamaHost, ':', error.message);
|
console.error(
|
||||||
sendToRenderer('session-initializing', false);
|
"[LocalAI] Cannot connect to Ollama at",
|
||||||
sendToRenderer('update-status', 'Cannot connect to Ollama at ' + ollamaHost);
|
ollamaHost,
|
||||||
|
":",
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
|
sendToRenderer("session-initializing", false);
|
||||||
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Cannot connect to Ollama at " + ollamaHost,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load Whisper model
|
// Load Whisper model
|
||||||
const pipeline = await loadWhisperPipeline(whisperModel);
|
const pipeline = await loadWhisperPipeline(whisperModel);
|
||||||
if (!pipeline) {
|
if (!pipeline) {
|
||||||
sendToRenderer('session-initializing', false);
|
sendToRenderer("session-initializing", false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,15 +719,15 @@ async function initializeLocalSession(ollamaHost, model, whisperModel, profile,
|
|||||||
initializeNewSession(profile, customPrompt);
|
initializeNewSession(profile, customPrompt);
|
||||||
|
|
||||||
isLocalActive = true;
|
isLocalActive = true;
|
||||||
sendToRenderer('session-initializing', false);
|
sendToRenderer("session-initializing", false);
|
||||||
sendToRenderer('update-status', 'Local AI ready - Listening...');
|
sendToRenderer("update-status", "Local AI ready - Listening...");
|
||||||
|
|
||||||
console.log('[LocalAI] Session initialized successfully');
|
console.log("[LocalAI] Session initialized successfully");
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LocalAI] Initialization error:', error);
|
console.error("[LocalAI] Initialization error:", error);
|
||||||
sendToRenderer('session-initializing', false);
|
sendToRenderer("session-initializing", false);
|
||||||
sendToRenderer('update-status', 'Local AI error: ' + error.message);
|
sendToRenderer("update-status", "Local AI error: " + error.message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,7 +743,7 @@ function processLocalAudio(monoChunk24k) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeLocalSession() {
|
function closeLocalSession() {
|
||||||
console.log('[LocalAI] Closing local session');
|
console.log("[LocalAI] Closing local session");
|
||||||
isLocalActive = false;
|
isLocalActive = false;
|
||||||
isSpeaking = false;
|
isSpeaking = false;
|
||||||
speechBuffers = [];
|
speechBuffers = [];
|
||||||
@@ -344,7 +754,8 @@ function closeLocalSession() {
|
|||||||
ollamaClient = null;
|
ollamaClient = null;
|
||||||
ollamaModel = null;
|
ollamaModel = null;
|
||||||
currentSystemPrompt = null;
|
currentSystemPrompt = null;
|
||||||
// Note: whisperPipeline is kept loaded to avoid reloading on next session
|
// Note: whisperWorker is kept alive to avoid reloading model on next session
|
||||||
|
// To fully clean up, call killWhisperWorker()
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLocalSessionActive() {
|
function isLocalSessionActive() {
|
||||||
@@ -355,7 +766,7 @@ function isLocalSessionActive() {
|
|||||||
|
|
||||||
async function sendLocalText(text) {
|
async function sendLocalText(text) {
|
||||||
if (!isLocalActive || !ollamaClient) {
|
if (!isLocalActive || !ollamaClient) {
|
||||||
return { success: false, error: 'No active local session' };
|
return { success: false, error: "No active local session" };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -368,28 +779,31 @@ async function sendLocalText(text) {
|
|||||||
|
|
||||||
async function sendLocalImage(base64Data, prompt) {
|
async function sendLocalImage(base64Data, prompt) {
|
||||||
if (!isLocalActive || !ollamaClient) {
|
if (!isLocalActive || !ollamaClient) {
|
||||||
return { success: false, error: 'No active local session' };
|
return { success: false, error: "No active local session" };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[LocalAI] Sending image to Ollama');
|
console.log("[LocalAI] Sending image to Ollama");
|
||||||
sendToRenderer('update-status', 'Analyzing image...');
|
sendToRenderer("update-status", "Analyzing image...");
|
||||||
|
|
||||||
const userMessage = {
|
const userMessage = {
|
||||||
role: 'user',
|
role: "user",
|
||||||
content: prompt,
|
content: prompt,
|
||||||
images: [base64Data],
|
images: [base64Data],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store text-only version in history
|
// Store text-only version in history
|
||||||
localConversationHistory.push({ role: 'user', content: prompt });
|
localConversationHistory.push({ role: "user", content: prompt });
|
||||||
|
|
||||||
if (localConversationHistory.length > 20) {
|
if (localConversationHistory.length > 20) {
|
||||||
localConversationHistory = localConversationHistory.slice(-20);
|
localConversationHistory = localConversationHistory.slice(-20);
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: 'system', content: currentSystemPrompt || 'You are a helpful assistant.' },
|
{
|
||||||
|
role: "system",
|
||||||
|
content: currentSystemPrompt || "You are a helpful assistant.",
|
||||||
|
},
|
||||||
...localConversationHistory.slice(0, -1),
|
...localConversationHistory.slice(0, -1),
|
||||||
userMessage,
|
userMessage,
|
||||||
];
|
];
|
||||||
@@ -400,29 +814,32 @@ async function sendLocalImage(base64Data, prompt) {
|
|||||||
stream: true,
|
stream: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
let fullText = '';
|
let fullText = "";
|
||||||
let isFirst = true;
|
let isFirst = true;
|
||||||
|
|
||||||
for await (const part of response) {
|
for await (const part of response) {
|
||||||
const token = part.message?.content || '';
|
const token = part.message?.content || "";
|
||||||
if (token) {
|
if (token) {
|
||||||
fullText += token;
|
fullText += token;
|
||||||
sendToRenderer(isFirst ? 'new-response' : 'update-response', fullText);
|
sendToRenderer(isFirst ? "new-response" : "update-response", fullText);
|
||||||
isFirst = false;
|
isFirst = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fullText.trim()) {
|
if (fullText.trim()) {
|
||||||
localConversationHistory.push({ role: 'assistant', content: fullText.trim() });
|
localConversationHistory.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: fullText.trim(),
|
||||||
|
});
|
||||||
saveConversationTurn(prompt, fullText);
|
saveConversationTurn(prompt, fullText);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[LocalAI] Image response completed');
|
console.log("[LocalAI] Image response completed");
|
||||||
sendToRenderer('update-status', 'Listening...');
|
sendToRenderer("update-status", "Listening...");
|
||||||
return { success: true, text: fullText, model: ollamaModel };
|
return { success: true, text: fullText, model: ollamaModel };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LocalAI] Image error:', error);
|
console.error("[LocalAI] Image error:", error);
|
||||||
sendToRenderer('update-status', 'Ollama error: ' + error.message);
|
sendToRenderer("update-status", "Ollama error: " + error.message);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
+69
-48
@@ -1,13 +1,37 @@
|
|||||||
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)`,
|
||||||
|
|
||||||
|
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:**
|
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
|
||||||
@@ -39,13 +63,6 @@ Provide only the exact words to say in **markdown format**. No coaching, no "you
|
|||||||
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:**
|
|
||||||
- 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:**
|
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
|
||||||
@@ -70,13 +87,6 @@ Provide only the exact words to say in **markdown format**. Be persuasive but no
|
|||||||
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:**
|
|
||||||
- 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:**
|
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
|
||||||
@@ -101,13 +111,6 @@ Provide only the exact words to say in **markdown format**. Be clear, concise, a
|
|||||||
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:**
|
|
||||||
- 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:**
|
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
|
||||||
@@ -132,13 +135,6 @@ Provide only the exact words to say in **markdown format**. Be confident, engagi
|
|||||||
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:**
|
|
||||||
- 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:**
|
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
|
||||||
@@ -163,13 +159,6 @@ Provide only the exact words to say in **markdown format**. Focus on finding win
|
|||||||
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:**
|
|
||||||
- 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:**
|
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
|
||||||
@@ -201,25 +190,57 @@ Provide direct exam answers in **markdown format**. Include the question text, t
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
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(
|
||||||
|
profile,
|
||||||
|
customPrompt = "",
|
||||||
|
googleSearchEnabled = true,
|
||||||
|
responseMode = "brief",
|
||||||
|
) {
|
||||||
const promptParts = profilePrompts[profile] || profilePrompts.interview;
|
const promptParts = profilePrompts[profile] || profilePrompts.interview;
|
||||||
return buildSystemPrompt(promptParts, customPrompt, googleSearchEnabled);
|
return buildSystemPrompt(
|
||||||
|
promptParts,
|
||||||
|
customPrompt,
|
||||||
|
googleSearchEnabled,
|
||||||
|
responseMode,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
profilePrompts,
|
profilePrompts,
|
||||||
|
responseModeFormats,
|
||||||
|
codingAwareness,
|
||||||
getSystemPrompt,
|
getSystemPrompt,
|
||||||
};
|
};
|
||||||
|
|||||||
+426
-269
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" });
|
||||||
+131
-70
@@ -1,6 +1,6 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -20,21 +20,21 @@ function createWindow(sendToRenderer, geminiSessionRef) {
|
|||||||
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);
|
||||||
@@ -42,20 +42,20 @@ function createWindow(sendToRenderer, geminiSessionRef) {
|
|||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,14 +66,14 @@ function createWindow(sendToRenderer, geminiSessionRef) {
|
|||||||
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;
|
||||||
@@ -84,7 +84,12 @@ function createWindow(sendToRenderer, geminiSessionRef) {
|
|||||||
keybinds = { ...defaultKeybinds, ...savedKeybinds };
|
keybinds = { ...defaultKeybinds, ...savedKeybinds };
|
||||||
}
|
}
|
||||||
|
|
||||||
updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessionRef);
|
updateGlobalShortcuts(
|
||||||
|
keybinds,
|
||||||
|
mainWindow,
|
||||||
|
sendToRenderer,
|
||||||
|
geminiSessionRef,
|
||||||
|
);
|
||||||
}, 150);
|
}, 150);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,25 +99,31 @@ function createWindow(sendToRenderer, geminiSessionRef) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
@@ -146,7 +157,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -170,7 +181,10 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
});
|
});
|
||||||
console.log(`Registered toggleVisibility: ${keybinds.toggleVisibility}`);
|
console.log(`Registered toggleVisibility: ${keybinds.toggleVisibility}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`, error);
|
console.error(
|
||||||
|
`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,16 +195,24 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
mouseEventsIgnored = !mouseEventsIgnored;
|
mouseEventsIgnored = !mouseEventsIgnored;
|
||||||
if (mouseEventsIgnored) {
|
if (mouseEventsIgnored) {
|
||||||
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||||
console.log('Mouse events ignored');
|
console.log("Mouse events ignored");
|
||||||
} else {
|
} else {
|
||||||
mainWindow.setIgnoreMouseEvents(false);
|
mainWindow.setIgnoreMouseEvents(false);
|
||||||
console.log('Mouse events enabled');
|
console.log("Mouse events enabled");
|
||||||
}
|
}
|
||||||
mainWindow.webContents.send('click-through-toggled', mouseEventsIgnored);
|
mainWindow.webContents.send(
|
||||||
|
"click-through-toggled",
|
||||||
|
mouseEventsIgnored,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
console.log(`Registered toggleClickThrough: ${keybinds.toggleClickThrough}`);
|
console.log(
|
||||||
|
`Registered toggleClickThrough: ${keybinds.toggleClickThrough}`,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register toggleClickThrough (${keybinds.toggleClickThrough}):`, error);
|
console.error(
|
||||||
|
`Failed to register toggleClickThrough (${keybinds.toggleClickThrough}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,23 +220,26 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
if (keybinds.nextStep) {
|
if (keybinds.nextStep) {
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(keybinds.nextStep, async () => {
|
globalShortcut.register(keybinds.nextStep, async () => {
|
||||||
console.log('Next step shortcut triggered');
|
console.log("Next step shortcut triggered");
|
||||||
try {
|
try {
|
||||||
// Determine the shortcut key format
|
// Determine the shortcut key format
|
||||||
const isMac = process.platform === 'darwin';
|
const isMac = process.platform === "darwin";
|
||||||
const shortcutKey = isMac ? 'cmd+enter' : 'ctrl+enter';
|
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) {
|
} catch (error) {
|
||||||
console.error('Error handling next step shortcut:', error);
|
console.error("Error handling next step shortcut:", error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log(`Registered nextStep: ${keybinds.nextStep}`);
|
console.log(`Registered nextStep: ${keybinds.nextStep}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register nextStep (${keybinds.nextStep}):`, error);
|
console.error(
|
||||||
|
`Failed to register nextStep (${keybinds.nextStep}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,12 +247,15 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,12 +263,15 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,12 +279,15 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,12 +295,31 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
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 expand response shortcut
|
||||||
|
if (keybinds.expandResponse) {
|
||||||
|
try {
|
||||||
|
globalShortcut.register(keybinds.expandResponse, () => {
|
||||||
|
console.log("Expand response shortcut triggered");
|
||||||
|
sendToRenderer("expand-response");
|
||||||
|
});
|
||||||
|
console.log(`Registered expandResponse: ${keybinds.expandResponse}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to register expandResponse (${keybinds.expandResponse}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +327,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
if (keybinds.emergencyErase) {
|
if (keybinds.emergencyErase) {
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(keybinds.emergencyErase, () => {
|
globalShortcut.register(keybinds.emergencyErase, () => {
|
||||||
console.log('Emergency Erase triggered!');
|
console.log("Emergency Erase triggered!");
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.hide();
|
mainWindow.hide();
|
||||||
|
|
||||||
@@ -283,28 +336,31 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
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}`);
|
console.log(`Registered emergencyErase: ${keybinds.emergencyErase}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register emergencyErase (${keybinds.emergencyErase}):`, 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;
|
||||||
@@ -323,22 +379,27 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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()) {
|
||||||
@@ -348,12 +409,12 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
|
|||||||
}
|
}
|
||||||
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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user