Refactor window management and global shortcuts handling

This commit is contained in:
Илья Глазунов 2026-02-15 00:34:37 +03:00
parent 494e692738
commit 4cf48ee0af
7 changed files with 5415 additions and 4431 deletions

View File

@ -1,12 +1,12 @@
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
import { MainView } from '../views/MainView.js';
import { CustomizeView } from '../views/CustomizeView.js';
import { HelpView } from '../views/HelpView.js';
import { HistoryView } from '../views/HistoryView.js';
import { AssistantView } from '../views/AssistantView.js';
import { OnboardingView } from '../views/OnboardingView.js';
import { AICustomizeView } from '../views/AICustomizeView.js';
import { FeedbackView } from '../views/FeedbackView.js';
import { html, css, LitElement } from "../../assets/lit-core-2.7.4.min.js";
import { MainView } from "../views/MainView.js";
import { CustomizeView } from "../views/CustomizeView.js";
import { HelpView } from "../views/HelpView.js";
import { HistoryView } from "../views/HistoryView.js";
import { AssistantView } from "../views/AssistantView.js";
import { OnboardingView } from "../views/OnboardingView.js";
import { AICustomizeView } from "../views/AICustomizeView.js";
import { FeedbackView } from "../views/FeedbackView.js";
export class CheatingDaddyApp extends LitElement {
static styles = css`
@ -81,15 +81,15 @@ export class CheatingDaddyApp extends LitElement {
}
.traffic-light.close {
background: #FF5F57;
background: #ff5f57;
}
.traffic-light.minimize {
background: #FEBC2E;
background: #febc2e;
}
.traffic-light.maximize {
background: #28C840;
background: #28c840;
}
.sidebar {
@ -100,7 +100,10 @@ export class CheatingDaddyApp extends LitElement {
display: flex;
flex-direction: column;
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 {
@ -144,7 +147,9 @@ export class CheatingDaddyApp extends LitElement {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
cursor: pointer;
transition: color var(--transition), background var(--transition);
transition:
color var(--transition),
background var(--transition);
border: none;
background: none;
width: 100%;
@ -187,7 +192,9 @@ export class CheatingDaddyApp extends LitElement {
font-weight: var(--font-weight-medium);
cursor: pointer;
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;
}
@ -197,11 +204,23 @@ export class CheatingDaddyApp extends LitElement {
}
@keyframes update-wobble {
0%, 90%, 100% { transform: rotate(0deg); }
92% { transform: rotate(-2deg); }
94% { transform: rotate(2deg); }
96% { transform: rotate(-1.5deg); }
98% { transform: rotate(1.5deg); }
0%,
90%,
100% {
transform: rotate(0deg);
}
92% {
transform: rotate(-2deg);
}
94% {
transform: rotate(2deg);
}
96% {
transform: rotate(-1.5deg);
}
98% {
transform: rotate(1.5deg);
}
}
.update-btn svg {
@ -367,16 +386,16 @@ export class CheatingDaddyApp extends LitElement {
constructor() {
super();
this.currentView = 'main';
this.statusText = '';
this.currentView = "main";
this.statusText = "";
this.startTime = null;
this.isRecording = false;
this.sessionActive = false;
this.selectedProfile = 'interview';
this.selectedLanguage = 'en-US';
this.selectedScreenshotInterval = '5';
this.selectedImageQuality = 'medium';
this.layoutMode = 'normal';
this.selectedProfile = "interview";
this.selectedLanguage = "en-US";
this.selectedScreenshotInterval = "5";
this.selectedImageQuality = "medium";
this.layoutMode = "normal";
this.responses = [];
this.currentResponseIndex = -1;
this._viewInstances = new Map();
@ -388,7 +407,7 @@ export class CheatingDaddyApp extends LitElement {
this._timerInterval = null;
this._updateAvailable = false;
this._whisperDownloading = false;
this._localVersion = '';
this._localVersion = "";
this._loadFromStorage();
this._checkForUpdates();
@ -399,16 +418,22 @@ export class CheatingDaddyApp extends LitElement {
this._localVersion = await cheatingDaddy.getVersion();
this.requestUpdate();
const res = await fetch('https://raw.githubusercontent.com/ShiftyX1/Mastermind/refs/heads/master/package.json');
const res = await fetch(
"https://raw.githubusercontent.com/ShiftyX1/Mastermind/refs/heads/master/package.json",
);
if (!res.ok) return;
const remote = await res.json();
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 [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.requestUpdate();
}
@ -421,20 +446,20 @@ export class CheatingDaddyApp extends LitElement {
try {
const [config, prefs] = await Promise.all([
cheatingDaddy.storage.getConfig(),
cheatingDaddy.storage.getPreferences()
cheatingDaddy.storage.getPreferences(),
]);
this.currentView = config.onboarded ? 'main' : 'onboarding';
this.selectedProfile = prefs.selectedProfile || 'interview';
this.selectedLanguage = prefs.selectedLanguage || 'en-US';
this.selectedScreenshotInterval = prefs.selectedScreenshotInterval || '5';
this.selectedImageQuality = prefs.selectedImageQuality || 'medium';
this.layoutMode = config.layout || 'normal';
this.currentView = config.onboarded ? "main" : "onboarding";
this.selectedProfile = prefs.selectedProfile || "interview";
this.selectedLanguage = prefs.selectedLanguage || "en-US";
this.selectedScreenshotInterval = prefs.selectedScreenshotInterval || "5";
this.selectedImageQuality = prefs.selectedImageQuality || "medium";
this.layoutMode = config.layout || "normal";
this._storageLoaded = true;
this.requestUpdate();
} catch (error) {
console.error('Error loading from storage:', error);
console.error("Error loading from storage:", error);
this._storageLoaded = true;
this.requestUpdate();
}
@ -444,13 +469,23 @@ export class CheatingDaddyApp extends LitElement {
super.connectedCallback();
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.on('new-response', (_, response) => this.addNewResponse(response));
ipcRenderer.on('update-response', (_, response) => this.updateCurrentResponse(response));
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; });
const { ipcRenderer } = window.require("electron");
ipcRenderer.on("new-response", (_, response) =>
this.addNewResponse(response),
);
ipcRenderer.on("update-response", (_, response) =>
this.updateCurrentResponse(response),
);
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;
});
}
}
@ -458,13 +493,13 @@ export class CheatingDaddyApp extends LitElement {
super.disconnectedCallback();
this._stopTimer();
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.removeAllListeners('new-response');
ipcRenderer.removeAllListeners('update-response');
ipcRenderer.removeAllListeners('update-status');
ipcRenderer.removeAllListeners('click-through-toggled');
ipcRenderer.removeAllListeners('reconnect-failed');
ipcRenderer.removeAllListeners('whisper-downloading');
const { ipcRenderer } = window.require("electron");
ipcRenderer.removeAllListeners("new-response");
ipcRenderer.removeAllListeners("update-response");
ipcRenderer.removeAllListeners("update-status");
ipcRenderer.removeAllListeners("click-through-toggled");
ipcRenderer.removeAllListeners("reconnect-failed");
ipcRenderer.removeAllListeners("whisper-downloading");
}
}
@ -485,12 +520,12 @@ export class CheatingDaddyApp extends LitElement {
}
getElapsedTime() {
if (!this.startTime) return '0:00';
if (!this.startTime) return "0:00";
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
const h = Math.floor(elapsed / 3600);
const m = Math.floor((elapsed % 3600) / 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)}`;
return `${m}:${pad(s)}`;
}
@ -499,7 +534,11 @@ export class CheatingDaddyApp extends LitElement {
setStatus(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;
}
}
@ -531,34 +570,34 @@ export class CheatingDaddyApp extends LitElement {
}
async handleClose() {
if (this.currentView === 'assistant') {
if (this.currentView === "assistant") {
cheatingDaddy.stopCapture();
if (window.require) {
const { ipcRenderer } = window.require('electron');
await ipcRenderer.invoke('close-session');
const { ipcRenderer } = window.require("electron");
await ipcRenderer.invoke("close-session");
}
this.sessionActive = false;
this._stopTimer();
this.currentView = 'main';
this.currentView = "main";
} else {
if (window.require) {
const { ipcRenderer } = window.require('electron');
await ipcRenderer.invoke('quit-application');
const { ipcRenderer } = window.require("electron");
await ipcRenderer.invoke("quit-application");
}
}
}
async _handleMinimize() {
if (window.require) {
const { ipcRenderer } = window.require('electron');
await ipcRenderer.invoke('window-minimize');
const { ipcRenderer } = window.require("electron");
await ipcRenderer.invoke("window-minimize");
}
}
async handleHideToggle() {
if (window.require) {
const { ipcRenderer } = window.require('electron');
await ipcRenderer.invoke('toggle-window-visibility');
const { ipcRenderer } = window.require("electron");
await ipcRenderer.invoke("toggle-window-visibility");
}
}
@ -566,12 +605,12 @@ export class CheatingDaddyApp extends LitElement {
async handleStart() {
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);
if (!success) {
const mainView = this.shadowRoot.querySelector('main-view');
const mainView = this.shadowRoot.querySelector("main-view");
if (mainView && mainView.triggerApiKeyError) {
mainView.triggerApiKeyError();
}
@ -579,37 +618,49 @@ export class CheatingDaddyApp extends LitElement {
}
} else {
const apiKey = await cheatingDaddy.storage.getApiKey();
if (!apiKey || apiKey === '') {
const mainView = this.shadowRoot.querySelector('main-view');
if (!apiKey || apiKey === "") {
const mainView = this.shadowRoot.querySelector("main-view");
if (mainView && mainView.triggerApiKeyError) {
mainView.triggerApiKeyError();
}
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.currentResponseIndex = -1;
this.startTime = Date.now();
this.sessionActive = true;
this.currentView = 'assistant';
this.currentView = "assistant";
this._startTimer();
}
async handleAPIKeyHelp() {
if (window.require) {
const { ipcRenderer } = window.require('electron');
await ipcRenderer.invoke('open-external', 'https://cheatingdaddy.com/help/api-key');
const { ipcRenderer } = window.require("electron");
await ipcRenderer.invoke(
"open-external",
"https://cheatingdaddy.com/help/api-key",
);
}
}
async handleGroqAPIKeyHelp() {
if (window.require) {
const { ipcRenderer } = window.require('electron');
await ipcRenderer.invoke('open-external', 'https://console.groq.com/keys');
const { ipcRenderer } = window.require("electron");
await ipcRenderer.invoke(
"open-external",
"https://console.groq.com/keys",
);
}
}
@ -617,33 +668,39 @@ export class CheatingDaddyApp extends LitElement {
async handleProfileChange(profile) {
this.selectedProfile = profile;
await cheatingDaddy.storage.updatePreference('selectedProfile', profile);
await cheatingDaddy.storage.updatePreference("selectedProfile", profile);
}
async handleLanguageChange(language) {
this.selectedLanguage = language;
await cheatingDaddy.storage.updatePreference('selectedLanguage', language);
await cheatingDaddy.storage.updatePreference("selectedLanguage", language);
}
async handleScreenshotIntervalChange(interval) {
this.selectedScreenshotInterval = interval;
await cheatingDaddy.storage.updatePreference('selectedScreenshotInterval', interval);
await cheatingDaddy.storage.updatePreference(
"selectedScreenshotInterval",
interval,
);
}
async handleImageQualityChange(quality) {
this.selectedImageQuality = quality;
await cheatingDaddy.storage.updatePreference('selectedImageQuality', quality);
await cheatingDaddy.storage.updatePreference(
"selectedImageQuality",
quality,
);
}
async handleLayoutModeChange(layoutMode) {
this.layoutMode = layoutMode;
await cheatingDaddy.storage.updateConfig('layout', layoutMode);
await cheatingDaddy.storage.updateConfig("layout", layoutMode);
if (window.require) {
try {
const { ipcRenderer } = window.require('electron');
await ipcRenderer.invoke('update-sizes');
const { ipcRenderer } = window.require("electron");
await ipcRenderer.invoke("update-sizes");
} catch (error) {
console.error('Failed to update sizes:', error);
console.error("Failed to update sizes:", error);
}
}
this.requestUpdate();
@ -651,17 +708,27 @@ export class CheatingDaddyApp extends LitElement {
async handleExternalLinkClick(url) {
if (window.require) {
const { ipcRenderer } = window.require('electron');
await ipcRenderer.invoke('open-external', url);
const { ipcRenderer } = window.require("electron");
await ipcRenderer.invoke("open-external", url);
}
}
async handleSendText(message) {
const result = await window.cheatingDaddy.sendTextMessage(message);
if (!result.success) {
this.setStatus('Error sending message: ' + result.error);
this.setStatus("Error sending message: " + result.error);
} 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;
}
}
@ -673,29 +740,29 @@ export class CheatingDaddyApp extends LitElement {
}
handleOnboardingComplete() {
this.currentView = 'main';
this.currentView = "main";
}
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('currentView') && window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.send('view-changed', this.currentView);
if (changedProperties.has("currentView") && window.require) {
const { ipcRenderer } = window.require("electron");
ipcRenderer.send("view-changed", this.currentView);
}
}
// ── Helpers ──
_isLiveMode() {
return this.currentView === 'assistant';
return this.currentView === "assistant";
}
// ── Render ──
renderCurrentView() {
switch (this.currentView) {
case 'onboarding':
case "onboarding":
return html`
<onboarding-view
.onComplete=${() => this.handleOnboardingComplete()}
@ -703,26 +770,26 @@ export class CheatingDaddyApp extends LitElement {
></onboarding-view>
`;
case 'main':
case "main":
return html`
<main-view
.selectedProfile=${this.selectedProfile}
.onProfileChange=${p => this.handleProfileChange(p)}
.onProfileChange=${(p) => this.handleProfileChange(p)}
.onStart=${() => this.handleStart()}
.onExternalLink=${url => this.handleExternalLinkClick(url)}
.onExternalLink=${(url) => this.handleExternalLinkClick(url)}
.whisperDownloading=${this._whisperDownloading}
></main-view>
`;
case 'ai-customize':
case "ai-customize":
return html`
<ai-customize-view
.selectedProfile=${this.selectedProfile}
.onProfileChange=${p => this.handleProfileChange(p)}
.onProfileChange=${(p) => this.handleProfileChange(p)}
></ai-customize-view>
`;
case 'customize':
case "customize":
return html`
<customize-view
.selectedProfile=${this.selectedProfile}
@ -730,30 +797,34 @@ export class CheatingDaddyApp extends LitElement {
.selectedScreenshotInterval=${this.selectedScreenshotInterval}
.selectedImageQuality=${this.selectedImageQuality}
.layoutMode=${this.layoutMode}
.onProfileChange=${p => this.handleProfileChange(p)}
.onLanguageChange=${l => this.handleLanguageChange(l)}
.onScreenshotIntervalChange=${i => this.handleScreenshotIntervalChange(i)}
.onImageQualityChange=${q => this.handleImageQualityChange(q)}
.onLayoutModeChange=${lm => this.handleLayoutModeChange(lm)}
.onProfileChange=${(p) => this.handleProfileChange(p)}
.onLanguageChange=${(l) => this.handleLanguageChange(l)}
.onScreenshotIntervalChange=${(i) =>
this.handleScreenshotIntervalChange(i)}
.onImageQualityChange=${(q) => this.handleImageQualityChange(q)}
.onLayoutModeChange=${(lm) => this.handleLayoutModeChange(lm)}
></customize-view>
`;
case 'feedback':
case "feedback":
return html`<feedback-view></feedback-view>`;
case 'help':
return html`<help-view .onExternalLinkClick=${url => this.handleExternalLinkClick(url)}></help-view>`;
case "help":
return html`<help-view
.onExternalLinkClick=${(url) => this.handleExternalLinkClick(url)}
></help-view>`;
case 'history':
case "history":
return html`<history-view></history-view>`;
case 'assistant':
case "assistant":
return html`
<assistant-view
.responses=${this.responses}
.currentResponseIndex=${this.currentResponseIndex}
.selectedProfile=${this.selectedProfile}
.onSendText=${msg => this.handleSendText(msg)}
.onSendText=${(msg) => this.handleSendText(msg)}
.onExpandResponse=${() => this.handleExpandResponse()}
.shouldAnimateResponse=${this.shouldAnimateResponse}
@response-index-changed=${this.handleResponseIndexChanged}
@response-animation-complete=${() => {
@ -771,74 +842,238 @@ export class CheatingDaddyApp extends LitElement {
renderSidebar() {
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: '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>` },
{
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: "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`
<div class="sidebar ${this._isLiveMode() ? 'hidden' : ''}">
<div class="sidebar ${this._isLiveMode() ? "hidden" : ""}">
<div class="sidebar-brand">
<h1>Mastermind</h1>
</div>
<nav class="sidebar-nav">
${items.map(item => html`
${items.map(
(item) => html`
<button
class="nav-item ${this.currentView === item.id ? 'active' : ''}"
class="nav-item ${this.currentView === item.id ? "active" : ""}"
@click=${() => this.navigate(item.id)}
title=${item.label}
>
${item.icon}
${item.label}
${item.icon} ${item.label}
</button>
`)}
`,
)}
</nav>
<div class="sidebar-footer">
${this._updateAvailable ? html`
<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>
${this._updateAvailable
? html`
<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
</button>
` : html`
<div class="version-text">v${this._localVersion}</div>
`}
`
: html` <div class="version-text">v${this._localVersion}</div> `}
</div>
</div>
`;
}
renderLiveBar() {
if (!this._isLiveMode()) return '';
if (!this._isLiveMode()) return "";
const profileLabels = {
interview: 'Interview',
sales: 'Sales Call',
meeting: 'Meeting',
presentation: 'Presentation',
negotiation: 'Negotiation',
exam: 'Exam',
interview: "Interview",
sales: "Sales Call",
meeting: "Meeting",
presentation: "Presentation",
negotiation: "Negotiation",
exam: "Exam",
};
return html`
<div class="live-bar">
<div class="live-bar-left">
<button class="live-bar-back" @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" />
<button
class="live-bar-back"
@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>
</button>
</div>
<div class="live-bar-center">
${profileLabels[this.selectedProfile] || 'Session'}
${profileLabels[this.selectedProfile] || "Session"}
</div>
<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>
${this._isClickThrough ? html`<span class="live-bar-text">[click through]</span>` : ''}
<span class="live-bar-text clickable" @click=${() => this.handleHideToggle()}>[hide]</span>
${this._isClickThrough
? html`<span class="live-bar-text">[click through]</span>`
: ""}
<span
class="live-bar-text clickable"
@click=${() => this.handleHideToggle()}
>[hide]</span
>
</div>
</div>
`;
@ -846,30 +1081,34 @@ export class CheatingDaddyApp extends LitElement {
render() {
// Onboarding is fullscreen, no sidebar
if (this.currentView === 'onboarding') {
return html`
<div class="fullscreen">
${this.renderCurrentView()}
</div>
`;
if (this.currentView === "onboarding") {
return html` <div class="fullscreen">${this.renderCurrentView()}</div> `;
}
const isLive = this._isLiveMode();
return html`
<div class="app-shell">
<div class="top-drag-bar ${isLive ? 'hidden' : ''}">
<div class="top-drag-bar ${isLive ? "hidden" : ""}">
<div class="traffic-lights">
<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 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>
</div>
<div class="drag-region"></div>
</div>
${this.renderSidebar()}
<div class="content">
${isLive ? this.renderLiveBar() : ''}
<div class="content-inner ${isLive ? 'live' : ''}">
${isLive ? this.renderLiveBar() : ""}
<div class="content-inner ${isLive ? "live" : ""}">
${this.renderCurrentView()}
</div>
</div>
@ -878,4 +1117,4 @@ export class CheatingDaddyApp extends LitElement {
}
}
customElements.define('cheating-daddy-app', CheatingDaddyApp);
customElements.define("cheating-daddy-app", CheatingDaddyApp);

View File

@ -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 {
static styles = css`
@ -54,12 +54,22 @@ export class AssistantView extends LitElement {
font-weight: var(--font-weight-semibold);
}
.response-container h1 { font-size: 1.5em; }
.response-container h2 { font-size: 1.3em; }
.response-container h3 { font-size: 1.15em; }
.response-container h4 { font-size: 1.05em; }
.response-container h1 {
font-size: 1.5em;
}
.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 h6 { font-size: 1em; }
.response-container h6 {
font-size: 1em;
}
.response-container p {
margin: 0.6em 0;
@ -263,7 +273,9 @@ export class AssistantView extends LitElement {
display: flex;
align-items: center;
gap: 4px;
transition: border-color 0.4s ease, background var(--transition);
transition:
border-color 0.4s ease,
background var(--transition);
flex-shrink: 0;
overflow: hidden;
}
@ -298,6 +310,52 @@ export class AssistantView extends LitElement {
height: calc(100% + 2px);
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 = {
@ -305,28 +363,32 @@ export class AssistantView extends LitElement {
currentResponseIndex: { type: Number },
selectedProfile: { type: String },
onSendText: { type: Function },
onExpandResponse: { type: Function },
shouldAnimateResponse: { type: Boolean },
isAnalyzing: { type: Boolean, state: true },
isExpanding: { type: Boolean, state: true },
};
constructor() {
super();
this.responses = [];
this.currentResponseIndex = -1;
this.selectedProfile = 'interview';
this.selectedProfile = "interview";
this.onSendText = () => {};
this.onExpandResponse = () => {};
this.isAnalyzing = false;
this.isExpanding = false;
this._animFrame = null;
}
getProfileNames() {
return {
interview: 'Job Interview',
sales: 'Sales Call',
meeting: 'Business Meeting',
presentation: 'Presentation',
negotiation: 'Negotiation',
exam: 'Exam Assistant',
interview: "Job Interview",
sales: "Sales Call",
meeting: "Business Meeting",
presentation: "Presentation",
negotiation: "Negotiation",
exam: "Exam Assistant",
};
}
@ -334,11 +396,11 @@ export class AssistantView extends LitElement {
const profileNames = this.getProfileNames();
return this.responses.length > 0 && this.currentResponseIndex >= 0
? this.responses[this.currentResponseIndex]
: `Listening to your ${profileNames[this.selectedProfile] || 'session'}...`;
: `Listening to your ${profileNames[this.selectedProfile] || "session"}...`;
}
renderMarkdown(content) {
if (typeof window !== 'undefined' && window.marked) {
if (typeof window !== "undefined" && window.marked) {
try {
window.marked.setOptions({
breaks: true,
@ -349,7 +411,7 @@ export class AssistantView extends LitElement {
rendered = this.wrapWordsInSpans(rendered);
return rendered;
} catch (error) {
console.warn('Error parsing markdown:', error);
console.warn("Error parsing markdown:", error);
return content;
}
}
@ -358,17 +420,21 @@ export class AssistantView extends LitElement {
wrapWordsInSpans(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const tagsToSkip = ['PRE'];
const doc = parser.parseFromString(html, "text/html");
const tagsToSkip = ["PRE"];
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 frag = document.createDocumentFragment();
words.forEach(word => {
words.forEach((word) => {
if (word.trim()) {
const span = document.createElement('span');
span.setAttribute('data-word', '');
const span = document.createElement("span");
span.setAttribute("data-word", "");
span.textContent = word;
frag.appendChild(span);
} else {
@ -376,7 +442,10 @@ export class AssistantView extends LitElement {
}
});
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);
}
}
@ -388,9 +457,9 @@ export class AssistantView extends LitElement {
if (this.currentResponseIndex > 0) {
this.currentResponseIndex--;
this.dispatchEvent(
new CustomEvent('response-index-changed', {
new CustomEvent("response-index-changed", {
detail: { index: this.currentResponseIndex },
})
}),
);
this.requestUpdate();
}
@ -400,16 +469,16 @@ export class AssistantView extends LitElement {
if (this.currentResponseIndex < this.responses.length - 1) {
this.currentResponseIndex++;
this.dispatchEvent(
new CustomEvent('response-index-changed', {
new CustomEvent("response-index-changed", {
detail: { index: this.currentResponseIndex },
})
}),
);
this.requestUpdate();
}
}
scrollResponseUp() {
const container = this.shadowRoot.querySelector('.response-container');
const container = this.shadowRoot.querySelector(".response-container");
if (container) {
const scrollAmount = container.clientHeight * 0.3;
container.scrollTop = Math.max(0, container.scrollTop - scrollAmount);
@ -417,10 +486,13 @@ export class AssistantView extends LitElement {
}
scrollResponseDown() {
const container = this.shadowRoot.querySelector('.response-container');
const container = this.shadowRoot.querySelector(".response-container");
if (container) {
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 +500,19 @@ export class AssistantView extends LitElement {
super.connectedCallback();
if (window.require) {
const { ipcRenderer } = window.require('electron');
const { ipcRenderer } = window.require("electron");
this.handlePreviousResponse = () => this.navigateToPreviousResponse();
this.handleNextResponse = () => this.navigateToNextResponse();
this.handleScrollUp = () => this.scrollResponseUp();
this.handleScrollDown = () => this.scrollResponseDown();
this.handleExpandHotkey = () => this.handleExpandResponse();
ipcRenderer.on('navigate-previous-response', this.handlePreviousResponse);
ipcRenderer.on('navigate-next-response', this.handleNextResponse);
ipcRenderer.on('scroll-response-up', this.handleScrollUp);
ipcRenderer.on('scroll-response-down', this.handleScrollDown);
ipcRenderer.on("navigate-previous-response", this.handlePreviousResponse);
ipcRenderer.on("navigate-next-response", this.handleNextResponse);
ipcRenderer.on("scroll-response-up", this.handleScrollUp);
ipcRenderer.on("scroll-response-down", this.handleScrollDown);
ipcRenderer.on("expand-response", this.handleExpandHotkey);
}
}
@ -447,25 +521,40 @@ export class AssistantView extends LitElement {
this._stopWaveformAnimation();
if (window.require) {
const { ipcRenderer } = window.require('electron');
if (this.handlePreviousResponse) ipcRenderer.removeListener('navigate-previous-response', 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);
const { ipcRenderer } = window.require("electron");
if (this.handlePreviousResponse)
ipcRenderer.removeListener(
"navigate-previous-response",
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() {
const textInput = this.shadowRoot.querySelector('#textInput');
const textInput = this.shadowRoot.querySelector("#textInput");
if (textInput && textInput.value.trim()) {
const message = textInput.value.trim();
textInput.value = '';
textInput.value = "";
await this.onSendText(message);
}
}
handleTextKeydown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
this.handleSendText();
}
@ -480,10 +569,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() {
const canvas = this.shadowRoot.querySelector('.analyze-canvas');
const canvas = this.shadowRoot.querySelector(".analyze-canvas");
if (!canvas) return;
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext("2d");
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
@ -491,7 +592,8 @@ export class AssistantView extends LitElement {
canvas.height = rect.height * 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 FADE_IN = 0.5; // seconds
const PARTICLE_SPREAD = 4; // px inward from border
@ -542,7 +644,11 @@ export class AssistantView extends LitElement {
// Pre-seed random offsets for stable particles
const seeds = [];
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) => {
@ -585,13 +691,17 @@ export class AssistantView extends LitElement {
ctx.strokeStyle = dangerColor;
ctx.globalAlpha = wave.opacity * fade;
ctx.lineWidth = wave.width;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineCap = "round";
ctx.lineJoin = "round";
for (let x = 0; x <= w; x++) {
const norm = x / w;
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);
else ctx.lineTo(x, y);
}
@ -610,16 +720,16 @@ export class AssistantView extends LitElement {
cancelAnimationFrame(this._animFrame);
this._animFrame = null;
}
const canvas = this.shadowRoot.querySelector('.analyze-canvas');
const canvas = this.shadowRoot.querySelector(".analyze-canvas");
if (canvas) {
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
scrollToBottom() {
setTimeout(() => {
const container = this.shadowRoot.querySelector('.response-container');
const container = this.shadowRoot.querySelector(".response-container");
if (container) {
container.scrollTop = container.scrollHeight;
}
@ -633,11 +743,14 @@ export class AssistantView extends LitElement {
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('responses') || changedProperties.has('currentResponseIndex')) {
if (
changedProperties.has("responses") ||
changedProperties.has("currentResponseIndex")
) {
this.updateResponseContent();
}
if (changedProperties.has('isAnalyzing')) {
if (changedProperties.has("isAnalyzing")) {
if (this.isAnalyzing) {
this._startWaveformAnimation();
} else {
@ -645,46 +758,116 @@ 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) {
this.isAnalyzing = false;
this.isExpanding = false;
}
}
}
updateResponseContent() {
const container = this.shadowRoot.querySelector('#responseContainer');
const container = this.shadowRoot.querySelector("#responseContainer");
if (container) {
const currentResponse = this.getCurrentResponse();
const renderedResponse = this.renderMarkdown(currentResponse);
container.innerHTML = renderedResponse;
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() {
const hasMultipleResponses = this.responses.length > 1;
const hasResponse =
this.responses.length > 0 && this.currentResponseIndex >= 0;
return html`
<div class="response-container" id="responseContainer"></div>
${hasMultipleResponses ? html`
${hasMultipleResponses || hasResponse
? html`
<div class="response-nav">
<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" />
${hasMultipleResponses
? html`
<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>
</button>
<span class="response-counter">${this.currentResponseIndex + 1} of ${this.responses.length}</span>
<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" />
<span class="response-counter"
>${this.currentResponseIndex + 1} of
${this.responses.length}</span
>
<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>
</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 class="input-bar">
<div class="input-bar-inner">
@ -695,11 +878,26 @@ export class AssistantView extends LitElement {
@keydown=${this.handleTextKeydown}
/>
</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>
<span class="analyze-btn-content">
<svg 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
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>
Analyze Screen
</span>
@ -709,4 +907,4 @@ export class AssistantView extends LitElement {
}
}
customElements.define('assistant-view', AssistantView);
customElements.define("assistant-view", AssistantView);

View File

@ -1,5 +1,5 @@
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
import { unifiedPageStyles } from './sharedPageStyles.js';
import { html, css, LitElement } from "../../assets/lit-core-2.7.4.min.js";
import { unifiedPageStyles } from "./sharedPageStyles.js";
export class CustomizeView extends LitElement {
static styles = [
@ -22,7 +22,7 @@ export class CustomizeView extends LitElement {
}
.warning-callout::before {
content: '';
content: "";
position: absolute;
top: -6px;
left: 16px;
@ -198,26 +198,26 @@ export class CustomizeView extends LitElement {
constructor() {
super();
this.selectedProfile = 'interview';
this.selectedLanguage = 'en-US';
this.selectedImageQuality = 'medium';
this.layoutMode = 'normal';
this.selectedProfile = "interview";
this.selectedLanguage = "en-US";
this.selectedImageQuality = "medium";
this.layoutMode = "normal";
this.keybinds = this.getDefaultKeybinds();
this.onProfileChange = () => {};
this.onLanguageChange = () => {};
this.onImageQualityChange = () => {};
this.onLayoutModeChange = () => {};
this.googleSearchEnabled = true;
this.providerMode = 'byok';
this.providerMode = "byok";
this.isClearing = false;
this.isRestoring = false;
this.clearStatusMessage = '';
this.clearStatusType = '';
this.clearStatusMessage = "";
this.clearStatusType = "";
this.backgroundTransparency = 0.8;
this.fontSize = 20;
this.audioMode = 'speaker_only';
this.customPrompt = '';
this.theme = 'dark';
this.audioMode = "speaker_only";
this.customPrompt = "";
this.theme = "dark";
this._loadFromStorage();
}
@ -227,14 +227,17 @@ export class CustomizeView extends LitElement {
async _loadFromStorage() {
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.providerMode = prefs.providerMode || 'byok';
this.providerMode = prefs.providerMode || "byok";
this.backgroundTransparency = prefs.backgroundTransparency ?? 0.8;
this.fontSize = prefs.fontSize ?? 20;
this.audioMode = prefs.audioMode ?? 'speaker_only';
this.customPrompt = prefs.customPrompt ?? '';
this.theme = prefs.theme ?? 'dark';
this.audioMode = prefs.audioMode ?? "speaker_only";
this.customPrompt = prefs.customPrompt ?? "";
this.theme = prefs.theme ?? "dark";
if (keybinds) {
this.keybinds = { ...this.getDefaultKeybinds(), ...keybinds };
}
@ -242,94 +245,144 @@ export class CustomizeView extends LitElement {
this.updateFontSize();
this.requestUpdate();
} catch (error) {
console.error('Error loading settings:', error);
console.error("Error loading settings:", error);
}
}
getProfiles() {
return [
{ value: 'interview', name: 'Job Interview' },
{ value: 'sales', name: 'Sales Call' },
{ value: 'meeting', name: 'Business Meeting' },
{ value: 'presentation', name: 'Presentation' },
{ value: 'negotiation', name: 'Negotiation' },
{ value: 'exam', name: 'Exam Assistant' },
{ value: "interview", name: "Job Interview" },
{ value: "sales", name: "Sales Call" },
{ value: "meeting", name: "Business Meeting" },
{ value: "presentation", name: "Presentation" },
{ value: "negotiation", name: "Negotiation" },
{ value: "exam", name: "Exam Assistant" },
];
}
getLanguages() {
return [
{ value: 'en-US', name: 'English (US)' },
{ value: 'en-GB', name: 'English (UK)' },
{ value: 'en-AU', name: 'English (Australia)' },
{ value: 'en-IN', name: 'English (India)' },
{ value: 'de-DE', name: 'German (Germany)' },
{ value: 'es-US', name: 'Spanish (US)' },
{ value: 'es-ES', name: 'Spanish (Spain)' },
{ value: 'fr-FR', name: 'French (France)' },
{ value: 'fr-CA', name: 'French (Canada)' },
{ value: 'hi-IN', name: 'Hindi (India)' },
{ value: 'pt-BR', name: 'Portuguese (Brazil)' },
{ value: 'ar-XA', name: 'Arabic (Generic)' },
{ value: 'id-ID', name: 'Indonesian (Indonesia)' },
{ value: 'it-IT', name: 'Italian (Italy)' },
{ value: 'ja-JP', name: 'Japanese (Japan)' },
{ value: 'tr-TR', name: 'Turkish (Turkey)' },
{ value: 'vi-VN', name: 'Vietnamese (Vietnam)' },
{ value: 'bn-IN', name: 'Bengali (India)' },
{ value: 'gu-IN', name: 'Gujarati (India)' },
{ value: 'kn-IN', name: 'Kannada (India)' },
{ value: 'ml-IN', name: 'Malayalam (India)' },
{ value: 'mr-IN', name: 'Marathi (India)' },
{ value: 'ta-IN', name: 'Tamil (India)' },
{ value: 'te-IN', name: 'Telugu (India)' },
{ value: 'nl-NL', name: 'Dutch (Netherlands)' },
{ value: 'ko-KR', name: 'Korean (South Korea)' },
{ value: 'cmn-CN', name: 'Mandarin Chinese (China)' },
{ value: 'pl-PL', name: 'Polish (Poland)' },
{ value: 'ru-RU', name: 'Russian (Russia)' },
{ value: 'th-TH', name: 'Thai (Thailand)' },
{ value: "en-US", name: "English (US)" },
{ value: "en-GB", name: "English (UK)" },
{ value: "en-AU", name: "English (Australia)" },
{ value: "en-IN", name: "English (India)" },
{ value: "de-DE", name: "German (Germany)" },
{ value: "es-US", name: "Spanish (US)" },
{ value: "es-ES", name: "Spanish (Spain)" },
{ value: "fr-FR", name: "French (France)" },
{ value: "fr-CA", name: "French (Canada)" },
{ value: "hi-IN", name: "Hindi (India)" },
{ value: "pt-BR", name: "Portuguese (Brazil)" },
{ value: "ar-XA", name: "Arabic (Generic)" },
{ value: "id-ID", name: "Indonesian (Indonesia)" },
{ value: "it-IT", name: "Italian (Italy)" },
{ value: "ja-JP", name: "Japanese (Japan)" },
{ value: "tr-TR", name: "Turkish (Turkey)" },
{ value: "vi-VN", name: "Vietnamese (Vietnam)" },
{ value: "bn-IN", name: "Bengali (India)" },
{ value: "gu-IN", name: "Gujarati (India)" },
{ value: "kn-IN", name: "Kannada (India)" },
{ value: "ml-IN", name: "Malayalam (India)" },
{ value: "mr-IN", name: "Marathi (India)" },
{ value: "ta-IN", name: "Tamil (India)" },
{ value: "te-IN", name: "Telugu (India)" },
{ value: "nl-NL", name: "Dutch (Netherlands)" },
{ value: "ko-KR", name: "Korean (South Korea)" },
{ value: "cmn-CN", name: "Mandarin Chinese (China)" },
{ value: "pl-PL", name: "Polish (Poland)" },
{ value: "ru-RU", name: "Russian (Russia)" },
{ value: "th-TH", name: "Thai (Thailand)" },
];
}
getDefaultKeybinds() {
const isMac = cheatingDaddy.isMacOS || navigator.platform.includes('Mac');
const isMac = cheatingDaddy.isMacOS || navigator.platform.includes("Mac");
return {
moveUp: isMac ? 'Alt+Up' : 'Ctrl+Up',
moveDown: isMac ? 'Alt+Down' : 'Ctrl+Down',
moveLeft: isMac ? 'Alt+Left' : 'Ctrl+Left',
moveRight: isMac ? 'Alt+Right' : 'Ctrl+Right',
toggleVisibility: isMac ? 'Cmd+\\' : 'Ctrl+\\',
toggleClickThrough: isMac ? 'Cmd+M' : 'Ctrl+M',
nextStep: isMac ? 'Cmd+Enter' : 'Ctrl+Enter',
previousResponse: isMac ? 'Cmd+[' : 'Ctrl+[',
nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]',
scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up',
scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down',
moveUp: isMac ? "Alt+Up" : "Ctrl+Up",
moveDown: isMac ? "Alt+Down" : "Ctrl+Down",
moveLeft: isMac ? "Alt+Left" : "Ctrl+Left",
moveRight: isMac ? "Alt+Right" : "Ctrl+Right",
toggleVisibility: isMac ? "Cmd+\\" : "Ctrl+\\",
toggleClickThrough: isMac ? "Cmd+M" : "Ctrl+M",
nextStep: isMac ? "Cmd+Enter" : "Ctrl+Enter",
previousResponse: isMac ? "Cmd+[" : "Ctrl+[",
nextResponse: isMac ? "Cmd+]" : "Ctrl+]",
scrollUp: isMac ? "Cmd+Shift+Up" : "Ctrl+Shift+Up",
scrollDown: isMac ? "Cmd+Shift+Down" : "Ctrl+Shift+Down",
expandResponse: isMac ? "Cmd+E" : "Ctrl+E",
};
}
getKeybindActions() {
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: '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: "moveUp",
name: "Move Window Up",
description: "Move the app window up",
},
{
key: "moveDown",
name: "Move Window Down",
description: "Move the app window down",
},
{
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() {
await cheatingDaddy.storage.setKeybinds(this.keybinds);
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.send('update-keybinds', this.keybinds);
const { ipcRenderer } = window.require("electron");
ipcRenderer.send("update-keybinds", this.keybinds);
}
}
@ -355,18 +408,24 @@ export class CustomizeView extends LitElement {
async handleCustomPromptInput(e) {
this.customPrompt = e.target.value;
await cheatingDaddy.storage.updatePreference('customPrompt', this.customPrompt);
await cheatingDaddy.storage.updatePreference(
"customPrompt",
this.customPrompt,
);
}
async handleAudioModeSelect(e) {
this.audioMode = e.target.value;
await cheatingDaddy.storage.updatePreference('audioMode', this.audioMode);
await cheatingDaddy.storage.updatePreference("audioMode", this.audioMode);
this.requestUpdate();
}
async handleProviderModeChange(e) {
this.providerMode = e.target.value;
await cheatingDaddy.storage.updatePreference('providerMode', this.providerMode);
await cheatingDaddy.storage.updatePreference(
"providerMode",
this.providerMode,
);
this.requestUpdate();
}
@ -379,13 +438,19 @@ export class CustomizeView extends LitElement {
async handleGoogleSearchChange(e) {
this.googleSearchEnabled = e.target.checked;
await cheatingDaddy.storage.updatePreference('googleSearchEnabled', this.googleSearchEnabled);
await cheatingDaddy.storage.updatePreference(
"googleSearchEnabled",
this.googleSearchEnabled,
);
if (window.require) {
try {
const { ipcRenderer } = window.require('electron');
await ipcRenderer.invoke('update-google-search-setting', this.googleSearchEnabled);
const { ipcRenderer } = window.require("electron");
await ipcRenderer.invoke(
"update-google-search-setting",
this.googleSearchEnabled,
);
} catch (error) {
console.error('Failed to notify main process:', error);
console.error("Failed to notify main process:", error);
}
}
this.requestUpdate();
@ -393,25 +458,34 @@ export class CustomizeView extends LitElement {
async handleBackgroundTransparencyChange(e) {
this.backgroundTransparency = parseFloat(e.target.value);
await cheatingDaddy.storage.updatePreference('backgroundTransparency', this.backgroundTransparency);
await cheatingDaddy.storage.updatePreference(
"backgroundTransparency",
this.backgroundTransparency,
);
this.updateBackgroundAppearance();
this.requestUpdate();
}
updateBackgroundAppearance() {
const colors = cheatingDaddy.theme.get(this.theme);
cheatingDaddy.theme.applyBackgrounds(colors.background, this.backgroundTransparency);
cheatingDaddy.theme.applyBackgrounds(
colors.background,
this.backgroundTransparency,
);
}
async handleFontSizeChange(e) {
this.fontSize = parseInt(e.target.value, 10);
await cheatingDaddy.storage.updatePreference('fontSize', this.fontSize);
await cheatingDaddy.storage.updatePreference("fontSize", this.fontSize);
this.updateFontSize();
this.requestUpdate();
}
updateFontSize() {
document.documentElement.style.setProperty('--response-font-size', `${this.fontSize}px`);
document.documentElement.style.setProperty(
"--response-font-size",
`${this.fontSize}px`,
);
}
handleKeybindChange(action, value) {
@ -421,50 +495,50 @@ export class CustomizeView extends LitElement {
}
handleKeybindFocus(e) {
e.target.placeholder = 'Press key combination...';
e.target.placeholder = "Press key combination...";
e.target.select();
}
handleKeybindInput(e) {
e.preventDefault();
const modifiers = [];
if (e.ctrlKey) modifiers.push('Ctrl');
if (e.metaKey) modifiers.push('Cmd');
if (e.altKey) modifiers.push('Alt');
if (e.shiftKey) modifiers.push('Shift');
if (e.ctrlKey) modifiers.push("Ctrl");
if (e.metaKey) modifiers.push("Cmd");
if (e.altKey) modifiers.push("Alt");
if (e.shiftKey) modifiers.push("Shift");
let mainKey = e.key;
switch (e.code) {
case 'ArrowUp':
mainKey = 'Up';
case "ArrowUp":
mainKey = "Up";
break;
case 'ArrowDown':
mainKey = 'Down';
case "ArrowDown":
mainKey = "Down";
break;
case 'ArrowLeft':
mainKey = 'Left';
case "ArrowLeft":
mainKey = "Left";
break;
case 'ArrowRight':
mainKey = 'Right';
case "ArrowRight":
mainKey = "Right";
break;
case 'Enter':
mainKey = 'Enter';
case "Enter":
mainKey = "Enter";
break;
case 'Space':
mainKey = 'Space';
case "Space":
mainKey = "Space";
break;
case 'Backslash':
mainKey = '\\';
case "Backslash":
mainKey = "\\";
break;
default:
if (e.key.length === 1) mainKey = e.key.toUpperCase();
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 keybind = [...modifiers, mainKey].join('+');
const keybind = [...modifiers, mainKey].join("+");
this.handleKeybindChange(action, keybind);
e.target.value = keybind;
e.target.blur();
@ -474,8 +548,8 @@ export class CustomizeView extends LitElement {
this.keybinds = this.getDefaultKeybinds();
await cheatingDaddy.storage.setKeybinds(null);
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.send('update-keybinds', this.keybinds);
const { ipcRenderer } = window.require("electron");
ipcRenderer.send("update-keybinds", this.keybinds);
}
this.requestUpdate();
}
@ -483,22 +557,22 @@ export class CustomizeView extends LitElement {
async restoreAllSettings() {
if (this.isRestoring) return;
this.isRestoring = true;
this.clearStatusMessage = '';
this.clearStatusType = '';
this.clearStatusMessage = "";
this.clearStatusType = "";
this.requestUpdate();
try {
// Restore all preferences to defaults
const defaults = {
customPrompt: '',
selectedProfile: 'interview',
selectedLanguage: 'en-US',
selectedScreenshotInterval: '5',
selectedImageQuality: 'medium',
audioMode: 'speaker_only',
customPrompt: "",
selectedProfile: "interview",
selectedLanguage: "en-US",
selectedScreenshotInterval: "5",
selectedImageQuality: "medium",
audioMode: "speaker_only",
fontSize: 20,
backgroundTransparency: 0.8,
googleSearchEnabled: false,
theme: 'dark',
theme: "dark",
};
for (const [key, value] of Object.entries(defaults)) {
await cheatingDaddy.storage.updatePreference(key, value);
@ -508,8 +582,8 @@ export class CustomizeView extends LitElement {
this.keybinds = this.getDefaultKeybinds();
await cheatingDaddy.storage.setKeybinds(null);
if (window.require) {
const { ipcRenderer } = window.require('electron');
ipcRenderer.send('update-keybinds', this.keybinds);
const { ipcRenderer } = window.require("electron");
ipcRenderer.send("update-keybinds", this.keybinds);
}
// Apply to local state
@ -533,12 +607,12 @@ export class CustomizeView extends LitElement {
this.updateFontSize();
await cheatingDaddy.theme.save(defaults.theme);
this.clearStatusMessage = 'All settings restored to defaults';
this.clearStatusType = 'success';
this.clearStatusMessage = "All settings restored to defaults";
this.clearStatusType = "success";
} catch (error) {
console.error('Error restoring settings:', error);
console.error("Error restoring settings:", error);
this.clearStatusMessage = `Error restoring settings: ${error.message}`;
this.clearStatusType = 'error';
this.clearStatusType = "error";
} finally {
this.isRestoring = false;
this.requestUpdate();
@ -548,28 +622,28 @@ export class CustomizeView extends LitElement {
async clearLocalData() {
if (this.isClearing) return;
this.isClearing = true;
this.clearStatusMessage = '';
this.clearStatusType = '';
this.clearStatusMessage = "";
this.clearStatusType = "";
this.requestUpdate();
try {
await cheatingDaddy.storage.clearAll();
this.clearStatusMessage = 'Successfully cleared all local data';
this.clearStatusType = 'success';
this.clearStatusMessage = "Successfully cleared all local data";
this.clearStatusType = "success";
this.requestUpdate();
setTimeout(() => {
this.clearStatusMessage = 'Closing application...';
this.clearStatusMessage = "Closing application...";
this.requestUpdate();
setTimeout(async () => {
if (window.require) {
const { ipcRenderer } = window.require('electron');
await ipcRenderer.invoke('quit-application');
const { ipcRenderer } = window.require("electron");
await ipcRenderer.invoke("quit-application");
}
}, 1000);
}, 2000);
} catch (error) {
console.error('Error clearing data:', error);
console.error("Error clearing data:", error);
this.clearStatusMessage = `Error clearing data: ${error.message}`;
this.clearStatusType = 'error';
this.clearStatusType = "error";
} finally {
this.isClearing = false;
this.requestUpdate();
@ -583,7 +657,11 @@ export class CustomizeView extends LitElement {
<div class="form-grid">
<div class="form-group">
<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="local">Local AI (Ollama)</option>
</select>
@ -600,18 +678,31 @@ export class CustomizeView extends LitElement {
<div class="form-grid">
<div class="form-group">
<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="mic_only">Microphone Only (Me)</option>
<option value="both">Both Speaker and Microphone</option>
</select>
</div>
${this.audioMode !== 'speaker_only' ? html`
<div class="warning-callout">May cause unexpected behavior. Only change this if you know what you're doing.</div>
` : ''}
${this.audioMode !== "speaker_only"
? html`
<div class="warning-callout">
May cause unexpected behavior. Only change this if you know
what you're doing.
</div>
`
: ""}
<div class="form-group">
<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="medium">Medium Quality</option>
<option value="low">Low Quality</option>
@ -629,8 +720,17 @@ export class CustomizeView extends LitElement {
<div class="form-grid">
<div class="form-group">
<label class="form-label">Speech Language</label>
<select class="control" .value=${this.selectedLanguage} @change=${this.handleLanguageSelect}>
${this.getLanguages().map(language => html`<option value=${language.value}>${language.name}</option>`)}
<select
class="control"
.value=${this.selectedLanguage}
@change=${this.handleLanguageSelect}
>
${this.getLanguages().map(
(language) =>
html`<option value=${language.value}>
${language.name}
</option>`,
)}
</select>
</div>
</div>
@ -645,14 +745,23 @@ export class CustomizeView extends LitElement {
<div class="form-grid">
<div class="form-group">
<label class="form-label">Theme</label>
<select class="control" .value=${this.theme} @change=${this.handleThemeChange}>
${this.getThemes().map(theme => html`<option value=${theme.value}>${theme.name}</option>`)}
<select
class="control"
.value=${this.theme}
@change=${this.handleThemeChange}
>
${this.getThemes().map(
(theme) =>
html`<option value=${theme.value}>${theme.name}</option>`,
)}
</select>
</div>
<div class="form-group slider-wrap">
<div class="slider-header">
<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>
<input
class="slider-input"
@ -688,7 +797,8 @@ export class CustomizeView extends LitElement {
return html`
<section class="surface">
<div class="surface-title">Keyboard Shortcuts</div>
${this.getKeybindActions().map(action => html`
${this.getKeybindActions().map(
(action) => html`
<div class="keybind-row">
<span class="keybind-name">${action.name}</span>
<input
@ -701,9 +811,16 @@ export class CustomizeView extends LitElement {
readonly
/>
</div>
`)}
`,
)}
<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>
</section>
`;
@ -714,16 +831,32 @@ export class CustomizeView extends LitElement {
<section class="surface danger-surface">
<div class="surface-title danger">Privacy and Data</div>
<div style="display:flex;gap:var(--space-sm);flex-wrap:wrap;">
<button class="danger-button" @click=${this.restoreAllSettings} ?disabled=${this.isRestoring}>
${this.isRestoring ? 'Restoring...' : 'Restore all settings'}
<button
class="danger-button"
@click=${this.restoreAllSettings}
?disabled=${this.isRestoring}
>
${this.isRestoring ? "Restoring..." : "Restore all settings"}
</button>
<button class="danger-button" @click=${this.clearLocalData} ?disabled=${this.isClearing}>
${this.isClearing ? 'Clearing...' : 'Delete all data'}
<button
class="danger-button"
@click=${this.clearLocalData}
?disabled=${this.isClearing}
>
${this.isClearing ? "Clearing..." : "Delete all data"}
</button>
</div>
${this.clearStatusMessage ? html`
<div class="status ${this.clearStatusType === 'success' ? 'success' : 'error'}">${this.clearStatusMessage}</div>
` : ''}
${this.clearStatusMessage
? html`
<div
class="status ${this.clearStatusType === "success"
? "success"
: "error"}"
>
${this.clearStatusMessage}
</div>
`
: ""}
</section>
`;
}
@ -733,16 +866,13 @@ export class CustomizeView extends LitElement {
<div class="unified-page">
<div class="unified-wrap">
<div class="page-title">Settings</div>
${this.renderAISection()}
${this.renderAudioSection()}
${this.renderLanguageSection()}
${this.renderAppearanceSection()}
${this.renderKeyboardSection()}
${this.renderPrivacySection()}
${this.renderAISection()} ${this.renderAudioSection()}
${this.renderLanguageSection()} ${this.renderAppearanceSection()}
${this.renderKeyboardSection()} ${this.renderPrivacySection()}
</div>
</div>
`;
}
}
customElements.define('customize-view', CustomizeView);
customElements.define("customize-view", CustomizeView);

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,37 @@
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.`,
formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
const responseModeFormats = {
brief: `**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`,
- 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:**
- 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: {
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:**
- 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
@ -70,13 +87,6 @@ Provide only the exact words to say in **markdown format**. Be persuasive but no
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.`,
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:**
- 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
@ -101,13 +111,6 @@ Provide only the exact words to say in **markdown format**. Be clear, concise, a
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.`,
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:**
- 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
@ -132,13 +135,6 @@ Provide only the exact words to say in **markdown format**. Be confident, engagi
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.`,
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:**
- 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
@ -163,13 +159,6 @@ Provide only the exact words to say in **markdown format**. Focus on finding win
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.`,
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:**
- 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
@ -201,25 +190,57 @@ Provide direct exam answers in **markdown format**. Include the question text, t
},
};
function buildSystemPrompt(promptParts, customPrompt = '', googleSearchEnabled = true) {
const sections = [promptParts.intro, '\n\n', promptParts.formatRequirements];
function buildSystemPrompt(
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
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;
return buildSystemPrompt(promptParts, customPrompt, googleSearchEnabled);
return buildSystemPrompt(
promptParts,
customPrompt,
googleSearchEnabled,
responseMode,
);
}
module.exports = {
profilePrompts,
responseModeFormats,
codingAwareness,
getSystemPrompt,
};

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
const { BrowserWindow, globalShortcut, ipcMain, screen } = require('electron');
const path = require('node:path');
const storage = require('../storage');
const { BrowserWindow, globalShortcut, ipcMain, screen } = require("electron");
const path = require("node:path");
const storage = require("../storage");
let mouseEventsIgnored = false;
@ -20,21 +20,21 @@ function createWindow(sendToRenderer, geminiSessionRef) {
nodeIntegration: true,
contextIsolation: false, // TODO: change to true
backgroundThrottling: false,
enableBlinkFeatures: 'GetDisplayMedia',
enableBlinkFeatures: "GetDisplayMedia",
webSecurity: true,
allowRunningInsecureContent: false,
},
backgroundColor: '#00000000',
backgroundColor: "#00000000",
});
const { session, desktopCapturer } = require('electron');
const { session, desktopCapturer } = require("electron");
session.defaultSession.setDisplayMediaRequestHandler(
(request, callback) => {
desktopCapturer.getSources({ types: ['screen'] }).then(sources => {
callback({ video: sources[0], audio: 'loopback' });
desktopCapturer.getSources({ types: ["screen"] }).then((sources) => {
callback({ video: sources[0], audio: "loopback" });
});
},
{ useSystemPicker: true }
{ useSystemPicker: true },
);
mainWindow.setResizable(false);
@ -42,20 +42,20 @@ function createWindow(sendToRenderer, geminiSessionRef) {
mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
// Hide from Windows taskbar
if (process.platform === 'win32') {
if (process.platform === "win32") {
try {
mainWindow.setSkipTaskbar(true);
} 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
if (process.platform === 'darwin') {
if (process.platform === "darwin") {
try {
mainWindow.setHiddenInMissionControl(true);
} 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;
mainWindow.setPosition(x, y);
if (process.platform === 'win32') {
mainWindow.setAlwaysOnTop(true, 'screen-saver', 1);
if (process.platform === "win32") {
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
mainWindow.webContents.once('dom-ready', () => {
mainWindow.webContents.once("dom-ready", () => {
setTimeout(() => {
const defaultKeybinds = getDefaultKeybinds();
let keybinds = defaultKeybinds;
@ -84,7 +84,12 @@ function createWindow(sendToRenderer, geminiSessionRef) {
keybinds = { ...defaultKeybinds, ...savedKeybinds };
}
updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessionRef);
updateGlobalShortcuts(
keybinds,
mainWindow,
sendToRenderer,
geminiSessionRef,
);
}, 150);
});
@ -94,25 +99,31 @@ function createWindow(sendToRenderer, geminiSessionRef) {
}
function getDefaultKeybinds() {
const isMac = process.platform === 'darwin';
const isMac = process.platform === "darwin";
return {
moveUp: isMac ? 'Alt+Up' : 'Ctrl+Up',
moveDown: isMac ? 'Alt+Down' : 'Ctrl+Down',
moveLeft: isMac ? 'Alt+Left' : 'Ctrl+Left',
moveRight: isMac ? 'Alt+Right' : 'Ctrl+Right',
toggleVisibility: isMac ? 'Cmd+\\' : 'Ctrl+\\',
toggleClickThrough: isMac ? 'Cmd+M' : 'Ctrl+M',
nextStep: isMac ? 'Cmd+Enter' : 'Ctrl+Enter',
previousResponse: isMac ? 'Cmd+[' : 'Ctrl+[',
nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]',
scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up',
scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down',
emergencyErase: isMac ? 'Cmd+Shift+E' : 'Ctrl+Shift+E',
moveUp: isMac ? "Alt+Up" : "Ctrl+Up",
moveDown: isMac ? "Alt+Down" : "Ctrl+Down",
moveLeft: isMac ? "Alt+Left" : "Ctrl+Left",
moveRight: isMac ? "Alt+Right" : "Ctrl+Right",
toggleVisibility: isMac ? "Cmd+\\" : "Ctrl+\\",
toggleClickThrough: isMac ? "Cmd+M" : "Ctrl+M",
nextStep: isMac ? "Cmd+Enter" : "Ctrl+Enter",
previousResponse: isMac ? "Cmd+[" : "Ctrl+[",
nextResponse: isMac ? "Cmd+]" : "Ctrl+]",
scrollUp: isMac ? "Cmd+Shift+Up" : "Ctrl+Shift+Up",
scrollDown: isMac ? "Cmd+Shift+Down" : "Ctrl+Shift+Down",
expandResponse: isMac ? "Cmd+E" : "Ctrl+E",
emergencyErase: isMac ? "Cmd+Shift+E" : "Ctrl+Shift+E",
};
}
function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessionRef) {
console.log('Updating global shortcuts with:', keybinds);
function updateGlobalShortcuts(
keybinds,
mainWindow,
sendToRenderer,
geminiSessionRef,
) {
console.log("Updating global shortcuts with:", keybinds);
// Unregister all existing shortcuts
globalShortcut.unregisterAll();
@ -146,7 +157,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
};
// Register each movement shortcut
Object.keys(movementActions).forEach(action => {
Object.keys(movementActions).forEach((action) => {
const keybind = keybinds[action];
if (keybind) {
try {
@ -170,7 +181,10 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
});
console.log(`Registered toggleVisibility: ${keybinds.toggleVisibility}`);
} 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;
if (mouseEventsIgnored) {
mainWindow.setIgnoreMouseEvents(true, { forward: true });
console.log('Mouse events ignored');
console.log("Mouse events ignored");
} else {
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) {
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) {
try {
globalShortcut.register(keybinds.nextStep, async () => {
console.log('Next step shortcut triggered');
console.log("Next step shortcut triggered");
try {
// Determine the shortcut key format
const isMac = process.platform === 'darwin';
const shortcutKey = isMac ? 'cmd+enter' : 'ctrl+enter';
const isMac = process.platform === "darwin";
const shortcutKey = isMac ? "cmd+enter" : "ctrl+enter";
// Use the new handleShortcut function
mainWindow.webContents.executeJavaScript(`
cheatingDaddy.handleShortcut('${shortcutKey}');
`);
} catch (error) {
console.error('Error handling next step shortcut:', error);
console.error("Error handling next step shortcut:", error);
}
});
console.log(`Registered nextStep: ${keybinds.nextStep}`);
} catch (error) {
console.error(`Failed to register nextStep (${keybinds.nextStep}):`, error);
console.error(
`Failed to register nextStep (${keybinds.nextStep}):`,
error,
);
}
}
@ -222,12 +247,15 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
if (keybinds.previousResponse) {
try {
globalShortcut.register(keybinds.previousResponse, () => {
console.log('Previous response shortcut triggered');
sendToRenderer('navigate-previous-response');
console.log("Previous response shortcut triggered");
sendToRenderer("navigate-previous-response");
});
console.log(`Registered previousResponse: ${keybinds.previousResponse}`);
} catch (error) {
console.error(`Failed to register previousResponse (${keybinds.previousResponse}):`, error);
console.error(
`Failed to register previousResponse (${keybinds.previousResponse}):`,
error,
);
}
}
@ -235,12 +263,15 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
if (keybinds.nextResponse) {
try {
globalShortcut.register(keybinds.nextResponse, () => {
console.log('Next response shortcut triggered');
sendToRenderer('navigate-next-response');
console.log("Next response shortcut triggered");
sendToRenderer("navigate-next-response");
});
console.log(`Registered nextResponse: ${keybinds.nextResponse}`);
} catch (error) {
console.error(`Failed to register nextResponse (${keybinds.nextResponse}):`, error);
console.error(
`Failed to register nextResponse (${keybinds.nextResponse}):`,
error,
);
}
}
@ -248,12 +279,15 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
if (keybinds.scrollUp) {
try {
globalShortcut.register(keybinds.scrollUp, () => {
console.log('Scroll up shortcut triggered');
sendToRenderer('scroll-response-up');
console.log("Scroll up shortcut triggered");
sendToRenderer("scroll-response-up");
});
console.log(`Registered scrollUp: ${keybinds.scrollUp}`);
} catch (error) {
console.error(`Failed to register scrollUp (${keybinds.scrollUp}):`, error);
console.error(
`Failed to register scrollUp (${keybinds.scrollUp}):`,
error,
);
}
}
@ -261,12 +295,31 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
if (keybinds.scrollDown) {
try {
globalShortcut.register(keybinds.scrollDown, () => {
console.log('Scroll down shortcut triggered');
sendToRenderer('scroll-response-down');
console.log("Scroll down shortcut triggered");
sendToRenderer("scroll-response-down");
});
console.log(`Registered scrollDown: ${keybinds.scrollDown}`);
} catch (error) {
console.error(`Failed to register scrollDown (${keybinds.scrollDown}):`, error);
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) {
try {
globalShortcut.register(keybinds.emergencyErase, () => {
console.log('Emergency Erase triggered!');
console.log("Emergency Erase triggered!");
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.hide();
@ -283,28 +336,31 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
geminiSessionRef.current = null;
}
sendToRenderer('clear-sensitive-data');
sendToRenderer("clear-sensitive-data");
setTimeout(() => {
const { app } = require('electron');
const { app } = require("electron");
app.quit();
}, 300);
}
});
console.log(`Registered emergencyErase: ${keybinds.emergencyErase}`);
} 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) {
ipcMain.on('view-changed', (event, view) => {
ipcMain.on("view-changed", (event, view) => {
if (!mainWindow.isDestroyed()) {
const primaryDisplay = screen.getPrimaryDisplay();
const { width: screenWidth } = primaryDisplay.workAreaSize;
if (view === 'assistant') {
if (view === "assistant") {
// Shrink window for live view
const liveWidth = 850;
const liveHeight = 400;
@ -323,22 +379,27 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
}
});
ipcMain.handle('window-minimize', () => {
ipcMain.handle("window-minimize", () => {
if (!mainWindow.isDestroyed()) {
mainWindow.minimize();
}
});
ipcMain.on('update-keybinds', (event, newKeybinds) => {
ipcMain.on("update-keybinds", (event, newKeybinds) => {
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 {
if (mainWindow.isDestroyed()) {
return { success: false, error: 'Window has been destroyed' };
return { success: false, error: "Window has been destroyed" };
}
if (mainWindow.isVisible()) {
@ -348,12 +409,12 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
}
return { success: true };
} catch (error) {
console.error('Error toggling window visibility:', error);
console.error("Error toggling window visibility:", error);
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.
// This handler is kept for compatibility but is a no-op now.
return { success: true };