Compare commits
28
Commits
310b6b3fbd
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
219f35cc04 | ||
|
|
b6560f3c6c | ||
|
|
b1d9130b50 | ||
|
|
07c39455be | ||
|
|
39fe8d948f | ||
|
|
1330af8d19 | ||
|
|
c161f251ed | ||
|
|
d111b88886 | ||
|
|
043b5d159e | ||
|
|
851edc6da1 | ||
|
|
c68a546e72 | ||
|
|
09b2530714 | ||
|
|
a9dce5bf3c | ||
|
|
31d50c9713 | ||
|
|
bbad79875c | ||
|
|
7f15b65eb1 | ||
|
|
d6dbaa3141 | ||
|
|
2ebde60dcd | ||
|
|
0d56e06724 | ||
|
|
526bc4e877 | ||
|
|
684b61755c | ||
|
|
1b74968006 | ||
|
|
4cf48ee0af | ||
|
|
494e692738 | ||
|
|
8b216bbb33 | ||
|
|
bd62cf5524 | ||
|
|
bfd76dc0c1 | ||
|
|
430895d9ab |
@@ -64,10 +64,11 @@ jobs:
|
|||||||
out/make/**/*.deb
|
out/make/**/*.deb
|
||||||
out/make/**/*.rpm
|
out/make/**/*.rpm
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
compression-level: 0
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: macos-latest
|
||||||
if: github.server_url == 'https://github.com'
|
if: github.server_url == 'https://github.com'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -1,58 +1,279 @@
|
|||||||
<!-- <img width="1299" height="424" alt="cd (1)" src="https://github.com/user-attachments/assets/b25fff4d-043d-4f38-9985-f832ae0d0f6e" /> -->
|
<div align="center">
|
||||||
# Mastermind
|
<img src="assets/images/logo.png" alt="Mastermind Logo" width="200"/>
|
||||||
|
|
||||||
|
# Mastermind
|
||||||
|
|
||||||
|
### Your AI Assistant for High-Stakes Conversations
|
||||||
|
|
||||||
|
*Real-time contextual suggestions when you need them most*
|
||||||
|
|
||||||
|
[](https://github.com/ShiftyX1/Mastermind/actions/workflows/release.yml)
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://github.com/ShiftyX1/Mastermind/releases)
|
||||||
|
[](https://github.com/ShiftyX1/Mastermind/releases)
|
||||||
|
[](#requirements)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> [!NOTE]
|
## What is Mastermind?
|
||||||
> Use latest MacOS and Windows version, older versions have limited support
|
|
||||||
|
|
||||||
> [!NOTE]
|
Mastermind is an **AI assistant** for high-stakes conversations. Whether you're in a job interview, closing a deal, or navigating a complex negotiation, Mastermind analyzes what you see and hear in real-time, providing contextual suggestions and talking points to support your responses.
|
||||||
> During testing it wont answer if you ask something, you need to simulate interviewer asking question, which it will answer
|
|
||||||
|
|
||||||
A real-time AI assistant that provides contextual help during video calls, interviews, presentations, and meetings using screen capture and audio analysis. It is fork of [Cheating Daddy](https://github.com/sohzm/cheating-daddy) project.
|
Think of it as having an experienced coach reviewing the conversation and offering suggestions, helping you recall relevant information and structure your thoughts more effectively. The AI provides support material—you still need to understand, adapt, and deliver the responses in your own words.
|
||||||
|
|
||||||
## Features
|
> [!WARNING]
|
||||||
|
> **AI models can and do make mistakes.** Suggestions may contain errors, outdated information, or inappropriate content. This tool is designed to assist people who already have relevant knowledge and need help organizing their thoughts—not to fake expertise you don't possess. Always verify critical information and use your own judgment.
|
||||||
|
|
||||||
- **Live AI Assistance**: Real-time help powered by Gemini API / OpenAI SDK / OpenAI Realtime API, so you can choose which one you want to use
|
### The Hidden Assistant Advantage
|
||||||
- **Screen & Audio Capture**: Analyzes what you see and hear for contextual responses
|
|
||||||
- **Multiple Profiles**: Interview, Sales Call, Business Meeting, Presentation, Negotiation
|
|
||||||
- **Transparent Overlay**: Always-on-top window that can be positioned anywhere, if something goes wrong you can hide it without stoping session and losing context!
|
|
||||||
- **Click-through Mode**: Make window transparent to clicks when needed
|
|
||||||
- **Cross-platform**: Works on macOS, Windows, and Linux (kinda, dont use, just for testing rn)
|
|
||||||
|
|
||||||
## Setup
|
Mastermind operates discreetly with a transparent overlay that blends into your screen. The system analyzes both visual content and audio in real-time, generating contextual suggestions within seconds. It adapts its suggestions based on your selected scenario—interview, sales, meeting, or presentation. Ghost mode allows you to interact with content behind the overlay without closing it.
|
||||||
|
|
||||||
1. **Get a API Key**: Visit [Google AI Studio](https://aistudio.google.com/apikey) or [OpenAI](https://platform.openai.com/docs/api-reference) or any other OpenAI-compatible API!
|
**Remember:** This is an assistive tool, not a magic solution. It works best when you have genuine knowledge and need support organizing your thoughts under pressure.
|
||||||
2. **Install Dependencies**: `pnpm install`
|
|
||||||
3. **Run the App**: `pnpm start`
|
|
||||||
|
|
||||||
## Usage
|
## Key Features
|
||||||
|
|
||||||
1. Enter your API key in the main window, select provider and model you want to use in preferences
|
### Real-Time Multi-Modal Analysis
|
||||||
2. Choose your profile and language in settings
|
Mastermind captures your screen and audio simultaneously, processing both visual content and audio streams to understand conversation context. It analyzes what's being discussed and generates relevant suggestions based on that context. The system supports dual-stream audio capture to distinguish between system audio and your microphone input, though transcription accuracy depends on audio quality, accents, and background noise.
|
||||||
3. Click "Start Session" to begin, if you want to use push-to-talk mode, you can enable it in preferences
|
|
||||||
4. Position the window using keyboard shortcuts, or use your mouse to move it
|
### Local-First Privacy Option
|
||||||
5. The AI will provide real-time assistance based on your screen and system audio/microphone input, you can also send text messages to AI by pressing Enter
|
Choose between cloud AI providers for maximum performance or run everything locally with Ollama integration and offline Whisper.js transcription. When using local processing, no audio or screen data ever leaves your machine. The local transcription engine uses ONNX Runtime with GPU acceleration support for fast, private speech-to-text conversion.
|
||||||
|
|
||||||
|
### Conversation History & Context
|
||||||
|
Mastermind saves conversation turns during the session, building context as the conversation progresses. You can view session history and export conversations for later review. The AI uses this accumulated context to provide more relevant suggestions as it learns about the discussion topic—though the quality of contextual understanding is limited by the AI model's capabilities and the clarity of the captured audio/screen content.
|
||||||
|
|
||||||
|
### Specialized Profiles
|
||||||
|
|
||||||
|
Mastermind comes with six pre-configured personas designed for different scenarios:
|
||||||
|
|
||||||
|
**Job Interview** — Suggested responses to technical and behavioral questions, STAR method frameworks, and structured talking points based on your background.
|
||||||
|
|
||||||
|
**Sales Call** — Objection handling suggestions, closing technique ideas, pricing strategy considerations, and rapport building approaches.
|
||||||
|
|
||||||
|
**Business Meeting** — Data-driven talking points, strategic recommendations, and action-oriented communication suggestions.
|
||||||
|
|
||||||
|
**Presentation** — Fact-checking support, audience engagement ideas, and recovery suggestions for unexpected situations.
|
||||||
|
|
||||||
|
**Negotiation** — Tactical considerations, counter-offer frameworks, and strategic talking points to support your position.
|
||||||
|
|
||||||
|
**Exam Assistant** — Information lookup and answer suggestions for exam questions, optimized for quick reference.
|
||||||
|
|
||||||
|
### Invisible Design
|
||||||
|
The transparent overlay stays on top without blocking your view, with keyboard-driven positioning for quick adjustments. You can hide the window instantly with one click if needed, and customize opacity to match your environment perfectly.
|
||||||
|
|
||||||
|
### Flexible AI Backend
|
||||||
|
|
||||||
|
Mastermind supports multiple AI providers and can work with both cloud and local models:
|
||||||
|
|
||||||
|
**Google Gemini** — Fast, cost-effective multimodal processing with excellent vision capabilities. Supports Gemini 2.0 Flash with real-time API for ultra-low latency responses.
|
||||||
|
|
||||||
|
**OpenAI** — Industry-leading language understanding with GPT-4 and GPT-4o models.
|
||||||
|
|
||||||
|
**Groq** — High-speed inference with competitive pricing and excellent performance.
|
||||||
|
|
||||||
|
**Ollama** — Run completely local AI models on your machine for full privacy. No data ever leaves your device.
|
||||||
|
|
||||||
|
**Any OpenAI-Compatible API** — Connect to LocalAI, LM Studio, or any custom endpoint that follows the OpenAI API format.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
**Response Modes** — Toggle between Brief mode (1-3 sentences, optimal for quick glances) and Detailed mode (comprehensive explanations with full context) based on your needs during the session.
|
||||||
|
|
||||||
|
**Google Search Integration** — Optional real-time web search capability allows the AI to fetch current information. Note that search results may include outdated or incorrect information—always verify critical facts from authoritative sources.
|
||||||
|
|
||||||
|
**Custom System Prompts** — Tailor the AI's behavior with custom instructions specific to your industry, role, or situation. Add your resume, company information, or specialized knowledge to improve context relevance.
|
||||||
|
|
||||||
|
**Multi-Language Support** — Works in 30+ languages including English, Spanish, German, French, Japanese, Korean, Chinese, Hindi, Arabic, and many more. Auto-detection available for multilingual conversations, though accuracy varies by language and accent.
|
||||||
|
|
||||||
|
**Customizable Keyboard Shortcuts** — Every shortcut can be remapped to your preference. Create your own workflow that feels natural to you.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
#### For Users (Recommended)
|
||||||
|
|
||||||
|
Download the latest release for your platform from the [GitHub Releases](https://github.com/ShiftyX1/Mastermind/releases) page:
|
||||||
|
|
||||||
|
**macOS:**
|
||||||
|
1. Download `Mastermind-[version].dmg`
|
||||||
|
2. Open the DMG file and drag Mastermind to your Applications folder
|
||||||
|
3. Launch Mastermind from Applications (you may need to allow the app in System Preferences → Security & Privacy on first launch)
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
1. Download `Mastermind-[version]-Setup.exe`
|
||||||
|
2. Run the installer and follow the setup wizard
|
||||||
|
3. Launch Mastermind from the Start menu or desktop shortcut
|
||||||
|
|
||||||
|
#### For Developers
|
||||||
|
|
||||||
|
If you want to build from source or contribute to development:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/ShiftyX1/Mastermind.git
|
||||||
|
cd Mastermind
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Launch in development mode
|
||||||
|
pnpm start
|
||||||
|
|
||||||
|
# Build distributable packages (DMG for macOS, Setup.exe for Windows)
|
||||||
|
pnpm run make
|
||||||
|
|
||||||
|
# Package without creating installers
|
||||||
|
pnpm run package
|
||||||
|
```
|
||||||
|
|
||||||
|
### First-Time Setup
|
||||||
|
|
||||||
|
**Get Your AI Key:** Start by obtaining an API key from [Google AI Studio](https://aistudio.google.com/apikey) (recommended for beginners), [OpenAI Platform](https://platform.openai.com/api-keys), [Groq Console](https://console.groq.com), or configure a local Ollama instance for complete privacy.
|
||||||
|
|
||||||
|
**Configure Your Assistant:** Enter your API key in the main window and select your preferred AI provider and model. Choose your primary use case profile from the six available scenarios. Select your language or use Auto for multilingual support.
|
||||||
|
|
||||||
|
**Start Your Session:** Click "Start Session" to activate your hidden assistant. Grant screen recording and audio capture permissions when prompted by your system. Position the overlay window where it's most useful and adjust opacity to blend naturally with your environment.
|
||||||
|
|
||||||
|
### Daily Usage
|
||||||
|
|
||||||
|
**Starting a Session:** Select the appropriate profile for your scenario—Interview, Sales, Meeting, Presentation, Negotiation, or Exam. Adjust opacity and position to blend naturally with your environment. Choose your audio mode: Speaker Only (system audio), Microphone Only, or Both for dual-stream capture.
|
||||||
|
|
||||||
|
**During Your Conversation:** The AI analyzes your screen and audio context in real-time, with suggestions appearing in the overlay. You can type questions directly for clarification and use keyboard shortcuts to reposition or hide the window. Toggle between brief and detailed response modes depending on your needs.
|
||||||
|
|
||||||
|
**Important:** Treat AI suggestions as reference material, not verified facts. Quickly scan suggestions, extract useful points, and deliver responses in your own words with your own understanding. Don't read AI responses verbatim—this often sounds unnatural and may include errors.
|
||||||
|
|
||||||
|
**Pro Tips:** Position the window in your natural eye-line to avoid obvious glances. Use click-through mode when you need to interact with content behind the overlay. Keep sessions focused on one topic for better context. Enable local transcription when working with sensitive information.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Keyboard Shortcuts
|
## Keyboard Shortcuts
|
||||||
|
|
||||||
|
Master these shortcuts for seamless, discreet operation:
|
||||||
|
|
||||||
|
| Action | Shortcut | Purpose |
|
||||||
|
|--------|----------|---------|
|
||||||
|
| **Move Window** | `Ctrl/Cmd + Arrow Keys` | Reposition without using mouse |
|
||||||
|
| **Toggle Click-Through** | `Ctrl/Cmd + M` | Make window transparent to clicks |
|
||||||
|
| **Quick Hide** | `Ctrl/Cmd + \` | Instantly hide/show or go back |
|
||||||
|
| **Send Message** | `Enter` | Send text query to AI |
|
||||||
|
| **Quick Position** | Custom | Set your favorite window positions |
|
||||||
|
|
||||||
|
> **Pro Tip**: All shortcuts are fully customizable in settings. Create your own stealth workflow!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audio Capture Technology
|
||||||
|
|
||||||
|
Mastermind uses advanced audio capture to understand conversations in real-time, with support for both cloud and local transcription.
|
||||||
|
|
||||||
|
**macOS** — Leverages [SystemAudioDump](https://github.com/sohzm/systemAudioDump) for crystal-clear system audio capture. Supports three modes: Speaker Only (system audio), Microphone Only (your voice), or Both (simultaneous dual-stream capture).
|
||||||
|
|
||||||
|
**Windows** — Professional loopback audio capture for system sounds, with full microphone support and dual-stream capabilities for capturing both sides of the conversation.
|
||||||
|
|
||||||
|
**Linux** — Microphone input support. System audio capture is currently in development.
|
||||||
|
|
||||||
|
**Local Transcription** — Built-in offline speech-to-text using Whisper.js powered by ONNX Runtime. Your audio is processed locally on your machine without sending data to external services. Supports GPU acceleration on compatible hardware.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
### Job Interviews
|
||||||
|
Get suggested responses and frameworks for technical questions, helping you structure your thoughts using proven methods like STAR. Mastermind can help you recall relevant examples from your background and organize talking points, but you need to adapt and deliver them authentically in your own voice.
|
||||||
|
|
||||||
|
### Sales & Client Calls
|
||||||
|
Access reference material for objection handling and competitive positioning. The system can suggest talking points and strategies by analyzing the conversation context, but closing deals requires genuine understanding of your product and the client's needs—AI suggestions are starting points, not scripts to read verbatim.
|
||||||
|
|
||||||
|
### Business Negotiations
|
||||||
|
Receive strategic considerations and framework suggestions based on the conversation flow. Mastermind can help you structure counter-offers and identify discussion points, but successful negotiation requires reading the room, building rapport, and making judgment calls that AI cannot make for you.
|
||||||
|
|
||||||
|
### Presentations & Demos
|
||||||
|
Get quick fact-checking and audience engagement ideas during your presentation. If questions arise, Mastermind can suggest relevant information, but you should verify accuracy and ensure you genuinely understand what you're presenting—especially important for technical content where deep knowledge is expected.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
| Component | Requirement |
|
||||||
|
|-----------|-------------|
|
||||||
|
| **Operating System** | macOS 10.15+, Windows 10/11 (latest versions recommended) |
|
||||||
|
| **Permissions** | Screen recording, audio capture (system audio and/or microphone) |
|
||||||
|
| **Internet** | Required for cloud AI providers (Gemini, OpenAI, Groq). Optional for local Ollama models |
|
||||||
|
| **AI Provider** | API key from Gemini, OpenAI, Groq, or local Ollama installation |
|
||||||
|
| **For Local Transcription** | 4GB+ RAM recommended, GPU acceleration optional but recommended |
|
||||||
|
|
||||||
|
**Current Version:** 0.7.3
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> All keyboard shortcuts are customizable in settings. You can check some default shortcuts below.
|
> **Platform Support**: macOS and Windows are fully supported and tested. Linux support is experimental.
|
||||||
|
|
||||||
- **Window Movement**: `Ctrl/Cmd + Arrow Keys` - Move window
|
> [!TIP]
|
||||||
- **Click-through**: `Ctrl/Cmd + M` - Toggle mouse events
|
> **Testing Mode**: When testing, simulate someone asking you questions. The AI responds to detected questions rather than your own queries.
|
||||||
- **Close/Back**: `Ctrl/Cmd + \` - Close window or go back
|
|
||||||
- **Send Message**: `Enter` - Send text to AI
|
|
||||||
|
|
||||||
## Audio Capture
|
---
|
||||||
|
|
||||||
- **macOS**: [SystemAudioDump](https://github.com/Mohammed-Yasin-Mulla/Sound) for system audio capture, you can use microphone input as well
|
## Known Limitations
|
||||||
- **Windows**: Loopback audio capture, you can use microphone input as well
|
|
||||||
- **Linux**: Microphone input
|
|
||||||
|
|
||||||
## Requirements
|
**AI Response Quality** — AI models can make mistakes, provide outdated information, or misinterpret context. Always verify critical information and use suggestions as supporting material, not absolute truth. The quality of responses depends heavily on the AI model you choose and the context you provide.
|
||||||
|
|
||||||
- Electron-compatible OS (macOS, Windows, Linux)
|
**Not a Replacement for Knowledge** — Mastermind is a tool to help you recall and structure information, not to replace your actual expertise. The most effective use is when you already understand the subject matter and need help articulating or remembering specific details.
|
||||||
- AI Provider API key
|
|
||||||
- Screen recording permissions
|
**Linux Support** — System audio capture is not yet implemented on Linux. Only microphone input is currently supported.
|
||||||
- Microphone/audio permissions
|
|
||||||
|
**Local Transcription Performance** — First-time usage requires downloading the Whisper model files (approximately 150MB). Transcription speed depends on your hardware; GPU acceleration is recommended for optimal performance.
|
||||||
|
|
||||||
|
**macOS Permissions** — Screen recording and audio capture require explicit system permissions. You may need to restart the app after granting permissions on first launch.
|
||||||
|
|
||||||
|
**Session Context** — The AI maintains context only within the current session. Starting a new session clears previous conversation history (though history can be saved and viewed later).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Privacy & Ethics
|
||||||
|
|
||||||
|
**Your Data, Your Control:** All audio and screen capture happens locally on your device. API communications are direct and clear between you and your chosen provider. For complete privacy, you can use local AI models through Ollama without any data leaving your machine.
|
||||||
|
|
||||||
|
**Critical Disclaimers:**
|
||||||
|
- AI models can generate incorrect, biased, or inappropriate content. Always verify important information from authoritative sources.
|
||||||
|
- This tool provides suggestions, not verified facts. You are responsible for the accuracy of what you say.
|
||||||
|
- Relying entirely on AI suggestions without understanding the content can backfire—especially in technical or expert conversations where follow-up questions will reveal lack of genuine knowledge.
|
||||||
|
|
||||||
|
**Responsible Use:**
|
||||||
|
Mastermind is designed as a preparation and cognitive support tool—like having notes or a reference guide. It works best when you already have foundational knowledge and need help organizing thoughts or recalling details under pressure.
|
||||||
|
|
||||||
|
**Ethical Boundaries:** Always comply with the rules and policies of your specific context. Many situations explicitly prohibit external assistance:
|
||||||
|
- Academic exams and certification tests typically ban any form of external help
|
||||||
|
- Some professional interviews and assessments prohibit such tools
|
||||||
|
- Certain regulated industries have strict rules about information access during calls
|
||||||
|
- Using AI assistance where prohibited can result in serious consequences, including job loss or legal issues
|
||||||
|
|
||||||
|
**Use this tool to support your genuine expertise, not to fake knowledge you don't have.** The best outcomes happen when AI assists someone who understands the subject, not when it replaces actual competence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Based on the excellent work from [Cheating Daddy](https://github.com/sohzm/cheating-daddy).
|
||||||
|
|
||||||
|
Contributions are welcome! Please see [AGENTS.md](AGENTS.md) for development guidelines.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the GPL-3.0 License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
### A tool to support your expertise, not replace it
|
||||||
|
|
||||||
|
*Use responsibly. Verify information. Understand what you're saying.*
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 238 KiB |
+32
-23
@@ -1,14 +1,15 @@
|
|||||||
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");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
packagerConfig: {
|
packagerConfig: {
|
||||||
asar: {
|
asar: {
|
||||||
unpack: '**/{onnxruntime-node,onnxruntime-common,@huggingface/transformers,sharp,@img}/**',
|
unpack:
|
||||||
|
"**/{onnxruntime-node,onnxruntime-common,@huggingface/transformers,sharp,@img}/**",
|
||||||
},
|
},
|
||||||
extraResource: ['./src/assets/SystemAudioDump'],
|
extraResource: ["./src/assets/SystemAudioDump"],
|
||||||
name: 'Cheating Daddy',
|
name: "Mastermind",
|
||||||
icon: 'src/assets/logo',
|
icon: "src/assets/logo",
|
||||||
// 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
|
// also fuck apple
|
||||||
@@ -27,40 +28,48 @@ module.exports = {
|
|||||||
// teamId: 'your team id',
|
// teamId: 'your team id',
|
||||||
// },
|
// },
|
||||||
},
|
},
|
||||||
rebuildConfig: {},
|
rebuildConfig: {
|
||||||
|
// Ensure onnxruntime-node is rebuilt against Electron's Node.js headers
|
||||||
|
// so the native binding matches the ABI used in packaged builds.
|
||||||
|
onlyModules: ["onnxruntime-node", "sharp"],
|
||||||
|
},
|
||||||
makers: [
|
makers: [
|
||||||
{
|
{
|
||||||
name: '@electron-forge/maker-squirrel',
|
name: "@electron-forge/maker-squirrel",
|
||||||
config: {
|
config: {
|
||||||
name: 'cheating-daddy',
|
name: "mastermind",
|
||||||
productName: 'Cheating Daddy',
|
productName: "Mastermind",
|
||||||
shortcutName: 'Cheating Daddy',
|
shortcutName: "Mastermind",
|
||||||
createDesktopShortcut: true,
|
createDesktopShortcut: true,
|
||||||
createStartMenuShortcut: true,
|
createStartMenuShortcut: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '@electron-forge/maker-dmg',
|
name: "@electron-forge/maker-dmg",
|
||||||
platforms: ['darwin'],
|
platforms: ["darwin"],
|
||||||
|
config: {
|
||||||
|
format: "UDZO",
|
||||||
|
icon: "src/assets/logo.icns",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '@reforged/maker-appimage',
|
name: "@reforged/maker-appimage",
|
||||||
platforms: ['linux'],
|
platforms: ["linux"],
|
||||||
config: {
|
config: {
|
||||||
options: {
|
options: {
|
||||||
name: 'Cheating Daddy',
|
name: "Mastermind",
|
||||||
productName: 'Cheating Daddy',
|
productName: "Mastermind",
|
||||||
genericName: 'AI Assistant',
|
genericName: "AI Assistant",
|
||||||
description: 'AI assistant for interviews and learning',
|
description: "AI assistant for interviews and learning",
|
||||||
categories: ['Development', 'Education'],
|
categories: ["Development", "Education"],
|
||||||
icon: 'src/assets/logo.png'
|
icon: "src/assets/logo.png",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
{
|
{
|
||||||
name: '@electron-forge/plugin-auto-unpack-natives',
|
name: "@electron-forge/plugin-auto-unpack-natives",
|
||||||
config: {},
|
config: {},
|
||||||
},
|
},
|
||||||
// Fuses are used to enable/disable various Electron functionality
|
// Fuses are used to enable/disable various Electron functionality
|
||||||
|
|||||||
+17
-13
@@ -1,26 +1,27 @@
|
|||||||
{
|
{
|
||||||
"name": "cheating-daddy",
|
"name": "mastermind",
|
||||||
"productName": "cheating-daddy",
|
"productName": "Mastermind",
|
||||||
"version": "0.7.0",
|
"version": "0.7.9",
|
||||||
"description": "cheating daddy",
|
"description": "Mastermind AI assistant",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron-forge start",
|
"start": "electron-forge start",
|
||||||
"package": "electron-forge package",
|
"package": "electron-forge package",
|
||||||
"make": "electron-forge make",
|
"make": "electron-forge make",
|
||||||
"publish": "electron-forge publish",
|
"publish": "electron-forge publish",
|
||||||
"lint": "echo \"No linting configured\""
|
"lint": "echo \"No linting configured\"",
|
||||||
|
"postinstall": "electron-rebuild -f -w onnxruntime-node"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"cheating daddy",
|
"mastermind",
|
||||||
"cheating daddy ai",
|
"mastermind ai",
|
||||||
"cheating daddy ai assistant",
|
"mastermind ai assistant",
|
||||||
"cheating daddy ai assistant for interviews",
|
"mastermind ai assistant for interviews",
|
||||||
"cheating daddy ai assistant for interviews"
|
"mastermind ai assistant for interviews"
|
||||||
],
|
],
|
||||||
"author": {
|
"author": {
|
||||||
"name": "sohzm",
|
"name": "ShiftyX1",
|
||||||
"email": "sohambharambe9@gmail.com"
|
"email": "lead@pyserve.org"
|
||||||
},
|
},
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
"@huggingface/transformers": "^3.8.1",
|
"@huggingface/transformers": "^3.8.1",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"ollama": "^0.6.3",
|
"ollama": "^0.6.3",
|
||||||
|
"openai": "^6.22.0",
|
||||||
"p-retry": "^4.6.2",
|
"p-retry": "^4.6.2",
|
||||||
"ws": "^8.19.0"
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
@@ -41,8 +43,10 @@
|
|||||||
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
|
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
|
||||||
"@electron-forge/plugin-fuses": "^7.8.1",
|
"@electron-forge/plugin-fuses": "^7.8.1",
|
||||||
"@electron/fuses": "^1.8.0",
|
"@electron/fuses": "^1.8.0",
|
||||||
|
"@electron/rebuild": "^3.7.1",
|
||||||
"@reforged/maker-appimage": "^5.0.0",
|
"@reforged/maker-appimage": "^5.0.0",
|
||||||
"electron": "^30.0.5"
|
"electron": "^30.0.5",
|
||||||
|
"electron-icon-builder": "^2.0.1"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
Generated
+1679
-3
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,6 @@
|
|||||||
|
packages:
|
||||||
|
- '.'
|
||||||
|
|
||||||
onlyBuiltDependencies:
|
onlyBuiltDependencies:
|
||||||
- electron
|
- electron
|
||||||
- electron-winstaller
|
- electron-winstaller
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 353 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 190 KiB |
@@ -242,15 +242,15 @@ export class AppHeader extends LitElement {
|
|||||||
|
|
||||||
getViewTitle() {
|
getViewTitle() {
|
||||||
const titles = {
|
const titles = {
|
||||||
onboarding: 'Welcome to Cheating Daddy',
|
onboarding: 'Welcome to Mastermind',
|
||||||
main: 'Cheating Daddy',
|
main: 'Mastermind',
|
||||||
customize: 'Customize',
|
customize: 'Customize',
|
||||||
help: 'Help & Shortcuts',
|
help: 'Help & Shortcuts',
|
||||||
history: 'Conversation History',
|
history: 'Conversation History',
|
||||||
advanced: 'Advanced Tools',
|
advanced: 'Advanced Tools',
|
||||||
assistant: 'Cheating Daddy',
|
assistant: 'Mastermind',
|
||||||
};
|
};
|
||||||
return titles[this.currentView] || 'Cheating Daddy';
|
return titles[this.currentView] || 'Mastermind';
|
||||||
}
|
}
|
||||||
|
|
||||||
getElapsedTime() {
|
getElapsedTime() {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
|
import { html, css, LitElement } from "../../assets/lit-core-2.7.4.min.js";
|
||||||
import { MainView } from '../views/MainView.js';
|
import { MainView } from "../views/MainView.js";
|
||||||
import { CustomizeView } from '../views/CustomizeView.js';
|
import { CustomizeView } from "../views/CustomizeView.js";
|
||||||
import { HelpView } from '../views/HelpView.js';
|
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 { AICustomizeView } from '../views/AICustomizeView.js';
|
import { AICustomizeView } from "../views/AICustomizeView.js";
|
||||||
import { FeedbackView } from '../views/FeedbackView.js';
|
import { FeedbackView } from "../views/FeedbackView.js";
|
||||||
|
|
||||||
export class CheatingDaddyApp extends LitElement {
|
export class CheatingDaddyApp extends LitElement {
|
||||||
static styles = css`
|
static styles = css`
|
||||||
@@ -81,15 +81,15 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.traffic-light.close {
|
.traffic-light.close {
|
||||||
background: #FF5F57;
|
background: #ff5f57;
|
||||||
}
|
}
|
||||||
|
|
||||||
.traffic-light.minimize {
|
.traffic-light.minimize {
|
||||||
background: #FEBC2E;
|
background: #febc2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.traffic-light.maximize {
|
.traffic-light.maximize {
|
||||||
background: #28C840;
|
background: #28c840;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
@@ -100,7 +100,10 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 42px 0 var(--space-md) 0;
|
padding: 42px 0 var(--space-md) 0;
|
||||||
transition: width var(--transition), min-width var(--transition), opacity var(--transition);
|
transition:
|
||||||
|
width var(--transition),
|
||||||
|
min-width var(--transition),
|
||||||
|
opacity var(--transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.hidden {
|
.sidebar.hidden {
|
||||||
@@ -144,7 +147,9 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color var(--transition), background var(--transition);
|
transition:
|
||||||
|
color var(--transition),
|
||||||
|
background var(--transition);
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -187,7 +192,9 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
transition: background var(--transition), border-color var(--transition);
|
transition:
|
||||||
|
background var(--transition),
|
||||||
|
border-color var(--transition);
|
||||||
animation: update-wobble 5s ease-in-out infinite;
|
animation: update-wobble 5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,11 +204,23 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes update-wobble {
|
@keyframes update-wobble {
|
||||||
0%, 90%, 100% { transform: rotate(0deg); }
|
0%,
|
||||||
92% { transform: rotate(-2deg); }
|
90%,
|
||||||
94% { transform: rotate(2deg); }
|
100% {
|
||||||
96% { transform: rotate(-1.5deg); }
|
transform: rotate(0deg);
|
||||||
98% { transform: rotate(1.5deg); }
|
}
|
||||||
|
92% {
|
||||||
|
transform: rotate(-2deg);
|
||||||
|
}
|
||||||
|
94% {
|
||||||
|
transform: rotate(2deg);
|
||||||
|
}
|
||||||
|
96% {
|
||||||
|
transform: rotate(-1.5deg);
|
||||||
|
}
|
||||||
|
98% {
|
||||||
|
transform: rotate(1.5deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-btn svg {
|
.update-btn svg {
|
||||||
@@ -363,20 +382,21 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
_storageLoaded: { state: true },
|
_storageLoaded: { state: true },
|
||||||
_updateAvailable: { state: true },
|
_updateAvailable: { state: true },
|
||||||
_whisperDownloading: { state: true },
|
_whisperDownloading: { state: true },
|
||||||
|
_whisperProgress: { state: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.currentView = 'main';
|
this.currentView = "main";
|
||||||
this.statusText = '';
|
this.statusText = "";
|
||||||
this.startTime = null;
|
this.startTime = null;
|
||||||
this.isRecording = false;
|
this.isRecording = false;
|
||||||
this.sessionActive = false;
|
this.sessionActive = false;
|
||||||
this.selectedProfile = 'interview';
|
this.selectedProfile = "interview";
|
||||||
this.selectedLanguage = 'en-US';
|
this.selectedLanguage = "en-US";
|
||||||
this.selectedScreenshotInterval = '5';
|
this.selectedScreenshotInterval = "5";
|
||||||
this.selectedImageQuality = 'medium';
|
this.selectedImageQuality = "medium";
|
||||||
this.layoutMode = 'normal';
|
this.layoutMode = "normal";
|
||||||
this.responses = [];
|
this.responses = [];
|
||||||
this.currentResponseIndex = -1;
|
this.currentResponseIndex = -1;
|
||||||
this._viewInstances = new Map();
|
this._viewInstances = new Map();
|
||||||
@@ -388,7 +408,8 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
this._timerInterval = null;
|
this._timerInterval = null;
|
||||||
this._updateAvailable = false;
|
this._updateAvailable = false;
|
||||||
this._whisperDownloading = false;
|
this._whisperDownloading = false;
|
||||||
this._localVersion = '';
|
this._whisperProgress = null;
|
||||||
|
this._localVersion = "";
|
||||||
|
|
||||||
this._loadFromStorage();
|
this._loadFromStorage();
|
||||||
this._checkForUpdates();
|
this._checkForUpdates();
|
||||||
@@ -399,16 +420,22 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
this._localVersion = await cheatingDaddy.getVersion();
|
this._localVersion = await cheatingDaddy.getVersion();
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
|
|
||||||
const res = await fetch('https://raw.githubusercontent.com/sohzm/cheating-daddy/refs/heads/master/package.json');
|
const res = await fetch(
|
||||||
|
"https://raw.githubusercontent.com/ShiftyX1/Mastermind/refs/heads/master/package.json",
|
||||||
|
);
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const remote = await res.json();
|
const remote = await res.json();
|
||||||
const remoteVersion = remote.version;
|
const remoteVersion = remote.version;
|
||||||
|
|
||||||
const toNum = v => v.split('.').map(Number);
|
const toNum = (v) => v.split(".").map(Number);
|
||||||
const [rMaj, rMin, rPatch] = toNum(remoteVersion);
|
const [rMaj, rMin, rPatch] = toNum(remoteVersion);
|
||||||
const [lMaj, lMin, lPatch] = toNum(this._localVersion);
|
const [lMaj, lMin, lPatch] = toNum(this._localVersion);
|
||||||
|
|
||||||
if (rMaj > lMaj || (rMaj === lMaj && rMin > lMin) || (rMaj === lMaj && rMin === lMin && rPatch > lPatch)) {
|
if (
|
||||||
|
rMaj > lMaj ||
|
||||||
|
(rMaj === lMaj && rMin > lMin) ||
|
||||||
|
(rMaj === lMaj && rMin === lMin && rPatch > lPatch)
|
||||||
|
) {
|
||||||
this._updateAvailable = true;
|
this._updateAvailable = true;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
@@ -421,20 +448,20 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
try {
|
try {
|
||||||
const [config, prefs] = await Promise.all([
|
const [config, prefs] = await Promise.all([
|
||||||
cheatingDaddy.storage.getConfig(),
|
cheatingDaddy.storage.getConfig(),
|
||||||
cheatingDaddy.storage.getPreferences()
|
cheatingDaddy.storage.getPreferences(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.currentView = config.onboarded ? 'main' : 'onboarding';
|
this.currentView = config.onboarded ? "main" : "onboarding";
|
||||||
this.selectedProfile = prefs.selectedProfile || 'interview';
|
this.selectedProfile = prefs.selectedProfile || "interview";
|
||||||
this.selectedLanguage = prefs.selectedLanguage || 'en-US';
|
this.selectedLanguage = prefs.selectedLanguage || "en-US";
|
||||||
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";
|
||||||
|
|
||||||
this._storageLoaded = true;
|
this._storageLoaded = true;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading from storage:', error);
|
console.error("Error loading from storage:", error);
|
||||||
this._storageLoaded = true;
|
this._storageLoaded = true;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
@@ -444,13 +471,27 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
ipcRenderer.on('new-response', (_, response) => this.addNewResponse(response));
|
ipcRenderer.on("new-response", (_, response) =>
|
||||||
ipcRenderer.on('update-response', (_, response) => this.updateCurrentResponse(response));
|
this.addNewResponse(response),
|
||||||
ipcRenderer.on('update-status', (_, status) => this.setStatus(status));
|
);
|
||||||
ipcRenderer.on('click-through-toggled', (_, isEnabled) => { this._isClickThrough = isEnabled; });
|
ipcRenderer.on("update-response", (_, response) =>
|
||||||
ipcRenderer.on('reconnect-failed', (_, data) => this.addNewResponse(data.message));
|
this.updateCurrentResponse(response),
|
||||||
ipcRenderer.on('whisper-downloading', (_, downloading) => { this._whisperDownloading = downloading; });
|
);
|
||||||
|
ipcRenderer.on("update-status", (_, status) => this.setStatus(status));
|
||||||
|
ipcRenderer.on("click-through-toggled", (_, isEnabled) => {
|
||||||
|
this._isClickThrough = isEnabled;
|
||||||
|
});
|
||||||
|
ipcRenderer.on("reconnect-failed", (_, data) =>
|
||||||
|
this.addNewResponse(data.message),
|
||||||
|
);
|
||||||
|
ipcRenderer.on("whisper-downloading", (_, downloading) => {
|
||||||
|
this._whisperDownloading = downloading;
|
||||||
|
if (!downloading) this._whisperProgress = null;
|
||||||
|
});
|
||||||
|
ipcRenderer.on("whisper-progress", (_, progress) => {
|
||||||
|
this._whisperProgress = progress;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,13 +499,14 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this._stopTimer();
|
this._stopTimer();
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
ipcRenderer.removeAllListeners('new-response');
|
ipcRenderer.removeAllListeners("new-response");
|
||||||
ipcRenderer.removeAllListeners('update-response');
|
ipcRenderer.removeAllListeners("update-response");
|
||||||
ipcRenderer.removeAllListeners('update-status');
|
ipcRenderer.removeAllListeners("update-status");
|
||||||
ipcRenderer.removeAllListeners('click-through-toggled');
|
ipcRenderer.removeAllListeners("click-through-toggled");
|
||||||
ipcRenderer.removeAllListeners('reconnect-failed');
|
ipcRenderer.removeAllListeners("reconnect-failed");
|
||||||
ipcRenderer.removeAllListeners('whisper-downloading');
|
ipcRenderer.removeAllListeners("whisper-downloading");
|
||||||
|
ipcRenderer.removeAllListeners("whisper-progress");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,12 +527,12 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getElapsedTime() {
|
getElapsedTime() {
|
||||||
if (!this.startTime) return '0:00';
|
if (!this.startTime) return "0:00";
|
||||||
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
||||||
const h = Math.floor(elapsed / 3600);
|
const h = Math.floor(elapsed / 3600);
|
||||||
const m = Math.floor((elapsed % 3600) / 60);
|
const m = Math.floor((elapsed % 3600) / 60);
|
||||||
const s = elapsed % 60;
|
const s = elapsed % 60;
|
||||||
const pad = n => String(n).padStart(2, '0');
|
const pad = (n) => String(n).padStart(2, "0");
|
||||||
if (h > 0) return `${h}:${pad(m)}:${pad(s)}`;
|
if (h > 0) return `${h}:${pad(m)}:${pad(s)}`;
|
||||||
return `${m}:${pad(s)}`;
|
return `${m}:${pad(s)}`;
|
||||||
}
|
}
|
||||||
@@ -499,7 +541,11 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
|
|
||||||
setStatus(text) {
|
setStatus(text) {
|
||||||
this.statusText = text;
|
this.statusText = text;
|
||||||
if (text.includes('Ready') || text.includes('Listening') || text.includes('Error')) {
|
if (
|
||||||
|
text.includes("Ready") ||
|
||||||
|
text.includes("Listening") ||
|
||||||
|
text.includes("Error")
|
||||||
|
) {
|
||||||
this._currentResponseIsComplete = true;
|
this._currentResponseIsComplete = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -531,34 +577,34 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleClose() {
|
async handleClose() {
|
||||||
if (this.currentView === 'assistant') {
|
if (this.currentView === "assistant") {
|
||||||
cheatingDaddy.stopCapture();
|
cheatingDaddy.stopCapture();
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('close-session');
|
await ipcRenderer.invoke("close-session");
|
||||||
}
|
}
|
||||||
this.sessionActive = false;
|
this.sessionActive = false;
|
||||||
this._stopTimer();
|
this._stopTimer();
|
||||||
this.currentView = 'main';
|
this.currentView = "main";
|
||||||
} else {
|
} else {
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('quit-application');
|
await ipcRenderer.invoke("quit-application");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _handleMinimize() {
|
async _handleMinimize() {
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('window-minimize');
|
await ipcRenderer.invoke("window-minimize");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleHideToggle() {
|
async handleHideToggle() {
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('toggle-window-visibility');
|
await ipcRenderer.invoke("toggle-window-visibility");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,12 +612,12 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
|
|
||||||
async handleStart() {
|
async handleStart() {
|
||||||
const prefs = await cheatingDaddy.storage.getPreferences();
|
const prefs = await cheatingDaddy.storage.getPreferences();
|
||||||
const providerMode = prefs.providerMode || 'byok';
|
const providerMode = prefs.providerMode || "byok";
|
||||||
|
|
||||||
if (providerMode === 'local') {
|
if (providerMode === "local") {
|
||||||
const success = await cheatingDaddy.initializeLocal(this.selectedProfile);
|
const success = await cheatingDaddy.initializeLocal(this.selectedProfile);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
const mainView = this.shadowRoot.querySelector('main-view');
|
const mainView = this.shadowRoot.querySelector("main-view");
|
||||||
if (mainView && mainView.triggerApiKeyError) {
|
if (mainView && mainView.triggerApiKeyError) {
|
||||||
mainView.triggerApiKeyError();
|
mainView.triggerApiKeyError();
|
||||||
}
|
}
|
||||||
@@ -579,37 +625,49 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const apiKey = await cheatingDaddy.storage.getApiKey();
|
const apiKey = await cheatingDaddy.storage.getApiKey();
|
||||||
if (!apiKey || apiKey === '') {
|
if (!apiKey || apiKey === "") {
|
||||||
const mainView = this.shadowRoot.querySelector('main-view');
|
const mainView = this.shadowRoot.querySelector("main-view");
|
||||||
if (mainView && mainView.triggerApiKeyError) {
|
if (mainView && mainView.triggerApiKeyError) {
|
||||||
mainView.triggerApiKeyError();
|
mainView.triggerApiKeyError();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await cheatingDaddy.initializeGemini(this.selectedProfile, this.selectedLanguage);
|
await cheatingDaddy.initializeGemini(
|
||||||
|
this.selectedProfile,
|
||||||
|
this.selectedLanguage,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
cheatingDaddy.startCapture(this.selectedScreenshotInterval, this.selectedImageQuality);
|
cheatingDaddy.startCapture(
|
||||||
|
this.selectedScreenshotInterval,
|
||||||
|
this.selectedImageQuality,
|
||||||
|
);
|
||||||
this.responses = [];
|
this.responses = [];
|
||||||
this.currentResponseIndex = -1;
|
this.currentResponseIndex = -1;
|
||||||
this.startTime = Date.now();
|
this.startTime = Date.now();
|
||||||
this.sessionActive = true;
|
this.sessionActive = true;
|
||||||
this.currentView = 'assistant';
|
this.currentView = "assistant";
|
||||||
this._startTimer();
|
this._startTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleAPIKeyHelp() {
|
async handleAPIKeyHelp() {
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('open-external', 'https://cheatingdaddy.com/help/api-key');
|
await ipcRenderer.invoke(
|
||||||
|
"open-external",
|
||||||
|
"https://cheatingdaddy.com/help/api-key",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleGroqAPIKeyHelp() {
|
async handleGroqAPIKeyHelp() {
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('open-external', 'https://console.groq.com/keys');
|
await ipcRenderer.invoke(
|
||||||
|
"open-external",
|
||||||
|
"https://console.groq.com/keys",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,33 +675,39 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
|
|
||||||
async handleProfileChange(profile) {
|
async handleProfileChange(profile) {
|
||||||
this.selectedProfile = profile;
|
this.selectedProfile = profile;
|
||||||
await cheatingDaddy.storage.updatePreference('selectedProfile', profile);
|
await cheatingDaddy.storage.updatePreference("selectedProfile", profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleLanguageChange(language) {
|
async handleLanguageChange(language) {
|
||||||
this.selectedLanguage = language;
|
this.selectedLanguage = language;
|
||||||
await cheatingDaddy.storage.updatePreference('selectedLanguage', language);
|
await cheatingDaddy.storage.updatePreference("selectedLanguage", language);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleScreenshotIntervalChange(interval) {
|
async handleScreenshotIntervalChange(interval) {
|
||||||
this.selectedScreenshotInterval = interval;
|
this.selectedScreenshotInterval = interval;
|
||||||
await cheatingDaddy.storage.updatePreference('selectedScreenshotInterval', interval);
|
await cheatingDaddy.storage.updatePreference(
|
||||||
|
"selectedScreenshotInterval",
|
||||||
|
interval,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleImageQualityChange(quality) {
|
async handleImageQualityChange(quality) {
|
||||||
this.selectedImageQuality = quality;
|
this.selectedImageQuality = quality;
|
||||||
await cheatingDaddy.storage.updatePreference('selectedImageQuality', quality);
|
await cheatingDaddy.storage.updatePreference(
|
||||||
|
"selectedImageQuality",
|
||||||
|
quality,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleLayoutModeChange(layoutMode) {
|
async handleLayoutModeChange(layoutMode) {
|
||||||
this.layoutMode = layoutMode;
|
this.layoutMode = layoutMode;
|
||||||
await cheatingDaddy.storage.updateConfig('layout', layoutMode);
|
await cheatingDaddy.storage.updateConfig("layout", layoutMode);
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
try {
|
try {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('update-sizes');
|
await ipcRenderer.invoke("update-sizes");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update sizes:', error);
|
console.error("Failed to update sizes:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
@@ -651,17 +715,27 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
|
|
||||||
async handleExternalLinkClick(url) {
|
async handleExternalLinkClick(url) {
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('open-external', url);
|
await ipcRenderer.invoke("open-external", url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSendText(message) {
|
async handleSendText(message) {
|
||||||
const result = await window.cheatingDaddy.sendTextMessage(message);
|
const result = await window.cheatingDaddy.sendTextMessage(message);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
this.setStatus('Error sending message: ' + result.error);
|
this.setStatus("Error sending message: " + result.error);
|
||||||
} else {
|
} else {
|
||||||
this.setStatus('Message sent...');
|
this.setStatus("Message sent...");
|
||||||
|
this._awaitingNewResponse = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleExpandResponse() {
|
||||||
|
const result = await window.cheatingDaddy.expandLastResponse();
|
||||||
|
if (!result.success) {
|
||||||
|
this.setStatus("Error expanding: " + (result.error || "Unknown error"));
|
||||||
|
} else {
|
||||||
|
this.setStatus("Expanding response...");
|
||||||
this._awaitingNewResponse = true;
|
this._awaitingNewResponse = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -673,29 +747,29 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleOnboardingComplete() {
|
handleOnboardingComplete() {
|
||||||
this.currentView = 'main';
|
this.currentView = "main";
|
||||||
}
|
}
|
||||||
|
|
||||||
updated(changedProperties) {
|
updated(changedProperties) {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
if (changedProperties.has('currentView') && window.require) {
|
if (changedProperties.has("currentView") && window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
ipcRenderer.send('view-changed', this.currentView);
|
ipcRenderer.send("view-changed", this.currentView);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ──
|
// ── Helpers ──
|
||||||
|
|
||||||
_isLiveMode() {
|
_isLiveMode() {
|
||||||
return this.currentView === 'assistant';
|
return this.currentView === "assistant";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Render ──
|
// ── Render ──
|
||||||
|
|
||||||
renderCurrentView() {
|
renderCurrentView() {
|
||||||
switch (this.currentView) {
|
switch (this.currentView) {
|
||||||
case 'onboarding':
|
case "onboarding":
|
||||||
return html`
|
return html`
|
||||||
<onboarding-view
|
<onboarding-view
|
||||||
.onComplete=${() => this.handleOnboardingComplete()}
|
.onComplete=${() => this.handleOnboardingComplete()}
|
||||||
@@ -703,26 +777,27 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
></onboarding-view>
|
></onboarding-view>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
case 'main':
|
case "main":
|
||||||
return html`
|
return html`
|
||||||
<main-view
|
<main-view
|
||||||
.selectedProfile=${this.selectedProfile}
|
.selectedProfile=${this.selectedProfile}
|
||||||
.onProfileChange=${p => this.handleProfileChange(p)}
|
.onProfileChange=${(p) => this.handleProfileChange(p)}
|
||||||
.onStart=${() => this.handleStart()}
|
.onStart=${() => this.handleStart()}
|
||||||
.onExternalLink=${url => this.handleExternalLinkClick(url)}
|
.onExternalLink=${(url) => this.handleExternalLinkClick(url)}
|
||||||
.whisperDownloading=${this._whisperDownloading}
|
.whisperDownloading=${this._whisperDownloading}
|
||||||
|
.whisperProgress=${this._whisperProgress}
|
||||||
></main-view>
|
></main-view>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
case 'ai-customize':
|
case "ai-customize":
|
||||||
return html`
|
return html`
|
||||||
<ai-customize-view
|
<ai-customize-view
|
||||||
.selectedProfile=${this.selectedProfile}
|
.selectedProfile=${this.selectedProfile}
|
||||||
.onProfileChange=${p => this.handleProfileChange(p)}
|
.onProfileChange=${(p) => this.handleProfileChange(p)}
|
||||||
></ai-customize-view>
|
></ai-customize-view>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
case 'customize':
|
case "customize":
|
||||||
return html`
|
return html`
|
||||||
<customize-view
|
<customize-view
|
||||||
.selectedProfile=${this.selectedProfile}
|
.selectedProfile=${this.selectedProfile}
|
||||||
@@ -730,30 +805,34 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
.selectedScreenshotInterval=${this.selectedScreenshotInterval}
|
.selectedScreenshotInterval=${this.selectedScreenshotInterval}
|
||||||
.selectedImageQuality=${this.selectedImageQuality}
|
.selectedImageQuality=${this.selectedImageQuality}
|
||||||
.layoutMode=${this.layoutMode}
|
.layoutMode=${this.layoutMode}
|
||||||
.onProfileChange=${p => this.handleProfileChange(p)}
|
.onProfileChange=${(p) => this.handleProfileChange(p)}
|
||||||
.onLanguageChange=${l => this.handleLanguageChange(l)}
|
.onLanguageChange=${(l) => this.handleLanguageChange(l)}
|
||||||
.onScreenshotIntervalChange=${i => this.handleScreenshotIntervalChange(i)}
|
.onScreenshotIntervalChange=${(i) =>
|
||||||
.onImageQualityChange=${q => this.handleImageQualityChange(q)}
|
this.handleScreenshotIntervalChange(i)}
|
||||||
.onLayoutModeChange=${lm => this.handleLayoutModeChange(lm)}
|
.onImageQualityChange=${(q) => this.handleImageQualityChange(q)}
|
||||||
|
.onLayoutModeChange=${(lm) => this.handleLayoutModeChange(lm)}
|
||||||
></customize-view>
|
></customize-view>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
case 'feedback':
|
case "feedback":
|
||||||
return html`<feedback-view></feedback-view>`;
|
return html`<feedback-view></feedback-view>`;
|
||||||
|
|
||||||
case 'help':
|
case "help":
|
||||||
return html`<help-view .onExternalLinkClick=${url => this.handleExternalLinkClick(url)}></help-view>`;
|
return html`<help-view
|
||||||
|
.onExternalLinkClick=${(url) => this.handleExternalLinkClick(url)}
|
||||||
|
></help-view>`;
|
||||||
|
|
||||||
case 'history':
|
case "history":
|
||||||
return html`<history-view></history-view>`;
|
return html`<history-view></history-view>`;
|
||||||
|
|
||||||
case 'assistant':
|
case "assistant":
|
||||||
return html`
|
return html`
|
||||||
<assistant-view
|
<assistant-view
|
||||||
.responses=${this.responses}
|
.responses=${this.responses}
|
||||||
.currentResponseIndex=${this.currentResponseIndex}
|
.currentResponseIndex=${this.currentResponseIndex}
|
||||||
.selectedProfile=${this.selectedProfile}
|
.selectedProfile=${this.selectedProfile}
|
||||||
.onSendText=${msg => this.handleSendText(msg)}
|
.onSendText=${(msg) => this.handleSendText(msg)}
|
||||||
|
.onExpandResponse=${() => this.handleExpandResponse()}
|
||||||
.shouldAnimateResponse=${this.shouldAnimateResponse}
|
.shouldAnimateResponse=${this.shouldAnimateResponse}
|
||||||
@response-index-changed=${this.handleResponseIndexChanged}
|
@response-index-changed=${this.handleResponseIndexChanged}
|
||||||
@response-animation-complete=${() => {
|
@response-animation-complete=${() => {
|
||||||
@@ -771,74 +850,238 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
|
|
||||||
renderSidebar() {
|
renderSidebar() {
|
||||||
const items = [
|
const items = [
|
||||||
{ id: 'main', label: 'Home', icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="m19 8.71l-5.333-4.148a2.666 2.666 0 0 0-3.274 0L5.059 8.71a2.67 2.67 0 0 0-1.029 2.105v7.2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.2c0-.823-.38-1.6-1.03-2.105"/><path d="M16 15c-2.21 1.333-5.792 1.333-8 0"/></g></svg>` },
|
{
|
||||||
{ id: 'ai-customize', label: 'AI Customization', icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 3v7h6l-8 11v-7H5z" /></svg>` },
|
id: "main",
|
||||||
{ id: 'history', label: 'History', icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M10 20.777a9 9 0 0 1-2.48-.969M14 3.223a9.003 9.003 0 0 1 0 17.554m-9.421-3.684a9 9 0 0 1-1.227-2.592M3.124 10.5c.16-.95.468-1.85.9-2.675l.169-.305m2.714-2.941A9 9 0 0 1 10 3.223"/><path d="M12 8v4l3 3"/></g></svg>` },
|
label: "Home",
|
||||||
{ id: 'customize', label: 'Settings', icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M19.875 6.27A2.23 2.23 0 0 1 21 8.218v7.284c0 .809-.443 1.555-1.158 1.948l-6.75 4.27a2.27 2.27 0 0 1-2.184 0l-6.75-4.27A2.23 2.23 0 0 1 3 15.502V8.217c0-.809.443-1.554 1.158-1.947l6.75-3.98a2.33 2.33 0 0 1 2.25 0l6.75 3.98z"/><path d="M9 12a3 3 0 1 0 6 0a3 3 0 1 0-6 0"/></g></svg>` },
|
icon: html`<svg
|
||||||
{ id: 'feedback', label: 'Feedback', icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-5l-5 3v-3H6a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3zM9.5 9h.01m4.99 0h.01"/><path d="M9.5 13a3.5 3.5 0 0 0 5 0"/></g></svg>` },
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{ id: 'help', label: 'Help', icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M12 3c7.2 0 9 1.8 9 9s-1.8 9-9 9s-9-1.8-9-9s1.8-9 9-9m0 13v.01"/><path d="M12 13a2 2 0 0 0 .914-3.782a1.98 1.98 0 0 0-2.414.483"/></g></svg>` },
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m19 8.71l-5.333-4.148a2.666 2.666 0 0 0-3.274 0L5.059 8.71a2.67 2.67 0 0 0-1.029 2.105v7.2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.2c0-.823-.38-1.6-1.03-2.105"
|
||||||
|
/>
|
||||||
|
<path d="M16 15c-2.21 1.333-5.792 1.333-8 0" />
|
||||||
|
</g>
|
||||||
|
</svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ai-customize",
|
||||||
|
label: "AI Customization",
|
||||||
|
icon: html`<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 3v7h6l-8 11v-7H5z"
|
||||||
|
/>
|
||||||
|
</svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "history",
|
||||||
|
label: "History",
|
||||||
|
icon: html`<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10 20.777a9 9 0 0 1-2.48-.969M14 3.223a9.003 9.003 0 0 1 0 17.554m-9.421-3.684a9 9 0 0 1-1.227-2.592M3.124 10.5c.16-.95.468-1.85.9-2.675l.169-.305m2.714-2.941A9 9 0 0 1 10 3.223"
|
||||||
|
/>
|
||||||
|
<path d="M12 8v4l3 3" />
|
||||||
|
</g>
|
||||||
|
</svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "customize",
|
||||||
|
label: "Settings",
|
||||||
|
icon: html`<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M19.875 6.27A2.23 2.23 0 0 1 21 8.218v7.284c0 .809-.443 1.555-1.158 1.948l-6.75 4.27a2.27 2.27 0 0 1-2.184 0l-6.75-4.27A2.23 2.23 0 0 1 3 15.502V8.217c0-.809.443-1.554 1.158-1.947l6.75-3.98a2.33 2.33 0 0 1 2.25 0l6.75 3.98z"
|
||||||
|
/>
|
||||||
|
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 1 0-6 0" />
|
||||||
|
</g>
|
||||||
|
</svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "feedback",
|
||||||
|
label: "Feedback",
|
||||||
|
icon: html`<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-5l-5 3v-3H6a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3zM9.5 9h.01m4.99 0h.01"
|
||||||
|
/>
|
||||||
|
<path d="M9.5 13a3.5 3.5 0 0 0 5 0" />
|
||||||
|
</g>
|
||||||
|
</svg>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "help",
|
||||||
|
label: "Help",
|
||||||
|
icon: html`<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 3c7.2 0 9 1.8 9 9s-1.8 9-9 9s-9-1.8-9-9s1.8-9 9-9m0 13v.01"
|
||||||
|
/>
|
||||||
|
<path d="M12 13a2 2 0 0 0 .914-3.782a1.98 1.98 0 0 0-2.414.483" />
|
||||||
|
</g>
|
||||||
|
</svg>`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="sidebar ${this._isLiveMode() ? 'hidden' : ''}">
|
<div class="sidebar ${this._isLiveMode() ? "hidden" : ""}">
|
||||||
<div class="sidebar-brand">
|
<div class="sidebar-brand">
|
||||||
<h1>Cheating Daddy</h1>
|
<h1>Mastermind</h1>
|
||||||
</div>
|
</div>
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
${items.map(item => html`
|
${items.map(
|
||||||
|
(item) => html`
|
||||||
<button
|
<button
|
||||||
class="nav-item ${this.currentView === item.id ? 'active' : ''}"
|
class="nav-item ${this.currentView === item.id ? "active" : ""}"
|
||||||
@click=${() => this.navigate(item.id)}
|
@click=${() => this.navigate(item.id)}
|
||||||
title=${item.label}
|
title=${item.label}
|
||||||
>
|
>
|
||||||
${item.icon}
|
${item.icon} ${item.label}
|
||||||
${item.label}
|
|
||||||
</button>
|
</button>
|
||||||
`)}
|
`,
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
${this._updateAvailable ? html`
|
${this._updateAvailable
|
||||||
<button class="update-btn" @click=${() => this.handleExternalLinkClick('https://cheatingdaddy.com/download')}>
|
? html`
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2M7 11l5 5l5-5m-5-7v12" /></svg>
|
<button
|
||||||
|
class="update-btn"
|
||||||
|
@click=${() =>
|
||||||
|
this.handleExternalLinkClick(
|
||||||
|
"https://cheatingdaddy.com/download",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2M7 11l5 5l5-5m-5-7v12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
Update available
|
Update available
|
||||||
</button>
|
</button>
|
||||||
` : html`
|
`
|
||||||
<div class="version-text">v${this._localVersion}</div>
|
: html` <div class="version-text">v${this._localVersion}</div> `}
|
||||||
`}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLiveBar() {
|
renderLiveBar() {
|
||||||
if (!this._isLiveMode()) return '';
|
if (!this._isLiveMode()) return "";
|
||||||
|
|
||||||
const profileLabels = {
|
const profileLabels = {
|
||||||
interview: 'Interview',
|
interview: "Interview",
|
||||||
sales: 'Sales Call',
|
sales: "Sales Call",
|
||||||
meeting: 'Meeting',
|
meeting: "Meeting",
|
||||||
presentation: 'Presentation',
|
presentation: "Presentation",
|
||||||
negotiation: 'Negotiation',
|
negotiation: "Negotiation",
|
||||||
exam: 'Exam',
|
exam: "Exam",
|
||||||
};
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="live-bar">
|
<div class="live-bar">
|
||||||
<div class="live-bar-left">
|
<div class="live-bar-left">
|
||||||
<button class="live-bar-back" @click=${() => this.handleClose()} title="End session">
|
<button
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
class="live-bar-back"
|
||||||
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 0 1-.02 1.06L8.832 10l3.938 3.71a.75.75 0 1 1-1.04 1.08l-4.5-4.25a.75.75 0 0 1 0-1.08l4.5-4.25a.75.75 0 0 1 1.06.02Z" clip-rule="evenodd" />
|
@click=${() => this.handleClose()}
|
||||||
|
title="End session"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M12.79 5.23a.75.75 0 0 1-.02 1.06L8.832 10l3.938 3.71a.75.75 0 1 1-1.04 1.08l-4.5-4.25a.75.75 0 0 1 0-1.08l4.5-4.25a.75.75 0 0 1 1.06.02Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="live-bar-center">
|
<div class="live-bar-center">
|
||||||
${profileLabels[this.selectedProfile] || 'Session'}
|
${profileLabels[this.selectedProfile] || "Session"}
|
||||||
</div>
|
</div>
|
||||||
<div class="live-bar-right">
|
<div class="live-bar-right">
|
||||||
${this.statusText ? html`<span class="live-bar-text">${this.statusText}</span>` : ''}
|
${this.statusText
|
||||||
|
? html`<span class="live-bar-text">${this.statusText}</span>`
|
||||||
|
: ""}
|
||||||
<span class="live-bar-text">${this.getElapsedTime()}</span>
|
<span class="live-bar-text">${this.getElapsedTime()}</span>
|
||||||
${this._isClickThrough ? html`<span class="live-bar-text">[click through]</span>` : ''}
|
${this._isClickThrough
|
||||||
<span class="live-bar-text clickable" @click=${() => this.handleHideToggle()}>[hide]</span>
|
? html`<span class="live-bar-text">[click through]</span>`
|
||||||
|
: ""}
|
||||||
|
<span
|
||||||
|
class="live-bar-text clickable"
|
||||||
|
@click=${() => this.handleHideToggle()}
|
||||||
|
>[hide]</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -846,30 +1089,34 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
// Onboarding is fullscreen, no sidebar
|
// Onboarding is fullscreen, no sidebar
|
||||||
if (this.currentView === 'onboarding') {
|
if (this.currentView === "onboarding") {
|
||||||
return html`
|
return html` <div class="fullscreen">${this.renderCurrentView()}</div> `;
|
||||||
<div class="fullscreen">
|
|
||||||
${this.renderCurrentView()}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLive = this._isLiveMode();
|
const isLive = this._isLiveMode();
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
<div class="top-drag-bar ${isLive ? 'hidden' : ''}">
|
<div class="top-drag-bar ${isLive ? "hidden" : ""}">
|
||||||
<div class="traffic-lights">
|
<div class="traffic-lights">
|
||||||
<button class="traffic-light close" @click=${() => this.handleClose()} title="Close"></button>
|
<button
|
||||||
<button class="traffic-light minimize" @click=${() => this._handleMinimize()} title="Minimize"></button>
|
class="traffic-light close"
|
||||||
|
@click=${() => this.handleClose()}
|
||||||
|
title="Close"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
class="traffic-light minimize"
|
||||||
|
@click=${() => this._handleMinimize()}
|
||||||
|
title="Minimize"
|
||||||
|
></button>
|
||||||
<button class="traffic-light maximize" title="Maximize"></button>
|
<button class="traffic-light maximize" title="Maximize"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="drag-region"></div>
|
<div class="drag-region"></div>
|
||||||
</div>
|
</div>
|
||||||
${this.renderSidebar()}
|
${this.renderSidebar()}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
${isLive ? this.renderLiveBar() : ''}
|
${isLive ? this.renderLiveBar() : ""}
|
||||||
<div class="content-inner ${isLive ? 'live' : ''}">
|
<div class="content-inner ${isLive ? "live" : ""}">
|
||||||
${this.renderCurrentView()}
|
${this.renderCurrentView()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -878,4 +1125,4 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('cheating-daddy-app', CheatingDaddyApp);
|
customElements.define("cheating-daddy-app", CheatingDaddyApp);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
|
import { html, css, LitElement } from "../../assets/lit-core-2.7.4.min.js";
|
||||||
|
|
||||||
export class AssistantView extends LitElement {
|
export class AssistantView extends LitElement {
|
||||||
static styles = css`
|
static styles = css`
|
||||||
@@ -54,12 +54,22 @@ export class AssistantView extends LitElement {
|
|||||||
font-weight: var(--font-weight-semibold);
|
font-weight: var(--font-weight-semibold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.response-container h1 { font-size: 1.5em; }
|
.response-container h1 {
|
||||||
.response-container h2 { font-size: 1.3em; }
|
font-size: 1.5em;
|
||||||
.response-container h3 { font-size: 1.15em; }
|
}
|
||||||
.response-container h4 { font-size: 1.05em; }
|
.response-container h2 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
.response-container h3 {
|
||||||
|
font-size: 1.15em;
|
||||||
|
}
|
||||||
|
.response-container h4 {
|
||||||
|
font-size: 1.05em;
|
||||||
|
}
|
||||||
.response-container h5,
|
.response-container h5,
|
||||||
.response-container h6 { font-size: 1em; }
|
.response-container h6 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.response-container p {
|
.response-container p {
|
||||||
margin: 0.6em 0;
|
margin: 0.6em 0;
|
||||||
@@ -100,11 +110,220 @@ export class AssistantView extends LitElement {
|
|||||||
padding: var(--space-md);
|
padding: var(--space-md);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
margin: 0.8em 0;
|
margin: 0.8em 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container pre::before {
|
||||||
|
content: attr(data-language);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-top: none;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom-left-radius: var(--radius-sm);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.response-container pre code {
|
.response-container pre code {
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Syntax highlighting for code blocks ── */
|
||||||
|
/* Default (Dark theme) */
|
||||||
|
.response-container .hljs {
|
||||||
|
color: #c9d1d9;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-doctag,
|
||||||
|
.response-container .hljs-keyword,
|
||||||
|
.response-container .hljs-meta .hljs-keyword,
|
||||||
|
.response-container .hljs-template-tag,
|
||||||
|
.response-container .hljs-template-variable,
|
||||||
|
.response-container .hljs-type,
|
||||||
|
.response-container .hljs-variable.language_ {
|
||||||
|
color: #ff7b72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-title,
|
||||||
|
.response-container .hljs-title.class_,
|
||||||
|
.response-container .hljs-title.class_.inherited__,
|
||||||
|
.response-container .hljs-title.function_ {
|
||||||
|
color: #d2a8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-attr,
|
||||||
|
.response-container .hljs-attribute,
|
||||||
|
.response-container .hljs-literal,
|
||||||
|
.response-container .hljs-meta,
|
||||||
|
.response-container .hljs-number,
|
||||||
|
.response-container .hljs-operator,
|
||||||
|
.response-container .hljs-selector-attr,
|
||||||
|
.response-container .hljs-selector-class,
|
||||||
|
.response-container .hljs-selector-id,
|
||||||
|
.response-container .hljs-variable {
|
||||||
|
color: #79c0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-meta .hljs-string,
|
||||||
|
.response-container .hljs-regexp,
|
||||||
|
.response-container .hljs-string {
|
||||||
|
color: #a5d6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-built_in,
|
||||||
|
.response-container .hljs-symbol {
|
||||||
|
color: #ffa657;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-code,
|
||||||
|
.response-container .hljs-comment,
|
||||||
|
.response-container .hljs-formula {
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-name,
|
||||||
|
.response-container .hljs-quote,
|
||||||
|
.response-container .hljs-selector-pseudo,
|
||||||
|
.response-container .hljs-selector-tag {
|
||||||
|
color: #7ee787;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-subst {
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-section {
|
||||||
|
color: #1f6feb;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-bullet {
|
||||||
|
color: #f2cc60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-emphasis {
|
||||||
|
color: #c9d1d9;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-strong {
|
||||||
|
color: #c9d1d9;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-addition {
|
||||||
|
color: #aff5b4;
|
||||||
|
background-color: #033a16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container .hljs-deletion {
|
||||||
|
color: #ffdcd7;
|
||||||
|
background-color: #67060c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light theme syntax highlighting */
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs {
|
||||||
|
color: #24292f;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-doctag,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-keyword,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-meta .hljs-keyword,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-template-tag,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-template-variable,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-type,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-variable.language_ {
|
||||||
|
color: #cf222e;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-title,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-title.class_,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-title.class_.inherited__,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-title.function_ {
|
||||||
|
color: #8250df;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-attr,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-attribute,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-literal,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-meta,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-number,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-operator,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-selector-attr,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-selector-class,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-selector-id,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-variable {
|
||||||
|
color: #0550ae;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-meta .hljs-string,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-regexp,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-string {
|
||||||
|
color: #0a3069;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-built_in,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-symbol {
|
||||||
|
color: #953800;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-code,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-comment,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-formula {
|
||||||
|
color: #6e7781;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-name,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-quote,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-selector-pseudo,
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-selector-tag {
|
||||||
|
color: #116329;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-subst {
|
||||||
|
color: #24292f;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-section {
|
||||||
|
color: #0969da;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-bullet {
|
||||||
|
color: #953800;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-emphasis {
|
||||||
|
color: #24292f;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-strong {
|
||||||
|
color: #24292f;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-addition {
|
||||||
|
color: #116329;
|
||||||
|
background-color: #dafbe1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(body[data-theme-type="light"]) .response-container .hljs-deletion {
|
||||||
|
color: #82071e;
|
||||||
|
background-color: #ffebe9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.response-container a {
|
.response-container a {
|
||||||
@@ -263,7 +482,9 @@ export class AssistantView extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
transition: border-color 0.4s ease, background var(--transition);
|
transition:
|
||||||
|
border-color 0.4s ease,
|
||||||
|
background var(--transition);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -298,6 +519,52 @@ export class AssistantView extends LitElement {
|
|||||||
height: calc(100% + 2px);
|
height: calc(100% + 2px);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Expand button ── */
|
||||||
|
|
||||||
|
.expand-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--space-xs) var(--space-md);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
background: var(--bg-app);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
padding: var(--space-xs) var(--space-md);
|
||||||
|
border-radius: 100px;
|
||||||
|
height: 26px;
|
||||||
|
transition:
|
||||||
|
color var(--transition),
|
||||||
|
border-color var(--transition),
|
||||||
|
background var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-btn:hover:not(:disabled) {
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-btn svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
static properties = {
|
static properties = {
|
||||||
@@ -305,28 +572,32 @@ export class AssistantView extends LitElement {
|
|||||||
currentResponseIndex: { type: Number },
|
currentResponseIndex: { type: Number },
|
||||||
selectedProfile: { type: String },
|
selectedProfile: { type: String },
|
||||||
onSendText: { type: Function },
|
onSendText: { type: Function },
|
||||||
|
onExpandResponse: { type: Function },
|
||||||
shouldAnimateResponse: { type: Boolean },
|
shouldAnimateResponse: { type: Boolean },
|
||||||
isAnalyzing: { type: Boolean, state: true },
|
isAnalyzing: { type: Boolean, state: true },
|
||||||
|
isExpanding: { type: Boolean, state: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.responses = [];
|
this.responses = [];
|
||||||
this.currentResponseIndex = -1;
|
this.currentResponseIndex = -1;
|
||||||
this.selectedProfile = 'interview';
|
this.selectedProfile = "interview";
|
||||||
this.onSendText = () => {};
|
this.onSendText = () => {};
|
||||||
|
this.onExpandResponse = () => {};
|
||||||
this.isAnalyzing = false;
|
this.isAnalyzing = false;
|
||||||
|
this.isExpanding = false;
|
||||||
this._animFrame = null;
|
this._animFrame = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getProfileNames() {
|
getProfileNames() {
|
||||||
return {
|
return {
|
||||||
interview: 'Job Interview',
|
interview: "Job Interview",
|
||||||
sales: 'Sales Call',
|
sales: "Sales Call",
|
||||||
meeting: 'Business Meeting',
|
meeting: "Business Meeting",
|
||||||
presentation: 'Presentation',
|
presentation: "Presentation",
|
||||||
negotiation: 'Negotiation',
|
negotiation: "Negotiation",
|
||||||
exam: 'Exam Assistant',
|
exam: "Exam Assistant",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,22 +605,45 @@ export class AssistantView extends LitElement {
|
|||||||
const profileNames = this.getProfileNames();
|
const profileNames = this.getProfileNames();
|
||||||
return this.responses.length > 0 && this.currentResponseIndex >= 0
|
return this.responses.length > 0 && this.currentResponseIndex >= 0
|
||||||
? this.responses[this.currentResponseIndex]
|
? this.responses[this.currentResponseIndex]
|
||||||
: `Listening to your ${profileNames[this.selectedProfile] || 'session'}...`;
|
: `Listening to your ${profileNames[this.selectedProfile] || "session"}...`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMarkdown(content) {
|
renderMarkdown(content) {
|
||||||
if (typeof window !== 'undefined' && window.marked) {
|
if (typeof window !== "undefined" && window.marked) {
|
||||||
try {
|
try {
|
||||||
|
// Configure marked to use highlight.js for syntax highlighting
|
||||||
window.marked.setOptions({
|
window.marked.setOptions({
|
||||||
breaks: true,
|
breaks: true,
|
||||||
gfm: true,
|
gfm: true,
|
||||||
sanitize: false,
|
sanitize: false,
|
||||||
|
highlight: (code, lang) => {
|
||||||
|
if (window.hljs && lang) {
|
||||||
|
try {
|
||||||
|
return window.hljs.highlight(code, { language: lang }).value;
|
||||||
|
} catch (e) {
|
||||||
|
// If language is not recognized, try auto-detection
|
||||||
|
try {
|
||||||
|
return window.hljs.highlightAuto(code).value;
|
||||||
|
} catch (err) {
|
||||||
|
return window.hljs.escapeHtml(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (window.hljs) {
|
||||||
|
// Auto-detect language if not specified
|
||||||
|
try {
|
||||||
|
return window.hljs.highlightAuto(code).value;
|
||||||
|
} catch (e) {
|
||||||
|
return window.hljs.escapeHtml(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
let rendered = window.marked.parse(content);
|
let rendered = window.marked.parse(content);
|
||||||
rendered = this.wrapWordsInSpans(rendered);
|
rendered = this.wrapWordsInSpans(rendered);
|
||||||
return rendered;
|
return rendered;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error parsing markdown:', error);
|
console.warn("Error parsing markdown:", error);
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,17 +652,21 @@ export class AssistantView extends LitElement {
|
|||||||
|
|
||||||
wrapWordsInSpans(html) {
|
wrapWordsInSpans(html) {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
const doc = parser.parseFromString(html, "text/html");
|
||||||
const tagsToSkip = ['PRE'];
|
const tagsToSkip = ["PRE", "CODE"];
|
||||||
|
|
||||||
function wrap(node) {
|
function wrap(node) {
|
||||||
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() && !tagsToSkip.includes(node.parentNode.tagName)) {
|
if (
|
||||||
|
node.nodeType === Node.TEXT_NODE &&
|
||||||
|
node.textContent.trim() &&
|
||||||
|
!tagsToSkip.includes(node.parentNode.tagName)
|
||||||
|
) {
|
||||||
const words = node.textContent.split(/(\s+)/);
|
const words = node.textContent.split(/(\s+)/);
|
||||||
const frag = document.createDocumentFragment();
|
const frag = document.createDocumentFragment();
|
||||||
words.forEach(word => {
|
words.forEach((word) => {
|
||||||
if (word.trim()) {
|
if (word.trim()) {
|
||||||
const span = document.createElement('span');
|
const span = document.createElement("span");
|
||||||
span.setAttribute('data-word', '');
|
span.setAttribute("data-word", "");
|
||||||
span.textContent = word;
|
span.textContent = word;
|
||||||
frag.appendChild(span);
|
frag.appendChild(span);
|
||||||
} else {
|
} else {
|
||||||
@@ -376,7 +674,10 @@ export class AssistantView extends LitElement {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
node.parentNode.replaceChild(frag, node);
|
node.parentNode.replaceChild(frag, node);
|
||||||
} else if (node.nodeType === Node.ELEMENT_NODE && !tagsToSkip.includes(node.tagName)) {
|
} else if (
|
||||||
|
node.nodeType === Node.ELEMENT_NODE &&
|
||||||
|
!tagsToSkip.includes(node.tagName)
|
||||||
|
) {
|
||||||
Array.from(node.childNodes).forEach(wrap);
|
Array.from(node.childNodes).forEach(wrap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,13 +685,56 @@ export class AssistantView extends LitElement {
|
|||||||
return doc.body.innerHTML;
|
return doc.body.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyCodeHighlighting(container) {
|
||||||
|
if (!window.hljs) return;
|
||||||
|
|
||||||
|
// Find all code blocks in the rendered content
|
||||||
|
const codeBlocks = container.querySelectorAll("pre code");
|
||||||
|
|
||||||
|
codeBlocks.forEach((block) => {
|
||||||
|
const pre = block.parentElement;
|
||||||
|
if (!pre || pre.tagName !== "PRE") return;
|
||||||
|
|
||||||
|
// Skip if already highlighted
|
||||||
|
if (block.classList.contains("hljs")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = block.textContent;
|
||||||
|
let lang = block.className.replace(/language-|lang-/, "") || "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (lang && window.hljs.getLanguage(lang)) {
|
||||||
|
block.innerHTML = window.hljs.highlight(code, { language: lang }).value;
|
||||||
|
} else {
|
||||||
|
// Auto-detect language
|
||||||
|
const result = window.hljs.highlightAuto(code);
|
||||||
|
block.innerHTML = result.value;
|
||||||
|
if (result.language && !lang) {
|
||||||
|
lang = result.language;
|
||||||
|
block.className = `language-${lang}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
block.classList.add("hljs");
|
||||||
|
|
||||||
|
// Set data-language attribute on pre tag for display
|
||||||
|
if (lang) {
|
||||||
|
pre.setAttribute("data-language", lang);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error highlighting code block:", e);
|
||||||
|
// Leave block as-is if highlighting fails
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
navigateToPreviousResponse() {
|
navigateToPreviousResponse() {
|
||||||
if (this.currentResponseIndex > 0) {
|
if (this.currentResponseIndex > 0) {
|
||||||
this.currentResponseIndex--;
|
this.currentResponseIndex--;
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('response-index-changed', {
|
new CustomEvent("response-index-changed", {
|
||||||
detail: { index: this.currentResponseIndex },
|
detail: { index: this.currentResponseIndex },
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
@@ -400,16 +744,16 @@ export class AssistantView extends LitElement {
|
|||||||
if (this.currentResponseIndex < this.responses.length - 1) {
|
if (this.currentResponseIndex < this.responses.length - 1) {
|
||||||
this.currentResponseIndex++;
|
this.currentResponseIndex++;
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('response-index-changed', {
|
new CustomEvent("response-index-changed", {
|
||||||
detail: { index: this.currentResponseIndex },
|
detail: { index: this.currentResponseIndex },
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollResponseUp() {
|
scrollResponseUp() {
|
||||||
const container = this.shadowRoot.querySelector('.response-container');
|
const container = this.shadowRoot.querySelector(".response-container");
|
||||||
if (container) {
|
if (container) {
|
||||||
const scrollAmount = container.clientHeight * 0.3;
|
const scrollAmount = container.clientHeight * 0.3;
|
||||||
container.scrollTop = Math.max(0, container.scrollTop - scrollAmount);
|
container.scrollTop = Math.max(0, container.scrollTop - scrollAmount);
|
||||||
@@ -417,10 +761,13 @@ export class AssistantView extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollResponseDown() {
|
scrollResponseDown() {
|
||||||
const container = this.shadowRoot.querySelector('.response-container');
|
const container = this.shadowRoot.querySelector(".response-container");
|
||||||
if (container) {
|
if (container) {
|
||||||
const scrollAmount = container.clientHeight * 0.3;
|
const scrollAmount = container.clientHeight * 0.3;
|
||||||
container.scrollTop = Math.min(container.scrollHeight - container.clientHeight, container.scrollTop + scrollAmount);
|
container.scrollTop = Math.min(
|
||||||
|
container.scrollHeight - container.clientHeight,
|
||||||
|
container.scrollTop + scrollAmount,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,17 +775,19 @@ export class AssistantView extends LitElement {
|
|||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
|
|
||||||
this.handlePreviousResponse = () => this.navigateToPreviousResponse();
|
this.handlePreviousResponse = () => this.navigateToPreviousResponse();
|
||||||
this.handleNextResponse = () => this.navigateToNextResponse();
|
this.handleNextResponse = () => this.navigateToNextResponse();
|
||||||
this.handleScrollUp = () => this.scrollResponseUp();
|
this.handleScrollUp = () => this.scrollResponseUp();
|
||||||
this.handleScrollDown = () => this.scrollResponseDown();
|
this.handleScrollDown = () => this.scrollResponseDown();
|
||||||
|
this.handleExpandHotkey = () => this.handleExpandResponse();
|
||||||
|
|
||||||
ipcRenderer.on('navigate-previous-response', this.handlePreviousResponse);
|
ipcRenderer.on("navigate-previous-response", this.handlePreviousResponse);
|
||||||
ipcRenderer.on('navigate-next-response', this.handleNextResponse);
|
ipcRenderer.on("navigate-next-response", this.handleNextResponse);
|
||||||
ipcRenderer.on('scroll-response-up', this.handleScrollUp);
|
ipcRenderer.on("scroll-response-up", this.handleScrollUp);
|
||||||
ipcRenderer.on('scroll-response-down', this.handleScrollDown);
|
ipcRenderer.on("scroll-response-down", this.handleScrollDown);
|
||||||
|
ipcRenderer.on("expand-response", this.handleExpandHotkey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,25 +796,40 @@ export class AssistantView extends LitElement {
|
|||||||
this._stopWaveformAnimation();
|
this._stopWaveformAnimation();
|
||||||
|
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
if (this.handlePreviousResponse) ipcRenderer.removeListener('navigate-previous-response', this.handlePreviousResponse);
|
if (this.handlePreviousResponse)
|
||||||
if (this.handleNextResponse) ipcRenderer.removeListener('navigate-next-response', this.handleNextResponse);
|
ipcRenderer.removeListener(
|
||||||
if (this.handleScrollUp) ipcRenderer.removeListener('scroll-response-up', this.handleScrollUp);
|
"navigate-previous-response",
|
||||||
if (this.handleScrollDown) ipcRenderer.removeListener('scroll-response-down', this.handleScrollDown);
|
this.handlePreviousResponse,
|
||||||
|
);
|
||||||
|
if (this.handleNextResponse)
|
||||||
|
ipcRenderer.removeListener(
|
||||||
|
"navigate-next-response",
|
||||||
|
this.handleNextResponse,
|
||||||
|
);
|
||||||
|
if (this.handleScrollUp)
|
||||||
|
ipcRenderer.removeListener("scroll-response-up", this.handleScrollUp);
|
||||||
|
if (this.handleScrollDown)
|
||||||
|
ipcRenderer.removeListener(
|
||||||
|
"scroll-response-down",
|
||||||
|
this.handleScrollDown,
|
||||||
|
);
|
||||||
|
if (this.handleExpandHotkey)
|
||||||
|
ipcRenderer.removeListener("expand-response", this.handleExpandHotkey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSendText() {
|
async handleSendText() {
|
||||||
const textInput = this.shadowRoot.querySelector('#textInput');
|
const textInput = this.shadowRoot.querySelector("#textInput");
|
||||||
if (textInput && textInput.value.trim()) {
|
if (textInput && textInput.value.trim()) {
|
||||||
const message = textInput.value.trim();
|
const message = textInput.value.trim();
|
||||||
textInput.value = '';
|
textInput.value = "";
|
||||||
await this.onSendText(message);
|
await this.onSendText(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTextKeydown(e) {
|
handleTextKeydown(e) {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.handleSendText();
|
this.handleSendText();
|
||||||
}
|
}
|
||||||
@@ -480,10 +844,22 @@ export class AssistantView extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleExpandResponse() {
|
||||||
|
if (
|
||||||
|
this.isExpanding ||
|
||||||
|
this.responses.length === 0 ||
|
||||||
|
this.currentResponseIndex < 0
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
this.isExpanding = true;
|
||||||
|
this._responseCountWhenStarted = this.responses.length;
|
||||||
|
await this.onExpandResponse();
|
||||||
|
}
|
||||||
|
|
||||||
_startWaveformAnimation() {
|
_startWaveformAnimation() {
|
||||||
const canvas = this.shadowRoot.querySelector('.analyze-canvas');
|
const canvas = this.shadowRoot.querySelector(".analyze-canvas");
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext("2d");
|
||||||
const dpr = window.devicePixelRatio || 1;
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
@@ -491,7 +867,8 @@ export class AssistantView extends LitElement {
|
|||||||
canvas.height = rect.height * dpr;
|
canvas.height = rect.height * dpr;
|
||||||
ctx.scale(dpr, dpr);
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
const dangerColor = getComputedStyle(this).getPropertyValue('--danger').trim() || '#EF4444';
|
const dangerColor =
|
||||||
|
getComputedStyle(this).getPropertyValue("--danger").trim() || "#EF4444";
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
const FADE_IN = 0.5; // seconds
|
const FADE_IN = 0.5; // seconds
|
||||||
const PARTICLE_SPREAD = 4; // px inward from border
|
const PARTICLE_SPREAD = 4; // px inward from border
|
||||||
@@ -542,7 +919,11 @@ export class AssistantView extends LitElement {
|
|||||||
// Pre-seed random offsets for stable particles
|
// Pre-seed random offsets for stable particles
|
||||||
const seeds = [];
|
const seeds = [];
|
||||||
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
||||||
seeds.push({ pos: Math.random(), drift: Math.random(), depthSeed: Math.random() });
|
seeds.push({
|
||||||
|
pos: Math.random(),
|
||||||
|
drift: Math.random(),
|
||||||
|
depthSeed: Math.random(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const draw = (now) => {
|
const draw = (now) => {
|
||||||
@@ -585,13 +966,17 @@ export class AssistantView extends LitElement {
|
|||||||
ctx.strokeStyle = dangerColor;
|
ctx.strokeStyle = dangerColor;
|
||||||
ctx.globalAlpha = wave.opacity * fade;
|
ctx.globalAlpha = wave.opacity * fade;
|
||||||
ctx.lineWidth = wave.width;
|
ctx.lineWidth = wave.width;
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = "round";
|
||||||
ctx.lineJoin = 'round';
|
ctx.lineJoin = "round";
|
||||||
|
|
||||||
for (let x = 0; x <= w; x++) {
|
for (let x = 0; x <= w; x++) {
|
||||||
const norm = x / w;
|
const norm = x / w;
|
||||||
const envelope = Math.sin(norm * Math.PI);
|
const envelope = Math.sin(norm * Math.PI);
|
||||||
const y = midY + Math.sin(norm * Math.PI * 2 * wave.freq + elapsed * wave.speed) * (midY * wave.amp) * envelope;
|
const y =
|
||||||
|
midY +
|
||||||
|
Math.sin(norm * Math.PI * 2 * wave.freq + elapsed * wave.speed) *
|
||||||
|
(midY * wave.amp) *
|
||||||
|
envelope;
|
||||||
if (x === 0) ctx.moveTo(x, y);
|
if (x === 0) ctx.moveTo(x, y);
|
||||||
else ctx.lineTo(x, y);
|
else ctx.lineTo(x, y);
|
||||||
}
|
}
|
||||||
@@ -610,16 +995,16 @@ export class AssistantView extends LitElement {
|
|||||||
cancelAnimationFrame(this._animFrame);
|
cancelAnimationFrame(this._animFrame);
|
||||||
this._animFrame = null;
|
this._animFrame = null;
|
||||||
}
|
}
|
||||||
const canvas = this.shadowRoot.querySelector('.analyze-canvas');
|
const canvas = this.shadowRoot.querySelector(".analyze-canvas");
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext("2d");
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const container = this.shadowRoot.querySelector('.response-container');
|
const container = this.shadowRoot.querySelector(".response-container");
|
||||||
if (container) {
|
if (container) {
|
||||||
container.scrollTop = container.scrollHeight;
|
container.scrollTop = container.scrollHeight;
|
||||||
}
|
}
|
||||||
@@ -633,11 +1018,14 @@ export class AssistantView extends LitElement {
|
|||||||
|
|
||||||
updated(changedProperties) {
|
updated(changedProperties) {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
if (changedProperties.has('responses') || changedProperties.has('currentResponseIndex')) {
|
if (
|
||||||
|
changedProperties.has("responses") ||
|
||||||
|
changedProperties.has("currentResponseIndex")
|
||||||
|
) {
|
||||||
this.updateResponseContent();
|
this.updateResponseContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProperties.has('isAnalyzing')) {
|
if (changedProperties.has("isAnalyzing")) {
|
||||||
if (this.isAnalyzing) {
|
if (this.isAnalyzing) {
|
||||||
this._startWaveformAnimation();
|
this._startWaveformAnimation();
|
||||||
} else {
|
} else {
|
||||||
@@ -645,46 +1033,120 @@ export class AssistantView extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProperties.has('responses') && this.isAnalyzing) {
|
if (
|
||||||
|
changedProperties.has("responses") &&
|
||||||
|
(this.isAnalyzing || this.isExpanding)
|
||||||
|
) {
|
||||||
if (this.responses.length > this._responseCountWhenStarted) {
|
if (this.responses.length > this._responseCountWhenStarted) {
|
||||||
this.isAnalyzing = false;
|
this.isAnalyzing = false;
|
||||||
|
this.isExpanding = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateResponseContent() {
|
updateResponseContent() {
|
||||||
const container = this.shadowRoot.querySelector('#responseContainer');
|
const container = this.shadowRoot.querySelector("#responseContainer");
|
||||||
if (container) {
|
if (container) {
|
||||||
const currentResponse = this.getCurrentResponse();
|
const currentResponse = this.getCurrentResponse();
|
||||||
const renderedResponse = this.renderMarkdown(currentResponse);
|
const renderedResponse = this.renderMarkdown(currentResponse);
|
||||||
container.innerHTML = renderedResponse;
|
container.innerHTML = renderedResponse;
|
||||||
|
|
||||||
|
// Apply syntax highlighting to code blocks
|
||||||
|
this.applyCodeHighlighting(container);
|
||||||
|
|
||||||
if (this.shouldAnimateResponse) {
|
if (this.shouldAnimateResponse) {
|
||||||
this.dispatchEvent(new CustomEvent('response-animation-complete', { bubbles: true, composed: true }));
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("response-animation-complete", {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const hasMultipleResponses = this.responses.length > 1;
|
const hasMultipleResponses = this.responses.length > 1;
|
||||||
|
const hasResponse =
|
||||||
|
this.responses.length > 0 && this.currentResponseIndex >= 0;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="response-container" id="responseContainer"></div>
|
<div class="response-container" id="responseContainer"></div>
|
||||||
|
|
||||||
${hasMultipleResponses ? html`
|
${hasMultipleResponses || hasResponse
|
||||||
|
? html`
|
||||||
<div class="response-nav">
|
<div class="response-nav">
|
||||||
<button class="nav-btn" @click=${this.navigateToPreviousResponse} ?disabled=${this.currentResponseIndex <= 0} title="Previous response">
|
${hasMultipleResponses
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
? html`
|
||||||
<path fill-rule="evenodd" d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" />
|
<button
|
||||||
|
class="nav-btn"
|
||||||
|
@click=${this.navigateToPreviousResponse}
|
||||||
|
?disabled=${this.currentResponseIndex <= 0}
|
||||||
|
title="Previous response"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<span class="response-counter">${this.currentResponseIndex + 1} of ${this.responses.length}</span>
|
<span class="response-counter"
|
||||||
<button class="nav-btn" @click=${this.navigateToNextResponse} ?disabled=${this.currentResponseIndex >= this.responses.length - 1} title="Next response">
|
>${this.currentResponseIndex + 1} of
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
${this.responses.length}</span
|
||||||
<path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
|
>
|
||||||
|
<button
|
||||||
|
class="nav-btn"
|
||||||
|
@click=${this.navigateToNextResponse}
|
||||||
|
?disabled=${this.currentResponseIndex >=
|
||||||
|
this.responses.length - 1}
|
||||||
|
title="Next response"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${hasResponse
|
||||||
|
? html`
|
||||||
|
<button
|
||||||
|
class="expand-btn"
|
||||||
|
@click=${this.handleExpandResponse}
|
||||||
|
?disabled=${this.isExpanding}
|
||||||
|
title="Expand this response with more detail"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.168l3.71-3.938a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
${this.isExpanding ? "Expanding..." : "Expand"}
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
`
|
||||||
|
: ""}
|
||||||
|
|
||||||
<div class="input-bar">
|
<div class="input-bar">
|
||||||
<div class="input-bar-inner">
|
<div class="input-bar-inner">
|
||||||
@@ -695,11 +1157,26 @@ export class AssistantView extends LitElement {
|
|||||||
@keydown=${this.handleTextKeydown}
|
@keydown=${this.handleTextKeydown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button class="analyze-btn ${this.isAnalyzing ? 'analyzing' : ''}" @click=${this.handleScreenAnswer}>
|
<button
|
||||||
|
class="analyze-btn ${this.isAnalyzing ? "analyzing" : ""}"
|
||||||
|
@click=${this.handleScreenAnswer}
|
||||||
|
>
|
||||||
<canvas class="analyze-canvas"></canvas>
|
<canvas class="analyze-canvas"></canvas>
|
||||||
<span class="analyze-btn-content">
|
<span class="analyze-btn-content">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24">
|
<svg
|
||||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 3v7h6l-8 11v-7H5z" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 3v7h6l-8 11v-7H5z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Analyze Screen
|
Analyze Screen
|
||||||
</span>
|
</span>
|
||||||
@@ -709,4 +1186,4 @@ export class AssistantView extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('assistant-view', AssistantView);
|
customElements.define("assistant-view", AssistantView);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
|
import { html, css, LitElement } from "../../assets/lit-core-2.7.4.min.js";
|
||||||
import { unifiedPageStyles } from './sharedPageStyles.js';
|
import { unifiedPageStyles } from "./sharedPageStyles.js";
|
||||||
|
|
||||||
export class CustomizeView extends LitElement {
|
export class CustomizeView extends LitElement {
|
||||||
static styles = [
|
static styles = [
|
||||||
@@ -22,7 +22,7 @@ export class CustomizeView extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.warning-callout::before {
|
.warning-callout::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -6px;
|
top: -6px;
|
||||||
left: 16px;
|
left: 16px;
|
||||||
@@ -198,26 +198,26 @@ export class CustomizeView extends LitElement {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.selectedProfile = 'interview';
|
this.selectedProfile = "interview";
|
||||||
this.selectedLanguage = 'en-US';
|
this.selectedLanguage = "en-US";
|
||||||
this.selectedImageQuality = 'medium';
|
this.selectedImageQuality = "medium";
|
||||||
this.layoutMode = 'normal';
|
this.layoutMode = "normal";
|
||||||
this.keybinds = this.getDefaultKeybinds();
|
this.keybinds = this.getDefaultKeybinds();
|
||||||
this.onProfileChange = () => {};
|
this.onProfileChange = () => {};
|
||||||
this.onLanguageChange = () => {};
|
this.onLanguageChange = () => {};
|
||||||
this.onImageQualityChange = () => {};
|
this.onImageQualityChange = () => {};
|
||||||
this.onLayoutModeChange = () => {};
|
this.onLayoutModeChange = () => {};
|
||||||
this.googleSearchEnabled = true;
|
this.googleSearchEnabled = true;
|
||||||
this.providerMode = 'byok';
|
this.providerMode = "byok";
|
||||||
this.isClearing = false;
|
this.isClearing = false;
|
||||||
this.isRestoring = false;
|
this.isRestoring = false;
|
||||||
this.clearStatusMessage = '';
|
this.clearStatusMessage = "";
|
||||||
this.clearStatusType = '';
|
this.clearStatusType = "";
|
||||||
this.backgroundTransparency = 0.8;
|
this.backgroundTransparency = 0.8;
|
||||||
this.fontSize = 20;
|
this.fontSize = 20;
|
||||||
this.audioMode = 'speaker_only';
|
this.audioMode = "speaker_only";
|
||||||
this.customPrompt = '';
|
this.customPrompt = "";
|
||||||
this.theme = 'dark';
|
this.theme = "dark";
|
||||||
this._loadFromStorage();
|
this._loadFromStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,14 +227,17 @@ export class CustomizeView extends LitElement {
|
|||||||
|
|
||||||
async _loadFromStorage() {
|
async _loadFromStorage() {
|
||||||
try {
|
try {
|
||||||
const [prefs, keybinds] = await Promise.all([cheatingDaddy.storage.getPreferences(), cheatingDaddy.storage.getKeybinds()]);
|
const [prefs, keybinds] = await Promise.all([
|
||||||
|
cheatingDaddy.storage.getPreferences(),
|
||||||
|
cheatingDaddy.storage.getKeybinds(),
|
||||||
|
]);
|
||||||
this.googleSearchEnabled = prefs.googleSearchEnabled ?? true;
|
this.googleSearchEnabled = prefs.googleSearchEnabled ?? true;
|
||||||
this.providerMode = prefs.providerMode || 'byok';
|
this.providerMode = prefs.providerMode || "byok";
|
||||||
this.backgroundTransparency = prefs.backgroundTransparency ?? 0.8;
|
this.backgroundTransparency = prefs.backgroundTransparency ?? 0.8;
|
||||||
this.fontSize = prefs.fontSize ?? 20;
|
this.fontSize = prefs.fontSize ?? 20;
|
||||||
this.audioMode = prefs.audioMode ?? 'speaker_only';
|
this.audioMode = prefs.audioMode ?? "speaker_only";
|
||||||
this.customPrompt = prefs.customPrompt ?? '';
|
this.customPrompt = prefs.customPrompt ?? "";
|
||||||
this.theme = prefs.theme ?? 'dark';
|
this.theme = prefs.theme ?? "dark";
|
||||||
if (keybinds) {
|
if (keybinds) {
|
||||||
this.keybinds = { ...this.getDefaultKeybinds(), ...keybinds };
|
this.keybinds = { ...this.getDefaultKeybinds(), ...keybinds };
|
||||||
}
|
}
|
||||||
@@ -242,94 +245,145 @@ export class CustomizeView extends LitElement {
|
|||||||
this.updateFontSize();
|
this.updateFontSize();
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading settings:', error);
|
console.error("Error loading settings:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getProfiles() {
|
getProfiles() {
|
||||||
return [
|
return [
|
||||||
{ value: 'interview', name: 'Job Interview' },
|
{ value: "interview", name: "Job Interview" },
|
||||||
{ value: 'sales', name: 'Sales Call' },
|
{ value: "sales", name: "Sales Call" },
|
||||||
{ value: 'meeting', name: 'Business Meeting' },
|
{ value: "meeting", name: "Business Meeting" },
|
||||||
{ value: 'presentation', name: 'Presentation' },
|
{ value: "presentation", name: "Presentation" },
|
||||||
{ value: 'negotiation', name: 'Negotiation' },
|
{ value: "negotiation", name: "Negotiation" },
|
||||||
{ value: 'exam', name: 'Exam Assistant' },
|
{ value: "exam", name: "Exam Assistant" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
getLanguages() {
|
getLanguages() {
|
||||||
return [
|
return [
|
||||||
{ value: 'en-US', name: 'English (US)' },
|
{ value: "auto", name: "Auto (Multilingual)" },
|
||||||
{ value: 'en-GB', name: 'English (UK)' },
|
{ value: "en-US", name: "English (US)" },
|
||||||
{ value: 'en-AU', name: 'English (Australia)' },
|
{ value: "en-GB", name: "English (UK)" },
|
||||||
{ value: 'en-IN', name: 'English (India)' },
|
{ value: "en-AU", name: "English (Australia)" },
|
||||||
{ value: 'de-DE', name: 'German (Germany)' },
|
{ value: "en-IN", name: "English (India)" },
|
||||||
{ value: 'es-US', name: 'Spanish (US)' },
|
{ value: "de-DE", name: "German (Germany)" },
|
||||||
{ value: 'es-ES', name: 'Spanish (Spain)' },
|
{ value: "es-US", name: "Spanish (US)" },
|
||||||
{ value: 'fr-FR', name: 'French (France)' },
|
{ value: "es-ES", name: "Spanish (Spain)" },
|
||||||
{ value: 'fr-CA', name: 'French (Canada)' },
|
{ value: "fr-FR", name: "French (France)" },
|
||||||
{ value: 'hi-IN', name: 'Hindi (India)' },
|
{ value: "fr-CA", name: "French (Canada)" },
|
||||||
{ value: 'pt-BR', name: 'Portuguese (Brazil)' },
|
{ value: "hi-IN", name: "Hindi (India)" },
|
||||||
{ value: 'ar-XA', name: 'Arabic (Generic)' },
|
{ value: "pt-BR", name: "Portuguese (Brazil)" },
|
||||||
{ value: 'id-ID', name: 'Indonesian (Indonesia)' },
|
{ value: "ar-XA", name: "Arabic (Generic)" },
|
||||||
{ value: 'it-IT', name: 'Italian (Italy)' },
|
{ value: "id-ID", name: "Indonesian (Indonesia)" },
|
||||||
{ value: 'ja-JP', name: 'Japanese (Japan)' },
|
{ value: "it-IT", name: "Italian (Italy)" },
|
||||||
{ value: 'tr-TR', name: 'Turkish (Turkey)' },
|
{ value: "ja-JP", name: "Japanese (Japan)" },
|
||||||
{ value: 'vi-VN', name: 'Vietnamese (Vietnam)' },
|
{ value: "tr-TR", name: "Turkish (Turkey)" },
|
||||||
{ value: 'bn-IN', name: 'Bengali (India)' },
|
{ value: "vi-VN", name: "Vietnamese (Vietnam)" },
|
||||||
{ value: 'gu-IN', name: 'Gujarati (India)' },
|
{ value: "bn-IN", name: "Bengali (India)" },
|
||||||
{ value: 'kn-IN', name: 'Kannada (India)' },
|
{ value: "gu-IN", name: "Gujarati (India)" },
|
||||||
{ value: 'ml-IN', name: 'Malayalam (India)' },
|
{ value: "kn-IN", name: "Kannada (India)" },
|
||||||
{ value: 'mr-IN', name: 'Marathi (India)' },
|
{ value: "ml-IN", name: "Malayalam (India)" },
|
||||||
{ value: 'ta-IN', name: 'Tamil (India)' },
|
{ value: "mr-IN", name: "Marathi (India)" },
|
||||||
{ value: 'te-IN', name: 'Telugu (India)' },
|
{ value: "ta-IN", name: "Tamil (India)" },
|
||||||
{ value: 'nl-NL', name: 'Dutch (Netherlands)' },
|
{ value: "te-IN", name: "Telugu (India)" },
|
||||||
{ value: 'ko-KR', name: 'Korean (South Korea)' },
|
{ value: "nl-NL", name: "Dutch (Netherlands)" },
|
||||||
{ value: 'cmn-CN', name: 'Mandarin Chinese (China)' },
|
{ value: "ko-KR", name: "Korean (South Korea)" },
|
||||||
{ value: 'pl-PL', name: 'Polish (Poland)' },
|
{ value: "cmn-CN", name: "Mandarin Chinese (China)" },
|
||||||
{ value: 'ru-RU', name: 'Russian (Russia)' },
|
{ value: "pl-PL", name: "Polish (Poland)" },
|
||||||
{ value: 'th-TH', name: 'Thai (Thailand)' },
|
{ value: "ru-RU", name: "Russian (Russia)" },
|
||||||
|
{ value: "th-TH", name: "Thai (Thailand)" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultKeybinds() {
|
getDefaultKeybinds() {
|
||||||
const isMac = cheatingDaddy.isMacOS || navigator.platform.includes('Mac');
|
const isMac = cheatingDaddy.isMacOS || navigator.platform.includes("Mac");
|
||||||
return {
|
return {
|
||||||
moveUp: isMac ? 'Alt+Up' : 'Ctrl+Up',
|
moveUp: isMac ? "Alt+Up" : "Ctrl+Up",
|
||||||
moveDown: isMac ? 'Alt+Down' : 'Ctrl+Down',
|
moveDown: isMac ? "Alt+Down" : "Ctrl+Down",
|
||||||
moveLeft: isMac ? 'Alt+Left' : 'Ctrl+Left',
|
moveLeft: isMac ? "Alt+Left" : "Ctrl+Left",
|
||||||
moveRight: isMac ? 'Alt+Right' : 'Ctrl+Right',
|
moveRight: isMac ? "Alt+Right" : "Ctrl+Right",
|
||||||
toggleVisibility: isMac ? 'Cmd+\\' : 'Ctrl+\\',
|
toggleVisibility: isMac ? "Cmd+\\" : "Ctrl+\\",
|
||||||
toggleClickThrough: isMac ? 'Cmd+M' : 'Ctrl+M',
|
toggleClickThrough: isMac ? "Cmd+M" : "Ctrl+M",
|
||||||
nextStep: isMac ? 'Cmd+Enter' : 'Ctrl+Enter',
|
nextStep: isMac ? "Cmd+Enter" : "Ctrl+Enter",
|
||||||
previousResponse: isMac ? 'Cmd+[' : 'Ctrl+[',
|
previousResponse: isMac ? "Cmd+[" : "Ctrl+[",
|
||||||
nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]',
|
nextResponse: isMac ? "Cmd+]" : "Ctrl+]",
|
||||||
scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up',
|
scrollUp: isMac ? "Cmd+Shift+Up" : "Ctrl+Shift+Up",
|
||||||
scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down',
|
scrollDown: isMac ? "Cmd+Shift+Down" : "Ctrl+Shift+Down",
|
||||||
|
expandResponse: isMac ? "Cmd+E" : "Ctrl+E",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getKeybindActions() {
|
getKeybindActions() {
|
||||||
return [
|
return [
|
||||||
{ key: 'moveUp', name: 'Move Window Up', description: 'Move the app window up' },
|
{
|
||||||
{ key: 'moveDown', name: 'Move Window Down', description: 'Move the app window down' },
|
key: "moveUp",
|
||||||
{ key: 'moveLeft', name: 'Move Window Left', description: 'Move the app window left' },
|
name: "Move Window Up",
|
||||||
{ key: 'moveRight', name: 'Move Window Right', description: 'Move the app window right' },
|
description: "Move the app window up",
|
||||||
{ key: 'toggleVisibility', name: 'Toggle Visibility', description: 'Show or hide the app window' },
|
},
|
||||||
{ key: 'toggleClickThrough', name: 'Toggle Click-through', description: 'Enable or disable click-through mode' },
|
{
|
||||||
{ key: 'nextStep', name: 'Ask Next Step', description: 'Take screenshot and ask for next step' },
|
key: "moveDown",
|
||||||
{ key: 'previousResponse', name: 'Previous Response', description: 'Move to previous AI response' },
|
name: "Move Window Down",
|
||||||
{ key: 'nextResponse', name: 'Next Response', description: 'Move to next AI response' },
|
description: "Move the app window down",
|
||||||
{ key: 'scrollUp', name: 'Scroll Response Up', description: 'Scroll response content upward' },
|
},
|
||||||
{ key: 'scrollDown', name: 'Scroll Response Down', description: 'Scroll response content downward' },
|
{
|
||||||
|
key: "moveLeft",
|
||||||
|
name: "Move Window Left",
|
||||||
|
description: "Move the app window left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "moveRight",
|
||||||
|
name: "Move Window Right",
|
||||||
|
description: "Move the app window right",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "toggleVisibility",
|
||||||
|
name: "Toggle Visibility",
|
||||||
|
description: "Show or hide the app window",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "toggleClickThrough",
|
||||||
|
name: "Toggle Click-through",
|
||||||
|
description: "Enable or disable click-through mode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "nextStep",
|
||||||
|
name: "Ask Next Step",
|
||||||
|
description: "Take screenshot and ask for next step",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "previousResponse",
|
||||||
|
name: "Previous Response",
|
||||||
|
description: "Move to previous AI response",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "nextResponse",
|
||||||
|
name: "Next Response",
|
||||||
|
description: "Move to next AI response",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "scrollUp",
|
||||||
|
name: "Scroll Response Up",
|
||||||
|
description: "Scroll response content upward",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "scrollDown",
|
||||||
|
name: "Scroll Response Down",
|
||||||
|
description: "Scroll response content downward",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "expandResponse",
|
||||||
|
name: "Expand Response",
|
||||||
|
description: "Expand the current response with more detail",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveKeybinds() {
|
async saveKeybinds() {
|
||||||
await cheatingDaddy.storage.setKeybinds(this.keybinds);
|
await cheatingDaddy.storage.setKeybinds(this.keybinds);
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
ipcRenderer.send('update-keybinds', this.keybinds);
|
ipcRenderer.send("update-keybinds", this.keybinds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,18 +409,24 @@ export class CustomizeView extends LitElement {
|
|||||||
|
|
||||||
async handleCustomPromptInput(e) {
|
async handleCustomPromptInput(e) {
|
||||||
this.customPrompt = e.target.value;
|
this.customPrompt = e.target.value;
|
||||||
await cheatingDaddy.storage.updatePreference('customPrompt', this.customPrompt);
|
await cheatingDaddy.storage.updatePreference(
|
||||||
|
"customPrompt",
|
||||||
|
this.customPrompt,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleAudioModeSelect(e) {
|
async handleAudioModeSelect(e) {
|
||||||
this.audioMode = e.target.value;
|
this.audioMode = e.target.value;
|
||||||
await cheatingDaddy.storage.updatePreference('audioMode', this.audioMode);
|
await cheatingDaddy.storage.updatePreference("audioMode", this.audioMode);
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleProviderModeChange(e) {
|
async handleProviderModeChange(e) {
|
||||||
this.providerMode = e.target.value;
|
this.providerMode = e.target.value;
|
||||||
await cheatingDaddy.storage.updatePreference('providerMode', this.providerMode);
|
await cheatingDaddy.storage.updatePreference(
|
||||||
|
"providerMode",
|
||||||
|
this.providerMode,
|
||||||
|
);
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,13 +439,19 @@ export class CustomizeView extends LitElement {
|
|||||||
|
|
||||||
async handleGoogleSearchChange(e) {
|
async handleGoogleSearchChange(e) {
|
||||||
this.googleSearchEnabled = e.target.checked;
|
this.googleSearchEnabled = e.target.checked;
|
||||||
await cheatingDaddy.storage.updatePreference('googleSearchEnabled', this.googleSearchEnabled);
|
await cheatingDaddy.storage.updatePreference(
|
||||||
|
"googleSearchEnabled",
|
||||||
|
this.googleSearchEnabled,
|
||||||
|
);
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
try {
|
try {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('update-google-search-setting', this.googleSearchEnabled);
|
await ipcRenderer.invoke(
|
||||||
|
"update-google-search-setting",
|
||||||
|
this.googleSearchEnabled,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to notify main process:', error);
|
console.error("Failed to notify main process:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
@@ -393,25 +459,34 @@ export class CustomizeView extends LitElement {
|
|||||||
|
|
||||||
async handleBackgroundTransparencyChange(e) {
|
async handleBackgroundTransparencyChange(e) {
|
||||||
this.backgroundTransparency = parseFloat(e.target.value);
|
this.backgroundTransparency = parseFloat(e.target.value);
|
||||||
await cheatingDaddy.storage.updatePreference('backgroundTransparency', this.backgroundTransparency);
|
await cheatingDaddy.storage.updatePreference(
|
||||||
|
"backgroundTransparency",
|
||||||
|
this.backgroundTransparency,
|
||||||
|
);
|
||||||
this.updateBackgroundAppearance();
|
this.updateBackgroundAppearance();
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBackgroundAppearance() {
|
updateBackgroundAppearance() {
|
||||||
const colors = cheatingDaddy.theme.get(this.theme);
|
const colors = cheatingDaddy.theme.get(this.theme);
|
||||||
cheatingDaddy.theme.applyBackgrounds(colors.background, this.backgroundTransparency);
|
cheatingDaddy.theme.applyBackgrounds(
|
||||||
|
colors.background,
|
||||||
|
this.backgroundTransparency,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleFontSizeChange(e) {
|
async handleFontSizeChange(e) {
|
||||||
this.fontSize = parseInt(e.target.value, 10);
|
this.fontSize = parseInt(e.target.value, 10);
|
||||||
await cheatingDaddy.storage.updatePreference('fontSize', this.fontSize);
|
await cheatingDaddy.storage.updatePreference("fontSize", this.fontSize);
|
||||||
this.updateFontSize();
|
this.updateFontSize();
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFontSize() {
|
updateFontSize() {
|
||||||
document.documentElement.style.setProperty('--response-font-size', `${this.fontSize}px`);
|
document.documentElement.style.setProperty(
|
||||||
|
"--response-font-size",
|
||||||
|
`${this.fontSize}px`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeybindChange(action, value) {
|
handleKeybindChange(action, value) {
|
||||||
@@ -421,50 +496,50 @@ export class CustomizeView extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleKeybindFocus(e) {
|
handleKeybindFocus(e) {
|
||||||
e.target.placeholder = 'Press key combination...';
|
e.target.placeholder = "Press key combination...";
|
||||||
e.target.select();
|
e.target.select();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeybindInput(e) {
|
handleKeybindInput(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const modifiers = [];
|
const modifiers = [];
|
||||||
if (e.ctrlKey) modifiers.push('Ctrl');
|
if (e.ctrlKey) modifiers.push("Ctrl");
|
||||||
if (e.metaKey) modifiers.push('Cmd');
|
if (e.metaKey) modifiers.push("Cmd");
|
||||||
if (e.altKey) modifiers.push('Alt');
|
if (e.altKey) modifiers.push("Alt");
|
||||||
if (e.shiftKey) modifiers.push('Shift');
|
if (e.shiftKey) modifiers.push("Shift");
|
||||||
let mainKey = e.key;
|
let mainKey = e.key;
|
||||||
|
|
||||||
switch (e.code) {
|
switch (e.code) {
|
||||||
case 'ArrowUp':
|
case "ArrowUp":
|
||||||
mainKey = 'Up';
|
mainKey = "Up";
|
||||||
break;
|
break;
|
||||||
case 'ArrowDown':
|
case "ArrowDown":
|
||||||
mainKey = 'Down';
|
mainKey = "Down";
|
||||||
break;
|
break;
|
||||||
case 'ArrowLeft':
|
case "ArrowLeft":
|
||||||
mainKey = 'Left';
|
mainKey = "Left";
|
||||||
break;
|
break;
|
||||||
case 'ArrowRight':
|
case "ArrowRight":
|
||||||
mainKey = 'Right';
|
mainKey = "Right";
|
||||||
break;
|
break;
|
||||||
case 'Enter':
|
case "Enter":
|
||||||
mainKey = 'Enter';
|
mainKey = "Enter";
|
||||||
break;
|
break;
|
||||||
case 'Space':
|
case "Space":
|
||||||
mainKey = 'Space';
|
mainKey = "Space";
|
||||||
break;
|
break;
|
||||||
case 'Backslash':
|
case "Backslash":
|
||||||
mainKey = '\\';
|
mainKey = "\\";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (e.key.length === 1) mainKey = e.key.toUpperCase();
|
if (e.key.length === 1) mainKey = e.key.toUpperCase();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['Control', 'Meta', 'Alt', 'Shift'].includes(e.key)) return;
|
if (["Control", "Meta", "Alt", "Shift"].includes(e.key)) return;
|
||||||
|
|
||||||
const action = e.target.dataset.action;
|
const action = e.target.dataset.action;
|
||||||
const keybind = [...modifiers, mainKey].join('+');
|
const keybind = [...modifiers, mainKey].join("+");
|
||||||
this.handleKeybindChange(action, keybind);
|
this.handleKeybindChange(action, keybind);
|
||||||
e.target.value = keybind;
|
e.target.value = keybind;
|
||||||
e.target.blur();
|
e.target.blur();
|
||||||
@@ -474,8 +549,8 @@ export class CustomizeView extends LitElement {
|
|||||||
this.keybinds = this.getDefaultKeybinds();
|
this.keybinds = this.getDefaultKeybinds();
|
||||||
await cheatingDaddy.storage.setKeybinds(null);
|
await cheatingDaddy.storage.setKeybinds(null);
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
ipcRenderer.send('update-keybinds', this.keybinds);
|
ipcRenderer.send("update-keybinds", this.keybinds);
|
||||||
}
|
}
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
@@ -483,22 +558,22 @@ export class CustomizeView extends LitElement {
|
|||||||
async restoreAllSettings() {
|
async restoreAllSettings() {
|
||||||
if (this.isRestoring) return;
|
if (this.isRestoring) return;
|
||||||
this.isRestoring = true;
|
this.isRestoring = true;
|
||||||
this.clearStatusMessage = '';
|
this.clearStatusMessage = "";
|
||||||
this.clearStatusType = '';
|
this.clearStatusType = "";
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
try {
|
try {
|
||||||
// Restore all preferences to defaults
|
// Restore all preferences to defaults
|
||||||
const defaults = {
|
const defaults = {
|
||||||
customPrompt: '',
|
customPrompt: "",
|
||||||
selectedProfile: 'interview',
|
selectedProfile: "interview",
|
||||||
selectedLanguage: 'en-US',
|
selectedLanguage: "en-US",
|
||||||
selectedScreenshotInterval: '5',
|
selectedScreenshotInterval: "5",
|
||||||
selectedImageQuality: 'medium',
|
selectedImageQuality: "medium",
|
||||||
audioMode: 'speaker_only',
|
audioMode: "speaker_only",
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
backgroundTransparency: 0.8,
|
backgroundTransparency: 0.8,
|
||||||
googleSearchEnabled: false,
|
googleSearchEnabled: false,
|
||||||
theme: 'dark',
|
theme: "dark",
|
||||||
};
|
};
|
||||||
for (const [key, value] of Object.entries(defaults)) {
|
for (const [key, value] of Object.entries(defaults)) {
|
||||||
await cheatingDaddy.storage.updatePreference(key, value);
|
await cheatingDaddy.storage.updatePreference(key, value);
|
||||||
@@ -508,8 +583,8 @@ export class CustomizeView extends LitElement {
|
|||||||
this.keybinds = this.getDefaultKeybinds();
|
this.keybinds = this.getDefaultKeybinds();
|
||||||
await cheatingDaddy.storage.setKeybinds(null);
|
await cheatingDaddy.storage.setKeybinds(null);
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
ipcRenderer.send('update-keybinds', this.keybinds);
|
ipcRenderer.send("update-keybinds", this.keybinds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply to local state
|
// Apply to local state
|
||||||
@@ -533,12 +608,12 @@ export class CustomizeView extends LitElement {
|
|||||||
this.updateFontSize();
|
this.updateFontSize();
|
||||||
await cheatingDaddy.theme.save(defaults.theme);
|
await cheatingDaddy.theme.save(defaults.theme);
|
||||||
|
|
||||||
this.clearStatusMessage = 'All settings restored to defaults';
|
this.clearStatusMessage = "All settings restored to defaults";
|
||||||
this.clearStatusType = 'success';
|
this.clearStatusType = "success";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error restoring settings:', error);
|
console.error("Error restoring settings:", error);
|
||||||
this.clearStatusMessage = `Error restoring settings: ${error.message}`;
|
this.clearStatusMessage = `Error restoring settings: ${error.message}`;
|
||||||
this.clearStatusType = 'error';
|
this.clearStatusType = "error";
|
||||||
} finally {
|
} finally {
|
||||||
this.isRestoring = false;
|
this.isRestoring = false;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
@@ -548,28 +623,28 @@ export class CustomizeView extends LitElement {
|
|||||||
async clearLocalData() {
|
async clearLocalData() {
|
||||||
if (this.isClearing) return;
|
if (this.isClearing) return;
|
||||||
this.isClearing = true;
|
this.isClearing = true;
|
||||||
this.clearStatusMessage = '';
|
this.clearStatusMessage = "";
|
||||||
this.clearStatusType = '';
|
this.clearStatusType = "";
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
try {
|
try {
|
||||||
await cheatingDaddy.storage.clearAll();
|
await cheatingDaddy.storage.clearAll();
|
||||||
this.clearStatusMessage = 'Successfully cleared all local data';
|
this.clearStatusMessage = "Successfully cleared all local data";
|
||||||
this.clearStatusType = 'success';
|
this.clearStatusType = "success";
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.clearStatusMessage = 'Closing application...';
|
this.clearStatusMessage = "Closing application...";
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
if (window.require) {
|
if (window.require) {
|
||||||
const { ipcRenderer } = window.require('electron');
|
const { ipcRenderer } = window.require("electron");
|
||||||
await ipcRenderer.invoke('quit-application');
|
await ipcRenderer.invoke("quit-application");
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error clearing data:', error);
|
console.error("Error clearing data:", error);
|
||||||
this.clearStatusMessage = `Error clearing data: ${error.message}`;
|
this.clearStatusMessage = `Error clearing data: ${error.message}`;
|
||||||
this.clearStatusType = 'error';
|
this.clearStatusType = "error";
|
||||||
} finally {
|
} finally {
|
||||||
this.isClearing = false;
|
this.isClearing = false;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
@@ -583,7 +658,11 @@ export class CustomizeView extends LitElement {
|
|||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Regime</label>
|
<label class="form-label">Regime</label>
|
||||||
<select class="control" .value=${this.providerMode} @change=${this.handleProviderModeChange}>
|
<select
|
||||||
|
class="control"
|
||||||
|
.value=${this.providerMode}
|
||||||
|
@change=${this.handleProviderModeChange}
|
||||||
|
>
|
||||||
<option value="byok">BYOK (API Keys)</option>
|
<option value="byok">BYOK (API Keys)</option>
|
||||||
<option value="local">Local AI (Ollama)</option>
|
<option value="local">Local AI (Ollama)</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -600,18 +679,31 @@ export class CustomizeView extends LitElement {
|
|||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Audio Mode</label>
|
<label class="form-label">Audio Mode</label>
|
||||||
<select class="control" .value=${this.audioMode} @change=${this.handleAudioModeSelect}>
|
<select
|
||||||
|
class="control"
|
||||||
|
.value=${this.audioMode}
|
||||||
|
@change=${this.handleAudioModeSelect}
|
||||||
|
>
|
||||||
<option value="speaker_only">Speaker Only (Interviewer)</option>
|
<option value="speaker_only">Speaker Only (Interviewer)</option>
|
||||||
<option value="mic_only">Microphone Only (Me)</option>
|
<option value="mic_only">Microphone Only (Me)</option>
|
||||||
<option value="both">Both Speaker and Microphone</option>
|
<option value="both">Both Speaker and Microphone</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
${this.audioMode !== 'speaker_only' ? html`
|
${this.audioMode !== "speaker_only"
|
||||||
<div class="warning-callout">May cause unexpected behavior. Only change this if you know what you're doing.</div>
|
? html`
|
||||||
` : ''}
|
<div class="warning-callout">
|
||||||
|
May cause unexpected behavior. Only change this if you know
|
||||||
|
what you're doing.
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Image Quality</label>
|
<label class="form-label">Image Quality</label>
|
||||||
<select class="control" .value=${this.selectedImageQuality} @change=${this.handleImageQualitySelect}>
|
<select
|
||||||
|
class="control"
|
||||||
|
.value=${this.selectedImageQuality}
|
||||||
|
@change=${this.handleImageQualitySelect}
|
||||||
|
>
|
||||||
<option value="high">High Quality</option>
|
<option value="high">High Quality</option>
|
||||||
<option value="medium">Medium Quality</option>
|
<option value="medium">Medium Quality</option>
|
||||||
<option value="low">Low Quality</option>
|
<option value="low">Low Quality</option>
|
||||||
@@ -629,8 +721,20 @@ export class CustomizeView extends LitElement {
|
|||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Speech Language</label>
|
<label class="form-label">Speech Language</label>
|
||||||
<select class="control" .value=${this.selectedLanguage} @change=${this.handleLanguageSelect}>
|
<select
|
||||||
${this.getLanguages().map(language => html`<option value=${language.value}>${language.name}</option>`)}
|
class="control"
|
||||||
|
.value=${this.selectedLanguage}
|
||||||
|
@change=${this.handleLanguageSelect}
|
||||||
|
>
|
||||||
|
${this.getLanguages().map(
|
||||||
|
(language) =>
|
||||||
|
html`<option
|
||||||
|
value=${language.value}
|
||||||
|
?selected=${language.value === this.selectedLanguage}
|
||||||
|
>
|
||||||
|
${language.name}
|
||||||
|
</option>`,
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -645,14 +749,23 @@ export class CustomizeView extends LitElement {
|
|||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Theme</label>
|
<label class="form-label">Theme</label>
|
||||||
<select class="control" .value=${this.theme} @change=${this.handleThemeChange}>
|
<select
|
||||||
${this.getThemes().map(theme => html`<option value=${theme.value}>${theme.name}</option>`)}
|
class="control"
|
||||||
|
.value=${this.theme}
|
||||||
|
@change=${this.handleThemeChange}
|
||||||
|
>
|
||||||
|
${this.getThemes().map(
|
||||||
|
(theme) =>
|
||||||
|
html`<option value=${theme.value}>${theme.name}</option>`,
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group slider-wrap">
|
<div class="form-group slider-wrap">
|
||||||
<div class="slider-header">
|
<div class="slider-header">
|
||||||
<label class="form-label">Background Transparency</label>
|
<label class="form-label">Background Transparency</label>
|
||||||
<span class="slider-value">${Math.round(this.backgroundTransparency * 100)}%</span>
|
<span class="slider-value"
|
||||||
|
>${Math.round(this.backgroundTransparency * 100)}%</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
class="slider-input"
|
class="slider-input"
|
||||||
@@ -688,7 +801,8 @@ export class CustomizeView extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<section class="surface">
|
<section class="surface">
|
||||||
<div class="surface-title">Keyboard Shortcuts</div>
|
<div class="surface-title">Keyboard Shortcuts</div>
|
||||||
${this.getKeybindActions().map(action => html`
|
${this.getKeybindActions().map(
|
||||||
|
(action) => html`
|
||||||
<div class="keybind-row">
|
<div class="keybind-row">
|
||||||
<span class="keybind-name">${action.name}</span>
|
<span class="keybind-name">${action.name}</span>
|
||||||
<input
|
<input
|
||||||
@@ -701,9 +815,16 @@ export class CustomizeView extends LitElement {
|
|||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
`)}
|
`,
|
||||||
|
)}
|
||||||
<div style="margin-top: var(--space-sm);">
|
<div style="margin-top: var(--space-sm);">
|
||||||
<button class="control" style="width:auto;padding:8px 10px;" @click=${this.resetKeybinds}>Reset to defaults</button>
|
<button
|
||||||
|
class="control"
|
||||||
|
style="width:auto;padding:8px 10px;"
|
||||||
|
@click=${this.resetKeybinds}
|
||||||
|
>
|
||||||
|
Reset to defaults
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
@@ -714,16 +835,32 @@ export class CustomizeView extends LitElement {
|
|||||||
<section class="surface danger-surface">
|
<section class="surface danger-surface">
|
||||||
<div class="surface-title danger">Privacy and Data</div>
|
<div class="surface-title danger">Privacy and Data</div>
|
||||||
<div style="display:flex;gap:var(--space-sm);flex-wrap:wrap;">
|
<div style="display:flex;gap:var(--space-sm);flex-wrap:wrap;">
|
||||||
<button class="danger-button" @click=${this.restoreAllSettings} ?disabled=${this.isRestoring}>
|
<button
|
||||||
${this.isRestoring ? 'Restoring...' : 'Restore all settings'}
|
class="danger-button"
|
||||||
|
@click=${this.restoreAllSettings}
|
||||||
|
?disabled=${this.isRestoring}
|
||||||
|
>
|
||||||
|
${this.isRestoring ? "Restoring..." : "Restore all settings"}
|
||||||
</button>
|
</button>
|
||||||
<button class="danger-button" @click=${this.clearLocalData} ?disabled=${this.isClearing}>
|
<button
|
||||||
${this.isClearing ? 'Clearing...' : 'Delete all data'}
|
class="danger-button"
|
||||||
|
@click=${this.clearLocalData}
|
||||||
|
?disabled=${this.isClearing}
|
||||||
|
>
|
||||||
|
${this.isClearing ? "Clearing..." : "Delete all data"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
${this.clearStatusMessage ? html`
|
${this.clearStatusMessage
|
||||||
<div class="status ${this.clearStatusType === 'success' ? 'success' : 'error'}">${this.clearStatusMessage}</div>
|
? html`
|
||||||
` : ''}
|
<div
|
||||||
|
class="status ${this.clearStatusType === "success"
|
||||||
|
? "success"
|
||||||
|
: "error"}"
|
||||||
|
>
|
||||||
|
${this.clearStatusMessage}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
</section>
|
</section>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -733,16 +870,13 @@ export class CustomizeView extends LitElement {
|
|||||||
<div class="unified-page">
|
<div class="unified-page">
|
||||||
<div class="unified-wrap">
|
<div class="unified-wrap">
|
||||||
<div class="page-title">Settings</div>
|
<div class="page-title">Settings</div>
|
||||||
${this.renderAISection()}
|
${this.renderAISection()} ${this.renderAudioSection()}
|
||||||
${this.renderAudioSection()}
|
${this.renderLanguageSection()} ${this.renderAppearanceSection()}
|
||||||
${this.renderLanguageSection()}
|
${this.renderKeyboardSection()} ${this.renderPrivacySection()}
|
||||||
${this.renderAppearanceSection()}
|
|
||||||
${this.renderKeyboardSection()}
|
|
||||||
${this.renderPrivacySection()}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('customize-view', CustomizeView);
|
customElements.define("customize-view", CustomizeView);
|
||||||
|
|||||||
+765
-108
File diff suppressed because it is too large
Load Diff
@@ -312,7 +312,7 @@ export class OnboardingView extends LitElement {
|
|||||||
if (this.currentSlide === 0) {
|
if (this.currentSlide === 0) {
|
||||||
return html`
|
return html`
|
||||||
<div class="slide">
|
<div class="slide">
|
||||||
<div class="slide-title">Cheating Daddy</div>
|
<div class="slide-title">Mastermind</div>
|
||||||
<div class="slide-text">Real-time AI that listens, watches, and helps during interviews, meetings, and exams.</div>
|
<div class="slide-text">Real-time AI that listens, watches, and helps during interviews, meetings, and exams.</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn-primary" @click=${() => { this.currentSlide = 1; }}>Continue</button>
|
<button class="btn-primary" @click=${() => { this.currentSlide = 1; }}>Continue</button>
|
||||||
|
|||||||
+99
-62
@@ -1,11 +1,43 @@
|
|||||||
if (require('electron-squirrel-startup')) {
|
if (require("electron-squirrel-startup")) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { app, BrowserWindow, shell, ipcMain } = require('electron');
|
// ── Global crash handlers to prevent silent process termination ──
|
||||||
const { createWindow, updateGlobalShortcuts } = require('./utils/window');
|
process.on("uncaughtException", (error) => {
|
||||||
const { setupGeminiIpcHandlers, stopMacOSAudioCapture, sendToRenderer } = require('./utils/gemini');
|
console.error("[FATAL] Uncaught exception:", error);
|
||||||
const storage = require('./storage');
|
try {
|
||||||
|
const { sendToRenderer } = require("./utils/gemini");
|
||||||
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Fatal error: " + (error?.message || "unknown"),
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
// sendToRenderer may not be available yet
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (reason) => {
|
||||||
|
console.error("[FATAL] Unhandled promise rejection:", reason);
|
||||||
|
try {
|
||||||
|
const { sendToRenderer } = require("./utils/gemini");
|
||||||
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Unhandled error: " +
|
||||||
|
(reason instanceof Error ? reason.message : String(reason)),
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
// sendToRenderer may not be available yet
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { app, BrowserWindow, shell, ipcMain } = require("electron");
|
||||||
|
const { createWindow, updateGlobalShortcuts } = require("./utils/window");
|
||||||
|
const {
|
||||||
|
setupGeminiIpcHandlers,
|
||||||
|
stopMacOSAudioCapture,
|
||||||
|
sendToRenderer,
|
||||||
|
} = require("./utils/gemini");
|
||||||
|
const storage = require("./storage");
|
||||||
|
|
||||||
const geminiSessionRef = { current: null };
|
const geminiSessionRef = { current: null };
|
||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
@@ -20,9 +52,9 @@ app.whenReady().then(async () => {
|
|||||||
storage.initializeStorage();
|
storage.initializeStorage();
|
||||||
|
|
||||||
// Trigger screen recording permission prompt on macOS if not already granted
|
// Trigger screen recording permission prompt on macOS if not already granted
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === "darwin") {
|
||||||
const { desktopCapturer } = require('electron');
|
const { desktopCapturer } = require("electron");
|
||||||
desktopCapturer.getSources({ types: ['screen'] }).catch(() => {});
|
desktopCapturer.getSources({ types: ["screen"] }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
@@ -31,18 +63,18 @@ app.whenReady().then(async () => {
|
|||||||
setupGeneralIpcHandlers();
|
setupGeneralIpcHandlers();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on("window-all-closed", () => {
|
||||||
stopMacOSAudioCapture();
|
stopMacOSAudioCapture();
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== "darwin") {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('before-quit', () => {
|
app.on("before-quit", () => {
|
||||||
stopMacOSAudioCapture();
|
stopMacOSAudioCapture();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on("activate", () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
}
|
}
|
||||||
@@ -50,250 +82,255 @@ app.on('activate', () => {
|
|||||||
|
|
||||||
function setupStorageIpcHandlers() {
|
function setupStorageIpcHandlers() {
|
||||||
// ============ CONFIG ============
|
// ============ CONFIG ============
|
||||||
ipcMain.handle('storage:get-config', async () => {
|
ipcMain.handle("storage:get-config", async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getConfig() };
|
return { success: true, data: storage.getConfig() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting config:', error);
|
console.error("Error getting config:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:set-config', async (event, config) => {
|
ipcMain.handle("storage:set-config", async (event, config) => {
|
||||||
try {
|
try {
|
||||||
storage.setConfig(config);
|
storage.setConfig(config);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting config:', error);
|
console.error("Error setting config:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:update-config', async (event, key, value) => {
|
ipcMain.handle("storage:update-config", async (event, key, value) => {
|
||||||
try {
|
try {
|
||||||
storage.updateConfig(key, value);
|
storage.updateConfig(key, value);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating config:', error);
|
console.error("Error updating config:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ CREDENTIALS ============
|
// ============ CREDENTIALS ============
|
||||||
ipcMain.handle('storage:get-credentials', async () => {
|
ipcMain.handle("storage:get-credentials", async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getCredentials() };
|
return { success: true, data: storage.getCredentials() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting credentials:', error);
|
console.error("Error getting credentials:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:set-credentials', async (event, credentials) => {
|
ipcMain.handle("storage:set-credentials", async (event, credentials) => {
|
||||||
try {
|
try {
|
||||||
storage.setCredentials(credentials);
|
storage.setCredentials(credentials);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting credentials:', error);
|
console.error("Error setting credentials:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:get-api-key', async () => {
|
ipcMain.handle("storage:get-api-key", async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getApiKey() };
|
return { success: true, data: storage.getApiKey() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting API key:', error);
|
console.error("Error getting API key:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:set-api-key', async (event, apiKey) => {
|
ipcMain.handle("storage:set-api-key", async (event, apiKey) => {
|
||||||
try {
|
try {
|
||||||
storage.setApiKey(apiKey);
|
storage.setApiKey(apiKey);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting API key:', error);
|
console.error("Error setting API key:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:get-groq-api-key', async () => {
|
ipcMain.handle("storage:get-groq-api-key", async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getGroqApiKey() };
|
return { success: true, data: storage.getGroqApiKey() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting Groq API key:', error);
|
console.error("Error getting Groq API key:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:set-groq-api-key', async (event, groqApiKey) => {
|
ipcMain.handle("storage:set-groq-api-key", async (event, groqApiKey) => {
|
||||||
try {
|
try {
|
||||||
storage.setGroqApiKey(groqApiKey);
|
storage.setGroqApiKey(groqApiKey);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting Groq API key:', error);
|
console.error("Error setting Groq API key:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ PREFERENCES ============
|
// ============ PREFERENCES ============
|
||||||
ipcMain.handle('storage:get-preferences', async () => {
|
ipcMain.handle("storage:get-preferences", async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getPreferences() };
|
return { success: true, data: storage.getPreferences() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting preferences:', error);
|
console.error("Error getting preferences:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:set-preferences', async (event, preferences) => {
|
ipcMain.handle("storage:set-preferences", async (event, preferences) => {
|
||||||
try {
|
try {
|
||||||
storage.setPreferences(preferences);
|
storage.setPreferences(preferences);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting preferences:', error);
|
console.error("Error setting preferences:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:update-preference', async (event, key, value) => {
|
ipcMain.handle("storage:update-preference", async (event, key, value) => {
|
||||||
try {
|
try {
|
||||||
storage.updatePreference(key, value);
|
storage.updatePreference(key, value);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating preference:', error);
|
console.error("Error updating preference:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ KEYBINDS ============
|
// ============ KEYBINDS ============
|
||||||
ipcMain.handle('storage:get-keybinds', async () => {
|
ipcMain.handle("storage:get-keybinds", async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getKeybinds() };
|
return { success: true, data: storage.getKeybinds() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting keybinds:', error);
|
console.error("Error getting keybinds:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:set-keybinds', async (event, keybinds) => {
|
ipcMain.handle("storage:set-keybinds", async (event, keybinds) => {
|
||||||
try {
|
try {
|
||||||
storage.setKeybinds(keybinds);
|
storage.setKeybinds(keybinds);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting keybinds:', error);
|
console.error("Error setting keybinds:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ HISTORY ============
|
// ============ HISTORY ============
|
||||||
ipcMain.handle('storage:get-all-sessions', async () => {
|
ipcMain.handle("storage:get-all-sessions", async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getAllSessions() };
|
return { success: true, data: storage.getAllSessions() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting sessions:', error);
|
console.error("Error getting sessions:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:get-session', async (event, sessionId) => {
|
ipcMain.handle("storage:get-session", async (event, sessionId) => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getSession(sessionId) };
|
return { success: true, data: storage.getSession(sessionId) };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting session:', error);
|
console.error("Error getting session:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:save-session', async (event, sessionId, data) => {
|
ipcMain.handle("storage:save-session", async (event, sessionId, data) => {
|
||||||
try {
|
try {
|
||||||
storage.saveSession(sessionId, data);
|
storage.saveSession(sessionId, data);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving session:', error);
|
console.error("Error saving session:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:delete-session', async (event, sessionId) => {
|
ipcMain.handle("storage:delete-session", async (event, sessionId) => {
|
||||||
try {
|
try {
|
||||||
storage.deleteSession(sessionId);
|
storage.deleteSession(sessionId);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting session:', error);
|
console.error("Error deleting session:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('storage:delete-all-sessions', async () => {
|
ipcMain.handle("storage:delete-all-sessions", async () => {
|
||||||
try {
|
try {
|
||||||
storage.deleteAllSessions();
|
storage.deleteAllSessions();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting all sessions:', error);
|
console.error("Error deleting all sessions:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ LIMITS ============
|
// ============ LIMITS ============
|
||||||
ipcMain.handle('storage:get-today-limits', async () => {
|
ipcMain.handle("storage:get-today-limits", async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getTodayLimits() };
|
return { success: true, data: storage.getTodayLimits() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting today limits:', error);
|
console.error("Error getting today limits:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ CLEAR ALL ============
|
// ============ CLEAR ALL ============
|
||||||
ipcMain.handle('storage:clear-all', async () => {
|
ipcMain.handle("storage:clear-all", async () => {
|
||||||
try {
|
try {
|
||||||
storage.clearAllData();
|
storage.clearAllData();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error clearing all data:', error);
|
console.error("Error clearing all data:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupGeneralIpcHandlers() {
|
function setupGeneralIpcHandlers() {
|
||||||
ipcMain.handle('get-app-version', async () => {
|
ipcMain.handle("get-app-version", async () => {
|
||||||
return app.getVersion();
|
return app.getVersion();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('quit-application', async event => {
|
ipcMain.handle("quit-application", async (event) => {
|
||||||
try {
|
try {
|
||||||
stopMacOSAudioCapture();
|
stopMacOSAudioCapture();
|
||||||
app.quit();
|
app.quit();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error quitting application:', error);
|
console.error("Error quitting application:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('open-external', async (event, url) => {
|
ipcMain.handle("open-external", async (event, url) => {
|
||||||
try {
|
try {
|
||||||
await shell.openExternal(url);
|
await shell.openExternal(url);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error opening external URL:', error);
|
console.error("Error opening external URL:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('update-keybinds', (event, newKeybinds) => {
|
ipcMain.on("update-keybinds", (event, newKeybinds) => {
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
// Also save to storage
|
// Also save to storage
|
||||||
storage.setKeybinds(newKeybinds);
|
storage.setKeybinds(newKeybinds);
|
||||||
updateGlobalShortcuts(newKeybinds, mainWindow, sendToRenderer, geminiSessionRef);
|
updateGlobalShortcuts(
|
||||||
|
newKeybinds,
|
||||||
|
mainWindow,
|
||||||
|
sendToRenderer,
|
||||||
|
geminiSessionRef,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Debug logging from renderer
|
// Debug logging from renderer
|
||||||
ipcMain.on('log-message', (event, msg) => {
|
ipcMain.on("log-message", (event, msg) => {
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+128
-85
@@ -1,6 +1,6 @@
|
|||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const os = require('os');
|
const os = require("os");
|
||||||
|
|
||||||
const CONFIG_VERSION = 1;
|
const CONFIG_VERSION = 1;
|
||||||
|
|
||||||
@@ -8,34 +8,39 @@ 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 = {
|
||||||
apiKey: '',
|
apiKey: "",
|
||||||
groqApiKey: ''
|
groqApiKey: "",
|
||||||
|
openaiCompatibleApiKey: "",
|
||||||
|
openaiCompatibleBaseUrl: "",
|
||||||
|
openaiCompatibleModel: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_PREFERENCES = {
|
const DEFAULT_PREFERENCES = {
|
||||||
customPrompt: '',
|
customPrompt: "",
|
||||||
selectedProfile: 'interview',
|
selectedProfile: "interview",
|
||||||
selectedLanguage: 'en-US',
|
selectedLanguage: "en-US",
|
||||||
selectedScreenshotInterval: '5',
|
selectedScreenshotInterval: "5",
|
||||||
selectedImageQuality: 'medium',
|
selectedImageQuality: "medium",
|
||||||
advancedMode: false,
|
advancedMode: false,
|
||||||
audioMode: 'speaker_only',
|
audioMode: "speaker_only",
|
||||||
fontSize: 'medium',
|
fontSize: "medium",
|
||||||
backgroundTransparency: 0.8,
|
backgroundTransparency: 0.8,
|
||||||
googleSearchEnabled: false,
|
googleSearchEnabled: false,
|
||||||
ollamaHost: 'http://127.0.0.1:11434',
|
responseProvider: "gemini",
|
||||||
ollamaModel: 'llama3.1',
|
ollamaHost: "http://127.0.0.1:11434",
|
||||||
whisperModel: 'Xenova/whisper-small',
|
ollamaModel: "llama3.1",
|
||||||
|
whisperModel: "Xenova/whisper-small",
|
||||||
|
whisperDevice: "", // '' = auto-detect, 'cpu' = native, 'wasm' = compatible
|
||||||
};
|
};
|
||||||
|
|
||||||
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 }, flashLite: { count }, groq: { 'qwen3-32b': { chars, limit }, 'gpt-oss-120b': { chars, limit }, 'gpt-oss-20b': { chars, limit } }, gemini: { 'gemma-3-27b-it': { chars } } }
|
data: [], // Array of { date: 'YYYY-MM-DD', flash: { count }, flashLite: { count }, groq: { 'qwen3-32b': { chars, limit }, 'gpt-oss-120b': { chars, limit }, 'gpt-oss-20b': { chars, limit } }, gemini: { 'gemma-3-27b-it': { chars } } }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the config directory path based on OS
|
// Get the config directory path based on OS
|
||||||
@@ -43,12 +48,22 @@ function getConfigDir() {
|
|||||||
const platform = os.platform();
|
const platform = os.platform();
|
||||||
let configDir;
|
let configDir;
|
||||||
|
|
||||||
if (platform === 'win32') {
|
if (platform === "win32") {
|
||||||
configDir = path.join(os.homedir(), 'AppData', 'Roaming', 'cheating-daddy-config');
|
configDir = path.join(
|
||||||
} else if (platform === 'darwin') {
|
os.homedir(),
|
||||||
configDir = path.join(os.homedir(), 'Library', 'Application Support', 'cheating-daddy-config');
|
"AppData",
|
||||||
|
"Roaming",
|
||||||
|
"cheating-daddy-config",
|
||||||
|
);
|
||||||
|
} else if (platform === "darwin") {
|
||||||
|
configDir = path.join(
|
||||||
|
os.homedir(),
|
||||||
|
"Library",
|
||||||
|
"Application Support",
|
||||||
|
"cheating-daddy-config",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
configDir = path.join(os.homedir(), '.config', 'cheating-daddy-config');
|
configDir = path.join(os.homedir(), ".config", "cheating-daddy-config");
|
||||||
}
|
}
|
||||||
|
|
||||||
return configDir;
|
return configDir;
|
||||||
@@ -56,34 +71,34 @@ function getConfigDir() {
|
|||||||
|
|
||||||
// File paths
|
// File paths
|
||||||
function getConfigPath() {
|
function getConfigPath() {
|
||||||
return path.join(getConfigDir(), 'config.json');
|
return path.join(getConfigDir(), "config.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCredentialsPath() {
|
function getCredentialsPath() {
|
||||||
return path.join(getConfigDir(), 'credentials.json');
|
return path.join(getConfigDir(), "credentials.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPreferencesPath() {
|
function getPreferencesPath() {
|
||||||
return path.join(getConfigDir(), 'preferences.json');
|
return path.join(getConfigDir(), "preferences.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getKeybindsPath() {
|
function getKeybindsPath() {
|
||||||
return path.join(getConfigDir(), 'keybinds.json');
|
return path.join(getConfigDir(), "keybinds.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLimitsPath() {
|
function getLimitsPath() {
|
||||||
return path.join(getConfigDir(), 'limits.json');
|
return path.join(getConfigDir(), "limits.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHistoryDir() {
|
function getHistoryDir() {
|
||||||
return path.join(getConfigDir(), 'history');
|
return path.join(getConfigDir(), "history");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to read JSON file safely
|
// Helper to read JSON file safely
|
||||||
function readJsonFile(filePath, defaultValue) {
|
function readJsonFile(filePath, defaultValue) {
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
const data = fs.readFileSync(filePath, 'utf8');
|
const data = fs.readFileSync(filePath, "utf8");
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -99,7 +114,7 @@ function writeJsonFile(filePath, data) {
|
|||||||
if (!fs.existsSync(dir)) {
|
if (!fs.existsSync(dir)) {
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error writing ${filePath}:`, error.message);
|
console.error(`Error writing ${filePath}:`, error.message);
|
||||||
@@ -115,7 +130,7 @@ function needsReset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
||||||
return !config.configVersion || config.configVersion !== CONFIG_VERSION;
|
return !config.configVersion || config.configVersion !== CONFIG_VERSION;
|
||||||
} catch {
|
} catch {
|
||||||
return true;
|
return true;
|
||||||
@@ -126,7 +141,7 @@ function needsReset() {
|
|||||||
function resetConfigDir() {
|
function resetConfigDir() {
|
||||||
const configDir = getConfigDir();
|
const configDir = getConfigDir();
|
||||||
|
|
||||||
console.log('Resetting config directory...');
|
console.log("Resetting config directory...");
|
||||||
|
|
||||||
// Remove existing directory if it exists
|
// Remove existing directory if it exists
|
||||||
if (fs.existsSync(configDir)) {
|
if (fs.existsSync(configDir)) {
|
||||||
@@ -142,7 +157,7 @@ function resetConfigDir() {
|
|||||||
writeJsonFile(getCredentialsPath(), DEFAULT_CREDENTIALS);
|
writeJsonFile(getCredentialsPath(), DEFAULT_CREDENTIALS);
|
||||||
writeJsonFile(getPreferencesPath(), DEFAULT_PREFERENCES);
|
writeJsonFile(getPreferencesPath(), DEFAULT_PREFERENCES);
|
||||||
|
|
||||||
console.log('Config directory initialized with defaults');
|
console.log("Config directory initialized with defaults");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize storage - call this on app startup
|
// Initialize storage - call this on app startup
|
||||||
@@ -189,7 +204,7 @@ function setCredentials(credentials) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getApiKey() {
|
function getApiKey() {
|
||||||
return getCredentials().apiKey || '';
|
return getCredentials().apiKey || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function setApiKey(apiKey) {
|
function setApiKey(apiKey) {
|
||||||
@@ -197,13 +212,30 @@ function setApiKey(apiKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getGroqApiKey() {
|
function getGroqApiKey() {
|
||||||
return getCredentials().groqApiKey || '';
|
return getCredentials().groqApiKey || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function setGroqApiKey(groqApiKey) {
|
function setGroqApiKey(groqApiKey) {
|
||||||
return setCredentials({ groqApiKey });
|
return setCredentials({ groqApiKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOpenAICompatibleConfig() {
|
||||||
|
const creds = getCredentials();
|
||||||
|
return {
|
||||||
|
apiKey: creds.openaiCompatibleApiKey || "",
|
||||||
|
baseUrl: creds.openaiCompatibleBaseUrl || "",
|
||||||
|
model: creds.openaiCompatibleModel || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOpenAICompatibleConfig(apiKey, baseUrl, model) {
|
||||||
|
return setCredentials({
|
||||||
|
openaiCompatibleApiKey: apiKey,
|
||||||
|
openaiCompatibleBaseUrl: baseUrl,
|
||||||
|
openaiCompatibleModel: model,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ============ PREFERENCES ============
|
// ============ PREFERENCES ============
|
||||||
|
|
||||||
function getPreferences() {
|
function getPreferences() {
|
||||||
@@ -245,7 +277,7 @@ function setLimits(limits) {
|
|||||||
|
|
||||||
function getTodayDateString() {
|
function getTodayDateString() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
return now.toISOString().split('T')[0]; // YYYY-MM-DD
|
return now.toISOString().split("T")[0]; // YYYY-MM-DD
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTodayLimits() {
|
function getTodayLimits() {
|
||||||
@@ -253,21 +285,21 @@ function getTodayLimits() {
|
|||||||
const today = getTodayDateString();
|
const today = getTodayDateString();
|
||||||
|
|
||||||
// Find today's entry
|
// Find today's entry
|
||||||
const todayEntry = limits.data.find(entry => entry.date === today);
|
const todayEntry = limits.data.find((entry) => entry.date === today);
|
||||||
|
|
||||||
if (todayEntry) {
|
if (todayEntry) {
|
||||||
// ensure new fields exist
|
// ensure new fields exist
|
||||||
if(!todayEntry.groq) {
|
if (!todayEntry.groq) {
|
||||||
todayEntry.groq = {
|
todayEntry.groq = {
|
||||||
'qwen3-32b': { chars: 0, limit: 1500000 },
|
"qwen3-32b": { chars: 0, limit: 1500000 },
|
||||||
'gpt-oss-120b': { chars: 0, limit: 600000 },
|
"gpt-oss-120b": { chars: 0, limit: 600000 },
|
||||||
'gpt-oss-20b': { chars: 0, limit: 600000 },
|
"gpt-oss-20b": { chars: 0, limit: 600000 },
|
||||||
'kimi-k2-instruct': { chars: 0, limit: 600000 }
|
"kimi-k2-instruct": { chars: 0, limit: 600000 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if(!todayEntry.gemini) {
|
if (!todayEntry.gemini) {
|
||||||
todayEntry.gemini = {
|
todayEntry.gemini = {
|
||||||
'gemma-3-27b-it': { chars: 0 }
|
"gemma-3-27b-it": { chars: 0 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
setLimits(limits);
|
setLimits(limits);
|
||||||
@@ -275,20 +307,20 @@ function getTodayLimits() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// No entry for today - clean old entries and create new one
|
// No entry for today - clean old entries and create new one
|
||||||
limits.data = limits.data.filter(entry => entry.date === today);
|
limits.data = limits.data.filter((entry) => entry.date === today);
|
||||||
const newEntry = {
|
const newEntry = {
|
||||||
date: today,
|
date: today,
|
||||||
flash: { count: 0 },
|
flash: { count: 0 },
|
||||||
flashLite: { count: 0 },
|
flashLite: { count: 0 },
|
||||||
groq: {
|
groq: {
|
||||||
'qwen3-32b': { chars: 0, limit: 1500000 },
|
"qwen3-32b": { chars: 0, limit: 1500000 },
|
||||||
'gpt-oss-120b': { chars: 0, limit: 600000 },
|
"gpt-oss-120b": { chars: 0, limit: 600000 },
|
||||||
'gpt-oss-20b': { chars: 0, limit: 600000 },
|
"gpt-oss-20b": { chars: 0, limit: 600000 },
|
||||||
'kimi-k2-instruct': { chars: 0, limit: 600000 }
|
"kimi-k2-instruct": { chars: 0, limit: 600000 },
|
||||||
},
|
},
|
||||||
gemini: {
|
gemini: {
|
||||||
'gemma-3-27b-it': { chars: 0 }
|
"gemma-3-27b-it": { chars: 0 },
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
limits.data.push(newEntry);
|
limits.data.push(newEntry);
|
||||||
setLimits(limits);
|
setLimits(limits);
|
||||||
@@ -301,7 +333,7 @@ function incrementLimitCount(model) {
|
|||||||
const today = getTodayDateString();
|
const today = getTodayDateString();
|
||||||
|
|
||||||
// Find or create today's entry
|
// Find or create today's entry
|
||||||
let todayEntry = limits.data.find(entry => entry.date === today);
|
let todayEntry = limits.data.find((entry) => entry.date === today);
|
||||||
|
|
||||||
if (!todayEntry) {
|
if (!todayEntry) {
|
||||||
// Clean old entries and create new one
|
// Clean old entries and create new one
|
||||||
@@ -309,18 +341,18 @@ 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 {
|
||||||
// Clean old entries, keep only today
|
// Clean old entries, keep only today
|
||||||
limits.data = limits.data.filter(entry => entry.date === today);
|
limits.data = limits.data.filter((entry) => entry.date === today);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment the appropriate model count
|
// Increment the appropriate model count
|
||||||
if (model === 'gemini-2.5-flash') {
|
if (model === "gemini-2.5-flash") {
|
||||||
todayEntry.flash.count++;
|
todayEntry.flash.count++;
|
||||||
} else if (model === 'gemini-2.5-flash-lite') {
|
} else if (model === "gemini-2.5-flash-lite") {
|
||||||
todayEntry.flashLite.count++;
|
todayEntry.flashLite.count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,9 +365,9 @@ function incrementCharUsage(provider, model, charCount) {
|
|||||||
|
|
||||||
const limits = getLimits();
|
const limits = getLimits();
|
||||||
const today = getTodayDateString();
|
const today = getTodayDateString();
|
||||||
const todayEntry = limits.data.find(entry => entry.date === today);
|
const todayEntry = limits.data.find((entry) => entry.date === today);
|
||||||
|
|
||||||
if(todayEntry[provider] && todayEntry[provider][model]) {
|
if (todayEntry[provider] && todayEntry[provider][model]) {
|
||||||
todayEntry[provider][model].chars += charCount;
|
todayEntry[provider][model].chars += charCount;
|
||||||
setLimits(limits);
|
setLimits(limits);
|
||||||
}
|
}
|
||||||
@@ -349,29 +381,29 @@ function getAvailableModel() {
|
|||||||
// RPD limits: flash = 20, flash-lite = 20
|
// RPD limits: flash = 20, flash-lite = 20
|
||||||
// After both exhausted, fall back to flash (for paid API users)
|
// After both exhausted, fall back to flash (for paid API users)
|
||||||
if (todayLimits.flash.count < 20) {
|
if (todayLimits.flash.count < 20) {
|
||||||
return 'gemini-2.5-flash';
|
return "gemini-2.5-flash";
|
||||||
} else if (todayLimits.flashLite.count < 20) {
|
} else if (todayLimits.flashLite.count < 20) {
|
||||||
return 'gemini-2.5-flash-lite';
|
return "gemini-2.5-flash-lite";
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'gemini-2.5-flash'; // Default to flash for paid API users
|
return "gemini-2.5-flash"; // Default to flash for paid API users
|
||||||
}
|
}
|
||||||
|
|
||||||
function getModelForToday() {
|
function getModelForToday() {
|
||||||
const todayEntry = getTodayLimits();
|
const todayEntry = getTodayLimits();
|
||||||
const groq = todayEntry.groq;
|
const groq = todayEntry.groq;
|
||||||
|
|
||||||
if (groq['qwen3-32b'].chars < groq['qwen3-32b'].limit) {
|
if (groq["qwen3-32b"].chars < groq["qwen3-32b"].limit) {
|
||||||
return 'qwen/qwen3-32b';
|
return "qwen/qwen3-32b";
|
||||||
}
|
}
|
||||||
if (groq['gpt-oss-120b'].chars < groq['gpt-oss-120b'].limit) {
|
if (groq["gpt-oss-120b"].chars < groq["gpt-oss-120b"].limit) {
|
||||||
return 'openai/gpt-oss-120b';
|
return "openai/gpt-oss-120b";
|
||||||
}
|
}
|
||||||
if (groq['gpt-oss-20b'].chars < groq['gpt-oss-20b'].limit) {
|
if (groq["gpt-oss-20b"].chars < groq["gpt-oss-20b"].limit) {
|
||||||
return 'openai/gpt-oss-20b';
|
return "openai/gpt-oss-20b";
|
||||||
}
|
}
|
||||||
if (groq['kimi-k2-instruct'].chars < groq['kimi-k2-instruct'].limit) {
|
if (groq["kimi-k2-instruct"].chars < groq["kimi-k2-instruct"].limit) {
|
||||||
return 'moonshotai/kimi-k2-instruct';
|
return "moonshotai/kimi-k2-instruct";
|
||||||
}
|
}
|
||||||
|
|
||||||
// All limits exhausted
|
// All limits exhausted
|
||||||
@@ -398,8 +430,12 @@ function saveSession(sessionId, data) {
|
|||||||
profile: data.profile || existingSession?.profile || null,
|
profile: data.profile || existingSession?.profile || null,
|
||||||
customPrompt: data.customPrompt || existingSession?.customPrompt || null,
|
customPrompt: data.customPrompt || existingSession?.customPrompt || null,
|
||||||
// Conversation data
|
// Conversation data
|
||||||
conversationHistory: data.conversationHistory || existingSession?.conversationHistory || [],
|
conversationHistory:
|
||||||
screenAnalysisHistory: data.screenAnalysisHistory || existingSession?.screenAnalysisHistory || []
|
data.conversationHistory || existingSession?.conversationHistory || [],
|
||||||
|
screenAnalysisHistory:
|
||||||
|
data.screenAnalysisHistory ||
|
||||||
|
existingSession?.screenAnalysisHistory ||
|
||||||
|
[],
|
||||||
};
|
};
|
||||||
return writeJsonFile(sessionPath, sessionData);
|
return writeJsonFile(sessionPath, sessionData);
|
||||||
}
|
}
|
||||||
@@ -416,17 +452,19 @@ function getAllSessions() {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = fs.readdirSync(historyDir)
|
const files = fs
|
||||||
.filter(f => f.endsWith('.json'))
|
.readdirSync(historyDir)
|
||||||
|
.filter((f) => f.endsWith(".json"))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
// Sort by timestamp descending (newest first)
|
// Sort by timestamp descending (newest first)
|
||||||
const tsA = parseInt(a.replace('.json', ''));
|
const tsA = parseInt(a.replace(".json", ""));
|
||||||
const tsB = parseInt(b.replace('.json', ''));
|
const tsB = parseInt(b.replace(".json", ""));
|
||||||
return tsB - tsA;
|
return tsB - tsA;
|
||||||
});
|
});
|
||||||
|
|
||||||
return files.map(file => {
|
return files
|
||||||
const sessionId = file.replace('.json', '');
|
.map((file) => {
|
||||||
|
const sessionId = file.replace(".json", "");
|
||||||
const data = readJsonFile(path.join(historyDir, file), null);
|
const data = readJsonFile(path.join(historyDir, file), null);
|
||||||
if (data) {
|
if (data) {
|
||||||
return {
|
return {
|
||||||
@@ -436,13 +474,14 @@ function getAllSessions() {
|
|||||||
messageCount: data.conversationHistory?.length || 0,
|
messageCount: data.conversationHistory?.length || 0,
|
||||||
screenAnalysisCount: data.screenAnalysisHistory?.length || 0,
|
screenAnalysisCount: data.screenAnalysisHistory?.length || 0,
|
||||||
profile: data.profile || null,
|
profile: data.profile || null,
|
||||||
customPrompt: data.customPrompt || null
|
customPrompt: data.customPrompt || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}).filter(Boolean);
|
})
|
||||||
|
.filter(Boolean);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error reading sessions:', error.message);
|
console.error("Error reading sessions:", error.message);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -455,7 +494,7 @@ function deleteSession(sessionId) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting session:', error.message);
|
console.error("Error deleting session:", error.message);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -464,14 +503,16 @@ function deleteAllSessions() {
|
|||||||
const historyDir = getHistoryDir();
|
const historyDir = getHistoryDir();
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(historyDir)) {
|
if (fs.existsSync(historyDir)) {
|
||||||
const files = fs.readdirSync(historyDir).filter(f => f.endsWith('.json'));
|
const files = fs
|
||||||
files.forEach(file => {
|
.readdirSync(historyDir)
|
||||||
|
.filter((f) => f.endsWith(".json"));
|
||||||
|
files.forEach((file) => {
|
||||||
fs.unlinkSync(path.join(historyDir, file));
|
fs.unlinkSync(path.join(historyDir, file));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting all sessions:', error.message);
|
console.error("Error deleting all sessions:", error.message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -500,6 +541,8 @@ module.exports = {
|
|||||||
setApiKey,
|
setApiKey,
|
||||||
getGroqApiKey,
|
getGroqApiKey,
|
||||||
setGroqApiKey,
|
setGroqApiKey,
|
||||||
|
getOpenAICompatibleConfig,
|
||||||
|
setOpenAICompatibleConfig,
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
getPreferences,
|
getPreferences,
|
||||||
@@ -527,5 +570,5 @@ module.exports = {
|
|||||||
deleteAllSessions,
|
deleteAllSessions,
|
||||||
|
|
||||||
// Clear all
|
// Clear all
|
||||||
clearAllData
|
clearAllData,
|
||||||
};
|
};
|
||||||
|
|||||||
+611
-267
File diff suppressed because it is too large
Load Diff
+533
-116
@@ -1,17 +1,31 @@
|
|||||||
const { Ollama } = require('ollama');
|
const { Ollama } = require("ollama");
|
||||||
const { getSystemPrompt } = require('./prompts');
|
const { getSystemPrompt } = require("./prompts");
|
||||||
const { sendToRenderer, initializeNewSession, saveConversationTurn } = require('./gemini');
|
const {
|
||||||
|
sendToRenderer,
|
||||||
|
initializeNewSession,
|
||||||
|
saveConversationTurn,
|
||||||
|
} = require("./gemini");
|
||||||
|
const { fork } = require("child_process");
|
||||||
|
const path = require("path");
|
||||||
|
const { getSystemNode } = require("./nodeDetect");
|
||||||
|
|
||||||
// ── State ──
|
// ── State ──
|
||||||
|
|
||||||
let ollamaClient = null;
|
let ollamaClient = null;
|
||||||
let ollamaModel = null;
|
let ollamaModel = null;
|
||||||
let whisperPipeline = null;
|
let whisperWorker = null;
|
||||||
let isWhisperLoading = false;
|
let isWhisperLoading = false;
|
||||||
|
let whisperReady = false;
|
||||||
let localConversationHistory = [];
|
let localConversationHistory = [];
|
||||||
let currentSystemPrompt = null;
|
let currentSystemPrompt = null;
|
||||||
let isLocalActive = false;
|
let isLocalActive = false;
|
||||||
|
|
||||||
|
// Set when we intentionally kill the worker to suppress crash handling
|
||||||
|
let whisperShuttingDown = false;
|
||||||
|
|
||||||
|
// Pending transcription callback (one at a time)
|
||||||
|
let pendingTranscribe = null;
|
||||||
|
|
||||||
// VAD state
|
// VAD state
|
||||||
let isSpeaking = false;
|
let isSpeaking = false;
|
||||||
let speechBuffers = [];
|
let speechBuffers = [];
|
||||||
@@ -20,13 +34,32 @@ let speechFrameCount = 0;
|
|||||||
|
|
||||||
// VAD configuration
|
// VAD configuration
|
||||||
const VAD_MODES = {
|
const VAD_MODES = {
|
||||||
NORMAL: { energyThreshold: 0.01, speechFramesRequired: 3, silenceFramesRequired: 30 },
|
NORMAL: {
|
||||||
LOW_BITRATE: { energyThreshold: 0.008, speechFramesRequired: 4, silenceFramesRequired: 35 },
|
energyThreshold: 0.01,
|
||||||
AGGRESSIVE: { energyThreshold: 0.015, speechFramesRequired: 2, silenceFramesRequired: 20 },
|
speechFramesRequired: 3,
|
||||||
VERY_AGGRESSIVE: { energyThreshold: 0.02, speechFramesRequired: 2, silenceFramesRequired: 15 },
|
silenceFramesRequired: 30,
|
||||||
|
},
|
||||||
|
LOW_BITRATE: {
|
||||||
|
energyThreshold: 0.008,
|
||||||
|
speechFramesRequired: 4,
|
||||||
|
silenceFramesRequired: 35,
|
||||||
|
},
|
||||||
|
AGGRESSIVE: {
|
||||||
|
energyThreshold: 0.015,
|
||||||
|
speechFramesRequired: 2,
|
||||||
|
silenceFramesRequired: 20,
|
||||||
|
},
|
||||||
|
VERY_AGGRESSIVE: {
|
||||||
|
energyThreshold: 0.02,
|
||||||
|
speechFramesRequired: 2,
|
||||||
|
silenceFramesRequired: 15,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let vadConfig = VAD_MODES.VERY_AGGRESSIVE;
|
let vadConfig = VAD_MODES.VERY_AGGRESSIVE;
|
||||||
|
|
||||||
|
// Maximum speech buffer size: ~30 seconds at 16kHz, 16-bit mono
|
||||||
|
const MAX_SPEECH_BUFFER_BYTES = 16000 * 2 * 30; // 960,000 bytes
|
||||||
|
|
||||||
// Audio resampling buffer
|
// Audio resampling buffer
|
||||||
let resampleRemainder = Buffer.alloc(0);
|
let resampleRemainder = Buffer.alloc(0);
|
||||||
|
|
||||||
@@ -47,15 +80,24 @@ function resample24kTo16k(inputBuffer) {
|
|||||||
const frac = srcPos - srcIndex;
|
const frac = srcPos - srcIndex;
|
||||||
|
|
||||||
const s0 = combined.readInt16LE(srcIndex * 2);
|
const s0 = combined.readInt16LE(srcIndex * 2);
|
||||||
const s1 = srcIndex + 1 < inputSamples ? combined.readInt16LE((srcIndex + 1) * 2) : s0;
|
const s1 =
|
||||||
|
srcIndex + 1 < inputSamples
|
||||||
|
? combined.readInt16LE((srcIndex + 1) * 2)
|
||||||
|
: s0;
|
||||||
const interpolated = Math.round(s0 + frac * (s1 - s0));
|
const interpolated = Math.round(s0 + frac * (s1 - s0));
|
||||||
outputBuffer.writeInt16LE(Math.max(-32768, Math.min(32767, interpolated)), i * 2);
|
outputBuffer.writeInt16LE(
|
||||||
|
Math.max(-32768, Math.min(32767, interpolated)),
|
||||||
|
i * 2,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store remainder for next call
|
// Store remainder for next call
|
||||||
const consumedInputSamples = Math.ceil((outputSamples * 3) / 2);
|
const consumedInputSamples = Math.ceil((outputSamples * 3) / 2);
|
||||||
const remainderStart = consumedInputSamples * 2;
|
const remainderStart = consumedInputSamples * 2;
|
||||||
resampleRemainder = remainderStart < combined.length ? combined.slice(remainderStart) : Buffer.alloc(0);
|
resampleRemainder =
|
||||||
|
remainderStart < combined.length
|
||||||
|
? combined.slice(remainderStart)
|
||||||
|
: Buffer.alloc(0);
|
||||||
|
|
||||||
return outputBuffer;
|
return outputBuffer;
|
||||||
}
|
}
|
||||||
@@ -84,8 +126,8 @@ function processVAD(pcm16kBuffer) {
|
|||||||
if (!isSpeaking && speechFrameCount >= vadConfig.speechFramesRequired) {
|
if (!isSpeaking && speechFrameCount >= vadConfig.speechFramesRequired) {
|
||||||
isSpeaking = true;
|
isSpeaking = true;
|
||||||
speechBuffers = [];
|
speechBuffers = [];
|
||||||
console.log('[LocalAI] Speech started (RMS:', rms.toFixed(4), ')');
|
console.log("[LocalAI] Speech started (RMS:", rms.toFixed(4), ")");
|
||||||
sendToRenderer('update-status', 'Listening... (speech detected)');
|
sendToRenderer("update-status", "Listening... (speech detected)");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
silenceFrameCount++;
|
silenceFrameCount++;
|
||||||
@@ -93,13 +135,23 @@ function processVAD(pcm16kBuffer) {
|
|||||||
|
|
||||||
if (isSpeaking && silenceFrameCount >= vadConfig.silenceFramesRequired) {
|
if (isSpeaking && silenceFrameCount >= vadConfig.silenceFramesRequired) {
|
||||||
isSpeaking = false;
|
isSpeaking = false;
|
||||||
console.log('[LocalAI] Speech ended, accumulated', speechBuffers.length, 'chunks');
|
console.log(
|
||||||
sendToRenderer('update-status', 'Transcribing...');
|
"[LocalAI] Speech ended, accumulated",
|
||||||
|
speechBuffers.length,
|
||||||
|
"chunks",
|
||||||
|
);
|
||||||
|
sendToRenderer("update-status", "Transcribing...");
|
||||||
|
|
||||||
// Trigger transcription with accumulated audio
|
// Trigger transcription with accumulated audio
|
||||||
const audioData = Buffer.concat(speechBuffers);
|
const audioData = Buffer.concat(speechBuffers);
|
||||||
speechBuffers = [];
|
speechBuffers = [];
|
||||||
handleSpeechEnd(audioData);
|
handleSpeechEnd(audioData).catch((err) => {
|
||||||
|
console.error("[LocalAI] handleSpeechEnd crashed:", err);
|
||||||
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Transcription error: " + (err?.message || "unknown"),
|
||||||
|
);
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,76 +159,395 @@ function processVAD(pcm16kBuffer) {
|
|||||||
// Accumulate audio during speech
|
// Accumulate audio during speech
|
||||||
if (isSpeaking) {
|
if (isSpeaking) {
|
||||||
speechBuffers.push(Buffer.from(pcm16kBuffer));
|
speechBuffers.push(Buffer.from(pcm16kBuffer));
|
||||||
|
|
||||||
|
// Cap buffer at ~30 seconds to prevent OOM and ONNX tensor overflow
|
||||||
|
const totalBytes = speechBuffers.reduce((sum, b) => sum + b.length, 0);
|
||||||
|
if (totalBytes >= MAX_SPEECH_BUFFER_BYTES) {
|
||||||
|
isSpeaking = false;
|
||||||
|
console.log(
|
||||||
|
"[LocalAI] Speech buffer limit reached (" +
|
||||||
|
totalBytes +
|
||||||
|
" bytes), forcing transcription",
|
||||||
|
);
|
||||||
|
sendToRenderer("update-status", "Transcribing (max length reached)...");
|
||||||
|
const audioData = Buffer.concat(speechBuffers);
|
||||||
|
speechBuffers = [];
|
||||||
|
silenceFrameCount = 0;
|
||||||
|
speechFrameCount = 0;
|
||||||
|
handleSpeechEnd(audioData).catch((err) => {
|
||||||
|
console.error("[LocalAI] handleSpeechEnd crashed:", err);
|
||||||
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Transcription error: " + (err?.message || "unknown"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Whisper Transcription ──
|
// ── Whisper Worker (isolated child process) ──
|
||||||
|
|
||||||
|
function spawnWhisperWorker() {
|
||||||
|
if (whisperWorker) return;
|
||||||
|
|
||||||
|
const workerPath = path.join(__dirname, "whisperWorker.js");
|
||||||
|
console.log("[LocalAI] Spawning Whisper worker:", workerPath);
|
||||||
|
|
||||||
|
// Determine the best way to spawn the worker:
|
||||||
|
// 1. System Node.js (preferred) — native addons were compiled against this
|
||||||
|
// ABI, so onnxruntime-node works without SIGTRAP / ABI mismatches.
|
||||||
|
// 2. Electron utilityProcess (packaged builds) — proper Node.js child
|
||||||
|
// process API that doesn't require the RunAsNode fuse.
|
||||||
|
// 3. ELECTRON_RUN_AS_NODE (last resort, dev only) — the old approach that
|
||||||
|
// only works when the RunAsNode fuse isn't flipped.
|
||||||
|
|
||||||
|
const systemNode = getSystemNode();
|
||||||
|
|
||||||
|
if (systemNode) {
|
||||||
|
// Spawn with system Node.js — onnxruntime-node native binary matches ABI
|
||||||
|
console.log("[LocalAI] Using system Node.js:", systemNode.nodePath);
|
||||||
|
whisperWorker = fork(workerPath, [], {
|
||||||
|
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
||||||
|
execPath: systemNode.nodePath,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
// Unset ELECTRON_RUN_AS_NODE so the system node doesn't inherit it
|
||||||
|
ELECTRON_RUN_AS_NODE: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No system Node.js found — try utilityProcess (Electron >= 22)
|
||||||
|
// utilityProcess.fork() creates a proper child Node.js process without
|
||||||
|
// needing the RunAsNode fuse. Falls back to ELECTRON_RUN_AS_NODE for
|
||||||
|
// dev mode where fuses aren't applied.
|
||||||
|
try {
|
||||||
|
const { utilityProcess: UP } = require("electron");
|
||||||
|
if (UP && typeof UP.fork === "function") {
|
||||||
|
console.log("[LocalAI] Using Electron utilityProcess");
|
||||||
|
const up = UP.fork(workerPath);
|
||||||
|
// Wrap utilityProcess to look like a ChildProcess for the rest of localai.js
|
||||||
|
whisperWorker = wrapUtilityProcess(up);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// utilityProcess not available (older Electron or renderer context)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
"[LocalAI] No system Node.js — falling back to ELECTRON_RUN_AS_NODE (WASM backend will be used)",
|
||||||
|
);
|
||||||
|
whisperWorker = fork(workerPath, [], {
|
||||||
|
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
||||||
|
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
whisperWorker.stdout.on("data", (data) => {
|
||||||
|
console.log("[WhisperWorker stdout]", data.toString().trim());
|
||||||
|
});
|
||||||
|
whisperWorker.stderr.on("data", (data) => {
|
||||||
|
console.error("[WhisperWorker stderr]", data.toString().trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
whisperWorker.on("message", (msg) => {
|
||||||
|
switch (msg.type) {
|
||||||
|
case "ready":
|
||||||
|
console.log("[LocalAI] Whisper worker ready");
|
||||||
|
break;
|
||||||
|
case "load-result":
|
||||||
|
handleWorkerLoadResult(msg);
|
||||||
|
break;
|
||||||
|
case "transcribe-result":
|
||||||
|
handleWorkerTranscribeResult(msg);
|
||||||
|
break;
|
||||||
|
case "status":
|
||||||
|
sendToRenderer("update-status", msg.message);
|
||||||
|
break;
|
||||||
|
case "progress":
|
||||||
|
sendToRenderer("whisper-progress", {
|
||||||
|
file: msg.file,
|
||||||
|
progress: msg.progress,
|
||||||
|
loaded: msg.loaded,
|
||||||
|
total: msg.total,
|
||||||
|
status: msg.status,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
whisperWorker.on("exit", (code, signal) => {
|
||||||
|
console.error(
|
||||||
|
"[LocalAI] Whisper worker exited — code:",
|
||||||
|
code,
|
||||||
|
"signal:",
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
whisperWorker = null;
|
||||||
|
whisperReady = false;
|
||||||
|
|
||||||
|
// If we intentionally shut down, don't treat as crash
|
||||||
|
if (whisperShuttingDown) {
|
||||||
|
whisperShuttingDown = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject any pending transcription
|
||||||
|
if (pendingTranscribe) {
|
||||||
|
pendingTranscribe.reject(
|
||||||
|
new Error(
|
||||||
|
"Whisper worker crashed (code: " + code + ", signal: " + signal + ")",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
pendingTranscribe = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If session is still active, inform the user and respawn
|
||||||
|
if (isLocalActive) {
|
||||||
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Whisper crashed (signal: " +
|
||||||
|
(signal || code) +
|
||||||
|
"). Respawning worker...",
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isLocalActive) {
|
||||||
|
respawnWhisperWorker();
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
whisperWorker.on("error", (err) => {
|
||||||
|
console.error("[LocalAI] Whisper worker error:", err);
|
||||||
|
whisperWorker = null;
|
||||||
|
whisperReady = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap Electron's utilityProcess to behave like a ChildProcess (duck-typing)
|
||||||
|
* so the rest of localai.js can use the same API.
|
||||||
|
*/
|
||||||
|
function wrapUtilityProcess(up) {
|
||||||
|
const EventEmitter = require("events");
|
||||||
|
const wrapper = new EventEmitter();
|
||||||
|
|
||||||
|
// Forward messages
|
||||||
|
up.on("message", (msg) => wrapper.emit("message", msg));
|
||||||
|
|
||||||
|
// Map utilityProcess exit to ChildProcess-like exit event
|
||||||
|
up.on("exit", (code) => wrapper.emit("exit", code, null));
|
||||||
|
|
||||||
|
// Provide stdout/stderr stubs (utilityProcess pipes to parent console)
|
||||||
|
const { Readable } = require("stream");
|
||||||
|
wrapper.stdout = new Readable({ read() {} });
|
||||||
|
wrapper.stderr = new Readable({ read() {} });
|
||||||
|
|
||||||
|
wrapper.send = (data) => up.postMessage(data);
|
||||||
|
wrapper.kill = (signal) => up.kill();
|
||||||
|
wrapper.removeAllListeners = () => {
|
||||||
|
up.removeAllListeners();
|
||||||
|
EventEmitter.prototype.removeAllListeners.call(wrapper);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup stdout/stderr forwarding
|
||||||
|
wrapper.stdout.on("data", (data) => {
|
||||||
|
console.log("[WhisperWorker stdout]", data.toString().trim());
|
||||||
|
});
|
||||||
|
wrapper.stderr.on("data", (data) => {
|
||||||
|
console.error("[WhisperWorker stderr]", data.toString().trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pendingLoad = null;
|
||||||
|
|
||||||
|
function handleWorkerLoadResult(msg) {
|
||||||
|
if (msg.success) {
|
||||||
|
console.log(
|
||||||
|
"[LocalAI] Whisper model loaded successfully (in worker, device:",
|
||||||
|
msg.device || "unknown",
|
||||||
|
")",
|
||||||
|
);
|
||||||
|
whisperReady = true;
|
||||||
|
sendToRenderer("whisper-downloading", false);
|
||||||
|
isWhisperLoading = false;
|
||||||
|
if (pendingLoad) {
|
||||||
|
pendingLoad.resolve(true);
|
||||||
|
pendingLoad = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("[LocalAI] Whisper worker failed to load model:", msg.error);
|
||||||
|
sendToRenderer("whisper-downloading", false);
|
||||||
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Failed to load Whisper model: " + msg.error,
|
||||||
|
);
|
||||||
|
isWhisperLoading = false;
|
||||||
|
if (pendingLoad) {
|
||||||
|
pendingLoad.resolve(false);
|
||||||
|
pendingLoad = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWorkerTranscribeResult(msg) {
|
||||||
|
if (!pendingTranscribe) return;
|
||||||
|
if (msg.success) {
|
||||||
|
console.log("[LocalAI] Transcription:", msg.text);
|
||||||
|
pendingTranscribe.resolve(msg.text || null);
|
||||||
|
} else {
|
||||||
|
console.error("[LocalAI] Worker transcription error:", msg.error);
|
||||||
|
pendingTranscribe.resolve(null);
|
||||||
|
}
|
||||||
|
pendingTranscribe = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function respawnWhisperWorker() {
|
||||||
|
killWhisperWorker();
|
||||||
|
spawnWhisperWorker();
|
||||||
|
const { app } = require("electron");
|
||||||
|
const cacheDir = path.join(app.getPath("userData"), "whisper-models");
|
||||||
|
const modelName =
|
||||||
|
require("../storage").getPreferences().whisperModel ||
|
||||||
|
"Xenova/whisper-small";
|
||||||
|
sendToRenderer("whisper-downloading", true);
|
||||||
|
isWhisperLoading = true;
|
||||||
|
const device = resolveWhisperDevice();
|
||||||
|
whisperWorker.send({ type: "load", modelName, cacheDir, device });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine which ONNX backend to use for Whisper inference.
|
||||||
|
* - "cpu" → onnxruntime-node (fast, native — requires matching ABI)
|
||||||
|
* - "wasm" → onnxruntime-web (slower but universally compatible)
|
||||||
|
*
|
||||||
|
* When spawned with system Node.js, native CPU backend is safe.
|
||||||
|
* Otherwise default to WASM to prevent native crashes.
|
||||||
|
*/
|
||||||
|
function resolveWhisperDevice() {
|
||||||
|
const prefs = require("../storage").getPreferences();
|
||||||
|
if (prefs.whisperDevice) return prefs.whisperDevice;
|
||||||
|
// Auto-detect: if we're running with system Node.js, native is safe
|
||||||
|
const systemNode = getSystemNode();
|
||||||
|
return systemNode ? "cpu" : "wasm";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map the app's BCP-47 language tag (e.g. "en-US", "ru-RU") to the
|
||||||
|
* ISO 639-1 code that Whisper expects (e.g. "en", "ru").
|
||||||
|
* Returns "auto" when the user selected auto-detect, which tells the
|
||||||
|
* worker to let Whisper detect the language itself.
|
||||||
|
*/
|
||||||
|
function resolveWhisperLanguage() {
|
||||||
|
const prefs = require("../storage").getPreferences();
|
||||||
|
const lang = prefs.selectedLanguage || "en-US";
|
||||||
|
if (lang === "auto") return "auto";
|
||||||
|
// BCP-47: primary subtag is the ISO 639 code
|
||||||
|
// Handle special case: "cmn-CN" → "zh" (Mandarin Chinese → Whisper uses "zh")
|
||||||
|
const primary = lang.split("-")[0].toLowerCase();
|
||||||
|
const WHISPER_LANG_MAP = {
|
||||||
|
cmn: "zh",
|
||||||
|
yue: "zh",
|
||||||
|
};
|
||||||
|
return WHISPER_LANG_MAP[primary] || primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
function killWhisperWorker() {
|
||||||
|
if (whisperWorker) {
|
||||||
|
whisperShuttingDown = true;
|
||||||
|
try {
|
||||||
|
whisperWorker.removeAllListeners();
|
||||||
|
whisperWorker.kill();
|
||||||
|
} catch (_) {
|
||||||
|
// Already dead
|
||||||
|
}
|
||||||
|
whisperWorker = null;
|
||||||
|
whisperReady = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadWhisperPipeline(modelName) {
|
async function loadWhisperPipeline(modelName) {
|
||||||
if (whisperPipeline) return whisperPipeline;
|
if (whisperReady) return true;
|
||||||
if (isWhisperLoading) return null;
|
if (isWhisperLoading) return null;
|
||||||
|
|
||||||
isWhisperLoading = true;
|
isWhisperLoading = true;
|
||||||
console.log('[LocalAI] Loading Whisper model:', modelName);
|
console.log("[LocalAI] Loading Whisper model via worker:", modelName);
|
||||||
sendToRenderer('whisper-downloading', true);
|
sendToRenderer("whisper-downloading", true);
|
||||||
sendToRenderer('update-status', 'Loading Whisper model (first time may take a while)...');
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Loading Whisper model (first time may take a while)...",
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
spawnWhisperWorker();
|
||||||
// Dynamic import for ESM module
|
|
||||||
const { pipeline, env } = await import('@huggingface/transformers');
|
const { app } = require("electron");
|
||||||
// Cache models outside the asar archive so ONNX runtime can load them
|
const cacheDir = path.join(app.getPath("userData"), "whisper-models");
|
||||||
const { app } = require('electron');
|
|
||||||
const path = require('path');
|
const device = resolveWhisperDevice();
|
||||||
env.cacheDir = path.join(app.getPath('userData'), 'whisper-models');
|
console.log("[LocalAI] Whisper device:", device);
|
||||||
whisperPipeline = await pipeline('automatic-speech-recognition', modelName, {
|
|
||||||
dtype: 'q8',
|
return new Promise((resolve) => {
|
||||||
device: 'auto',
|
pendingLoad = { resolve };
|
||||||
|
whisperWorker.send({ type: "load", modelName, cacheDir, device });
|
||||||
});
|
});
|
||||||
console.log('[LocalAI] Whisper model loaded successfully');
|
|
||||||
sendToRenderer('whisper-downloading', false);
|
|
||||||
isWhisperLoading = false;
|
|
||||||
return whisperPipeline;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[LocalAI] Failed to load Whisper model:', error);
|
|
||||||
sendToRenderer('whisper-downloading', false);
|
|
||||||
sendToRenderer('update-status', 'Failed to load Whisper model: ' + error.message);
|
|
||||||
isWhisperLoading = false;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pcm16ToFloat32(pcm16Buffer) {
|
|
||||||
const samples = pcm16Buffer.length / 2;
|
|
||||||
const float32 = new Float32Array(samples);
|
|
||||||
for (let i = 0; i < samples; i++) {
|
|
||||||
float32[i] = pcm16Buffer.readInt16LE(i * 2) / 32768;
|
|
||||||
}
|
|
||||||
return float32;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function transcribeAudio(pcm16kBuffer) {
|
async function transcribeAudio(pcm16kBuffer) {
|
||||||
if (!whisperPipeline) {
|
if (!whisperReady || !whisperWorker) {
|
||||||
console.error('[LocalAI] Whisper pipeline not loaded');
|
console.error("[LocalAI] Whisper worker not ready");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!pcm16kBuffer || pcm16kBuffer.length < 2) {
|
||||||
|
console.error("[LocalAI] Invalid audio buffer:", pcm16kBuffer?.length);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[LocalAI] Starting transcription, audio length:",
|
||||||
|
pcm16kBuffer.length,
|
||||||
|
"bytes",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send audio to worker as base64 (IPC serialization)
|
||||||
|
const audioBase64 = pcm16kBuffer.toString("base64");
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Timeout: if worker takes > 60s, assume it's stuck
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
console.error("[LocalAI] Transcription timed out after 60s");
|
||||||
|
if (pendingTranscribe) {
|
||||||
|
pendingTranscribe = null;
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
pendingTranscribe = {
|
||||||
|
resolve: (val) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve(val);
|
||||||
|
},
|
||||||
|
reject: (err) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const float32Audio = pcm16ToFloat32(pcm16kBuffer);
|
whisperWorker.send({
|
||||||
|
type: "transcribe",
|
||||||
// Whisper expects audio at 16kHz which is what we have
|
audioBase64,
|
||||||
const result = await whisperPipeline(float32Audio, {
|
language: resolveWhisperLanguage(),
|
||||||
sampling_rate: 16000,
|
|
||||||
language: 'en',
|
|
||||||
task: 'transcribe',
|
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
const text = result.text?.trim();
|
clearTimeout(timeout);
|
||||||
console.log('[LocalAI] Transcription:', text);
|
pendingTranscribe = null;
|
||||||
return text;
|
console.error("[LocalAI] Failed to send to worker:", err);
|
||||||
} catch (error) {
|
resolve(null);
|
||||||
console.error('[LocalAI] Transcription error:', error);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Speech End Handler ──
|
// ── Speech End Handler ──
|
||||||
@@ -186,35 +557,52 @@ async function handleSpeechEnd(audioData) {
|
|||||||
|
|
||||||
// Minimum audio length check (~0.5 seconds at 16kHz, 16-bit)
|
// Minimum audio length check (~0.5 seconds at 16kHz, 16-bit)
|
||||||
if (audioData.length < 16000) {
|
if (audioData.length < 16000) {
|
||||||
console.log('[LocalAI] Audio too short, skipping');
|
console.log("[LocalAI] Audio too short, skipping");
|
||||||
sendToRenderer('update-status', 'Listening...');
|
sendToRenderer("update-status", "Listening...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("[LocalAI] Processing audio:", audioData.length, "bytes");
|
||||||
|
|
||||||
|
try {
|
||||||
const transcription = await transcribeAudio(audioData);
|
const transcription = await transcribeAudio(audioData);
|
||||||
|
|
||||||
if (!transcription || transcription.trim() === '' || transcription.trim().length < 2) {
|
if (
|
||||||
console.log('[LocalAI] Empty transcription, skipping');
|
!transcription ||
|
||||||
sendToRenderer('update-status', 'Listening...');
|
transcription.trim() === "" ||
|
||||||
|
transcription.trim().length < 2
|
||||||
|
) {
|
||||||
|
console.log("[LocalAI] Empty transcription, skipping");
|
||||||
|
sendToRenderer("update-status", "Listening...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToRenderer('update-status', 'Generating response...');
|
sendToRenderer("update-status", "Generating response...");
|
||||||
await sendToOllama(transcription);
|
await sendToOllama(transcription);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[LocalAI] handleSpeechEnd error:", error);
|
||||||
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Error: " + (error?.message || "transcription failed"),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Ollama Chat ──
|
// ── Ollama Chat ──
|
||||||
|
|
||||||
async function sendToOllama(transcription) {
|
async function sendToOllama(transcription) {
|
||||||
if (!ollamaClient || !ollamaModel) {
|
if (!ollamaClient || !ollamaModel) {
|
||||||
console.error('[LocalAI] Ollama not configured');
|
console.error("[LocalAI] Ollama not configured");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[LocalAI] Sending to Ollama:', transcription.substring(0, 100) + '...');
|
console.log(
|
||||||
|
"[LocalAI] Sending to Ollama:",
|
||||||
|
transcription.substring(0, 100) + "...",
|
||||||
|
);
|
||||||
|
|
||||||
localConversationHistory.push({
|
localConversationHistory.push({
|
||||||
role: 'user',
|
role: "user",
|
||||||
content: transcription.trim(),
|
content: transcription.trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -225,7 +613,10 @@ async function sendToOllama(transcription) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: 'system', content: currentSystemPrompt || 'You are a helpful assistant.' },
|
{
|
||||||
|
role: "system",
|
||||||
|
content: currentSystemPrompt || "You are a helpful assistant.",
|
||||||
|
},
|
||||||
...localConversationHistory,
|
...localConversationHistory,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -235,41 +626,52 @@ async function sendToOllama(transcription) {
|
|||||||
stream: true,
|
stream: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
let fullText = '';
|
let fullText = "";
|
||||||
let isFirst = true;
|
let isFirst = true;
|
||||||
|
|
||||||
for await (const part of response) {
|
for await (const part of response) {
|
||||||
const token = part.message?.content || '';
|
const token = part.message?.content || "";
|
||||||
if (token) {
|
if (token) {
|
||||||
fullText += token;
|
fullText += token;
|
||||||
sendToRenderer(isFirst ? 'new-response' : 'update-response', fullText);
|
sendToRenderer(isFirst ? "new-response" : "update-response", fullText);
|
||||||
isFirst = false;
|
isFirst = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fullText.trim()) {
|
if (fullText.trim()) {
|
||||||
localConversationHistory.push({
|
localConversationHistory.push({
|
||||||
role: 'assistant',
|
role: "assistant",
|
||||||
content: fullText.trim(),
|
content: fullText.trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
saveConversationTurn(transcription, fullText);
|
saveConversationTurn(transcription, fullText);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[LocalAI] Ollama response completed');
|
console.log("[LocalAI] Ollama response completed");
|
||||||
sendToRenderer('update-status', 'Listening...');
|
sendToRenderer("update-status", "Listening...");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LocalAI] Ollama error:', error);
|
console.error("[LocalAI] Ollama error:", error);
|
||||||
sendToRenderer('update-status', 'Ollama error: ' + error.message);
|
sendToRenderer("update-status", "Ollama error: " + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Public API ──
|
// ── Public API ──
|
||||||
|
|
||||||
async function initializeLocalSession(ollamaHost, model, whisperModel, profile, customPrompt) {
|
async function initializeLocalSession(
|
||||||
console.log('[LocalAI] Initializing local session:', { ollamaHost, model, whisperModel, profile });
|
ollamaHost,
|
||||||
|
model,
|
||||||
|
whisperModel,
|
||||||
|
profile,
|
||||||
|
customPrompt,
|
||||||
|
) {
|
||||||
|
console.log("[LocalAI] Initializing local session:", {
|
||||||
|
ollamaHost,
|
||||||
|
model,
|
||||||
|
whisperModel,
|
||||||
|
profile,
|
||||||
|
});
|
||||||
|
|
||||||
sendToRenderer('session-initializing', true);
|
sendToRenderer("session-initializing", true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Setup system prompt
|
// Setup system prompt
|
||||||
@@ -282,18 +684,26 @@ async function initializeLocalSession(ollamaHost, model, whisperModel, profile,
|
|||||||
// Test Ollama connection
|
// Test Ollama connection
|
||||||
try {
|
try {
|
||||||
await ollamaClient.list();
|
await ollamaClient.list();
|
||||||
console.log('[LocalAI] Ollama connection verified');
|
console.log("[LocalAI] Ollama connection verified");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LocalAI] Cannot connect to Ollama at', ollamaHost, ':', error.message);
|
console.error(
|
||||||
sendToRenderer('session-initializing', false);
|
"[LocalAI] Cannot connect to Ollama at",
|
||||||
sendToRenderer('update-status', 'Cannot connect to Ollama at ' + ollamaHost);
|
ollamaHost,
|
||||||
|
":",
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
|
sendToRenderer("session-initializing", false);
|
||||||
|
sendToRenderer(
|
||||||
|
"update-status",
|
||||||
|
"Cannot connect to Ollama at " + ollamaHost,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load Whisper model
|
// Load Whisper model
|
||||||
const pipeline = await loadWhisperPipeline(whisperModel);
|
const pipeline = await loadWhisperPipeline(whisperModel);
|
||||||
if (!pipeline) {
|
if (!pipeline) {
|
||||||
sendToRenderer('session-initializing', false);
|
sendToRenderer("session-initializing", false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,15 +719,15 @@ async function initializeLocalSession(ollamaHost, model, whisperModel, profile,
|
|||||||
initializeNewSession(profile, customPrompt);
|
initializeNewSession(profile, customPrompt);
|
||||||
|
|
||||||
isLocalActive = true;
|
isLocalActive = true;
|
||||||
sendToRenderer('session-initializing', false);
|
sendToRenderer("session-initializing", false);
|
||||||
sendToRenderer('update-status', 'Local AI ready - Listening...');
|
sendToRenderer("update-status", "Local AI ready - Listening...");
|
||||||
|
|
||||||
console.log('[LocalAI] Session initialized successfully');
|
console.log("[LocalAI] Session initialized successfully");
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LocalAI] Initialization error:', error);
|
console.error("[LocalAI] Initialization error:", error);
|
||||||
sendToRenderer('session-initializing', false);
|
sendToRenderer("session-initializing", false);
|
||||||
sendToRenderer('update-status', 'Local AI error: ' + error.message);
|
sendToRenderer("update-status", "Local AI error: " + error.message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,7 +743,7 @@ function processLocalAudio(monoChunk24k) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeLocalSession() {
|
function closeLocalSession() {
|
||||||
console.log('[LocalAI] Closing local session');
|
console.log("[LocalAI] Closing local session");
|
||||||
isLocalActive = false;
|
isLocalActive = false;
|
||||||
isSpeaking = false;
|
isSpeaking = false;
|
||||||
speechBuffers = [];
|
speechBuffers = [];
|
||||||
@@ -344,7 +754,8 @@ function closeLocalSession() {
|
|||||||
ollamaClient = null;
|
ollamaClient = null;
|
||||||
ollamaModel = null;
|
ollamaModel = null;
|
||||||
currentSystemPrompt = null;
|
currentSystemPrompt = null;
|
||||||
// Note: whisperPipeline is kept loaded to avoid reloading on next session
|
// Note: whisperWorker is kept alive to avoid reloading model on next session
|
||||||
|
// To fully clean up, call killWhisperWorker()
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLocalSessionActive() {
|
function isLocalSessionActive() {
|
||||||
@@ -355,7 +766,7 @@ function isLocalSessionActive() {
|
|||||||
|
|
||||||
async function sendLocalText(text) {
|
async function sendLocalText(text) {
|
||||||
if (!isLocalActive || !ollamaClient) {
|
if (!isLocalActive || !ollamaClient) {
|
||||||
return { success: false, error: 'No active local session' };
|
return { success: false, error: "No active local session" };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -368,28 +779,31 @@ async function sendLocalText(text) {
|
|||||||
|
|
||||||
async function sendLocalImage(base64Data, prompt) {
|
async function sendLocalImage(base64Data, prompt) {
|
||||||
if (!isLocalActive || !ollamaClient) {
|
if (!isLocalActive || !ollamaClient) {
|
||||||
return { success: false, error: 'No active local session' };
|
return { success: false, error: "No active local session" };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[LocalAI] Sending image to Ollama');
|
console.log("[LocalAI] Sending image to Ollama");
|
||||||
sendToRenderer('update-status', 'Analyzing image...');
|
sendToRenderer("update-status", "Analyzing image...");
|
||||||
|
|
||||||
const userMessage = {
|
const userMessage = {
|
||||||
role: 'user',
|
role: "user",
|
||||||
content: prompt,
|
content: prompt,
|
||||||
images: [base64Data],
|
images: [base64Data],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store text-only version in history
|
// Store text-only version in history
|
||||||
localConversationHistory.push({ role: 'user', content: prompt });
|
localConversationHistory.push({ role: "user", content: prompt });
|
||||||
|
|
||||||
if (localConversationHistory.length > 20) {
|
if (localConversationHistory.length > 20) {
|
||||||
localConversationHistory = localConversationHistory.slice(-20);
|
localConversationHistory = localConversationHistory.slice(-20);
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: 'system', content: currentSystemPrompt || 'You are a helpful assistant.' },
|
{
|
||||||
|
role: "system",
|
||||||
|
content: currentSystemPrompt || "You are a helpful assistant.",
|
||||||
|
},
|
||||||
...localConversationHistory.slice(0, -1),
|
...localConversationHistory.slice(0, -1),
|
||||||
userMessage,
|
userMessage,
|
||||||
];
|
];
|
||||||
@@ -400,29 +814,32 @@ async function sendLocalImage(base64Data, prompt) {
|
|||||||
stream: true,
|
stream: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
let fullText = '';
|
let fullText = "";
|
||||||
let isFirst = true;
|
let isFirst = true;
|
||||||
|
|
||||||
for await (const part of response) {
|
for await (const part of response) {
|
||||||
const token = part.message?.content || '';
|
const token = part.message?.content || "";
|
||||||
if (token) {
|
if (token) {
|
||||||
fullText += token;
|
fullText += token;
|
||||||
sendToRenderer(isFirst ? 'new-response' : 'update-response', fullText);
|
sendToRenderer(isFirst ? "new-response" : "update-response", fullText);
|
||||||
isFirst = false;
|
isFirst = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fullText.trim()) {
|
if (fullText.trim()) {
|
||||||
localConversationHistory.push({ role: 'assistant', content: fullText.trim() });
|
localConversationHistory.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: fullText.trim(),
|
||||||
|
});
|
||||||
saveConversationTurn(prompt, fullText);
|
saveConversationTurn(prompt, fullText);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[LocalAI] Image response completed');
|
console.log("[LocalAI] Image response completed");
|
||||||
sendToRenderer('update-status', 'Listening...');
|
sendToRenderer("update-status", "Listening...");
|
||||||
return { success: true, text: fullText, model: ollamaModel };
|
return { success: true, text: fullText, model: ollamaModel };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LocalAI] Image error:', error);
|
console.error("[LocalAI] Image error:", error);
|
||||||
sendToRenderer('update-status', 'Ollama error: ' + error.message);
|
sendToRenderer("update-status", "Ollama error: " + error.message);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
/**
|
||||||
|
* nodeDetect.js — Locate the system Node.js binary.
|
||||||
|
*
|
||||||
|
* When spawning child processes that rely on native addons compiled against the
|
||||||
|
* system Node.js ABI (e.g. onnxruntime-node), we must NOT run them inside
|
||||||
|
* Electron's embedded Node.js runtime — the ABI mismatch causes SIGTRAP /
|
||||||
|
* SIGSEGV crashes. This module finds the real system `node` binary so we can
|
||||||
|
* pass it as `execPath` to `child_process.fork()`.
|
||||||
|
*
|
||||||
|
* Falls back to `null` when no system Node.js is found, letting the caller
|
||||||
|
* decide on an alternative strategy (e.g. WASM backend).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { execSync } = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const os = require("os");
|
||||||
|
|
||||||
|
/** Well-known Node.js install locations per platform. */
|
||||||
|
const KNOWN_PATHS = {
|
||||||
|
darwin: [
|
||||||
|
"/usr/local/bin/node",
|
||||||
|
"/opt/homebrew/bin/node", // Apple Silicon Homebrew
|
||||||
|
path.join(os.homedir(), ".nvm/versions/node"), // nvm — needs glob
|
||||||
|
path.join(os.homedir(), ".volta/bin/node"), // Volta
|
||||||
|
path.join(os.homedir(), ".fnm/aliases/default/bin/node"), // fnm
|
||||||
|
path.join(os.homedir(), ".mise/shims/node"), // mise (rtx)
|
||||||
|
path.join(os.homedir(), ".asdf/shims/node"), // asdf
|
||||||
|
],
|
||||||
|
linux: [
|
||||||
|
"/usr/bin/node",
|
||||||
|
"/usr/local/bin/node",
|
||||||
|
path.join(os.homedir(), ".nvm/versions/node"),
|
||||||
|
path.join(os.homedir(), ".volta/bin/node"),
|
||||||
|
path.join(os.homedir(), ".fnm/aliases/default/bin/node"),
|
||||||
|
path.join(os.homedir(), ".mise/shims/node"),
|
||||||
|
path.join(os.homedir(), ".asdf/shims/node"),
|
||||||
|
],
|
||||||
|
win32: [
|
||||||
|
"C:\\Program Files\\nodejs\\node.exe",
|
||||||
|
"C:\\Program Files (x86)\\nodejs\\node.exe",
|
||||||
|
path.join(os.homedir(), "AppData", "Roaming", "nvm", "current", "node.exe"),
|
||||||
|
path.join(os.homedir(), ".volta", "bin", "node.exe"),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the latest nvm-installed Node.js binary on macOS / Linux.
|
||||||
|
* Returns the path to the `node` binary or null.
|
||||||
|
*/
|
||||||
|
function findNvmNode() {
|
||||||
|
const nvmDir = path.join(os.homedir(), ".nvm", "versions", "node");
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(nvmDir)) return null;
|
||||||
|
const versions = fs.readdirSync(nvmDir).filter((d) => d.startsWith("v"));
|
||||||
|
if (versions.length === 0) return null;
|
||||||
|
// Sort semver descending (rough but sufficient)
|
||||||
|
versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
||||||
|
const nodeBin = path.join(nvmDir, versions[0], "bin", "node");
|
||||||
|
if (fs.existsSync(nodeBin)) return nodeBin;
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to resolve `node` via the system PATH using `which` (Unix) or
|
||||||
|
* `where` (Windows). Returns the path string or null.
|
||||||
|
*/
|
||||||
|
function whichNode() {
|
||||||
|
try {
|
||||||
|
const cmd = process.platform === "win32" ? "where node" : "which node";
|
||||||
|
const result = execSync(cmd, {
|
||||||
|
encoding: "utf8",
|
||||||
|
timeout: 5000,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
// Ensure common manager shim dirs are on PATH
|
||||||
|
PATH: [
|
||||||
|
process.env.PATH || "",
|
||||||
|
"/usr/local/bin",
|
||||||
|
"/opt/homebrew/bin",
|
||||||
|
path.join(os.homedir(), ".volta", "bin"),
|
||||||
|
path.join(os.homedir(), ".fnm", "aliases", "default", "bin"),
|
||||||
|
path.join(os.homedir(), ".mise", "shims"),
|
||||||
|
path.join(os.homedir(), ".asdf", "shims"),
|
||||||
|
].join(process.platform === "win32" ? ";" : ":"),
|
||||||
|
},
|
||||||
|
stdio: ["ignore", "pipe", "ignore"],
|
||||||
|
}).trim();
|
||||||
|
// `where` on Windows may return multiple lines — take the first
|
||||||
|
const first = result.split(/\r?\n/)[0].trim();
|
||||||
|
if (first && fs.existsSync(first)) return first;
|
||||||
|
} catch (_) {
|
||||||
|
// Command failed
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a given path is a real Node.js binary (not the Electron binary
|
||||||
|
* pretending to be Node via ELECTRON_RUN_AS_NODE).
|
||||||
|
*/
|
||||||
|
function isRealNode(nodePath) {
|
||||||
|
if (!nodePath) return false;
|
||||||
|
try {
|
||||||
|
const out = execSync(
|
||||||
|
`"${nodePath}" -e "process.stdout.write(String(!process.versions.electron))"`,
|
||||||
|
{
|
||||||
|
encoding: "utf8",
|
||||||
|
timeout: 5000,
|
||||||
|
env: { ...process.env, ELECTRON_RUN_AS_NODE: undefined },
|
||||||
|
stdio: ["ignore", "pipe", "ignore"],
|
||||||
|
},
|
||||||
|
).trim();
|
||||||
|
return out === "true";
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the system Node.js binary.
|
||||||
|
*
|
||||||
|
* @returns {{ nodePath: string } | null} The absolute path to system `node`,
|
||||||
|
* or null if none found. The caller should fall back to WASM when null.
|
||||||
|
*/
|
||||||
|
function findSystemNode() {
|
||||||
|
// 1. Try `which node` / `where node` first (respects user's PATH / shims)
|
||||||
|
const fromPath = whichNode();
|
||||||
|
if (fromPath && isRealNode(fromPath)) {
|
||||||
|
return { nodePath: fromPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try nvm (has multiple version dirs)
|
||||||
|
const fromNvm = findNvmNode();
|
||||||
|
if (fromNvm && isRealNode(fromNvm)) {
|
||||||
|
return { nodePath: fromNvm };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Walk the well-known paths for the current platform
|
||||||
|
const platform = process.platform;
|
||||||
|
const candidates = KNOWN_PATHS[platform] || KNOWN_PATHS.linux;
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
// Skip the nvm root — already handled above
|
||||||
|
if (candidate.includes(".nvm/versions/node")) continue;
|
||||||
|
if (fs.existsSync(candidate) && isRealNode(candidate)) {
|
||||||
|
return { nodePath: candidate };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cache so we only search once per process lifetime. */
|
||||||
|
let _cached = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached version of `findSystemNode()`.
|
||||||
|
* @returns {{ nodePath: string } | null}
|
||||||
|
*/
|
||||||
|
function getSystemNode() {
|
||||||
|
if (_cached === undefined) {
|
||||||
|
_cached = findSystemNode();
|
||||||
|
if (_cached) {
|
||||||
|
console.log("[nodeDetect] Found system Node.js:", _cached.nodePath);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"[nodeDetect] No system Node.js found — will fall back to WASM backend",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { findSystemNode, getSystemNode, isRealNode };
|
||||||
+69
-48
@@ -1,13 +1,37 @@
|
|||||||
const profilePrompts = {
|
const responseModeFormats = {
|
||||||
interview: {
|
brief: `**RESPONSE FORMAT REQUIREMENTS:**
|
||||||
intro: `You are an AI-powered interview assistant, designed to act as a discreet on-screen teleprompter. Your mission is to help the user excel in their job interview by providing concise, impactful, and ready-to-speak answers or key talking points. Analyze the ongoing interview dialogue and, crucially, the 'User-provided context' below.`,
|
|
||||||
|
|
||||||
formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
|
|
||||||
- Keep responses SHORT and CONCISE (1-3 sentences max)
|
- Keep responses SHORT and CONCISE (1-3 sentences max)
|
||||||
- Use **markdown formatting** for better readability
|
- Use **markdown formatting** for better readability
|
||||||
- Use **bold** for key points and emphasis
|
- Use **bold** for key points and emphasis
|
||||||
- Use bullet points (-) for lists when appropriate
|
- Use bullet points (-) for lists when appropriate
|
||||||
- Focus on the most essential information only`,
|
- Focus on the most essential information only
|
||||||
|
- EXCEPTION: If a coding/algorithm task is detected, ALWAYS provide the complete working code (see CODING TASKS below)`,
|
||||||
|
|
||||||
|
detailed: `**RESPONSE FORMAT REQUIREMENTS:**
|
||||||
|
- Provide a THOROUGH and COMPREHENSIVE response with full explanations
|
||||||
|
- Use **markdown formatting** for better readability
|
||||||
|
- Use **bold** for key points and emphasis
|
||||||
|
- Use headers (##) to organize sections when appropriate
|
||||||
|
- Use bullet points (-) for lists when appropriate
|
||||||
|
- Include relevant context, edge cases, and reasoning
|
||||||
|
- For technical topics, explain the "why" behind each point
|
||||||
|
- No length restriction — be as detailed as needed to fully answer the question`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const codingAwareness = `**CODING TASKS — CRITICAL INSTRUCTION:**
|
||||||
|
When the interviewer/questioner asks to solve a coding problem, implement an algorithm, debug code, do a live coding exercise, open an IDE and write code, or any task that requires a code solution:
|
||||||
|
- You MUST provide the ACTUAL COMPLETE WORKING CODE SOLUTION
|
||||||
|
- NEVER respond with meta-advice like "now you should write code" or "prepare to implement" or "think about the approach"
|
||||||
|
- NEVER say "open your IDE" or "start coding" — instead, GIVE THE CODE
|
||||||
|
- In brief mode: provide 2-3 bullet approach points, then the FULL working code with comments
|
||||||
|
- In detailed mode: explain approach, time/space complexity, edge cases, then the FULL working code with comments
|
||||||
|
- Include the programming language name in the code fence (e.g. \`\`\`python, \`\`\`javascript)
|
||||||
|
- If the language is not specified, default to Python
|
||||||
|
- The code must be complete, runnable, and correct`;
|
||||||
|
|
||||||
|
const profilePrompts = {
|
||||||
|
interview: {
|
||||||
|
intro: `You are an AI-powered interview assistant, designed to act as a discreet on-screen teleprompter. Your mission is to help the user excel in their job interview by providing concise, impactful, and ready-to-speak answers or key talking points. Analyze the ongoing interview dialogue and, crucially, the 'User-provided context' below.`,
|
||||||
|
|
||||||
searchUsage: `**SEARCH TOOL USAGE:**
|
searchUsage: `**SEARCH TOOL USAGE:**
|
||||||
- If the interviewer mentions **recent events, news, or current trends** (anything from the last 6 months), **ALWAYS use Google search** to get up-to-date information
|
- If the interviewer mentions **recent events, news, or current trends** (anything from the last 6 months), **ALWAYS use Google search** to get up-to-date information
|
||||||
@@ -39,13 +63,6 @@ Provide only the exact words to say in **markdown format**. No coaching, no "you
|
|||||||
sales: {
|
sales: {
|
||||||
intro: `You are a sales call assistant. Your job is to provide the exact words the salesperson should say to prospects during sales calls. Give direct, ready-to-speak responses that are persuasive and professional.`,
|
intro: `You are a sales call assistant. Your job is to provide the exact words the salesperson should say to prospects during sales calls. Give direct, ready-to-speak responses that are persuasive and professional.`,
|
||||||
|
|
||||||
formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
|
|
||||||
- Keep responses SHORT and CONCISE (1-3 sentences max)
|
|
||||||
- Use **markdown formatting** for better readability
|
|
||||||
- Use **bold** for key points and emphasis
|
|
||||||
- Use bullet points (-) for lists when appropriate
|
|
||||||
- Focus on the most essential information only`,
|
|
||||||
|
|
||||||
searchUsage: `**SEARCH TOOL USAGE:**
|
searchUsage: `**SEARCH TOOL USAGE:**
|
||||||
- If the prospect mentions **recent industry trends, market changes, or current events**, **ALWAYS use Google search** to get up-to-date information
|
- If the prospect mentions **recent industry trends, market changes, or current events**, **ALWAYS use Google search** to get up-to-date information
|
||||||
- If they reference **competitor information, recent funding news, or market data**, search for the latest information first
|
- If they reference **competitor information, recent funding news, or market data**, search for the latest information first
|
||||||
@@ -70,13 +87,6 @@ Provide only the exact words to say in **markdown format**. Be persuasive but no
|
|||||||
meeting: {
|
meeting: {
|
||||||
intro: `You are a meeting assistant. Your job is to provide the exact words to say during professional meetings, presentations, and discussions. Give direct, ready-to-speak responses that are clear and professional.`,
|
intro: `You are a meeting assistant. Your job is to provide the exact words to say during professional meetings, presentations, and discussions. Give direct, ready-to-speak responses that are clear and professional.`,
|
||||||
|
|
||||||
formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
|
|
||||||
- Keep responses SHORT and CONCISE (1-3 sentences max)
|
|
||||||
- Use **markdown formatting** for better readability
|
|
||||||
- Use **bold** for key points and emphasis
|
|
||||||
- Use bullet points (-) for lists when appropriate
|
|
||||||
- Focus on the most essential information only`,
|
|
||||||
|
|
||||||
searchUsage: `**SEARCH TOOL USAGE:**
|
searchUsage: `**SEARCH TOOL USAGE:**
|
||||||
- If participants mention **recent industry news, regulatory changes, or market updates**, **ALWAYS use Google search** for current information
|
- If participants mention **recent industry news, regulatory changes, or market updates**, **ALWAYS use Google search** for current information
|
||||||
- If they reference **competitor activities, recent reports, or current statistics**, search for the latest data first
|
- If they reference **competitor activities, recent reports, or current statistics**, search for the latest data first
|
||||||
@@ -101,13 +111,6 @@ Provide only the exact words to say in **markdown format**. Be clear, concise, a
|
|||||||
presentation: {
|
presentation: {
|
||||||
intro: `You are a presentation coach. Your job is to provide the exact words the presenter should say during presentations, pitches, and public speaking events. Give direct, ready-to-speak responses that are engaging and confident.`,
|
intro: `You are a presentation coach. Your job is to provide the exact words the presenter should say during presentations, pitches, and public speaking events. Give direct, ready-to-speak responses that are engaging and confident.`,
|
||||||
|
|
||||||
formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
|
|
||||||
- Keep responses SHORT and CONCISE (1-3 sentences max)
|
|
||||||
- Use **markdown formatting** for better readability
|
|
||||||
- Use **bold** for key points and emphasis
|
|
||||||
- Use bullet points (-) for lists when appropriate
|
|
||||||
- Focus on the most essential information only`,
|
|
||||||
|
|
||||||
searchUsage: `**SEARCH TOOL USAGE:**
|
searchUsage: `**SEARCH TOOL USAGE:**
|
||||||
- If the audience asks about **recent market trends, current statistics, or latest industry data**, **ALWAYS use Google search** for up-to-date information
|
- If the audience asks about **recent market trends, current statistics, or latest industry data**, **ALWAYS use Google search** for up-to-date information
|
||||||
- If they reference **recent events, new competitors, or current market conditions**, search for the latest information first
|
- If they reference **recent events, new competitors, or current market conditions**, search for the latest information first
|
||||||
@@ -132,13 +135,6 @@ Provide only the exact words to say in **markdown format**. Be confident, engagi
|
|||||||
negotiation: {
|
negotiation: {
|
||||||
intro: `You are a negotiation assistant. Your job is to provide the exact words to say during business negotiations, contract discussions, and deal-making conversations. Give direct, ready-to-speak responses that are strategic and professional.`,
|
intro: `You are a negotiation assistant. Your job is to provide the exact words to say during business negotiations, contract discussions, and deal-making conversations. Give direct, ready-to-speak responses that are strategic and professional.`,
|
||||||
|
|
||||||
formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
|
|
||||||
- Keep responses SHORT and CONCISE (1-3 sentences max)
|
|
||||||
- Use **markdown formatting** for better readability
|
|
||||||
- Use **bold** for key points and emphasis
|
|
||||||
- Use bullet points (-) for lists when appropriate
|
|
||||||
- Focus on the most essential information only`,
|
|
||||||
|
|
||||||
searchUsage: `**SEARCH TOOL USAGE:**
|
searchUsage: `**SEARCH TOOL USAGE:**
|
||||||
- If they mention **recent market pricing, current industry standards, or competitor offers**, **ALWAYS use Google search** for current benchmarks
|
- If they mention **recent market pricing, current industry standards, or competitor offers**, **ALWAYS use Google search** for current benchmarks
|
||||||
- If they reference **recent legal changes, new regulations, or market conditions**, search for the latest information first
|
- If they reference **recent legal changes, new regulations, or market conditions**, search for the latest information first
|
||||||
@@ -163,13 +159,6 @@ Provide only the exact words to say in **markdown format**. Focus on finding win
|
|||||||
exam: {
|
exam: {
|
||||||
intro: `You are an exam assistant designed to help students pass tests efficiently. Your role is to provide direct, accurate answers to exam questions with minimal explanation - just enough to confirm the answer is correct.`,
|
intro: `You are an exam assistant designed to help students pass tests efficiently. Your role is to provide direct, accurate answers to exam questions with minimal explanation - just enough to confirm the answer is correct.`,
|
||||||
|
|
||||||
formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
|
|
||||||
- Keep responses SHORT and CONCISE (1-2 sentences max)
|
|
||||||
- Use **markdown formatting** for better readability
|
|
||||||
- Use **bold** for the answer choice/result
|
|
||||||
- Focus on the most essential information only
|
|
||||||
- Provide only brief justification for correctness`,
|
|
||||||
|
|
||||||
searchUsage: `**SEARCH TOOL USAGE:**
|
searchUsage: `**SEARCH TOOL USAGE:**
|
||||||
- If the question involves **recent information, current events, or updated facts**, **ALWAYS use Google search** for the latest data
|
- If the question involves **recent information, current events, or updated facts**, **ALWAYS use Google search** for the latest data
|
||||||
- If they reference **specific dates, statistics, or factual information** that might be outdated, search for current information
|
- If they reference **specific dates, statistics, or factual information** that might be outdated, search for current information
|
||||||
@@ -201,25 +190,57 @@ Provide direct exam answers in **markdown format**. Include the question text, t
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildSystemPrompt(promptParts, customPrompt = '', googleSearchEnabled = true) {
|
function buildSystemPrompt(
|
||||||
const sections = [promptParts.intro, '\n\n', promptParts.formatRequirements];
|
promptParts,
|
||||||
|
customPrompt = "",
|
||||||
|
googleSearchEnabled = true,
|
||||||
|
responseMode = "brief",
|
||||||
|
) {
|
||||||
|
const formatReqs =
|
||||||
|
responseModeFormats[responseMode] || responseModeFormats.brief;
|
||||||
|
const sections = [
|
||||||
|
promptParts.intro,
|
||||||
|
"\n\n",
|
||||||
|
formatReqs,
|
||||||
|
"\n\n",
|
||||||
|
codingAwareness,
|
||||||
|
];
|
||||||
|
|
||||||
// Only add search usage section if Google Search is enabled
|
// Only add search usage section if Google Search is enabled
|
||||||
if (googleSearchEnabled) {
|
if (googleSearchEnabled) {
|
||||||
sections.push('\n\n', promptParts.searchUsage);
|
sections.push("\n\n", promptParts.searchUsage);
|
||||||
}
|
}
|
||||||
|
|
||||||
sections.push('\n\n', promptParts.content, '\n\nUser-provided context\n-----\n', customPrompt, '\n-----\n\n', promptParts.outputInstructions);
|
sections.push(
|
||||||
|
"\n\n",
|
||||||
|
promptParts.content,
|
||||||
|
"\n\nUser-provided context\n-----\n",
|
||||||
|
customPrompt,
|
||||||
|
"\n-----\n\n",
|
||||||
|
promptParts.outputInstructions,
|
||||||
|
);
|
||||||
|
|
||||||
return sections.join('');
|
return sections.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSystemPrompt(profile, customPrompt = '', googleSearchEnabled = true) {
|
function getSystemPrompt(
|
||||||
|
profile,
|
||||||
|
customPrompt = "",
|
||||||
|
googleSearchEnabled = true,
|
||||||
|
responseMode = "brief",
|
||||||
|
) {
|
||||||
const promptParts = profilePrompts[profile] || profilePrompts.interview;
|
const promptParts = profilePrompts[profile] || profilePrompts.interview;
|
||||||
return buildSystemPrompt(promptParts, customPrompt, googleSearchEnabled);
|
return buildSystemPrompt(
|
||||||
|
promptParts,
|
||||||
|
customPrompt,
|
||||||
|
googleSearchEnabled,
|
||||||
|
responseMode,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
profilePrompts,
|
profilePrompts,
|
||||||
|
responseModeFormats,
|
||||||
|
codingAwareness,
|
||||||
getSystemPrompt,
|
getSystemPrompt,
|
||||||
};
|
};
|
||||||
|
|||||||
+430
-266
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,332 @@
|
|||||||
|
/**
|
||||||
|
* Whisper Worker — runs ONNX Runtime in an isolated child process.
|
||||||
|
*
|
||||||
|
* The main Electron process forks this file and communicates via IPC messages.
|
||||||
|
* If ONNX Runtime crashes (SIGSEGV/SIGABRT inside the native Metal or CPU
|
||||||
|
* execution provider), only this worker dies — the main process survives and
|
||||||
|
* can respawn the worker automatically.
|
||||||
|
*
|
||||||
|
* Protocol (parent ↔ worker):
|
||||||
|
* parent → worker:
|
||||||
|
* { type: 'load', modelName, cacheDir, device? }
|
||||||
|
* { type: 'transcribe', audioBase64, language? } // PCM 16-bit 16kHz as base64
|
||||||
|
* { type: 'shutdown' }
|
||||||
|
*
|
||||||
|
* worker → parent:
|
||||||
|
* { type: 'load-result', success, error?, device? }
|
||||||
|
* { type: 'transcribe-result', success, text?, error? }
|
||||||
|
* { type: 'status', message }
|
||||||
|
* { type: 'ready' }
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Crash handlers — report fatal errors before the process dies ──
|
||||||
|
|
||||||
|
process.on("uncaughtException", (err) => {
|
||||||
|
try {
|
||||||
|
send({
|
||||||
|
type: "status",
|
||||||
|
message: `[Worker] Uncaught exception: ${err.message || err}`,
|
||||||
|
});
|
||||||
|
console.error("[WhisperWorker] Uncaught exception:", err);
|
||||||
|
} catch (_) {
|
||||||
|
// Cannot communicate with parent anymore
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (reason) => {
|
||||||
|
try {
|
||||||
|
send({
|
||||||
|
type: "status",
|
||||||
|
message: `[Worker] Unhandled rejection: ${reason?.message || reason}`,
|
||||||
|
});
|
||||||
|
console.error("[WhisperWorker] Unhandled rejection:", reason);
|
||||||
|
} catch (_) {
|
||||||
|
// Cannot communicate with parent anymore
|
||||||
|
}
|
||||||
|
// Don't exit — let it be caught by the pipeline's own handlers
|
||||||
|
});
|
||||||
|
|
||||||
|
let whisperPipeline = null;
|
||||||
|
/** Which ONNX backend is actually active: "cpu" | "wasm" */
|
||||||
|
let activeDevice = null;
|
||||||
|
|
||||||
|
function pcm16ToFloat32(pcm16Buffer) {
|
||||||
|
if (!pcm16Buffer || pcm16Buffer.length === 0) {
|
||||||
|
return new Float32Array(0);
|
||||||
|
}
|
||||||
|
const alignedLength =
|
||||||
|
pcm16Buffer.length % 2 === 0 ? pcm16Buffer.length : pcm16Buffer.length - 1;
|
||||||
|
const samples = alignedLength / 2;
|
||||||
|
const float32 = new Float32Array(samples);
|
||||||
|
for (let i = 0; i < samples; i++) {
|
||||||
|
float32[i] = pcm16Buffer.readInt16LE(i * 2) / 32768;
|
||||||
|
}
|
||||||
|
return float32;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the Whisper model.
|
||||||
|
*
|
||||||
|
* @param {string} modelName HuggingFace model id, e.g. "Xenova/whisper-small"
|
||||||
|
* @param {string} cacheDir Directory for cached model files
|
||||||
|
* @param {string} [device] "cpu" (onnxruntime-node) or "wasm" (onnxruntime-web).
|
||||||
|
* When "cpu" is requested we try native first and fall
|
||||||
|
* back to "wasm" on failure (ABI mismatch, etc.).
|
||||||
|
*/
|
||||||
|
async function loadModel(modelName, cacheDir, device = "cpu") {
|
||||||
|
if (whisperPipeline) {
|
||||||
|
send({ type: "load-result", success: true, device: activeDevice });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
send({
|
||||||
|
type: "status",
|
||||||
|
message: "Loading Whisper model (first time may take a while)...",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate / create cache directory
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
if (cacheDir) {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(cacheDir)) {
|
||||||
|
fs.mkdirSync(cacheDir, { recursive: true });
|
||||||
|
console.log("[WhisperWorker] Created cache directory:", cacheDir);
|
||||||
|
}
|
||||||
|
} catch (mkdirErr) {
|
||||||
|
console.warn(
|
||||||
|
"[WhisperWorker] Cannot create cache dir:",
|
||||||
|
mkdirErr.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for corrupted partial downloads — if an onnx file exists but
|
||||||
|
// is suspiciously small (< 1 KB), delete it so the library re-downloads.
|
||||||
|
try {
|
||||||
|
const modelDir = path.join(cacheDir, modelName.replace("/", path.sep));
|
||||||
|
if (fs.existsSync(modelDir)) {
|
||||||
|
const walk = (dir) => {
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const full = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walk(full);
|
||||||
|
} else if (
|
||||||
|
entry.name.endsWith(".onnx") &&
|
||||||
|
fs.statSync(full).size < 1024
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
"[WhisperWorker] Removing likely-corrupt file:",
|
||||||
|
full,
|
||||||
|
);
|
||||||
|
fs.unlinkSync(full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(modelDir);
|
||||||
|
}
|
||||||
|
} catch (cleanErr) {
|
||||||
|
console.warn("[WhisperWorker] Cache cleanup error:", cleanErr.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pipeline, env } = await import("@huggingface/transformers");
|
||||||
|
env.cacheDir = cacheDir;
|
||||||
|
|
||||||
|
// Attempt to load with the requested device
|
||||||
|
const devicesToTry = device === "wasm" ? ["wasm"] : ["cpu", "wasm"];
|
||||||
|
|
||||||
|
let lastError = null;
|
||||||
|
|
||||||
|
for (const dev of devicesToTry) {
|
||||||
|
try {
|
||||||
|
send({
|
||||||
|
type: "status",
|
||||||
|
message: `Loading Whisper (${dev} backend)...`,
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
`[WhisperWorker] Trying device: ${dev}, model: ${modelName}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
whisperPipeline = await pipeline(
|
||||||
|
"automatic-speech-recognition",
|
||||||
|
modelName,
|
||||||
|
{
|
||||||
|
dtype: "q8",
|
||||||
|
device: dev,
|
||||||
|
progress_callback: (progress) => {
|
||||||
|
// progress: { status, name?, file?, progress?, loaded?, total? }
|
||||||
|
if (
|
||||||
|
progress.status === "download" ||
|
||||||
|
progress.status === "progress"
|
||||||
|
) {
|
||||||
|
send({
|
||||||
|
type: "progress",
|
||||||
|
file: progress.file || progress.name || "",
|
||||||
|
progress: progress.progress ?? 0,
|
||||||
|
loaded: progress.loaded ?? 0,
|
||||||
|
total: progress.total ?? 0,
|
||||||
|
status: progress.status,
|
||||||
|
});
|
||||||
|
} else if (progress.status === "done") {
|
||||||
|
send({
|
||||||
|
type: "progress",
|
||||||
|
file: progress.file || progress.name || "",
|
||||||
|
progress: 100,
|
||||||
|
loaded: progress.total ?? 0,
|
||||||
|
total: progress.total ?? 0,
|
||||||
|
status: "done",
|
||||||
|
});
|
||||||
|
} else if (progress.status === "initiate") {
|
||||||
|
send({
|
||||||
|
type: "progress",
|
||||||
|
file: progress.file || progress.name || "",
|
||||||
|
progress: 0,
|
||||||
|
loaded: 0,
|
||||||
|
total: 0,
|
||||||
|
status: "initiate",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
activeDevice = dev;
|
||||||
|
console.log(
|
||||||
|
`[WhisperWorker] Model loaded successfully (device: ${dev})`,
|
||||||
|
);
|
||||||
|
send({ type: "load-result", success: true, device: dev });
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
lastError = err;
|
||||||
|
console.error(
|
||||||
|
`[WhisperWorker] Failed to load with device "${dev}":`,
|
||||||
|
err.message || err,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dev === "cpu" && devicesToTry.includes("wasm")) {
|
||||||
|
send({
|
||||||
|
type: "status",
|
||||||
|
message: `Native CPU backend failed (${err.message}). Trying WASM fallback...`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset pipeline state before retry
|
||||||
|
whisperPipeline = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All devices failed
|
||||||
|
throw lastError || new Error("All ONNX backends failed");
|
||||||
|
} catch (error) {
|
||||||
|
send({ type: "load-result", success: false, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transcribe(audioBase64, language) {
|
||||||
|
if (!whisperPipeline) {
|
||||||
|
send({
|
||||||
|
type: "transcribe-result",
|
||||||
|
success: false,
|
||||||
|
error: "Whisper pipeline not loaded",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pcm16Buffer = Buffer.from(audioBase64, "base64");
|
||||||
|
|
||||||
|
if (pcm16Buffer.length < 2) {
|
||||||
|
send({
|
||||||
|
type: "transcribe-result",
|
||||||
|
success: false,
|
||||||
|
error: "Audio buffer too small",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap at ~30 seconds (16kHz, 16-bit mono)
|
||||||
|
const maxBytes = 16000 * 2 * 30;
|
||||||
|
const audioData =
|
||||||
|
pcm16Buffer.length > maxBytes
|
||||||
|
? pcm16Buffer.slice(0, maxBytes)
|
||||||
|
: pcm16Buffer;
|
||||||
|
|
||||||
|
const float32Audio = pcm16ToFloat32(audioData);
|
||||||
|
if (float32Audio.length === 0) {
|
||||||
|
send({
|
||||||
|
type: "transcribe-result",
|
||||||
|
success: false,
|
||||||
|
error: "Empty audio after conversion",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build pipeline options with the requested language
|
||||||
|
const pipelineOpts = {
|
||||||
|
sampling_rate: 16000,
|
||||||
|
task: "transcribe",
|
||||||
|
};
|
||||||
|
if (language && language !== "auto") {
|
||||||
|
pipelineOpts.language = language;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await whisperPipeline(float32Audio, pipelineOpts);
|
||||||
|
|
||||||
|
const text = result.text?.trim() || "";
|
||||||
|
send({ type: "transcribe-result", success: true, text });
|
||||||
|
} catch (error) {
|
||||||
|
send({
|
||||||
|
type: "transcribe-result",
|
||||||
|
success: false,
|
||||||
|
error: error.message || String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function send(msg) {
|
||||||
|
try {
|
||||||
|
if (process.send) {
|
||||||
|
process.send(msg);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Parent may have disconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on("message", (msg) => {
|
||||||
|
switch (msg.type) {
|
||||||
|
case "load":
|
||||||
|
loadModel(msg.modelName, msg.cacheDir, msg.device).catch((err) => {
|
||||||
|
send({ type: "load-result", success: false, error: err.message });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "transcribe":
|
||||||
|
transcribe(msg.audioBase64, msg.language).catch((err) => {
|
||||||
|
send({ type: "transcribe-result", success: false, error: err.message });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "shutdown":
|
||||||
|
// Dispose the ONNX session gracefully before exiting to avoid
|
||||||
|
// native cleanup race conditions (SIGABRT on mutex destroy).
|
||||||
|
(async () => {
|
||||||
|
if (whisperPipeline) {
|
||||||
|
try {
|
||||||
|
if (typeof whisperPipeline.dispose === "function") {
|
||||||
|
await whisperPipeline.dispose();
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Best-effort cleanup
|
||||||
|
}
|
||||||
|
whisperPipeline = null;
|
||||||
|
}
|
||||||
|
// Small delay to let native threads wind down
|
||||||
|
setTimeout(() => process.exit(0), 200);
|
||||||
|
})();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Signal readiness to parent
|
||||||
|
send({ type: "ready" });
|
||||||
+131
-70
@@ -1,6 +1,6 @@
|
|||||||
const { BrowserWindow, globalShortcut, ipcMain, screen } = require('electron');
|
const { BrowserWindow, globalShortcut, ipcMain, screen } = require("electron");
|
||||||
const path = require('node:path');
|
const path = require("node:path");
|
||||||
const storage = require('../storage');
|
const storage = require("../storage");
|
||||||
|
|
||||||
let mouseEventsIgnored = false;
|
let mouseEventsIgnored = false;
|
||||||
|
|
||||||
@@ -20,21 +20,21 @@ function createWindow(sendToRenderer, geminiSessionRef) {
|
|||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
contextIsolation: false, // TODO: change to true
|
contextIsolation: false, // TODO: change to true
|
||||||
backgroundThrottling: false,
|
backgroundThrottling: false,
|
||||||
enableBlinkFeatures: 'GetDisplayMedia',
|
enableBlinkFeatures: "GetDisplayMedia",
|
||||||
webSecurity: true,
|
webSecurity: true,
|
||||||
allowRunningInsecureContent: false,
|
allowRunningInsecureContent: false,
|
||||||
},
|
},
|
||||||
backgroundColor: '#00000000',
|
backgroundColor: "#00000000",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { session, desktopCapturer } = require('electron');
|
const { session, desktopCapturer } = require("electron");
|
||||||
session.defaultSession.setDisplayMediaRequestHandler(
|
session.defaultSession.setDisplayMediaRequestHandler(
|
||||||
(request, callback) => {
|
(request, callback) => {
|
||||||
desktopCapturer.getSources({ types: ['screen'] }).then(sources => {
|
desktopCapturer.getSources({ types: ["screen"] }).then((sources) => {
|
||||||
callback({ video: sources[0], audio: 'loopback' });
|
callback({ video: sources[0], audio: "loopback" });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ useSystemPicker: true }
|
{ useSystemPicker: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
mainWindow.setResizable(false);
|
mainWindow.setResizable(false);
|
||||||
@@ -42,20 +42,20 @@ function createWindow(sendToRenderer, geminiSessionRef) {
|
|||||||
mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
||||||
|
|
||||||
// Hide from Windows taskbar
|
// Hide from Windows taskbar
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === "win32") {
|
||||||
try {
|
try {
|
||||||
mainWindow.setSkipTaskbar(true);
|
mainWindow.setSkipTaskbar(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Could not hide from taskbar:', error.message);
|
console.warn("Could not hide from taskbar:", error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide from Mission Control on macOS
|
// Hide from Mission Control on macOS
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === "darwin") {
|
||||||
try {
|
try {
|
||||||
mainWindow.setHiddenInMissionControl(true);
|
mainWindow.setHiddenInMissionControl(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Could not hide from Mission Control:', error.message);
|
console.warn("Could not hide from Mission Control:", error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,14 +66,14 @@ function createWindow(sendToRenderer, geminiSessionRef) {
|
|||||||
const y = 0;
|
const y = 0;
|
||||||
mainWindow.setPosition(x, y);
|
mainWindow.setPosition(x, y);
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === "win32") {
|
||||||
mainWindow.setAlwaysOnTop(true, 'screen-saver', 1);
|
mainWindow.setAlwaysOnTop(true, "screen-saver", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
mainWindow.loadFile(path.join(__dirname, '../index.html'));
|
mainWindow.loadFile(path.join(__dirname, "../index.html"));
|
||||||
|
|
||||||
// After window is created, initialize keybinds
|
// After window is created, initialize keybinds
|
||||||
mainWindow.webContents.once('dom-ready', () => {
|
mainWindow.webContents.once("dom-ready", () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const defaultKeybinds = getDefaultKeybinds();
|
const defaultKeybinds = getDefaultKeybinds();
|
||||||
let keybinds = defaultKeybinds;
|
let keybinds = defaultKeybinds;
|
||||||
@@ -84,7 +84,12 @@ function createWindow(sendToRenderer, geminiSessionRef) {
|
|||||||
keybinds = { ...defaultKeybinds, ...savedKeybinds };
|
keybinds = { ...defaultKeybinds, ...savedKeybinds };
|
||||||
}
|
}
|
||||||
|
|
||||||
updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessionRef);
|
updateGlobalShortcuts(
|
||||||
|
keybinds,
|
||||||
|
mainWindow,
|
||||||
|
sendToRenderer,
|
||||||
|
geminiSessionRef,
|
||||||
|
);
|
||||||
}, 150);
|
}, 150);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,25 +99,31 @@ function createWindow(sendToRenderer, geminiSessionRef) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultKeybinds() {
|
function getDefaultKeybinds() {
|
||||||
const isMac = process.platform === 'darwin';
|
const isMac = process.platform === "darwin";
|
||||||
return {
|
return {
|
||||||
moveUp: isMac ? 'Alt+Up' : 'Ctrl+Up',
|
moveUp: isMac ? "Alt+Up" : "Ctrl+Up",
|
||||||
moveDown: isMac ? 'Alt+Down' : 'Ctrl+Down',
|
moveDown: isMac ? "Alt+Down" : "Ctrl+Down",
|
||||||
moveLeft: isMac ? 'Alt+Left' : 'Ctrl+Left',
|
moveLeft: isMac ? "Alt+Left" : "Ctrl+Left",
|
||||||
moveRight: isMac ? 'Alt+Right' : 'Ctrl+Right',
|
moveRight: isMac ? "Alt+Right" : "Ctrl+Right",
|
||||||
toggleVisibility: isMac ? 'Cmd+\\' : 'Ctrl+\\',
|
toggleVisibility: isMac ? "Cmd+\\" : "Ctrl+\\",
|
||||||
toggleClickThrough: isMac ? 'Cmd+M' : 'Ctrl+M',
|
toggleClickThrough: isMac ? "Cmd+M" : "Ctrl+M",
|
||||||
nextStep: isMac ? 'Cmd+Enter' : 'Ctrl+Enter',
|
nextStep: isMac ? "Cmd+Enter" : "Ctrl+Enter",
|
||||||
previousResponse: isMac ? 'Cmd+[' : 'Ctrl+[',
|
previousResponse: isMac ? "Cmd+[" : "Ctrl+[",
|
||||||
nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]',
|
nextResponse: isMac ? "Cmd+]" : "Ctrl+]",
|
||||||
scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up',
|
scrollUp: isMac ? "Cmd+Shift+Up" : "Ctrl+Shift+Up",
|
||||||
scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down',
|
scrollDown: isMac ? "Cmd+Shift+Down" : "Ctrl+Shift+Down",
|
||||||
emergencyErase: isMac ? 'Cmd+Shift+E' : 'Ctrl+Shift+E',
|
expandResponse: isMac ? "Cmd+E" : "Ctrl+E",
|
||||||
|
emergencyErase: isMac ? "Cmd+Shift+E" : "Ctrl+Shift+E",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessionRef) {
|
function updateGlobalShortcuts(
|
||||||
console.log('Updating global shortcuts with:', keybinds);
|
keybinds,
|
||||||
|
mainWindow,
|
||||||
|
sendToRenderer,
|
||||||
|
geminiSessionRef,
|
||||||
|
) {
|
||||||
|
console.log("Updating global shortcuts with:", keybinds);
|
||||||
|
|
||||||
// Unregister all existing shortcuts
|
// Unregister all existing shortcuts
|
||||||
globalShortcut.unregisterAll();
|
globalShortcut.unregisterAll();
|
||||||
@@ -146,7 +157,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Register each movement shortcut
|
// Register each movement shortcut
|
||||||
Object.keys(movementActions).forEach(action => {
|
Object.keys(movementActions).forEach((action) => {
|
||||||
const keybind = keybinds[action];
|
const keybind = keybinds[action];
|
||||||
if (keybind) {
|
if (keybind) {
|
||||||
try {
|
try {
|
||||||
@@ -170,7 +181,10 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
});
|
});
|
||||||
console.log(`Registered toggleVisibility: ${keybinds.toggleVisibility}`);
|
console.log(`Registered toggleVisibility: ${keybinds.toggleVisibility}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`, error);
|
console.error(
|
||||||
|
`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,16 +195,24 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
mouseEventsIgnored = !mouseEventsIgnored;
|
mouseEventsIgnored = !mouseEventsIgnored;
|
||||||
if (mouseEventsIgnored) {
|
if (mouseEventsIgnored) {
|
||||||
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||||
console.log('Mouse events ignored');
|
console.log("Mouse events ignored");
|
||||||
} else {
|
} else {
|
||||||
mainWindow.setIgnoreMouseEvents(false);
|
mainWindow.setIgnoreMouseEvents(false);
|
||||||
console.log('Mouse events enabled');
|
console.log("Mouse events enabled");
|
||||||
}
|
}
|
||||||
mainWindow.webContents.send('click-through-toggled', mouseEventsIgnored);
|
mainWindow.webContents.send(
|
||||||
|
"click-through-toggled",
|
||||||
|
mouseEventsIgnored,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
console.log(`Registered toggleClickThrough: ${keybinds.toggleClickThrough}`);
|
console.log(
|
||||||
|
`Registered toggleClickThrough: ${keybinds.toggleClickThrough}`,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register toggleClickThrough (${keybinds.toggleClickThrough}):`, error);
|
console.error(
|
||||||
|
`Failed to register toggleClickThrough (${keybinds.toggleClickThrough}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,23 +220,26 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
if (keybinds.nextStep) {
|
if (keybinds.nextStep) {
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(keybinds.nextStep, async () => {
|
globalShortcut.register(keybinds.nextStep, async () => {
|
||||||
console.log('Next step shortcut triggered');
|
console.log("Next step shortcut triggered");
|
||||||
try {
|
try {
|
||||||
// Determine the shortcut key format
|
// Determine the shortcut key format
|
||||||
const isMac = process.platform === 'darwin';
|
const isMac = process.platform === "darwin";
|
||||||
const shortcutKey = isMac ? 'cmd+enter' : 'ctrl+enter';
|
const shortcutKey = isMac ? "cmd+enter" : "ctrl+enter";
|
||||||
|
|
||||||
// Use the new handleShortcut function
|
// Use the new handleShortcut function
|
||||||
mainWindow.webContents.executeJavaScript(`
|
mainWindow.webContents.executeJavaScript(`
|
||||||
cheatingDaddy.handleShortcut('${shortcutKey}');
|
cheatingDaddy.handleShortcut('${shortcutKey}');
|
||||||
`);
|
`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error handling next step shortcut:', error);
|
console.error("Error handling next step shortcut:", error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log(`Registered nextStep: ${keybinds.nextStep}`);
|
console.log(`Registered nextStep: ${keybinds.nextStep}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register nextStep (${keybinds.nextStep}):`, error);
|
console.error(
|
||||||
|
`Failed to register nextStep (${keybinds.nextStep}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,12 +247,15 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
if (keybinds.previousResponse) {
|
if (keybinds.previousResponse) {
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(keybinds.previousResponse, () => {
|
globalShortcut.register(keybinds.previousResponse, () => {
|
||||||
console.log('Previous response shortcut triggered');
|
console.log("Previous response shortcut triggered");
|
||||||
sendToRenderer('navigate-previous-response');
|
sendToRenderer("navigate-previous-response");
|
||||||
});
|
});
|
||||||
console.log(`Registered previousResponse: ${keybinds.previousResponse}`);
|
console.log(`Registered previousResponse: ${keybinds.previousResponse}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register previousResponse (${keybinds.previousResponse}):`, error);
|
console.error(
|
||||||
|
`Failed to register previousResponse (${keybinds.previousResponse}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,12 +263,15 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
if (keybinds.nextResponse) {
|
if (keybinds.nextResponse) {
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(keybinds.nextResponse, () => {
|
globalShortcut.register(keybinds.nextResponse, () => {
|
||||||
console.log('Next response shortcut triggered');
|
console.log("Next response shortcut triggered");
|
||||||
sendToRenderer('navigate-next-response');
|
sendToRenderer("navigate-next-response");
|
||||||
});
|
});
|
||||||
console.log(`Registered nextResponse: ${keybinds.nextResponse}`);
|
console.log(`Registered nextResponse: ${keybinds.nextResponse}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register nextResponse (${keybinds.nextResponse}):`, error);
|
console.error(
|
||||||
|
`Failed to register nextResponse (${keybinds.nextResponse}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,12 +279,15 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
if (keybinds.scrollUp) {
|
if (keybinds.scrollUp) {
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(keybinds.scrollUp, () => {
|
globalShortcut.register(keybinds.scrollUp, () => {
|
||||||
console.log('Scroll up shortcut triggered');
|
console.log("Scroll up shortcut triggered");
|
||||||
sendToRenderer('scroll-response-up');
|
sendToRenderer("scroll-response-up");
|
||||||
});
|
});
|
||||||
console.log(`Registered scrollUp: ${keybinds.scrollUp}`);
|
console.log(`Registered scrollUp: ${keybinds.scrollUp}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register scrollUp (${keybinds.scrollUp}):`, error);
|
console.error(
|
||||||
|
`Failed to register scrollUp (${keybinds.scrollUp}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,12 +295,31 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
if (keybinds.scrollDown) {
|
if (keybinds.scrollDown) {
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(keybinds.scrollDown, () => {
|
globalShortcut.register(keybinds.scrollDown, () => {
|
||||||
console.log('Scroll down shortcut triggered');
|
console.log("Scroll down shortcut triggered");
|
||||||
sendToRenderer('scroll-response-down');
|
sendToRenderer("scroll-response-down");
|
||||||
});
|
});
|
||||||
console.log(`Registered scrollDown: ${keybinds.scrollDown}`);
|
console.log(`Registered scrollDown: ${keybinds.scrollDown}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register scrollDown (${keybinds.scrollDown}):`, error);
|
console.error(
|
||||||
|
`Failed to register scrollDown (${keybinds.scrollDown}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register expand response shortcut
|
||||||
|
if (keybinds.expandResponse) {
|
||||||
|
try {
|
||||||
|
globalShortcut.register(keybinds.expandResponse, () => {
|
||||||
|
console.log("Expand response shortcut triggered");
|
||||||
|
sendToRenderer("expand-response");
|
||||||
|
});
|
||||||
|
console.log(`Registered expandResponse: ${keybinds.expandResponse}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to register expandResponse (${keybinds.expandResponse}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +327,7 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
if (keybinds.emergencyErase) {
|
if (keybinds.emergencyErase) {
|
||||||
try {
|
try {
|
||||||
globalShortcut.register(keybinds.emergencyErase, () => {
|
globalShortcut.register(keybinds.emergencyErase, () => {
|
||||||
console.log('Emergency Erase triggered!');
|
console.log("Emergency Erase triggered!");
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.hide();
|
mainWindow.hide();
|
||||||
|
|
||||||
@@ -283,28 +336,31 @@ function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessi
|
|||||||
geminiSessionRef.current = null;
|
geminiSessionRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToRenderer('clear-sensitive-data');
|
sendToRenderer("clear-sensitive-data");
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const { app } = require('electron');
|
const { app } = require("electron");
|
||||||
app.quit();
|
app.quit();
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log(`Registered emergencyErase: ${keybinds.emergencyErase}`);
|
console.log(`Registered emergencyErase: ${keybinds.emergencyErase}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register emergencyErase (${keybinds.emergencyErase}):`, error);
|
console.error(
|
||||||
|
`Failed to register emergencyErase (${keybinds.emergencyErase}):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
|
function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
|
||||||
ipcMain.on('view-changed', (event, view) => {
|
ipcMain.on("view-changed", (event, view) => {
|
||||||
if (!mainWindow.isDestroyed()) {
|
if (!mainWindow.isDestroyed()) {
|
||||||
const primaryDisplay = screen.getPrimaryDisplay();
|
const primaryDisplay = screen.getPrimaryDisplay();
|
||||||
const { width: screenWidth } = primaryDisplay.workAreaSize;
|
const { width: screenWidth } = primaryDisplay.workAreaSize;
|
||||||
|
|
||||||
if (view === 'assistant') {
|
if (view === "assistant") {
|
||||||
// Shrink window for live view
|
// Shrink window for live view
|
||||||
const liveWidth = 850;
|
const liveWidth = 850;
|
||||||
const liveHeight = 400;
|
const liveHeight = 400;
|
||||||
@@ -323,22 +379,27 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('window-minimize', () => {
|
ipcMain.handle("window-minimize", () => {
|
||||||
if (!mainWindow.isDestroyed()) {
|
if (!mainWindow.isDestroyed()) {
|
||||||
mainWindow.minimize();
|
mainWindow.minimize();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('update-keybinds', (event, newKeybinds) => {
|
ipcMain.on("update-keybinds", (event, newKeybinds) => {
|
||||||
if (!mainWindow.isDestroyed()) {
|
if (!mainWindow.isDestroyed()) {
|
||||||
updateGlobalShortcuts(newKeybinds, mainWindow, sendToRenderer, geminiSessionRef);
|
updateGlobalShortcuts(
|
||||||
|
newKeybinds,
|
||||||
|
mainWindow,
|
||||||
|
sendToRenderer,
|
||||||
|
geminiSessionRef,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('toggle-window-visibility', async event => {
|
ipcMain.handle("toggle-window-visibility", async (event) => {
|
||||||
try {
|
try {
|
||||||
if (mainWindow.isDestroyed()) {
|
if (mainWindow.isDestroyed()) {
|
||||||
return { success: false, error: 'Window has been destroyed' };
|
return { success: false, error: "Window has been destroyed" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mainWindow.isVisible()) {
|
if (mainWindow.isVisible()) {
|
||||||
@@ -348,12 +409,12 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
|
|||||||
}
|
}
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling window visibility:', error);
|
console.error("Error toggling window visibility:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('update-sizes', async event => {
|
ipcMain.handle("update-sizes", async (event) => {
|
||||||
// With the sidebar layout, the window size is user-controlled.
|
// With the sidebar layout, the window size is user-controlled.
|
||||||
// This handler is kept for compatibility but is a no-op now.
|
// This handler is kept for compatibility but is a no-op now.
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|||||||
Reference in New Issue
Block a user