Compare commits

...

2 Commits

Author SHA1 Message Date
Илья Глазунов
06e178762d Rename project from "Cheating Daddy" to "Mastermind" across all files, update version to 0.6.0, and implement migration functionality for users with existing configurations. Enhance onboarding experience with migration options and update relevant documentation.
Some checks failed
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-01-16 01:16:43 +03:00
Илья Глазунов
656e8f0932 Implement Push-to-Talk feature and enhance audio input settings in AssistantView and CustomizeView. Update README for API key instructions and improve audio processing logic in OpenAI SDK. Adjust pnpm-lock.yaml for dependency updates. 2026-01-16 00:41:58 +03:00
21 changed files with 5503 additions and 4223 deletions

View File

@ -1,10 +1,5 @@
<img width="1299" height="424" alt="cd (1)" src="https://github.com/user-attachments/assets/b25fff4d-043d-4f38-9985-f832ae0d0f6e" /> <!-- <img width="1299" height="424" alt="cd (1)" src="https://github.com/user-attachments/assets/b25fff4d-043d-4f38-9985-f832ae0d0f6e" /> -->
# Mastermind
## Recall.ai - API for desktop recording
If youre looking for a hosted desktop recording API, consider checking out [Recall.ai](https://www.recall.ai/product/desktop-recording-sdk/?utm_source=github&utm_medium=sponsorship&utm_campaign=sohzm-cheating-daddy), an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
This project is sponsored by Recall.ai.
--- ---
@ -14,33 +9,36 @@ This project is sponsored by Recall.ai.
> [!NOTE] > [!NOTE]
> During testing it wont answer if you ask something, you need to simulate interviewer asking question, which it will answer > During testing it wont answer if you ask something, you need to simulate interviewer asking question, which it will answer
A real-time AI assistant that provides contextual help during video calls, interviews, presentations, and meetings using screen capture and audio analysis. A real-time AI assistant that provides contextual help during video calls, interviews, presentations, and meetings using screen capture and audio analysis. It is fork of [Cheating Daddy](https://github.com/sohzm/cheating-daddy) project.
## Features ## Features
- **Live AI Assistance**: Real-time help powered by Google Gemini 2.0 Flash Live - **Live AI Assistance**: Real-time help powered by Gemini API / OpenAI SDK / OpenAI Realtime API, so you can choose which one you want to use
- **Screen & Audio Capture**: Analyzes what you see and hear for contextual responses - **Screen & Audio Capture**: Analyzes what you see and hear for contextual responses
- **Multiple Profiles**: Interview, Sales Call, Business Meeting, Presentation, Negotiation - **Multiple Profiles**: Interview, Sales Call, Business Meeting, Presentation, Negotiation
- **Transparent Overlay**: Always-on-top window that can be positioned anywhere - **Transparent Overlay**: Always-on-top window that can be positioned anywhere, if something goes wrong you can hide it without stoping session and losing context!
- **Click-through Mode**: Make window transparent to clicks when needed - **Click-through Mode**: Make window transparent to clicks when needed
- **Cross-platform**: Works on macOS, Windows, and Linux (kinda, dont use, just for testing rn) - **Cross-platform**: Works on macOS, Windows, and Linux (kinda, dont use, just for testing rn)
## Setup ## Setup
1. **Get a Gemini API Key**: Visit [Google AI Studio](https://aistudio.google.com/apikey) 1. **Get a API Key**: Visit [Google AI Studio](https://aistudio.google.com/apikey) or [OpenAI](https://platform.openai.com/docs/api-reference) or any other OpenAI-compatible API!
2. **Install Dependencies**: `npm install` 2. **Install Dependencies**: `pnpm install`
3. **Run the App**: `npm start` 3. **Run the App**: `pnpm start`
## Usage ## Usage
1. Enter your Gemini API key in the main window 1. Enter your API key in the main window, select provider and model you want to use in preferences
2. Choose your profile and language in settings 2. Choose your profile and language in settings
3. Click "Start Session" to begin 3. Click "Start Session" to begin, if you want to use push-to-talk mode, you can enable it in preferences
4. Position the window using keyboard shortcuts 4. Position the window using keyboard shortcuts, or use your mouse to move it
5. The AI will provide real-time assistance based on your screen and what interview asks 5. The AI will provide real-time assistance based on your screen and system audio/microphone input, you can also send text messages to AI by pressing Enter
## Keyboard Shortcuts ## Keyboard Shortcuts
> [!NOTE]
> All keyboard shortcuts are customizable in settings. You can check some default shortcuts below.
- **Window Movement**: `Ctrl/Cmd + Arrow Keys` - Move window - **Window Movement**: `Ctrl/Cmd + Arrow Keys` - Move window
- **Click-through**: `Ctrl/Cmd + M` - Toggle mouse events - **Click-through**: `Ctrl/Cmd + M` - Toggle mouse events
- **Close/Back**: `Ctrl/Cmd + \` - Close window or go back - **Close/Back**: `Ctrl/Cmd + \` - Close window or go back
@ -48,13 +46,13 @@ A real-time AI assistant that provides contextual help during video calls, inter
## Audio Capture ## Audio Capture
- **macOS**: [SystemAudioDump](https://github.com/Mohammed-Yasin-Mulla/Sound) for system audio - **macOS**: [SystemAudioDump](https://github.com/Mohammed-Yasin-Mulla/Sound) for system audio capture, you can use microphone input as well
- **Windows**: Loopback audio capture - **Windows**: Loopback audio capture, you can use microphone input as well
- **Linux**: Microphone input - **Linux**: Microphone input
## Requirements ## Requirements
- Electron-compatible OS (macOS, Windows, Linux) - Electron-compatible OS (macOS, Windows, Linux)
- Gemini API key - AI Provider API key
- Screen recording permissions - Screen recording permissions
- Microphone/audio permissions - Microphone/audio permissions

View File

@ -7,7 +7,7 @@ module.exports = {
packagerConfig: { packagerConfig: {
asar: true, asar: true,
extraResource: ['./src/assets/SystemAudioDump'], extraResource: ['./src/assets/SystemAudioDump'],
name: 'Cheating Daddy', name: 'Mastermind',
icon: 'src/assets/logo', icon: 'src/assets/logo',
// Fix executable permissions after packaging // Fix executable permissions after packaging
afterCopy: [ afterCopy: [
@ -51,9 +51,9 @@ module.exports = {
{ {
name: '@electron-forge/maker-squirrel', name: '@electron-forge/maker-squirrel',
config: { config: {
name: 'cheating-daddy', name: 'mastermind',
productName: 'Cheating Daddy', productName: 'Mastermind',
shortcutName: 'Cheating Daddy', shortcutName: 'Mastermind',
createDesktopShortcut: true, createDesktopShortcut: true,
createStartMenuShortcut: true, createStartMenuShortcut: true,
}, },
@ -62,7 +62,7 @@ module.exports = {
name: '@electron-forge/maker-dmg', name: '@electron-forge/maker-dmg',
platforms: ['darwin'], platforms: ['darwin'],
config: { config: {
name: 'CheatingDaddy', name: 'Mastermind',
format: 'ULFO', format: 'ULFO',
}, },
}, },
@ -71,10 +71,10 @@ module.exports = {
platforms: ['linux'], platforms: ['linux'],
config: { config: {
options: { options: {
name: 'Cheating Daddy', name: 'Mastermind',
productName: 'Cheating Daddy', productName: 'Mastermind',
genericName: 'AI Assistant', genericName: 'AI Assistant',
description: 'AI assistant for interviews and learning', description: 'AI assistant for video calls, interviews, presentations, and meetings',
categories: ['Development', 'Education'], categories: ['Development', 'Education'],
icon: 'src/assets/logo.png', icon: 'src/assets/logo.png',
}, },

View File

@ -1,8 +1,8 @@
{ {
"name": "cheating-daddy", "name": "mastermind",
"productName": "cheating-daddy", "productName": "mastermind",
"version": "0.5.11", "version": "0.6.0",
"description": "cheating daddy", "description": "Mastermind",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"start": "electron-forge start", "start": "electron-forge start",
@ -12,11 +12,11 @@
"lint": "echo \"No linting configured\"" "lint": "echo \"No linting configured\""
}, },
"keywords": [ "keywords": [
"cheating daddy", "mastermind",
"cheating daddy ai", "mastermind ai",
"cheating daddy ai assistant", "mastermind ai assistant",
"cheating daddy ai assistant for interviews", "mastermind ai assistant for interviews",
"cheating daddy ai assistant for interviews" "mastermind ai assistant for interviews"
], ],
"author": { "author": {
"name": "ShiftyX1", "name": "ShiftyX1",

8886
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -86,7 +86,7 @@ function analyzeAudioBuffer(buffer, label = 'Audio') {
// Save audio buffer with metadata for debugging // Save audio buffer with metadata for debugging
function saveDebugAudio(buffer, type, timestamp = Date.now()) { function saveDebugAudio(buffer, type, timestamp = Date.now()) {
const homeDir = require('os').homedir(); const homeDir = require('os').homedir();
const debugDir = path.join(homeDir, 'cheating-daddy-debug'); const debugDir = path.join(homeDir, 'mastermind-debug');
if (!fs.existsSync(debugDir)) { if (!fs.existsSync(debugDir)) {
fs.mkdirSync(debugDir, { recursive: true }); fs.mkdirSync(debugDir, { recursive: true });

View File

@ -314,8 +314,8 @@ export class AppHeader extends LitElement {
async _checkForUpdates() { async _checkForUpdates() {
try { try {
const currentVersion = await cheatingDaddy.getVersion(); const currentVersion = await mastermind.getVersion();
const response = await fetch('https://raw.githubusercontent.com/sohzm/cheating-daddy/refs/heads/master/package.json'); const response = await fetch('https://raw.githubusercontent.com/ShiftyX1/Mastermind/refs/heads/master/package.json');
if (!response.ok) return; if (!response.ok) return;
const remotePackage = await response.json(); const remotePackage = await response.json();
@ -344,7 +344,7 @@ export class AppHeader extends LitElement {
async _openUpdatePage() { async _openUpdatePage() {
const { ipcRenderer } = require('electron'); const { ipcRenderer } = require('electron');
await ipcRenderer.invoke('open-external', 'https://cheatingdaddy.com'); await ipcRenderer.invoke('open-external', 'https://github.com/ShiftyX1/Mastermind');
} }
disconnectedCallback() { disconnectedCallback() {
@ -396,15 +396,15 @@ export class AppHeader extends LitElement {
getViewTitle() { getViewTitle() {
const titles = { const titles = {
onboarding: 'Welcome to Cheating Daddy', onboarding: 'Welcome to Mastermind',
main: 'Cheating Daddy', main: 'Mastermind',
customize: 'Customize', customize: 'Customize',
help: 'Help & Shortcuts', help: 'Help & Shortcuts',
history: 'Conversation History', history: 'Conversation History',
advanced: 'Advanced Tools', advanced: 'Advanced Tools',
assistant: 'Cheating Daddy', assistant: 'Mastermind',
}; };
return titles[this.currentView] || 'Cheating Daddy'; return titles[this.currentView] || 'Mastermind';
} }
getElapsedTime() { getElapsedTime() {
@ -539,7 +539,7 @@ export class AppHeader extends LitElement {
${this.currentView === 'assistant' ${this.currentView === 'assistant'
? html` ? html`
<button @click=${this.onHideToggleClick} class="button"> <button @click=${this.onHideToggleClick} class="button">
Hide&nbsp;&nbsp;<span class="key" style="pointer-events: none;">${cheatingDaddy.isMacOS ? 'Cmd' : 'Ctrl'}</span Hide&nbsp;&nbsp;<span class="key" style="pointer-events: none;">${mastermind.isMacOS ? 'Cmd' : 'Ctrl'}</span
>&nbsp;&nbsp;<span class="key">&bsol;</span> >&nbsp;&nbsp;<span class="key">&bsol;</span>
</button> </button>
<button @click=${this.onCloseClick} class="icon-button window-close"> <button @click=${this.onCloseClick} class="icon-button window-close">

View File

@ -8,7 +8,7 @@ import { AssistantView } from '../views/AssistantView.js';
import { OnboardingView } from '../views/OnboardingView.js'; import { OnboardingView } from '../views/OnboardingView.js';
import { ScreenPickerDialog } from '../views/ScreenPickerDialog.js'; import { ScreenPickerDialog } from '../views/ScreenPickerDialog.js';
export class CheatingDaddyApp extends LitElement { export class MastermindApp extends LitElement {
static styles = css` static styles = css`
* { * {
box-sizing: border-box; box-sizing: border-box;
@ -154,9 +154,9 @@ export class CheatingDaddyApp extends LitElement {
async _loadFromStorage() { async _loadFromStorage() {
try { try {
const [config, prefs, openaiSdkCreds] = await Promise.all([ const [config, prefs, openaiSdkCreds] = await Promise.all([
cheatingDaddy.storage.getConfig(), mastermind.storage.getConfig(),
cheatingDaddy.storage.getPreferences(), mastermind.storage.getPreferences(),
cheatingDaddy.storage.getOpenAISDKCredentials(), mastermind.storage.getOpenAISDKCredentials(),
]); ]);
// Check onboarding status // Check onboarding status
@ -325,7 +325,7 @@ export class CheatingDaddyApp extends LitElement {
if (this.currentView === 'customize' || this.currentView === 'help' || this.currentView === 'history') { if (this.currentView === 'customize' || this.currentView === 'help' || this.currentView === 'history') {
this.currentView = 'main'; this.currentView = 'main';
} else if (this.currentView === 'assistant') { } else if (this.currentView === 'assistant') {
cheatingDaddy.stopCapture(); mastermind.stopCapture();
// Close the session // Close the session
if (window.require) { if (window.require) {
@ -354,7 +354,7 @@ export class CheatingDaddyApp extends LitElement {
// Main view event handlers // Main view event handlers
async handleStart() { async handleStart() {
// check if api key is empty do nothing // check if api key is empty do nothing
const apiKey = await cheatingDaddy.storage.getApiKey(); const apiKey = await mastermind.storage.getApiKey();
if (!apiKey || apiKey === '') { if (!apiKey || apiKey === '') {
// Trigger the red blink animation on the API key input // Trigger the red blink animation on the API key input
const mainView = this.shadowRoot.querySelector('main-view'); const mainView = this.shadowRoot.querySelector('main-view');
@ -364,9 +364,9 @@ export class CheatingDaddyApp extends LitElement {
return; return;
} }
await cheatingDaddy.initializeGemini(this.selectedProfile, this.selectedLanguage); await mastermind.initializeGemini(this.selectedProfile, this.selectedLanguage);
// Pass the screenshot interval as string (including 'manual' option) // Pass the screenshot interval as string (including 'manual' option)
cheatingDaddy.startCapture(this.selectedScreenshotInterval, this.selectedImageQuality); mastermind.startCapture(this.selectedScreenshotInterval, this.selectedImageQuality);
this.responses = []; this.responses = [];
this.currentResponseIndex = -1; this.currentResponseIndex = -1;
this.startTime = Date.now(); this.startTime = Date.now();
@ -383,22 +383,22 @@ export class CheatingDaddyApp extends LitElement {
// Customize view event handlers // Customize view event handlers
async handleProfileChange(profile) { async handleProfileChange(profile) {
this.selectedProfile = profile; this.selectedProfile = profile;
await cheatingDaddy.storage.updatePreference('selectedProfile', profile); await mastermind.storage.updatePreference('selectedProfile', profile);
} }
async handleLanguageChange(language) { async handleLanguageChange(language) {
this.selectedLanguage = language; this.selectedLanguage = language;
await cheatingDaddy.storage.updatePreference('selectedLanguage', language); await mastermind.storage.updatePreference('selectedLanguage', language);
} }
async handleScreenshotIntervalChange(interval) { async handleScreenshotIntervalChange(interval) {
this.selectedScreenshotInterval = interval; this.selectedScreenshotInterval = interval;
await cheatingDaddy.storage.updatePreference('selectedScreenshotInterval', interval); await mastermind.storage.updatePreference('selectedScreenshotInterval', interval);
} }
async handleImageQualityChange(quality) { async handleImageQualityChange(quality) {
this.selectedImageQuality = quality; this.selectedImageQuality = quality;
await cheatingDaddy.storage.updatePreference('selectedImageQuality', quality); await mastermind.storage.updatePreference('selectedImageQuality', quality);
} }
handleBackClick() { handleBackClick() {
@ -416,7 +416,7 @@ export class CheatingDaddyApp extends LitElement {
// Assistant view event handlers // Assistant view event handlers
async handleSendText(message) { async handleSendText(message) {
const result = await window.cheatingDaddy.sendTextMessage(message); const result = await window.mastermind.sendTextMessage(message);
if (!result.success) { if (!result.success) {
console.error('Failed to send message:', result.error); console.error('Failed to send message:', result.error);
@ -582,7 +582,7 @@ export class CheatingDaddyApp extends LitElement {
async handleLayoutModeChange(layoutMode) { async handleLayoutModeChange(layoutMode) {
this.layoutMode = layoutMode; this.layoutMode = layoutMode;
await cheatingDaddy.storage.updateConfig('layout', layoutMode); await mastermind.storage.updateConfig('layout', layoutMode);
this.updateLayoutMode(); this.updateLayoutMode();
// Notify main process about layout change for window resizing // Notify main process about layout change for window resizing
@ -637,4 +637,4 @@ export class CheatingDaddyApp extends LitElement {
} }
} }
customElements.define('cheating-daddy-app', CheatingDaddyApp); customElements.define('mastermind-app', MastermindApp);

View File

@ -1,5 +1,5 @@
// Main app components // Main app components
export { CheatingDaddyApp } from './app/CheatingDaddyApp.js'; export { MastermindApp } from './app/MastermindApp.js';
export { AppHeader } from './app/AppHeader.js'; export { AppHeader } from './app/AppHeader.js';
// View components // View components

View File

@ -366,6 +366,57 @@ export class AssistantView extends LitElement {
.region-select-btn span { .region-select-btn span {
margin-left: 4px; margin-left: 4px;
} }
.ptt-toggle-btn {
display: flex;
align-items: center;
justify-content: center;
background: transparent;
color: var(--text-secondary);
border: 1px solid var(--border-color);
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
cursor: pointer;
transition: all 0.15s ease;
}
.ptt-toggle-btn:hover {
background: var(--hover-background);
color: var(--text-color);
border-color: var(--text-color);
}
.ptt-toggle-btn.active {
color: var(--error-color);
border-color: var(--error-color);
}
.ptt-indicator {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
color: var(--text-secondary);
margin-bottom: 6px;
}
.ptt-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--border-color);
box-shadow: 0 0 0 1px var(--border-color);
}
.ptt-dot.active {
background: var(--error-color);
box-shadow: 0 0 0 1px var(--error-color);
}
.ptt-label {
font-family: 'SF Mono', Monaco, monospace;
}
`; `;
static properties = { static properties = {
@ -377,6 +428,9 @@ export class AssistantView extends LitElement {
flashCount: { type: Number }, flashCount: { type: Number },
flashLiteCount: { type: Number }, flashLiteCount: { type: Number },
aiProvider: { type: String }, aiProvider: { type: String },
pushToTalkActive: { type: Boolean },
audioInputMode: { type: String },
pushToTalkKeybind: { type: String },
}; };
constructor() { constructor() {
@ -388,6 +442,9 @@ export class AssistantView extends LitElement {
this.flashCount = 0; this.flashCount = 0;
this.flashLiteCount = 0; this.flashLiteCount = 0;
this.aiProvider = 'gemini'; this.aiProvider = 'gemini';
this.pushToTalkActive = false;
this.audioInputMode = 'auto';
this.pushToTalkKeybind = '';
} }
getProfileNames() { getProfileNames() {
@ -507,6 +564,7 @@ export class AssistantView extends LitElement {
// Load limits on mount // Load limits on mount
this.loadLimits(); this.loadLimits();
this.loadPushToTalkKeybind();
// Set up IPC listeners for keyboard shortcuts // Set up IPC listeners for keyboard shortcuts
if (window.require) { if (window.require) {
@ -532,10 +590,17 @@ export class AssistantView extends LitElement {
this.scrollResponseDown(); this.scrollResponseDown();
}; };
this.handlePushToTalkState = (event, state) => {
this.pushToTalkActive = state?.active ?? false;
this.audioInputMode = state?.inputMode ?? 'auto';
this.requestUpdate();
};
ipcRenderer.on('navigate-previous-response', this.handlePreviousResponse); ipcRenderer.on('navigate-previous-response', this.handlePreviousResponse);
ipcRenderer.on('navigate-next-response', this.handleNextResponse); ipcRenderer.on('navigate-next-response', this.handleNextResponse);
ipcRenderer.on('scroll-response-up', this.handleScrollUp); ipcRenderer.on('scroll-response-up', this.handleScrollUp);
ipcRenderer.on('scroll-response-down', this.handleScrollDown); ipcRenderer.on('scroll-response-down', this.handleScrollDown);
ipcRenderer.on('push-to-talk-state', this.handlePushToTalkState);
} }
} }
@ -557,6 +622,9 @@ export class AssistantView extends LitElement {
if (this.handleScrollDown) { if (this.handleScrollDown) {
ipcRenderer.removeListener('scroll-response-down', this.handleScrollDown); ipcRenderer.removeListener('scroll-response-down', this.handleScrollDown);
} }
if (this.handlePushToTalkState) {
ipcRenderer.removeListener('push-to-talk-state', this.handlePushToTalkState);
}
} }
} }
@ -577,13 +645,22 @@ export class AssistantView extends LitElement {
} }
async loadLimits() { async loadLimits() {
if (window.cheatingDaddy?.storage?.getTodayLimits) { if (window.mastermind?.storage?.getTodayLimits) {
const limits = await window.cheatingDaddy.storage.getTodayLimits(); const limits = await window.mastermind.storage.getTodayLimits();
this.flashCount = limits.flash?.count || 0; this.flashCount = limits.flash?.count || 0;
this.flashLiteCount = limits.flashLite?.count || 0; this.flashLiteCount = limits.flashLite?.count || 0;
} }
} }
async loadPushToTalkKeybind() {
if (window.mastermind?.storage?.getKeybinds) {
const isMac = window.mastermind?.isMacOS || navigator.platform.includes('Mac');
const defaultKeybind = isMac ? 'Ctrl+Space' : 'Ctrl+Space';
const keybinds = await window.mastermind.storage.getKeybinds();
this.pushToTalkKeybind = keybinds?.pushToTalk || defaultKeybind;
}
}
getTotalUsed() { getTotalUsed() {
return this.flashCount + this.flashLiteCount; return this.flashCount + this.flashLiteCount;
} }
@ -608,6 +685,14 @@ export class AssistantView extends LitElement {
} }
} }
handlePushToTalkToggle() {
if (!window.require) {
return;
}
const { ipcRenderer } = window.require('electron');
ipcRenderer.send('push-to-talk-toggle');
}
scrollToBottom() { scrollToBottom() {
setTimeout(() => { setTimeout(() => {
const container = this.shadowRoot.querySelector('.response-container'); const container = this.shadowRoot.querySelector('.response-container');
@ -649,10 +734,26 @@ export class AssistantView extends LitElement {
render() { render() {
const responseCounter = this.getResponseCounter(); const responseCounter = this.getResponseCounter();
const showPushToTalk = this.aiProvider === 'openai-sdk' && this.audioInputMode === 'push-to-talk';
const keybindLabel = this.pushToTalkKeybind || 'Hotkey';
const pushToTalkLabel = this.pushToTalkActive
? 'Recording...'
: `Press ${keybindLabel} to start/stop`;
const pushToTalkButtonLabel = this.pushToTalkActive ? 'Stop' : 'Record';
return html` return html`
<div class="response-container" id="responseContainer"></div> <div class="response-container" id="responseContainer"></div>
${showPushToTalk
? html`
<div class="ptt-indicator">
<span class="ptt-dot ${this.pushToTalkActive ? 'active' : ''}"></span>
<span>Push-to-Talk:</span>
<span class="ptt-label">${pushToTalkLabel}</span>
</div>
`
: ''}
<div class="text-input-container"> <div class="text-input-container">
<button class="nav-button" @click=${this.navigateToPreviousResponse} ?disabled=${this.currentResponseIndex <= 0}> <button class="nav-button" @click=${this.navigateToPreviousResponse} ?disabled=${this.currentResponseIndex <= 0}>
<svg width="24px" height="24px" stroke-width="1.7" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="24px" height="24px" stroke-width="1.7" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -671,6 +772,17 @@ export class AssistantView extends LitElement {
<input type="text" id="textInput" placeholder="Type a message to the AI..." @keydown=${this.handleTextKeydown} /> <input type="text" id="textInput" placeholder="Type a message to the AI..." @keydown=${this.handleTextKeydown} />
<div class="capture-buttons"> <div class="capture-buttons">
${showPushToTalk
? html`
<button
class="ptt-toggle-btn ${this.pushToTalkActive ? 'active' : ''}"
@click=${this.handlePushToTalkToggle}
title="Toggle Push-to-Talk recording"
>
${pushToTalkButtonLabel}
</button>
`
: ''}
<button class="region-select-btn" @click=${this.handleRegionSelect} title="Select region to analyze (like Win+Shift+S)"> <button class="region-select-btn" @click=${this.handleRegionSelect} title="Select region to analyze (like Win+Shift+S)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path <path

View File

@ -537,6 +537,7 @@ export class CustomizeView extends LitElement {
color: var(--error-color); color: var(--error-color);
border-left: 2px solid var(--error-color); border-left: 2px solid var(--error-color);
} }
`; `;
static properties = { static properties = {
@ -549,6 +550,7 @@ export class CustomizeView extends LitElement {
backgroundTransparency: { type: Number }, backgroundTransparency: { type: Number },
fontSize: { type: Number }, fontSize: { type: Number },
theme: { type: String }, theme: { type: String },
audioInputMode: { type: String },
onProfileChange: { type: Function }, onProfileChange: { type: Function },
onLanguageChange: { type: Function }, onLanguageChange: { type: Function },
onImageQualityChange: { type: Function }, onImageQualityChange: { type: Function },
@ -587,6 +589,7 @@ export class CustomizeView extends LitElement {
// Audio mode default // Audio mode default
this.audioMode = 'speaker_only'; this.audioMode = 'speaker_only';
this.audioInputMode = 'auto';
// Custom prompt // Custom prompt
this.customPrompt = ''; this.customPrompt = '';
@ -614,7 +617,7 @@ export class CustomizeView extends LitElement {
} }
getThemes() { getThemes() {
return cheatingDaddy.theme.getAll(); return mastermind.theme.getAll();
} }
setActiveSection(section) { setActiveSection(section) {
@ -784,17 +787,18 @@ export class CustomizeView extends LitElement {
async _loadFromStorage() { async _loadFromStorage() {
try { try {
const [prefs, keybinds, credentials, openaiCreds, openaiSdkCreds] = await Promise.all([ const [prefs, keybinds, credentials, openaiCreds, openaiSdkCreds] = await Promise.all([
cheatingDaddy.storage.getPreferences(), mastermind.storage.getPreferences(),
cheatingDaddy.storage.getKeybinds(), mastermind.storage.getKeybinds(),
cheatingDaddy.storage.getCredentials(), mastermind.storage.getCredentials(),
cheatingDaddy.storage.getOpenAICredentials(), mastermind.storage.getOpenAICredentials(),
cheatingDaddy.storage.getOpenAISDKCredentials(), mastermind.storage.getOpenAISDKCredentials(),
]); ]);
this.googleSearchEnabled = prefs.googleSearchEnabled ?? true; this.googleSearchEnabled = prefs.googleSearchEnabled ?? true;
this.backgroundTransparency = prefs.backgroundTransparency ?? 0.8; this.backgroundTransparency = prefs.backgroundTransparency ?? 0.8;
this.fontSize = prefs.fontSize ?? 20; this.fontSize = prefs.fontSize ?? 20;
this.audioMode = prefs.audioMode ?? 'speaker_only'; this.audioMode = prefs.audioMode ?? 'speaker_only';
this.audioInputMode = prefs.audioInputMode ?? 'auto';
this.customPrompt = prefs.customPrompt ?? ''; this.customPrompt = prefs.customPrompt ?? '';
this.theme = prefs.theme ?? 'dark'; this.theme = prefs.theme ?? 'dark';
this.aiProvider = prefs.aiProvider ?? 'gemini'; this.aiProvider = prefs.aiProvider ?? 'gemini';
@ -820,6 +824,7 @@ export class CustomizeView extends LitElement {
this.updateBackgroundTransparency(); this.updateBackgroundTransparency();
this.updateFontSize(); this.updateFontSize();
this.notifyPushToTalkSettings();
this.requestUpdate(); this.requestUpdate();
} catch (error) { } catch (error) {
console.error('Error loading settings:', error); console.error('Error loading settings:', error);
@ -832,6 +837,10 @@ export class CustomizeView extends LitElement {
resizeLayout(); resizeLayout();
} }
disconnectedCallback() {
super.disconnectedCallback();
}
getProfiles() { getProfiles() {
return [ return [
{ {
@ -935,24 +944,46 @@ export class CustomizeView extends LitElement {
async handleCustomPromptInput(e) { async handleCustomPromptInput(e) {
this.customPrompt = e.target.value; this.customPrompt = e.target.value;
await cheatingDaddy.storage.updatePreference('customPrompt', e.target.value); await mastermind.storage.updatePreference('customPrompt', e.target.value);
} }
async handleAudioModeSelect(e) { async handleAudioModeSelect(e) {
this.audioMode = e.target.value; this.audioMode = e.target.value;
await cheatingDaddy.storage.updatePreference('audioMode', e.target.value); await mastermind.storage.updatePreference('audioMode', e.target.value);
this.requestUpdate(); this.requestUpdate();
} }
async handleAudioInputModeChange(e) {
this.audioInputMode = e.target.value;
await mastermind.storage.updatePreference('audioInputMode', e.target.value);
this.notifyPushToTalkSettings();
this.requestUpdate();
}
notifyPushToTalkSettings() {
if (!window.require) {
return;
}
try {
const { ipcRenderer } = window.require('electron');
ipcRenderer.send('update-push-to-talk-settings', {
inputMode: this.audioInputMode,
});
ipcRenderer.send('update-keybinds', this.keybinds);
} catch (error) {
console.error('Failed to notify push-to-talk settings:', error);
}
}
async handleThemeChange(e) { async handleThemeChange(e) {
this.theme = e.target.value; this.theme = e.target.value;
await cheatingDaddy.theme.save(this.theme); await mastermind.theme.save(this.theme);
this.updateBackgroundAppearance(); this.updateBackgroundAppearance();
this.requestUpdate(); this.requestUpdate();
} }
getDefaultKeybinds() { getDefaultKeybinds() {
const isMac = cheatingDaddy.isMacOS || navigator.platform.includes('Mac'); const isMac = mastermind.isMacOS || navigator.platform.includes('Mac');
return { return {
moveUp: isMac ? 'Alt+Up' : 'Ctrl+Up', moveUp: isMac ? 'Alt+Up' : 'Ctrl+Up',
moveDown: isMac ? 'Alt+Down' : 'Ctrl+Down', moveDown: isMac ? 'Alt+Down' : 'Ctrl+Down',
@ -965,11 +996,12 @@ export class CustomizeView extends LitElement {
nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]', nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]',
scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up', scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up',
scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down', scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down',
pushToTalk: isMac ? 'Ctrl+Space' : 'Ctrl+Space',
}; };
} }
async saveKeybinds() { async saveKeybinds() {
await cheatingDaddy.storage.setKeybinds(this.keybinds); await mastermind.storage.setKeybinds(this.keybinds);
// Send to main process to update global shortcuts // Send to main process to update global shortcuts
if (window.require) { if (window.require) {
const { ipcRenderer } = window.require('electron'); const { ipcRenderer } = window.require('electron');
@ -985,7 +1017,7 @@ export class CustomizeView extends LitElement {
async resetKeybinds() { async resetKeybinds() {
this.keybinds = this.getDefaultKeybinds(); this.keybinds = this.getDefaultKeybinds();
await cheatingDaddy.storage.setKeybinds(null); await mastermind.storage.setKeybinds(null);
this.requestUpdate(); this.requestUpdate();
if (window.require) { if (window.require) {
const { ipcRenderer } = window.require('electron'); const { ipcRenderer } = window.require('electron');
@ -1050,6 +1082,11 @@ export class CustomizeView extends LitElement {
name: 'Scroll Response Down', name: 'Scroll Response Down',
description: 'Scroll the AI response content down', description: 'Scroll the AI response content down',
}, },
{
key: 'pushToTalk',
name: 'Push-to-Talk',
description: 'Activate audio recording (OpenAI SDK only)',
},
]; ];
} }
@ -1130,7 +1167,7 @@ export class CustomizeView extends LitElement {
async handleGoogleSearchChange(e) { async handleGoogleSearchChange(e) {
this.googleSearchEnabled = e.target.checked; this.googleSearchEnabled = e.target.checked;
await cheatingDaddy.storage.updatePreference('googleSearchEnabled', this.googleSearchEnabled); await mastermind.storage.updatePreference('googleSearchEnabled', this.googleSearchEnabled);
// Notify main process if available // Notify main process if available
if (window.require) { if (window.require) {
@ -1147,32 +1184,32 @@ export class CustomizeView extends LitElement {
async handleAIProviderChange(e) { async handleAIProviderChange(e) {
this.aiProvider = e.target.value; this.aiProvider = e.target.value;
await cheatingDaddy.storage.updatePreference('aiProvider', e.target.value); await mastermind.storage.updatePreference('aiProvider', e.target.value);
this.requestUpdate(); this.requestUpdate();
} }
async handleGeminiApiKeyInput(e) { async handleGeminiApiKeyInput(e) {
this.geminiApiKey = e.target.value; this.geminiApiKey = e.target.value;
await cheatingDaddy.storage.setApiKey(e.target.value); await mastermind.storage.setApiKey(e.target.value);
} }
async handleOpenAIApiKeyInput(e) { async handleOpenAIApiKeyInput(e) {
this.openaiApiKey = e.target.value; this.openaiApiKey = e.target.value;
await cheatingDaddy.storage.setOpenAICredentials({ await mastermind.storage.setOpenAICredentials({
apiKey: e.target.value, apiKey: e.target.value,
}); });
} }
async handleOpenAIBaseUrlInput(e) { async handleOpenAIBaseUrlInput(e) {
this.openaiBaseUrl = e.target.value; this.openaiBaseUrl = e.target.value;
await cheatingDaddy.storage.setOpenAICredentials({ await mastermind.storage.setOpenAICredentials({
baseUrl: e.target.value, baseUrl: e.target.value,
}); });
} }
async handleOpenAIModelInput(e) { async handleOpenAIModelInput(e) {
this.openaiModel = e.target.value; this.openaiModel = e.target.value;
await cheatingDaddy.storage.setOpenAICredentials({ await mastermind.storage.setOpenAICredentials({
model: e.target.value, model: e.target.value,
}); });
} }
@ -1180,35 +1217,35 @@ export class CustomizeView extends LitElement {
// OpenAI SDK handlers // OpenAI SDK handlers
async handleOpenAISdkApiKeyInput(e) { async handleOpenAISdkApiKeyInput(e) {
this.openaiSdkApiKey = e.target.value; this.openaiSdkApiKey = e.target.value;
await cheatingDaddy.storage.setOpenAISDKCredentials({ await mastermind.storage.setOpenAISDKCredentials({
apiKey: e.target.value, apiKey: e.target.value,
}); });
} }
async handleOpenAISdkBaseUrlInput(e) { async handleOpenAISdkBaseUrlInput(e) {
this.openaiSdkBaseUrl = e.target.value; this.openaiSdkBaseUrl = e.target.value;
await cheatingDaddy.storage.setOpenAISDKCredentials({ await mastermind.storage.setOpenAISDKCredentials({
baseUrl: e.target.value, baseUrl: e.target.value,
}); });
} }
async handleOpenAISdkModelInput(e) { async handleOpenAISdkModelInput(e) {
this.openaiSdkModel = e.target.value; this.openaiSdkModel = e.target.value;
await cheatingDaddy.storage.setOpenAISDKCredentials({ await mastermind.storage.setOpenAISDKCredentials({
model: e.target.value, model: e.target.value,
}); });
} }
async handleOpenAISdkVisionModelInput(e) { async handleOpenAISdkVisionModelInput(e) {
this.openaiSdkVisionModel = e.target.value; this.openaiSdkVisionModel = e.target.value;
await cheatingDaddy.storage.setOpenAISDKCredentials({ await mastermind.storage.setOpenAISDKCredentials({
visionModel: e.target.value, visionModel: e.target.value,
}); });
} }
async handleOpenAISdkWhisperModelInput(e) { async handleOpenAISdkWhisperModelInput(e) {
this.openaiSdkWhisperModel = e.target.value; this.openaiSdkWhisperModel = e.target.value;
await cheatingDaddy.storage.setOpenAISDKCredentials({ await mastermind.storage.setOpenAISDKCredentials({
whisperModel: e.target.value, whisperModel: e.target.value,
}); });
} }
@ -1222,7 +1259,7 @@ export class CustomizeView extends LitElement {
this.requestUpdate(); this.requestUpdate();
try { try {
await cheatingDaddy.storage.clearAll(); await mastermind.storage.clearAll();
this.clearStatusMessage = 'Successfully cleared all local data'; this.clearStatusMessage = 'Successfully cleared all local data';
this.clearStatusType = 'success'; this.clearStatusType = 'success';
@ -1251,15 +1288,15 @@ export class CustomizeView extends LitElement {
async handleBackgroundTransparencyChange(e) { async handleBackgroundTransparencyChange(e) {
this.backgroundTransparency = parseFloat(e.target.value); this.backgroundTransparency = parseFloat(e.target.value);
await cheatingDaddy.storage.updatePreference('backgroundTransparency', this.backgroundTransparency); await mastermind.storage.updatePreference('backgroundTransparency', this.backgroundTransparency);
this.updateBackgroundAppearance(); this.updateBackgroundAppearance();
this.requestUpdate(); this.requestUpdate();
} }
updateBackgroundAppearance() { updateBackgroundAppearance() {
// Use theme's background color // Use theme's background color
const colors = cheatingDaddy.theme.get(this.theme); const colors = mastermind.theme.get(this.theme);
cheatingDaddy.theme.applyBackgrounds(colors.background, this.backgroundTransparency); mastermind.theme.applyBackgrounds(colors.background, this.backgroundTransparency);
} }
// Keep old function name for backwards compatibility // Keep old function name for backwards compatibility
@ -1269,7 +1306,7 @@ export class CustomizeView extends LitElement {
async handleFontSizeChange(e) { async handleFontSizeChange(e) {
this.fontSize = parseInt(e.target.value, 10); this.fontSize = parseInt(e.target.value, 10);
await cheatingDaddy.storage.updatePreference('fontSize', this.fontSize); await mastermind.storage.updatePreference('fontSize', this.fontSize);
this.updateFontSize(); this.updateFontSize();
this.requestUpdate(); this.requestUpdate();
} }
@ -1319,6 +1356,9 @@ export class CustomizeView extends LitElement {
} }
renderAudioSection() { renderAudioSection() {
const isPushToTalkAvailable = this.aiProvider === 'openai-sdk';
const pushToTalkDisabled = !isPushToTalkAvailable;
return html` return html`
<div class="content-header">Audio Settings</div> <div class="content-header">Audio Settings</div>
<div class="form-grid"> <div class="form-grid">
@ -1331,6 +1371,28 @@ export class CustomizeView extends LitElement {
</select> </select>
<div class="form-description">Choose which audio sources to capture for the AI.</div> <div class="form-description">Choose which audio sources to capture for the AI.</div>
</div> </div>
<div class="form-group">
<label class="form-label">Audio Input Mode</label>
<select
class="form-control"
.value=${this.audioInputMode}
@change=${this.handleAudioInputModeChange}
?disabled=${pushToTalkDisabled}
>
<option value="auto">Automatic (Always Listening)</option>
<option value="push-to-talk">Push-to-Talk (Hotkey Activated)</option>
</select>
<div class="form-description">
${pushToTalkDisabled
? 'Push-to-Talk is available only with the OpenAI SDK provider.'
: this.audioInputMode === 'auto'
? 'Audio is continuously recorded and transcribed when silence is detected.'
: 'Audio recording starts when you press and hold/toggle the hotkey.'}
</div>
</div>
${this.audioInputMode === 'push-to-talk'
? html`<div class="form-description">Use the Push-to-Talk hotkey (toggle) to start/stop recording.</div>`
: ''}
</div> </div>
`; `;
} }

View File

@ -243,7 +243,7 @@ export class HelpView extends LitElement {
async _loadKeybinds() { async _loadKeybinds() {
try { try {
const keybinds = await cheatingDaddy.storage.getKeybinds(); const keybinds = await mastermind.storage.getKeybinds();
if (keybinds) { if (keybinds) {
this.keybinds = { ...this.getDefaultKeybinds(), ...keybinds }; this.keybinds = { ...this.getDefaultKeybinds(), ...keybinds };
this.requestUpdate(); this.requestUpdate();
@ -260,7 +260,7 @@ export class HelpView extends LitElement {
} }
getDefaultKeybinds() { getDefaultKeybinds() {
const isMac = cheatingDaddy.isMacOS || navigator.platform.includes('Mac'); const isMac = mastermind.isMacOS || navigator.platform.includes('Mac');
return { return {
moveUp: isMac ? 'Alt+Up' : 'Ctrl+Up', moveUp: isMac ? 'Alt+Up' : 'Ctrl+Up',
moveDown: isMac ? 'Alt+Down' : 'Ctrl+Down', moveDown: isMac ? 'Alt+Down' : 'Ctrl+Down',
@ -285,8 +285,8 @@ export class HelpView extends LitElement {
} }
render() { render() {
const isMacOS = cheatingDaddy.isMacOS || false; const isMacOS = mastermind.isMacOS || false;
const isLinux = cheatingDaddy.isLinux || false; const isLinux = mastermind.isLinux || false;
return html` return html`
<div class="help-container"> <div class="help-container">
@ -295,7 +295,7 @@ export class HelpView extends LitElement {
<span>Community & Support</span> <span>Community & Support</span>
</div> </div>
<div class="community-links"> <div class="community-links">
<div class="community-link" @click=${() => this.handleExternalLinkClick('https://cheatingdaddy.com')}> <!-- <div class="community-link" @click=${() => this.handleExternalLinkClick('https://github.com/ShiftyX1/Mastermind')}>
<svg <svg
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
@ -312,8 +312,8 @@ export class HelpView extends LitElement {
></path> ></path>
</svg> </svg>
Website Website
</div> </div> -->
<div class="community-link" @click=${() => this.handleExternalLinkClick('https://github.com/sohzm/cheating-daddy')}> <div class="community-link" @click=${() => this.handleExternalLinkClick('https://github.com/ShiftyX1/Mastermind')}>
<svg <svg
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
@ -329,7 +329,7 @@ export class HelpView extends LitElement {
</svg> </svg>
GitHub GitHub
</div> </div>
<div class="community-link" @click=${() => this.handleExternalLinkClick('https://discord.gg/GCBdubnXfJ')}> <!-- <div class="community-link" @click=${() => this.handleExternalLinkClick('https://discord.gg/GCBdubnXfJ')}>
<svg <svg
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
@ -353,7 +353,7 @@ export class HelpView extends LitElement {
></path> ></path>
</svg> </svg>
Discord Discord
</div> </div> -->
</div> </div>
</div> </div>
@ -442,7 +442,7 @@ export class HelpView extends LitElement {
<span>How to Use</span> <span>How to Use</span>
</div> </div>
<div class="usage-steps"> <div class="usage-steps">
<div class="usage-step"><strong>Start a Session:</strong> Enter your Gemini API key and click "Start Session"</div> <div class="usage-step"><strong>Start a Session:</strong> Enter your AI Provider API key and click "Start Session"</div>
<div class="usage-step"><strong>Customize:</strong> Choose your profile and language in the settings</div> <div class="usage-step"><strong>Customize:</strong> Choose your profile and language in the settings</div>
<div class="usage-step"> <div class="usage-step">
<strong>Position Window:</strong> Use keyboard shortcuts to move the window to your desired location <strong>Position Window:</strong> Use keyboard shortcuts to move the window to your desired location

View File

@ -380,7 +380,7 @@ export class HistoryView extends LitElement {
async loadSessions() { async loadSessions() {
try { try {
this.loading = true; this.loading = true;
this.sessions = await cheatingDaddy.storage.getAllSessions(); this.sessions = await mastermind.storage.getAllSessions();
} catch (error) { } catch (error) {
console.error('Error loading conversation sessions:', error); console.error('Error loading conversation sessions:', error);
this.sessions = []; this.sessions = [];
@ -392,7 +392,7 @@ export class HistoryView extends LitElement {
async loadSelectedSession(sessionId) { async loadSelectedSession(sessionId) {
try { try {
const session = await cheatingDaddy.storage.getSession(sessionId); const session = await mastermind.storage.getSession(sessionId);
if (session) { if (session) {
this.selectedSession = session; this.selectedSession = session;
this.requestUpdate(); this.requestUpdate();

View File

@ -150,7 +150,7 @@ export class MainView extends LitElement {
} }
async _loadApiKey() { async _loadApiKey() {
this.apiKey = await cheatingDaddy.storage.getApiKey(); this.apiKey = await mastermind.storage.getApiKey();
this.requestUpdate(); this.requestUpdate();
} }
@ -186,7 +186,7 @@ export class MainView extends LitElement {
async handleInput(e) { async handleInput(e) {
this.apiKey = e.target.value; this.apiKey = e.target.value;
await cheatingDaddy.storage.setApiKey(e.target.value); await mastermind.storage.setApiKey(e.target.value);
// Clear error state when user starts typing // Clear error state when user starts typing
if (this.showApiKeyError) { if (this.showApiKeyError) {
this.showApiKeyError = false; this.showApiKeyError = false;
@ -226,7 +226,7 @@ export class MainView extends LitElement {
<div class="input-group"> <div class="input-group">
<input <input
type="password" type="password"
placeholder="Enter your Gemini API Key" placeholder="Enter your AI Provider API Key"
.value=${this.apiKey} .value=${this.apiKey}
@input=${this.handleInput} @input=${this.handleInput}
class="${this.showApiKeyError ? 'api-key-error' : ''}" class="${this.showApiKeyError ? 'api-key-error' : ''}"
@ -235,10 +235,10 @@ export class MainView extends LitElement {
${this.getStartButtonText()} ${this.getStartButtonText()}
</button> </button>
</div> </div>
<p class="description"> <!-- <p class="description">
dont have an api key? dont have an api key?
<span @click=${this.handleAPIKeyHelpClick} class="link">get one here</span> <span @click=${this.handleAPIKeyHelpClick} class="link">get one here</span>
</p> </p> -->
`; `;
} }
} }

View File

@ -157,6 +157,49 @@ export class OnboardingView extends LitElement {
opacity: 0.8; opacity: 0.8;
} }
.migration-buttons {
display: flex;
gap: 12px;
margin-top: 24px;
}
.migration-button {
flex: 1;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.migration-button.primary {
background: rgba(59, 130, 246, 0.8);
color: #ffffff;
border-color: rgba(59, 130, 246, 0.9);
}
.migration-button.primary:hover {
background: rgba(59, 130, 246, 0.9);
border-color: rgba(59, 130, 246, 1);
}
.migration-button.secondary {
background: rgba(255, 255, 255, 0.08);
color: #e5e5e5;
border-color: rgba(255, 255, 255, 0.1);
}
.migration-button.secondary:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.2);
}
.migration-button:active {
transform: scale(0.98);
}
.navigation { .navigation {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
@ -239,6 +282,7 @@ export class OnboardingView extends LitElement {
static properties = { static properties = {
currentSlide: { type: Number }, currentSlide: { type: Number },
contextText: { type: String }, contextText: { type: String },
hasOldConfig: { type: Boolean },
onComplete: { type: Function }, onComplete: { type: Function },
onClose: { type: Function }, onClose: { type: Function },
}; };
@ -247,6 +291,7 @@ export class OnboardingView extends LitElement {
super(); super();
this.currentSlide = 0; this.currentSlide = 0;
this.contextText = ''; this.contextText = '';
this.hasOldConfig = false;
this.onComplete = () => {}; this.onComplete = () => {};
this.onClose = () => {}; this.onClose = () => {};
this.canvas = null; this.canvas = null;
@ -297,7 +342,16 @@ export class OnboardingView extends LitElement {
[30, 40, 35], // Muted green [30, 40, 35], // Muted green
[5, 15, 10], // Almost black [5, 15, 10], // Almost black
], ],
// Slide 5 - Complete (Dark warm gray) // Slide 5 - Migration (Dark teal-gray)
[
[20, 30, 30], // Dark teal-gray
[15, 25, 25], // Darker teal-gray
[25, 35, 35], // Slightly teal
[10, 20, 20], // Very dark teal
[30, 40, 40], // Muted teal
[5, 15, 15], // Almost black
],
// Slide 6 - Complete (Dark warm gray)
[ [
[30, 25, 20], // Dark warm gray [30, 25, 20], // Dark warm gray
[25, 20, 15], // Darker warm [25, 20, 15], // Darker warm
@ -309,13 +363,25 @@ export class OnboardingView extends LitElement {
]; ];
} }
firstUpdated() { async firstUpdated() {
this.canvas = this.shadowRoot.querySelector('.gradient-canvas'); this.canvas = this.shadowRoot.querySelector('.gradient-canvas');
this.ctx = this.canvas.getContext('2d'); this.ctx = this.canvas.getContext('2d');
this.resizeCanvas(); this.resizeCanvas();
this.startGradientAnimation(); this.startGradientAnimation();
window.addEventListener('resize', () => this.resizeCanvas()); window.addEventListener('resize', () => this.resizeCanvas());
// Check if old config exists
if (window.mastermind && window.mastermind.storage) {
try {
this.hasOldConfig = await window.mastermind.storage.hasOldConfig();
console.log('Has old config:', this.hasOldConfig);
this.requestUpdate(); // Force re-render with new hasOldConfig value
} catch (error) {
console.error('Error checking old config:', error);
this.hasOldConfig = false;
}
}
} }
disconnectedCallback() { disconnectedCallback() {
@ -414,7 +480,7 @@ export class OnboardingView extends LitElement {
} }
nextSlide() { nextSlide() {
if (this.currentSlide < 4) { if (this.currentSlide < 5) {
this.startColorTransition(this.currentSlide + 1); this.startColorTransition(this.currentSlide + 1);
} else { } else {
this.completeOnboarding(); this.completeOnboarding();
@ -462,11 +528,23 @@ export class OnboardingView extends LitElement {
} }
} }
async handleMigrate() {
const success = await window.mastermind.storage.migrateFromOldConfig();
if (success) {
console.log('Migration completed successfully');
}
this.nextSlide();
}
async handleSkipMigration() {
this.nextSlide();
}
async completeOnboarding() { async completeOnboarding() {
if (this.contextText.trim()) { if (this.contextText.trim()) {
await cheatingDaddy.storage.updatePreference('customPrompt', this.contextText.trim()); await mastermind.storage.updatePreference('customPrompt', this.contextText.trim());
} }
await cheatingDaddy.storage.updateConfig('onboarded', true); await mastermind.storage.updateConfig('onboarded', true);
this.onComplete(); this.onComplete();
} }
@ -474,9 +552,9 @@ export class OnboardingView extends LitElement {
const slides = [ const slides = [
{ {
icon: 'assets/onboarding/welcome.svg', icon: 'assets/onboarding/welcome.svg',
title: 'Welcome to Cheating Daddy', title: 'Welcome to Mastermind',
content: content:
'Your AI assistant that listens and watches, then provides intelligent suggestions automatically during interviews and meetings.', 'Your AI assistant that listens and watches, then provides intelligent suggestions automatically during interviews, meetings, and presentations.',
}, },
{ {
icon: 'assets/onboarding/security.svg', icon: 'assets/onboarding/security.svg',
@ -495,10 +573,18 @@ export class OnboardingView extends LitElement {
content: '', content: '',
showFeatures: true, showFeatures: true,
}, },
{
icon: 'assets/onboarding/context.svg',
title: 'Migrate Settings?',
content: this.hasOldConfig
? 'Mastermind is a fork of Cheating Daddy. We detected existing Cheating Daddy settings on your system. Would you like to automatically migrate your settings, API keys, and history?'
: 'Mastermind is a fork of Cheating Daddy. No previous settings were detected.',
showMigration: this.hasOldConfig,
},
{ {
icon: 'assets/onboarding/ready.svg', icon: 'assets/onboarding/ready.svg',
title: 'Ready to Go', title: 'Ready to Go',
content: 'Add your Gemini API key in settings and start getting AI-powered assistance in real-time.', content: 'Choose your AI Provider and start getting AI-powered assistance in real-time.',
}, },
]; ];
@ -552,6 +638,18 @@ export class OnboardingView extends LitElement {
</div> </div>
` `
: ''} : ''}
${slide.showMigration
? html`
<div class="migration-buttons">
<button class="migration-button primary" @click=${this.handleMigrate}>
Migrate Settings
</button>
<button class="migration-button secondary" @click=${this.handleSkipMigration}>
Start Fresh
</button>
</div>
`
: ''}
</div> </div>
<div class="navigation"> <div class="navigation">
@ -562,7 +660,7 @@ export class OnboardingView extends LitElement {
</button> </button>
<div class="progress-dots"> <div class="progress-dots">
${[0, 1, 2, 3, 4].map( ${[0, 1, 2, 3, 4, 5].map(
index => html` index => html`
<div <div
class="dot ${index === this.currentSlide ? 'active' : ''}" class="dot ${index === this.currentSlide ? 'active' : ''}"
@ -577,7 +675,7 @@ export class OnboardingView extends LitElement {
</div> </div>
<button class="nav-button" @click=${this.nextSlide}> <button class="nav-button" @click=${this.nextSlide}>
${this.currentSlide === 4 ${this.currentSlide === 5
? 'Get Started' ? 'Get Started'
: html` : html`
<svg width="16px" height="16px" stroke-width="2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16px" height="16px" stroke-width="2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">

View File

@ -123,7 +123,7 @@
box-sizing: border-box; box-sizing: border-box;
} }
cheating-daddy-app { mastermind-app {
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -134,10 +134,9 @@
<script src="assets/marked-4.3.0.min.js"></script> <script src="assets/marked-4.3.0.min.js"></script>
<script src="assets/highlight-11.9.0.min.js"></script> <script src="assets/highlight-11.9.0.min.js"></script>
<link rel="stylesheet" href="assets/highlight-vscode-dark.min.css" /> <link rel="stylesheet" href="assets/highlight-vscode-dark.min.css" />
<script type="module" src="components/app/CheatingDaddyApp.js"></script> <script type="module" src="components/app/MastermindApp.js"></script>
<cheating-daddy-app id="cheatingDaddy"></cheating-daddy-app> <mastermind-app id="mastermind"></mastermind-app>
<script src="script.js"></script>
<script src="utils/renderer.js"></script> <script src="utils/renderer.js"></script>
</body> </body>
</html> </html>

View File

@ -295,6 +295,26 @@ function setupStorageIpcHandlers() {
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
// ============ MIGRATION ============
ipcMain.handle('storage:has-old-config', async () => {
try {
return { success: true, data: storage.hasOldConfig() };
} catch (error) {
console.error('Error checking old config:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('storage:migrate-from-old-config', async () => {
try {
const success = storage.migrateFromOldConfig();
return { success: true, data: success };
} catch (error) {
console.error('Error migrating from old config:', error);
return { success: false, error: error.message };
}
});
} }
function setupGeneralIpcHandlers() { function setupGeneralIpcHandlers() {

View File

@ -33,6 +33,7 @@ const DEFAULT_PREFERENCES = {
selectedImageQuality: 'medium', selectedImageQuality: 'medium',
advancedMode: false, advancedMode: false,
audioMode: 'speaker_only', audioMode: 'speaker_only',
audioInputMode: 'auto',
fontSize: 'medium', fontSize: 'medium',
backgroundTransparency: 0.8, backgroundTransparency: 0.8,
googleSearchEnabled: false, googleSearchEnabled: false,
@ -50,6 +51,22 @@ function getConfigDir() {
const platform = os.platform(); const platform = os.platform();
let configDir; let configDir;
if (platform === 'win32') {
configDir = path.join(os.homedir(), 'AppData', 'Roaming', 'mastermind-config');
} else if (platform === 'darwin') {
configDir = path.join(os.homedir(), 'Library', 'Application Support', 'mastermind-config');
} else {
configDir = path.join(os.homedir(), '.config', 'mastermind-config');
}
return configDir;
}
// Get the old config directory path for migration
function getOldConfigDir() {
const platform = os.platform();
let configDir;
if (platform === 'win32') { if (platform === 'win32') {
configDir = path.join(os.homedir(), 'AppData', 'Roaming', 'cheating-daddy-config'); configDir = path.join(os.homedir(), 'AppData', 'Roaming', 'cheating-daddy-config');
} else if (platform === 'darwin') { } else if (platform === 'darwin') {
@ -61,6 +78,43 @@ function getConfigDir() {
return configDir; return configDir;
} }
// Check if old config directory exists
function hasOldConfig() {
const oldDir = getOldConfigDir();
return fs.existsSync(oldDir);
}
// Migrate config from old directory to new directory if needed
function migrateFromOldConfig() {
const oldDir = getOldConfigDir();
const newDir = getConfigDir();
if (!fs.existsSync(oldDir)) {
console.log('No old config found to migrate');
return false;
}
if (fs.existsSync(newDir)) {
// NOTE: Does not matter if the new config directory already exists, we will overwrite it with the old config
fs.rmSync(newDir, { recursive: true, force: true });
console.log('New config directory already exists, overwriting with old config');
}
console.log(`Migrating config from ${oldDir} to ${newDir}...`);
try {
const parentDir = path.dirname(newDir);
if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true });
}
fs.renameSync(oldDir, newDir);
console.log('Migration successful');
return true;
} catch (error) {
console.error('Migration failed:', error.message);
return false;
}
}
// File paths // File paths
function getConfigPath() { function getConfigPath() {
return path.join(getConfigDir(), 'config.json'); return path.join(getConfigDir(), 'config.json');
@ -468,6 +522,10 @@ module.exports = {
initializeStorage, initializeStorage,
getConfigDir, getConfigDir,
// Migration
hasOldConfig,
migrateFromOldConfig,
// Config // Config
getConfig, getConfig,
setConfig, setConfig,

View File

@ -186,6 +186,7 @@ async function initializeAISession(customPrompt = '', profile = 'interview', lan
try { try {
await openaiSdkProvider.initializeOpenAISDK(providerConfig); await openaiSdkProvider.initializeOpenAISDK(providerConfig);
openaiSdkProvider.setSystemPrompt(systemPrompt); openaiSdkProvider.setSystemPrompt(systemPrompt);
openaiSdkProvider.updatePushToTalkSettings(prefs.audioInputMode || 'auto');
sendToRenderer('update-status', 'Ready (OpenAI SDK)'); sendToRenderer('update-status', 'Ready (OpenAI SDK)');
return true; return true;
} catch (error) { } catch (error) {
@ -325,6 +326,16 @@ function setupAIProviderIpcHandlers(geminiSessionRef) {
saveConversationTurn(transcription, response); saveConversationTurn(transcription, response);
}); });
ipcMain.on('push-to-talk-toggle', () => {
if (currentProvider === 'openai-sdk') {
openaiSdkProvider.togglePushToTalk();
}
});
ipcMain.on('update-push-to-talk-settings', (event, { inputMode } = {}) => {
openaiSdkProvider.updatePushToTalkSettings(inputMode || 'auto');
});
ipcMain.handle('initialize-ai-session', async (event, customPrompt, profile, language) => { ipcMain.handle('initialize-ai-session', async (event, customPrompt, profile, language) => {
return await initializeAISession(customPrompt, profile, language); return await initializeAISession(customPrompt, profile, language);
}); });

View File

@ -14,6 +14,8 @@ let openaiClient = null;
let currentConfig = null; let currentConfig = null;
let conversationMessages = []; let conversationMessages = [];
let isProcessing = false; let isProcessing = false;
let audioInputMode = 'auto';
let isPushToTalkActive = false;
// macOS audio capture // macOS audio capture
let systemAudioProc = null; let systemAudioProc = null;
@ -294,6 +296,18 @@ async function processAudioChunk(base64Audio, mimeType) {
const now = Date.now(); const now = Date.now();
const buffer = Buffer.from(base64Audio, 'base64'); const buffer = Buffer.from(base64Audio, 'base64');
if (audioInputMode === 'push-to-talk') {
if (!isPushToTalkActive) {
return { success: true, ignored: true };
}
// In push-to-talk mode we only buffer while active
audioChunks.push(buffer);
lastAudioTime = now;
return { success: true, buffering: true };
}
// Track first chunk time for duration-based flushing // Track first chunk time for duration-based flushing
if (audioChunks.length === 0) { if (audioChunks.length === 0) {
firstChunkTime = now; firstChunkTime = now;
@ -380,6 +394,97 @@ async function flushAudioAndTranscribe() {
} }
} }
function notifyPushToTalkState() {
sendToRenderer('push-to-talk-state', {
active: isPushToTalkActive,
inputMode: audioInputMode,
});
}
function resetRealtimeAudioBuffer() {
audioChunks = [];
firstChunkTime = 0;
lastAudioTime = 0;
if (silenceCheckTimer) {
clearTimeout(silenceCheckTimer);
silenceCheckTimer = null;
}
if (windowsTranscriptionTimer) {
clearInterval(windowsTranscriptionTimer);
windowsTranscriptionTimer = null;
}
}
function updateTranscriptionTimerForPushToTalk() {
if (audioInputMode === 'push-to-talk') {
stopTranscriptionTimer();
return;
}
if (systemAudioProc && !transcriptionTimer) {
startTranscriptionTimer();
}
}
async function setPushToTalkActive(active) {
const wasActive = isPushToTalkActive;
isPushToTalkActive = active;
if (active) {
// Starting recording - clear any old buffers
resetRealtimeAudioBuffer();
audioBuffer = Buffer.alloc(0);
console.log('Push-to-Talk: Recording started');
sendToRenderer('update-status', 'Recording...');
}
notifyPushToTalkState();
// When user stops recording in PTT mode, send audio for transcription
if (!active && wasActive && audioInputMode === 'push-to-talk') {
console.log('Push-to-Talk: Recording stopped, transcribing...');
sendToRenderer('update-status', 'Transcribing...');
// For browser-based audio (Windows)
if (audioChunks.length > 0) {
await flushAudioAndTranscribe();
}
// For macOS SystemAudioDump
if (audioBuffer.length > 0) {
await transcribeBufferedAudio(true); // Force transcription
}
sendToRenderer('update-status', 'Listening...');
}
}
async function togglePushToTalk() {
if (isPushToTalkActive) {
await setPushToTalkActive(false);
} else {
await setPushToTalkActive(true);
}
}
function updatePushToTalkSettings(inputMode) {
if (inputMode) {
audioInputMode = inputMode;
}
if (audioInputMode !== 'push-to-talk' && isPushToTalkActive) {
isPushToTalkActive = false;
}
if (audioInputMode !== 'push-to-talk') {
resetRealtimeAudioBuffer();
audioBuffer = Buffer.alloc(0);
}
notifyPushToTalkState();
updateTranscriptionTimerForPushToTalk();
}
function clearConversation() { function clearConversation() {
const systemMessage = conversationMessages.find(m => m.role === 'system'); const systemMessage = conversationMessages.find(m => m.role === 'system');
conversationMessages = systemMessage ? [systemMessage] : []; conversationMessages = systemMessage ? [systemMessage] : [];
@ -403,6 +508,7 @@ function closeOpenAISDK() {
conversationMessages = []; conversationMessages = [];
audioChunks = []; audioChunks = [];
isProcessing = false; isProcessing = false;
isPushToTalkActive = false;
// Clear timers // Clear timers
if (silenceCheckTimer) { if (silenceCheckTimer) {
@ -414,6 +520,7 @@ function closeOpenAISDK() {
windowsTranscriptionTimer = null; windowsTranscriptionTimer = null;
} }
notifyPushToTalkState();
sendToRenderer('update-status', 'Disconnected'); sendToRenderer('update-status', 'Disconnected');
} }
@ -461,11 +568,16 @@ function hasSpeech(buffer, threshold = 500) {
return rms > threshold; return rms > threshold;
} }
async function transcribeBufferedAudio() { async function transcribeBufferedAudio(forcePTT = false) {
if (audioBuffer.length === 0 || isProcessing) { if (audioBuffer.length === 0 || isProcessing) {
return; return;
} }
// In push-to-talk mode, only transcribe when explicitly requested (forcePTT=true)
if (audioInputMode === 'push-to-talk' && !forcePTT) {
return;
}
// Calculate audio duration // Calculate audio duration
const bytesPerSample = 2; const bytesPerSample = 2;
const audioDurationMs = (audioBuffer.length / bytesPerSample / SAMPLE_RATE) * 1000; const audioDurationMs = (audioBuffer.length / bytesPerSample / SAMPLE_RATE) * 1000;
@ -475,7 +587,8 @@ async function transcribeBufferedAudio() {
} }
// Check if there's actual speech in the audio (Voice Activity Detection) // Check if there's actual speech in the audio (Voice Activity Detection)
if (!hasSpeech(audioBuffer)) { // Skip VAD check in PTT mode - user explicitly wants to transcribe
if (!forcePTT && !hasSpeech(audioBuffer)) {
// Clear buffer if it's just silence/noise // Clear buffer if it's just silence/noise
audioBuffer = Buffer.alloc(0); audioBuffer = Buffer.alloc(0);
return; return;
@ -487,7 +600,9 @@ async function transcribeBufferedAudio() {
try { try {
console.log(`Transcribing ${audioDurationMs.toFixed(0)}ms of audio...`); console.log(`Transcribing ${audioDurationMs.toFixed(0)}ms of audio...`);
sendToRenderer('update-status', 'Transcribing...'); if (!forcePTT) {
sendToRenderer('update-status', 'Transcribing...');
}
const transcription = await transcribeAudio(currentBuffer, 'audio/wav'); const transcription = await transcribeAudio(currentBuffer, 'audio/wav');
@ -497,12 +612,18 @@ async function transcribeBufferedAudio() {
// Send to chat // Send to chat
await sendTextMessage(transcription); await sendTextMessage(transcription);
} else if (forcePTT) {
console.log('Push-to-Talk: No speech detected in recording');
} }
sendToRenderer('update-status', 'Listening...'); if (!forcePTT) {
sendToRenderer('update-status', 'Listening...');
}
} catch (error) { } catch (error) {
console.error('Transcription error:', error); console.error('Transcription error:', error);
sendToRenderer('update-status', 'Listening...'); if (!forcePTT) {
sendToRenderer('update-status', 'Listening...');
}
} }
} }
@ -598,6 +719,10 @@ async function startMacOSAudioCapture() {
// Convert stereo to mono // Convert stereo to mono
const monoChunk = CHANNELS === 2 ? convertStereoToMono(chunk) : chunk; const monoChunk = CHANNELS === 2 ? convertStereoToMono(chunk) : chunk;
if (audioInputMode === 'push-to-talk' && !isPushToTalkActive) {
continue;
}
// Add to audio buffer for transcription // Add to audio buffer for transcription
audioBuffer = Buffer.concat([audioBuffer, monoChunk]); audioBuffer = Buffer.concat([audioBuffer, monoChunk]);
@ -643,7 +768,7 @@ async function startMacOSAudioCapture() {
}); });
// Start periodic transcription // Start periodic transcription
startTranscriptionTimer(); updateTranscriptionTimerForPushToTalk();
sendToRenderer('update-status', 'Listening...'); sendToRenderer('update-status', 'Listening...');
@ -651,6 +776,10 @@ async function startMacOSAudioCapture() {
} }
function startTranscriptionTimer() { function startTranscriptionTimer() {
// Don't start auto-transcription timer in push-to-talk mode
if (audioInputMode === 'push-to-talk') {
return;
}
stopTranscriptionTimer(); stopTranscriptionTimer();
transcriptionTimer = setInterval(transcribeBufferedAudio, TRANSCRIPTION_INTERVAL_MS); transcriptionTimer = setInterval(transcribeBufferedAudio, TRANSCRIPTION_INTERVAL_MS);
} }
@ -682,6 +811,8 @@ module.exports = {
sendImageMessage, sendImageMessage,
processAudioChunk, processAudioChunk,
flushAudioAndTranscribe, flushAudioAndTranscribe,
togglePushToTalk,
updatePushToTalkSettings,
clearConversation, clearConversation,
closeOpenAISDK, closeOpenAISDK,
startMacOSAudioCapture, startMacOSAudioCapture,

View File

@ -137,6 +137,17 @@ const storage = {
const result = await ipcRenderer.invoke('storage:get-today-limits'); const result = await ipcRenderer.invoke('storage:get-today-limits');
return result.success ? result.data : { flash: { count: 0 }, flashLite: { count: 0 } }; return result.success ? result.data : { flash: { count: 0 }, flashLite: { count: 0 } };
}, },
// Migration
hasOldConfig() {
// Note: This is synchronous in the main process, but we need to use invoke which is async
// So we'll make this return a promise
return ipcRenderer.invoke('storage:has-old-config').then(result => (result.success ? result.data : false));
},
async migrateFromOldConfig() {
const result = await ipcRenderer.invoke('storage:migrate-from-old-config');
return result.success ? result.data : false;
},
}; };
// Cache for preferences to avoid async calls in hot paths // Cache for preferences to avoid async calls in hot paths
@ -174,16 +185,20 @@ async function initializeGemini(profile = 'interview', language = 'en-US') {
const prefs = await storage.getPreferences(); const prefs = await storage.getPreferences();
const success = await ipcRenderer.invoke('initialize-ai-session', prefs.customPrompt || '', profile, language); const success = await ipcRenderer.invoke('initialize-ai-session', prefs.customPrompt || '', profile, language);
if (success) { if (success) {
cheatingDaddy.setStatus('Live'); mastermind.setStatus('Live');
} else { } else {
cheatingDaddy.setStatus('Error: Failed to initialize AI session Gemini'); mastermind.setStatus('Error: Failed to initialize AI session Gemini');
} }
} }
// Listen for status updates // Listen for status updates
ipcRenderer.on('update-status', (event, status) => { ipcRenderer.on('update-status', (event, status) => {
console.log('Status update:', status); console.log('Status update:', status);
cheatingDaddy.setStatus(status); mastermind.setStatus(status);
});
ipcRenderer.on('push-to-talk-toggle', () => {
ipcRenderer.send('push-to-talk-toggle');
}); });
async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'medium') { async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'medium') {
@ -302,18 +317,18 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
} else { } else {
// Windows - show custom screen picker first // Windows - show custom screen picker first
logToMain('info', '=== Starting Windows audio capture ==='); logToMain('info', '=== Starting Windows audio capture ===');
cheatingDaddy.setStatus('Choose screen to share...'); mastermind.setStatus('Choose screen to share...');
// Show screen picker dialog // Show screen picker dialog
const appElement = document.querySelector('cheating-daddy-app'); const appElement = document.querySelector('mastermind-app');
const pickerResult = await appElement.showScreenPickerDialog(); const pickerResult = await appElement.showScreenPickerDialog();
if (pickerResult.cancelled) { if (pickerResult.cancelled) {
cheatingDaddy.setStatus('Cancelled'); mastermind.setStatus('Cancelled');
return; return;
} }
cheatingDaddy.setStatus('Starting capture...'); mastermind.setStatus('Starting capture...');
mediaStream = await navigator.mediaDevices.getDisplayMedia({ mediaStream = await navigator.mediaDevices.getDisplayMedia({
video: { video: {
@ -347,7 +362,7 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
if (audioTracks.length === 0) { if (audioTracks.length === 0) {
logToMain('warn', 'WARNING: No audio tracks! User must check "Share audio" in screen picker dialog'); logToMain('warn', 'WARNING: No audio tracks! User must check "Share audio" in screen picker dialog');
cheatingDaddy.setStatus('Warning: No audio - enable "Share audio" checkbox'); mastermind.setStatus('Warning: No audio - enable "Share audio" checkbox');
} else { } else {
logToMain('info', 'Audio track acquired, setting up processing...'); logToMain('info', 'Audio track acquired, setting up processing...');
// Setup audio processing for Windows loopback audio only // Setup audio processing for Windows loopback audio only
@ -399,7 +414,7 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
errorMessage = 'Screen selection was cancelled. Please try again.'; errorMessage = 'Screen selection was cancelled. Please try again.';
} }
cheatingDaddy.setStatus('Error: ' + errorMessage); mastermind.setStatus('Error: ' + errorMessage);
} }
} }
@ -520,7 +535,7 @@ function setupWindowsLoopbackProcessing() {
// Log progress every 100 chunks (~10 seconds) // Log progress every 100 chunks (~10 seconds)
if (chunkCount === 1) { if (chunkCount === 1) {
logToMain('info', 'First audio chunk sent to AI'); logToMain('info', 'First audio chunk sent to AI');
cheatingDaddy.setStatus('Listening...'); mastermind.setStatus('Listening...');
} else if (chunkCount % 100 === 0) { } else if (chunkCount % 100 === 0) {
// Calculate max amplitude to check if we're getting real audio // Calculate max amplitude to check if we're getting real audio
const maxAmp = Math.max(...chunk.map(Math.abs)); const maxAmp = Math.max(...chunk.map(Math.abs));
@ -535,7 +550,7 @@ function setupWindowsLoopbackProcessing() {
logToMain('info', 'Windows audio processing pipeline connected'); logToMain('info', 'Windows audio processing pipeline connected');
} catch (err) { } catch (err) {
logToMain('error', 'Error setting up Windows audio:', err.message, err.stack); logToMain('error', 'Error setting up Windows audio:', err.message, err.stack);
cheatingDaddy.setStatus('Audio error: ' + err.message); mastermind.setStatus('Audio error: ' + err.message);
} }
} }
@ -690,7 +705,7 @@ async function startRegionSelection() {
if (!mediaStream) { if (!mediaStream) {
console.error('No media stream available. Please start capture first.'); console.error('No media stream available. Please start capture first.');
cheatingDaddy?.addNewResponse('Please start screen capture first before selecting a region.'); mastermind?.addNewResponse('Please start screen capture first before selecting a region.');
return; return;
} }
@ -796,7 +811,7 @@ async function captureRegionFromScreenshot(rect, screenshotDataUrl) {
console.log(`Region capture response completed from ${result.model}`); console.log(`Region capture response completed from ${result.model}`);
} else { } else {
console.error('Failed to get region capture response:', result.error); console.error('Failed to get region capture response:', result.error);
cheatingDaddy.addNewResponse(`Error: ${result.error}`); mastermind.addNewResponse(`Error: ${result.error}`);
} }
}; };
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
@ -888,7 +903,7 @@ async function captureManualScreenshot(imageQuality = null) {
// Response already displayed via streaming events (new-response/update-response) // Response already displayed via streaming events (new-response/update-response)
} else { } else {
console.error('Failed to get image response:', result.error); console.error('Failed to get image response:', result.error);
cheatingDaddy.addNewResponse(`Error: ${result.error}`); mastermind.addNewResponse(`Error: ${result.error}`);
} }
}; };
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
@ -1011,11 +1026,11 @@ ipcRenderer.on('clear-sensitive-data', async () => {
// Handle shortcuts based on current view // Handle shortcuts based on current view
function handleShortcut(shortcutKey) { function handleShortcut(shortcutKey) {
const currentView = cheatingDaddy.getCurrentView(); const currentView = mastermind.getCurrentView();
if (shortcutKey === 'ctrl+enter' || shortcutKey === 'cmd+enter') { if (shortcutKey === 'ctrl+enter' || shortcutKey === 'cmd+enter') {
if (currentView === 'main') { if (currentView === 'main') {
cheatingDaddy.element().handleStart(); mastermind.element().handleStart();
} else { } else {
captureManualScreenshot(); captureManualScreenshot();
} }
@ -1023,7 +1038,7 @@ function handleShortcut(shortcutKey) {
} }
// Create reference to the main app element // Create reference to the main app element
const cheatingDaddyApp = document.querySelector('cheating-daddy-app'); const mastermindApp = document.querySelector('mastermind-app');
// ============ THEME SYSTEM ============ // ============ THEME SYSTEM ============
const theme = { const theme = {
@ -1257,23 +1272,23 @@ const theme = {
}, },
}; };
// Consolidated cheatingDaddy object - all functions in one place // Consolidated mastermind object - all functions in one place
const cheatingDaddy = { const mastermind = {
// App version // App version
getVersion: async () => ipcRenderer.invoke('get-app-version'), getVersion: async () => ipcRenderer.invoke('get-app-version'),
// Element access // Element access
element: () => cheatingDaddyApp, element: () => mastermindApp,
e: () => cheatingDaddyApp, e: () => mastermindApp,
// App state functions - access properties directly from the app element // App state functions - access properties directly from the app element
getCurrentView: () => cheatingDaddyApp.currentView, getCurrentView: () => mastermindApp.currentView,
getLayoutMode: () => cheatingDaddyApp.layoutMode, getLayoutMode: () => mastermindApp.layoutMode,
// Status and response functions // Status and response functions
setStatus: text => cheatingDaddyApp.setStatus(text), setStatus: text => mastermindApp.setStatus(text),
addNewResponse: response => cheatingDaddyApp.addNewResponse(response), addNewResponse: response => mastermindApp.addNewResponse(response),
updateCurrentResponse: response => cheatingDaddyApp.updateCurrentResponse(response), updateCurrentResponse: response => mastermindApp.updateCurrentResponse(response),
// Core functionality // Core functionality
initializeGemini, initializeGemini,
@ -1297,7 +1312,7 @@ const cheatingDaddy = {
}; };
// Make it globally available // Make it globally available
window.cheatingDaddy = cheatingDaddy; window.mastermind = mastermind;
// Load theme after DOM is ready // Load theme after DOM is ready
if (document.readyState === 'loading') { if (document.readyState === 'loading') {

View File

@ -9,6 +9,7 @@ let windowResizing = false;
let resizeAnimation = null; let resizeAnimation = null;
const RESIZE_ANIMATION_DURATION = 500; // milliseconds const RESIZE_ANIMATION_DURATION = 500; // milliseconds
function createWindow(sendToRenderer, geminiSessionRef) { function createWindow(sendToRenderer, geminiSessionRef) {
// Get layout preference (default to 'normal') // Get layout preference (default to 'normal')
let windowWidth = 1100; let windowWidth = 1100;
@ -155,6 +156,7 @@ function getDefaultKeybinds() {
scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up', scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up',
scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down', scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down',
emergencyErase: isMac ? 'Cmd+Shift+E' : 'Ctrl+Shift+E', emergencyErase: isMac ? 'Cmd+Shift+E' : 'Ctrl+Shift+E',
pushToTalk: isMac ? 'Ctrl+Space' : 'Ctrl+Space',
}; };
} }
@ -164,6 +166,10 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
// Unregister all existing shortcuts // Unregister all existing shortcuts
globalShortcut.unregisterAll(); globalShortcut.unregisterAll();
const prefs = storage.getPreferences();
const audioInputMode = prefs.audioInputMode || 'auto';
const enablePushToTalk = audioInputMode === 'push-to-talk';
const primaryDisplay = screen.getPrimaryDisplay(); const primaryDisplay = screen.getPrimaryDisplay();
const { width, height } = primaryDisplay.workAreaSize; const { width, height } = primaryDisplay.workAreaSize;
const moveIncrement = Math.floor(Math.min(width, height) * 0.1); const moveIncrement = Math.floor(Math.min(width, height) * 0.1);
@ -253,7 +259,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
// Use the new handleShortcut function // Use the new handleShortcut function
mainWindow.webContents.executeJavaScript(` mainWindow.webContents.executeJavaScript(`
cheatingDaddy.handleShortcut('${shortcutKey}'); mastermind.handleShortcut('${shortcutKey}');
`); `);
} catch (error) { } catch (error) {
console.error('Error handling next step shortcut:', error); console.error('Error handling next step shortcut:', error);
@ -343,6 +349,18 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
console.error(`Failed to register emergencyErase (${keybinds.emergencyErase}):`, error); console.error(`Failed to register emergencyErase (${keybinds.emergencyErase}):`, error);
} }
} }
// Register push-to-talk shortcut (OpenAI SDK only, gated by preferences)
if (keybinds.pushToTalk && enablePushToTalk) {
try {
globalShortcut.register(keybinds.pushToTalk, () => {
sendToRenderer('push-to-talk-toggle');
});
console.log(`Registered pushToTalk (toggle): ${keybinds.pushToTalk}`);
} catch (error) {
console.error(`Failed to register pushToTalk (${keybinds.pushToTalk}):`, error);
}
}
} }
function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) { function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
@ -474,8 +492,8 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
// Get current view and layout mode from renderer // Get current view and layout mode from renderer
let viewName, layoutMode; let viewName, layoutMode;
try { try {
viewName = await event.sender.executeJavaScript('cheatingDaddy.getCurrentView()'); viewName = await event.sender.executeJavaScript('mastermind.getCurrentView()');
layoutMode = await event.sender.executeJavaScript('cheatingDaddy.getLayoutMode()'); layoutMode = await event.sender.executeJavaScript('mastermind.getLayoutMode()');
} catch (error) { } catch (error) {
console.warn('Failed to get view/layout from renderer, using defaults:', error); console.warn('Failed to get view/layout from renderer, using defaults:', error);
viewName = 'main'; viewName = 'main';