Compare commits
2 Commits
669c019fd8
...
06e178762d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06e178762d | ||
|
|
656e8f0932 |
38
README.md
38
README.md
@ -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 you’re 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
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
18
package.json
18
package.json
@ -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
8886
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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 });
|
||||||
|
|||||||
@ -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 <span class="key" style="pointer-events: none;">${cheatingDaddy.isMacOS ? 'Cmd' : 'Ctrl'}</span
|
Hide <span class="key" style="pointer-events: none;">${mastermind.isMacOS ? 'Cmd' : 'Ctrl'}</span
|
||||||
> <span class="key">\</span>
|
> <span class="key">\</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click=${this.onCloseClick} class="icon-button window-close">
|
<button @click=${this.onCloseClick} class="icon-button window-close">
|
||||||
|
|||||||
@ -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);
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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> -->
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
20
src/index.js
20
src/index.js
@ -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() {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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') {
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user