Compare commits

12 Commits
Author SHA1 Message Date
Илья Глазунов 669c019fd8 prettier fix 2026-01-15 23:26:09 +03:00
Илья Глазунов 528dfe01a1 Bump version to 0.5.11 and enhance audio processing with timer-based transcription for Windows
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-01-15 21:30:21 +03:00
Илья Глазунов fb6c8e3fc0 Add custom screen picker dialog for Windows audio capture and update version to 0.5.10
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-01-15 21:14:50 +03:00
Илья Глазунов 76d6fc2749 Refactor Windows audio capture setup to streamline processing and remove redundant logging
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-01-15 20:45:34 +03:00
Илья Глазунов 2f013b4751 Bump version to 0.5.8 and add silence detection timer for audio processing
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-01-15 20:21:06 +03:00
Илья Глазунов 66dce4415a Update author information in package.json 2026-01-15 19:48:41 +03:00
Илья Глазунов 6460349fc7 Bump version to 0.5.7 and update screen capture handler to use system picker
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-01-15 19:47:44 +03:00
Илья Глазунов 6926c27f20 похуй
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-01-15 19:37:13 +03:00
Илья Глазунов cbf82f9317 Refactor screen capture handler to use system picker across all platforms
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-01-15 19:28:45 +03:00
Илья Глазунов bf9ec15912 Bump version to 0.5.3 and enhance Windows audio capture using loopback
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-01-15 19:20:34 +03:00
Илья Глазунов 01b8415a02 Bump version to 0.5.2 and enhance logging for renderer and audio processing
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-01-15 19:03:54 +03:00
Илья Глазунов 4edfaf4906 Implement logging functionality and enhance audio capture error handling
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
2026-01-15 18:50:55 +03:00
24 changed files with 5603 additions and 4652 deletions
+82 -82
View File
@@ -1,100 +1,100 @@
name: Build and Release name: Build and Release
on: on:
push: push:
tags: tags:
- 'v*.*.*' - 'v*.*.*'
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs: jobs:
build: build:
if: github.server_url == 'https://github.com' 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
runs-on: ${{ matrix.os }} 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
steps: runs-on: ${{ matrix.os }}
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js steps:
uses: actions/setup-node@v4 - name: Checkout code
with: uses: actions/checkout@v4
node-version: 20
- name: Setup pnpm - name: Setup Node.js
uses: pnpm/action-setup@v4 uses: actions/setup-node@v4
with: with:
version: 9 node-version: 20
- name: Install dependencies - name: Setup pnpm
run: pnpm install uses: pnpm/action-setup@v4
with:
version: 9
- name: Build for ${{ matrix.platform }}-${{ matrix.arch }} - name: Install dependencies
run: pnpm run make -- --arch=${{ matrix.arch }} run: pnpm install
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload artifacts - name: Build for ${{ matrix.platform }}-${{ matrix.arch }}
uses: actions/upload-artifact@v4 run: pnpm run make -- --arch=${{ matrix.arch }}
with: env:
name: release-${{ matrix.platform }}-${{ matrix.arch }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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: - name: Upload artifacts
needs: build uses: actions/upload-artifact@v4
runs-on: ubuntu-latest with:
if: github.server_url == 'https://github.com' name: release-${{ matrix.platform }}-${{ matrix.arch }}
path: |
permissions: out/make/**/*.dmg
contents: write out/make/**/*.zip
out/make/**/*.exe
out/make/**/*.AppImage
out/make/**/*.deb
out/make/**/*.rpm
if-no-files-found: ignore
steps: release:
- name: Download all artifacts needs: build
uses: actions/download-artifact@v4 runs-on: ubuntu-latest
with: if: github.server_url == 'https://github.com'
path: artifacts
pattern: release-*
merge-multiple: true
- name: List artifacts permissions:
run: find artifacts -type f | head -50 contents: write
- name: Create Draft Release steps:
uses: softprops/action-gh-release@v2 - name: Download all artifacts
with: uses: actions/download-artifact@v4
draft: true with:
generate_release_notes: true path: artifacts
files: | pattern: release-*
artifacts/**/*.dmg merge-multiple: true
artifacts/**/*.zip
artifacts/**/*.exe - name: List artifacts
artifacts/**/*.AppImage run: find artifacts -type f | head -50
artifacts/**/*.deb
artifacts/**/*.rpm - name: Create Draft Release
env: uses: softprops/action-gh-release@v2
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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 }}
+4 -4
View File
@@ -10,16 +10,16 @@ packaging.
Install dependencies and run the development app: Install dependencies and run the development app:
``` ```
1. npm install 1. pnpm install
2. npm start 2. pnpm start
``` ```
## Style ## Style
Run `npx prettier --write .` before committing. Prettier uses the settings in Run `pnpm prettier --write .` before committing. Prettier uses the settings in
`.prettierrc` (four-space indentation, print width 150, semicolons and single `.prettierrc` (four-space indentation, print width 150, semicolons and single
quotes). `src/assets` and `node_modules` are ignored via `.prettierignore`. quotes). `src/assets` and `node_modules` are ignored via `.prettierignore`.
The project does not provide linting; `npm run lint` simply prints The project does not provide linting; `pnpm run lint` simply prints
"No linting configured". "No linting configured".
## Code standards ## Code standards
+2
View File
@@ -10,6 +10,8 @@
<true/> <true/>
<key>com.apple.security.cs.disable-library-validation</key> <key>com.apple.security.cs.disable-library-validation</key>
<true/> <true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.device.audio-input</key> <key>com.apple.security.device.audio-input</key>
<true/> <true/>
<key>com.apple.security.device.microphone</key> <key>com.apple.security.device.microphone</key>
+35 -10
View File
@@ -1,5 +1,7 @@
const { FusesPlugin } = require('@electron-forge/plugin-fuses'); const { FusesPlugin } = require('@electron-forge/plugin-fuses');
const { FuseV1Options, FuseVersion } = require('@electron/fuses'); const { FuseV1Options, FuseVersion } = require('@electron/fuses');
const path = require('path');
const fs = require('fs');
module.exports = { module.exports = {
packagerConfig: { packagerConfig: {
@@ -7,18 +9,37 @@ module.exports = {
extraResource: ['./src/assets/SystemAudioDump'], extraResource: ['./src/assets/SystemAudioDump'],
name: 'Cheating Daddy', name: 'Cheating Daddy',
icon: 'src/assets/logo', 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 // use `security find-identity -v -p codesigning` to find your identity
// for macos signing // for macos signing
// also fuck apple // Disabled for local builds - ad-hoc signing causes issues
// osxSign: { // osxSign: {
// identity: '<paste your identity here>', // identity: '-', // ad-hoc signing (no Apple Developer account needed)
// optionsForFile: (filePath) => { // optionsForFile: (filePath) => {
// return { // return {
// entitlements: 'entitlements.plist', // entitlements: 'entitlements.plist',
// }; // };
// }, // },
// }, // },
// notarize if off cuz i ran this for 6 hours and it still didnt finish // notarize is off - requires Apple Developer account
// osxNotarize: { // osxNotarize: {
// appleId: 'your apple id', // appleId: 'your apple id',
// appleIdPassword: 'app specific password', // appleIdPassword: 'app specific password',
@@ -40,6 +61,10 @@ module.exports = {
{ {
name: '@electron-forge/maker-dmg', name: '@electron-forge/maker-dmg',
platforms: ['darwin'], platforms: ['darwin'],
config: {
name: 'CheatingDaddy',
format: 'ULFO',
},
}, },
{ {
name: '@reforged/maker-appimage', name: '@reforged/maker-appimage',
@@ -51,8 +76,8 @@ module.exports = {
genericName: 'AI Assistant', genericName: 'AI Assistant',
description: 'AI assistant for interviews and learning', description: 'AI assistant for interviews and learning',
categories: ['Development', 'Education'], categories: ['Development', 'Education'],
icon: 'src/assets/logo.png' icon: 'src/assets/logo.png',
} },
}, },
}, },
], ],
+4 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "cheating-daddy", "name": "cheating-daddy",
"productName": "cheating-daddy", "productName": "cheating-daddy",
"version": "0.5.0", "version": "0.5.11",
"description": "cheating daddy", "description": "cheating daddy",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
@@ -19,8 +19,8 @@
"cheating daddy ai assistant for interviews" "cheating daddy ai assistant for interviews"
], ],
"author": { "author": {
"name": "sohzm", "name": "ShiftyX1",
"email": "sohambharambe9@gmail.com" "email": "lead@pyserve.org"
}, },
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
@@ -39,6 +39,7 @@
"@electron-forge/plugin-auto-unpack-natives": "^7.11.1", "@electron-forge/plugin-auto-unpack-natives": "^7.11.1",
"@electron-forge/plugin-fuses": "^7.11.1", "@electron-forge/plugin-fuses": "^7.11.1",
"@electron/fuses": "^2.0.0", "@electron/fuses": "^2.0.0",
"@electron/osx-sign": "^2.3.0",
"@reforged/maker-appimage": "^5.1.1", "@reforged/maker-appimage": "^5.1.1",
"electron": "^39.2.7" "electron": "^39.2.7"
} }
+4064 -4050
View File
File diff suppressed because it is too large Load Diff
+58 -26
View File
@@ -3,7 +3,11 @@ import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
export class AppHeader extends LitElement { export class AppHeader extends LitElement {
static styles = css` static styles = css`
* { * {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family:
'Inter',
-apple-system,
BlinkMacSystemFont,
sans-serif;
cursor: default; cursor: default;
user-select: none; user-select: none;
} }
@@ -155,9 +159,11 @@ export class AppHeader extends LitElement {
white-space: normal; white-space: normal;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.15s ease, visibility 0.15s ease; transition:
opacity 0.15s ease,
visibility 0.15s ease;
pointer-events: none; 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; z-index: 1000;
line-height: 1.4; line-height: 1.4;
} }
@@ -224,9 +230,11 @@ export class AppHeader extends LitElement {
white-space: nowrap; white-space: nowrap;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.15s ease, visibility 0.15s ease; transition:
opacity 0.15s ease,
visibility 0.15s ease;
pointer-events: none; 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; z-index: 1000;
} }
@@ -421,7 +429,7 @@ export class AppHeader extends LitElement {
const names = { const names = {
'gemini': 'Gemini', 'gemini': 'Gemini',
'openai-realtime': 'OpenAI Realtime', 'openai-realtime': 'OpenAI Realtime',
'openai-sdk': 'OpenAI SDK' 'openai-sdk': 'OpenAI SDK',
}; };
return names[this.aiProvider] || this.aiProvider; return names[this.aiProvider] || this.aiProvider;
} }
@@ -433,7 +441,7 @@ export class AppHeader extends LitElement {
} }
const { model, visionModel, whisperModel } = this.modelInfo; const { model, visionModel, whisperModel } = this.modelInfo;
// Show a compact badge with tooltip for model details // Show a compact badge with tooltip for model details
return html` return html`
<div class="model-badge-wrapper"> <div class="model-badge-wrapper">
@@ -471,39 +479,59 @@ export class AppHeader extends LitElement {
<span>${elapsedTime}</span> <span>${elapsedTime}</span>
<div class="status-wrapper"> <div class="status-wrapper">
<span class="status-text ${isError ? 'error' : ''}">${shortStatus}</span> <span class="status-text ${isError ? 'error' : ''}">${shortStatus}</span>
${isError ? html` ${isError
<div class="status-tooltip"> ? html`
<div class="tooltip-label">Error Details</div> <div class="status-tooltip">
<div class="tooltip-content">${this.statusText}</div> <div class="tooltip-label">Error Details</div>
</div> <div class="tooltip-content">${this.statusText}</div>
` : ''} </div>
`
: ''}
</div> </div>
${this.isClickThrough ? html`<span class="click-through-indicator">click-through</span>` : ''} ${this.isClickThrough ? html`<span class="click-through-indicator">click-through</span>` : ''}
` `
: ''} : ''}
${this.currentView === 'main' ${this.currentView === 'main'
? html` ? html`
${this.updateAvailable ? html` ${this.updateAvailable
<button class="update-button" @click=${this._openUpdatePage}> ? html`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor"> <button class="update-button" @click=${this._openUpdatePage}>
<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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
</svg> <path
Update available fill-rule="evenodd"
</button> 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}> <button class="icon-button" @click=${this.onHistoryClick}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path 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> </svg>
</button> </button>
<button class="icon-button" @click=${this.onCustomizeClick}> <button class="icon-button" @click=${this.onCustomizeClick}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path 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> </svg>
</button> </button>
<button class="icon-button" @click=${this.onHelpClick}> <button class="icon-button" @click=${this.onHelpClick}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path 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> </svg>
</button> </button>
` `
@@ -516,14 +544,18 @@ export class AppHeader extends LitElement {
</button> </button>
<button @click=${this.onCloseClick} class="icon-button window-close"> <button @click=${this.onCloseClick} class="icon-button window-close">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path 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> </svg>
</button> </button>
` `
: html` : html`
<button @click=${this.isNavigationView() ? this.onBackClick : this.onCloseClick} class="icon-button window-close"> <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"> <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> </svg>
</button> </button>
`} `}
+76 -20
View File
@@ -6,12 +6,17 @@ import { HelpView } from '../views/HelpView.js';
import { HistoryView } from '../views/HistoryView.js'; import { HistoryView } from '../views/HistoryView.js';
import { AssistantView } from '../views/AssistantView.js'; import { AssistantView } from '../views/AssistantView.js';
import { OnboardingView } from '../views/OnboardingView.js'; import { OnboardingView } from '../views/OnboardingView.js';
import { ScreenPickerDialog } from '../views/ScreenPickerDialog.js';
export class CheatingDaddyApp extends LitElement { export class CheatingDaddyApp extends LitElement {
static styles = css` static styles = css`
* { * {
box-sizing: border-box; box-sizing: border-box;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family:
'Inter',
-apple-system,
BlinkMacSystemFont,
sans-serif;
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
cursor: default; cursor: default;
@@ -112,6 +117,8 @@ export class CheatingDaddyApp extends LitElement {
_storageLoaded: { state: true }, _storageLoaded: { state: true },
aiProvider: { type: String }, aiProvider: { type: String },
modelInfo: { type: Object }, modelInfo: { type: Object },
showScreenPicker: { type: Boolean },
screenSources: { type: Array },
}; };
constructor() { constructor() {
@@ -137,6 +144,8 @@ export class CheatingDaddyApp extends LitElement {
this._storageLoaded = false; this._storageLoaded = false;
this.aiProvider = 'gemini'; this.aiProvider = 'gemini';
this.modelInfo = { model: '', visionModel: '', whisperModel: '' }; this.modelInfo = { model: '', visionModel: '', whisperModel: '' };
this.showScreenPicker = false;
this.screenSources = [];
// Load from storage // Load from storage
this._loadFromStorage(); this._loadFromStorage();
@@ -147,17 +156,14 @@ export class CheatingDaddyApp extends LitElement {
const [config, prefs, openaiSdkCreds] = await Promise.all([ const [config, prefs, openaiSdkCreds] = await Promise.all([
cheatingDaddy.storage.getConfig(), cheatingDaddy.storage.getConfig(),
cheatingDaddy.storage.getPreferences(), cheatingDaddy.storage.getPreferences(),
cheatingDaddy.storage.getOpenAISDKCredentials() cheatingDaddy.storage.getOpenAISDKCredentials(),
]); ]);
// Check onboarding status // Check onboarding status
this.currentView = config.onboarded ? 'main' : 'onboarding'; this.currentView = config.onboarded ? 'main' : 'onboarding';
// Apply background appearance (color + transparency) // Apply background appearance (color + transparency)
this.applyBackgroundAppearance( this.applyBackgroundAppearance(prefs.backgroundColor ?? '#1e1e1e', prefs.backgroundTransparency ?? 0.8);
prefs.backgroundColor ?? '#1e1e1e',
prefs.backgroundTransparency ?? 0.8
);
// Load preferences // Load preferences
this.selectedProfile = prefs.selectedProfile || 'interview'; this.selectedProfile = prefs.selectedProfile || 'interview';
@@ -165,13 +171,13 @@ export class CheatingDaddyApp extends LitElement {
this.selectedScreenshotInterval = prefs.selectedScreenshotInterval || '5'; this.selectedScreenshotInterval = prefs.selectedScreenshotInterval || '5';
this.selectedImageQuality = prefs.selectedImageQuality || 'medium'; this.selectedImageQuality = prefs.selectedImageQuality || 'medium';
this.layoutMode = config.layout || 'normal'; this.layoutMode = config.layout || 'normal';
// Load AI provider and model info // Load AI provider and model info
this.aiProvider = prefs.aiProvider || 'gemini'; this.aiProvider = prefs.aiProvider || 'gemini';
this.modelInfo = { this.modelInfo = {
model: openaiSdkCreds.model || 'gpt-4o', model: openaiSdkCreds.model || 'gpt-4o',
visionModel: openaiSdkCreds.visionModel || 'gpt-4o', visionModel: openaiSdkCreds.visionModel || 'gpt-4o',
whisperModel: openaiSdkCreds.whisperModel || 'whisper-1' whisperModel: openaiSdkCreds.whisperModel || 'whisper-1',
}; };
this._storageLoaded = true; this._storageLoaded = true;
@@ -186,18 +192,20 @@ export class CheatingDaddyApp extends LitElement {
hexToRgb(hex) { hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? { return result
r: parseInt(result[1], 16), ? {
g: parseInt(result[2], 16), r: parseInt(result[1], 16),
b: parseInt(result[3], 16) g: parseInt(result[2], 16),
} : { r: 30, g: 30, b: 30 }; b: parseInt(result[3], 16),
}
: { r: 30, g: 30, b: 30 };
} }
lightenColor(rgb, amount) { lightenColor(rgb, amount) {
return { return {
r: Math.min(255, rgb.r + amount), r: Math.min(255, rgb.r + amount),
g: Math.min(255, rgb.g + amount), g: Math.min(255, rgb.g + amount),
b: Math.min(255, rgb.b + amount) b: Math.min(255, rgb.b + amount),
}; };
} }
@@ -313,7 +321,7 @@ export class CheatingDaddyApp extends LitElement {
this.requestUpdate(); this.requestUpdate();
} }
async handleClose() { async handleClose() {
if (this.currentView === 'customize' || this.currentView === 'help' || this.currentView === 'history') { if (this.currentView === 'customize' || this.currentView === 'help' || this.currentView === 'history') {
this.currentView = 'main'; this.currentView = 'main';
} else if (this.currentView === 'assistant') { } else if (this.currentView === 'assistant') {
@@ -520,11 +528,11 @@ export class CheatingDaddyApp extends LitElement {
render() { render() {
const viewClassMap = { const viewClassMap = {
'assistant': 'assistant-view', assistant: 'assistant-view',
'onboarding': 'onboarding-view', onboarding: 'onboarding-view',
'customize': 'settings-view', customize: 'settings-view',
'help': 'help-view', help: 'help-view',
'history': 'history-view', history: 'history-view',
}; };
const mainContentClass = `main-content ${viewClassMap[this.currentView] || 'with-border'}`; const mainContentClass = `main-content ${viewClassMap[this.currentView] || 'with-border'}`;
@@ -549,6 +557,16 @@ export class CheatingDaddyApp extends LitElement {
<div class="view-container">${this.renderCurrentView()}</div> <div class="view-container">${this.renderCurrentView()}</div>
</div> </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> </div>
`; `;
} }
@@ -579,6 +597,44 @@ export class CheatingDaddyApp extends LitElement {
this.requestUpdate(); 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('cheating-daddy-app', CheatingDaddyApp); customElements.define('cheating-daddy-app', CheatingDaddyApp);
+54 -26
View File
@@ -9,7 +9,11 @@ export class AssistantView extends LitElement {
} }
* { * {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family:
'Inter',
-apple-system,
BlinkMacSystemFont,
sans-serif;
cursor: default; cursor: default;
} }
@@ -51,12 +55,24 @@ export class AssistantView extends LitElement {
font-weight: 600; font-weight: 600;
} }
.response-container h1 { font-size: 1.6em; } .response-container h1 {
.response-container h2 { font-size: 1.4em; } font-size: 1.6em;
.response-container h3 { font-size: 1.2em; } }
.response-container h4 { font-size: 1.1em; } .response-container h2 {
.response-container h5 { font-size: 1em; } font-size: 1.4em;
.response-container h6 { font-size: 0.9em; } }
.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 { .response-container p {
margin: 0.6em 0; margin: 0.6em 0;
@@ -268,9 +284,11 @@ export class AssistantView extends LitElement {
white-space: nowrap; white-space: nowrap;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.15s ease, visibility 0.15s ease; transition:
opacity 0.15s ease,
visibility 0.15s ease;
pointer-events: none; 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; z-index: 100;
} }
@@ -310,7 +328,7 @@ export class AssistantView extends LitElement {
.tooltip-note { .tooltip-note {
margin-top: 6px; margin-top: 6px;
padding-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; opacity: 0.5;
font-size: 10px; font-size: 10px;
} }
@@ -655,30 +673,40 @@ export class AssistantView extends LitElement {
<div class="capture-buttons"> <div class="capture-buttons">
<button class="region-select-btn" @click=${this.handleRegionSelect} title="Select region to analyze (like Win+Shift+S)"> <button class="region-select-btn" @click=${this.handleRegionSelect} title="Select region to analyze (like Win+Shift+S)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path 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> </svg>
<span>Select region</span> <span>Select region</span>
</button> </button>
<div class="screen-answer-btn-wrapper"> <div class="screen-answer-btn-wrapper">
${this.aiProvider === 'gemini' ? html` ${this.aiProvider === 'gemini'
<div class="tooltip"> ? html`
<div class="tooltip-row"> <div class="tooltip">
<span class="tooltip-label">Flash</span> <div class="tooltip-row">
<span class="tooltip-value">${this.flashCount}/20</span> <span class="tooltip-label">Flash</span>
</div> <span class="tooltip-value">${this.flashCount}/20</span>
<div class="tooltip-row"> </div>
<span class="tooltip-label">Flash Lite</span> <div class="tooltip-row">
<span class="tooltip-value">${this.flashLiteCount}/20</span> <span class="tooltip-label">Flash Lite</span>
</div> <span class="tooltip-value">${this.flashLiteCount}/20</span>
<div class="tooltip-note">Resets every 24 hours</div> </div>
</div> <div class="tooltip-note">Resets every 24 hours</div>
` : ''} </div>
`
: ''}
<button class="screen-answer-btn" @click=${this.handleScreenAnswer}> <button class="screen-answer-btn" @click=${this.handleScreenAnswer}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path 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> </svg>
<span>Full screen</span> <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> </button>
</div> </div>
</div> </div>
+258 -192
View File
@@ -4,7 +4,11 @@ import { resizeLayout } from '../../utils/windowResize.js';
export class CustomizeView extends LitElement { export class CustomizeView extends LitElement {
static styles = css` static styles = css`
* { * {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family:
'Inter',
-apple-system,
BlinkMacSystemFont,
sans-serif;
cursor: default; cursor: default;
user-select: none; user-select: none;
} }
@@ -634,31 +638,87 @@ export class CustomizeView extends LitElement {
renderSidebarIcon(icon) { renderSidebarIcon(icon) {
const icons = { 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"> user: html`<svg
<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> 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> <circle cx="12" cy="7" r="4"></circle>
</svg>`, </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="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> <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<line x1="12" y1="19" x2="12" y2="23"></line> <line x1="12" y1="19" x2="12" y2="23"></line>
<line x1="8" y1="23" x2="16" y2="23"></line> <line x1="8" y1="23" x2="16" y2="23"></line>
</svg>`, </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> <circle cx="12" cy="12" r="10"></circle>
<line x1="2" y1="12" x2="22" y2="12"></line> <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> <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>`, </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> <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="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line> <line x1="12" y1="17" x2="12" y2="21"></line>
</svg>`, </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> <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> <circle cx="12" cy="13" r="4"></circle>
</svg>`, </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> <rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect>
<path d="M6 8h.001"></path> <path d="M6 8h.001"></path>
<path d="M10 8h.001"></path> <path d="M10 8h.001"></path>
@@ -669,11 +729,29 @@ export class CustomizeView extends LitElement {
<path d="M16 12h.001"></path> <path d="M16 12h.001"></path>
<path d="M7 16h10"></path> <path d="M7 16h10"></path>
</svg>`, </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> <circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line> <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>`, </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="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
<rect x="9" y="9" width="6" height="6"></rect> <rect x="9" y="9" width="6" height="6"></rect>
<line x1="9" y1="1" x2="9" y2="4"></line> <line x1="9" y1="1" x2="9" y2="4"></line>
@@ -685,7 +763,16 @@ export class CustomizeView extends LitElement {
<line x1="1" y1="9" x2="4" y2="9"></line> <line x1="1" y1="9" x2="4" y2="9"></line>
<line x1="1" y1="14" x2="4" y2="14"></line> <line x1="1" y1="14" x2="4" y2="14"></line>
</svg>`, </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> <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="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line> <line x1="12" y1="17" x2="12.01" y2="17"></line>
@@ -701,7 +788,7 @@ export class CustomizeView extends LitElement {
cheatingDaddy.storage.getKeybinds(), cheatingDaddy.storage.getKeybinds(),
cheatingDaddy.storage.getCredentials(), cheatingDaddy.storage.getCredentials(),
cheatingDaddy.storage.getOpenAICredentials(), cheatingDaddy.storage.getOpenAICredentials(),
cheatingDaddy.storage.getOpenAISDKCredentials() cheatingDaddy.storage.getOpenAISDKCredentials(),
]); ]);
this.googleSearchEnabled = prefs.googleSearchEnabled ?? true; this.googleSearchEnabled = prefs.googleSearchEnabled ?? true;
@@ -711,15 +798,15 @@ export class CustomizeView extends LitElement {
this.customPrompt = prefs.customPrompt ?? ''; this.customPrompt = prefs.customPrompt ?? '';
this.theme = prefs.theme ?? 'dark'; this.theme = prefs.theme ?? 'dark';
this.aiProvider = prefs.aiProvider ?? 'gemini'; this.aiProvider = prefs.aiProvider ?? 'gemini';
// Load Gemini API key // Load Gemini API key
this.geminiApiKey = credentials.apiKey ?? ''; this.geminiApiKey = credentials.apiKey ?? '';
// Load OpenAI Realtime credentials // Load OpenAI Realtime credentials
this.openaiApiKey = openaiCreds.apiKey ?? ''; this.openaiApiKey = openaiCreds.apiKey ?? '';
this.openaiBaseUrl = openaiCreds.baseUrl ?? ''; this.openaiBaseUrl = openaiCreds.baseUrl ?? '';
this.openaiModel = openaiCreds.model ?? 'gpt-4o-realtime-preview-2024-12-17'; this.openaiModel = openaiCreds.model ?? 'gpt-4o-realtime-preview-2024-12-17';
// Load OpenAI SDK credentials // Load OpenAI SDK credentials
this.openaiSdkApiKey = openaiSdkCreds.apiKey ?? ''; this.openaiSdkApiKey = openaiSdkCreds.apiKey ?? '';
this.openaiSdkBaseUrl = openaiSdkCreds.baseUrl ?? ''; this.openaiSdkBaseUrl = openaiSdkCreds.baseUrl ?? '';
@@ -1072,21 +1159,21 @@ export class CustomizeView extends LitElement {
async handleOpenAIApiKeyInput(e) { async handleOpenAIApiKeyInput(e) {
this.openaiApiKey = e.target.value; this.openaiApiKey = e.target.value;
await cheatingDaddy.storage.setOpenAICredentials({ await cheatingDaddy.storage.setOpenAICredentials({
apiKey: e.target.value apiKey: e.target.value,
}); });
} }
async handleOpenAIBaseUrlInput(e) { async handleOpenAIBaseUrlInput(e) {
this.openaiBaseUrl = e.target.value; this.openaiBaseUrl = e.target.value;
await cheatingDaddy.storage.setOpenAICredentials({ await cheatingDaddy.storage.setOpenAICredentials({
baseUrl: e.target.value baseUrl: e.target.value,
}); });
} }
async handleOpenAIModelInput(e) { async handleOpenAIModelInput(e) {
this.openaiModel = e.target.value; this.openaiModel = e.target.value;
await cheatingDaddy.storage.setOpenAICredentials({ await cheatingDaddy.storage.setOpenAICredentials({
model: e.target.value model: e.target.value,
}); });
} }
@@ -1094,35 +1181,35 @@ export class CustomizeView extends LitElement {
async handleOpenAISdkApiKeyInput(e) { async handleOpenAISdkApiKeyInput(e) {
this.openaiSdkApiKey = e.target.value; this.openaiSdkApiKey = e.target.value;
await cheatingDaddy.storage.setOpenAISDKCredentials({ await cheatingDaddy.storage.setOpenAISDKCredentials({
apiKey: e.target.value apiKey: e.target.value,
}); });
} }
async handleOpenAISdkBaseUrlInput(e) { async handleOpenAISdkBaseUrlInput(e) {
this.openaiSdkBaseUrl = e.target.value; this.openaiSdkBaseUrl = e.target.value;
await cheatingDaddy.storage.setOpenAISDKCredentials({ await cheatingDaddy.storage.setOpenAISDKCredentials({
baseUrl: e.target.value baseUrl: e.target.value,
}); });
} }
async handleOpenAISdkModelInput(e) { async handleOpenAISdkModelInput(e) {
this.openaiSdkModel = e.target.value; this.openaiSdkModel = e.target.value;
await cheatingDaddy.storage.setOpenAISDKCredentials({ await cheatingDaddy.storage.setOpenAISDKCredentials({
model: e.target.value model: e.target.value,
}); });
} }
async handleOpenAISdkVisionModelInput(e) { async handleOpenAISdkVisionModelInput(e) {
this.openaiSdkVisionModel = e.target.value; this.openaiSdkVisionModel = e.target.value;
await cheatingDaddy.storage.setOpenAISDKCredentials({ await cheatingDaddy.storage.setOpenAISDKCredentials({
visionModel: e.target.value visionModel: e.target.value,
}); });
} }
async handleOpenAISdkWhisperModelInput(e) { async handleOpenAISdkWhisperModelInput(e) {
this.openaiSdkWhisperModel = e.target.value; this.openaiSdkWhisperModel = e.target.value;
await cheatingDaddy.storage.setOpenAISDKCredentials({ await cheatingDaddy.storage.setOpenAISDKCredentials({
whisperModel: e.target.value whisperModel: e.target.value,
}); });
} }
@@ -1209,9 +1296,7 @@ export class CustomizeView extends LitElement {
<select class="form-control" .value=${this.selectedProfile} @change=${this.handleProfileSelect}> <select class="form-control" .value=${this.selectedProfile} @change=${this.handleProfileSelect}>
${profiles.map( ${profiles.map(
profile => html` profile => html`
<option value=${profile.value} ?selected=${this.selectedProfile === profile.value}> <option value=${profile.value} ?selected=${this.selectedProfile === profile.value}>${profile.name}</option>
${profile.name}
</option>
` `
)} )}
</select> </select>
@@ -1221,15 +1306,12 @@ export class CustomizeView extends LitElement {
<label class="form-label">Custom AI Instructions</label> <label class="form-label">Custom AI Instructions</label>
<textarea <textarea
class="form-control" class="form-control"
placeholder="Add specific instructions for how you want the AI to behave during ${ placeholder="Add specific instructions for how you want the AI to behave during ${profileNames[this.selectedProfile] ||
profileNames[this.selectedProfile] || 'this interaction' 'this interaction'}..."
}..."
.value=${this.customPrompt} .value=${this.customPrompt}
@input=${this.handleCustomPromptInput} @input=${this.handleCustomPromptInput}
></textarea> ></textarea>
<div class="form-description"> <div class="form-description">Personalize the AI's behavior with specific instructions</div>
Personalize the AI's behavior with specific instructions
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1247,9 +1329,7 @@ export class CustomizeView extends LitElement {
<option value="mic_only">Microphone Only (Me)</option> <option value="mic_only">Microphone Only (Me)</option>
<option value="both">Both Speaker & Microphone</option> <option value="both">Both Speaker & Microphone</option>
</select> </select>
<div class="form-description"> <div class="form-description">Choose which audio sources to capture for the AI.</div>
Choose which audio sources to capture for the AI.
</div>
</div> </div>
</div> </div>
`; `;
@@ -1270,9 +1350,7 @@ export class CustomizeView extends LitElement {
<select class="form-control" .value=${this.selectedLanguage} @change=${this.handleLanguageSelect}> <select class="form-control" .value=${this.selectedLanguage} @change=${this.handleLanguageSelect}>
${languages.map( ${languages.map(
language => html` language => html`
<option value=${language.value} ?selected=${this.selectedLanguage === language.value}> <option value=${language.value} ?selected=${this.selectedLanguage === language.value}>${language.name}</option>
${language.name}
</option>
` `
)} )}
</select> </select>
@@ -1295,17 +1373,9 @@ export class CustomizeView extends LitElement {
<span class="current-selection">${currentTheme?.name || 'Dark'}</span> <span class="current-selection">${currentTheme?.name || 'Dark'}</span>
</label> </label>
<select class="form-control" .value=${this.theme} @change=${this.handleThemeChange}> <select class="form-control" .value=${this.theme} @change=${this.handleThemeChange}>
${themes.map( ${themes.map(theme => html` <option value=${theme.value} ?selected=${this.theme === theme.value}>${theme.name}</option> `)}
theme => html`
<option value=${theme.value} ?selected=${this.theme === theme.value}>
${theme.name}
</option>
`
)}
</select> </select>
<div class="form-description"> <div class="form-description">Choose a color theme for the interface</div>
Choose a color theme for the interface
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -1318,10 +1388,7 @@ export class CustomizeView extends LitElement {
<option value="compact" ?selected=${this.layoutMode === 'compact'}>Compact</option> <option value="compact" ?selected=${this.layoutMode === 'compact'}>Compact</option>
</select> </select>
<div class="form-description"> <div class="form-description">
${this.layoutMode === 'compact' ${this.layoutMode === 'compact' ? 'Smaller window with reduced padding' : 'Standard layout with comfortable spacing'}
? 'Smaller window with reduced padding'
: 'Standard layout with comfortable spacing'
}
</div> </div>
</div> </div>
@@ -1379,7 +1446,9 @@ export class CustomizeView extends LitElement {
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
Image Quality 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> </label>
<select class="form-control" .value=${this.selectedImageQuality} @change=${this.handleImageQualitySelect}> <select class="form-control" .value=${this.selectedImageQuality} @change=${this.handleImageQualitySelect}>
<option value="high" ?selected=${this.selectedImageQuality === 'high'}>High Quality</option> <option value="high" ?selected=${this.selectedImageQuality === 'high'}>High Quality</option>
@@ -1391,8 +1460,7 @@ export class CustomizeView extends LitElement {
? 'Best quality, uses more tokens' ? 'Best quality, uses more tokens'
: this.selectedImageQuality === 'medium' : this.selectedImageQuality === 'medium'
? 'Balanced quality and token usage' ? 'Balanced quality and token usage'
: 'Lower quality, uses fewer tokens' : 'Lower quality, uses fewer tokens'}
}
</div> </div>
</div> </div>
</div> </div>
@@ -1446,9 +1514,9 @@ export class CustomizeView extends LitElement {
renderAIProviderSection() { renderAIProviderSection() {
const providerNames = { const providerNames = {
'gemini': 'Google Gemini', gemini: 'Google Gemini',
'openai-realtime': 'OpenAI Realtime', 'openai-realtime': 'OpenAI Realtime',
'openai-sdk': 'OpenAI SDK (BotHub, etc.)' 'openai-sdk': 'OpenAI SDK (BotHub, etc.)',
}; };
return html` return html`
@@ -1464,135 +1532,136 @@ export class CustomizeView extends LitElement {
<option value="openai-realtime" ?selected=${this.aiProvider === 'openai-realtime'}>OpenAI Realtime API</option> <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> <option value="openai-sdk" ?selected=${this.aiProvider === 'openai-sdk'}>OpenAI SDK (BotHub, Azure, etc.)</option>
</select> </select>
<div class="form-description"> <div class="form-description">Choose which AI provider to use for conversations and screen analysis</div>
Choose which AI provider to use for conversations and screen analysis
</div>
</div> </div>
${this.aiProvider === 'gemini' ? html` ${this.aiProvider === 'gemini'
<div class="form-group full-width"> ? html`
<label class="form-label">Gemini API Key</label> <div class="form-group full-width">
<input <label class="form-label">Gemini API Key</label>
type="password" <input
class="form-control" type="password"
placeholder="Enter your Gemini API key" class="form-control"
.value=${this.geminiApiKey} placeholder="Enter your Gemini API key"
@input=${this.handleGeminiApiKeyInput} .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 class="form-description">
</div> Get your API key from
</div> <a href="https://aistudio.google.com/app/apikey" target="_blank" style="color: var(--text-color);"
` : this.aiProvider === 'openai-realtime' ? html` >Google AI Studio</a
<div class="form-group full-width"> >
<label class="form-label">OpenAI API Key</label> </div>
<input </div>
type="password" `
class="form-control" : this.aiProvider === 'openai-realtime'
placeholder="Enter your OpenAI API key" ? html`
.value=${this.openaiApiKey} <div class="form-group full-width">
@input=${this.handleOpenAIApiKeyInput} <label class="form-label">OpenAI API Key</label>
/> <input
<div class="form-description"> type="password"
Get your API key from <a href="https://platform.openai.com/api-keys" target="_blank" style="color: var(--text-color);">OpenAI Platform</a> class="form-control"
</div> placeholder="Enter your OpenAI API key"
</div> .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"> <div class="form-group full-width">
<label class="form-label">Base URL (Optional)</label> <label class="form-label">Base URL (Optional)</label>
<input <input
type="text" type="text"
class="form-control" class="form-control"
placeholder="wss://api.openai.com/v1/realtime (leave empty for default)" placeholder="wss://api.openai.com/v1/realtime (leave empty for default)"
.value=${this.openaiBaseUrl} .value=${this.openaiBaseUrl}
@input=${this.handleOpenAIBaseUrlInput} @input=${this.handleOpenAIBaseUrlInput}
/> />
<div class="form-description"> <div class="form-description">Override the base URL for OpenAI-compatible APIs</div>
Override the base URL for OpenAI-compatible APIs </div>
</div>
</div>
<div class="form-group full-width"> <div class="form-group full-width">
<label class="form-label">Model</label> <label class="form-label">Model</label>
<input <input
type="text" type="text"
class="form-control" class="form-control"
placeholder="gpt-4o-realtime-preview-2024-12-17" placeholder="gpt-4o-realtime-preview-2024-12-17"
.value=${this.openaiModel} .value=${this.openaiModel}
@input=${this.handleOpenAIModelInput} @input=${this.handleOpenAIModelInput}
/> />
<div class="form-description"> <div class="form-description">Realtime API model to use</div>
Realtime API model to use </div>
</div> `
</div> : html`
` : html` <div class="form-group full-width">
<div class="form-group full-width"> <label class="form-label">API Key</label>
<label class="form-label">API Key</label> <input
<input type="password"
type="password" class="form-control"
class="form-control" placeholder="Enter your API key"
placeholder="Enter your API key" .value=${this.openaiSdkApiKey}
.value=${this.openaiSdkApiKey} @input=${this.handleOpenAISdkApiKeyInput}
@input=${this.handleOpenAISdkApiKeyInput} />
/> <div class="form-description">API key for your provider (BotHub, Azure, OpenRouter, etc.)</div>
<div class="form-description"> </div>
API key for your provider (BotHub, Azure, OpenRouter, etc.)
</div>
</div>
<div class="form-group full-width"> <div class="form-group full-width">
<label class="form-label">Base URL</label> <label class="form-label">Base URL</label>
<input <input
type="text" type="text"
class="form-control" class="form-control"
placeholder="https://bothub.chat/api/v2/openai/v1" placeholder="https://bothub.chat/api/v2/openai/v1"
.value=${this.openaiSdkBaseUrl} .value=${this.openaiSdkBaseUrl}
@input=${this.handleOpenAISdkBaseUrlInput} @input=${this.handleOpenAISdkBaseUrlInput}
/> />
<div class="form-description"> <div class="form-description">API endpoint URL (e.g., https://bothub.chat/api/v2/openai/v1)</div>
API endpoint URL (e.g., https://bothub.chat/api/v2/openai/v1) </div>
</div>
</div>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label class="form-label">Chat Model</label> <label class="form-label">Chat Model</label>
<input <input
type="text" type="text"
class="form-control" class="form-control"
placeholder="gpt-4o" placeholder="gpt-4o"
.value=${this.openaiSdkModel} .value=${this.openaiSdkModel}
@input=${this.handleOpenAISdkModelInput} @input=${this.handleOpenAISdkModelInput}
/> />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Vision Model</label> <label class="form-label">Vision Model</label>
<input <input
type="text" type="text"
class="form-control" class="form-control"
placeholder="gpt-4o" placeholder="gpt-4o"
.value=${this.openaiSdkVisionModel} .value=${this.openaiSdkVisionModel}
@input=${this.handleOpenAISdkVisionModelInput} @input=${this.handleOpenAISdkVisionModelInput}
/> />
</div> </div>
</div> </div>
<div class="form-group full-width"> <div class="form-group full-width">
<label class="form-label">Whisper Model (Transcription)</label> <label class="form-label">Whisper Model (Transcription)</label>
<input <input
type="text" type="text"
class="form-control" class="form-control"
placeholder="whisper-1" placeholder="whisper-1"
.value=${this.openaiSdkWhisperModel} .value=${this.openaiSdkWhisperModel}
@input=${this.handleOpenAISdkWhisperModelInput} @input=${this.handleOpenAISdkWhisperModelInput}
/> />
<div class="form-description"> <div class="form-description">Model for audio transcription</div>
Model for audio transcription </div>
</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. <strong>Note:</strong> You must restart the AI session for provider changes to take effect.
</div> </div>
</div> </div>
@@ -1628,20 +1697,19 @@ export class CustomizeView extends LitElement {
<div class="form-group"> <div class="form-group">
<label class="form-label" style="color: var(--error-color);">Data Management</label> <label class="form-label" style="color: var(--error-color);">Data Management</label>
<div class="form-description" style="margin-bottom: 12px;"> <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> </div>
<button <button class="danger-button" @click=${this.clearLocalData} ?disabled=${this.isClearing}>
class="danger-button"
@click=${this.clearLocalData}
?disabled=${this.isClearing}
>
${this.isClearing ? 'Clearing...' : 'Clear All Local Data'} ${this.isClearing ? 'Clearing...' : 'Clear All Local Data'}
</button> </button>
${this.clearStatusMessage ? html` ${this.clearStatusMessage
<div class="status-message ${this.clearStatusType === 'success' ? 'status-success' : 'status-error'}"> ? html`
${this.clearStatusMessage} <div class="status-message ${this.clearStatusType === 'success' ? 'status-success' : 'status-error'}">
</div> ${this.clearStatusMessage}
` : ''} </div>
`
: ''}
</div> </div>
</div> </div>
`; `;
@@ -1690,9 +1758,7 @@ export class CustomizeView extends LitElement {
` `
)} )}
</nav> </nav>
<div class="settings-content"> <div class="settings-content">${this.renderSectionContent()}</div>
${this.renderSectionContent()}
</div>
</div> </div>
`; `;
} }
+92 -14
View File
@@ -4,7 +4,11 @@ import { resizeLayout } from '../../utils/windowResize.js';
export class HelpView extends LitElement { export class HelpView extends LitElement {
static styles = css` static styles = css`
* { * {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family:
'Inter',
-apple-system,
BlinkMacSystemFont,
sans-serif;
cursor: default; cursor: default;
user-select: none; user-select: none;
} }
@@ -170,6 +174,25 @@ export class HelpView extends LitElement {
flex-shrink: 0; 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 { .usage-steps {
counter-reset: step-counter; counter-reset: step-counter;
} }
@@ -273,26 +296,61 @@ export class HelpView extends LitElement {
</div> </div>
<div class="community-links"> <div class="community-links">
<div class="community-link" @click=${() => this.handleExternalLinkClick('https://cheatingdaddy.com')}> <div class="community-link" @click=${() => this.handleExternalLinkClick('https://cheatingdaddy.com')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg
<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> viewBox="0 0 24 24"
<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> 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> </svg>
Website Website
</div> </div>
<div class="community-link" @click=${() => this.handleExternalLinkClick('https://github.com/sohzm/cheating-daddy')}> <div class="community-link" @click=${() => this.handleExternalLinkClick('https://github.com/sohzm/cheating-daddy')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg
<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> 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> <path d="M9 20.0267C6 20.9999 3.5 20.0267 2 17.0267"></path>
</svg> </svg>
GitHub GitHub
</div> </div>
<div class="community-link" @click=${() => this.handleExternalLinkClick('https://discord.gg/GCBdubnXfJ')}> <div class="community-link" @click=${() => this.handleExternalLinkClick('https://discord.gg/GCBdubnXfJ')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <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="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
<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> 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 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>
<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="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> </svg>
Discord Discord
</div> </div>
@@ -376,9 +434,7 @@ export class HelpView extends LitElement {
</div> </div>
</div> </div>
</div> </div>
<div class="description" style="margin-top: 12px; text-align: center;"> <div class="description" style="margin-top: 12px; text-align: center;">You can customize these shortcuts in Settings.</div>
You can customize these shortcuts in Settings.
</div>
</div> </div>
<div class="option-group"> <div class="option-group">
@@ -442,9 +498,31 @@ export class HelpView extends LitElement {
</div> </div>
<div class="description">The AI listens to conversations and provides contextual assistance based on what it hears.</div> <div class="description">The AI listens to conversations and provides contextual assistance based on what it hears.</div>
</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> </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); customElements.define('help-view', HelpView);
+35 -35
View File
@@ -4,7 +4,11 @@ import { resizeLayout } from '../../utils/windowResize.js';
export class HistoryView extends LitElement { export class HistoryView extends LitElement {
static styles = css` static styles = css`
* { * {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family:
'Inter',
-apple-system,
BlinkMacSystemFont,
sans-serif;
cursor: default; cursor: default;
user-select: none; user-select: none;
} }
@@ -505,18 +509,22 @@ export class HistoryView extends LitElement {
return html` return html`
<div class="session-context"> <div class="session-context">
${profile ? html` ${profile
<div class="session-context-row"> ? html`
<span class="context-label">Profile:</span> <div class="session-context-row">
<span class="context-value">${profileNames[profile] || profile}</span> <span class="context-label">Profile:</span>
</div> <span class="context-value">${profileNames[profile] || profile}</span>
` : ''} </div>
${customPrompt ? html` `
<div class="session-context-row"> : ''}
<span class="context-label">Custom Prompt:</span> ${customPrompt
<span class="custom-prompt-value">${customPrompt}</span> ? html`
</div> <div class="session-context-row">
` : ''} <span class="context-label">Custom Prompt:</span>
<span class="custom-prompt-value">${customPrompt}</span>
</div>
`
: ''}
</div> </div>
`; `;
} }
@@ -559,9 +567,14 @@ export class HistoryView extends LitElement {
return html`<div class="empty-state">No screen analysis data available</div>`; return html`<div class="empty-state">No screen analysis data available</div>`;
} }
return screenAnalysisHistory.map(analysis => html` return screenAnalysisHistory.map(
<div class="message screen"><div class="analysis-meta">${this.formatTimestamp(analysis.timestamp)} ${analysis.model || 'unknown model'}</div>${analysis.response}</div> analysis => html`
`); <div class="message screen">
<div class="analysis-meta">${this.formatTimestamp(analysis.timestamp)} ${analysis.model || 'unknown model'}</div>
${analysis.response}
</div>
`
);
} }
renderConversationView() { renderConversationView() {
@@ -604,22 +617,13 @@ export class HistoryView extends LitElement {
</div> </div>
</div> </div>
<div class="view-tabs"> <div class="view-tabs">
<button <button class="view-tab ${this.activeTab === 'conversation' ? 'active' : ''}" @click=${() => this.handleTabClick('conversation')}>
class="view-tab ${this.activeTab === 'conversation' ? 'active' : ''}"
@click=${() => this.handleTabClick('conversation')}
>
Conversation ${hasConversation ? `(${conversationHistory.length})` : ''} Conversation ${hasConversation ? `(${conversationHistory.length})` : ''}
</button> </button>
<button <button class="view-tab ${this.activeTab === 'screen' ? 'active' : ''}" @click=${() => this.handleTabClick('screen')}>
class="view-tab ${this.activeTab === 'screen' ? 'active' : ''}"
@click=${() => this.handleTabClick('screen')}
>
Screen ${hasScreenAnalysis ? `(${screenAnalysisHistory.length})` : ''} Screen ${hasScreenAnalysis ? `(${screenAnalysisHistory.length})` : ''}
</button> </button>
<button <button class="view-tab ${this.activeTab === 'context' ? 'active' : ''}" @click=${() => this.handleTabClick('context')}>
class="view-tab ${this.activeTab === 'context' ? 'active' : ''}"
@click=${() => this.handleTabClick('context')}
>
Context ${hasContext ? '' : '(empty)'} Context ${hasContext ? '' : '(empty)'}
</button> </button>
</div> </div>
@@ -627,8 +631,8 @@ export class HistoryView extends LitElement {
${this.activeTab === 'conversation' ${this.activeTab === 'conversation'
? this.renderConversationContent() ? this.renderConversationContent()
: this.activeTab === 'screen' : this.activeTab === 'screen'
? this.renderScreenAnalysisContent() ? this.renderScreenAnalysisContent()
: this.renderContextContent()} : this.renderContextContent()}
</div> </div>
`; `;
} }
@@ -638,11 +642,7 @@ export class HistoryView extends LitElement {
return html`<div class="history-container">${this.renderConversationView()}</div>`; return html`<div class="history-container">${this.renderConversationView()}</div>`;
} }
return html` return html` <div class="history-container">${this.renderSessionsList()}</div> `;
<div class="history-container">
${this.renderSessionsList()}
</div>
`;
} }
} }
+7 -2
View File
@@ -4,7 +4,11 @@ import { resizeLayout } from '../../utils/windowResize.js';
export class MainView extends LitElement { export class MainView extends LitElement {
static styles = css` static styles = css`
* { * {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; font-family:
'Inter',
-apple-system,
BlinkMacSystemFont,
sans-serif;
cursor: default; cursor: default;
user-select: none; user-select: none;
} }
@@ -54,7 +58,8 @@ export class MainView extends LitElement {
} }
@keyframes blink-red { @keyframes blink-red {
0%, 100% { 0%,
100% {
border-color: var(--border-color); border-color: var(--border-color);
} }
50% { 50% {
+3 -1
View File
@@ -512,7 +512,9 @@ export class OnboardingView extends LitElement {
<div class="onboarding-container"> <div class="onboarding-container">
<button class="close-button" @click=${this.handleClose} title="Close"> <button class="close-button" @click=${this.handleClose} title="Close">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path 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> </svg>
</button> </button>
<canvas class="gradient-canvas"></canvas> <canvas class="gradient-canvas"></canvas>
+175
View File
@@ -0,0 +1,175 @@
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);
+30
View File
@@ -6,6 +6,7 @@ const { app, BrowserWindow, shell, ipcMain } = require('electron');
const { createWindow, updateGlobalShortcuts } = require('./utils/window'); const { createWindow, updateGlobalShortcuts } = require('./utils/window');
const { setupAIProviderIpcHandlers } = require('./utils/ai-provider-manager'); const { setupAIProviderIpcHandlers } = require('./utils/ai-provider-manager');
const { stopMacOSAudioCapture } = require('./utils/gemini'); const { stopMacOSAudioCapture } = require('./utils/gemini');
const { initLogger, closeLogger, getLogPath } = require('./utils/logger');
const storage = require('./storage'); const storage = require('./storage');
const geminiSessionRef = { current: null }; const geminiSessionRef = { current: null };
@@ -24,6 +25,10 @@ function createMainWindow() {
} }
app.whenReady().then(async () => { 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) // Initialize storage (checks version, resets if needed)
storage.initializeStorage(); storage.initializeStorage();
@@ -31,10 +36,22 @@ app.whenReady().then(async () => {
setupAIProviderIpcHandlers(geminiSessionRef); setupAIProviderIpcHandlers(geminiSessionRef);
setupStorageIpcHandlers(); setupStorageIpcHandlers();
setupGeneralIpcHandlers(); 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', () => { app.on('window-all-closed', () => {
stopMacOSAudioCapture(); stopMacOSAudioCapture();
closeLogger();
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
app.quit(); app.quit();
} }
@@ -42,6 +59,7 @@ app.on('window-all-closed', () => {
app.on('before-quit', () => { app.on('before-quit', () => {
stopMacOSAudioCapture(); stopMacOSAudioCapture();
closeLogger();
}); });
app.on('activate', () => { app.on('activate', () => {
@@ -284,6 +302,18 @@ function setupGeneralIpcHandlers() {
return app.getVersion(); 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 => { ipcMain.handle('quit-application', async event => {
try { try {
stopMacOSAudioCapture(); stopMacOSAudioCapture();
+30 -27
View File
@@ -8,7 +8,7 @@ const CONFIG_VERSION = 1;
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
configVersion: CONFIG_VERSION, configVersion: CONFIG_VERSION,
onboarded: false, onboarded: false,
layout: 'normal' layout: 'normal',
}; };
const DEFAULT_CREDENTIALS = { const DEFAULT_CREDENTIALS = {
@@ -22,7 +22,7 @@ const DEFAULT_CREDENTIALS = {
openaiSdkBaseUrl: '', openaiSdkBaseUrl: '',
openaiSdkModel: 'gpt-4o', openaiSdkModel: 'gpt-4o',
openaiSdkVisionModel: 'gpt-4o', openaiSdkVisionModel: 'gpt-4o',
openaiSdkWhisperModel: 'whisper-1' openaiSdkWhisperModel: 'whisper-1',
}; };
const DEFAULT_PREFERENCES = { const DEFAULT_PREFERENCES = {
@@ -36,13 +36,13 @@ const DEFAULT_PREFERENCES = {
fontSize: 'medium', fontSize: 'medium',
backgroundTransparency: 0.8, backgroundTransparency: 0.8,
googleSearchEnabled: false, googleSearchEnabled: false,
aiProvider: 'gemini' aiProvider: 'gemini',
}; };
const DEFAULT_KEYBINDS = null; // null means use system defaults const DEFAULT_KEYBINDS = null; // null means use system defaults
const DEFAULT_LIMITS = { const DEFAULT_LIMITS = {
data: [] // Array of { date: 'YYYY-MM-DD', flash: { count: 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 // Get the config directory path based on OS
@@ -208,7 +208,7 @@ function getOpenAICredentials() {
return { return {
apiKey: creds.openaiApiKey || '', apiKey: creds.openaiApiKey || '',
baseUrl: creds.openaiBaseUrl || '', baseUrl: creds.openaiBaseUrl || '',
model: creds.openaiModel || 'gpt-4o-realtime-preview-2024-12-17' model: creds.openaiModel || 'gpt-4o-realtime-preview-2024-12-17',
}; };
} }
@@ -227,7 +227,7 @@ function getOpenAISDKCredentials() {
baseUrl: creds.openaiSdkBaseUrl || '', baseUrl: creds.openaiSdkBaseUrl || '',
model: creds.openaiSdkModel || 'gpt-4o', model: creds.openaiSdkModel || 'gpt-4o',
visionModel: creds.openaiSdkVisionModel || 'gpt-4o', visionModel: creds.openaiSdkVisionModel || 'gpt-4o',
whisperModel: creds.openaiSdkWhisperModel || 'whisper-1' whisperModel: creds.openaiSdkWhisperModel || 'whisper-1',
}; };
} }
@@ -301,7 +301,7 @@ function getTodayLimits() {
const newEntry = { const newEntry = {
date: today, date: today,
flash: { count: 0 }, flash: { count: 0 },
flashLite: { count: 0 } flashLite: { count: 0 },
}; };
limits.data.push(newEntry); limits.data.push(newEntry);
setLimits(limits); setLimits(limits);
@@ -322,7 +322,7 @@ function incrementLimitCount(model) {
todayEntry = { todayEntry = {
date: today, date: today,
flash: { count: 0 }, flash: { count: 0 },
flashLite: { count: 0 } flashLite: { count: 0 },
}; };
limits.data.push(todayEntry); limits.data.push(todayEntry);
} else { } else {
@@ -376,7 +376,7 @@ function saveSession(sessionId, data) {
customPrompt: data.customPrompt || existingSession?.customPrompt || null, customPrompt: data.customPrompt || existingSession?.customPrompt || null,
// Conversation data // Conversation data
conversationHistory: data.conversationHistory || existingSession?.conversationHistory || [], conversationHistory: data.conversationHistory || existingSession?.conversationHistory || [],
screenAnalysisHistory: data.screenAnalysisHistory || existingSession?.screenAnalysisHistory || [] screenAnalysisHistory: data.screenAnalysisHistory || existingSession?.screenAnalysisHistory || [],
}; };
return writeJsonFile(sessionPath, sessionData); return writeJsonFile(sessionPath, sessionData);
} }
@@ -393,7 +393,8 @@ function getAllSessions() {
return []; return [];
} }
const files = fs.readdirSync(historyDir) const files = fs
.readdirSync(historyDir)
.filter(f => f.endsWith('.json')) .filter(f => f.endsWith('.json'))
.sort((a, b) => { .sort((a, b) => {
// Sort by timestamp descending (newest first) // Sort by timestamp descending (newest first)
@@ -402,22 +403,24 @@ function getAllSessions() {
return tsB - tsA; return tsB - tsA;
}); });
return files.map(file => { return files
const sessionId = file.replace('.json', ''); .map(file => {
const data = readJsonFile(path.join(historyDir, file), null); const sessionId = file.replace('.json', '');
if (data) { const data = readJsonFile(path.join(historyDir, file), null);
return { if (data) {
sessionId, return {
createdAt: data.createdAt, sessionId,
lastUpdated: data.lastUpdated, createdAt: data.createdAt,
messageCount: data.conversationHistory?.length || 0, lastUpdated: data.lastUpdated,
screenAnalysisCount: data.screenAnalysisHistory?.length || 0, messageCount: data.conversationHistory?.length || 0,
profile: data.profile || null, screenAnalysisCount: data.screenAnalysisHistory?.length || 0,
customPrompt: data.customPrompt || null profile: data.profile || null,
}; customPrompt: data.customPrompt || null,
} };
return null; }
}).filter(Boolean); return null;
})
.filter(Boolean);
} catch (error) { } catch (error) {
console.error('Error reading sessions:', error.message); console.error('Error reading sessions:', error.message);
return []; return [];
@@ -504,5 +507,5 @@ module.exports = {
deleteAllSessions, deleteAllSessions,
// Clear all // Clear all
clearAllData clearAllData,
}; };
+63 -12
View File
@@ -52,9 +52,7 @@ function buildContextMessage() {
if (validTurns.length === 0) return null; if (validTurns.length === 0) return null;
const contextLines = validTurns.map(turn => const contextLines = validTurns.map(turn => `[Interviewer]: ${turn.transcription.trim()}\n[Your answer]: ${turn.ai_response.trim()}`);
`[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.`; return `Session reconnected. Here's the conversation so far:\n\n${contextLines.join('\n\n')}\n\nContinue from here.`;
} }
@@ -74,7 +72,7 @@ function initializeNewSession(profile = null, customPrompt = null) {
sendToRenderer('save-session-context', { sendToRenderer('save-session-context', {
sessionId: currentSessionId, sessionId: currentSessionId,
profile: profile, profile: profile,
customPrompt: customPrompt || '' customPrompt: customPrompt || '',
}); });
} }
} }
@@ -110,7 +108,7 @@ function saveScreenAnalysis(prompt, response, model) {
timestamp: Date.now(), timestamp: Date.now(),
prompt: prompt, prompt: prompt,
response: response.trim(), response: response.trim(),
model: model model: model,
}; };
screenAnalysisHistory.push(analysisEntry); screenAnalysisHistory.push(analysisEntry);
@@ -122,7 +120,7 @@ function saveScreenAnalysis(prompt, response, model) {
analysis: analysisEntry, analysis: analysisEntry,
fullHistory: screenAnalysisHistory, fullHistory: screenAnalysisHistory,
profile: currentProfile, profile: currentProfile,
customPrompt: currentCustomPrompt customPrompt: currentCustomPrompt,
}); });
} }
@@ -418,10 +416,12 @@ async function startMacOSAudioCapture(geminiSessionRef) {
// Kill any existing SystemAudioDump processes first // Kill any existing SystemAudioDump processes first
await killExistingSystemAudioDump(); await killExistingSystemAudioDump();
console.log('Starting macOS audio capture with SystemAudioDump...'); console.log('=== Starting macOS audio capture ===');
sendToRenderer('update-status', 'Starting audio capture...');
const { app } = require('electron'); const { app } = require('electron');
const path = require('path'); const path = require('path');
const fs = require('fs');
let systemAudioPath; let systemAudioPath;
if (app.isPackaged) { if (app.isPackaged) {
@@ -430,7 +430,35 @@ async function startMacOSAudioCapture(geminiSessionRef) {
systemAudioPath = path.join(__dirname, '../assets', 'SystemAudioDump'); systemAudioPath = path.join(__dirname, '../assets', 'SystemAudioDump');
} }
console.log('SystemAudioDump path:', systemAudioPath); 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;
}
}
const spawnOptions = { const spawnOptions = {
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
@@ -439,10 +467,12 @@ async function startMacOSAudioCapture(geminiSessionRef) {
}, },
}; };
console.log('Spawning SystemAudioDump...');
systemAudioProc = spawn(systemAudioPath, [], spawnOptions); systemAudioProc = spawn(systemAudioPath, [], spawnOptions);
if (!systemAudioProc.pid) { if (!systemAudioProc.pid) {
console.error('Failed to start SystemAudioDump'); console.error('FATAL: Failed to start SystemAudioDump - no PID');
sendToRenderer('update-status', 'Error: Audio capture failed to start');
return false; return false;
} }
@@ -455,8 +485,16 @@ async function startMacOSAudioCapture(geminiSessionRef) {
const CHUNK_SIZE = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_DURATION; const CHUNK_SIZE = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_DURATION;
let audioBuffer = Buffer.alloc(0); let audioBuffer = Buffer.alloc(0);
let chunkCount = 0;
let firstDataReceived = false;
systemAudioProc.stdout.on('data', data => { 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]); audioBuffer = Buffer.concat([audioBuffer, data]);
while (audioBuffer.length >= CHUNK_SIZE) { while (audioBuffer.length >= CHUNK_SIZE) {
@@ -467,6 +505,11 @@ async function startMacOSAudioCapture(geminiSessionRef) {
const base64Data = monoChunk.toString('base64'); const base64Data = monoChunk.toString('base64');
sendAudioToGemini(base64Data, geminiSessionRef); sendAudioToGemini(base64Data, geminiSessionRef);
chunkCount++;
if (chunkCount % 100 === 0) {
console.log(`Audio: ${chunkCount} chunks processed`);
}
if (process.env.DEBUG_AUDIO) { if (process.env.DEBUG_AUDIO) {
console.log(`Processed audio chunk: ${chunk.length} bytes`); console.log(`Processed audio chunk: ${chunk.length} bytes`);
saveDebugAudio(monoChunk, 'system_audio'); saveDebugAudio(monoChunk, 'system_audio');
@@ -480,16 +523,24 @@ async function startMacOSAudioCapture(geminiSessionRef) {
}); });
systemAudioProc.stderr.on('data', data => { systemAudioProc.stderr.on('data', data => {
console.error('SystemAudioDump stderr:', data.toString()); const msg = data.toString();
console.error('SystemAudioDump stderr:', msg);
if (msg.toLowerCase().includes('error')) {
sendToRenderer('update-status', 'Audio error: ' + msg.substring(0, 50));
}
}); });
systemAudioProc.on('close', code => { systemAudioProc.on('close', code => {
console.log('SystemAudioDump process closed with code:', code); console.log('SystemAudioDump closed with code:', code, 'chunks processed:', chunkCount);
if (code !== 0 && code !== null) {
sendToRenderer('update-status', `Audio stopped (exit: ${code})`);
}
systemAudioProc = null; systemAudioProc = null;
}); });
systemAudioProc.on('error', err => { systemAudioProc.on('error', err => {
console.error('SystemAudioDump process error:', err); console.error('SystemAudioDump spawn error:', err.message);
sendToRenderer('update-status', 'Audio error: ' + err.message);
systemAudioProc = null; systemAudioProc = null;
}); });
+99
View File
@@ -0,0 +1,99 @@
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,
};
+3 -1
View File
@@ -311,7 +311,9 @@ async function sendImageToOpenAI(base64Data, prompt, config) {
const { apiKey, baseUrl, model } = config; const { apiKey, baseUrl, model } = config;
// OpenAI doesn't support images in Realtime API yet, use standard Chat Completions // 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 { try {
const response = await fetch(apiEndpoint, { const response = await fetch(apiEndpoint, {
+138 -10
View File
@@ -277,10 +277,14 @@ async function sendImageMessage(base64Image, prompt) {
} }
// Process audio chunk and get response // Process audio chunk and get response
// This accumulates audio and transcribes when silence is detected // This accumulates audio and transcribes when silence is detected or timer expires
let audioChunks = []; let audioChunks = [];
let lastAudioTime = 0; let lastAudioTime = 0;
let firstChunkTime = 0;
const SILENCE_THRESHOLD_MS = 1500; // 1.5 seconds of silence 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) { async function processAudioChunk(base64Audio, mimeType) {
if (!openaiClient) { if (!openaiClient) {
@@ -290,12 +294,43 @@ async function processAudioChunk(base64Audio, mimeType) {
const now = Date.now(); const now = Date.now();
const buffer = Buffer.from(base64Audio, 'base64'); const buffer = Buffer.from(base64Audio, 'base64');
// 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 // Add to audio buffer
audioChunks.push(buffer); audioChunks.push(buffer);
lastAudioTime = now; lastAudioTime = now;
// Check for silence (no new audio for SILENCE_THRESHOLD_MS) // Clear existing timer
// This is a simple approach - in production you'd want proper VAD 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);
return { success: true, buffering: true }; return { success: true, buffering: true };
} }
@@ -304,15 +339,30 @@ async function flushAudioAndTranscribe() {
return { success: true, text: '' }; return { success: true, text: '' };
} }
// Clear Windows transcription timer
if (windowsTranscriptionTimer) {
clearInterval(windowsTranscriptionTimer);
windowsTranscriptionTimer = null;
}
try { try {
// Combine all audio chunks // Combine all audio chunks
const combinedBuffer = Buffer.concat(audioChunks); const combinedBuffer = Buffer.concat(audioChunks);
const chunkCount = audioChunks.length;
audioChunks = []; 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 // Transcribe
const transcription = await transcribeAudio(combinedBuffer); const transcription = await transcribeAudio(combinedBuffer);
if (transcription && transcription.trim()) { if (transcription && transcription.trim()) {
console.log('Transcription result:', transcription);
// Send to chat // Send to chat
const response = await sendTextMessage(transcription); const response = await sendTextMessage(transcription);
@@ -334,6 +384,16 @@ function clearConversation() {
const systemMessage = conversationMessages.find(m => m.role === 'system'); const systemMessage = conversationMessages.find(m => m.role === 'system');
conversationMessages = systemMessage ? [systemMessage] : []; conversationMessages = systemMessage ? [systemMessage] : [];
audioChunks = []; audioChunks = [];
// Clear timers
if (silenceCheckTimer) {
clearTimeout(silenceCheckTimer);
silenceCheckTimer = null;
}
if (windowsTranscriptionTimer) {
clearInterval(windowsTranscriptionTimer);
windowsTranscriptionTimer = null;
}
} }
function closeOpenAISDK() { function closeOpenAISDK() {
@@ -343,6 +403,17 @@ function closeOpenAISDK() {
conversationMessages = []; conversationMessages = [];
audioChunks = []; audioChunks = [];
isProcessing = false; isProcessing = false;
// Clear timers
if (silenceCheckTimer) {
clearTimeout(silenceCheckTimer);
silenceCheckTimer = null;
}
if (windowsTranscriptionTimer) {
clearInterval(windowsTranscriptionTimer);
windowsTranscriptionTimer = null;
}
sendToRenderer('update-status', 'Disconnected'); sendToRenderer('update-status', 'Disconnected');
} }
@@ -441,9 +512,11 @@ async function startMacOSAudioCapture() {
// Kill any existing SystemAudioDump processes first // Kill any existing SystemAudioDump processes first
await killExistingSystemAudioDump(); await killExistingSystemAudioDump();
console.log('Starting macOS audio capture with SystemAudioDump for OpenAI SDK...'); console.log('=== Starting macOS audio capture (OpenAI SDK) ===');
sendToRenderer('update-status', 'Starting audio capture...');
const { app } = require('electron'); const { app } = require('electron');
const fs = require('fs');
let systemAudioPath; let systemAudioPath;
if (app.isPackaged) { if (app.isPackaged) {
@@ -452,7 +525,35 @@ async function startMacOSAudioCapture() {
systemAudioPath = path.join(__dirname, '../assets', 'SystemAudioDump'); systemAudioPath = path.join(__dirname, '../assets', 'SystemAudioDump');
} }
console.log('SystemAudioDump path:', systemAudioPath); 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;
}
}
const spawnOptions = { const spawnOptions = {
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
@@ -461,10 +562,12 @@ async function startMacOSAudioCapture() {
}, },
}; };
console.log('Spawning SystemAudioDump...');
systemAudioProc = spawn(systemAudioPath, [], spawnOptions); systemAudioProc = spawn(systemAudioPath, [], spawnOptions);
if (!systemAudioProc.pid) { if (!systemAudioProc.pid) {
console.error('Failed to start SystemAudioDump'); console.error('FATAL: Failed to start SystemAudioDump - no PID');
sendToRenderer('update-status', 'Error: Audio capture failed to start');
return false; return false;
} }
@@ -476,8 +579,16 @@ async function startMacOSAudioCapture() {
const CHUNK_SIZE = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_DURATION; const CHUNK_SIZE = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_DURATION;
let tempBuffer = Buffer.alloc(0); let tempBuffer = Buffer.alloc(0);
let chunkCount = 0;
let firstDataReceived = false;
systemAudioProc.stdout.on('data', data => { 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]); tempBuffer = Buffer.concat([tempBuffer, data]);
while (tempBuffer.length >= CHUNK_SIZE) { while (tempBuffer.length >= CHUNK_SIZE) {
@@ -489,6 +600,11 @@ async function startMacOSAudioCapture() {
// Add to audio buffer for transcription // Add to audio buffer for transcription
audioBuffer = Buffer.concat([audioBuffer, monoChunk]); 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) // Limit buffer size (max 30 seconds of audio)
@@ -499,21 +615,33 @@ async function startMacOSAudioCapture() {
}); });
systemAudioProc.stderr.on('data', data => { systemAudioProc.stderr.on('data', data => {
console.error('SystemAudioDump stderr:', data.toString()); const msg = data.toString();
console.error('SystemAudioDump stderr:', msg);
if (msg.toLowerCase().includes('error')) {
sendToRenderer('update-status', 'Audio error: ' + msg.substring(0, 50));
}
}); });
systemAudioProc.on('close', code => { systemAudioProc.on('close', (code, signal) => {
console.log('SystemAudioDump process closed with code:', code); 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 = null; systemAudioProc = null;
stopTranscriptionTimer(); stopTranscriptionTimer();
}); });
systemAudioProc.on('error', err => { systemAudioProc.on('error', err => {
console.error('SystemAudioDump process error:', err); console.error('SystemAudioDump spawn error:', err.message, err.stack);
sendToRenderer('update-status', 'Audio error: ' + err.message);
systemAudioProc = null; systemAudioProc = null;
stopTranscriptionTimer(); stopTranscriptionTimer();
}); });
systemAudioProc.on('exit', (code, signal) => {
console.log('SystemAudioDump exit event:', { code, signal });
});
// Start periodic transcription // Start periodic transcription
startTranscriptionTimer(); startTranscriptionTimer();
+221 -88
View File
@@ -18,6 +18,29 @@ let currentImageQuality = 'medium'; // Store current image quality for manual sc
const isLinux = process.platform === 'linux'; const isLinux = process.platform === 'linux';
const isMacOS = process.platform === 'darwin'; 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 ============ // ============ STORAGE API ============
// Wrapper for IPC-based storage access // Wrapper for IPC-based storage access
@@ -113,7 +136,7 @@ const storage = {
async getTodayLimits() { async getTodayLimits() {
const result = await ipcRenderer.invoke('storage:get-today-limits'); const result = await ipcRenderer.invoke('storage:get-today-limits');
return result.success ? result.data : { flash: { count: 0 }, flashLite: { count: 0 } }; return result.success ? result.data : { flash: { count: 0 }, flashLite: { count: 0 } };
} },
}; };
// Cache for preferences to avoid async calls in hot paths // Cache for preferences to avoid async calls in hot paths
@@ -277,7 +300,21 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
console.log('Linux capture started - system audio:', mediaStream.getAudioTracks().length > 0, 'microphone mode:', audioMode); console.log('Linux capture started - system audio:', mediaStream.getAudioTracks().length > 0, 'microphone mode:', audioMode);
} else { } else {
// Windows - use display media with loopback for system audio // Windows - show custom screen picker first
logToMain('info', '=== Starting Windows audio capture ===');
cheatingDaddy.setStatus('Choose screen to share...');
// Show screen picker dialog
const appElement = document.querySelector('cheating-daddy-app');
const pickerResult = await appElement.showScreenPickerDialog();
if (pickerResult.cancelled) {
cheatingDaddy.setStatus('Cancelled');
return;
}
cheatingDaddy.setStatus('Starting capture...');
mediaStream = await navigator.mediaDevices.getDisplayMedia({ mediaStream = await navigator.mediaDevices.getDisplayMedia({
video: { video: {
frameRate: 1, frameRate: 1,
@@ -293,10 +330,29 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
}, },
}); });
console.log('Windows capture started with loopback audio'); const audioTracks = mediaStream.getAudioTracks();
const videoTracks = mediaStream.getVideoTracks();
// Setup audio processing for Windows loopback audio only logToMain('info', 'Windows capture result:', {
setupWindowsLoopbackProcessing(); 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');
cheatingDaddy.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();
}
if (audioMode === 'mic_only' || audioMode === 'both') { if (audioMode === 'mic_only' || audioMode === 'both') {
let micStream = null; let micStream = null;
@@ -329,10 +385,10 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
console.log('Manual mode enabled - screenshots will be captured on demand only'); console.log('Manual mode enabled - screenshots will be captured on demand only');
} catch (err) { } catch (err) {
console.error('Error starting capture:', err); console.error('Error starting capture:', err);
// Provide more helpful error messages based on error type // Provide more helpful error messages based on error type
let errorMessage = err.message || 'Failed to start capture'; let errorMessage = err.message || 'Failed to start capture';
if (errorMessage.toLowerCase().includes('timeout')) { if (errorMessage.toLowerCase().includes('timeout')) {
errorMessage = 'Screen capture timed out. Please try again and select a screen quickly.'; errorMessage = 'Screen capture timed out. Please try again and select a screen quickly.';
} else if (errorMessage.toLowerCase().includes('permission') || errorMessage.toLowerCase().includes('denied')) { } else if (errorMessage.toLowerCase().includes('permission') || errorMessage.toLowerCase().includes('denied')) {
@@ -342,7 +398,7 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
} else if (errorMessage.toLowerCase().includes('aborted') || errorMessage.toLowerCase().includes('cancel')) { } else if (errorMessage.toLowerCase().includes('aborted') || errorMessage.toLowerCase().includes('cancel')) {
errorMessage = 'Screen selection was cancelled. Please try again.'; errorMessage = 'Screen selection was cancelled. Please try again.';
} }
cheatingDaddy.setStatus('Error: ' + errorMessage); cheatingDaddy.setStatus('Error: ' + errorMessage);
} }
} }
@@ -412,32 +468,75 @@ function setupLinuxSystemAudioProcessing() {
function setupWindowsLoopbackProcessing() { function setupWindowsLoopbackProcessing() {
// Setup audio processing for Windows loopback audio only // Setup audio processing for Windows loopback audio only
audioContext = new AudioContext({ sampleRate: SAMPLE_RATE }); logToMain('info', 'Setting up Windows loopback audio processing...');
const source = audioContext.createMediaStreamSource(mediaStream);
audioProcessor = audioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
let audioBuffer = []; try {
const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION; audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
audioProcessor.onaudioprocess = async e => { logToMain('info', 'AudioContext created:', {
const inputData = e.inputBuffer.getChannelData(0); state: audioContext.state,
audioBuffer.push(...inputData); sampleRate: audioContext.sampleRate,
});
// Process audio in chunks // Resume AudioContext if suspended (Chrome policy)
while (audioBuffer.length >= samplesPerChunk) { if (audioContext.state === 'suspended') {
const chunk = audioBuffer.splice(0, samplesPerChunk); logToMain('warn', 'AudioContext suspended, attempting resume...');
const pcmData16 = convertFloat32ToInt16(chunk); audioContext
const base64Data = arrayBufferToBase64(pcmData16.buffer); .resume()
.then(() => {
await ipcRenderer.invoke('send-audio-content', { logToMain('info', 'AudioContext resumed successfully');
data: base64Data, })
mimeType: 'audio/pcm;rate=24000', .catch(err => {
}); logToMain('error', 'Failed to resume AudioContext:', err.message);
});
} }
};
source.connect(audioProcessor); const source = audioContext.createMediaStreamSource(mediaStream);
audioProcessor.connect(audioContext.destination); 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');
cheatingDaddy.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);
cheatingDaddy.setStatus('Audio error: ' + err.message);
}
} }
async function captureScreenshot(imageQuality = 'medium', isManual = false) { async function captureScreenshot(imageQuality = 'medium', isManual = false) {
@@ -669,17 +768,7 @@ async function captureRegionFromScreenshot(rect, screenshotDataUrl) {
const cropContext = cropCanvas.getContext('2d'); const cropContext = cropCanvas.getContext('2d');
// Draw only the selected region // Draw only the selected region
cropContext.drawImage( cropContext.drawImage(img, scaledRect.left, scaledRect.top, scaledRect.width, scaledRect.height, 0, 0, scaledRect.width, scaledRect.height);
img,
scaledRect.left,
scaledRect.top,
scaledRect.width,
scaledRect.height,
0,
0,
scaledRect.width,
scaledRect.height
);
// Convert to blob and send // Convert to blob and send
cropCanvas.toBlob( cropCanvas.toBlob(
@@ -892,7 +981,7 @@ ipcRenderer.on('save-session-context', async (event, data) => {
try { try {
await storage.saveSession(data.sessionId, { await storage.saveSession(data.sessionId, {
profile: data.profile, profile: data.profile,
customPrompt: data.customPrompt customPrompt: data.customPrompt,
}); });
console.log('Session context saved:', data.sessionId, 'profile:', data.profile); console.log('Session context saved:', data.sessionId, 'profile:', data.profile);
} catch (error) { } catch (error) {
@@ -906,7 +995,7 @@ ipcRenderer.on('save-screen-analysis', async (event, data) => {
await storage.saveSession(data.sessionId, { await storage.saveSession(data.sessionId, {
screenAnalysisHistory: data.fullHistory, screenAnalysisHistory: data.fullHistory,
profile: data.profile, profile: data.profile,
customPrompt: data.customPrompt customPrompt: data.customPrompt,
}); });
console.log('Screen analysis saved:', data.sessionId); console.log('Screen analysis saved:', data.sessionId);
} catch (error) { } catch (error) {
@@ -941,60 +1030,102 @@ const theme = {
themes: { themes: {
dark: { dark: {
background: '#1e1e1e', background: '#1e1e1e',
text: '#e0e0e0', textSecondary: '#a0a0a0', textMuted: '#6b6b6b', text: '#e0e0e0',
border: '#333333', accent: '#ffffff', textSecondary: '#a0a0a0',
btnPrimaryBg: '#ffffff', btnPrimaryText: '#000000', btnPrimaryHover: '#e0e0e0', textMuted: '#6b6b6b',
tooltipBg: '#1a1a1a', tooltipText: '#ffffff', border: '#333333',
keyBg: 'rgba(255,255,255,0.1)' accent: '#ffffff',
btnPrimaryBg: '#ffffff',
btnPrimaryText: '#000000',
btnPrimaryHover: '#e0e0e0',
tooltipBg: '#1a1a1a',
tooltipText: '#ffffff',
keyBg: 'rgba(255,255,255,0.1)',
}, },
light: { light: {
background: '#ffffff', background: '#ffffff',
text: '#1a1a1a', textSecondary: '#555555', textMuted: '#888888', text: '#1a1a1a',
border: '#e0e0e0', accent: '#000000', textSecondary: '#555555',
btnPrimaryBg: '#1a1a1a', btnPrimaryText: '#ffffff', btnPrimaryHover: '#333333', textMuted: '#888888',
tooltipBg: '#1a1a1a', tooltipText: '#ffffff', border: '#e0e0e0',
keyBg: 'rgba(0,0,0,0.1)' accent: '#000000',
btnPrimaryBg: '#1a1a1a',
btnPrimaryText: '#ffffff',
btnPrimaryHover: '#333333',
tooltipBg: '#1a1a1a',
tooltipText: '#ffffff',
keyBg: 'rgba(0,0,0,0.1)',
}, },
midnight: { midnight: {
background: '#0d1117', background: '#0d1117',
text: '#c9d1d9', textSecondary: '#8b949e', textMuted: '#6e7681', text: '#c9d1d9',
border: '#30363d', accent: '#58a6ff', textSecondary: '#8b949e',
btnPrimaryBg: '#58a6ff', btnPrimaryText: '#0d1117', btnPrimaryHover: '#79b8ff', textMuted: '#6e7681',
tooltipBg: '#161b22', tooltipText: '#c9d1d9', border: '#30363d',
keyBg: 'rgba(88,166,255,0.15)' accent: '#58a6ff',
btnPrimaryBg: '#58a6ff',
btnPrimaryText: '#0d1117',
btnPrimaryHover: '#79b8ff',
tooltipBg: '#161b22',
tooltipText: '#c9d1d9',
keyBg: 'rgba(88,166,255,0.15)',
}, },
sepia: { sepia: {
background: '#f4ecd8', background: '#f4ecd8',
text: '#5c4b37', textSecondary: '#7a6a56', textMuted: '#998875', text: '#5c4b37',
border: '#d4c8b0', accent: '#8b4513', textSecondary: '#7a6a56',
btnPrimaryBg: '#5c4b37', btnPrimaryText: '#f4ecd8', btnPrimaryHover: '#7a6a56', textMuted: '#998875',
tooltipBg: '#5c4b37', tooltipText: '#f4ecd8', border: '#d4c8b0',
keyBg: 'rgba(92,75,55,0.15)' accent: '#8b4513',
btnPrimaryBg: '#5c4b37',
btnPrimaryText: '#f4ecd8',
btnPrimaryHover: '#7a6a56',
tooltipBg: '#5c4b37',
tooltipText: '#f4ecd8',
keyBg: 'rgba(92,75,55,0.15)',
}, },
nord: { nord: {
background: '#2e3440', background: '#2e3440',
text: '#eceff4', textSecondary: '#d8dee9', textMuted: '#4c566a', text: '#eceff4',
border: '#3b4252', accent: '#88c0d0', textSecondary: '#d8dee9',
btnPrimaryBg: '#88c0d0', btnPrimaryText: '#2e3440', btnPrimaryHover: '#8fbcbb', textMuted: '#4c566a',
tooltipBg: '#3b4252', tooltipText: '#eceff4', border: '#3b4252',
keyBg: 'rgba(136,192,208,0.15)' accent: '#88c0d0',
btnPrimaryBg: '#88c0d0',
btnPrimaryText: '#2e3440',
btnPrimaryHover: '#8fbcbb',
tooltipBg: '#3b4252',
tooltipText: '#eceff4',
keyBg: 'rgba(136,192,208,0.15)',
}, },
dracula: { dracula: {
background: '#282a36', background: '#282a36',
text: '#f8f8f2', textSecondary: '#bd93f9', textMuted: '#6272a4', text: '#f8f8f2',
border: '#44475a', accent: '#ff79c6', textSecondary: '#bd93f9',
btnPrimaryBg: '#ff79c6', btnPrimaryText: '#282a36', btnPrimaryHover: '#ff92d0', textMuted: '#6272a4',
tooltipBg: '#44475a', tooltipText: '#f8f8f2', border: '#44475a',
keyBg: 'rgba(255,121,198,0.15)' accent: '#ff79c6',
btnPrimaryBg: '#ff79c6',
btnPrimaryText: '#282a36',
btnPrimaryHover: '#ff92d0',
tooltipBg: '#44475a',
tooltipText: '#f8f8f2',
keyBg: 'rgba(255,121,198,0.15)',
}, },
abyss: { abyss: {
background: '#0a0a0a', background: '#0a0a0a',
text: '#d4d4d4', textSecondary: '#808080', textMuted: '#505050', text: '#d4d4d4',
border: '#1a1a1a', accent: '#ffffff', textSecondary: '#808080',
btnPrimaryBg: '#ffffff', btnPrimaryText: '#0a0a0a', btnPrimaryHover: '#d4d4d4', textMuted: '#505050',
tooltipBg: '#141414', tooltipText: '#d4d4d4', border: '#1a1a1a',
keyBg: 'rgba(255,255,255,0.08)' accent: '#ffffff',
} btnPrimaryBg: '#ffffff',
btnPrimaryText: '#0a0a0a',
btnPrimaryHover: '#d4d4d4',
tooltipBg: '#141414',
tooltipText: '#d4d4d4',
keyBg: 'rgba(255,255,255,0.08)',
},
}, },
current: 'dark', current: 'dark',
@@ -1011,29 +1142,31 @@ const theme = {
sepia: 'Sepia', sepia: 'Sepia',
nord: 'Nord', nord: 'Nord',
dracula: 'Dracula', dracula: 'Dracula',
abyss: 'Abyss' abyss: 'Abyss',
}; };
return Object.keys(this.themes).map(key => ({ return Object.keys(this.themes).map(key => ({
value: key, value: key,
name: names[key] || key, name: names[key] || key,
colors: this.themes[key] colors: this.themes[key],
})); }));
}, },
hexToRgb(hex) { hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? { return result
r: parseInt(result[1], 16), ? {
g: parseInt(result[2], 16), r: parseInt(result[1], 16),
b: parseInt(result[3], 16) g: parseInt(result[2], 16),
} : { r: 30, g: 30, b: 30 }; b: parseInt(result[3], 16),
}
: { r: 30, g: 30, b: 30 };
}, },
lightenColor(rgb, amount) { lightenColor(rgb, amount) {
return { return {
r: Math.min(255, rgb.r + amount), r: Math.min(255, rgb.r + amount),
g: Math.min(255, rgb.g + amount), g: Math.min(255, rgb.g + amount),
b: Math.min(255, rgb.b + amount) b: Math.min(255, rgb.b + amount),
}; };
}, },
@@ -1041,7 +1174,7 @@ const theme = {
return { return {
r: Math.max(0, rgb.r - amount), r: Math.max(0, rgb.r - amount),
g: Math.max(0, rgb.g - amount), g: Math.max(0, rgb.g - amount),
b: Math.max(0, rgb.b - amount) b: Math.max(0, rgb.b - amount),
}; };
}, },
@@ -1121,7 +1254,7 @@ const theme = {
async save(themeName) { async save(themeName) {
await storage.updatePreference('theme', themeName); await storage.updatePreference('theme', themeName);
this.apply(themeName); this.apply(themeName);
} },
}; };
// Consolidated cheatingDaddy object - all functions in one place // Consolidated cheatingDaddy object - all functions in one place
+69 -48
View File
@@ -33,60 +33,55 @@ function createWindow(sendToRenderer, geminiSessionRef) {
}); });
const { session, desktopCapturer } = require('electron'); const { session, desktopCapturer } = require('electron');
// Setup display media request handler for screen capture // Store selected source for Windows custom picker
// On macOS, use system picker for better UX let selectedSourceId = null;
// Setup display media handler based on platform
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
// macOS: Use native system picker
session.defaultSession.setDisplayMediaRequestHandler( session.defaultSession.setDisplayMediaRequestHandler(
async (request, callback) => { (request, callback) => {
try { desktopCapturer.getSources({ types: ['screen'] }).then(sources => {
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' }); 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 } { 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.setResizable(false);
mainWindow.setContentProtection(true); mainWindow.setContentProtection(true);
mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
@@ -558,7 +553,10 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
const primaryDisplay = screen.getPrimaryDisplay(); const primaryDisplay = screen.getPrimaryDisplay();
// Calculate bounds that cover all displays // 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 => { displays.forEach(display => {
minX = Math.min(minX, display.bounds.x); minX = Math.min(minX, display.bounds.x);
minY = Math.min(minY, display.bounds.y); minY = Math.min(minY, display.bounds.y);
@@ -717,7 +715,7 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
regionSelectionWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`); regionSelectionWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`);
return new Promise((resolve) => { return new Promise(resolve => {
ipcMain.once('region-selected', (event, rect) => { ipcMain.once('region-selected', (event, rect) => {
if (regionSelectionWindow && !regionSelectionWindow.isDestroyed()) { if (regionSelectionWindow && !regionSelectionWindow.isDestroyed()) {
regionSelectionWindow.close(); regionSelectionWindow.close();
@@ -760,6 +758,29 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
return { success: false, error: error.message }; 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 = { module.exports = {
+1 -1
View File
@@ -12,4 +12,4 @@ export async function resizeLayout() {
} catch (error) { } catch (error) {
console.error('Error resizing window:', error); console.error('Error resizing window:', error);
} }
} }