Compare commits
No commits in common. "master" and "v0.5.0" have entirely different histories.
164
.github/workflows/release.yml
vendored
164
.github/workflows/release.yml
vendored
@ -1,100 +1,100 @@
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.server_url == 'https://github.com'
|
||||
build:
|
||||
if: github.server_url == 'https://github.com'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
platform: darwin
|
||||
arch: x64
|
||||
- os: macos-latest
|
||||
platform: darwin
|
||||
arch: arm64
|
||||
- os: ubuntu-latest
|
||||
platform: linux
|
||||
arch: x64
|
||||
- os: windows-latest
|
||||
platform: win32
|
||||
arch: x64
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
platform: darwin
|
||||
arch: x64
|
||||
- os: macos-latest
|
||||
platform: darwin
|
||||
arch: arm64
|
||||
- os: ubuntu-latest
|
||||
platform: linux
|
||||
arch: x64
|
||||
- os: windows-latest
|
||||
platform: win32
|
||||
arch: x64
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Build for ${{ matrix.platform }}-${{ matrix.arch }}
|
||||
run: pnpm run make -- --arch=${{ matrix.arch }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build for ${{ matrix.platform }}-${{ matrix.arch }}
|
||||
run: pnpm run make -- --arch=${{ matrix.arch }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-${{ matrix.platform }}-${{ matrix.arch }}
|
||||
path: |
|
||||
out/make/**/*.dmg
|
||||
out/make/**/*.zip
|
||||
out/make/**/*.exe
|
||||
out/make/**/*.AppImage
|
||||
out/make/**/*.deb
|
||||
out/make/**/*.rpm
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-${{ matrix.platform }}-${{ matrix.arch }}
|
||||
path: |
|
||||
out/make/**/*.dmg
|
||||
out/make/**/*.zip
|
||||
out/make/**/*.exe
|
||||
out/make/**/*.AppImage
|
||||
out/make/**/*.deb
|
||||
out/make/**/*.rpm
|
||||
if-no-files-found: ignore
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.server_url == 'https://github.com'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.server_url == 'https://github.com'
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
pattern: release-*
|
||||
merge-multiple: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
- name: List artifacts
|
||||
run: find artifacts -type f | head -50
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
pattern: release-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: List artifacts
|
||||
run: find artifacts -type f | head -50
|
||||
|
||||
- name: Create Draft Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
artifacts/**/*.dmg
|
||||
artifacts/**/*.zip
|
||||
artifacts/**/*.exe
|
||||
artifacts/**/*.AppImage
|
||||
artifacts/**/*.deb
|
||||
artifacts/**/*.rpm
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Create Draft Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
artifacts/**/*.dmg
|
||||
artifacts/**/*.zip
|
||||
artifacts/**/*.exe
|
||||
artifacts/**/*.AppImage
|
||||
artifacts/**/*.deb
|
||||
artifacts/**/*.rpm
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@ -10,16 +10,16 @@ packaging.
|
||||
Install dependencies and run the development app:
|
||||
|
||||
```
|
||||
1. pnpm install
|
||||
2. pnpm start
|
||||
1. npm install
|
||||
2. npm start
|
||||
```
|
||||
|
||||
## Style
|
||||
|
||||
Run `pnpm prettier --write .` before committing. Prettier uses the settings in
|
||||
Run `npx prettier --write .` before committing. Prettier uses the settings in
|
||||
`.prettierrc` (four-space indentation, print width 150, semicolons and single
|
||||
quotes). `src/assets` and `node_modules` are ignored via `.prettierignore`.
|
||||
The project does not provide linting; `pnpm run lint` simply prints
|
||||
The project does not provide linting; `npm run lint` simply prints
|
||||
"No linting configured".
|
||||
|
||||
## Code standards
|
||||
|
||||
38
README.md
38
README.md
@ -1,5 +1,10 @@
|
||||
<!-- <img width="1299" height="424" alt="cd (1)" src="https://github.com/user-attachments/assets/b25fff4d-043d-4f38-9985-f832ae0d0f6e" /> -->
|
||||
# Mastermind
|
||||
<img width="1299" height="424" alt="cd (1)" src="https://github.com/user-attachments/assets/b25fff4d-043d-4f38-9985-f832ae0d0f6e" />
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
@ -9,36 +14,33 @@
|
||||
> [!NOTE]
|
||||
> 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. It is fork of [Cheating Daddy](https://github.com/sohzm/cheating-daddy) project.
|
||||
A real-time AI assistant that provides contextual help during video calls, interviews, presentations, and meetings using screen capture and audio analysis.
|
||||
|
||||
## Features
|
||||
|
||||
- **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
|
||||
- **Live AI Assistance**: Real-time help powered by Google Gemini 2.0 Flash Live
|
||||
- **Screen & Audio Capture**: Analyzes what you see and hear for contextual responses
|
||||
- **Multiple Profiles**: Interview, Sales Call, Business Meeting, Presentation, Negotiation
|
||||
- **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!
|
||||
- **Transparent Overlay**: Always-on-top window that can be positioned anywhere
|
||||
- **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)
|
||||
|
||||
## Setup
|
||||
|
||||
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**: `pnpm install`
|
||||
3. **Run the App**: `pnpm start`
|
||||
1. **Get a Gemini API Key**: Visit [Google AI Studio](https://aistudio.google.com/apikey)
|
||||
2. **Install Dependencies**: `npm install`
|
||||
3. **Run the App**: `npm start`
|
||||
|
||||
## Usage
|
||||
|
||||
1. Enter your API key in the main window, select provider and model you want to use in preferences
|
||||
1. Enter your Gemini API key in the main window
|
||||
2. Choose your profile and language in settings
|
||||
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, or use your mouse to move it
|
||||
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
|
||||
3. Click "Start Session" to begin
|
||||
4. Position the window using keyboard shortcuts
|
||||
5. The AI will provide real-time assistance based on your screen and what interview asks
|
||||
|
||||
## 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
|
||||
- **Click-through**: `Ctrl/Cmd + M` - Toggle mouse events
|
||||
- **Close/Back**: `Ctrl/Cmd + \` - Close window or go back
|
||||
@ -46,13 +48,13 @@ A real-time AI assistant that provides contextual help during video calls, inter
|
||||
|
||||
## Audio Capture
|
||||
|
||||
- **macOS**: [SystemAudioDump](https://github.com/Mohammed-Yasin-Mulla/Sound) for system audio capture, you can use microphone input as well
|
||||
- **Windows**: Loopback audio capture, you can use microphone input as well
|
||||
- **macOS**: [SystemAudioDump](https://github.com/Mohammed-Yasin-Mulla/Sound) for system audio
|
||||
- **Windows**: Loopback audio capture
|
||||
- **Linux**: Microphone input
|
||||
|
||||
## Requirements
|
||||
|
||||
- Electron-compatible OS (macOS, Windows, Linux)
|
||||
- AI Provider API key
|
||||
- Gemini API key
|
||||
- Screen recording permissions
|
||||
- Microphone/audio permissions
|
||||
|
||||
@ -10,8 +10,6 @@
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.microphone</key>
|
||||
|
||||
@ -1,45 +1,24 @@
|
||||
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
|
||||
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
module.exports = {
|
||||
packagerConfig: {
|
||||
asar: true,
|
||||
extraResource: ['./src/assets/SystemAudioDump'],
|
||||
name: 'Mastermind',
|
||||
name: 'Cheating Daddy',
|
||||
icon: 'src/assets/logo',
|
||||
// Fix executable permissions after packaging
|
||||
afterCopy: [
|
||||
(buildPath, electronVersion, platform, arch, callback) => {
|
||||
if (platform === 'darwin') {
|
||||
const systemAudioDump = path.join(buildPath, '..', 'Resources', 'SystemAudioDump');
|
||||
if (fs.existsSync(systemAudioDump)) {
|
||||
try {
|
||||
fs.chmodSync(systemAudioDump, 0o755);
|
||||
console.log('✓ Set executable permissions for SystemAudioDump');
|
||||
} catch (err) {
|
||||
console.error('✗ Failed to set permissions:', err.message);
|
||||
}
|
||||
} else {
|
||||
console.warn('SystemAudioDump not found at:', systemAudioDump);
|
||||
}
|
||||
}
|
||||
callback();
|
||||
},
|
||||
],
|
||||
// use `security find-identity -v -p codesigning` to find your identity
|
||||
// for macos signing
|
||||
// Disabled for local builds - ad-hoc signing causes issues
|
||||
// also fuck apple
|
||||
// osxSign: {
|
||||
// identity: '-', // ad-hoc signing (no Apple Developer account needed)
|
||||
// optionsForFile: (filePath) => {
|
||||
// return {
|
||||
// entitlements: 'entitlements.plist',
|
||||
// };
|
||||
// },
|
||||
// identity: '<paste your identity here>',
|
||||
// optionsForFile: (filePath) => {
|
||||
// return {
|
||||
// entitlements: 'entitlements.plist',
|
||||
// };
|
||||
// },
|
||||
// },
|
||||
// notarize is off - requires Apple Developer account
|
||||
// notarize if off cuz i ran this for 6 hours and it still didnt finish
|
||||
// osxNotarize: {
|
||||
// appleId: 'your apple id',
|
||||
// appleIdPassword: 'app specific password',
|
||||
@ -51,9 +30,9 @@ module.exports = {
|
||||
{
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
config: {
|
||||
name: 'mastermind',
|
||||
productName: 'Mastermind',
|
||||
shortcutName: 'Mastermind',
|
||||
name: 'cheating-daddy',
|
||||
productName: 'Cheating Daddy',
|
||||
shortcutName: 'Cheating Daddy',
|
||||
createDesktopShortcut: true,
|
||||
createStartMenuShortcut: true,
|
||||
},
|
||||
@ -61,23 +40,19 @@ module.exports = {
|
||||
{
|
||||
name: '@electron-forge/maker-dmg',
|
||||
platforms: ['darwin'],
|
||||
config: {
|
||||
name: 'Mastermind',
|
||||
format: 'ULFO',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@reforged/maker-appimage',
|
||||
platforms: ['linux'],
|
||||
config: {
|
||||
options: {
|
||||
name: 'Mastermind',
|
||||
productName: 'Mastermind',
|
||||
name: 'Cheating Daddy',
|
||||
productName: 'Cheating Daddy',
|
||||
genericName: 'AI Assistant',
|
||||
description: 'AI assistant for video calls, interviews, presentations, and meetings',
|
||||
description: 'AI assistant for interviews and learning',
|
||||
categories: ['Development', 'Education'],
|
||||
icon: 'src/assets/logo.png',
|
||||
},
|
||||
icon: 'src/assets/logo.png'
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
23
package.json
23
package.json
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "mastermind",
|
||||
"productName": "mastermind",
|
||||
"version": "0.6.0",
|
||||
"description": "Mastermind",
|
||||
"name": "cheating-daddy",
|
||||
"productName": "cheating-daddy",
|
||||
"version": "0.5.0",
|
||||
"description": "cheating daddy",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
@ -12,15 +12,15 @@
|
||||
"lint": "echo \"No linting configured\""
|
||||
},
|
||||
"keywords": [
|
||||
"mastermind",
|
||||
"mastermind ai",
|
||||
"mastermind ai assistant",
|
||||
"mastermind ai assistant for interviews",
|
||||
"mastermind ai assistant for interviews"
|
||||
"cheating daddy",
|
||||
"cheating daddy ai",
|
||||
"cheating daddy ai assistant",
|
||||
"cheating daddy ai assistant for interviews",
|
||||
"cheating daddy ai assistant for interviews"
|
||||
],
|
||||
"author": {
|
||||
"name": "ShiftyX1",
|
||||
"email": "lead@pyserve.org"
|
||||
"name": "sohzm",
|
||||
"email": "sohambharambe9@gmail.com"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
@ -39,7 +39,6 @@
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.11.1",
|
||||
"@electron-forge/plugin-fuses": "^7.11.1",
|
||||
"@electron/fuses": "^2.0.0",
|
||||
"@electron/osx-sign": "^2.3.0",
|
||||
"@reforged/maker-appimage": "^5.1.1",
|
||||
"electron": "^39.2.7"
|
||||
}
|
||||
|
||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@ -48,9 +48,6 @@ importers:
|
||||
'@electron/fuses':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
'@electron/osx-sign':
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
'@reforged/maker-appimage':
|
||||
specifier: ^5.1.1
|
||||
version: 5.1.1
|
||||
@ -176,11 +173,6 @@ packages:
|
||||
engines: {node: '>=12.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@electron/osx-sign@2.3.0':
|
||||
resolution: {integrity: sha512-qh/BkWUHITP2W1grtCWn8Pm4EML3q1Rfo2tnd/KHo+f1le5gAYotBc6yEfK6X2VHyN21mPZ3CjvOiYOwjimBlA==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
hasBin: true
|
||||
|
||||
'@electron/packager@18.4.4':
|
||||
resolution: {integrity: sha512-fTUCmgL25WXTcFpM1M72VmFP8w3E4d+KNzWxmTDRpvwkfn/S206MAtM2cy0GF78KS9AwASMOUmlOIzCHeNxcGQ==}
|
||||
engines: {node: '>= 16.13.0'}
|
||||
@ -2495,15 +2487,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@electron/osx-sign@2.3.0':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
isbinaryfile: 4.0.10
|
||||
plist: 3.1.0
|
||||
semver: 7.7.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@electron/packager@18.4.4':
|
||||
dependencies:
|
||||
'@electron/asar': 3.4.1
|
||||
|
||||
@ -86,7 +86,7 @@ function analyzeAudioBuffer(buffer, label = 'Audio') {
|
||||
// Save audio buffer with metadata for debugging
|
||||
function saveDebugAudio(buffer, type, timestamp = Date.now()) {
|
||||
const homeDir = require('os').homedir();
|
||||
const debugDir = path.join(homeDir, 'mastermind-debug');
|
||||
const debugDir = path.join(homeDir, 'cheating-daddy-debug');
|
||||
|
||||
if (!fs.existsSync(debugDir)) {
|
||||
fs.mkdirSync(debugDir, { recursive: true });
|
||||
|
||||
@ -3,11 +3,7 @@ import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
|
||||
export class AppHeader extends LitElement {
|
||||
static styles = css`
|
||||
* {
|
||||
font-family:
|
||||
'Inter',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
sans-serif;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
@ -159,11 +155,9 @@ export class AppHeader extends LitElement {
|
||||
white-space: normal;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition:
|
||||
opacity 0.15s ease,
|
||||
visibility 0.15s ease;
|
||||
transition: opacity 0.15s ease, visibility 0.15s ease;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 1000;
|
||||
line-height: 1.4;
|
||||
}
|
||||
@ -230,11 +224,9 @@ export class AppHeader extends LitElement {
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition:
|
||||
opacity 0.15s ease,
|
||||
visibility 0.15s ease;
|
||||
transition: opacity 0.15s ease, visibility 0.15s ease;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@ -314,8 +306,8 @@ export class AppHeader extends LitElement {
|
||||
|
||||
async _checkForUpdates() {
|
||||
try {
|
||||
const currentVersion = await mastermind.getVersion();
|
||||
const response = await fetch('https://raw.githubusercontent.com/ShiftyX1/Mastermind/refs/heads/master/package.json');
|
||||
const currentVersion = await cheatingDaddy.getVersion();
|
||||
const response = await fetch('https://raw.githubusercontent.com/sohzm/cheating-daddy/refs/heads/master/package.json');
|
||||
if (!response.ok) return;
|
||||
|
||||
const remotePackage = await response.json();
|
||||
@ -344,7 +336,7 @@ export class AppHeader extends LitElement {
|
||||
|
||||
async _openUpdatePage() {
|
||||
const { ipcRenderer } = require('electron');
|
||||
await ipcRenderer.invoke('open-external', 'https://github.com/ShiftyX1/Mastermind');
|
||||
await ipcRenderer.invoke('open-external', 'https://cheatingdaddy.com');
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@ -396,15 +388,15 @@ export class AppHeader extends LitElement {
|
||||
|
||||
getViewTitle() {
|
||||
const titles = {
|
||||
onboarding: 'Welcome to Mastermind',
|
||||
main: 'Mastermind',
|
||||
onboarding: 'Welcome to Cheating Daddy',
|
||||
main: 'Cheating Daddy',
|
||||
customize: 'Customize',
|
||||
help: 'Help & Shortcuts',
|
||||
history: 'Conversation History',
|
||||
advanced: 'Advanced Tools',
|
||||
assistant: 'Mastermind',
|
||||
assistant: 'Cheating Daddy',
|
||||
};
|
||||
return titles[this.currentView] || 'Mastermind';
|
||||
return titles[this.currentView] || 'Cheating Daddy';
|
||||
}
|
||||
|
||||
getElapsedTime() {
|
||||
@ -429,7 +421,7 @@ export class AppHeader extends LitElement {
|
||||
const names = {
|
||||
'gemini': 'Gemini',
|
||||
'openai-realtime': 'OpenAI Realtime',
|
||||
'openai-sdk': 'OpenAI SDK',
|
||||
'openai-sdk': 'OpenAI SDK'
|
||||
};
|
||||
return names[this.aiProvider] || this.aiProvider;
|
||||
}
|
||||
@ -441,7 +433,7 @@ export class AppHeader extends LitElement {
|
||||
}
|
||||
|
||||
const { model, visionModel, whisperModel } = this.modelInfo;
|
||||
|
||||
|
||||
// Show a compact badge with tooltip for model details
|
||||
return html`
|
||||
<div class="model-badge-wrapper">
|
||||
@ -479,59 +471,39 @@ export class AppHeader extends LitElement {
|
||||
<span>${elapsedTime}</span>
|
||||
<div class="status-wrapper">
|
||||
<span class="status-text ${isError ? 'error' : ''}">${shortStatus}</span>
|
||||
${isError
|
||||
? html`
|
||||
<div class="status-tooltip">
|
||||
<div class="tooltip-label">Error Details</div>
|
||||
<div class="tooltip-content">${this.statusText}</div>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${isError ? html`
|
||||
<div class="status-tooltip">
|
||||
<div class="tooltip-label">Error Details</div>
|
||||
<div class="tooltip-content">${this.statusText}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
${this.isClickThrough ? html`<span class="click-through-indicator">click-through</span>` : ''}
|
||||
`
|
||||
: ''}
|
||||
${this.currentView === 'main'
|
||||
? html`
|
||||
${this.updateAvailable
|
||||
? html`
|
||||
<button class="update-button" @click=${this._openUpdatePage}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Update available
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
${this.updateAvailable ? html`
|
||||
<button class="update-button" @click=${this._openUpdatePage}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Update available
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="icon-button" @click=${this.onHistoryClick}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm.75-13a.75.75 0 0 0-1.5 0v5c0 .414.336.75.75.75h4a.75.75 0 0 0 0-1.5h-3.25V5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm.75-13a.75.75 0 0 0-1.5 0v5c0 .414.336.75.75.75h4a.75.75 0 0 0 0-1.5h-3.25V5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="icon-button" @click=${this.onCustomizeClick}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M7.84 1.804A1 1 0 0 1 8.82 1h2.36a1 1 0 0 1 .98.804l.331 1.652a6.993 6.993 0 0 1 1.929 1.115l1.598-.54a1 1 0 0 1 1.186.447l1.18 2.044a1 1 0 0 1-.205 1.251l-1.267 1.113a7.047 7.047 0 0 1 0 2.228l1.267 1.113a1 1 0 0 1 .206 1.25l-1.18 2.045a1 1 0 0 1-1.187.447l-1.598-.54a6.993 6.993 0 0 1-1.929 1.115l-.33 1.652a1 1 0 0 1-.98.804H8.82a1 1 0 0 1-.98-.804l-.331-1.652a6.993 6.993 0 0 1-1.929-1.115l-1.598.54a1 1 0 0 1-1.186-.447l-1.18-2.044a1 1 0 0 1 .205-1.251l1.267-1.114a7.05 7.05 0 0 1 0-2.227L1.821 7.773a1 1 0 0 1-.206-1.25l1.18-2.045a1 1 0 0 1 1.187-.447l1.598.54A6.992 6.992 0 0 1 7.51 3.456l.33-1.652ZM10 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path fill-rule="evenodd" d="M7.84 1.804A1 1 0 0 1 8.82 1h2.36a1 1 0 0 1 .98.804l.331 1.652a6.993 6.993 0 0 1 1.929 1.115l1.598-.54a1 1 0 0 1 1.186.447l1.18 2.044a1 1 0 0 1-.205 1.251l-1.267 1.113a7.047 7.047 0 0 1 0 2.228l1.267 1.113a1 1 0 0 1 .206 1.25l-1.18 2.045a1 1 0 0 1-1.187.447l-1.598-.54a6.993 6.993 0 0 1-1.929 1.115l-.33 1.652a1 1 0 0 1-.98.804H8.82a1 1 0 0 1-.98-.804l-.331-1.652a6.993 6.993 0 0 1-1.929-1.115l-1.598.54a1 1 0 0 1-1.186-.447l-1.18-2.044a1 1 0 0 1 .205-1.251l1.267-1.114a7.05 7.05 0 0 1 0-2.227L1.821 7.773a1 1 0 0 1-.206-1.25l1.18-2.045a1 1 0 0 1 1.187-.447l1.598.54A6.992 6.992 0 0 1 7.51 3.456l.33-1.652ZM10 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="icon-button" @click=${this.onHelpClick}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0ZM8.94 6.94a.75.75 0 1 1-1.061-1.061 3 3 0 1 1 2.871 5.026v.345a.75.75 0 0 1-1.5 0v-.5c0-.72.57-1.172 1.081-1.287A1.5 1.5 0 1 0 8.94 6.94ZM10 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0ZM8.94 6.94a.75.75 0 1 1-1.061-1.061 3 3 0 1 1 2.871 5.026v.345a.75.75 0 0 1-1.5 0v-.5c0-.72.57-1.172 1.081-1.287A1.5 1.5 0 1 0 8.94 6.94ZM10 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
`
|
||||
@ -539,23 +511,19 @@ export class AppHeader extends LitElement {
|
||||
${this.currentView === 'assistant'
|
||||
? html`
|
||||
<button @click=${this.onHideToggleClick} class="button">
|
||||
Hide <span class="key" style="pointer-events: none;">${mastermind.isMacOS ? 'Cmd' : 'Ctrl'}</span
|
||||
Hide <span class="key" style="pointer-events: none;">${cheatingDaddy.isMacOS ? 'Cmd' : 'Ctrl'}</span
|
||||
> <span class="key">\</span>
|
||||
</button>
|
||||
<button @click=${this.onCloseClick} class="icon-button window-close">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"
|
||||
/>
|
||||
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
|
||||
</svg>
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
<button @click=${this.isNavigationView() ? this.onBackClick : this.onCloseClick} class="icon-button window-close">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"
|
||||
/>
|
||||
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
|
||||
</svg>
|
||||
</button>
|
||||
`}
|
||||
|
||||
@ -6,17 +6,12 @@ import { HelpView } from '../views/HelpView.js';
|
||||
import { HistoryView } from '../views/HistoryView.js';
|
||||
import { AssistantView } from '../views/AssistantView.js';
|
||||
import { OnboardingView } from '../views/OnboardingView.js';
|
||||
import { ScreenPickerDialog } from '../views/ScreenPickerDialog.js';
|
||||
|
||||
export class MastermindApp extends LitElement {
|
||||
export class CheatingDaddyApp extends LitElement {
|
||||
static styles = css`
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family:
|
||||
'Inter',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
sans-serif;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
cursor: default;
|
||||
@ -117,8 +112,6 @@ export class MastermindApp extends LitElement {
|
||||
_storageLoaded: { state: true },
|
||||
aiProvider: { type: String },
|
||||
modelInfo: { type: Object },
|
||||
showScreenPicker: { type: Boolean },
|
||||
screenSources: { type: Array },
|
||||
};
|
||||
|
||||
constructor() {
|
||||
@ -144,8 +137,6 @@ export class MastermindApp extends LitElement {
|
||||
this._storageLoaded = false;
|
||||
this.aiProvider = 'gemini';
|
||||
this.modelInfo = { model: '', visionModel: '', whisperModel: '' };
|
||||
this.showScreenPicker = false;
|
||||
this.screenSources = [];
|
||||
|
||||
// Load from storage
|
||||
this._loadFromStorage();
|
||||
@ -154,16 +145,19 @@ export class MastermindApp extends LitElement {
|
||||
async _loadFromStorage() {
|
||||
try {
|
||||
const [config, prefs, openaiSdkCreds] = await Promise.all([
|
||||
mastermind.storage.getConfig(),
|
||||
mastermind.storage.getPreferences(),
|
||||
mastermind.storage.getOpenAISDKCredentials(),
|
||||
cheatingDaddy.storage.getConfig(),
|
||||
cheatingDaddy.storage.getPreferences(),
|
||||
cheatingDaddy.storage.getOpenAISDKCredentials()
|
||||
]);
|
||||
|
||||
// Check onboarding status
|
||||
this.currentView = config.onboarded ? 'main' : 'onboarding';
|
||||
|
||||
// Apply background appearance (color + transparency)
|
||||
this.applyBackgroundAppearance(prefs.backgroundColor ?? '#1e1e1e', prefs.backgroundTransparency ?? 0.8);
|
||||
this.applyBackgroundAppearance(
|
||||
prefs.backgroundColor ?? '#1e1e1e',
|
||||
prefs.backgroundTransparency ?? 0.8
|
||||
);
|
||||
|
||||
// Load preferences
|
||||
this.selectedProfile = prefs.selectedProfile || 'interview';
|
||||
@ -171,13 +165,13 @@ export class MastermindApp extends LitElement {
|
||||
this.selectedScreenshotInterval = prefs.selectedScreenshotInterval || '5';
|
||||
this.selectedImageQuality = prefs.selectedImageQuality || 'medium';
|
||||
this.layoutMode = config.layout || 'normal';
|
||||
|
||||
|
||||
// Load AI provider and model info
|
||||
this.aiProvider = prefs.aiProvider || 'gemini';
|
||||
this.modelInfo = {
|
||||
model: openaiSdkCreds.model || 'gpt-4o',
|
||||
visionModel: openaiSdkCreds.visionModel || 'gpt-4o',
|
||||
whisperModel: openaiSdkCreds.whisperModel || 'whisper-1',
|
||||
whisperModel: openaiSdkCreds.whisperModel || 'whisper-1'
|
||||
};
|
||||
|
||||
this._storageLoaded = true;
|
||||
@ -192,20 +186,18 @@ export class MastermindApp extends LitElement {
|
||||
|
||||
hexToRgb(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
}
|
||||
: { r: 30, g: 30, b: 30 };
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : { r: 30, g: 30, b: 30 };
|
||||
}
|
||||
|
||||
lightenColor(rgb, amount) {
|
||||
return {
|
||||
r: Math.min(255, rgb.r + amount),
|
||||
g: Math.min(255, rgb.g + amount),
|
||||
b: Math.min(255, rgb.b + amount),
|
||||
b: Math.min(255, rgb.b + amount)
|
||||
};
|
||||
}
|
||||
|
||||
@ -321,11 +313,11 @@ export class MastermindApp extends LitElement {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
async handleClose() {
|
||||
async handleClose() {
|
||||
if (this.currentView === 'customize' || this.currentView === 'help' || this.currentView === 'history') {
|
||||
this.currentView = 'main';
|
||||
} else if (this.currentView === 'assistant') {
|
||||
mastermind.stopCapture();
|
||||
cheatingDaddy.stopCapture();
|
||||
|
||||
// Close the session
|
||||
if (window.require) {
|
||||
@ -354,7 +346,7 @@ export class MastermindApp extends LitElement {
|
||||
// Main view event handlers
|
||||
async handleStart() {
|
||||
// check if api key is empty do nothing
|
||||
const apiKey = await mastermind.storage.getApiKey();
|
||||
const apiKey = await cheatingDaddy.storage.getApiKey();
|
||||
if (!apiKey || apiKey === '') {
|
||||
// Trigger the red blink animation on the API key input
|
||||
const mainView = this.shadowRoot.querySelector('main-view');
|
||||
@ -364,9 +356,9 @@ export class MastermindApp extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
await mastermind.initializeGemini(this.selectedProfile, this.selectedLanguage);
|
||||
await cheatingDaddy.initializeGemini(this.selectedProfile, this.selectedLanguage);
|
||||
// Pass the screenshot interval as string (including 'manual' option)
|
||||
mastermind.startCapture(this.selectedScreenshotInterval, this.selectedImageQuality);
|
||||
cheatingDaddy.startCapture(this.selectedScreenshotInterval, this.selectedImageQuality);
|
||||
this.responses = [];
|
||||
this.currentResponseIndex = -1;
|
||||
this.startTime = Date.now();
|
||||
@ -383,22 +375,22 @@ export class MastermindApp extends LitElement {
|
||||
// Customize view event handlers
|
||||
async handleProfileChange(profile) {
|
||||
this.selectedProfile = profile;
|
||||
await mastermind.storage.updatePreference('selectedProfile', profile);
|
||||
await cheatingDaddy.storage.updatePreference('selectedProfile', profile);
|
||||
}
|
||||
|
||||
async handleLanguageChange(language) {
|
||||
this.selectedLanguage = language;
|
||||
await mastermind.storage.updatePreference('selectedLanguage', language);
|
||||
await cheatingDaddy.storage.updatePreference('selectedLanguage', language);
|
||||
}
|
||||
|
||||
async handleScreenshotIntervalChange(interval) {
|
||||
this.selectedScreenshotInterval = interval;
|
||||
await mastermind.storage.updatePreference('selectedScreenshotInterval', interval);
|
||||
await cheatingDaddy.storage.updatePreference('selectedScreenshotInterval', interval);
|
||||
}
|
||||
|
||||
async handleImageQualityChange(quality) {
|
||||
this.selectedImageQuality = quality;
|
||||
await mastermind.storage.updatePreference('selectedImageQuality', quality);
|
||||
await cheatingDaddy.storage.updatePreference('selectedImageQuality', quality);
|
||||
}
|
||||
|
||||
handleBackClick() {
|
||||
@ -416,7 +408,7 @@ export class MastermindApp extends LitElement {
|
||||
|
||||
// Assistant view event handlers
|
||||
async handleSendText(message) {
|
||||
const result = await window.mastermind.sendTextMessage(message);
|
||||
const result = await window.cheatingDaddy.sendTextMessage(message);
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Failed to send message:', result.error);
|
||||
@ -528,11 +520,11 @@ export class MastermindApp extends LitElement {
|
||||
|
||||
render() {
|
||||
const viewClassMap = {
|
||||
assistant: 'assistant-view',
|
||||
onboarding: 'onboarding-view',
|
||||
customize: 'settings-view',
|
||||
help: 'help-view',
|
||||
history: 'history-view',
|
||||
'assistant': 'assistant-view',
|
||||
'onboarding': 'onboarding-view',
|
||||
'customize': 'settings-view',
|
||||
'help': 'help-view',
|
||||
'history': 'history-view',
|
||||
};
|
||||
const mainContentClass = `main-content ${viewClassMap[this.currentView] || 'with-border'}`;
|
||||
|
||||
@ -557,16 +549,6 @@ export class MastermindApp extends LitElement {
|
||||
<div class="view-container">${this.renderCurrentView()}</div>
|
||||
</div>
|
||||
</div>
|
||||
${this.showScreenPicker
|
||||
? html`
|
||||
<screen-picker-dialog
|
||||
?visible=${this.showScreenPicker}
|
||||
.sources=${this.screenSources}
|
||||
@source-selected=${this.handleSourceSelected}
|
||||
@cancelled=${this.handlePickerCancelled}
|
||||
></screen-picker-dialog>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -582,7 +564,7 @@ export class MastermindApp extends LitElement {
|
||||
|
||||
async handleLayoutModeChange(layoutMode) {
|
||||
this.layoutMode = layoutMode;
|
||||
await mastermind.storage.updateConfig('layout', layoutMode);
|
||||
await cheatingDaddy.storage.updateConfig('layout', layoutMode);
|
||||
this.updateLayoutMode();
|
||||
|
||||
// Notify main process about layout change for window resizing
|
||||
@ -597,44 +579,6 @@ export class MastermindApp extends LitElement {
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
async showScreenPickerDialog() {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
const result = await ipcRenderer.invoke('get-screen-sources');
|
||||
|
||||
if (result.success) {
|
||||
this.screenSources = result.sources;
|
||||
this.showScreenPicker = true;
|
||||
return new Promise(resolve => {
|
||||
this._screenPickerResolve = resolve;
|
||||
});
|
||||
} else {
|
||||
console.error('Failed to get screen sources:', result.error);
|
||||
return { cancelled: true };
|
||||
}
|
||||
}
|
||||
|
||||
async handleSourceSelected(event) {
|
||||
const { source } = event.detail;
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
// Tell main process which source was selected
|
||||
await ipcRenderer.invoke('set-selected-source', source.id);
|
||||
|
||||
this.showScreenPicker = false;
|
||||
if (this._screenPickerResolve) {
|
||||
this._screenPickerResolve({ source });
|
||||
this._screenPickerResolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
handlePickerCancelled() {
|
||||
this.showScreenPicker = false;
|
||||
if (this._screenPickerResolve) {
|
||||
this._screenPickerResolve({ cancelled: true });
|
||||
this._screenPickerResolve = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('mastermind-app', MastermindApp);
|
||||
customElements.define('cheating-daddy-app', CheatingDaddyApp);
|
||||
@ -1,5 +1,5 @@
|
||||
// Main app components
|
||||
export { MastermindApp } from './app/MastermindApp.js';
|
||||
export { CheatingDaddyApp } from './app/CheatingDaddyApp.js';
|
||||
export { AppHeader } from './app/AppHeader.js';
|
||||
|
||||
// View components
|
||||
|
||||
@ -9,11 +9,7 @@ export class AssistantView extends LitElement {
|
||||
}
|
||||
|
||||
* {
|
||||
font-family:
|
||||
'Inter',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
sans-serif;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@ -55,24 +51,12 @@ export class AssistantView extends LitElement {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.response-container h1 {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
.response-container h2 {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
.response-container h3 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.response-container h4 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.response-container h5 {
|
||||
font-size: 1em;
|
||||
}
|
||||
.response-container h6 {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.response-container h1 { font-size: 1.6em; }
|
||||
.response-container h2 { font-size: 1.4em; }
|
||||
.response-container h3 { font-size: 1.2em; }
|
||||
.response-container h4 { font-size: 1.1em; }
|
||||
.response-container h5 { font-size: 1em; }
|
||||
.response-container h6 { font-size: 0.9em; }
|
||||
|
||||
.response-container p {
|
||||
margin: 0.6em 0;
|
||||
@ -284,11 +268,9 @@ export class AssistantView extends LitElement {
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition:
|
||||
opacity 0.15s ease,
|
||||
visibility 0.15s ease;
|
||||
transition: opacity 0.15s ease, visibility 0.15s ease;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@ -328,7 +310,7 @@ export class AssistantView extends LitElement {
|
||||
.tooltip-note {
|
||||
margin-top: 6px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
opacity: 0.5;
|
||||
font-size: 10px;
|
||||
}
|
||||
@ -366,57 +348,6 @@ export class AssistantView extends LitElement {
|
||||
.region-select-btn span {
|
||||
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 = {
|
||||
@ -428,9 +359,6 @@ export class AssistantView extends LitElement {
|
||||
flashCount: { type: Number },
|
||||
flashLiteCount: { type: Number },
|
||||
aiProvider: { type: String },
|
||||
pushToTalkActive: { type: Boolean },
|
||||
audioInputMode: { type: String },
|
||||
pushToTalkKeybind: { type: String },
|
||||
};
|
||||
|
||||
constructor() {
|
||||
@ -442,9 +370,6 @@ export class AssistantView extends LitElement {
|
||||
this.flashCount = 0;
|
||||
this.flashLiteCount = 0;
|
||||
this.aiProvider = 'gemini';
|
||||
this.pushToTalkActive = false;
|
||||
this.audioInputMode = 'auto';
|
||||
this.pushToTalkKeybind = '';
|
||||
}
|
||||
|
||||
getProfileNames() {
|
||||
@ -564,7 +489,6 @@ export class AssistantView extends LitElement {
|
||||
|
||||
// Load limits on mount
|
||||
this.loadLimits();
|
||||
this.loadPushToTalkKeybind();
|
||||
|
||||
// Set up IPC listeners for keyboard shortcuts
|
||||
if (window.require) {
|
||||
@ -590,17 +514,10 @@ export class AssistantView extends LitElement {
|
||||
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-next-response', this.handleNextResponse);
|
||||
ipcRenderer.on('scroll-response-up', this.handleScrollUp);
|
||||
ipcRenderer.on('scroll-response-down', this.handleScrollDown);
|
||||
ipcRenderer.on('push-to-talk-state', this.handlePushToTalkState);
|
||||
}
|
||||
}
|
||||
|
||||
@ -622,9 +539,6 @@ export class AssistantView extends LitElement {
|
||||
if (this.handleScrollDown) {
|
||||
ipcRenderer.removeListener('scroll-response-down', this.handleScrollDown);
|
||||
}
|
||||
if (this.handlePushToTalkState) {
|
||||
ipcRenderer.removeListener('push-to-talk-state', this.handlePushToTalkState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -645,22 +559,13 @@ export class AssistantView extends LitElement {
|
||||
}
|
||||
|
||||
async loadLimits() {
|
||||
if (window.mastermind?.storage?.getTodayLimits) {
|
||||
const limits = await window.mastermind.storage.getTodayLimits();
|
||||
if (window.cheatingDaddy?.storage?.getTodayLimits) {
|
||||
const limits = await window.cheatingDaddy.storage.getTodayLimits();
|
||||
this.flashCount = limits.flash?.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() {
|
||||
return this.flashCount + this.flashLiteCount;
|
||||
}
|
||||
@ -685,14 +590,6 @@ export class AssistantView extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
handlePushToTalkToggle() {
|
||||
if (!window.require) {
|
||||
return;
|
||||
}
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.send('push-to-talk-toggle');
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
setTimeout(() => {
|
||||
const container = this.shadowRoot.querySelector('.response-container');
|
||||
@ -734,26 +631,10 @@ export class AssistantView extends LitElement {
|
||||
|
||||
render() {
|
||||
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`
|
||||
<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">
|
||||
<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">
|
||||
@ -772,53 +653,32 @@ export class AssistantView extends LitElement {
|
||||
<input type="text" id="textInput" placeholder="Type a message to the AI..." @keydown=${this.handleTextKeydown} />
|
||||
|
||||
<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)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.25 2A2.25 2.25 0 0 0 2 4.25v2.5A.75.75 0 0 0 3.5 6.75v-2.5a.75.75 0 0 1 .75-.75h2.5A.75.75 0 0 0 6.75 2h-2.5Zm9.5 0a.75.75 0 0 0 0 1.5h2.5a.75.75 0 0 1 .75.75v2.5a.75.75 0 0 0 1.5 0v-2.5A2.25 2.25 0 0 0 16.25 2h-2.5ZM3.5 13.25a.75.75 0 0 0-1.5 0v2.5A2.25 2.25 0 0 0 4.25 18h2.5a.75.75 0 0 0 0-1.5h-2.5a.75.75 0 0 1-.75-.75v-2.5Zm13.5 0a.75.75 0 0 0 1.5 0v2.5A2.25 2.25 0 0 1 16.25 18h-2.5a.75.75 0 0 1 0-1.5h2.5a.75.75 0 0 0 .75-.75v-2.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path fill-rule="evenodd" d="M4.25 2A2.25 2.25 0 0 0 2 4.25v2.5A.75.75 0 0 0 3.5 6.75v-2.5a.75.75 0 0 1 .75-.75h2.5A.75.75 0 0 0 6.75 2h-2.5Zm9.5 0a.75.75 0 0 0 0 1.5h2.5a.75.75 0 0 1 .75.75v2.5a.75.75 0 0 0 1.5 0v-2.5A2.25 2.25 0 0 0 16.25 2h-2.5ZM3.5 13.25a.75.75 0 0 0-1.5 0v2.5A2.25 2.25 0 0 0 4.25 18h2.5a.75.75 0 0 0 0-1.5h-2.5a.75.75 0 0 1-.75-.75v-2.5Zm13.5 0a.75.75 0 0 0 1.5 0v2.5A2.25 2.25 0 0 1 16.25 18h-2.5a.75.75 0 0 1 0-1.5h2.5a.75.75 0 0 0 .75-.75v-2.5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<span>Select region</span>
|
||||
</button>
|
||||
<div class="screen-answer-btn-wrapper">
|
||||
${this.aiProvider === 'gemini'
|
||||
? html`
|
||||
<div class="tooltip">
|
||||
<div class="tooltip-row">
|
||||
<span class="tooltip-label">Flash</span>
|
||||
<span class="tooltip-value">${this.flashCount}/20</span>
|
||||
</div>
|
||||
<div class="tooltip-row">
|
||||
<span class="tooltip-label">Flash Lite</span>
|
||||
<span class="tooltip-value">${this.flashLiteCount}/20</span>
|
||||
</div>
|
||||
<div class="tooltip-note">Resets every 24 hours</div>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${this.aiProvider === 'gemini' ? html`
|
||||
<div class="tooltip">
|
||||
<div class="tooltip-row">
|
||||
<span class="tooltip-label">Flash</span>
|
||||
<span class="tooltip-value">${this.flashCount}/20</span>
|
||||
</div>
|
||||
<div class="tooltip-row">
|
||||
<span class="tooltip-label">Flash Lite</span>
|
||||
<span class="tooltip-value">${this.flashLiteCount}/20</span>
|
||||
</div>
|
||||
<div class="tooltip-note">Resets every 24 hours</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<button class="screen-answer-btn" @click=${this.handleScreenAnswer}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
d="M15.98 1.804a1 1 0 0 0-1.96 0l-.24 1.192a1 1 0 0 1-.784.785l-1.192.238a1 1 0 0 0 0 1.962l1.192.238a1 1 0 0 1 .785.785l.238 1.192a1 1 0 0 0 1.962 0l.238-1.192a1 1 0 0 1 .785-.785l1.192-.238a1 1 0 0 0 0-1.962l-1.192-.238a1 1 0 0 1-.785-.785l-.238-1.192ZM6.949 5.684a1 1 0 0 0-1.898 0l-.683 2.051a1 1 0 0 1-.633.633l-2.051.683a1 1 0 0 0 0 1.898l2.051.684a1 1 0 0 1 .633.632l.683 2.051a1 1 0 0 0 1.898 0l.683-2.051a1 1 0 0 1 .633-.633l2.051-.683a1 1 0 0 0 0-1.898l-2.051-.683a1 1 0 0 1-.633-.633L6.95 5.684ZM13.949 13.684a1 1 0 0 0-1.898 0l-.184.551a1 1 0 0 1-.632.633l-.551.183a1 1 0 0 0 0 1.898l.551.183a1 1 0 0 1 .633.633l.183.551a1 1 0 0 0 1.898 0l.184-.551a1 1 0 0 1 .632-.633l.551-.183a1 1 0 0 0 0-1.898l-.551-.184a1 1 0 0 1-.633-.632l-.183-.551Z"
|
||||
/>
|
||||
<path d="M15.98 1.804a1 1 0 0 0-1.96 0l-.24 1.192a1 1 0 0 1-.784.785l-1.192.238a1 1 0 0 0 0 1.962l1.192.238a1 1 0 0 1 .785.785l.238 1.192a1 1 0 0 0 1.962 0l.238-1.192a1 1 0 0 1 .785-.785l1.192-.238a1 1 0 0 0 0-1.962l-1.192-.238a1 1 0 0 1-.785-.785l-.238-1.192ZM6.949 5.684a1 1 0 0 0-1.898 0l-.683 2.051a1 1 0 0 1-.633.633l-2.051.683a1 1 0 0 0 0 1.898l2.051.684a1 1 0 0 1 .633.632l.683 2.051a1 1 0 0 0 1.898 0l.683-2.051a1 1 0 0 1 .633-.633l2.051-.683a1 1 0 0 0 0-1.898l-2.051-.683a1 1 0 0 1-.633-.633L6.95 5.684ZM13.949 13.684a1 1 0 0 0-1.898 0l-.184.551a1 1 0 0 1-.632.633l-.551.183a1 1 0 0 0 0 1.898l.551.183a1 1 0 0 1 .633.633l.183.551a1 1 0 0 0 1.898 0l.184-.551a1 1 0 0 1 .632-.633l.551-.183a1 1 0 0 0 0-1.898l-.551-.184a1 1 0 0 1-.633-.632l-.183-.551Z" />
|
||||
</svg>
|
||||
<span>Full screen</span>
|
||||
${this.aiProvider === 'gemini'
|
||||
? html`<span class="usage-count">(${this.getTotalUsed()}/${this.getTotalAvailable()})</span>`
|
||||
: ''}
|
||||
${this.aiProvider === 'gemini' ? html`<span class="usage-count">(${this.getTotalUsed()}/${this.getTotalAvailable()})</span>` : ''}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4,11 +4,7 @@ import { resizeLayout } from '../../utils/windowResize.js';
|
||||
export class CustomizeView extends LitElement {
|
||||
static styles = css`
|
||||
* {
|
||||
font-family:
|
||||
'Inter',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
sans-serif;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
@ -537,7 +533,6 @@ export class CustomizeView extends LitElement {
|
||||
color: var(--error-color);
|
||||
border-left: 2px solid var(--error-color);
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
static properties = {
|
||||
@ -550,7 +545,6 @@ export class CustomizeView extends LitElement {
|
||||
backgroundTransparency: { type: Number },
|
||||
fontSize: { type: Number },
|
||||
theme: { type: String },
|
||||
audioInputMode: { type: String },
|
||||
onProfileChange: { type: Function },
|
||||
onLanguageChange: { type: Function },
|
||||
onImageQualityChange: { type: Function },
|
||||
@ -589,7 +583,6 @@ export class CustomizeView extends LitElement {
|
||||
|
||||
// Audio mode default
|
||||
this.audioMode = 'speaker_only';
|
||||
this.audioInputMode = 'auto';
|
||||
|
||||
// Custom prompt
|
||||
this.customPrompt = '';
|
||||
@ -617,7 +610,7 @@ export class CustomizeView extends LitElement {
|
||||
}
|
||||
|
||||
getThemes() {
|
||||
return mastermind.theme.getAll();
|
||||
return cheatingDaddy.theme.getAll();
|
||||
}
|
||||
|
||||
setActiveSection(section) {
|
||||
@ -641,87 +634,31 @@ export class CustomizeView extends LitElement {
|
||||
|
||||
renderSidebarIcon(icon) {
|
||||
const icons = {
|
||||
user: html`<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M19 21V19C19 17.9391 18.5786 16.9217 17.8284 16.1716C17.0783 15.4214 16.0609 15 15 15H9C7.93913 15 6.92172 15.4214 6.17157 16.1716C5.42143 16.9217 5 17.9391 5 19V21"
|
||||
></path>
|
||||
user: html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 21V19C19 17.9391 18.5786 16.9217 17.8284 16.1716C17.0783 15.4214 16.0609 15 15 15H9C7.93913 15 6.92172 15.4214 6.17157 16.1716C5.42143 16.9217 5 17.9391 5 19V21"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>`,
|
||||
mic: html`<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
mic: html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
|
||||
<line x1="12" y1="19" x2="12" y2="23"></line>
|
||||
<line x1="8" y1="23" x2="16" y2="23"></line>
|
||||
</svg>`,
|
||||
globe: html`<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
globe: html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="2" y1="12" x2="22" y2="12"></line>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
|
||||
</svg>`,
|
||||
display: html`<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
display: html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
|
||||
<line x1="8" y1="21" x2="16" y2="21"></line>
|
||||
<line x1="12" y1="17" x2="12" y2="21"></line>
|
||||
</svg>`,
|
||||
camera: html`<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
camera: html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path>
|
||||
<circle cx="12" cy="13" r="4"></circle>
|
||||
</svg>`,
|
||||
keyboard: html`<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
keyboard: html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect>
|
||||
<path d="M6 8h.001"></path>
|
||||
<path d="M10 8h.001"></path>
|
||||
@ -732,29 +669,11 @@ export class CustomizeView extends LitElement {
|
||||
<path d="M16 12h.001"></path>
|
||||
<path d="M7 16h10"></path>
|
||||
</svg>`,
|
||||
search: html`<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
search: html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>`,
|
||||
cpu: html`<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
cpu: html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
|
||||
<rect x="9" y="9" width="6" height="6"></rect>
|
||||
<line x1="9" y1="1" x2="9" y2="4"></line>
|
||||
@ -766,16 +685,7 @@ export class CustomizeView extends LitElement {
|
||||
<line x1="1" y1="9" x2="4" y2="9"></line>
|
||||
<line x1="1" y1="14" x2="4" y2="14"></line>
|
||||
</svg>`,
|
||||
warning: html`<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
warning: html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
||||
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||
@ -787,30 +697,29 @@ export class CustomizeView extends LitElement {
|
||||
async _loadFromStorage() {
|
||||
try {
|
||||
const [prefs, keybinds, credentials, openaiCreds, openaiSdkCreds] = await Promise.all([
|
||||
mastermind.storage.getPreferences(),
|
||||
mastermind.storage.getKeybinds(),
|
||||
mastermind.storage.getCredentials(),
|
||||
mastermind.storage.getOpenAICredentials(),
|
||||
mastermind.storage.getOpenAISDKCredentials(),
|
||||
cheatingDaddy.storage.getPreferences(),
|
||||
cheatingDaddy.storage.getKeybinds(),
|
||||
cheatingDaddy.storage.getCredentials(),
|
||||
cheatingDaddy.storage.getOpenAICredentials(),
|
||||
cheatingDaddy.storage.getOpenAISDKCredentials()
|
||||
]);
|
||||
|
||||
this.googleSearchEnabled = prefs.googleSearchEnabled ?? true;
|
||||
this.backgroundTransparency = prefs.backgroundTransparency ?? 0.8;
|
||||
this.fontSize = prefs.fontSize ?? 20;
|
||||
this.audioMode = prefs.audioMode ?? 'speaker_only';
|
||||
this.audioInputMode = prefs.audioInputMode ?? 'auto';
|
||||
this.customPrompt = prefs.customPrompt ?? '';
|
||||
this.theme = prefs.theme ?? 'dark';
|
||||
this.aiProvider = prefs.aiProvider ?? 'gemini';
|
||||
|
||||
|
||||
// Load Gemini API key
|
||||
this.geminiApiKey = credentials.apiKey ?? '';
|
||||
|
||||
|
||||
// Load OpenAI Realtime credentials
|
||||
this.openaiApiKey = openaiCreds.apiKey ?? '';
|
||||
this.openaiBaseUrl = openaiCreds.baseUrl ?? '';
|
||||
this.openaiModel = openaiCreds.model ?? 'gpt-4o-realtime-preview-2024-12-17';
|
||||
|
||||
|
||||
// Load OpenAI SDK credentials
|
||||
this.openaiSdkApiKey = openaiSdkCreds.apiKey ?? '';
|
||||
this.openaiSdkBaseUrl = openaiSdkCreds.baseUrl ?? '';
|
||||
@ -824,7 +733,6 @@ export class CustomizeView extends LitElement {
|
||||
|
||||
this.updateBackgroundTransparency();
|
||||
this.updateFontSize();
|
||||
this.notifyPushToTalkSettings();
|
||||
this.requestUpdate();
|
||||
} catch (error) {
|
||||
console.error('Error loading settings:', error);
|
||||
@ -837,10 +745,6 @@ export class CustomizeView extends LitElement {
|
||||
resizeLayout();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
getProfiles() {
|
||||
return [
|
||||
{
|
||||
@ -944,46 +848,24 @@ export class CustomizeView extends LitElement {
|
||||
|
||||
async handleCustomPromptInput(e) {
|
||||
this.customPrompt = e.target.value;
|
||||
await mastermind.storage.updatePreference('customPrompt', e.target.value);
|
||||
await cheatingDaddy.storage.updatePreference('customPrompt', e.target.value);
|
||||
}
|
||||
|
||||
async handleAudioModeSelect(e) {
|
||||
this.audioMode = e.target.value;
|
||||
await mastermind.storage.updatePreference('audioMode', e.target.value);
|
||||
await cheatingDaddy.storage.updatePreference('audioMode', e.target.value);
|
||||
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) {
|
||||
this.theme = e.target.value;
|
||||
await mastermind.theme.save(this.theme);
|
||||
await cheatingDaddy.theme.save(this.theme);
|
||||
this.updateBackgroundAppearance();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
getDefaultKeybinds() {
|
||||
const isMac = mastermind.isMacOS || navigator.platform.includes('Mac');
|
||||
const isMac = cheatingDaddy.isMacOS || navigator.platform.includes('Mac');
|
||||
return {
|
||||
moveUp: isMac ? 'Alt+Up' : 'Ctrl+Up',
|
||||
moveDown: isMac ? 'Alt+Down' : 'Ctrl+Down',
|
||||
@ -996,12 +878,11 @@ export class CustomizeView extends LitElement {
|
||||
nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]',
|
||||
scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up',
|
||||
scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down',
|
||||
pushToTalk: isMac ? 'Ctrl+Space' : 'Ctrl+Space',
|
||||
};
|
||||
}
|
||||
|
||||
async saveKeybinds() {
|
||||
await mastermind.storage.setKeybinds(this.keybinds);
|
||||
await cheatingDaddy.storage.setKeybinds(this.keybinds);
|
||||
// Send to main process to update global shortcuts
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
@ -1017,7 +898,7 @@ export class CustomizeView extends LitElement {
|
||||
|
||||
async resetKeybinds() {
|
||||
this.keybinds = this.getDefaultKeybinds();
|
||||
await mastermind.storage.setKeybinds(null);
|
||||
await cheatingDaddy.storage.setKeybinds(null);
|
||||
this.requestUpdate();
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
@ -1082,11 +963,6 @@ export class CustomizeView extends LitElement {
|
||||
name: 'Scroll Response Down',
|
||||
description: 'Scroll the AI response content down',
|
||||
},
|
||||
{
|
||||
key: 'pushToTalk',
|
||||
name: 'Push-to-Talk',
|
||||
description: 'Activate audio recording (OpenAI SDK only)',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -1167,7 +1043,7 @@ export class CustomizeView extends LitElement {
|
||||
|
||||
async handleGoogleSearchChange(e) {
|
||||
this.googleSearchEnabled = e.target.checked;
|
||||
await mastermind.storage.updatePreference('googleSearchEnabled', this.googleSearchEnabled);
|
||||
await cheatingDaddy.storage.updatePreference('googleSearchEnabled', this.googleSearchEnabled);
|
||||
|
||||
// Notify main process if available
|
||||
if (window.require) {
|
||||
@ -1184,69 +1060,69 @@ export class CustomizeView extends LitElement {
|
||||
|
||||
async handleAIProviderChange(e) {
|
||||
this.aiProvider = e.target.value;
|
||||
await mastermind.storage.updatePreference('aiProvider', e.target.value);
|
||||
await cheatingDaddy.storage.updatePreference('aiProvider', e.target.value);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
async handleGeminiApiKeyInput(e) {
|
||||
this.geminiApiKey = e.target.value;
|
||||
await mastermind.storage.setApiKey(e.target.value);
|
||||
await cheatingDaddy.storage.setApiKey(e.target.value);
|
||||
}
|
||||
|
||||
async handleOpenAIApiKeyInput(e) {
|
||||
this.openaiApiKey = e.target.value;
|
||||
await mastermind.storage.setOpenAICredentials({
|
||||
apiKey: e.target.value,
|
||||
await cheatingDaddy.storage.setOpenAICredentials({
|
||||
apiKey: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
async handleOpenAIBaseUrlInput(e) {
|
||||
this.openaiBaseUrl = e.target.value;
|
||||
await mastermind.storage.setOpenAICredentials({
|
||||
baseUrl: e.target.value,
|
||||
await cheatingDaddy.storage.setOpenAICredentials({
|
||||
baseUrl: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
async handleOpenAIModelInput(e) {
|
||||
this.openaiModel = e.target.value;
|
||||
await mastermind.storage.setOpenAICredentials({
|
||||
model: e.target.value,
|
||||
await cheatingDaddy.storage.setOpenAICredentials({
|
||||
model: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
// OpenAI SDK handlers
|
||||
async handleOpenAISdkApiKeyInput(e) {
|
||||
this.openaiSdkApiKey = e.target.value;
|
||||
await mastermind.storage.setOpenAISDKCredentials({
|
||||
apiKey: e.target.value,
|
||||
await cheatingDaddy.storage.setOpenAISDKCredentials({
|
||||
apiKey: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
async handleOpenAISdkBaseUrlInput(e) {
|
||||
this.openaiSdkBaseUrl = e.target.value;
|
||||
await mastermind.storage.setOpenAISDKCredentials({
|
||||
baseUrl: e.target.value,
|
||||
await cheatingDaddy.storage.setOpenAISDKCredentials({
|
||||
baseUrl: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
async handleOpenAISdkModelInput(e) {
|
||||
this.openaiSdkModel = e.target.value;
|
||||
await mastermind.storage.setOpenAISDKCredentials({
|
||||
model: e.target.value,
|
||||
await cheatingDaddy.storage.setOpenAISDKCredentials({
|
||||
model: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
async handleOpenAISdkVisionModelInput(e) {
|
||||
this.openaiSdkVisionModel = e.target.value;
|
||||
await mastermind.storage.setOpenAISDKCredentials({
|
||||
visionModel: e.target.value,
|
||||
await cheatingDaddy.storage.setOpenAISDKCredentials({
|
||||
visionModel: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
async handleOpenAISdkWhisperModelInput(e) {
|
||||
this.openaiSdkWhisperModel = e.target.value;
|
||||
await mastermind.storage.setOpenAISDKCredentials({
|
||||
whisperModel: e.target.value,
|
||||
await cheatingDaddy.storage.setOpenAISDKCredentials({
|
||||
whisperModel: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
@ -1259,7 +1135,7 @@ export class CustomizeView extends LitElement {
|
||||
this.requestUpdate();
|
||||
|
||||
try {
|
||||
await mastermind.storage.clearAll();
|
||||
await cheatingDaddy.storage.clearAll();
|
||||
|
||||
this.clearStatusMessage = 'Successfully cleared all local data';
|
||||
this.clearStatusType = 'success';
|
||||
@ -1288,15 +1164,15 @@ export class CustomizeView extends LitElement {
|
||||
|
||||
async handleBackgroundTransparencyChange(e) {
|
||||
this.backgroundTransparency = parseFloat(e.target.value);
|
||||
await mastermind.storage.updatePreference('backgroundTransparency', this.backgroundTransparency);
|
||||
await cheatingDaddy.storage.updatePreference('backgroundTransparency', this.backgroundTransparency);
|
||||
this.updateBackgroundAppearance();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
updateBackgroundAppearance() {
|
||||
// Use theme's background color
|
||||
const colors = mastermind.theme.get(this.theme);
|
||||
mastermind.theme.applyBackgrounds(colors.background, this.backgroundTransparency);
|
||||
const colors = cheatingDaddy.theme.get(this.theme);
|
||||
cheatingDaddy.theme.applyBackgrounds(colors.background, this.backgroundTransparency);
|
||||
}
|
||||
|
||||
// Keep old function name for backwards compatibility
|
||||
@ -1306,7 +1182,7 @@ export class CustomizeView extends LitElement {
|
||||
|
||||
async handleFontSizeChange(e) {
|
||||
this.fontSize = parseInt(e.target.value, 10);
|
||||
await mastermind.storage.updatePreference('fontSize', this.fontSize);
|
||||
await cheatingDaddy.storage.updatePreference('fontSize', this.fontSize);
|
||||
this.updateFontSize();
|
||||
this.requestUpdate();
|
||||
}
|
||||
@ -1333,7 +1209,9 @@ export class CustomizeView extends LitElement {
|
||||
<select class="form-control" .value=${this.selectedProfile} @change=${this.handleProfileSelect}>
|
||||
${profiles.map(
|
||||
profile => html`
|
||||
<option value=${profile.value} ?selected=${this.selectedProfile === profile.value}>${profile.name}</option>
|
||||
<option value=${profile.value} ?selected=${this.selectedProfile === profile.value}>
|
||||
${profile.name}
|
||||
</option>
|
||||
`
|
||||
)}
|
||||
</select>
|
||||
@ -1343,12 +1221,15 @@ export class CustomizeView extends LitElement {
|
||||
<label class="form-label">Custom AI Instructions</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
placeholder="Add specific instructions for how you want the AI to behave during ${profileNames[this.selectedProfile] ||
|
||||
'this interaction'}..."
|
||||
placeholder="Add specific instructions for how you want the AI to behave during ${
|
||||
profileNames[this.selectedProfile] || 'this interaction'
|
||||
}..."
|
||||
.value=${this.customPrompt}
|
||||
@input=${this.handleCustomPromptInput}
|
||||
></textarea>
|
||||
<div class="form-description">Personalize the AI's behavior with specific instructions</div>
|
||||
<div class="form-description">
|
||||
Personalize the AI's behavior with specific instructions
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1356,9 +1237,6 @@ export class CustomizeView extends LitElement {
|
||||
}
|
||||
|
||||
renderAudioSection() {
|
||||
const isPushToTalkAvailable = this.aiProvider === 'openai-sdk';
|
||||
const pushToTalkDisabled = !isPushToTalkAvailable;
|
||||
|
||||
return html`
|
||||
<div class="content-header">Audio Settings</div>
|
||||
<div class="form-grid">
|
||||
@ -1369,30 +1247,10 @@ export class CustomizeView extends LitElement {
|
||||
<option value="mic_only">Microphone Only (Me)</option>
|
||||
<option value="both">Both Speaker & Microphone</option>
|
||||
</select>
|
||||
<div class="form-description">Choose which audio sources to capture for the AI.</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.'}
|
||||
Choose which audio sources to capture for the AI.
|
||||
</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>
|
||||
`;
|
||||
}
|
||||
@ -1412,7 +1270,9 @@ export class CustomizeView extends LitElement {
|
||||
<select class="form-control" .value=${this.selectedLanguage} @change=${this.handleLanguageSelect}>
|
||||
${languages.map(
|
||||
language => html`
|
||||
<option value=${language.value} ?selected=${this.selectedLanguage === language.value}>${language.name}</option>
|
||||
<option value=${language.value} ?selected=${this.selectedLanguage === language.value}>
|
||||
${language.name}
|
||||
</option>
|
||||
`
|
||||
)}
|
||||
</select>
|
||||
@ -1435,9 +1295,17 @@ export class CustomizeView extends LitElement {
|
||||
<span class="current-selection">${currentTheme?.name || 'Dark'}</span>
|
||||
</label>
|
||||
<select class="form-control" .value=${this.theme} @change=${this.handleThemeChange}>
|
||||
${themes.map(theme => html` <option value=${theme.value} ?selected=${this.theme === theme.value}>${theme.name}</option> `)}
|
||||
${themes.map(
|
||||
theme => html`
|
||||
<option value=${theme.value} ?selected=${this.theme === theme.value}>
|
||||
${theme.name}
|
||||
</option>
|
||||
`
|
||||
)}
|
||||
</select>
|
||||
<div class="form-description">Choose a color theme for the interface</div>
|
||||
<div class="form-description">
|
||||
Choose a color theme for the interface
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@ -1450,7 +1318,10 @@ export class CustomizeView extends LitElement {
|
||||
<option value="compact" ?selected=${this.layoutMode === 'compact'}>Compact</option>
|
||||
</select>
|
||||
<div class="form-description">
|
||||
${this.layoutMode === 'compact' ? 'Smaller window with reduced padding' : 'Standard layout with comfortable spacing'}
|
||||
${this.layoutMode === 'compact'
|
||||
? 'Smaller window with reduced padding'
|
||||
: 'Standard layout with comfortable spacing'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1508,9 +1379,7 @@ export class CustomizeView extends LitElement {
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
Image Quality
|
||||
<span class="current-selection"
|
||||
>${this.selectedImageQuality.charAt(0).toUpperCase() + this.selectedImageQuality.slice(1)}</span
|
||||
>
|
||||
<span class="current-selection">${this.selectedImageQuality.charAt(0).toUpperCase() + this.selectedImageQuality.slice(1)}</span>
|
||||
</label>
|
||||
<select class="form-control" .value=${this.selectedImageQuality} @change=${this.handleImageQualitySelect}>
|
||||
<option value="high" ?selected=${this.selectedImageQuality === 'high'}>High Quality</option>
|
||||
@ -1522,7 +1391,8 @@ export class CustomizeView extends LitElement {
|
||||
? 'Best quality, uses more tokens'
|
||||
: this.selectedImageQuality === 'medium'
|
||||
? 'Balanced quality and token usage'
|
||||
: 'Lower quality, uses fewer tokens'}
|
||||
: 'Lower quality, uses fewer tokens'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1576,9 +1446,9 @@ export class CustomizeView extends LitElement {
|
||||
|
||||
renderAIProviderSection() {
|
||||
const providerNames = {
|
||||
gemini: 'Google Gemini',
|
||||
'gemini': 'Google Gemini',
|
||||
'openai-realtime': 'OpenAI Realtime',
|
||||
'openai-sdk': 'OpenAI SDK (BotHub, etc.)',
|
||||
'openai-sdk': 'OpenAI SDK (BotHub, etc.)'
|
||||
};
|
||||
|
||||
return html`
|
||||
@ -1594,136 +1464,135 @@ export class CustomizeView extends LitElement {
|
||||
<option value="openai-realtime" ?selected=${this.aiProvider === 'openai-realtime'}>OpenAI Realtime API</option>
|
||||
<option value="openai-sdk" ?selected=${this.aiProvider === 'openai-sdk'}>OpenAI SDK (BotHub, Azure, etc.)</option>
|
||||
</select>
|
||||
<div class="form-description">Choose which AI provider to use for conversations and screen analysis</div>
|
||||
<div class="form-description">
|
||||
Choose which AI provider to use for conversations and screen analysis
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.aiProvider === 'gemini'
|
||||
? html`
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Gemini API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
placeholder="Enter your Gemini API key"
|
||||
.value=${this.geminiApiKey}
|
||||
@input=${this.handleGeminiApiKeyInput}
|
||||
/>
|
||||
<div class="form-description">
|
||||
Get your API key from
|
||||
<a href="https://aistudio.google.com/app/apikey" target="_blank" style="color: var(--text-color);"
|
||||
>Google AI Studio</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: this.aiProvider === 'openai-realtime'
|
||||
? html`
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">OpenAI API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
placeholder="Enter your OpenAI API key"
|
||||
.value=${this.openaiApiKey}
|
||||
@input=${this.handleOpenAIApiKeyInput}
|
||||
/>
|
||||
<div class="form-description">
|
||||
Get your API key from
|
||||
<a href="https://platform.openai.com/api-keys" target="_blank" style="color: var(--text-color);"
|
||||
>OpenAI Platform</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
${this.aiProvider === 'gemini' ? html`
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Gemini API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
placeholder="Enter your Gemini API key"
|
||||
.value=${this.geminiApiKey}
|
||||
@input=${this.handleGeminiApiKeyInput}
|
||||
/>
|
||||
<div class="form-description">
|
||||
Get your API key from <a href="https://aistudio.google.com/app/apikey" target="_blank" style="color: var(--text-color);">Google AI Studio</a>
|
||||
</div>
|
||||
</div>
|
||||
` : this.aiProvider === 'openai-realtime' ? html`
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">OpenAI API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
placeholder="Enter your OpenAI API key"
|
||||
.value=${this.openaiApiKey}
|
||||
@input=${this.handleOpenAIApiKeyInput}
|
||||
/>
|
||||
<div class="form-description">
|
||||
Get your API key from <a href="https://platform.openai.com/api-keys" target="_blank" style="color: var(--text-color);">OpenAI Platform</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Base URL (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="wss://api.openai.com/v1/realtime (leave empty for default)"
|
||||
.value=${this.openaiBaseUrl}
|
||||
@input=${this.handleOpenAIBaseUrlInput}
|
||||
/>
|
||||
<div class="form-description">Override the base URL for OpenAI-compatible APIs</div>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Base URL (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="wss://api.openai.com/v1/realtime (leave empty for default)"
|
||||
.value=${this.openaiBaseUrl}
|
||||
@input=${this.handleOpenAIBaseUrlInput}
|
||||
/>
|
||||
<div class="form-description">
|
||||
Override the base URL for OpenAI-compatible APIs
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Model</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="gpt-4o-realtime-preview-2024-12-17"
|
||||
.value=${this.openaiModel}
|
||||
@input=${this.handleOpenAIModelInput}
|
||||
/>
|
||||
<div class="form-description">Realtime API model to use</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
placeholder="Enter your API key"
|
||||
.value=${this.openaiSdkApiKey}
|
||||
@input=${this.handleOpenAISdkApiKeyInput}
|
||||
/>
|
||||
<div class="form-description">API key for your provider (BotHub, Azure, OpenRouter, etc.)</div>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Model</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="gpt-4o-realtime-preview-2024-12-17"
|
||||
.value=${this.openaiModel}
|
||||
@input=${this.handleOpenAIModelInput}
|
||||
/>
|
||||
<div class="form-description">
|
||||
Realtime API model to use
|
||||
</div>
|
||||
</div>
|
||||
` : html`
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
placeholder="Enter your API key"
|
||||
.value=${this.openaiSdkApiKey}
|
||||
@input=${this.handleOpenAISdkApiKeyInput}
|
||||
/>
|
||||
<div class="form-description">
|
||||
API key for your provider (BotHub, Azure, OpenRouter, etc.)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Base URL</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="https://bothub.chat/api/v2/openai/v1"
|
||||
.value=${this.openaiSdkBaseUrl}
|
||||
@input=${this.handleOpenAISdkBaseUrlInput}
|
||||
/>
|
||||
<div class="form-description">API endpoint URL (e.g., https://bothub.chat/api/v2/openai/v1)</div>
|
||||
</div>
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Base URL</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="https://bothub.chat/api/v2/openai/v1"
|
||||
.value=${this.openaiSdkBaseUrl}
|
||||
@input=${this.handleOpenAISdkBaseUrlInput}
|
||||
/>
|
||||
<div class="form-description">
|
||||
API endpoint URL (e.g., https://bothub.chat/api/v2/openai/v1)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Chat Model</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="gpt-4o"
|
||||
.value=${this.openaiSdkModel}
|
||||
@input=${this.handleOpenAISdkModelInput}
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Vision Model</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="gpt-4o"
|
||||
.value=${this.openaiSdkVisionModel}
|
||||
@input=${this.handleOpenAISdkVisionModelInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Chat Model</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="gpt-4o"
|
||||
.value=${this.openaiSdkModel}
|
||||
@input=${this.handleOpenAISdkModelInput}
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Vision Model</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="gpt-4o"
|
||||
.value=${this.openaiSdkVisionModel}
|
||||
@input=${this.handleOpenAISdkVisionModelInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Whisper Model (Transcription)</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="whisper-1"
|
||||
.value=${this.openaiSdkWhisperModel}
|
||||
@input=${this.handleOpenAISdkWhisperModelInput}
|
||||
/>
|
||||
<div class="form-description">Model for audio transcription</div>
|
||||
</div>
|
||||
`}
|
||||
<div class="form-group full-width">
|
||||
<label class="form-label">Whisper Model (Transcription)</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="whisper-1"
|
||||
.value=${this.openaiSdkWhisperModel}
|
||||
@input=${this.handleOpenAISdkWhisperModelInput}
|
||||
/>
|
||||
<div class="form-description">
|
||||
Model for audio transcription
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
|
||||
<div
|
||||
class="form-description full-width"
|
||||
style="margin-top: 12px; padding: 12px; background: var(--bg-secondary); border-left: 2px solid var(--border-default); border-radius: 3px;"
|
||||
>
|
||||
<div class="form-description full-width" style="margin-top: 12px; padding: 12px; background: var(--bg-secondary); border-left: 2px solid var(--border-default); border-radius: 3px;">
|
||||
<strong>Note:</strong> You must restart the AI session for provider changes to take effect.
|
||||
</div>
|
||||
</div>
|
||||
@ -1759,19 +1628,20 @@ export class CustomizeView extends LitElement {
|
||||
<div class="form-group">
|
||||
<label class="form-label" style="color: var(--error-color);">Data Management</label>
|
||||
<div class="form-description" style="margin-bottom: 12px;">
|
||||
<strong>Warning:</strong> This action will permanently delete all local data including API keys, preferences, and session
|
||||
history. This cannot be undone.
|
||||
<strong>Warning:</strong> This action will permanently delete all local data including API keys, preferences, and session history. This cannot be undone.
|
||||
</div>
|
||||
<button class="danger-button" @click=${this.clearLocalData} ?disabled=${this.isClearing}>
|
||||
<button
|
||||
class="danger-button"
|
||||
@click=${this.clearLocalData}
|
||||
?disabled=${this.isClearing}
|
||||
>
|
||||
${this.isClearing ? 'Clearing...' : 'Clear All Local Data'}
|
||||
</button>
|
||||
${this.clearStatusMessage
|
||||
? html`
|
||||
<div class="status-message ${this.clearStatusType === 'success' ? 'status-success' : 'status-error'}">
|
||||
${this.clearStatusMessage}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${this.clearStatusMessage ? html`
|
||||
<div class="status-message ${this.clearStatusType === 'success' ? 'status-success' : 'status-error'}">
|
||||
${this.clearStatusMessage}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -1820,7 +1690,9 @@ export class CustomizeView extends LitElement {
|
||||
`
|
||||
)}
|
||||
</nav>
|
||||
<div class="settings-content">${this.renderSectionContent()}</div>
|
||||
<div class="settings-content">
|
||||
${this.renderSectionContent()}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -4,11 +4,7 @@ import { resizeLayout } from '../../utils/windowResize.js';
|
||||
export class HelpView extends LitElement {
|
||||
static styles = css`
|
||||
* {
|
||||
font-family:
|
||||
'Inter',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
sans-serif;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
@ -174,25 +170,6 @@ export class HelpView extends LitElement {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.open-logs-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: var(--text-color);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.open-logs-btn:hover {
|
||||
background: var(--hover-background);
|
||||
}
|
||||
|
||||
.usage-steps {
|
||||
counter-reset: step-counter;
|
||||
}
|
||||
@ -243,7 +220,7 @@ export class HelpView extends LitElement {
|
||||
|
||||
async _loadKeybinds() {
|
||||
try {
|
||||
const keybinds = await mastermind.storage.getKeybinds();
|
||||
const keybinds = await cheatingDaddy.storage.getKeybinds();
|
||||
if (keybinds) {
|
||||
this.keybinds = { ...this.getDefaultKeybinds(), ...keybinds };
|
||||
this.requestUpdate();
|
||||
@ -260,7 +237,7 @@ export class HelpView extends LitElement {
|
||||
}
|
||||
|
||||
getDefaultKeybinds() {
|
||||
const isMac = mastermind.isMacOS || navigator.platform.includes('Mac');
|
||||
const isMac = cheatingDaddy.isMacOS || navigator.platform.includes('Mac');
|
||||
return {
|
||||
moveUp: isMac ? 'Alt+Up' : 'Ctrl+Up',
|
||||
moveDown: isMac ? 'Alt+Down' : 'Ctrl+Down',
|
||||
@ -285,8 +262,8 @@ export class HelpView extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const isMacOS = mastermind.isMacOS || false;
|
||||
const isLinux = mastermind.isLinux || false;
|
||||
const isMacOS = cheatingDaddy.isMacOS || false;
|
||||
const isLinux = cheatingDaddy.isLinux || false;
|
||||
|
||||
return html`
|
||||
<div class="help-container">
|
||||
@ -295,65 +272,30 @@ export class HelpView extends LitElement {
|
||||
<span>Community & Support</span>
|
||||
</div>
|
||||
<div class="community-links">
|
||||
<!-- <div class="community-link" @click=${() => this.handleExternalLinkClick('https://github.com/ShiftyX1/Mastermind')}>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M14 11.9976C14 9.5059 11.683 7 8.85714 7C8.52241 7 7.41904 7.00001 7.14286 7.00001C4.30254 7.00001 2 9.23752 2 11.9976C2 14.376 3.70973 16.3664 6 16.8714C6.36756 16.9525 6.75006 16.9952 7.14286 16.9952"
|
||||
></path>
|
||||
<path
|
||||
d="M10 11.9976C10 14.4893 12.317 16.9952 15.1429 16.9952C15.4776 16.9952 16.581 16.9952 16.8571 16.9952C19.6975 16.9952 22 14.7577 22 11.9976C22 9.6192 20.2903 7.62884 18 7.12383C17.6324 7.04278 17.2499 6.99999 16.8571 6.99999"
|
||||
></path>
|
||||
<div class="community-link" @click=${() => this.handleExternalLinkClick('https://cheatingdaddy.com')}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 11.9976C14 9.5059 11.683 7 8.85714 7C8.52241 7 7.41904 7.00001 7.14286 7.00001C4.30254 7.00001 2 9.23752 2 11.9976C2 14.376 3.70973 16.3664 6 16.8714C6.36756 16.9525 6.75006 16.9952 7.14286 16.9952"></path>
|
||||
<path d="M10 11.9976C10 14.4893 12.317 16.9952 15.1429 16.9952C15.4776 16.9952 16.581 16.9952 16.8571 16.9952C19.6975 16.9952 22 14.7577 22 11.9976C22 9.6192 20.2903 7.62884 18 7.12383C17.6324 7.04278 17.2499 6.99999 16.8571 6.99999"></path>
|
||||
</svg>
|
||||
Website
|
||||
</div> -->
|
||||
<div class="community-link" @click=${() => this.handleExternalLinkClick('https://github.com/ShiftyX1/Mastermind')}>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M16 22.0268V19.1568C16.0375 18.68 15.9731 18.2006 15.811 17.7506C15.6489 17.3006 15.3929 16.8902 15.06 16.5468C18.2 16.1968 21.5 15.0068 21.5 9.54679C21.4997 8.15062 20.9627 6.80799 20 5.79679C20.4558 4.5753 20.4236 3.22514 19.91 2.02679C19.91 2.02679 18.73 1.67679 16 3.50679C13.708 2.88561 11.292 2.88561 8.99999 3.50679C6.26999 1.67679 5.08999 2.02679 5.08999 2.02679C4.57636 3.22514 4.54413 4.5753 4.99999 5.79679C4.03011 6.81549 3.49251 8.17026 3.49999 9.57679C3.49999 14.9968 6.79998 16.1868 9.93998 16.5768C9.61098 16.9168 9.35725 17.3222 9.19529 17.7667C9.03334 18.2112 8.96679 18.6849 8.99999 19.1568V22.0268"
|
||||
></path>
|
||||
</div>
|
||||
<div class="community-link" @click=${() => this.handleExternalLinkClick('https://github.com/sohzm/cheating-daddy')}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 22.0268V19.1568C16.0375 18.68 15.9731 18.2006 15.811 17.7506C15.6489 17.3006 15.3929 16.8902 15.06 16.5468C18.2 16.1968 21.5 15.0068 21.5 9.54679C21.4997 8.15062 20.9627 6.80799 20 5.79679C20.4558 4.5753 20.4236 3.22514 19.91 2.02679C19.91 2.02679 18.73 1.67679 16 3.50679C13.708 2.88561 11.292 2.88561 8.99999 3.50679C6.26999 1.67679 5.08999 2.02679 5.08999 2.02679C4.57636 3.22514 4.54413 4.5753 4.99999 5.79679C4.03011 6.81549 3.49251 8.17026 3.49999 9.57679C3.49999 14.9968 6.79998 16.1868 9.93998 16.5768C9.61098 16.9168 9.35725 17.3222 9.19529 17.7667C9.03334 18.2112 8.96679 18.6849 8.99999 19.1568V22.0268"></path>
|
||||
<path d="M9 20.0267C6 20.9999 3.5 20.0267 2 17.0267"></path>
|
||||
</svg>
|
||||
GitHub
|
||||
</div>
|
||||
<!-- <div class="community-link" @click=${() => this.handleExternalLinkClick('https://discord.gg/GCBdubnXfJ')}>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<div class="community-link" @click=${() => this.handleExternalLinkClick('https://discord.gg/GCBdubnXfJ')}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5.5 16C10.5 18.5 13.5 18.5 18.5 16"></path>
|
||||
<path
|
||||
d="M15.5 17.5L16.5 19.5C16.5 19.5 20.6713 18.1717 22 16C22 15 22.5301 7.85339 19 5.5C17.5 4.5 15 4 15 4L14 6H12"
|
||||
></path>
|
||||
<path
|
||||
d="M8.52832 17.5L7.52832 19.5C7.52832 19.5 3.35699 18.1717 2.02832 16C2.02832 15 1.49823 7.85339 5.02832 5.5C6.52832 4.5 9.02832 4 9.02832 4L10.0283 6H12.0283"
|
||||
></path>
|
||||
<path
|
||||
d="M8.5 14C7.67157 14 7 13.1046 7 12C7 10.8954 7.67157 10 8.5 10C9.32843 10 10 10.8954 10 12C10 13.1046 9.32843 14 8.5 14Z"
|
||||
></path>
|
||||
<path
|
||||
d="M15.5 14C14.6716 14 14 13.1046 14 12C14 10.8954 14.6716 10 15.5 10C16.3284 10 17 10.8954 17 12C17 13.1046 16.3284 14 15.5 14Z"
|
||||
></path>
|
||||
<path d="M15.5 17.5L16.5 19.5C16.5 19.5 20.6713 18.1717 22 16C22 15 22.5301 7.85339 19 5.5C17.5 4.5 15 4 15 4L14 6H12"></path>
|
||||
<path d="M8.52832 17.5L7.52832 19.5C7.52832 19.5 3.35699 18.1717 2.02832 16C2.02832 15 1.49823 7.85339 5.02832 5.5C6.52832 4.5 9.02832 4 9.02832 4L10.0283 6H12.0283"></path>
|
||||
<path d="M8.5 14C7.67157 14 7 13.1046 7 12C7 10.8954 7.67157 10 8.5 10C9.32843 10 10 10.8954 10 12C10 13.1046 9.32843 14 8.5 14Z"></path>
|
||||
<path d="M15.5 14C14.6716 14 14 13.1046 14 12C14 10.8954 14.6716 10 15.5 10C16.3284 10 17 10.8954 17 12C17 13.1046 16.3284 14 15.5 14Z"></path>
|
||||
</svg>
|
||||
Discord
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -434,7 +376,9 @@ export class HelpView extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="description" style="margin-top: 12px; text-align: center;">You can customize these shortcuts in Settings.</div>
|
||||
<div class="description" style="margin-top: 12px; text-align: center;">
|
||||
You can customize these shortcuts in Settings.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option-group">
|
||||
@ -442,7 +386,7 @@ export class HelpView extends LitElement {
|
||||
<span>How to Use</span>
|
||||
</div>
|
||||
<div class="usage-steps">
|
||||
<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>Start a Session:</strong> Enter your Gemini 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>Position Window:</strong> Use keyboard shortcuts to move the window to your desired location
|
||||
@ -498,31 +442,9 @@ export class HelpView extends LitElement {
|
||||
</div>
|
||||
<div class="description">The AI listens to conversations and provides contextual assistance based on what it hears.</div>
|
||||
</div>
|
||||
|
||||
<div class="option-group">
|
||||
<div class="option-label">
|
||||
<span>Troubleshooting</span>
|
||||
</div>
|
||||
<div class="description" style="margin-bottom: 12px;">
|
||||
If you're experiencing issues with audio capture or other features, check the application logs for diagnostic information.
|
||||
</div>
|
||||
<button class="open-logs-btn" @click=${this.openLogsFolder}>📁 Open Logs Folder</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async openLogsFolder() {
|
||||
try {
|
||||
const { ipcRenderer } = require('electron');
|
||||
const result = await ipcRenderer.invoke('open-logs-folder');
|
||||
if (!result.success) {
|
||||
console.error('Failed to open logs folder:', result.error);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error opening logs folder:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('help-view', HelpView);
|
||||
|
||||
@ -4,11 +4,7 @@ import { resizeLayout } from '../../utils/windowResize.js';
|
||||
export class HistoryView extends LitElement {
|
||||
static styles = css`
|
||||
* {
|
||||
font-family:
|
||||
'Inter',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
sans-serif;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
@ -380,7 +376,7 @@ export class HistoryView extends LitElement {
|
||||
async loadSessions() {
|
||||
try {
|
||||
this.loading = true;
|
||||
this.sessions = await mastermind.storage.getAllSessions();
|
||||
this.sessions = await cheatingDaddy.storage.getAllSessions();
|
||||
} catch (error) {
|
||||
console.error('Error loading conversation sessions:', error);
|
||||
this.sessions = [];
|
||||
@ -392,7 +388,7 @@ export class HistoryView extends LitElement {
|
||||
|
||||
async loadSelectedSession(sessionId) {
|
||||
try {
|
||||
const session = await mastermind.storage.getSession(sessionId);
|
||||
const session = await cheatingDaddy.storage.getSession(sessionId);
|
||||
if (session) {
|
||||
this.selectedSession = session;
|
||||
this.requestUpdate();
|
||||
@ -509,22 +505,18 @@ export class HistoryView extends LitElement {
|
||||
|
||||
return html`
|
||||
<div class="session-context">
|
||||
${profile
|
||||
? html`
|
||||
<div class="session-context-row">
|
||||
<span class="context-label">Profile:</span>
|
||||
<span class="context-value">${profileNames[profile] || profile}</span>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${customPrompt
|
||||
? html`
|
||||
<div class="session-context-row">
|
||||
<span class="context-label">Custom Prompt:</span>
|
||||
<span class="custom-prompt-value">${customPrompt}</span>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${profile ? html`
|
||||
<div class="session-context-row">
|
||||
<span class="context-label">Profile:</span>
|
||||
<span class="context-value">${profileNames[profile] || profile}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${customPrompt ? html`
|
||||
<div class="session-context-row">
|
||||
<span class="context-label">Custom Prompt:</span>
|
||||
<span class="custom-prompt-value">${customPrompt}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -567,14 +559,9 @@ export class HistoryView extends LitElement {
|
||||
return html`<div class="empty-state">No screen analysis data available</div>`;
|
||||
}
|
||||
|
||||
return screenAnalysisHistory.map(
|
||||
analysis => html`
|
||||
<div class="message screen">
|
||||
<div class="analysis-meta">${this.formatTimestamp(analysis.timestamp)} • ${analysis.model || 'unknown model'}</div>
|
||||
${analysis.response}
|
||||
</div>
|
||||
`
|
||||
);
|
||||
return screenAnalysisHistory.map(analysis => html`
|
||||
<div class="message screen"><div class="analysis-meta">${this.formatTimestamp(analysis.timestamp)} • ${analysis.model || 'unknown model'}</div>${analysis.response}</div>
|
||||
`);
|
||||
}
|
||||
|
||||
renderConversationView() {
|
||||
@ -617,13 +604,22 @@ export class HistoryView extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-tabs">
|
||||
<button class="view-tab ${this.activeTab === 'conversation' ? 'active' : ''}" @click=${() => this.handleTabClick('conversation')}>
|
||||
<button
|
||||
class="view-tab ${this.activeTab === 'conversation' ? 'active' : ''}"
|
||||
@click=${() => this.handleTabClick('conversation')}
|
||||
>
|
||||
Conversation ${hasConversation ? `(${conversationHistory.length})` : ''}
|
||||
</button>
|
||||
<button class="view-tab ${this.activeTab === 'screen' ? 'active' : ''}" @click=${() => this.handleTabClick('screen')}>
|
||||
<button
|
||||
class="view-tab ${this.activeTab === 'screen' ? 'active' : ''}"
|
||||
@click=${() => this.handleTabClick('screen')}
|
||||
>
|
||||
Screen ${hasScreenAnalysis ? `(${screenAnalysisHistory.length})` : ''}
|
||||
</button>
|
||||
<button class="view-tab ${this.activeTab === 'context' ? 'active' : ''}" @click=${() => this.handleTabClick('context')}>
|
||||
<button
|
||||
class="view-tab ${this.activeTab === 'context' ? 'active' : ''}"
|
||||
@click=${() => this.handleTabClick('context')}
|
||||
>
|
||||
Context ${hasContext ? '' : '(empty)'}
|
||||
</button>
|
||||
</div>
|
||||
@ -631,8 +627,8 @@ export class HistoryView extends LitElement {
|
||||
${this.activeTab === 'conversation'
|
||||
? this.renderConversationContent()
|
||||
: this.activeTab === 'screen'
|
||||
? this.renderScreenAnalysisContent()
|
||||
: this.renderContextContent()}
|
||||
? this.renderScreenAnalysisContent()
|
||||
: this.renderContextContent()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -642,7 +638,11 @@ export class HistoryView extends LitElement {
|
||||
return html`<div class="history-container">${this.renderConversationView()}</div>`;
|
||||
}
|
||||
|
||||
return html` <div class="history-container">${this.renderSessionsList()}</div> `;
|
||||
return html`
|
||||
<div class="history-container">
|
||||
${this.renderSessionsList()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,11 +4,7 @@ import { resizeLayout } from '../../utils/windowResize.js';
|
||||
export class MainView extends LitElement {
|
||||
static styles = css`
|
||||
* {
|
||||
font-family:
|
||||
'Inter',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
sans-serif;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
@ -58,8 +54,7 @@ export class MainView extends LitElement {
|
||||
}
|
||||
|
||||
@keyframes blink-red {
|
||||
0%,
|
||||
100% {
|
||||
0%, 100% {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
50% {
|
||||
@ -150,7 +145,7 @@ export class MainView extends LitElement {
|
||||
}
|
||||
|
||||
async _loadApiKey() {
|
||||
this.apiKey = await mastermind.storage.getApiKey();
|
||||
this.apiKey = await cheatingDaddy.storage.getApiKey();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@ -186,7 +181,7 @@ export class MainView extends LitElement {
|
||||
|
||||
async handleInput(e) {
|
||||
this.apiKey = e.target.value;
|
||||
await mastermind.storage.setApiKey(e.target.value);
|
||||
await cheatingDaddy.storage.setApiKey(e.target.value);
|
||||
// Clear error state when user starts typing
|
||||
if (this.showApiKeyError) {
|
||||
this.showApiKeyError = false;
|
||||
@ -226,7 +221,7 @@ export class MainView extends LitElement {
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Enter your AI Provider API Key"
|
||||
placeholder="Enter your Gemini API Key"
|
||||
.value=${this.apiKey}
|
||||
@input=${this.handleInput}
|
||||
class="${this.showApiKeyError ? 'api-key-error' : ''}"
|
||||
@ -235,10 +230,10 @@ export class MainView extends LitElement {
|
||||
${this.getStartButtonText()}
|
||||
</button>
|
||||
</div>
|
||||
<!-- <p class="description">
|
||||
<p class="description">
|
||||
dont have an api key?
|
||||
<span @click=${this.handleAPIKeyHelpClick} class="link">get one here</span>
|
||||
</p> -->
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,49 +157,6 @@ export class OnboardingView extends LitElement {
|
||||
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 {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@ -282,7 +239,6 @@ export class OnboardingView extends LitElement {
|
||||
static properties = {
|
||||
currentSlide: { type: Number },
|
||||
contextText: { type: String },
|
||||
hasOldConfig: { type: Boolean },
|
||||
onComplete: { type: Function },
|
||||
onClose: { type: Function },
|
||||
};
|
||||
@ -291,7 +247,6 @@ export class OnboardingView extends LitElement {
|
||||
super();
|
||||
this.currentSlide = 0;
|
||||
this.contextText = '';
|
||||
this.hasOldConfig = false;
|
||||
this.onComplete = () => {};
|
||||
this.onClose = () => {};
|
||||
this.canvas = null;
|
||||
@ -342,16 +297,7 @@ export class OnboardingView extends LitElement {
|
||||
[30, 40, 35], // Muted green
|
||||
[5, 15, 10], // Almost black
|
||||
],
|
||||
// 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)
|
||||
// Slide 5 - Complete (Dark warm gray)
|
||||
[
|
||||
[30, 25, 20], // Dark warm gray
|
||||
[25, 20, 15], // Darker warm
|
||||
@ -363,25 +309,13 @@ export class OnboardingView extends LitElement {
|
||||
];
|
||||
}
|
||||
|
||||
async firstUpdated() {
|
||||
firstUpdated() {
|
||||
this.canvas = this.shadowRoot.querySelector('.gradient-canvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.resizeCanvas();
|
||||
this.startGradientAnimation();
|
||||
|
||||
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() {
|
||||
@ -480,7 +414,7 @@ export class OnboardingView extends LitElement {
|
||||
}
|
||||
|
||||
nextSlide() {
|
||||
if (this.currentSlide < 5) {
|
||||
if (this.currentSlide < 4) {
|
||||
this.startColorTransition(this.currentSlide + 1);
|
||||
} else {
|
||||
this.completeOnboarding();
|
||||
@ -528,23 +462,11 @@ 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() {
|
||||
if (this.contextText.trim()) {
|
||||
await mastermind.storage.updatePreference('customPrompt', this.contextText.trim());
|
||||
await cheatingDaddy.storage.updatePreference('customPrompt', this.contextText.trim());
|
||||
}
|
||||
await mastermind.storage.updateConfig('onboarded', true);
|
||||
await cheatingDaddy.storage.updateConfig('onboarded', true);
|
||||
this.onComplete();
|
||||
}
|
||||
|
||||
@ -552,9 +474,9 @@ export class OnboardingView extends LitElement {
|
||||
const slides = [
|
||||
{
|
||||
icon: 'assets/onboarding/welcome.svg',
|
||||
title: 'Welcome to Mastermind',
|
||||
title: 'Welcome to Cheating Daddy',
|
||||
content:
|
||||
'Your AI assistant that listens and watches, then provides intelligent suggestions automatically during interviews, meetings, and presentations.',
|
||||
'Your AI assistant that listens and watches, then provides intelligent suggestions automatically during interviews and meetings.',
|
||||
},
|
||||
{
|
||||
icon: 'assets/onboarding/security.svg',
|
||||
@ -573,18 +495,10 @@ export class OnboardingView extends LitElement {
|
||||
content: '',
|
||||
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',
|
||||
title: 'Ready to Go',
|
||||
content: 'Choose your AI Provider and start getting AI-powered assistance in real-time.',
|
||||
content: 'Add your Gemini API key in settings and start getting AI-powered assistance in real-time.',
|
||||
},
|
||||
];
|
||||
|
||||
@ -598,9 +512,7 @@ export class OnboardingView extends LitElement {
|
||||
<div class="onboarding-container">
|
||||
<button class="close-button" @click=${this.handleClose} title="Close">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"
|
||||
/>
|
||||
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<canvas class="gradient-canvas"></canvas>
|
||||
@ -638,18 +550,6 @@ export class OnboardingView extends LitElement {
|
||||
</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 class="navigation">
|
||||
@ -660,7 +560,7 @@ export class OnboardingView extends LitElement {
|
||||
</button>
|
||||
|
||||
<div class="progress-dots">
|
||||
${[0, 1, 2, 3, 4, 5].map(
|
||||
${[0, 1, 2, 3, 4].map(
|
||||
index => html`
|
||||
<div
|
||||
class="dot ${index === this.currentSlide ? 'active' : ''}"
|
||||
@ -675,7 +575,7 @@ export class OnboardingView extends LitElement {
|
||||
</div>
|
||||
|
||||
<button class="nav-button" @click=${this.nextSlide}>
|
||||
${this.currentSlide === 5
|
||||
${this.currentSlide === 4
|
||||
? 'Get Started'
|
||||
: html`
|
||||
<svg width="16px" height="16px" stroke-width="2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
@ -1,175 +0,0 @@
|
||||
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
|
||||
|
||||
export class ScreenPickerDialog extends LitElement {
|
||||
static properties = {
|
||||
sources: { type: Array },
|
||||
visible: { type: Boolean },
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 10000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:host([visible]) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: var(--background-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
max-width: 800px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 16px 0;
|
||||
color: var(--text-color);
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sources-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.source-item {
|
||||
background: var(--input-background);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.source-item:hover {
|
||||
border-color: var(--border-default);
|
||||
background: var(--button-hover);
|
||||
}
|
||||
|
||||
.source-item.selected {
|
||||
border-color: var(--accent-color);
|
||||
background: var(--button-hover);
|
||||
}
|
||||
|
||||
.source-thumbnail {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
object-fit: contain;
|
||||
background: #1a1a1a;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.source-name {
|
||||
color: var(--text-color);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--button-background);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 8px 16px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: background-color 0.1s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--button-hover);
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
button.primary:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.sources = [];
|
||||
this.visible = false;
|
||||
this.selectedSource = null;
|
||||
}
|
||||
|
||||
selectSource(source) {
|
||||
this.selectedSource = source;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
confirm() {
|
||||
if (this.selectedSource) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('source-selected', {
|
||||
detail: { source: this.selectedSource },
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.dispatchEvent(new CustomEvent('cancelled'));
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="dialog">
|
||||
<h2>Choose screen or window to share</h2>
|
||||
<div class="sources-grid">
|
||||
${this.sources.map(
|
||||
source => html`
|
||||
<div
|
||||
class="source-item ${this.selectedSource?.id === source.id ? 'selected' : ''}"
|
||||
@click=${() => this.selectSource(source)}
|
||||
>
|
||||
<img class="source-thumbnail" src="${source.thumbnail}" alt="${source.name}" />
|
||||
<div class="source-name">${source.name}</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button @click=${this.cancel}>Cancel</button>
|
||||
<button class="primary" @click=${this.confirm} ?disabled=${!this.selectedSource}>Share</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('screen-picker-dialog', ScreenPickerDialog);
|
||||
@ -123,7 +123,7 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
mastermind-app {
|
||||
cheating-daddy-app {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -134,9 +134,10 @@
|
||||
<script src="assets/marked-4.3.0.min.js"></script>
|
||||
<script src="assets/highlight-11.9.0.min.js"></script>
|
||||
<link rel="stylesheet" href="assets/highlight-vscode-dark.min.css" />
|
||||
<script type="module" src="components/app/MastermindApp.js"></script>
|
||||
<script type="module" src="components/app/CheatingDaddyApp.js"></script>
|
||||
|
||||
<mastermind-app id="mastermind"></mastermind-app>
|
||||
<cheating-daddy-app id="cheatingDaddy"></cheating-daddy-app>
|
||||
<script src="script.js"></script>
|
||||
<script src="utils/renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
50
src/index.js
50
src/index.js
@ -6,7 +6,6 @@ const { app, BrowserWindow, shell, ipcMain } = require('electron');
|
||||
const { createWindow, updateGlobalShortcuts } = require('./utils/window');
|
||||
const { setupAIProviderIpcHandlers } = require('./utils/ai-provider-manager');
|
||||
const { stopMacOSAudioCapture } = require('./utils/gemini');
|
||||
const { initLogger, closeLogger, getLogPath } = require('./utils/logger');
|
||||
const storage = require('./storage');
|
||||
|
||||
const geminiSessionRef = { current: null };
|
||||
@ -25,10 +24,6 @@ function createMainWindow() {
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
// Initialize file logger first
|
||||
const logPath = initLogger();
|
||||
console.log('App starting, log file:', logPath);
|
||||
|
||||
// Initialize storage (checks version, resets if needed)
|
||||
storage.initializeStorage();
|
||||
|
||||
@ -36,22 +31,10 @@ app.whenReady().then(async () => {
|
||||
setupAIProviderIpcHandlers(geminiSessionRef);
|
||||
setupStorageIpcHandlers();
|
||||
setupGeneralIpcHandlers();
|
||||
|
||||
// Add handler to get log path from renderer
|
||||
ipcMain.handle('get-log-path', () => getLogPath());
|
||||
|
||||
// Add handler for renderer logs (so they go to the log file)
|
||||
ipcMain.on('renderer-log', (event, { level, message }) => {
|
||||
const prefix = '[RENDERER]';
|
||||
if (level === 'error') console.error(prefix, message);
|
||||
else if (level === 'warn') console.warn(prefix, message);
|
||||
else console.log(prefix, message);
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
stopMacOSAudioCapture();
|
||||
closeLogger();
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
@ -59,7 +42,6 @@ app.on('window-all-closed', () => {
|
||||
|
||||
app.on('before-quit', () => {
|
||||
stopMacOSAudioCapture();
|
||||
closeLogger();
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
@ -295,26 +277,6 @@ function setupStorageIpcHandlers() {
|
||||
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() {
|
||||
@ -322,18 +284,6 @@ function setupGeneralIpcHandlers() {
|
||||
return app.getVersion();
|
||||
});
|
||||
|
||||
ipcMain.handle('open-logs-folder', async () => {
|
||||
try {
|
||||
const logPath = getLogPath();
|
||||
const logsDir = require('path').dirname(logPath);
|
||||
await shell.openPath(logsDir);
|
||||
return { success: true, path: logsDir };
|
||||
} catch (error) {
|
||||
console.error('Error opening logs folder:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('quit-application', async event => {
|
||||
try {
|
||||
stopMacOSAudioCapture();
|
||||
|
||||
115
src/storage.js
115
src/storage.js
@ -8,7 +8,7 @@ const CONFIG_VERSION = 1;
|
||||
const DEFAULT_CONFIG = {
|
||||
configVersion: CONFIG_VERSION,
|
||||
onboarded: false,
|
||||
layout: 'normal',
|
||||
layout: 'normal'
|
||||
};
|
||||
|
||||
const DEFAULT_CREDENTIALS = {
|
||||
@ -22,7 +22,7 @@ const DEFAULT_CREDENTIALS = {
|
||||
openaiSdkBaseUrl: '',
|
||||
openaiSdkModel: 'gpt-4o',
|
||||
openaiSdkVisionModel: 'gpt-4o',
|
||||
openaiSdkWhisperModel: 'whisper-1',
|
||||
openaiSdkWhisperModel: 'whisper-1'
|
||||
};
|
||||
|
||||
const DEFAULT_PREFERENCES = {
|
||||
@ -33,17 +33,16 @@ const DEFAULT_PREFERENCES = {
|
||||
selectedImageQuality: 'medium',
|
||||
advancedMode: false,
|
||||
audioMode: 'speaker_only',
|
||||
audioInputMode: 'auto',
|
||||
fontSize: 'medium',
|
||||
backgroundTransparency: 0.8,
|
||||
googleSearchEnabled: false,
|
||||
aiProvider: 'gemini',
|
||||
aiProvider: 'gemini'
|
||||
};
|
||||
|
||||
const DEFAULT_KEYBINDS = null; // null means use system defaults
|
||||
|
||||
const DEFAULT_LIMITS = {
|
||||
data: [], // Array of { date: 'YYYY-MM-DD', flash: { count: 0 }, flashLite: { count: 0 } }
|
||||
data: [] // Array of { date: 'YYYY-MM-DD', flash: { count: 0 }, flashLite: { count: 0 } }
|
||||
};
|
||||
|
||||
// Get the config directory path based on OS
|
||||
@ -51,22 +50,6 @@ function getConfigDir() {
|
||||
const platform = os.platform();
|
||||
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') {
|
||||
configDir = path.join(os.homedir(), 'AppData', 'Roaming', 'cheating-daddy-config');
|
||||
} else if (platform === 'darwin') {
|
||||
@ -78,43 +61,6 @@ function getOldConfigDir() {
|
||||
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
|
||||
function getConfigPath() {
|
||||
return path.join(getConfigDir(), 'config.json');
|
||||
@ -262,7 +208,7 @@ function getOpenAICredentials() {
|
||||
return {
|
||||
apiKey: creds.openaiApiKey || '',
|
||||
baseUrl: creds.openaiBaseUrl || '',
|
||||
model: creds.openaiModel || 'gpt-4o-realtime-preview-2024-12-17',
|
||||
model: creds.openaiModel || 'gpt-4o-realtime-preview-2024-12-17'
|
||||
};
|
||||
}
|
||||
|
||||
@ -281,7 +227,7 @@ function getOpenAISDKCredentials() {
|
||||
baseUrl: creds.openaiSdkBaseUrl || '',
|
||||
model: creds.openaiSdkModel || 'gpt-4o',
|
||||
visionModel: creds.openaiSdkVisionModel || 'gpt-4o',
|
||||
whisperModel: creds.openaiSdkWhisperModel || 'whisper-1',
|
||||
whisperModel: creds.openaiSdkWhisperModel || 'whisper-1'
|
||||
};
|
||||
}
|
||||
|
||||
@ -355,7 +301,7 @@ function getTodayLimits() {
|
||||
const newEntry = {
|
||||
date: today,
|
||||
flash: { count: 0 },
|
||||
flashLite: { count: 0 },
|
||||
flashLite: { count: 0 }
|
||||
};
|
||||
limits.data.push(newEntry);
|
||||
setLimits(limits);
|
||||
@ -376,7 +322,7 @@ function incrementLimitCount(model) {
|
||||
todayEntry = {
|
||||
date: today,
|
||||
flash: { count: 0 },
|
||||
flashLite: { count: 0 },
|
||||
flashLite: { count: 0 }
|
||||
};
|
||||
limits.data.push(todayEntry);
|
||||
} else {
|
||||
@ -430,7 +376,7 @@ function saveSession(sessionId, data) {
|
||||
customPrompt: data.customPrompt || existingSession?.customPrompt || null,
|
||||
// Conversation data
|
||||
conversationHistory: data.conversationHistory || existingSession?.conversationHistory || [],
|
||||
screenAnalysisHistory: data.screenAnalysisHistory || existingSession?.screenAnalysisHistory || [],
|
||||
screenAnalysisHistory: data.screenAnalysisHistory || existingSession?.screenAnalysisHistory || []
|
||||
};
|
||||
return writeJsonFile(sessionPath, sessionData);
|
||||
}
|
||||
@ -447,8 +393,7 @@ function getAllSessions() {
|
||||
return [];
|
||||
}
|
||||
|
||||
const files = fs
|
||||
.readdirSync(historyDir)
|
||||
const files = fs.readdirSync(historyDir)
|
||||
.filter(f => f.endsWith('.json'))
|
||||
.sort((a, b) => {
|
||||
// Sort by timestamp descending (newest first)
|
||||
@ -457,24 +402,22 @@ function getAllSessions() {
|
||||
return tsB - tsA;
|
||||
});
|
||||
|
||||
return files
|
||||
.map(file => {
|
||||
const sessionId = file.replace('.json', '');
|
||||
const data = readJsonFile(path.join(historyDir, file), null);
|
||||
if (data) {
|
||||
return {
|
||||
sessionId,
|
||||
createdAt: data.createdAt,
|
||||
lastUpdated: data.lastUpdated,
|
||||
messageCount: data.conversationHistory?.length || 0,
|
||||
screenAnalysisCount: data.screenAnalysisHistory?.length || 0,
|
||||
profile: data.profile || null,
|
||||
customPrompt: data.customPrompt || null,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
return files.map(file => {
|
||||
const sessionId = file.replace('.json', '');
|
||||
const data = readJsonFile(path.join(historyDir, file), null);
|
||||
if (data) {
|
||||
return {
|
||||
sessionId,
|
||||
createdAt: data.createdAt,
|
||||
lastUpdated: data.lastUpdated,
|
||||
messageCount: data.conversationHistory?.length || 0,
|
||||
screenAnalysisCount: data.screenAnalysisHistory?.length || 0,
|
||||
profile: data.profile || null,
|
||||
customPrompt: data.customPrompt || null
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}).filter(Boolean);
|
||||
} catch (error) {
|
||||
console.error('Error reading sessions:', error.message);
|
||||
return [];
|
||||
@ -522,10 +465,6 @@ module.exports = {
|
||||
initializeStorage,
|
||||
getConfigDir,
|
||||
|
||||
// Migration
|
||||
hasOldConfig,
|
||||
migrateFromOldConfig,
|
||||
|
||||
// Config
|
||||
getConfig,
|
||||
setConfig,
|
||||
@ -565,5 +504,5 @@ module.exports = {
|
||||
deleteAllSessions,
|
||||
|
||||
// Clear all
|
||||
clearAllData,
|
||||
clearAllData
|
||||
};
|
||||
|
||||
@ -186,7 +186,6 @@ async function initializeAISession(customPrompt = '', profile = 'interview', lan
|
||||
try {
|
||||
await openaiSdkProvider.initializeOpenAISDK(providerConfig);
|
||||
openaiSdkProvider.setSystemPrompt(systemPrompt);
|
||||
openaiSdkProvider.updatePushToTalkSettings(prefs.audioInputMode || 'auto');
|
||||
sendToRenderer('update-status', 'Ready (OpenAI SDK)');
|
||||
return true;
|
||||
} catch (error) {
|
||||
@ -326,16 +325,6 @@ function setupAIProviderIpcHandlers(geminiSessionRef) {
|
||||
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) => {
|
||||
return await initializeAISession(customPrompt, profile, language);
|
||||
});
|
||||
|
||||
@ -52,7 +52,9 @@ function buildContextMessage() {
|
||||
|
||||
if (validTurns.length === 0) return null;
|
||||
|
||||
const contextLines = validTurns.map(turn => `[Interviewer]: ${turn.transcription.trim()}\n[Your answer]: ${turn.ai_response.trim()}`);
|
||||
const contextLines = validTurns.map(turn =>
|
||||
`[Interviewer]: ${turn.transcription.trim()}\n[Your answer]: ${turn.ai_response.trim()}`
|
||||
);
|
||||
|
||||
return `Session reconnected. Here's the conversation so far:\n\n${contextLines.join('\n\n')}\n\nContinue from here.`;
|
||||
}
|
||||
@ -72,7 +74,7 @@ function initializeNewSession(profile = null, customPrompt = null) {
|
||||
sendToRenderer('save-session-context', {
|
||||
sessionId: currentSessionId,
|
||||
profile: profile,
|
||||
customPrompt: customPrompt || '',
|
||||
customPrompt: customPrompt || ''
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -108,7 +110,7 @@ function saveScreenAnalysis(prompt, response, model) {
|
||||
timestamp: Date.now(),
|
||||
prompt: prompt,
|
||||
response: response.trim(),
|
||||
model: model,
|
||||
model: model
|
||||
};
|
||||
|
||||
screenAnalysisHistory.push(analysisEntry);
|
||||
@ -120,7 +122,7 @@ function saveScreenAnalysis(prompt, response, model) {
|
||||
analysis: analysisEntry,
|
||||
fullHistory: screenAnalysisHistory,
|
||||
profile: currentProfile,
|
||||
customPrompt: currentCustomPrompt,
|
||||
customPrompt: currentCustomPrompt
|
||||
});
|
||||
}
|
||||
|
||||
@ -416,12 +418,10 @@ async function startMacOSAudioCapture(geminiSessionRef) {
|
||||
// Kill any existing SystemAudioDump processes first
|
||||
await killExistingSystemAudioDump();
|
||||
|
||||
console.log('=== Starting macOS audio capture ===');
|
||||
sendToRenderer('update-status', 'Starting audio capture...');
|
||||
console.log('Starting macOS audio capture with SystemAudioDump...');
|
||||
|
||||
const { app } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
let systemAudioPath;
|
||||
if (app.isPackaged) {
|
||||
@ -430,35 +430,7 @@ async function startMacOSAudioCapture(geminiSessionRef) {
|
||||
systemAudioPath = path.join(__dirname, '../assets', 'SystemAudioDump');
|
||||
}
|
||||
|
||||
console.log('SystemAudioDump config:', {
|
||||
path: systemAudioPath,
|
||||
isPackaged: app.isPackaged,
|
||||
resourcesPath: process.resourcesPath,
|
||||
exists: fs.existsSync(systemAudioPath),
|
||||
});
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(systemAudioPath)) {
|
||||
console.error('FATAL: SystemAudioDump not found at:', systemAudioPath);
|
||||
sendToRenderer('update-status', 'Error: Audio binary not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check and fix executable permissions
|
||||
try {
|
||||
fs.accessSync(systemAudioPath, fs.constants.X_OK);
|
||||
console.log('SystemAudioDump is executable');
|
||||
} catch (err) {
|
||||
console.warn('SystemAudioDump not executable, fixing permissions...');
|
||||
try {
|
||||
fs.chmodSync(systemAudioPath, 0o755);
|
||||
console.log('Fixed executable permissions');
|
||||
} catch (chmodErr) {
|
||||
console.error('Failed to fix permissions:', chmodErr);
|
||||
sendToRenderer('update-status', 'Error: Cannot execute audio binary');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
console.log('SystemAudioDump path:', systemAudioPath);
|
||||
|
||||
const spawnOptions = {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
@ -467,12 +439,10 @@ async function startMacOSAudioCapture(geminiSessionRef) {
|
||||
},
|
||||
};
|
||||
|
||||
console.log('Spawning SystemAudioDump...');
|
||||
systemAudioProc = spawn(systemAudioPath, [], spawnOptions);
|
||||
|
||||
if (!systemAudioProc.pid) {
|
||||
console.error('FATAL: Failed to start SystemAudioDump - no PID');
|
||||
sendToRenderer('update-status', 'Error: Audio capture failed to start');
|
||||
console.error('Failed to start SystemAudioDump');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -485,16 +455,8 @@ async function startMacOSAudioCapture(geminiSessionRef) {
|
||||
const CHUNK_SIZE = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_DURATION;
|
||||
|
||||
let audioBuffer = Buffer.alloc(0);
|
||||
let chunkCount = 0;
|
||||
let firstDataReceived = false;
|
||||
|
||||
systemAudioProc.stdout.on('data', data => {
|
||||
if (!firstDataReceived) {
|
||||
firstDataReceived = true;
|
||||
console.log('First audio data received! Size:', data.length);
|
||||
sendToRenderer('update-status', 'Listening...');
|
||||
}
|
||||
|
||||
audioBuffer = Buffer.concat([audioBuffer, data]);
|
||||
|
||||
while (audioBuffer.length >= CHUNK_SIZE) {
|
||||
@ -505,11 +467,6 @@ async function startMacOSAudioCapture(geminiSessionRef) {
|
||||
const base64Data = monoChunk.toString('base64');
|
||||
sendAudioToGemini(base64Data, geminiSessionRef);
|
||||
|
||||
chunkCount++;
|
||||
if (chunkCount % 100 === 0) {
|
||||
console.log(`Audio: ${chunkCount} chunks processed`);
|
||||
}
|
||||
|
||||
if (process.env.DEBUG_AUDIO) {
|
||||
console.log(`Processed audio chunk: ${chunk.length} bytes`);
|
||||
saveDebugAudio(monoChunk, 'system_audio');
|
||||
@ -523,24 +480,16 @@ async function startMacOSAudioCapture(geminiSessionRef) {
|
||||
});
|
||||
|
||||
systemAudioProc.stderr.on('data', data => {
|
||||
const msg = data.toString();
|
||||
console.error('SystemAudioDump stderr:', msg);
|
||||
if (msg.toLowerCase().includes('error')) {
|
||||
sendToRenderer('update-status', 'Audio error: ' + msg.substring(0, 50));
|
||||
}
|
||||
console.error('SystemAudioDump stderr:', data.toString());
|
||||
});
|
||||
|
||||
systemAudioProc.on('close', code => {
|
||||
console.log('SystemAudioDump closed with code:', code, 'chunks processed:', chunkCount);
|
||||
if (code !== 0 && code !== null) {
|
||||
sendToRenderer('update-status', `Audio stopped (exit: ${code})`);
|
||||
}
|
||||
console.log('SystemAudioDump process closed with code:', code);
|
||||
systemAudioProc = null;
|
||||
});
|
||||
|
||||
systemAudioProc.on('error', err => {
|
||||
console.error('SystemAudioDump spawn error:', err.message);
|
||||
sendToRenderer('update-status', 'Audio error: ' + err.message);
|
||||
console.error('SystemAudioDump process error:', err);
|
||||
systemAudioProc = null;
|
||||
});
|
||||
|
||||
|
||||
@ -1,99 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { app } = require('electron');
|
||||
|
||||
let logFile = null;
|
||||
let logPath = null;
|
||||
|
||||
function getLogPath() {
|
||||
if (logPath) return logPath;
|
||||
|
||||
const userDataPath = app.getPath('userData');
|
||||
const logsDir = path.join(userDataPath, 'logs');
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create log file with timestamp
|
||||
const timestamp = new Date().toISOString().split('T')[0];
|
||||
logPath = path.join(logsDir, `app-${timestamp}.log`);
|
||||
|
||||
return logPath;
|
||||
}
|
||||
|
||||
function initLogger() {
|
||||
try {
|
||||
const filePath = getLogPath();
|
||||
logFile = fs.createWriteStream(filePath, { flags: 'a' });
|
||||
|
||||
const startMsg = `\n${'='.repeat(60)}\nApp started at ${new Date().toISOString()}\nPlatform: ${process.platform}, Arch: ${process.arch}\nElectron: ${process.versions.electron}, Node: ${process.versions.node}\nPackaged: ${app.isPackaged}\n${'='.repeat(60)}\n`;
|
||||
logFile.write(startMsg);
|
||||
|
||||
// Override console methods to also write to file
|
||||
const originalLog = console.log;
|
||||
const originalError = console.error;
|
||||
const originalWarn = console.warn;
|
||||
|
||||
console.log = (...args) => {
|
||||
originalLog.apply(console, args);
|
||||
writeLog('INFO', args);
|
||||
};
|
||||
|
||||
console.error = (...args) => {
|
||||
originalError.apply(console, args);
|
||||
writeLog('ERROR', args);
|
||||
};
|
||||
|
||||
console.warn = (...args) => {
|
||||
originalWarn.apply(console, args);
|
||||
writeLog('WARN', args);
|
||||
};
|
||||
|
||||
console.log('Logger initialized, writing to:', filePath);
|
||||
|
||||
return filePath;
|
||||
} catch (err) {
|
||||
console.error('Failed to initialize logger:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeLog(level, args) {
|
||||
if (!logFile) return;
|
||||
|
||||
try {
|
||||
const timestamp = new Date().toISOString();
|
||||
const message = args
|
||||
.map(arg => {
|
||||
if (typeof arg === 'object') {
|
||||
try {
|
||||
return JSON.stringify(arg, null, 2);
|
||||
} catch {
|
||||
return String(arg);
|
||||
}
|
||||
}
|
||||
return String(arg);
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
logFile.write(`[${timestamp}] [${level}] ${message}\n`);
|
||||
} catch (err) {
|
||||
// Silently fail - don't want logging errors to crash the app
|
||||
}
|
||||
}
|
||||
|
||||
function closeLogger() {
|
||||
if (logFile) {
|
||||
logFile.write(`\nApp closed at ${new Date().toISOString()}\n`);
|
||||
logFile.end();
|
||||
logFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initLogger,
|
||||
closeLogger,
|
||||
getLogPath,
|
||||
};
|
||||
@ -311,9 +311,7 @@ async function sendImageToOpenAI(base64Data, prompt, config) {
|
||||
const { apiKey, baseUrl, model } = config;
|
||||
|
||||
// OpenAI doesn't support images in Realtime API yet, use standard Chat Completions
|
||||
const apiEndpoint = baseUrl
|
||||
? `${baseUrl.replace('wss://', 'https://').replace('/v1/realtime', '')}/v1/chat/completions`
|
||||
: 'https://api.openai.com/v1/chat/completions';
|
||||
const apiEndpoint = baseUrl ? `${baseUrl.replace('wss://', 'https://').replace('/v1/realtime', '')}/v1/chat/completions` : 'https://api.openai.com/v1/chat/completions';
|
||||
|
||||
try {
|
||||
const response = await fetch(apiEndpoint, {
|
||||
|
||||
@ -14,8 +14,6 @@ let openaiClient = null;
|
||||
let currentConfig = null;
|
||||
let conversationMessages = [];
|
||||
let isProcessing = false;
|
||||
let audioInputMode = 'auto';
|
||||
let isPushToTalkActive = false;
|
||||
|
||||
// macOS audio capture
|
||||
let systemAudioProc = null;
|
||||
@ -279,14 +277,10 @@ async function sendImageMessage(base64Image, prompt) {
|
||||
}
|
||||
|
||||
// Process audio chunk and get response
|
||||
// This accumulates audio and transcribes when silence is detected or timer expires
|
||||
// This accumulates audio and transcribes when silence is detected
|
||||
let audioChunks = [];
|
||||
let lastAudioTime = 0;
|
||||
let firstChunkTime = 0;
|
||||
const SILENCE_THRESHOLD_MS = 1500; // 1.5 seconds of silence
|
||||
const MAX_BUFFER_DURATION_MS = 5000; // 5 seconds max buffering before forced transcription
|
||||
let silenceCheckTimer = null;
|
||||
let windowsTranscriptionTimer = null;
|
||||
|
||||
async function processAudioChunk(base64Audio, mimeType) {
|
||||
if (!openaiClient) {
|
||||
@ -296,55 +290,12 @@ async function processAudioChunk(base64Audio, mimeType) {
|
||||
const now = Date.now();
|
||||
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
|
||||
if (audioChunks.length === 0) {
|
||||
firstChunkTime = now;
|
||||
|
||||
// Start periodic transcription timer (Windows needs this)
|
||||
if (!windowsTranscriptionTimer && process.platform === 'win32') {
|
||||
console.log('Starting Windows periodic transcription timer...');
|
||||
windowsTranscriptionTimer = setInterval(async () => {
|
||||
if (audioChunks.length > 0) {
|
||||
const bufferDuration = Date.now() - firstChunkTime;
|
||||
if (bufferDuration >= MAX_BUFFER_DURATION_MS) {
|
||||
console.log(`Periodic flush: ${bufferDuration}ms of audio buffered`);
|
||||
await flushAudioAndTranscribe();
|
||||
}
|
||||
}
|
||||
}, 2000); // Check every 2 seconds
|
||||
}
|
||||
}
|
||||
|
||||
// Add to audio buffer
|
||||
audioChunks.push(buffer);
|
||||
lastAudioTime = now;
|
||||
|
||||
// Clear existing timer
|
||||
if (silenceCheckTimer) {
|
||||
clearTimeout(silenceCheckTimer);
|
||||
}
|
||||
|
||||
// Set timer to check for silence
|
||||
silenceCheckTimer = setTimeout(async () => {
|
||||
const silenceDuration = Date.now() - lastAudioTime;
|
||||
if (silenceDuration >= SILENCE_THRESHOLD_MS && audioChunks.length > 0) {
|
||||
console.log('Silence detected, flushing audio for transcription...');
|
||||
await flushAudioAndTranscribe();
|
||||
}
|
||||
}, SILENCE_THRESHOLD_MS);
|
||||
|
||||
// Check for silence (no new audio for SILENCE_THRESHOLD_MS)
|
||||
// This is a simple approach - in production you'd want proper VAD
|
||||
return { success: true, buffering: true };
|
||||
}
|
||||
|
||||
@ -353,30 +304,15 @@ async function flushAudioAndTranscribe() {
|
||||
return { success: true, text: '' };
|
||||
}
|
||||
|
||||
// Clear Windows transcription timer
|
||||
if (windowsTranscriptionTimer) {
|
||||
clearInterval(windowsTranscriptionTimer);
|
||||
windowsTranscriptionTimer = null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Combine all audio chunks
|
||||
const combinedBuffer = Buffer.concat(audioChunks);
|
||||
const chunkCount = audioChunks.length;
|
||||
audioChunks = [];
|
||||
firstChunkTime = 0;
|
||||
|
||||
// Calculate audio duration
|
||||
const bytesPerSample = 2;
|
||||
const audioDurationMs = (combinedBuffer.length / bytesPerSample / SAMPLE_RATE) * 1000;
|
||||
|
||||
console.log(`Transcribing ${chunkCount} chunks (${audioDurationMs.toFixed(0)}ms of audio)...`);
|
||||
|
||||
// Transcribe
|
||||
const transcription = await transcribeAudio(combinedBuffer);
|
||||
|
||||
if (transcription && transcription.trim()) {
|
||||
console.log('Transcription result:', transcription);
|
||||
// Send to chat
|
||||
const response = await sendTextMessage(transcription);
|
||||
|
||||
@ -394,111 +330,10 @@ 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() {
|
||||
const systemMessage = conversationMessages.find(m => m.role === 'system');
|
||||
conversationMessages = systemMessage ? [systemMessage] : [];
|
||||
audioChunks = [];
|
||||
|
||||
// Clear timers
|
||||
if (silenceCheckTimer) {
|
||||
clearTimeout(silenceCheckTimer);
|
||||
silenceCheckTimer = null;
|
||||
}
|
||||
if (windowsTranscriptionTimer) {
|
||||
clearInterval(windowsTranscriptionTimer);
|
||||
windowsTranscriptionTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function closeOpenAISDK() {
|
||||
@ -508,19 +343,6 @@ function closeOpenAISDK() {
|
||||
conversationMessages = [];
|
||||
audioChunks = [];
|
||||
isProcessing = false;
|
||||
isPushToTalkActive = false;
|
||||
|
||||
// Clear timers
|
||||
if (silenceCheckTimer) {
|
||||
clearTimeout(silenceCheckTimer);
|
||||
silenceCheckTimer = null;
|
||||
}
|
||||
if (windowsTranscriptionTimer) {
|
||||
clearInterval(windowsTranscriptionTimer);
|
||||
windowsTranscriptionTimer = null;
|
||||
}
|
||||
|
||||
notifyPushToTalkState();
|
||||
sendToRenderer('update-status', 'Disconnected');
|
||||
}
|
||||
|
||||
@ -568,16 +390,11 @@ function hasSpeech(buffer, threshold = 500) {
|
||||
return rms > threshold;
|
||||
}
|
||||
|
||||
async function transcribeBufferedAudio(forcePTT = false) {
|
||||
async function transcribeBufferedAudio() {
|
||||
if (audioBuffer.length === 0 || isProcessing) {
|
||||
return;
|
||||
}
|
||||
|
||||
// In push-to-talk mode, only transcribe when explicitly requested (forcePTT=true)
|
||||
if (audioInputMode === 'push-to-talk' && !forcePTT) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate audio duration
|
||||
const bytesPerSample = 2;
|
||||
const audioDurationMs = (audioBuffer.length / bytesPerSample / SAMPLE_RATE) * 1000;
|
||||
@ -587,8 +404,7 @@ async function transcribeBufferedAudio(forcePTT = false) {
|
||||
}
|
||||
|
||||
// Check if there's actual speech in the audio (Voice Activity Detection)
|
||||
// Skip VAD check in PTT mode - user explicitly wants to transcribe
|
||||
if (!forcePTT && !hasSpeech(audioBuffer)) {
|
||||
if (!hasSpeech(audioBuffer)) {
|
||||
// Clear buffer if it's just silence/noise
|
||||
audioBuffer = Buffer.alloc(0);
|
||||
return;
|
||||
@ -600,9 +416,7 @@ async function transcribeBufferedAudio(forcePTT = false) {
|
||||
|
||||
try {
|
||||
console.log(`Transcribing ${audioDurationMs.toFixed(0)}ms of audio...`);
|
||||
if (!forcePTT) {
|
||||
sendToRenderer('update-status', 'Transcribing...');
|
||||
}
|
||||
sendToRenderer('update-status', 'Transcribing...');
|
||||
|
||||
const transcription = await transcribeAudio(currentBuffer, 'audio/wav');
|
||||
|
||||
@ -612,18 +426,12 @@ async function transcribeBufferedAudio(forcePTT = false) {
|
||||
|
||||
// Send to chat
|
||||
await sendTextMessage(transcription);
|
||||
} else if (forcePTT) {
|
||||
console.log('Push-to-Talk: No speech detected in recording');
|
||||
}
|
||||
|
||||
if (!forcePTT) {
|
||||
sendToRenderer('update-status', 'Listening...');
|
||||
}
|
||||
sendToRenderer('update-status', 'Listening...');
|
||||
} catch (error) {
|
||||
console.error('Transcription error:', error);
|
||||
if (!forcePTT) {
|
||||
sendToRenderer('update-status', 'Listening...');
|
||||
}
|
||||
sendToRenderer('update-status', 'Listening...');
|
||||
}
|
||||
}
|
||||
|
||||
@ -633,11 +441,9 @@ async function startMacOSAudioCapture() {
|
||||
// Kill any existing SystemAudioDump processes first
|
||||
await killExistingSystemAudioDump();
|
||||
|
||||
console.log('=== Starting macOS audio capture (OpenAI SDK) ===');
|
||||
sendToRenderer('update-status', 'Starting audio capture...');
|
||||
console.log('Starting macOS audio capture with SystemAudioDump for OpenAI SDK...');
|
||||
|
||||
const { app } = require('electron');
|
||||
const fs = require('fs');
|
||||
|
||||
let systemAudioPath;
|
||||
if (app.isPackaged) {
|
||||
@ -646,35 +452,7 @@ async function startMacOSAudioCapture() {
|
||||
systemAudioPath = path.join(__dirname, '../assets', 'SystemAudioDump');
|
||||
}
|
||||
|
||||
console.log('SystemAudioDump config:', {
|
||||
path: systemAudioPath,
|
||||
isPackaged: app.isPackaged,
|
||||
resourcesPath: process.resourcesPath,
|
||||
exists: fs.existsSync(systemAudioPath),
|
||||
});
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(systemAudioPath)) {
|
||||
console.error('FATAL: SystemAudioDump not found at:', systemAudioPath);
|
||||
sendToRenderer('update-status', 'Error: Audio binary not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check and fix executable permissions
|
||||
try {
|
||||
fs.accessSync(systemAudioPath, fs.constants.X_OK);
|
||||
console.log('SystemAudioDump is executable');
|
||||
} catch (err) {
|
||||
console.warn('SystemAudioDump not executable, fixing permissions...');
|
||||
try {
|
||||
fs.chmodSync(systemAudioPath, 0o755);
|
||||
console.log('Fixed executable permissions');
|
||||
} catch (chmodErr) {
|
||||
console.error('Failed to fix permissions:', chmodErr);
|
||||
sendToRenderer('update-status', 'Error: Cannot execute audio binary');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
console.log('SystemAudioDump path:', systemAudioPath);
|
||||
|
||||
const spawnOptions = {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
@ -683,12 +461,10 @@ async function startMacOSAudioCapture() {
|
||||
},
|
||||
};
|
||||
|
||||
console.log('Spawning SystemAudioDump...');
|
||||
systemAudioProc = spawn(systemAudioPath, [], spawnOptions);
|
||||
|
||||
if (!systemAudioProc.pid) {
|
||||
console.error('FATAL: Failed to start SystemAudioDump - no PID');
|
||||
sendToRenderer('update-status', 'Error: Audio capture failed to start');
|
||||
console.error('Failed to start SystemAudioDump');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -700,16 +476,8 @@ async function startMacOSAudioCapture() {
|
||||
const CHUNK_SIZE = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_DURATION;
|
||||
|
||||
let tempBuffer = Buffer.alloc(0);
|
||||
let chunkCount = 0;
|
||||
let firstDataReceived = false;
|
||||
|
||||
systemAudioProc.stdout.on('data', data => {
|
||||
if (!firstDataReceived) {
|
||||
firstDataReceived = true;
|
||||
console.log('First audio data received! Size:', data.length);
|
||||
sendToRenderer('update-status', 'Listening...');
|
||||
}
|
||||
|
||||
tempBuffer = Buffer.concat([tempBuffer, data]);
|
||||
|
||||
while (tempBuffer.length >= CHUNK_SIZE) {
|
||||
@ -719,17 +487,8 @@ async function startMacOSAudioCapture() {
|
||||
// Convert stereo to mono
|
||||
const monoChunk = CHANNELS === 2 ? convertStereoToMono(chunk) : chunk;
|
||||
|
||||
if (audioInputMode === 'push-to-talk' && !isPushToTalkActive) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add to audio buffer for transcription
|
||||
audioBuffer = Buffer.concat([audioBuffer, monoChunk]);
|
||||
|
||||
chunkCount++;
|
||||
if (chunkCount % 100 === 0) {
|
||||
console.log(`Audio: ${chunkCount} chunks processed, buffer size: ${audioBuffer.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Limit buffer size (max 30 seconds of audio)
|
||||
@ -740,35 +499,23 @@ async function startMacOSAudioCapture() {
|
||||
});
|
||||
|
||||
systemAudioProc.stderr.on('data', data => {
|
||||
const msg = data.toString();
|
||||
console.error('SystemAudioDump stderr:', msg);
|
||||
if (msg.toLowerCase().includes('error')) {
|
||||
sendToRenderer('update-status', 'Audio error: ' + msg.substring(0, 50));
|
||||
}
|
||||
console.error('SystemAudioDump stderr:', data.toString());
|
||||
});
|
||||
|
||||
systemAudioProc.on('close', (code, signal) => {
|
||||
console.log('SystemAudioDump closed:', { code, signal, chunksProcessed: chunkCount, tempBufferSize: tempBuffer.length });
|
||||
if (code !== 0 && code !== null) {
|
||||
sendToRenderer('update-status', `Audio stopped (exit: ${code}, signal: ${signal})`);
|
||||
}
|
||||
systemAudioProc.on('close', code => {
|
||||
console.log('SystemAudioDump process closed with code:', code);
|
||||
systemAudioProc = null;
|
||||
stopTranscriptionTimer();
|
||||
});
|
||||
|
||||
systemAudioProc.on('error', err => {
|
||||
console.error('SystemAudioDump spawn error:', err.message, err.stack);
|
||||
sendToRenderer('update-status', 'Audio error: ' + err.message);
|
||||
console.error('SystemAudioDump process error:', err);
|
||||
systemAudioProc = null;
|
||||
stopTranscriptionTimer();
|
||||
});
|
||||
|
||||
systemAudioProc.on('exit', (code, signal) => {
|
||||
console.log('SystemAudioDump exit event:', { code, signal });
|
||||
});
|
||||
|
||||
// Start periodic transcription
|
||||
updateTranscriptionTimerForPushToTalk();
|
||||
startTranscriptionTimer();
|
||||
|
||||
sendToRenderer('update-status', 'Listening...');
|
||||
|
||||
@ -776,10 +523,6 @@ async function startMacOSAudioCapture() {
|
||||
}
|
||||
|
||||
function startTranscriptionTimer() {
|
||||
// Don't start auto-transcription timer in push-to-talk mode
|
||||
if (audioInputMode === 'push-to-talk') {
|
||||
return;
|
||||
}
|
||||
stopTranscriptionTimer();
|
||||
transcriptionTimer = setInterval(transcribeBufferedAudio, TRANSCRIPTION_INTERVAL_MS);
|
||||
}
|
||||
@ -811,8 +554,6 @@ module.exports = {
|
||||
sendImageMessage,
|
||||
processAudioChunk,
|
||||
flushAudioAndTranscribe,
|
||||
togglePushToTalk,
|
||||
updatePushToTalkSettings,
|
||||
clearConversation,
|
||||
closeOpenAISDK,
|
||||
startMacOSAudioCapture,
|
||||
|
||||
@ -18,29 +18,6 @@ let currentImageQuality = 'medium'; // Store current image quality for manual sc
|
||||
|
||||
const isLinux = process.platform === 'linux';
|
||||
const isMacOS = process.platform === 'darwin';
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// Send logs to main process for file logging
|
||||
function logToMain(level, ...args) {
|
||||
const message = args
|
||||
.map(arg => {
|
||||
if (typeof arg === 'object') {
|
||||
try {
|
||||
return JSON.stringify(arg);
|
||||
} catch {
|
||||
return String(arg);
|
||||
}
|
||||
}
|
||||
return String(arg);
|
||||
})
|
||||
.join(' ');
|
||||
ipcRenderer.send('renderer-log', { level, message });
|
||||
|
||||
// Also log to console
|
||||
if (level === 'error') console.error(...args);
|
||||
else if (level === 'warn') console.warn(...args);
|
||||
else console.log(...args);
|
||||
}
|
||||
|
||||
// ============ STORAGE API ============
|
||||
// Wrapper for IPC-based storage access
|
||||
@ -136,18 +113,7 @@ const storage = {
|
||||
async getTodayLimits() {
|
||||
const result = await ipcRenderer.invoke('storage:get-today-limits');
|
||||
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
|
||||
@ -185,20 +151,16 @@ async function initializeGemini(profile = 'interview', language = 'en-US') {
|
||||
const prefs = await storage.getPreferences();
|
||||
const success = await ipcRenderer.invoke('initialize-ai-session', prefs.customPrompt || '', profile, language);
|
||||
if (success) {
|
||||
mastermind.setStatus('Live');
|
||||
cheatingDaddy.setStatus('Live');
|
||||
} else {
|
||||
mastermind.setStatus('Error: Failed to initialize AI session Gemini');
|
||||
cheatingDaddy.setStatus('Error: Failed to initialize AI session Gemini');
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for status updates
|
||||
ipcRenderer.on('update-status', (event, status) => {
|
||||
console.log('Status update:', status);
|
||||
mastermind.setStatus(status);
|
||||
});
|
||||
|
||||
ipcRenderer.on('push-to-talk-toggle', () => {
|
||||
ipcRenderer.send('push-to-talk-toggle');
|
||||
cheatingDaddy.setStatus(status);
|
||||
});
|
||||
|
||||
async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'medium') {
|
||||
@ -315,21 +277,7 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
||||
|
||||
console.log('Linux capture started - system audio:', mediaStream.getAudioTracks().length > 0, 'microphone mode:', audioMode);
|
||||
} else {
|
||||
// Windows - show custom screen picker first
|
||||
logToMain('info', '=== Starting Windows audio capture ===');
|
||||
mastermind.setStatus('Choose screen to share...');
|
||||
|
||||
// Show screen picker dialog
|
||||
const appElement = document.querySelector('mastermind-app');
|
||||
const pickerResult = await appElement.showScreenPickerDialog();
|
||||
|
||||
if (pickerResult.cancelled) {
|
||||
mastermind.setStatus('Cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
mastermind.setStatus('Starting capture...');
|
||||
|
||||
// Windows - use display media with loopback for system audio
|
||||
mediaStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: {
|
||||
frameRate: 1,
|
||||
@ -345,29 +293,10 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
||||
},
|
||||
});
|
||||
|
||||
const audioTracks = mediaStream.getAudioTracks();
|
||||
const videoTracks = mediaStream.getVideoTracks();
|
||||
console.log('Windows capture started with loopback audio');
|
||||
|
||||
logToMain('info', 'Windows capture result:', {
|
||||
hasVideo: videoTracks.length > 0,
|
||||
hasAudio: audioTracks.length > 0,
|
||||
audioTrackInfo: audioTracks.map(t => ({
|
||||
label: t.label,
|
||||
enabled: t.enabled,
|
||||
muted: t.muted,
|
||||
readyState: t.readyState,
|
||||
settings: t.getSettings(),
|
||||
})),
|
||||
});
|
||||
|
||||
if (audioTracks.length === 0) {
|
||||
logToMain('warn', 'WARNING: No audio tracks! User must check "Share audio" in screen picker dialog');
|
||||
mastermind.setStatus('Warning: No audio - enable "Share audio" checkbox');
|
||||
} else {
|
||||
logToMain('info', 'Audio track acquired, setting up processing...');
|
||||
// Setup audio processing for Windows loopback audio only
|
||||
setupWindowsLoopbackProcessing();
|
||||
}
|
||||
// Setup audio processing for Windows loopback audio only
|
||||
setupWindowsLoopbackProcessing();
|
||||
|
||||
if (audioMode === 'mic_only' || audioMode === 'both') {
|
||||
let micStream = null;
|
||||
@ -400,10 +329,10 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
||||
console.log('Manual mode enabled - screenshots will be captured on demand only');
|
||||
} catch (err) {
|
||||
console.error('Error starting capture:', err);
|
||||
|
||||
|
||||
// Provide more helpful error messages based on error type
|
||||
let errorMessage = err.message || 'Failed to start capture';
|
||||
|
||||
|
||||
if (errorMessage.toLowerCase().includes('timeout')) {
|
||||
errorMessage = 'Screen capture timed out. Please try again and select a screen quickly.';
|
||||
} else if (errorMessage.toLowerCase().includes('permission') || errorMessage.toLowerCase().includes('denied')) {
|
||||
@ -413,8 +342,8 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
|
||||
} else if (errorMessage.toLowerCase().includes('aborted') || errorMessage.toLowerCase().includes('cancel')) {
|
||||
errorMessage = 'Screen selection was cancelled. Please try again.';
|
||||
}
|
||||
|
||||
mastermind.setStatus('Error: ' + errorMessage);
|
||||
|
||||
cheatingDaddy.setStatus('Error: ' + errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,75 +412,32 @@ function setupLinuxSystemAudioProcessing() {
|
||||
|
||||
function setupWindowsLoopbackProcessing() {
|
||||
// Setup audio processing for Windows loopback audio only
|
||||
logToMain('info', 'Setting up Windows loopback audio processing...');
|
||||
audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
|
||||
const source = audioContext.createMediaStreamSource(mediaStream);
|
||||
audioProcessor = audioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
|
||||
|
||||
try {
|
||||
audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
|
||||
let audioBuffer = [];
|
||||
const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
|
||||
|
||||
logToMain('info', 'AudioContext created:', {
|
||||
state: audioContext.state,
|
||||
sampleRate: audioContext.sampleRate,
|
||||
});
|
||||
audioProcessor.onaudioprocess = async e => {
|
||||
const inputData = e.inputBuffer.getChannelData(0);
|
||||
audioBuffer.push(...inputData);
|
||||
|
||||
// Resume AudioContext if suspended (Chrome policy)
|
||||
if (audioContext.state === 'suspended') {
|
||||
logToMain('warn', 'AudioContext suspended, attempting resume...');
|
||||
audioContext
|
||||
.resume()
|
||||
.then(() => {
|
||||
logToMain('info', 'AudioContext resumed successfully');
|
||||
})
|
||||
.catch(err => {
|
||||
logToMain('error', 'Failed to resume AudioContext:', err.message);
|
||||
});
|
||||
// Process audio in chunks
|
||||
while (audioBuffer.length >= samplesPerChunk) {
|
||||
const chunk = audioBuffer.splice(0, samplesPerChunk);
|
||||
const pcmData16 = convertFloat32ToInt16(chunk);
|
||||
const base64Data = arrayBufferToBase64(pcmData16.buffer);
|
||||
|
||||
await ipcRenderer.invoke('send-audio-content', {
|
||||
data: base64Data,
|
||||
mimeType: 'audio/pcm;rate=24000',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const source = audioContext.createMediaStreamSource(mediaStream);
|
||||
audioProcessor = audioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
|
||||
|
||||
let audioBuffer = [];
|
||||
const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
|
||||
let chunkCount = 0;
|
||||
let totalSamples = 0;
|
||||
|
||||
audioProcessor.onaudioprocess = async e => {
|
||||
const inputData = e.inputBuffer.getChannelData(0);
|
||||
audioBuffer.push(...inputData);
|
||||
totalSamples += inputData.length;
|
||||
|
||||
// Process audio in chunks
|
||||
while (audioBuffer.length >= samplesPerChunk) {
|
||||
const chunk = audioBuffer.splice(0, samplesPerChunk);
|
||||
const pcmData16 = convertFloat32ToInt16(chunk);
|
||||
const base64Data = arrayBufferToBase64(pcmData16.buffer);
|
||||
|
||||
await ipcRenderer.invoke('send-audio-content', {
|
||||
data: base64Data,
|
||||
mimeType: 'audio/pcm;rate=24000',
|
||||
});
|
||||
|
||||
chunkCount++;
|
||||
|
||||
// Log progress every 100 chunks (~10 seconds)
|
||||
if (chunkCount === 1) {
|
||||
logToMain('info', 'First audio chunk sent to AI');
|
||||
mastermind.setStatus('Listening...');
|
||||
} else if (chunkCount % 100 === 0) {
|
||||
// Calculate max amplitude to check if we're getting real audio
|
||||
const maxAmp = Math.max(...chunk.map(Math.abs));
|
||||
logToMain('info', `Audio progress: ${chunkCount} chunks, maxAmplitude: ${maxAmp.toFixed(4)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
source.connect(audioProcessor);
|
||||
audioProcessor.connect(audioContext.destination);
|
||||
|
||||
logToMain('info', 'Windows audio processing pipeline connected');
|
||||
} catch (err) {
|
||||
logToMain('error', 'Error setting up Windows audio:', err.message, err.stack);
|
||||
mastermind.setStatus('Audio error: ' + err.message);
|
||||
}
|
||||
source.connect(audioProcessor);
|
||||
audioProcessor.connect(audioContext.destination);
|
||||
}
|
||||
|
||||
async function captureScreenshot(imageQuality = 'medium', isManual = false) {
|
||||
@ -705,7 +591,7 @@ async function startRegionSelection() {
|
||||
|
||||
if (!mediaStream) {
|
||||
console.error('No media stream available. Please start capture first.');
|
||||
mastermind?.addNewResponse('Please start screen capture first before selecting a region.');
|
||||
cheatingDaddy?.addNewResponse('Please start screen capture first before selecting a region.');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -783,7 +669,17 @@ async function captureRegionFromScreenshot(rect, screenshotDataUrl) {
|
||||
const cropContext = cropCanvas.getContext('2d');
|
||||
|
||||
// Draw only the selected region
|
||||
cropContext.drawImage(img, scaledRect.left, scaledRect.top, scaledRect.width, scaledRect.height, 0, 0, scaledRect.width, scaledRect.height);
|
||||
cropContext.drawImage(
|
||||
img,
|
||||
scaledRect.left,
|
||||
scaledRect.top,
|
||||
scaledRect.width,
|
||||
scaledRect.height,
|
||||
0,
|
||||
0,
|
||||
scaledRect.width,
|
||||
scaledRect.height
|
||||
);
|
||||
|
||||
// Convert to blob and send
|
||||
cropCanvas.toBlob(
|
||||
@ -811,7 +707,7 @@ async function captureRegionFromScreenshot(rect, screenshotDataUrl) {
|
||||
console.log(`Region capture response completed from ${result.model}`);
|
||||
} else {
|
||||
console.error('Failed to get region capture response:', result.error);
|
||||
mastermind.addNewResponse(`Error: ${result.error}`);
|
||||
cheatingDaddy.addNewResponse(`Error: ${result.error}`);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
@ -903,7 +799,7 @@ async function captureManualScreenshot(imageQuality = null) {
|
||||
// Response already displayed via streaming events (new-response/update-response)
|
||||
} else {
|
||||
console.error('Failed to get image response:', result.error);
|
||||
mastermind.addNewResponse(`Error: ${result.error}`);
|
||||
cheatingDaddy.addNewResponse(`Error: ${result.error}`);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
@ -996,7 +892,7 @@ ipcRenderer.on('save-session-context', async (event, data) => {
|
||||
try {
|
||||
await storage.saveSession(data.sessionId, {
|
||||
profile: data.profile,
|
||||
customPrompt: data.customPrompt,
|
||||
customPrompt: data.customPrompt
|
||||
});
|
||||
console.log('Session context saved:', data.sessionId, 'profile:', data.profile);
|
||||
} catch (error) {
|
||||
@ -1010,7 +906,7 @@ ipcRenderer.on('save-screen-analysis', async (event, data) => {
|
||||
await storage.saveSession(data.sessionId, {
|
||||
screenAnalysisHistory: data.fullHistory,
|
||||
profile: data.profile,
|
||||
customPrompt: data.customPrompt,
|
||||
customPrompt: data.customPrompt
|
||||
});
|
||||
console.log('Screen analysis saved:', data.sessionId);
|
||||
} catch (error) {
|
||||
@ -1026,11 +922,11 @@ ipcRenderer.on('clear-sensitive-data', async () => {
|
||||
|
||||
// Handle shortcuts based on current view
|
||||
function handleShortcut(shortcutKey) {
|
||||
const currentView = mastermind.getCurrentView();
|
||||
const currentView = cheatingDaddy.getCurrentView();
|
||||
|
||||
if (shortcutKey === 'ctrl+enter' || shortcutKey === 'cmd+enter') {
|
||||
if (currentView === 'main') {
|
||||
mastermind.element().handleStart();
|
||||
cheatingDaddy.element().handleStart();
|
||||
} else {
|
||||
captureManualScreenshot();
|
||||
}
|
||||
@ -1038,109 +934,67 @@ function handleShortcut(shortcutKey) {
|
||||
}
|
||||
|
||||
// Create reference to the main app element
|
||||
const mastermindApp = document.querySelector('mastermind-app');
|
||||
const cheatingDaddyApp = document.querySelector('cheating-daddy-app');
|
||||
|
||||
// ============ THEME SYSTEM ============
|
||||
const theme = {
|
||||
themes: {
|
||||
dark: {
|
||||
background: '#1e1e1e',
|
||||
text: '#e0e0e0',
|
||||
textSecondary: '#a0a0a0',
|
||||
textMuted: '#6b6b6b',
|
||||
border: '#333333',
|
||||
accent: '#ffffff',
|
||||
btnPrimaryBg: '#ffffff',
|
||||
btnPrimaryText: '#000000',
|
||||
btnPrimaryHover: '#e0e0e0',
|
||||
tooltipBg: '#1a1a1a',
|
||||
tooltipText: '#ffffff',
|
||||
keyBg: 'rgba(255,255,255,0.1)',
|
||||
text: '#e0e0e0', textSecondary: '#a0a0a0', textMuted: '#6b6b6b',
|
||||
border: '#333333', accent: '#ffffff',
|
||||
btnPrimaryBg: '#ffffff', btnPrimaryText: '#000000', btnPrimaryHover: '#e0e0e0',
|
||||
tooltipBg: '#1a1a1a', tooltipText: '#ffffff',
|
||||
keyBg: 'rgba(255,255,255,0.1)'
|
||||
},
|
||||
light: {
|
||||
background: '#ffffff',
|
||||
text: '#1a1a1a',
|
||||
textSecondary: '#555555',
|
||||
textMuted: '#888888',
|
||||
border: '#e0e0e0',
|
||||
accent: '#000000',
|
||||
btnPrimaryBg: '#1a1a1a',
|
||||
btnPrimaryText: '#ffffff',
|
||||
btnPrimaryHover: '#333333',
|
||||
tooltipBg: '#1a1a1a',
|
||||
tooltipText: '#ffffff',
|
||||
keyBg: 'rgba(0,0,0,0.1)',
|
||||
text: '#1a1a1a', textSecondary: '#555555', textMuted: '#888888',
|
||||
border: '#e0e0e0', accent: '#000000',
|
||||
btnPrimaryBg: '#1a1a1a', btnPrimaryText: '#ffffff', btnPrimaryHover: '#333333',
|
||||
tooltipBg: '#1a1a1a', tooltipText: '#ffffff',
|
||||
keyBg: 'rgba(0,0,0,0.1)'
|
||||
},
|
||||
midnight: {
|
||||
background: '#0d1117',
|
||||
text: '#c9d1d9',
|
||||
textSecondary: '#8b949e',
|
||||
textMuted: '#6e7681',
|
||||
border: '#30363d',
|
||||
accent: '#58a6ff',
|
||||
btnPrimaryBg: '#58a6ff',
|
||||
btnPrimaryText: '#0d1117',
|
||||
btnPrimaryHover: '#79b8ff',
|
||||
tooltipBg: '#161b22',
|
||||
tooltipText: '#c9d1d9',
|
||||
keyBg: 'rgba(88,166,255,0.15)',
|
||||
text: '#c9d1d9', textSecondary: '#8b949e', textMuted: '#6e7681',
|
||||
border: '#30363d', accent: '#58a6ff',
|
||||
btnPrimaryBg: '#58a6ff', btnPrimaryText: '#0d1117', btnPrimaryHover: '#79b8ff',
|
||||
tooltipBg: '#161b22', tooltipText: '#c9d1d9',
|
||||
keyBg: 'rgba(88,166,255,0.15)'
|
||||
},
|
||||
sepia: {
|
||||
background: '#f4ecd8',
|
||||
text: '#5c4b37',
|
||||
textSecondary: '#7a6a56',
|
||||
textMuted: '#998875',
|
||||
border: '#d4c8b0',
|
||||
accent: '#8b4513',
|
||||
btnPrimaryBg: '#5c4b37',
|
||||
btnPrimaryText: '#f4ecd8',
|
||||
btnPrimaryHover: '#7a6a56',
|
||||
tooltipBg: '#5c4b37',
|
||||
tooltipText: '#f4ecd8',
|
||||
keyBg: 'rgba(92,75,55,0.15)',
|
||||
text: '#5c4b37', textSecondary: '#7a6a56', textMuted: '#998875',
|
||||
border: '#d4c8b0', accent: '#8b4513',
|
||||
btnPrimaryBg: '#5c4b37', btnPrimaryText: '#f4ecd8', btnPrimaryHover: '#7a6a56',
|
||||
tooltipBg: '#5c4b37', tooltipText: '#f4ecd8',
|
||||
keyBg: 'rgba(92,75,55,0.15)'
|
||||
},
|
||||
nord: {
|
||||
background: '#2e3440',
|
||||
text: '#eceff4',
|
||||
textSecondary: '#d8dee9',
|
||||
textMuted: '#4c566a',
|
||||
border: '#3b4252',
|
||||
accent: '#88c0d0',
|
||||
btnPrimaryBg: '#88c0d0',
|
||||
btnPrimaryText: '#2e3440',
|
||||
btnPrimaryHover: '#8fbcbb',
|
||||
tooltipBg: '#3b4252',
|
||||
tooltipText: '#eceff4',
|
||||
keyBg: 'rgba(136,192,208,0.15)',
|
||||
text: '#eceff4', textSecondary: '#d8dee9', textMuted: '#4c566a',
|
||||
border: '#3b4252', accent: '#88c0d0',
|
||||
btnPrimaryBg: '#88c0d0', btnPrimaryText: '#2e3440', btnPrimaryHover: '#8fbcbb',
|
||||
tooltipBg: '#3b4252', tooltipText: '#eceff4',
|
||||
keyBg: 'rgba(136,192,208,0.15)'
|
||||
},
|
||||
dracula: {
|
||||
background: '#282a36',
|
||||
text: '#f8f8f2',
|
||||
textSecondary: '#bd93f9',
|
||||
textMuted: '#6272a4',
|
||||
border: '#44475a',
|
||||
accent: '#ff79c6',
|
||||
btnPrimaryBg: '#ff79c6',
|
||||
btnPrimaryText: '#282a36',
|
||||
btnPrimaryHover: '#ff92d0',
|
||||
tooltipBg: '#44475a',
|
||||
tooltipText: '#f8f8f2',
|
||||
keyBg: 'rgba(255,121,198,0.15)',
|
||||
text: '#f8f8f2', textSecondary: '#bd93f9', textMuted: '#6272a4',
|
||||
border: '#44475a', accent: '#ff79c6',
|
||||
btnPrimaryBg: '#ff79c6', btnPrimaryText: '#282a36', btnPrimaryHover: '#ff92d0',
|
||||
tooltipBg: '#44475a', tooltipText: '#f8f8f2',
|
||||
keyBg: 'rgba(255,121,198,0.15)'
|
||||
},
|
||||
abyss: {
|
||||
background: '#0a0a0a',
|
||||
text: '#d4d4d4',
|
||||
textSecondary: '#808080',
|
||||
textMuted: '#505050',
|
||||
border: '#1a1a1a',
|
||||
accent: '#ffffff',
|
||||
btnPrimaryBg: '#ffffff',
|
||||
btnPrimaryText: '#0a0a0a',
|
||||
btnPrimaryHover: '#d4d4d4',
|
||||
tooltipBg: '#141414',
|
||||
tooltipText: '#d4d4d4',
|
||||
keyBg: 'rgba(255,255,255,0.08)',
|
||||
},
|
||||
text: '#d4d4d4', textSecondary: '#808080', textMuted: '#505050',
|
||||
border: '#1a1a1a', accent: '#ffffff',
|
||||
btnPrimaryBg: '#ffffff', btnPrimaryText: '#0a0a0a', btnPrimaryHover: '#d4d4d4',
|
||||
tooltipBg: '#141414', tooltipText: '#d4d4d4',
|
||||
keyBg: 'rgba(255,255,255,0.08)'
|
||||
}
|
||||
},
|
||||
|
||||
current: 'dark',
|
||||
@ -1157,31 +1011,29 @@ const theme = {
|
||||
sepia: 'Sepia',
|
||||
nord: 'Nord',
|
||||
dracula: 'Dracula',
|
||||
abyss: 'Abyss',
|
||||
abyss: 'Abyss'
|
||||
};
|
||||
return Object.keys(this.themes).map(key => ({
|
||||
value: key,
|
||||
name: names[key] || key,
|
||||
colors: this.themes[key],
|
||||
colors: this.themes[key]
|
||||
}));
|
||||
},
|
||||
|
||||
hexToRgb(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
}
|
||||
: { r: 30, g: 30, b: 30 };
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : { r: 30, g: 30, b: 30 };
|
||||
},
|
||||
|
||||
lightenColor(rgb, amount) {
|
||||
return {
|
||||
r: Math.min(255, rgb.r + amount),
|
||||
g: Math.min(255, rgb.g + amount),
|
||||
b: Math.min(255, rgb.b + amount),
|
||||
b: Math.min(255, rgb.b + amount)
|
||||
};
|
||||
},
|
||||
|
||||
@ -1189,7 +1041,7 @@ const theme = {
|
||||
return {
|
||||
r: Math.max(0, rgb.r - amount),
|
||||
g: Math.max(0, rgb.g - amount),
|
||||
b: Math.max(0, rgb.b - amount),
|
||||
b: Math.max(0, rgb.b - amount)
|
||||
};
|
||||
},
|
||||
|
||||
@ -1269,26 +1121,26 @@ const theme = {
|
||||
async save(themeName) {
|
||||
await storage.updatePreference('theme', themeName);
|
||||
this.apply(themeName);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// Consolidated mastermind object - all functions in one place
|
||||
const mastermind = {
|
||||
// Consolidated cheatingDaddy object - all functions in one place
|
||||
const cheatingDaddy = {
|
||||
// App version
|
||||
getVersion: async () => ipcRenderer.invoke('get-app-version'),
|
||||
|
||||
// Element access
|
||||
element: () => mastermindApp,
|
||||
e: () => mastermindApp,
|
||||
element: () => cheatingDaddyApp,
|
||||
e: () => cheatingDaddyApp,
|
||||
|
||||
// App state functions - access properties directly from the app element
|
||||
getCurrentView: () => mastermindApp.currentView,
|
||||
getLayoutMode: () => mastermindApp.layoutMode,
|
||||
getCurrentView: () => cheatingDaddyApp.currentView,
|
||||
getLayoutMode: () => cheatingDaddyApp.layoutMode,
|
||||
|
||||
// Status and response functions
|
||||
setStatus: text => mastermindApp.setStatus(text),
|
||||
addNewResponse: response => mastermindApp.addNewResponse(response),
|
||||
updateCurrentResponse: response => mastermindApp.updateCurrentResponse(response),
|
||||
setStatus: text => cheatingDaddyApp.setStatus(text),
|
||||
addNewResponse: response => cheatingDaddyApp.addNewResponse(response),
|
||||
updateCurrentResponse: response => cheatingDaddyApp.updateCurrentResponse(response),
|
||||
|
||||
// Core functionality
|
||||
initializeGemini,
|
||||
@ -1312,7 +1164,7 @@ const mastermind = {
|
||||
};
|
||||
|
||||
// Make it globally available
|
||||
window.mastermind = mastermind;
|
||||
window.cheatingDaddy = cheatingDaddy;
|
||||
|
||||
// Load theme after DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
|
||||
@ -9,7 +9,6 @@ let windowResizing = false;
|
||||
let resizeAnimation = null;
|
||||
const RESIZE_ANIMATION_DURATION = 500; // milliseconds
|
||||
|
||||
|
||||
function createWindow(sendToRenderer, geminiSessionRef) {
|
||||
// Get layout preference (default to 'normal')
|
||||
let windowWidth = 1100;
|
||||
@ -34,55 +33,60 @@ function createWindow(sendToRenderer, geminiSessionRef) {
|
||||
});
|
||||
|
||||
const { session, desktopCapturer } = require('electron');
|
||||
|
||||
// Store selected source for Windows custom picker
|
||||
let selectedSourceId = null;
|
||||
|
||||
// Setup display media handler based on platform
|
||||
|
||||
// Setup display media request handler for screen capture
|
||||
// On macOS, use system picker for better UX
|
||||
if (process.platform === 'darwin') {
|
||||
// macOS: Use native system picker
|
||||
session.defaultSession.setDisplayMediaRequestHandler(
|
||||
(request, callback) => {
|
||||
desktopCapturer.getSources({ types: ['screen'] }).then(sources => {
|
||||
async (request, callback) => {
|
||||
try {
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ['screen'],
|
||||
thumbnailSize: { width: 0, height: 0 } // Skip thumbnail generation for speed
|
||||
});
|
||||
|
||||
if (sources.length === 0) {
|
||||
console.error('No screen sources available');
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// On macOS, directly use the first screen (system already granted permission)
|
||||
console.log('Screen capture source:', sources[0].name);
|
||||
callback({ video: sources[0], audio: 'loopback' });
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting screen sources:', error);
|
||||
callback(null);
|
||||
}
|
||||
},
|
||||
{ useSystemPicker: false } // Disable system picker, use our source directly
|
||||
);
|
||||
} else {
|
||||
// On other platforms, use the system picker
|
||||
session.defaultSession.setDisplayMediaRequestHandler(
|
||||
async (request, callback) => {
|
||||
try {
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ['screen'],
|
||||
thumbnailSize: { width: 0, height: 0 }
|
||||
});
|
||||
|
||||
if (sources.length === 0) {
|
||||
console.error('No screen sources available');
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
|
||||
callback({ video: sources[0], audio: 'loopback' });
|
||||
} catch (error) {
|
||||
console.error('Error getting screen sources:', error);
|
||||
callback(null);
|
||||
}
|
||||
},
|
||||
{ useSystemPicker: true }
|
||||
);
|
||||
} else {
|
||||
// Windows/Linux: Use selected source from custom picker
|
||||
session.defaultSession.setDisplayMediaRequestHandler(async (request, callback) => {
|
||||
try {
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ['screen', 'window'],
|
||||
thumbnailSize: { width: 0, height: 0 },
|
||||
});
|
||||
|
||||
// Find the selected source or use first screen
|
||||
let source = sources[0];
|
||||
if (selectedSourceId) {
|
||||
const found = sources.find(s => s.id === selectedSourceId);
|
||||
if (found) source = found;
|
||||
}
|
||||
|
||||
if (source) {
|
||||
callback({ video: source, audio: 'loopback' });
|
||||
} else {
|
||||
callback({});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in display media handler:', error);
|
||||
callback({});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// IPC handler to set selected source
|
||||
ipcMain.handle('set-selected-source', async (event, sourceId) => {
|
||||
selectedSourceId = sourceId;
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
mainWindow.setResizable(false);
|
||||
mainWindow.setContentProtection(true);
|
||||
mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
||||
@ -156,7 +160,6 @@ function getDefaultKeybinds() {
|
||||
scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up',
|
||||
scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down',
|
||||
emergencyErase: isMac ? 'Cmd+Shift+E' : 'Ctrl+Shift+E',
|
||||
pushToTalk: isMac ? 'Ctrl+Space' : 'Ctrl+Space',
|
||||
};
|
||||
}
|
||||
|
||||
@ -166,10 +169,6 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
||||
// Unregister all existing shortcuts
|
||||
globalShortcut.unregisterAll();
|
||||
|
||||
const prefs = storage.getPreferences();
|
||||
const audioInputMode = prefs.audioInputMode || 'auto';
|
||||
const enablePushToTalk = audioInputMode === 'push-to-talk';
|
||||
|
||||
const primaryDisplay = screen.getPrimaryDisplay();
|
||||
const { width, height } = primaryDisplay.workAreaSize;
|
||||
const moveIncrement = Math.floor(Math.min(width, height) * 0.1);
|
||||
@ -259,7 +258,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
||||
|
||||
// Use the new handleShortcut function
|
||||
mainWindow.webContents.executeJavaScript(`
|
||||
mastermind.handleShortcut('${shortcutKey}');
|
||||
cheatingDaddy.handleShortcut('${shortcutKey}');
|
||||
`);
|
||||
} catch (error) {
|
||||
console.error('Error handling next step shortcut:', error);
|
||||
@ -349,18 +348,6 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
||||
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) {
|
||||
@ -492,8 +479,8 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
|
||||
// Get current view and layout mode from renderer
|
||||
let viewName, layoutMode;
|
||||
try {
|
||||
viewName = await event.sender.executeJavaScript('mastermind.getCurrentView()');
|
||||
layoutMode = await event.sender.executeJavaScript('mastermind.getLayoutMode()');
|
||||
viewName = await event.sender.executeJavaScript('cheatingDaddy.getCurrentView()');
|
||||
layoutMode = await event.sender.executeJavaScript('cheatingDaddy.getLayoutMode()');
|
||||
} catch (error) {
|
||||
console.warn('Failed to get view/layout from renderer, using defaults:', error);
|
||||
viewName = 'main';
|
||||
@ -571,10 +558,7 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
|
||||
const primaryDisplay = screen.getPrimaryDisplay();
|
||||
|
||||
// Calculate bounds that cover all displays
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
displays.forEach(display => {
|
||||
minX = Math.min(minX, display.bounds.x);
|
||||
minY = Math.min(minY, display.bounds.y);
|
||||
@ -733,7 +717,7 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
|
||||
|
||||
regionSelectionWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`);
|
||||
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve) => {
|
||||
ipcMain.once('region-selected', (event, rect) => {
|
||||
if (regionSelectionWindow && !regionSelectionWindow.isDestroyed()) {
|
||||
regionSelectionWindow.close();
|
||||
@ -776,29 +760,6 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Get available screen sources for picker
|
||||
ipcMain.handle('get-screen-sources', async () => {
|
||||
try {
|
||||
const { desktopCapturer } = require('electron');
|
||||
const sources = await desktopCapturer.getSources({
|
||||
types: ['screen', 'window'],
|
||||
thumbnailSize: { width: 150, height: 150 },
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
sources: sources.map(source => ({
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
thumbnail: source.thumbnail.toDataURL(),
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting screen sources:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@ -12,4 +12,4 @@ export async function resizeLayout() {
|
||||
} catch (error) {
|
||||
console.error('Error resizing window:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user