Skip to content

Issue/gemnai#188

Open
sathwik13198 wants to merge 130 commits intosohzm:masterfrom
sathwik13198:issue/gemnai
Open

Issue/gemnai#188
sathwik13198 wants to merge 130 commits intosohzm:masterfrom
sathwik13198:issue/gemnai

Conversation

@sathwik13198
Copy link

@sathwik13198 sathwik13198 commented Dec 27, 2025

Pseudo-Live Interview Assistant

A near-real-time interview assistant that listens to spoken interview questions, understands them, and generates concise, accurate answers using Gemini — without relying on gated or unsupported Live APIs.


📌 Overview

This project is designed to assist users during interviews by:

  • Listening to interview questions spoken aloud (Zoom / Google Meet / Microsoft Teams)
  • Understanding the question context
  • Generating correct and concise answers in near real time
  • Supporting screenshots along with text when required

The system follows a pseudo-live architecture that is production-safe, scalable, and compatible with currently available Gemini models.


❗ Original Issue (Gemini Live API)

The initial implementation attempted to use Gemini Live API (bidiGenerateContent) for streaming audio + reasoning.

Root Causes

1. Unsupported Schema

The Gemini Live API does not support:

  • Speaker diarization
  • enableSpeakerDiarization
  • minSpeakerCount

This caused early JSON validation failures.

2. Live API Access Is Gated

Inspection via GET /v1beta/models revealed:

  • No models available to the API key supported bidiGenerateContent
  • No *-live-* models were enabled
  • Live API access is project-level and allow-listed

➡️ This was an account / project limitation, not a code bug.


💡 Key Insight

Gemini is fundamentally a text-first reasoning model.

It does not require raw audio input to:

  • Understand interview questions
  • Generate accurate answers

Audio must be converted to text before reasoning, which is how Copilot-style tools work.


✅ Implemented Solution: Pseudo-Live Architecture

Instead of:

Audio → Gemini Live API

The system uses:

Audio → Speech-to-Text → Text → Gemini

This approach:

  • Avoids gated Live APIs
  • Is production-safe
  • Provides near-real-time performance
  • Works with currently available Gemini models

🧠 Final Architecture

Interviewer speaks (Zoom / Meet / Teams)
        ↓
System audio capture
        ↓
Voice Activity Detection (VAD)
        ↓
Streaming Speech-to-Text
        ↓
Clean interview question (text)
        ↓
Gemini 2.5 Flash (generateContent)
        ↓
Answer displayed to user

⏱️ Latency Characteristics

Typical timings:

  • Audio capture + VAD: ~10–50 ms
  • Streaming STT: ~100–300 ms
  • Gemini 2.5 Flash response: ~200–500 ms

Total end-to-end latency: ~400–800 ms


⚙️ Important Implementation Details

  • Partial transcripts are not sent to Gemini
  • Text is submitted only when:
    • Silence > ~600 ms
    • Sentence ends (?, .)
    • VAD commit event
  • Screenshots and text can be sent together in a single request

🏁 Final Verdict

This pseudo-live architecture:

  • Fully resolves the original Gemini API issue
  • Avoids unsupported and gated APIs
  • Is stable, scalable, and production-ready
  • Can later upgrade to the Live API with minimal changes

🙌 Credits & Acknowledgment

This repository is forked from Sushma1969.

All credit for the original idea, base architecture, and implementation goes to Sushma.
This fork only introduces a targeted fix and architectural adjustment required due to Gemini API limitations.


📄 License

This project follows the same license as the original repository.

Summary by CodeRabbit

  • New Features

    • Voice Activity Detection with Automatic and Manual modes, manual mic toggle, and keyboard toggle
    • Pseudo-live streaming assistant with real-time STT (multiple providers) and improved model selection/badge
    • Ollama optional chat provider and provider test in Settings
    • Enhanced code syntax highlighting and LaTeX rendering; copy-response/code shortcut
  • Documentation

    • Added architecture, integration, quick-start, testing, and STT comparison guides
  • Refactor

    • Removed History view; updated defaults (exam profile, compact layout) and window/stealth behavior
  • Tests

    • Expanded test suites for languages, prompts, VAD, timer, token tracking, and STT features

✏️ Tip: You can customize this high-level summary in your review settings.

amantiwari57 and others added 30 commits July 20, 2025 09:22
* Added Highlight.js version 11.9.0 for enhanced code highlighting capabilities.
* Introduced a new dark theme CSS for VSCode to improve code readability in dark mode.
* Updated Electron version to ^30.0.5 for improved stability.
* Added new dependencies including @modelcontextprotocol/sdk, accepts, ajv, body-parser, and others to enhance functionality.
* Updated various existing dependencies to their latest versions for better performance and security.
* Removed unnecessary dev flags from some dependencies.
* Updated the response container's CSS for improved code block appearance, including a new dark background and enhanced padding.
* Implemented detailed syntax highlighting for various programming languages using Highlight.js, improving readability and user experience.
* Added functionality to automatically detect and display the programming language in code blocks.
* Introduced a method to enhance code blocks with language-specific classes for better styling consistency.
* Increased default window height to improve user experience.
* Adjusted dynamic window height settings for various views to provide better layout consistency.
* Added Highlight.js and VSCode dark theme CSS for improved code block styling and syntax highlighting.
…est-cases-in-github-actions

Replace Jest with Vitest
* build: improve Windows CI workflow

* ci: run tests on ubuntu and windows
- Add global Ctrl+G keyboard shortcut to clear session and automatically restart
- Implement handleClearAndRestart method in CheatingDaddyApp for session management
- Add "Restart session" button in AppHeader with Ctrl+G styling matching Hide button
- Add visual shortcut hints across MainView, AssistantView, and AppHeader
- Ensure Ctrl+G works globally across all application views
- Automatically start new session after clearing (100ms delay)
- Added @ricky0123/vad-node dependency to original project
- Integrated VAD toggle in CustomizeView with persistent settings
- Enhanced renderer.js with VAD audio processing for both Linux and Windows
- Added VAD IPC handlers in main process (send-vad-audio-segment, update-vad-setting)
- VAD processor conditionally initialized based on user settings
- Maintains backward compatibility - works with or without VAD enabled
- All tests passing (10/10) including new VAD test suite
- Reduced CustomizeView padding from 12px to 8px
- Decreased settings container gaps from 12px to 8px
- Optimized form section margins and padding
- Reduced checkbox group margin-bottom from 10px to 6px
- Decreased window height for customize view (720px normal, 620px compact)
- Maintained all functionality while making UI more compact
- All tests passing (10/10)
- Main window: 650x450  800x450 (wider for better usability)
- Compact layout: 500x350  650x350 (proportionally wider)
- Customize view max-width: 600px  750px (matches new width)
- Final dimensions provide perfect balance:
  * Height: Optimal for screen real estate (much shorter than original)
  * Width: Comfortable for content readability and interaction
- Maintains responsive design and all functionality
- All tests passing (10/10)
- Removed CLEANUP_SUMMARY.md, CONTRIBUTING.md, FEATURES.md
- Removed FRESH_FORK_SETUP.md, GITHUB_CONTRIBUTION_GUIDE.md
- Removed temporary UI comparison images (New UI.jpg, Old UI.jpg, Width Issue.jpg)
- Local repository now matches intended final state
- Ready for clean pull request
Kanishk1420 and others added 26 commits November 10, 2025 14:34
@coderabbitai
Copy link

coderabbitai bot commented Dec 27, 2025

📝 Walkthrough

Walkthrough

Adds a production-ready pseudo-live pipeline: VAD-driven audio capture, streaming STT (Gemini/Google), a Pseudo-Live Orchestrator with circuit-breaker and metrics, expanded IPC/preload surface, UI updates for VAD/mic/provider, many new STT/orchestrator modules, large docs, and extensive tests.

Changes

Cohort / File(s) Summary
Orchestrator & Resilience
src/utils/pseudoLiveOrchestrator.js
New production-grade PseudoLiveOrchestrator, CircuitBreaker, PerformanceMetrics; VAD/STT coordination, transcript deduplication, Gemini submission, telemetry and lifecycle APIs.
STT Implementations
src/utils/geminiSTT.js, src/utils/googleSpeechSTT.js, src/utils/speechToText.js
New streaming STT services: GeminiSTT (streaming, buffering, question detection), GoogleSpeechSTT (Google Cloud streaming), and a SpeechToText scaffold.
VAD & Audio
src/utils/vad.js, src/utils/gemini.js (macOS VAD parts), src/utils/googleSpeechSTT.js
New VADProcessor (modes, state machine, buffering), VAD integration in gemini flows, PCM/Float32 helpers and VAD-aware audio routing.
Gemini Integration & IPC
src/utils/gemini.js, src/utils/renderer.js, src/preload.js, src/index.js
Expanded Gemini routing (live/regular), pseudo-live toggles, many new IPC handlers (initialize/enable/stop/status/metrics/update/toggle), macOS audio hooks, and wider renderer API surface.
UI Components & Views
src/components/app/AppHeader.js, src/components/app/CheatingDaddyApp.js, src/components/views/AssistantView.js, src/components/views/CustomizeView.js, src/components/views/MainView.js, src/components/views/HelpView.js
New/updated public properties and handlers for mode/model, mic/VAD, chat provider, restart flow, localized greetings, LaTeX/code rendering, and shortcut wiring.
New/Removed Components
src/components/common/CustomDropdown.js, src/components/views/HistoryView.js, src/components/index.js
Added CustomDropdown component; removed HistoryView and its export from barrel file.
Ollama Integration
src/utils/ollama.js, INTEGRATION_GUIDE.js, verify-ollama-integration.js, docs (OLLAMA_INTEGRATION_COMPLETE.md, OLLAMA_INTEGRATION_COMPLETE_FINAL.md)
New Ollama utilities, provider routing in gemini, preload exposure, settings UI wiring, and verification script plus docs.
Prompts & Utilities
src/utils/prompts.js, src/utils/pseudoLiveOrchestrator.js
Prompt rewrites (profiles, coding rules, language gating) and orchestrator-linked prompt handling.
Window, Stealth & Native
src/utils/window.js, src/utils/stealthFeatures.js, src/utils/windowsNative.js
Window movement/drift prevention, always-on-top maintenance, new shortcuts (copyCodeBlocks, toggleMicrophone, restart), and Windows native screen-share hiding helpers.
Testing
src/__tests__/* (many test files)
Extensive new tests: geminiConversation, languages, prompts, timer, tokenTracking, vad, etc.
Docs & Guides
ARCHITECTURE.md, PSEUDO_LIVE_README.md, PSEUDO_LIVE_INTEGRATION.js, IMPLEMENTATION_*, PRODUCTION_IMPLEMENTATION.md, TESTING_GUIDE.md, QUICK_START.md, STT_COMPARISON.md, DELIVERABLES.md
Large documentation additions covering architecture, integration, testing, deployment, STT comparisons, and quickstart.
Build & Config
forge.config.js, package.json, .gitignore
packager asar -> object unpack rule, afterCopy macOS chmod step, maker config tweaks, dependency additions/upgrades, and new .gitignore entry CLAUDE.md.
Assets & HTML
src/index.html, src/assets/katex-0.16.9.min.css
KaTeX assets added and integrated into renderer HTML.
New Utilities & Services
src/utils/geminiSTT.js, src/utils/googleSpeechSTT.js, src/utils/ollama.js, src/utils/pseudoLiveOrchestrator.js, src/utils/vad.js, src/utils/speechToText.js
Multiple new service modules exported for STT, orchestrator, VAD and Ollama interactions.
Removed Legacy Docs
AGENTS.md
Deleted (guidelines/planning notes removed).

Sequence Diagram(s)

sequenceDiagram
    participant User as Interviewee
    participant AudioCapture as System Audio Capture
    participant VAD as VAD Processor
    participant STT as STT Service
    participant Orchestrator as Pseudo-Live Orchestrator
    participant Gemini as Gemini API
    participant UI as Renderer/UI

    User ->> AudioCapture: Speak (PCM frames)
    AudioCapture ->> VAD: Send frames
    activate VAD
    VAD ->> VAD: Detect speech / buffer
    alt Speech detected
        VAD ->> Orchestrator: Emit speech segment
    end
    deactivate VAD

    activate Orchestrator
    Orchestrator ->> STT: transcribe(segment)
    deactivate Orchestrator

    activate STT
    STT ->> STT: stream -> accumulate transcript
    STT ->> Orchestrator: onTranscriptComplete(text)
    deactivate STT

    activate Orchestrator
    Orchestrator ->> Orchestrator: circuit-breaker check & dedupe
    Orchestrator ->> Gemini: send transcript + context
    Gemini ->> Orchestrator: return response
    Orchestrator ->> UI: update transcript & response
    deactivate Orchestrator

    UI ->> User: render response
Loading
sequenceDiagram
    participant Renderer as Renderer
    participant IPC as IPC Channel
    participant Main as Main Process
    participant Orchestrator as PseudoLiveOrchestrator
    participant VAD as VADProcessor
    participant STT as GeminiSTTService

    Renderer ->> IPC: initialize-pseudo-live(config)
    IPC ->> Main: invoke initialize
    Main ->> Orchestrator: new PseudoLiveOrchestrator(config)
    Orchestrator ->> VAD: init VADProcessor
    Orchestrator ->> STT: init GeminiSTTService
    Main ->> IPC: ready
    IPC ->> Renderer: ready

    Renderer ->> IPC: send-audio-segment(pcm)
    IPC ->> Main: route to orchestrator.processAudioFrame
    Main ->> Orchestrator: processAudioFrame(pcm)
    Orchestrator ->> VAD: process frame
    alt VAD segment -> speech
        VAD ->> Orchestrator: emit segment
        Orchestrator ->> STT: transcribe(segment)
        STT ->> Orchestrator: transcriptComplete
        Orchestrator ->> Main: update UI via IPC
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 I hopped through code to catch each voice—
VAD ears twitch, STT lends choice.
Circuits watch and metrics beam,
Pseudo-live brings the near-live dream.
Now interviews flow, swift as steam.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.83% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The PR title 'Issue/gemnai' is vague and does not clearly convey the substantial changes made in this PR, which include pseudo-live architecture, STT integration, VAD improvements, Ollama support, and extensive documentation. Use a more descriptive title that reflects the main changes, such as 'Implement pseudo-live interview assistant with STT and Ollama integration' or 'Replace Gemini Live API with pseudo-live architecture using STT'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (14)
src/components/common/CustomDropdown.js-17-20 (1)

17-20: Unusual container width may cause layout overflow.

The .dropdown-container has width: 120%, which will extend beyond its parent container and may cause horizontal scrolling or clipping issues.

🔎 Suggested fix
         .dropdown-container {
             position: relative;
-            width: 120%;
+            width: 100%;
         }
src/components/common/CustomDropdown.js-184-189 (1)

184-189: Click-outside detection may not work correctly with shadow DOM.

this.shadowRoot.contains(e.target) will return false for clicks inside the shadow DOM when the event originates from the document level, because e.target is retargeted to the host element. This could cause the dropdown to close unexpectedly when clicking inside it.

🔎 Suggested fix using composedPath()
     _handleClickOutside(e) {
-        if (!this.shadowRoot.contains(e.target) && this.isOpen) {
+        const path = e.composedPath();
+        if (!path.includes(this) && this.isOpen) {
             this.isOpen = false;
             this.requestUpdate();
         }
     }
src/utils/speechToText.js-264-268 (1)

264-268: Language in callback metadata uses stale config value.

The commitTranscript method passes STT_CONFIG.languageCode which may not reflect the language used during streaming if startStreaming was called with a different languageCode parameter. Use an instance property set during startStreaming.

PSEUDO_LIVE_README.md-39-39 (1)

39-39: Silence threshold documentation doesn't match implementation.

The documentation states "600ms threshold" but src/utils/vad.js defines silenceThreshold: 200. This discrepancy appears in multiple places in this file (lines 39, 81, 397).

Update all references to use the correct 200ms value, or update the code if 600ms is the intended threshold.

ARCHITECTURE.md-141-150 (1)

141-150: Documentation shows incorrect silenceThreshold value.

The documentation states silenceThreshold: 600 but src/utils/vad.js defines it as 200 (line 18). This discrepancy could mislead developers tuning VAD behavior.

🔎 Proposed fix
 **Thresholds:**
 ```javascript
 {
-    silenceThreshold: 600,    // 600ms silence = question done
+    silenceThreshold: 200,    // 200ms silence = question done (OPTIMIZED for faster response)
     minRecordingTime: 200,    // 200ms minimum
     maxRecordingTime: 20000,  // 20s maximum
     preSpeechPadFrames: 2,    // 2 frames before speech
     postSpeechPadFrames: 1,   // 1 frame after speech
 }
ARCHITECTURE.md-408-418 (1)

408-418: Latency percentages sum to 114%, which is mathematically incorrect.

The percentage breakdown for Gemini STT shows STT Buffering at 55% and Gemini Response at 52%, totaling 114% (plus 4% + 3% = 121%). This appears to be a copy-paste error from the previous section.

🔎 Proposed fix

Recalculate the percentages based on actual latency values:

 Component          Target    Actual    % of Total
 ─────────────────────────────────────────────────
-Audio Capture      10-50ms   ~30ms     4%
-VAD Processing     10-50ms   ~20ms     3%
-STT Buffering      300-500ms ~410ms    55%
-Gemini Response    200-500ms ~387ms    52%
+Audio Capture      10-50ms   ~30ms     4%
+VAD Processing     10-50ms   ~20ms     2%
+STT Buffering      300-500ms ~410ms    48%
+Gemini Response    200-500ms ~387ms    46%
 ─────────────────────────────────────────────────
 TOTAL             600-1000ms 847ms     100% ⚠️
src/components/views/MainView.js-310-312 (1)

310-312: Malformed HTML structure and platform-inconsistent shortcut hint.

  1. The HTML indentation is broken - the <p> tag alignment is inconsistent with the surrounding markup.
  2. The shortcut text hardcodes "Ctrl+Alt+R" but Mac users would expect "Cmd+Option+R". Other shortcuts in this file (see getStartButtonText()) already handle platform detection.
🔎 Proposed fix
-                    <p class="shortcut-hint">
-            Press <strong>Ctrl+Alt+R</strong> to clear session and automatically restart
-        </p>
+            <p class="shortcut-hint">
+                Press <strong>${this.isMac() ? 'Cmd+Option+R' : 'Ctrl+Alt+R'}</strong> to clear session and automatically restart
+            </p>

You'll need to extract the platform detection logic or add a helper method:

isMac() {
    return navigator.platform.toUpperCase().indexOf('MAC') >= 0;
}

Committable suggestion skipped: line range outside the PR's diff.

src/utils/windowsNative.js-98-99 (1)

98-99: Unused import: ref-napi is imported but never used.

Line 99 imports ref-napi but it's never referenced in the ffi-napi fallback logic.

🔎 Proposed fix
             const ffi = require('ffi-napi');
-            const ref = require('ref-napi');
src/utils/geminiSTT.js-193-194 (1)

193-194: Misleading log message in constructor.

"Service initialized" is logged in the constructor, but actual initialization (API key validation, model test) happens in the async initialize() method. This can mislead debugging.

🔎 Proposed fix
         console.log('✅ [GEMINI STT] Service initialized');
+        // Note: This is constructor completion. Call initialize() to fully set up the service.

Or change the message:

-        console.log('✅ [GEMINI STT] Service initialized');
+        console.log('🔧 [GEMINI STT] Service instance created (call initialize() to start)');
PSEUDO_LIVE_INTEGRATION.js-327-329 (1)

327-329: beforeunload handler cannot await async functions.

The beforeunload event handler is marked async and awaits shutdownPseudoLiveSystem(), but browsers do not wait for promises in beforeunload handlers. The shutdown may not complete before the page unloads.

🔎 Proposed fix

For Electron apps, consider using window.addEventListener('close') via IPC or the main process will-quit event instead. For synchronous cleanup in the renderer:

-window.addEventListener('beforeunload', async (e) => {
-    await shutdownPseudoLiveSystem();
-});
+window.addEventListener('beforeunload', (e) => {
+    // Trigger async cleanup but don't await
+    // Main process should handle graceful shutdown
+    window.api.stopPseudoLive?.();
+});

Or move cleanup to Electron's main process lifecycle events.

Committable suggestion skipped: line range outside the PR's diff.

PSEUDO_LIVE_INTEGRATION.js-475-491 (1)

475-491: Duplicate orchestrator-error listener may cause issues.

A listener for orchestrator-error is already registered at line 160. Adding another at line 475 means both handlers will fire on each error, potentially triggering double error notifications and conflicting restart logic.

🔎 Proposed fix

Consolidate the error handling into a single listener:

-// Errors
-window.api.on('orchestrator-error', (data) => {
-    const { error, timestamp } = data;
-    console.error('❌ Error:', error);
-    showErrorNotification('Error processing audio. Please try again.');
-});
+// Errors with auto-restart
+window.api.on('orchestrator-error', async (data) => {
+    const { error, timestamp } = data;
+    console.error('❌ Error:', error);
+    showErrorNotification('Error processing audio. Please try again.');
+    
+    // Auto-restart logic
+    if (restartAttempts < MAX_RESTART_ATTEMPTS) {
+        restartAttempts++;
+        console.log(`Attempting auto-restart (${restartAttempts}/${MAX_RESTART_ATTEMPTS})...`);
+        await new Promise(resolve => setTimeout(resolve, 5000));
+        const success = await initializePseudoLiveSystem();
+        if (success) {
+            console.log('✅ Auto-restart successful');
+            restartAttempts = 0;
+        }
+    } else {
+        showErrorNotification('System failed to recover. Please restart the application.');
+    }
+});

Then remove the duplicate listener at lines 475-491.

Committable suggestion skipped: line range outside the PR's diff.

src/components/app/CheatingDaddyApp.js-299-310 (1)

299-310: The default model 'gemini-3-pro-preview' is valid, but the interview mode model is deprecated.

The default model 'gemini-3-pro-preview' is a valid Gemini 3 series identifier currently available via the API. However, the code also hardcodes gemini-2.0-flash-exp for interview mode (line 306), which is a legacy 2.0 variant that is being deprecated with a scheduled shutdown. Consider updating it to a current stable model like gemini-2.5-flash or gemini-3-pro.

src/components/app/CheatingDaddyApp.js-334-356 (1)

334-356: Indentation issue in handleClearAndRestart method definition.

The method definition has extra leading spaces that break indentation consistency with other methods in the class.

🔎 Fix indentation
-        handleClearAndRestart() {
+    handleClearAndRestart() {
         // Clear the current session and responses
         this.responses = [];
src/components/views/CustomizeView.js-920-948 (1)

920-948: update-vad-setting IPC has no handler on the main process

handleVADChange invokes:

await ipcRenderer.invoke('update-vad-setting', this.vadEnabled);

but the main process only defines an update-vad-mode handler in setupGeminiIpcHandlers; there is no ipcMain.handle('update-vad-setting', ...). This will cause ipcRenderer.invoke to reject with “no handler” and log an error every time the checkbox is toggled.

Either add a no‑op/logging handler on the main side or remove the IPC call and rely purely on localStorage until you need main‑process behavior.

🧹 Nitpick comments (36)
src/config.js (1)

38-50: Consider privacy implications of logging the config directory path.

The config directory path at line 38 includes the user's home directory path. While useful for debugging, this could expose user identifiers in logs that may be shared for support purposes.

Consider using a relative indicator or redacting part of the path in production builds.

🔎 Optional: Redact home directory in logs
 function ensureConfigDir() {
     const configDir = getConfigDir();
-    console.log('Config directory path:', configDir);
+    console.log('Config directory path:', configDir.replace(os.homedir(), '~'));
     if (!fs.existsSync(configDir)) {
src/components/common/CustomDropdown.js (1)

217-265: Add keyboard navigation for accessibility.

The dropdown lacks keyboard support (Tab focus, Enter/Space to open, Escape to close, Arrow keys to navigate options). This is an accessibility gap that prevents keyboard-only users from operating the control.

Consider adding:

  • tabindex="0" and role="listbox" on the container
  • role="option" on each option
  • aria-expanded state
  • @keydown handler for Enter, Escape, and Arrow key navigation
PRODUCTION_IMPLEMENTATION.md (1)

9-29: Minor: Add language identifier to ASCII diagram code block.

Per static analysis, fenced code blocks should have a language specified for better rendering.

🔎 Suggested fix
-```
+```text
 ┌─────────────────────────────────────────────────────────────────────┐
src/utils/window.js (2)

113-148: Consider reducing the polling interval or using event-driven approach.

The maintainTopPosition interval runs every 2 seconds indefinitely. While the cleanup on closed is correct, consider:

  1. Using a longer interval (e.g., 5 seconds) since this is a fallback mechanism
  2. Only activating when the window loses focus, rather than constant polling

The blur handler at lines 139-148 already handles immediate restoration, so the interval is a safety net that could be less frequent.


477-527: Extract inline JavaScript to a separate function for maintainability.

The 50-line inline JavaScript string passed to executeJavaScript is difficult to maintain, test, and debug. Consider extracting this logic to a function exposed via the preload script or IPC handler.

🔎 Suggested approach

In preload.js, expose a dedicated function:

// preload.js
contextBridge.exposeInMainWorld('api', {
    toggleMicrophoneFromShortcut: async () => {
        // Move the logic here, accessible from main process via IPC
    }
});

Then in window.js:

-                    mainWindow.webContents.executeJavaScript(`
-                        (async () => {
-                            // ... 50 lines of code ...
-                        })();
-                    `).then(result => {
+                    mainWindow.webContents.executeJavaScript(
+                        `window.api.toggleMicrophoneFromShortcut()`
+                    ).then(result => {
src/utils/speechToText.js (1)

130-140: Mock implementation should be clearly documented or completed.

The initializeStreamingConnection method is a placeholder. Consider either implementing proper streaming via @google-cloud/speech or throwing a "not implemented" error to fail fast during development.

src/index.html (1)

105-107: Consider using local KaTeX CSS instead of CDN for consistency.

The KaTeX JS is loaded from local assets (assets/katex-0.16.9.min.js), but the CSS is fetched from CDN. Per the AI summary, src/assets/katex-0.16.9.min.css exists locally. Using it would:

  • Ensure offline functionality
  • Maintain version consistency between CSS and JS
  • Avoid external network dependencies
🔎 Proposed fix
         <link rel="stylesheet" href="assets/highlight-vscode-dark.min.css">
-        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
+        <link rel="stylesheet" href="assets/katex-0.16.9.min.css">
         <script src="assets/katex-0.16.9.min.js"></script>
STT_COMPARISON.md (1)

29-48: Add language specifier to code blocks for consistency.

The performance and cost analysis blocks use ASCII diagrams. Adding a language specifier (e.g., text or plaintext) improves markdown linter compliance and ensures consistent rendering. As per static analysis hints.

🔎 Example fix
 ### Performance
-```
+```text
 End-to-End Latency: 400-800ms
 ├─ Audio capture:    10-50ms
 ...
QUICK_START.md (1)

102-109: Add language specifier to expected output code block.

The expected console output block should have a language specifier for consistent formatting. As per static analysis hints.

🔎 Proposed fix
 **Expected console output:**
-```
+```text
 🎤 [ORCHESTRATOR] Received audio segment from VAD
 📝 [ORCHESTRATOR] Complete transcript: "What is machine learning?"
IMPLEMENTATION_SUMMARY.md (1)

261-278: Add language specifiers to fenced code blocks.

Several code blocks are missing language specifiers (e.g., lines 262 and 275). This affects syntax highlighting and markdown linting.

🔎 Proposed fix
 ### Built-in Metrics
-```
+```javascript
 {
     totalRequests: 47,
     averageLatency: 623,  // Within target!
 ### Access Metrics
-```
+```javascript
 const status = await window.electron.ipcRenderer.invoke('get-orchestrator-status');
 console.log('Performance:', status.metrics);
src/__tests__/vad.test.js (2)

46-57: Tests are trivial and don't validate actual behavior.

These tests only verify that string literals exist in a hardcoded array. They don't test any real VAD mode functionality or behavior.

🔎 Suggested improvement

Consider testing actual VAD mode behavior when VADProcessor can be instantiated:

describe('VAD Modes', () => {
    it('should initialize in LISTENING state for automatic mode', async () => {
        // When VAD library is available, test:
        // const processor = new VADProcessor(() => {}, null, 'automatic');
        // await processor.initializeVAD();
        // expect(processor.getState()).toBe(VADState.LISTENING);
    });

    it('should initialize in PAUSED state for manual mode', async () => {
        // const processor = new VADProcessor(() => {}, null, 'manual');
        // await processor.initializeVAD();
        // expect(processor.getState()).toBe(VADState.PAUSED);
    });
});

38-43: Test doesn't verify dual VAD mode support.

The test name says "should support dual VAD modes" but only checks that VADProcessor is defined. It doesn't validate that both modes work correctly.

src/__tests__/geminiConversation.test.js (2)

76-86: Tests are trivial and don't validate actual behavior.

Similar to the VAD mode tests, these only verify that string literals exist in a hardcoded array. They don't test any real session mode functionality.

Consider testing actual mode-specific behavior, such as different prompts or response handling for each mode.


101-113: Auto-reset behavior is documented but not tested.

The comment mentions "Auto-reset logic happens in gemini.js after 20 responses" but the test only verifies that 20 turns can be saved. Consider adding a test that verifies the auto-reset actually occurs.

🔎 Suggested improvement
it('should auto-reset session after 20 responses', () => {
    initializeNewSession();
    
    // Simulate 21 conversation turns
    for (let i = 0; i < 21; i++) {
        saveConversationTurn(`question ${i}`, `answer ${i}`);
    }
    
    const data = getCurrentSessionData();
    // Verify auto-reset occurred
    expect(data.history.length).toBeLessThan(21);
});
src/__tests__/timer.test.js (1)

3-19: Consider extracting and importing the actual function from AppHeader.js.

The test duplicates the getElapsedTime logic instead of importing it from AppHeader.js. This means if the implementation changes, tests may pass while actual behavior differs.

If the function can be extracted to a utility module, import it directly in tests:

// utils/timeFormat.js
export function getElapsedTime(startTime) { ... }

// In tests
import { getElapsedTime } from '../utils/timeFormat';
src/components/views/HelpView.js (1)

357-364: Inconsistent shortcut handling for "Clear session and restart".

The clearSession shortcut is rendered inline using a ternary on isMacOS, but it's not included in the keybinds object (lines 252-268). This means:

  1. It won't be customizable via Settings like other shortcuts
  2. It won't persist if saved to localStorage

If intentional (non-customizable shortcut), this is fine. Otherwise, add it to getDefaultKeybinds() for consistency.

🔎 Proposed fix to add clearSession to keybinds
 getDefaultKeybinds() {
     const isMac = cheddar.isMacOS || navigator.platform.includes('Mac');
     return {
         moveUp: isMac ? 'Alt+Up' : 'Ctrl+Up',
         // ... existing keybinds ...
         copyCodeBlocks: isMac ? 'Cmd+Shift+C' : 'Ctrl+Shift+C',
+        clearSession: isMac ? 'Cmd+Alt+R' : 'Ctrl+Alt+R',
     };
 }

Then update line 363:

-<div class="shortcut-keys">${this.formatKeybind(isMacOS ? 'Cmd+Alt+R' : 'Ctrl+Alt+R')}</div>
+<div class="shortcut-keys">${this.formatKeybind(this.keybinds.clearSession)}</div>
PSEUDO_LIVE_INTEGRATION.js (1)

173-207: Interval timer never cleared; potential resource leak.

setInterval at line 175 runs indefinitely. If this integration code runs in a component that can be unmounted/reloaded, the interval will continue running and accumulate.

🔎 Proposed fix

Store the interval ID and clear it during shutdown:

+let performanceMonitoringInterval = null;
+
 function startPerformanceMonitoring() {
-    setInterval(async () => {
+    performanceMonitoringInterval = setInterval(async () => {
         // ... existing code ...
     }, 10000);
 }

+function stopPerformanceMonitoring() {
+    if (performanceMonitoringInterval) {
+        clearInterval(performanceMonitoringInterval);
+        performanceMonitoringInterval = null;
+    }
+}

Call stopPerformanceMonitoring() in shutdownPseudoLiveSystem().

INTEGRATION_GUIDE.js (1)

77-78: Variable shadowing in example code.

The function parameter is named model and line 77 also uses model for the Gemini model instance. This could cause confusion or bugs when adapting the example.

🔎 Proposed fix
-                    const model = genaiClient.getGenerativeModel({ 
+                    const textModel = genaiClient.getGenerativeModel({ 
                         model: 'gemini-2.5-flash',
                         systemInstruction: { parts: [{ text: systemPrompt }] },
                     });
                     
-                    const result = await model.generateContent(input.text);
+                    const result = await textModel.generateContent(input.text);
src/__tests__/tokenTracking.test.js (2)

3-72: Test duplicates production TokenTracker implementation.

The TokenTracker class is re-implemented in this test file instead of importing from renderer.js. If the production code changes, these tests may pass while the actual implementation is broken (or vice versa).

Consider extracting TokenTracker to a separate module that can be imported by both renderer.js and this test file:

// src/utils/tokenTracker.js
class TokenTracker { /* ... */ }
module.exports = { TokenTracker };

// In renderer.js
const { TokenTracker } = require('./tokenTracker');

// In test
const { TokenTracker } = require('../utils/tokenTracker');

121-129: Timing-dependent test may be flaky.

This test relies on Date.now() differences, which can be affected by system load or CI environment timing variations. The tolerance range (60-68 tokens for ~2 seconds) is reasonable but tight.

🔎 Consider using time mocking

For more reliable tests, use Jest's timer mocks:

it('calculates audio tokens based on elapsed time', () => {
    jest.useFakeTimers();
    const tracker = new TokenTracker();
    
    tracker.audioStartTime = Date.now();
    jest.advanceTimersByTime(2000); // Advance exactly 2 seconds
    tracker.trackAudioTokens();
    
    // 2 seconds × 32 tokens/second = 64 tokens
    const tokens = tracker.getTokensInLastMinute();
    expect(tokens).toBe(64);
    
    jest.useRealTimers();
});
TESTING_GUIDE.md (1)

71-83: Add language specifiers to "Expected Output" code blocks.

Several code blocks showing expected console output lack language specifiers (lines 72, 121, 182, 242, 301, 355). This triggers markdownlint MD040 warnings.

🔎 Proposed fix

Add a language specifier (e.g., text, console, or plaintext) to expected output blocks:

 **Expected Output:**
-```
+```text
 🧪 Testing orchestrator initialization...
 🚀 [ORCHESTRATOR] Initializing pseudo-live pipeline...

Also applies to: 120-130, 182-189, 241-250, 300-309, 354-367

src/utils/windowsNative.js (2)

26-38: Duplicated buffer-to-HWND conversion logic.

The buffer-to-handle conversion appears twice: lines 26-38 for PowerShell and lines 110-117 for ffi-napi. Consider extracting to a helper function.

🔎 Proposed refactor
function bufferToHwnd(nativeHandle) {
    if (Buffer.isBuffer(nativeHandle)) {
        if (nativeHandle.length === 8) {
            return nativeHandle.readBigUInt64LE(0);
        } else if (nativeHandle.length === 4) {
            return nativeHandle.readUInt32LE(0);
        }
        console.warn('Unexpected window handle size:', nativeHandle.length);
        return null;
    }
    return nativeHandle;
}

Then use it in both paths.

Also applies to: 110-117


80-82: Ambiguous timeout handling may mask real failures.

Logging "PowerShell command timed out, but may have succeeded" provides unclear feedback. The window style may or may not be set, making debugging difficult.

Consider verifying success after timeout by checking the window style, or at minimum distinguish this from success in logs:

         } catch (timeoutError) {
-            console.log('PowerShell command timed out, but may have succeeded');
+            console.warn('PowerShell command timed out - style change unverified');
         }
src/__tests__/prompts.test.js (1)

136-144: Minimal language support test provides limited coverage.

This test only verifies the prompt is defined. Consider testing that language instructions are actually embedded or that getSystemPrompt accepts a language parameter.

🔎 Proposed enhancement

If the prompts module supports language-specific content, add more specific tests:

it('has language instruction section', () => {
    const prompt = getSystemPrompt('interview', '', true);
    
    // Verify base prompt structure
    expect(prompt).toBeDefined();
    expect(typeof prompt).toBe('string');
    
    // If language support is dynamic, test the integration point
    // expect(prompt).toContain('LANGUAGE');
    // Or test with a language parameter if supported
});
src/utils/geminiSTT.js (1)

302-320: Queue processing lacks rate limiting for Gemini API.

The processQueue() method processes items as fast as possible without delays between API calls. Under high load (rapid speech), this could hit Gemini's rate limits.

🔎 Proposed fix to add rate limiting
     async processQueue() {
         if (this.isProcessingQueue || this.processingQueue.length === 0) {
             return;
         }
         
         this.isProcessingQueue = true;
         
         while (this.processingQueue.length > 0) {
             const item = this.processingQueue.shift();
             
             try {
                 await this.transcribeAudio(item.audio, item.duration);
+                // Rate limiting: minimum 100ms between API calls
+                await new Promise(resolve => setTimeout(resolve, 100));
             } catch (error) {
                 console.error('❌ [GEMINI STT] Error transcribing audio:', error);
             }
         }
         
         this.isProcessingQueue = false;
     }
src/index.js (1)

128-145: Potential infinite loop if flushStorageData throws repeatedly.

Using event.preventDefault() followed by app.exit() is correct, but if flushStorageData() fails, the error is caught and logged, then app.exit() is called. This is fine, but consider that before-quit may be called multiple times if the first quit attempt is prevented. A guard flag would prevent redundant flush attempts.

🔎 Suggested improvement with guard flag
+let isQuitting = false;
+
 app.on('before-quit', async (event) => {
     stopMacOSAudioCapture();
 
+    if (isQuitting) return;
+    isQuitting = true;
+
     // Flush localStorage and other storage to disk before quitting
     // This is CRITICAL for macOS to persist localStorage between restarts
     event.preventDefault();
src/utils/googleSpeechSTT.js (3)

122-143: Duplicate interimResults configuration.

interimResults: true is set both in STREAMING_CONFIG (line 39) and again when calling streamingRecognize (line 132). The spread operator already includes it from STREAMING_CONFIG.

🔎 Remove redundant parameter
             // Create recognize stream
             this.recognizeStream = this.client
                 .streamingRecognize({
                     config: streamConfig,
-                    interimResults: true,
                 })
                 .on('error', this.handleStreamError.bind(this))
                 .on('data', this.handleStreamData.bind(this));

194-201: Stream error handling stops streaming but doesn't notify caller.

When a stream error occurs, handleStreamError stops the stream and logs the error, but there's no callback to notify the caller about the failure. Consider adding an onError callback similar to onTranscriptComplete.

🔎 Add error callback
 class GoogleSpeechSTT {
-    constructor(onTranscriptComplete, onTranscriptPartial = null) {
+    constructor(onTranscriptComplete, onTranscriptPartial = null, onError = null) {
         this.onTranscriptComplete = onTranscriptComplete;
         this.onTranscriptPartial = onTranscriptPartial;
+        this.onError = onError;
         // ...
     }

     handleStreamError(error) {
         console.error('❌ [GOOGLE STT] Stream error:', error);
         
         // Stop streaming and notify
         this.stopStreaming();
         
-        // TODO: Implement retry logic if needed
+        if (this.onError) {
+            this.onError(error);
+        }
     }

347-357: PCM conversion logic duplicated with pseudoLiveOrchestrator.js.

This float32ToPCM16 function is nearly identical to the conversion in pseudoLiveOrchestrator.js (lines 1251-1257). Consider extracting to a shared utility module to avoid duplication.

README.md (1)

85-97: Consider using proper headings for VAD mode sections.

The static analysis correctly identifies that bold text is being used instead of proper Markdown headings. Using ### or #### headings would improve document structure and accessibility.

🔎 Use proper headings
 ### Voice Activity Detection (VAD) Modes
 
-**Automatic Smart Detection Mode**
+#### Automatic Smart Detection Mode
 - Continuously detects and processes speech in real-time
 - Best for natural conversations where the interviewer speaks continuously
 - Mic is always ON and listening
 
-**Manual Push-to-Talk Mode**
+#### Manual Push-to-Talk Mode
 - User-controlled mic toggle (Ctrl/Cmd + Shift + M)
src/components/app/CheatingDaddyApp.js (1)

358-370: Platform detection uses navigator.platform which is deprecated.

navigator.platform is deprecated and may be removed in future browser versions. Consider using navigator.userAgentData.platform with a fallback, or the existing cheddar.isMacOS if available in this context.

🔎 Use more reliable platform detection
     handleGlobalKeydown(e) {
         // Handle Ctrl+Alt+R (or Cmd+Option+R on Mac) for clearing and restarting session
-        const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
+        const isMac = navigator.userAgentData?.platform === 'macOS' || 
+                      navigator.platform?.toUpperCase().indexOf('MAC') >= 0;
         const isClearShortcut = isMac
src/preload.js (1)

189-191: The send method lacks channel validation.

Unlike the on method which validates channels, the send method allows sending to any channel. Consider adding a whitelist for outbound channels as well.

🔎 Add channel validation for send
     send: (channel, data) => {
+        const validSendChannels = [
+            'update-keybinds',
+            'view-changed',
+            // Add other valid outbound channels
+        ];
+        
+        if (!validSendChannels.includes(channel)) {
+            console.warn(`[preload] Invalid send channel: ${channel}`);
+            return;
+        }
         ipcRenderer.send(channel, data);
     },
src/__tests__/languages.test.js (1)

503-782: Test assertions are repetitive and don't verify actual question processing.

Many tests iterate through languages and questions but only assert that the language mapping exists, which is already tested earlier. The inner loop over questions doesn't add value since the assertion is the same regardless of the question content.

Consider consolidating these tests or making them verify actual behavior:

-        it('should be ready to handle behavioral questions in all languages', () => {
-            for (const lang of SUPPORTED_LANGUAGES) {
-                const mappedLanguage = GEMINI_LANGUAGE_MAP[lang.code];
-                expect(mappedLanguage).toBeDefined();
-                
-                for (const question of REAL_INTERVIEW_QUESTIONS.interview.behavioral) {
-                    expect(mappedLanguage).toBe(lang.expectedLanguage);
-                }
-            }
-        });
+        it('should have language mappings for all supported languages', () => {
+            for (const lang of SUPPORTED_LANGUAGES) {
+                expect(GEMINI_LANGUAGE_MAP[lang.code]).toBe(lang.expectedLanguage);
+            }
+        });

The question data is valuable for documentation purposes but doesn't need to be iterated in tests unless you're verifying actual question processing behavior.

src/utils/renderer.js (1)

169-179: initializeGemini defaults prevent localStorage mode/model from ever being used

Because the function has default parameters mode = 'interview' and model = 'gemini-2.5-flash', the expressions:

const selectedMode = mode || localStorage.getItem('selectedMode') || 'interview';
const selectedModel = model || localStorage.getItem('selectedModel') || 'gemini-2.5-flash';

will always prefer the hard‑coded defaults when callers omit those arguments, ignoring selectedMode / selectedModel in localStorage.

If you intend to fall back to stored settings when the caller doesn’t specify mode/model, make the parameters nullable and only override when explicitly passed:

-async function initializeGemini(profile = 'interview', language = 'en-US', mode = 'interview', model = 'gemini-2.5-flash') {
+async function initializeGemini(profile = 'interview', language = 'en-US', mode = null, model = null) {
   const apiKey = localStorage.getItem('apiKey')?.trim();
   if (apiKey) {
-    const selectedMode = mode || localStorage.getItem('selectedMode') || 'interview';
-    const selectedModel = model || localStorage.getItem('selectedModel') || 'gemini-2.5-flash';
+    const selectedMode = mode || localStorage.getItem('selectedMode') || 'interview';
+    const selectedModel = model || localStorage.getItem('selectedModel') || 'gemini-2.5-flash';
src/components/views/CustomizeView.js (1)

1129-1158: Model description text doesn’t match actual options

The Exam Assistant description says:

“uses Gemini API 2.5 Flash or 2.5 Pro …”

but the actual options are gemini-2.5-flash and gemini-3-pro-preview. Consider updating the text to match the concrete models (2.5 Flash / 3 Pro Preview) to avoid confusion.

src/utils/gemini.js (1)

1330-1393: Image/screenshot routing logic is nuanced; consider documenting mode-dependent behavior

The new send-image-content and send-screenshot-with-text handlers distinguish:

  • Interview mode + manual screenshots: wait for Live session readiness and send screenshot plus a rich text prompt.
  • Interview mode + non-manual screenshots: send raw screenshots only.
  • Coding/Exam mode: use the combined send-screenshot-with-text path that sends both media and text in one call to the coding-mode “session”.

The branching itself is sound, but the behavior is fairly subtle (e.g., why manual screenshots in interview mode add text while automated ones don’t, and why Live sends two separate messages vs a single combined one in coding mode). A short comment block or docstring outlining these invariants would make future changes safer.

Also applies to: 1413-1459

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae5ff68 and 5b324a5.

⛔ Files ignored due to path filters (3)
  • package-lock.json is excluded by !**/package-lock.json
  • src/assets/katex-0.16.9.min.js is excluded by !**/*.min.js
  • src/assets/models/500px-Google_Gemini_icon_2025.svg.png is excluded by !**/*.png
📒 Files selected for processing (48)
  • .gitignore
  • AGENTS.md
  • ARCHITECTURE.md
  • IMPLEMENTATION_COMPLETE.md
  • IMPLEMENTATION_SUMMARY.md
  • INTEGRATION_GUIDE.js
  • PRODUCTION_IMPLEMENTATION.md
  • PSEUDO_LIVE_INTEGRATION.js
  • PSEUDO_LIVE_README.md
  • QUICK_START.md
  • README.md
  • STT_COMPARISON.md
  • TESTING_GUIDE.md
  • forge.config.js
  • package.json
  • src/__tests__/geminiConversation.test.js
  • src/__tests__/languages.test.js
  • src/__tests__/prompts.test.js
  • src/__tests__/timer.test.js
  • src/__tests__/tokenTracking.test.js
  • src/__tests__/vad.test.js
  • src/assets/katex-0.16.9.min.css
  • src/components/app/AppHeader.js
  • src/components/app/CheatingDaddyApp.js
  • src/components/common/CustomDropdown.js
  • src/components/index.js
  • src/components/views/AdvancedView.js
  • src/components/views/AssistantView.js
  • src/components/views/CustomizeView.js
  • src/components/views/HelpView.js
  • src/components/views/HistoryView.js
  • src/components/views/MainView.js
  • src/config.js
  • src/index.html
  • src/index.js
  • src/preload.js
  • src/script.js
  • src/utils/gemini.js
  • src/utils/geminiSTT.js
  • src/utils/googleSpeechSTT.js
  • src/utils/prompts.js
  • src/utils/pseudoLiveOrchestrator.js
  • src/utils/renderer.js
  • src/utils/speechToText.js
  • src/utils/stealthFeatures.js
  • src/utils/vad.js
  • src/utils/window.js
  • src/utils/windowsNative.js
💤 Files with no reviewable changes (3)
  • AGENTS.md
  • src/components/index.js
  • src/components/views/HistoryView.js
🧰 Additional context used
🧬 Code graph analysis (21)
src/utils/windowsNative.js (2)
src/utils/window.js (6)
  • require (1-1)
  • require (5-5)
  • require (72-72)
  • os (4-4)
  • path (2-2)
  • fs (3-3)
src/utils/stealthFeatures.js (3)
  • require (3-3)
  • require (22-22)
  • require (47-47)
src/utils/vad.js (3)
src/utils/gemini.js (12)
  • require (1-1)
  • require (2-2)
  • require (3-3)
  • require (4-4)
  • require (5-5)
  • require (6-6)
  • require (7-7)
  • require (8-8)
  • require (1053-1053)
  • i (1228-1228)
  • i (1241-1241)
  • i (1254-1254)
src/__tests__/vad.test.js (1)
  • require (1-1)
src/utils/renderer.js (4)
  • require (2-2)
  • VADProcessor (6-6)
  • i (151-151)
  • i (163-163)
src/utils/googleSpeechSTT.js (1)
src/utils/pseudoLiveOrchestrator.js (1)
  • QUESTION_DETECTION (65-70)
src/components/views/HelpView.js (2)
src/utils/window.js (2)
  • isMac (215-215)
  • isMac (546-546)
src/utils/renderer.js (1)
  • isMacOS (52-52)
src/__tests__/prompts.test.js (1)
src/utils/prompts.js (1)
  • profilePrompts (1-507)
src/preload.js (1)
src/__mocks__/electron.js (1)
  • ipcRenderer (5-5)
src/__tests__/vad.test.js (1)
src/utils/vad.js (2)
  • VAD_CONFIG (15-25)
  • VADState (7-13)
PSEUDO_LIVE_INTEGRATION.js (2)
src/utils/windowResize.js (1)
  • window (4-4)
src/utils/gemini.js (3)
  • response (258-258)
  • result (257-257)
  • result (698-698)
src/utils/geminiSTT.js (5)
src/preload.js (1)
  • require (4-4)
src/utils/gemini.js (15)
  • require (1-1)
  • require (2-2)
  • require (3-3)
  • require (4-4)
  • require (5-5)
  • require (6-6)
  • require (7-7)
  • require (8-8)
  • require (1053-1053)
  • result (257-257)
  • result (698-698)
  • i (1228-1228)
  • i (1241-1241)
  • i (1254-1254)
  • sample (1256-1256)
src/utils/pseudoLiveOrchestrator.js (2)
  • require (45-45)
  • require (46-46)
src/utils/speechToText.js (2)
  • require (14-14)
  • STT_CONFIG (17-30)
PSEUDO_LIVE_INTEGRATION.js (6)
  • apiKey (24-24)
  • result (216-216)
  • result (235-235)
  • result (257-257)
  • result (279-279)
  • result (308-308)
src/__tests__/languages.test.js (1)
src/utils/gemini.js (1)
  • selectedLanguageName (399-399)
src/utils/speechToText.js (4)
INTEGRATION_GUIDE.js (1)
  • require (21-21)
src/utils/geminiSTT.js (2)
  • require (34-34)
  • STT_CONFIG (37-59)
src/utils/pseudoLiveOrchestrator.js (2)
  • require (45-45)
  • require (46-46)
PSEUDO_LIVE_INTEGRATION.js (1)
  • apiKey (24-24)
src/utils/window.js (1)
src/utils/windowResize.js (1)
  • result (5-5)
src/script.js (1)
src/utils/windowResize.js (1)
  • window (4-4)
src/components/views/CustomizeView.js (5)
src/utils/gemini.js (1)
  • value (151-166)
src/utils/window.js (2)
  • isMac (215-215)
  • isMac (546-546)
src/utils/renderer.js (8)
  • vadEnabled (207-207)
  • vadEnabled (223-223)
  • vadEnabled (337-337)
  • vadEnabled (435-435)
  • vadMode (208-208)
  • vadMode (224-224)
  • vadMode (340-340)
  • vadMode (438-438)
src/utils/windowResize.js (1)
  • window (4-4)
src/__mocks__/electron.js (1)
  • ipcRenderer (5-5)
src/config.js (1)
forge.config.js (1)
  • fs (16-16)
src/__tests__/tokenTracking.test.js (1)
src/utils/renderer.js (10)
  • tilesX (80-80)
  • tilesY (81-81)
  • totalTiles (82-82)
  • elapsedSeconds (95-95)
  • audioTokens (98-98)
  • oneMinuteAgo (108-108)
  • maxTokensPerMin (126-126)
  • throttleAtPercent (127-127)
  • currentTokens (129-129)
  • throttleThreshold (130-130)
src/utils/renderer.js (2)
src/utils/gemini.js (9)
  • require (1-1)
  • require (2-2)
  • require (3-3)
  • require (4-4)
  • require (5-5)
  • require (6-6)
  • require (7-7)
  • require (8-8)
  • require (1053-1053)
src/__mocks__/electron.js (1)
  • ipcRenderer (5-5)
src/components/app/CheatingDaddyApp.js (2)
src/utils/renderer.js (2)
  • selectedMode (173-173)
  • selectedMode (633-633)
src/utils/window.js (2)
  • isMac (215-215)
  • isMac (546-546)
src/components/views/AssistantView.js (2)
PSEUDO_LIVE_INTEGRATION.js (1)
  • language (31-31)
src/__mocks__/electron.js (1)
  • ipcRenderer (5-5)
src/utils/gemini.js (2)
PSEUDO_LIVE_INTEGRATION.js (6)
  • result (216-216)
  • result (235-235)
  • result (257-257)
  • result (279-279)
  • result (308-308)
  • language (31-31)
INTEGRATION_GUIDE.js (1)
  • pseudoLiveOrchestrator (28-28)
src/components/app/AppHeader.js (1)
src/utils/renderer.js (1)
  • cheddar (863-896)
🪛 Biome (2.1.2)
src/assets/katex-0.16.9.min.css

[error] 1-1: Duplicate properties can lead to unexpected behavior and may override previous declarations unintentionally.

width is already defined here.

Remove or rename the duplicate property to ensure consistent styling.

(lint/suspicious/noDuplicateProperties)

src/index.js

[error] 5-5: Illegal return statement outside of a function

(parse)

🪛 LanguageTool
IMPLEMENTATION_SUMMARY.md

[grammar] ~5-~5: Ensure spelling is correct
Context: ...terview Assistant** that achieves 400-800ms end-to-end latency and works with **A...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~108-~108: Ensure spelling is correct
Context: ...peech (recommended) - Fastest: 100-300ms - Most accurate: 90-95% - Cost: $1.44...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~112-~112: Ensure spelling is correct
Context: ...mini STT (zero-cost) - Slower: 300-500ms - Good accuracy: 85-90% - Cost: $0 (u...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~159-~159: Ensure spelling is correct
Context: ...ff:** Slightly slower (300-500ms vs 100-300ms) Verdict: Perfect for budget-consc...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~337-~337: Ensure spelling is correct
Context: ...tions:** 1. Increase silence threshold (600ms → 800ms) 2. Use manual VAD mode for ful...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~369-~369: Ensure spelling is correct
Context: ...or monitoring ✅ Optimized thresholds (600ms silence) ✅ Streaming where possible (...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

STT_COMPARISON.md

[grammar] ~198-~198: Ensure spelling is correct
Context: ...rovider | Monthly Cost | Per Interview (1hr) | |----------|--------------|---------...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~225-~225: Ensure spelling is correct
Context: ... ⭐⭐⭐⭐⭐ Why? - Best performance (100-300ms STT) - True streaming with partial resu...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~350-~350: Ensure spelling is correct
Context: ...) and the performance is excellent (400-800ms total latency). If budget is a hard co...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

PRODUCTION_IMPLEMENTATION.md

[grammar] ~45-~45: Ensure spelling is correct
Context: ...r Real-Time Performance ✅ - Target: 400-800ms end-to-end latency - Streaming STT for ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~47-~47: Ensure spelling is correct
Context: ...ive results - Optimized VAD thresholds (600ms silence) - Minimal audio buffering ###...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~67-~67: Ensure spelling is correct
Context: ...ized thresholds: - Silence threshold: 600ms (fast question detection) - Min recor...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~295-~295: Ensure spelling is correct
Context: ...1. Increase silence threshold (default: 600ms → 800ms) 2. Reduce VAD sensitivity 3. U...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~436-~436: Ensure spelling is correct
Context: ... (proper error handling) 3. ✅ Fast (400-800ms end-to-end) 4. ✅ Scalable (can handle h...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

IMPLEMENTATION_COMPLETE.md

[grammar] ~42-~42: Use a hyphen to join words.
Context: ...* - Integration examples - Copy-paste ready code - Event handlers - UI u...

(QB_NEW_EN_HYPHEN)

ARCHITECTURE.md

[grammar] ~185-~185: Ensure spelling is correct
Context: ...l-time feedback } ``` Latency: 100-300ms (streaming) Accuracy: 90-95% **...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

PSEUDO_LIVE_README.md

[grammar] ~39-~39: Ensure spelling is correct
Context: ...modes - Optimized silence detection (600ms threshold) - Pre/post-speech padding...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~81-~81: Ensure spelling is correct
Context: ...anscript when: 1. Silence detected: 600ms of silence after speech 2. **Punctuatio...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~84-~84: Ensure spelling is correct
Context: ...ffer overflow) 4. Min duration met: 200ms minimum (filters out noise) ### 3. Err...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
QUICK_START.md

73-73: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


125-125: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


135-135: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


200-200: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

IMPLEMENTATION_SUMMARY.md

56-56: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


269-269: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


276-276: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


283-283: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


290-290: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

STT_COMPARISON.md

30-30: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


39-39: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


98-98: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


107-107: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


162-162: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

TESTING_GUIDE.md

56-56: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


269-269: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


276-276: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


283-283: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


290-290: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

README.md

87-87: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


92-92: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

PRODUCTION_IMPLEMENTATION.md

9-9: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


159-159: Bare URL used

(MD034, no-bare-urls)


190-190: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

ARCHITECTURE.md

56-56: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


269-269: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


276-276: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


283-283: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


290-290: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

PSEUDO_LIVE_README.md

9-9: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


17-17: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


283-283: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

Comment on lines +86 to +89
```bash
npm install @google-cloud/speech --save
```

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the actual lines in PRODUCTION_IMPLEMENTATION.md
echo "=== Lines 86-89 of PRODUCTION_IMPLEMENTATION.md ==="
sed -n '86,89p' PRODUCTION_IMPLEMENTATION.md

echo ""
echo "=== Lines 148-156 of PRODUCTION_IMPLEMENTATION.md ==="
sed -n '148,156p' PRODUCTION_IMPLEMENTATION.md

echo ""
echo "=== Check package.json for @google-cloud/speech ==="
cat package.json | grep -i "google-cloud/speech" || echo "Not found in package.json"

echo ""
echo "=== Search codebase for @google-cloud/speech references ==="
rg "@google-cloud/speech" --type js -n || echo "No references found"

echo ""
echo "=== Search for googleSpeechSTT files ==="
fd -t f "googleSpeechSTT" || echo "No googleSpeechSTT files found"

echo ""
echo "=== Search for any speech-related imports/usage in src ==="
rg "speech" -i --type js -l | head -20

Repository: sohzm/cheating-daddy

Length of output: 1219


🏁 Script executed:

# Let's examine the googleSpeechSTT.js file to understand the context better
echo "=== Full content of src/utils/googleSpeechSTT.js ==="
cat -n src/utils/googleSpeechSTT.js

echo ""
echo "=== Check if it's actually being used/imported ==="
rg "googleSpeechSTT" --type js -B 2 -A 2 || echo "No imports found"

echo ""
echo "=== Check current dependencies in package.json ==="
cat package.json | head -50

Repository: sohzm/cheating-daddy

Length of output: 16676


Missing dependency: @google-cloud/speech is required by src/utils/googleSpeechSTT.js but not in package.json.

The documentation correctly instructs users to install @google-cloud/speech and add it to package.json, but the actual package.json is missing this dependency. Since src/utils/googleSpeechSTT.js has a direct require('@google-cloud/speech') at line 24, this must be added to the dependencies in package.json or the module will fail at runtime. Add "@google-cloud/speech": "^6.0.0" to package.json dependencies.

🤖 Prompt for AI Agents
In PRODUCTION_IMPLEMENTATION.md around lines 86 to 89: package.json is missing
the runtime dependency "@google-cloud/speech" required by
src/utils/googleSpeechSTT.js (require at line 24); add "@google-cloud/speech":
"^6.0.0" to the package.json dependencies section (or run npm install --save
@google-cloud/speech to update package.json and lockfile) and commit the updated
package.json and package-lock.json/yarn.lock so the module is present at
runtime.

Comment on lines 565 to +587
constructor() {
super();
this.responses = [];
this.currentResponseIndex = -1;
this.selectedProfile = 'interview';
this.selectedLanguage = 'en-US';
this.onSendText = () => {};
this._lastAnimatedWordCount = 0;
this.copiedFeedback = false;
// Microphone starts as OFF by default (for manual mode)
this.micEnabled = false;
// Load VAD mode from localStorage
this.vadMode = localStorage.getItem('vadMode') || 'automatic';
// Load saved responses from localStorage
try {
this.savedResponses = JSON.parse(localStorage.getItem('savedResponses') || '[]');
} catch (e) {
this.savedResponses = [];
}

// Listen for VAD mode changes
this.setupVADModeListener();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l src/components/views/AssistantView.js

Repository: sohzm/cheating-daddy

Length of output: 106


🏁 Script executed:

cat -n src/components/views/AssistantView.js | sed -n '550,620p'

Repository: sohzm/cheating-daddy

Length of output: 2946


🏁 Script executed:

cat -n src/components/views/AssistantView.js | sed -n '1200,1250p'

Repository: sohzm/cheating-daddy

Length of output: 4375


🏁 Script executed:

cat -n src/components/views/AssistantView.js | grep -n "disconnectedCallback" -A 20

Repository: sohzm/cheating-daddy

Length of output: 1271


🏁 Script executed:

cat -n src/components/views/AssistantView.js | grep -n "setupVADModeListener" -A 30

Repository: sohzm/cheating-daddy

Length of output: 1500


🏁 Script executed:

cat -n src/components/views/AssistantView.js | grep -n "addEventListener.*storage" -B 5 -A 15

Repository: sohzm/cheating-daddy

Length of output: 984


🏁 Script executed:

find . -name "*.js" -type f | xargs grep -l "localStorage.setItem.*vadMode" | head -20

Repository: sohzm/cheating-daddy

Length of output: 132


🏁 Script executed:

find . -name "*Customize*" -o -name "*Settings*" | head -20

Repository: sohzm/cheating-daddy

Length of output: 103


🏁 Script executed:

grep -r "localStorage.setItem.*vadMode" --include="*.js" -B 2 -A 2

Repository: sohzm/cheating-daddy

Length of output: 681


🏁 Script executed:

cat -n src/components/views/CustomizeView.js | grep -n "handleVADModeChange" -B 3 -A 10

Repository: sohzm/cheating-daddy

Length of output: 2127


🏁 Script executed:

grep -r "addEventListener.*storage" --include="*.js" -B 2 -A 10

Repository: sohzm/cheating-daddy

Length of output: 1132


🏁 Script executed:

cat -n src/components/views/AssistantView.js | sed -n '986,1010p'

Repository: sohzm/cheating-daddy

Length of output: 1232


🏁 Script executed:

find . -name "*.html" | xargs grep -l "AssistantView\|CustomizeView" 2>/dev/null | head -5

Repository: sohzm/cheating-daddy

Length of output: 46


Storage listener accumulates and won't sync VAD mode within same window

The setupVADModeListener() method at lines 589–605 registers an anonymous storage event listener that is never removed. Two issues:

  1. Functional bug: The storage event only fires in other browsing contexts (tabs/windows), not in the document that calls localStorage.setItem(). If CustomizeView (which calls localStorage.setItem('vadMode', ...) at line 951) and AssistantView are in the same renderer window, the mic toggle visibility at line 1212 (this.vadMode === 'manual') won't update when settings change—the listener simply won't fire.

  2. Memory leak: The listener is never removed. disconnectedCallback() (line 986) cleans up IPC listeners but has no cleanup for the storage listener. Since the handler is an anonymous function not stored on this, it cannot be removed. Repeated mounts of this component will accumulate listeners.

Fix: Store the handler on this and remove it in disconnectedCallback():

setupVADModeListener() {
    this._onStorageChange = (e) => {
        if (e.key === 'vadMode') {
            this.vadMode = e.newValue || 'automatic';
            if (this.vadMode === 'automatic') {
                this.micEnabled = true;
                if (window.cheddar?.toggleMicrophone) {
                    window.cheddar.toggleMicrophone(true);
                }
            }
            this.requestUpdate();
        }
    };
    window.addEventListener('storage', this._onStorageChange);
}

disconnectedCallback() {
    super.disconnectedCallback();
    window.removeEventListener('storage', this._onStorageChange);
    // ... existing cleanup
}

Alternatively, if CustomizeView and AssistantView share the same window, use direct method calls or custom events instead of relying on cross-window storage events.

Also applies to: 589–605, 1212–1235

🤖 Prompt for AI Agents
In src/components/views/AssistantView.js around lines 565 to 587 and related
methods at 589–605 and 1212–1235, the storage event listener is registered as an
anonymous function and never removed, causing memory leaks and also failing to
update vadMode in the same window (storage events only fire across windows). Fix
by assigning the listener to a property (e.g., this._onStorageChange) in
setupVADModeListener so it can be removed later, remove it in
disconnectedCallback via window.removeEventListener('storage',
this._onStorageChange), and ensure same-window updates are handled either by
dispatching/handling a custom event or calling the update method directly from
CustomizeView when vadMode changes instead of relying solely on the storage
event.

Comment on lines +1 to +7
// Handle Squirrel events manually to prevent desktop shortcuts
if (require('electron').app) {
const squirrelCommand = process.argv[1];
if (handleSquirrelEvent(squirrelCommand)) {
return;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Illegal return statement at module level causes syntax error.

The static analysis correctly identifies that line 5 contains an illegal return statement. In Node.js/CommonJS modules, return at the top level is only valid inside a function. Additionally, handleSquirrelEvent is called on line 4 before it's defined on line 9.

🔎 Proposed fix: Wrap in an IIFE or restructure
-// Handle Squirrel events manually to prevent desktop shortcuts
-if (require('electron').app) {
-    const squirrelCommand = process.argv[1];
-    if (handleSquirrelEvent(squirrelCommand)) {
-        return;
-    }
-}
-
-function handleSquirrelEvent(squirrelCommand) {
+// Handle Squirrel events manually to prevent desktop shortcuts
+function handleSquirrelEvent(squirrelCommand) {
     const app = require('electron').app;
     // ... rest of function
 }
+
+// Check for Squirrel events before app initialization
+if (require('electron').app) {
+    const squirrelCommand = process.argv[1];
+    if (handleSquirrelEvent(squirrelCommand)) {
+        // Use process.exit() instead of return at module level
+        process.exit(0);
+    }
+}

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Biome (2.1.2)

[error] 5-5: Illegal return statement outside of a function

(parse)

🤖 Prompt for AI Agents
In src/index.js around lines 1 to 7, there is an illegal top-level "return" and
a call to handleSquirrelEvent before its definition; to fix, either wrap the
Squirrel-handling block in an immediately-invoked async function (IIFE) so
returns are valid, or replace the top-level return with a proper early-exit
(e.g., process.exit(0) or app.quit()) and ensure handleSquirrelEvent is defined
(move its declaration above this block or make it a hoisted function
declaration) so the call happens after the function exists.

Comment on lines +263 to +289
// VAD (Voice Activity Detection) handler
ipcMain.handle('send-vad-audio-segment', async (event, audioSegment) => {
try {
// Forward VAD-processed audio segment to Gemini or audio processing
// This handler bridges VAD output to existing audio processing pipeline
console.log('Received VAD audio segment:', audioSegment ? 'Valid segment' : 'Invalid segment');

// You can add additional processing here if needed
// For now, this just acknowledges receipt of the VAD segment
return { success: true };
} catch (error) {
console.error('Error processing VAD audio segment:', error);
return { success: false, error: error.message };
}
});

// VAD settings update handler
ipcMain.handle('update-vad-setting', async (event, vadEnabled) => {
try {
console.log('VAD setting updated:', vadEnabled ? 'enabled' : 'disabled');
// Store VAD setting if needed for main process
return { success: true };
} catch (error) {
console.error('Error updating VAD setting:', error);
return { success: false, error: error.message };
}
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for VAD handler references and VAD-related implementations
rg -n "send-vad-audio-segment|update-vad-setting" --type js -g '!node_modules'

Repository: sohzm/cheating-daddy

Length of output: 536


🏁 Script executed:

#!/bin/bash
# Search for VAD processor or orchestrator
rg -n "VAD|vad|orchestrator|Orchestrator" --type js -g '!node_modules' | head -50

Repository: sohzm/cheating-daddy

Length of output: 4286


🏁 Script executed:

#!/bin/bash
# Search for audio processing related code
rg -n "audio.*process|process.*audio|audio.*segment" --type js -g '!node_modules' | head -30

Repository: sohzm/cheating-daddy

Length of output: 2927


VAD IPC handlers lack integration with the actual orchestrator.

The send-vad-audio-segment and update-vad-setting handlers in src/index.js only log and return success without forwarding to the processing pipeline. The pseudoLiveOrchestrator contains the actual VAD handling logic (handleAudioSegmentFromVAD) which routes segments to the STT service, but these handlers do not invoke it. Wire the handlers to forward audio segments and VAD settings to the orchestrator instance.

🤖 Prompt for AI Agents
In src/index.js around lines 263 to 289, the two VAD ipc handlers only log and
return success but must forward data into the existing orchestrator; call the
orchestrator's handler for segments
(pseudoLiveOrchestrator.handleAudioSegmentFromVAD(audioSegment)) inside
send-vad-audio-segment and propagate any returned result/errors back to the
renderer, and call the orchestrator's VAD setting updater (e.g.,
pseudoLiveOrchestrator.setVADEnabled(vadEnabled) or
pseudoLiveOrchestrator.updateVADSetting(vadEnabled)) inside update-vad-setting
so the main process actually applies the setting; ensure you check
pseudoLiveOrchestrator exists before calling and return its success/failure info
instead of just a static success.

Comment on lines +162 to +168
off: (channel, callback) => {
if (callback) {
ipcRenderer.removeListener(channel, callback);
} else {
ipcRenderer.removeAllListeners(channel);
}
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The off method cannot properly remove listeners registered via on.

The on method wraps the callback in (_, data) => callback(data), but off tries to remove the original callback. Since the wrapped function is a different reference, removeListener won't find it.

🔎 Proposed fix: Track wrapped callbacks
+// Store wrapped callbacks for proper removal
+const callbackMap = new WeakMap();
+
 window.api = {
     // ...
     on: (channel, callback) => {
         const validChannels = [/* ... */];
         
         if (validChannels.includes(channel)) {
-            ipcRenderer.on(channel, (_, data) => callback(data));
+            const wrappedCallback = (_, data) => callback(data);
+            callbackMap.set(callback, wrappedCallback);
+            ipcRenderer.on(channel, wrappedCallback);
         } else {
             console.warn(`[preload] Invalid event channel: ${channel}`);
         }
     },
     
     off: (channel, callback) => {
         if (callback) {
-            ipcRenderer.removeListener(channel, callback);
+            const wrappedCallback = callbackMap.get(callback);
+            if (wrappedCallback) {
+                ipcRenderer.removeListener(channel, wrappedCallback);
+                callbackMap.delete(callback);
+            }
         } else {
             ipcRenderer.removeAllListeners(channel);
         }
     },

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/preload.js around lines 162-168, the off method cannot remove listeners
added via on because on wraps the original callback in a new function; modify
the implementation to store the wrapped function reference (e.g., a Map keyed by
channel and original callback -> wrappedCallback) when registering listeners in
on, use that stored wrappedCallback when calling ipcRenderer.removeListener in
off, ensure removeAllListeners also clears any stored entries for that channel,
and clean up map entries when listeners are removed to avoid memory leaks.

Comment on lines +68 to +88
async initialize() {
try {
console.log('🔧 [STT] Initializing authentication...');

// Initialize Google Auth with API key
this.authClient = new GoogleAuth({
credentials: {
client_email: 'speech-to-text@gemini-assistant.iam.gserviceaccount.com',
private_key: this.apiKey, // Using API key as credential
},
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
});

this.isInitialized = true;
console.log('✅ [STT] Service initialized successfully');
return true;
} catch (error) {
console.error('❌ [STT] Initialization failed:', error);
throw new Error(`STT initialization failed: ${error.message}`);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Incorrect authentication: API key cannot be used as a service account private key.

The GoogleAuth library expects actual service account credentials (a PEM-encoded private key and client email). Passing a Gemini API key as private_key will fail authentication. The hardcoded client_email is also invalid.

For API key-based authentication with Google Cloud Speech-to-Text, you need to either:

  1. Use REST API directly with ?key=API_KEY query parameter
  2. Use proper service account credentials (JSON key file)
🔎 Suggested approach for REST API with API key
-    async initialize() {
-        try {
-            console.log('🔧 [STT] Initializing authentication...');
-            
-            // Initialize Google Auth with API key
-            this.authClient = new GoogleAuth({
-                credentials: {
-                    client_email: 'speech-to-text@gemini-assistant.iam.gserviceaccount.com',
-                    private_key: this.apiKey, // Using API key as credential
-                },
-                scopes: ['https://www.googleapis.com/auth/cloud-platform'],
-            });
-            
-            this.isInitialized = true;
-            console.log('✅ [STT] Service initialized successfully');
-            return true;
-        } catch (error) {
-            console.error('❌ [STT] Initialization failed:', error);
-            throw new Error(`STT initialization failed: ${error.message}`);
-        }
-    }
+    async initialize() {
+        try {
+            console.log('🔧 [STT] Initializing authentication...');
+            
+            // Store API key for REST API calls
+            // Note: For production, use @google-cloud/speech with service account
+            if (!this.apiKey) {
+                throw new Error('API key is required');
+            }
+            
+            this.isInitialized = true;
+            console.log('✅ [STT] Service initialized successfully');
+            return true;
+        } catch (error) {
+            console.error('❌ [STT] Initialization failed:', error);
+            throw new Error(`STT initialization failed: ${error.message}`);
+        }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async initialize() {
try {
console.log('🔧 [STT] Initializing authentication...');
// Initialize Google Auth with API key
this.authClient = new GoogleAuth({
credentials: {
client_email: 'speech-to-text@gemini-assistant.iam.gserviceaccount.com',
private_key: this.apiKey, // Using API key as credential
},
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
});
this.isInitialized = true;
console.log('✅ [STT] Service initialized successfully');
return true;
} catch (error) {
console.error('❌ [STT] Initialization failed:', error);
throw new Error(`STT initialization failed: ${error.message}`);
}
}
async initialize() {
try {
console.log('🔧 [STT] Initializing authentication...');
// Store API key for REST API calls
// Note: For production, use @google-cloud/speech with service account
if (!this.apiKey) {
throw new Error('API key is required');
}
this.isInitialized = true;
console.log('✅ [STT] Service initialized successfully');
return true;
} catch (error) {
console.error('❌ [STT] Initialization failed:', error);
throw new Error(`STT initialization failed: ${error.message}`);
}
}
🤖 Prompt for AI Agents
In src/utils/speechToText.js around lines 68-88, the code incorrectly uses a
Gemini API key as a service-account private_key and hardcodes a client_email;
replace this by either 1) using proper service account JSON credentials: load
the service account JSON (from an env path or env var), parse client_email and
private_key and pass those credentials into new GoogleAuth({credentials: {...},
scopes: [...]}) OR 2) switch to API-key-based REST calls: remove GoogleAuth
usage and call the Speech-to-Text REST endpoint with the API key appended as
?key=API_KEY (or include the key from env), handling tokenless requests
accordingly; ensure no hardcoded client_email remains and that isInitialized is
only set true after successful auth/setup.

Comment on lines +318 to +321
updateLanguage(languageCode) {
console.log(`[STT] Updating language to: ${languageCode}`);
STT_CONFIG.languageCode = languageCode;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Mutating module-level constant creates shared state issues.

updateLanguage mutates the shared STT_CONFIG object, which affects all instances of SpeechToTextService. Store language as an instance property instead.

🔎 Proposed fix
+    constructor(apiKey, onTranscriptComplete, onTranscriptPartial = null) {
+        this.apiKey = apiKey;
+        this.onTranscriptComplete = onTranscriptComplete;
+        this.onTranscriptPartial = onTranscriptPartial;
+        
+        // Instance-level language setting
+        this.languageCode = STT_CONFIG.languageCode;
         // ... rest of constructor
     
     updateLanguage(languageCode) {
         console.log(`[STT] Updating language to: ${languageCode}`);
-        STT_CONFIG.languageCode = languageCode;
+        this.languageCode = languageCode;
     }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/utils/speechToText.js around lines 318-321, updateLanguage currently
mutates the module-level STT_CONFIG.languageCode causing shared state across
instances; change the service to store languageCode as an instance property
(e.g., this.languageCode set in the constructor with a default or passed-in
value), change updateLanguage to set this.languageCode instead of STT_CONFIG,
and refactor any internal methods that read STT_CONFIG.languageCode to read
this.languageCode so no module-level constant is mutated and each
SpeechToTextService instance maintains its own language state.

Comment on lines +130 to +144
// AUTOMATIC MODE: Stream audio continuously without VAD processing
// Since NonRealTimeVAD doesn't support streaming, we send audio directly in chunks
if (this.mode === 'automatic' && this.state === VADState.LISTENING) {
// Send audio chunk directly via the onCommit callback
// This ensures continuous streaming like before VAD was added
if (this.onCommit) {
this.onCommit(audioFrame, {
duration: 0,
frameCount: 1,
timestamp: Date.now(),
streaming: true // Flag to indicate streaming mode
});
}
return;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "streaming" --type js src/

Repository: sohzm/cheating-daddy

Length of output: 4031


🏁 Script executed:

# Also search for orchestrator and STT implementations
fd -e js src/ | xargs grep -l "orchestrator\|stt" 2>/dev/null | head -20

Repository: sohzm/cheating-daddy

Length of output: 385


🏁 Script executed:

# Search for where this VAD module is imported and used
rg -n "from.*vad\|import.*vad\|require.*vad" --type js src/

Repository: sohzm/cheating-daddy

Length of output: 46


🏁 Script executed:

sed -n '370,410p' src/utils/pseudoLiveOrchestrator.js

Repository: sohzm/cheating-daddy

Length of output: 1659


🏁 Script executed:

# Get broader context of the handleCommit/onCommit function
sed -n '350,420p' src/utils/pseudoLiveOrchestrator.js

Repository: sohzm/cheating-daddy

Length of output: 2662


🏁 Script executed:

# Check STT implementation to see if processAudioChunk accepts metadata
rg -n "processAudioChunk" --type js src/ -A 5

Repository: sohzm/cheating-daddy

Length of output: 2034


🏁 Script executed:

# Check the actual onCommit callback setup in orchestrator
rg -n "onCommit" --type js src/utils/vad.js -B 3 -A 3

Repository: sohzm/cheating-daddy

Length of output: 1421


Streaming flag is set but lost before reaching STT service.

In automatic mode with LISTENING state, audio frames are immediately committed with streaming: true flag. However, the orchestrator receives this flag but doesn't pass it to the STT service—both streaming and non-streaming audio go through the same processAudioChunk(audioSegment) call without metadata. As a result, STT implementations have no way to differentiate between streaming frames and complete segments, defeating the intent of the flag.

To fix: Either pass metadata to processAudioChunk() so STT can handle streaming frames differently, or use the flag in the orchestrator to apply different processing logic before sending to STT.

🤖 Prompt for AI Agents
In src/utils/vad.js around lines 130 to 144, the committed audioFrame is marked
with streaming: true but that flag is not forwarded to the STT path; update the
call flow so the streaming metadata is preserved: either change the
onCommit/onOrchestrator contract to include the metadata and forward it into
processAudioChunk(audioSegment, { streaming: true }) (or similar) so STT
implementations can detect streaming frames, or detect streaming in the
orchestrator when receiving commits and branch to a separate processing path
(e.g., accumulate vs immediate streaming) before calling processAudioChunk;
ensure the metadata key name matches existing interfaces and update any callers
to accept the extra param.

Comment on lines +336 to +348
// Clear buffers
this.audioBuffer = [];
this.silenceStartTime = 0;
this.consecutiveSilenceFrames = 0;
this.consecutiveSpeechFrames = 0;

// Send the audio segment via callback
if (this.onCommit) {
this.onCommit(completeAudioSegment, {
duration: recordingDuration,
frameCount: this.audioBuffer.length,
timestamp: Date.now()
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bug: frameCount in metadata is always 0.

this.audioBuffer is cleared on line 337 before frameCount is read on line 346. The metadata will always show frameCount: 0.

🔎 Proposed fix
+        // Capture frame count before clearing
+        const frameCount = this.audioBuffer.length;
+
         // Clear buffers
         this.audioBuffer = [];
         this.silenceStartTime = 0;
         this.consecutiveSilenceFrames = 0;
         this.consecutiveSpeechFrames = 0;

         // Send the audio segment via callback
         if (this.onCommit) {
             this.onCommit(completeAudioSegment, {
                 duration: recordingDuration,
-                frameCount: this.audioBuffer.length,
+                frameCount: frameCount,
                 timestamp: Date.now()
             });
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Clear buffers
this.audioBuffer = [];
this.silenceStartTime = 0;
this.consecutiveSilenceFrames = 0;
this.consecutiveSpeechFrames = 0;
// Send the audio segment via callback
if (this.onCommit) {
this.onCommit(completeAudioSegment, {
duration: recordingDuration,
frameCount: this.audioBuffer.length,
timestamp: Date.now()
});
// Capture frame count before clearing
const frameCount = this.audioBuffer.length;
// Clear buffers
this.audioBuffer = [];
this.silenceStartTime = 0;
this.consecutiveSilenceFrames = 0;
this.consecutiveSpeechFrames = 0;
// Send the audio segment via callback
if (this.onCommit) {
this.onCommit(completeAudioSegment, {
duration: recordingDuration,
frameCount: frameCount,
timestamp: Date.now()
});
🤖 Prompt for AI Agents
In src/utils/vad.js around lines 336-348, the code clears this.audioBuffer
before building the metadata so frameCount is always 0; compute and store
frameCount (e.g., const frameCount = this.audioBuffer.length or use
completeAudioSegment.length) and any other metadata values (duration/timestamp)
before resetting/clearing this.audioBuffer, then call this.onCommit with the
saved frameCount in the metadata and only after that clear the buffers.

Comment on lines +423 to +427
// Public method to update configuration
updateConfig(newConfig) {
Object.assign(VAD_CONFIG, newConfig);
console.log('VAD configuration updated:', VAD_CONFIG);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Mutating global VAD_CONFIG affects all instances.

updateConfig directly mutates the module-level VAD_CONFIG object, which means changes will affect all VADProcessor instances and persist across the application lifecycle.

🔎 Proposed fix

Store instance-specific config instead:

 constructor(onCommit, onStateChange = null, mode = 'automatic') {
     // ...
+    this.config = { ...VAD_CONFIG }; // Instance-specific copy
     // ...
 }

 // Public method to update configuration
 updateConfig(newConfig) {
-    Object.assign(VAD_CONFIG, newConfig);
-    console.log('VAD configuration updated:', VAD_CONFIG);
+    Object.assign(this.config, newConfig);
+    console.log('VAD configuration updated:', this.config);
 }

Then update all references from VAD_CONFIG to this.config within instance methods.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/utils/vad.js around lines 423 to 427, updateConfig currently mutates the
module-level VAD_CONFIG which affects all VADProcessor instances; change to set
an instance property (e.g., this.config = Object.assign({}, VAD_CONFIG,
newConfig) or merge into an existing this.config) and remove direct writes to
the module-level VAD_CONFIG, then update all instance methods to read from
this.config instead of VAD_CONFIG so configuration is instance-scoped and
non-global.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (4)
src/utils/renderer.js (1)

391-396: [DUPLICATE] Audio gating breaks non-VAD mode (Linux/Windows)

The past review comment remains valid. Lines 391-396 and 489-494 gate audio processing on microphoneEnabled alone, but microphoneEnabled is only set when VAD is active. When VAD is disabled or fails to load, microphoneEnabled stays false, blocking all audio processing—even for the original non-VAD path.

The condition should be:

if (isVADEnabled && !microphoneEnabled)

so that audio is only gated when VAD is actually in use.

Also applies to: 489-494

src/preload.js (1)

139-181: [DUPLICATE] off method cannot remove listeners registered via on

The past review comment remains valid. Line 164 wraps the callback in a new function (_, data) => callback(data), but line 177 tries to remove the original callback. Since the wrapper is a different reference, removeListener won't find it, and listeners leak.

The suggested fix (using a WeakMap to track wrapped callbacks) is correct and necessary to prevent memory leaks.

src/utils/gemini.js (1)

537-579: [DUPLICATE] geminiSessionRef undefined in error/close handlers; lastSessionParams cleared before use

The past review comment remains valid:

  1. Undefined geminiSessionRef: Lines 558 and 626 reference geminiSessionRef, which is not in scope within initializeGeminiSession. This will throw a ReferenceError. Use global.geminiSessionRef instead (set at line 1310), and guard against it being undefined.

  2. lastSessionParams cleared prematurely: Lines 551-552 (onerror) set lastSessionParams = null and then check if (lastSessionParams) at line 564, which will always be false. Similarly, lines 619-620 (onclose) clear it before using it. Capture the params in a local variable first (as correctly done at line 616 in onclose), then clear the global.

🔎 Proposed fix
 // In onerror (lines 551-564):
+const sessionParams = lastSessionParams ? { ...lastSessionParams } : null;
 lastSessionParams = null;
 reconnectionAttempts = maxReconnectionAttempts;
 
 usePseudoLive = true;
-if (!pseudoLiveOrchestrator) {
+if (!pseudoLiveOrchestrator && global.geminiSessionRef) {
     pseudoLiveOrchestrator = new PseudoLiveOrchestrator(
-        geminiSessionRef,
+        global.geminiSessionRef,
         sendToRenderer
     );
 }
 
-if (lastSessionParams) {
+if (sessionParams && sessionParams.apiKey) {
     pseudoLiveOrchestrator.initialize(
-        lastSessionParams.apiKey,
+        sessionParams.apiKey,
         'automatic',
-        lastSessionParams.language
+        sessionParams.language || 'en-US'
     // ...

Apply the same pattern in onclose (lines 595-641).

Also applies to: 595-641

src/components/views/AssistantView.js (1)

619-635: Storage listener accumulates and is never removed.

This issue was flagged in a previous review. The anonymous storage event listener in setupVADModeListener is never removed in disconnectedCallback, causing memory leaks on repeated component mounts. Additionally, the storage event only fires in other browsing contexts, so same-window changes from CustomizeView won't trigger this listener.

🧹 Nitpick comments (9)
src/utils/renderer.js (2)

169-183: Parameters overridden by localStorage lookups

Lines 173-174 unconditionally read mode and model from localStorage, overriding the function arguments. The ternary mode || localStorage.getItem(...) will use localStorage whenever mode is falsy (including undefined), but the parameters have default values in the signature, so callers passing explicit values will be ignored if they pass undefined.

Consider:

-const selectedMode = mode || localStorage.getItem('selectedMode') || 'interview';
-const selectedModel = model || localStorage.getItem('selectedModel') || 'gemini-2.5-flash';
+const selectedMode = mode ?? (localStorage.getItem('selectedMode') || 'interview');
+const selectedModel = model ?? (localStorage.getItem('selectedModel') || 'gemini-2.5-flash');

Using ?? (nullish coalescing) ensures only null/undefined trigger the fallback, preserving explicit empty strings if that's ever needed.


847-856: Direct access to vadProcessor.audioBuffer couples to internal state

Line 849 checks vadProcessor.audioBuffer to decide whether to commit. This tightly couples the toggle logic to VADProcessor's internal implementation. If VADProcessor refactors its buffer management, this breaks.

Consider adding a method to VADProcessor (e.g., hasRecordedAudio()) to encapsulate the check, or rely on commit() to be a no-op when there's no audio.

src/utils/ollama.js (1)

11-21: Consider adding timeouts to fetch calls

The fetch calls at lines 13, 30, and 81 lack timeout configuration. If Ollama is unresponsive, these can hang indefinitely, blocking the renderer or degrading UX.

Consider wrapping fetch with AbortSignal.timeout() (Node 18+) or a custom timeout promise:

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout

try {
    const response = await fetch(url, { signal: controller.signal });
    // ...
} finally {
    clearTimeout(timeoutId);
}

This ensures calls fail fast if Ollama is down or slow.

Also applies to: 28-46, 77-110

src/preload.js (1)

141-161: Consider centralizing the channel whitelist

Lines 141-161 hardcode the valid channels in the preload script. If new channels are added in other modules (e.g., gemini.js, pseudoLiveOrchestrator.js), they must be manually added here, risking missed updates.

Consider:

  • Exporting the channel list from a shared module (e.g., src/constants/ipcChannels.js) and importing it here and in other modules, or
  • Documenting the list's location prominently so maintainers remember to update it.

This reduces the chance of valid channels being blocked due to stale whitelists.

src/utils/gemini.js (1)

251-274: Timeout promise doesn't abort underlying request

Lines 259-263 use Promise.race to implement a 3-second timeout. If the timeout wins, the underlying generateContent call continues running, potentially wasting API quota and resources.

Consider using AbortController to cancel the request when the timeout fires:

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);

try {
    const result = await testModel.generateContent('test', { signal: controller.signal });
    // ...
} finally {
    clearTimeout(timeoutId);
}

Note: Check if the Gemini SDK supports AbortSignal. If not, the current approach is acceptable, but document that timed-out requests may still consume quota.

src/components/views/CustomizeView.js (3)

1129-1149: Simplify empty conditional block.

Lines 1139-1141 contain an empty block with just a comment. This can be simplified by using a negative condition check.

🔎 Proposed simplification
     async handleModeChange(e) {
         this.selectedMode = e.target.value;
         localStorage.setItem('selectedMode', this.selectedMode);

         // In interview mode, always use live API
         // In coding mode, user can choose between flash and pro
         if (this.selectedMode === 'interview') {
             this.selectedModel = 'gemini-2.5-flash';
-        } else {
-            // Keep current model selection for coding mode
-            if (this.selectedModel === 'gemini-2.5-flash' || this.selectedModel === 'gemini-3-pro-preview') {
-                // Keep the selection
-            } else {
-                // Default to pro for coding mode
-                this.selectedModel = 'gemini-3-pro-preview';
-            }
+        } else if (this.selectedModel !== 'gemini-2.5-flash' && this.selectedModel !== 'gemini-3-pro-preview') {
+            // Default to pro for coding mode if current model is not a valid option
+            this.selectedModel = 'gemini-3-pro-preview';
         }
         localStorage.setItem('selectedModel', this.selectedModel);

         this.requestUpdate();
     }

1589-1596: Inconsistent dropdown usage.

The Chat Provider section uses a native <select> element while all other dropdowns in this file use the <custom-dropdown> component. Consider using <custom-dropdown> for consistency.

🔎 Proposed fix
-                            <select
-                                class="form-control"
-                                .value=${this.chatProvider}
-                                @change=${this.handleChatProviderSelect}
-                            >
-                                <option value="gemini">Gemini (Default)</option>
-                                <option value="ollama">Ollama (Local)</option>
-                            </select>
+                            <custom-dropdown
+                                .value=${this.chatProvider}
+                                .options=${[
+                                    { value: 'gemini', label: 'Gemini (Default)' },
+                                    { value: 'ollama', label: 'Ollama (Local)' }
+                                ]}
+                                @change=${e => this.handleChatProviderSelect({ target: { value: e.detail.value } })}
+                            ></custom-dropdown>

1333-1338: Alert is disruptive; consider a non-blocking notification.

Using alert() blocks the UI and is jarring for users. A toast notification or inline message would provide a better user experience.

src/components/views/AssistantView.js (1)

685-745: Consider extracting localized greetings to a separate module.

The getLocalizedGreeting method contains a large object with 30+ translations, making this component harder to maintain. Consider extracting to a separate localization.js module.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b324a5 and b03d2a7.

📒 Files selected for processing (14)
  • DELIVERABLES.md
  • IMPLEMENTATION_SUMMARY.md
  • OLLAMA_INTEGRATION_COMPLETE.md
  • OLLAMA_INTEGRATION_COMPLETE_FINAL.md
  • QUICKSTART.md
  • src/components/app/CheatingDaddyApp.js
  • src/components/views/AssistantView.js
  • src/components/views/CustomizeView.js
  • src/components/views/MainView.js
  • src/preload.js
  • src/utils/gemini.js
  • src/utils/ollama.js
  • src/utils/renderer.js
  • verify-ollama-integration.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • IMPLEMENTATION_SUMMARY.md
  • src/components/views/MainView.js
🧰 Additional context used
🧬 Code graph analysis (7)
verify-ollama-integration.js (1)
src/utils/gemini.js (1)
  • path (1060-1060)
src/utils/ollama.js (1)
src/utils/gemini.js (1)
  • response (264-264)
src/utils/gemini.js (3)
PSEUDO_LIVE_INTEGRATION.js (9)
  • result (216-216)
  • result (235-235)
  • result (257-257)
  • result (279-279)
  • result (308-308)
  • language (31-31)
  • status (284-284)
  • status (461-461)
  • status (495-495)
INTEGRATION_GUIDE.js (1)
  • pseudoLiveOrchestrator (28-28)
src/utils/ollama.js (5)
  • response (13-15)
  • response (30-32)
  • response (81-95)
  • data (38-38)
  • data (101-101)
src/utils/renderer.js (2)
src/utils/gemini.js (9)
  • require (1-1)
  • require (2-2)
  • require (3-3)
  • require (4-4)
  • require (5-5)
  • require (6-6)
  • require (7-7)
  • require (8-8)
  • require (1059-1059)
src/__mocks__/electron.js (1)
  • ipcRenderer (5-5)
src/components/views/CustomizeView.js (4)
src/utils/gemini.js (3)
  • value (157-172)
  • result (263-263)
  • result (704-704)
src/utils/window.js (2)
  • isMac (215-215)
  • isMac (546-546)
src/utils/windowResize.js (2)
  • result (5-5)
  • window (4-4)
src/__mocks__/electron.js (1)
  • ipcRenderer (5-5)
src/components/app/CheatingDaddyApp.js (2)
src/utils/renderer.js (2)
  • selectedMode (173-173)
  • selectedMode (633-633)
src/utils/window.js (2)
  • isMac (215-215)
  • isMac (546-546)
src/preload.js (3)
src/utils/renderer.js (1)
  • require (2-2)
src/utils/windowResize.js (1)
  • window (4-4)
src/__mocks__/electron.js (1)
  • ipcRenderer (5-5)
🪛 LanguageTool
QUICKSTART.md

[style] ~228-~228: Using many exclamation marks might seem excessive (in this case: 5 exclamation marks for a text that’s 2474 characters long)
Context: ... 🎉 Enjoy your new Ollama integration! 🎉

(EN_EXCESSIVE_EXCLAMATION)

OLLAMA_INTEGRATION_COMPLETE.md

[style] ~380-~380: Some style guides suggest that commas should set off the year in a month-day-year date.
Context: ... Implementation Date: December 27, 2024 Status: ✅ COMPLETE & PRODUCTION-R...

(MISSING_COMMA_AFTER_YEAR)

🪛 markdownlint-cli2 (0.18.1)
OLLAMA_INTEGRATION_COMPLETE_FINAL.md

245-245: Bare URL used

(MD034, no-bare-urls)


316-316: Bare URL used

(MD034, no-bare-urls)

DELIVERABLES.md

145-145: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


185-185: Bare URL used

(MD034, no-bare-urls)


338-338: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (8)
src/components/app/CheatingDaddyApp.js (2)

163-166: LGTM!

Global keyboard listener is properly bound in connectedCallback and correctly removed in disconnectedCallback, preventing memory leaks.

Also applies to: 177-181


287-320: LGTM!

The mode and model selection logic correctly derives the mode from the profile, persists to localStorage, and initializes Gemini with appropriate parameters. The hardcoded model name for interview mode (gemini-2.0-flash-exp) is intentional based on the PR description.

OLLAMA_INTEGRATION_COMPLETE.md (1)

1-382: Documentation is comprehensive and well-organized.

The document thoroughly covers the Ollama integration including implementation scope, user workflow, technical details, and troubleshooting. This will be helpful for maintainers.

One minor style note: Consider adding a comma after the year in the date on line 380 per some style guides ("December 27, 2024,").

src/components/views/CustomizeView.js (2)

654-670: LGTM!

The per-profile prompt storage using customPrompt_${profile} keys is a good approach that maintains backward compatibility with the legacy customPrompt key while enabling per-profile customization.


1018-1050: LGTM!

VAD settings handlers correctly persist to localStorage and notify the main process via IPC. Error handling is in place for the IPC calls.

src/components/views/AssistantView.js (3)

796-897: LGTM!

The LaTeX rendering implementation correctly:

  • Checks for KaTeX availability before processing
  • Skips text nodes inside code blocks to avoid breaking code
  • Handles both display ($$...$$) and inline ($...$) math modes
  • Uses proper error handling with fallbacks

1108-1135: LGTM!

The extractCodeBlocks method has sensible logic for extracting code from markdown responses. The length threshold of 10 characters for inline code blocks is a reasonable heuristic to avoid copying single-word code spans.


143-326: LGTM!

Comprehensive VS Code-style syntax highlighting CSS with proper specificity using !important to override highlight.js defaults. The language-specific enhancements for JavaScript, Python, HTML, CSS, SQL, and JSON are well-organized.

Comment on lines +334 to +364
async handleStartOllamaChat() {
try {
// Set chat provider to Ollama
if (window.api && window.api.setChatProvider) {
const result = await window.api.setChatProvider('ollama');
if (!result.success) {
this.setStatus(`Error: ${result.error || 'Failed to connect to Ollama'}`);
return;
}
}

// Set in localStorage
localStorage.setItem('chatProvider', 'ollama');

// Initialize responses and state for Ollama chat session
this.responses = [];
this.currentResponseIndex = -1;
this.startTime = Date.now();

// Set status to indicate Ollama mode
this.setStatus('Ready - Ollama Chat');

// Open AssistantView directly (no Gemini initialization needed)
this.currentView = 'assistant';

console.log('✅ Started Ollama chat session');
} catch (error) {
console.error('Error starting Ollama chat:', error);
this.setStatus(`Error: ${error.message || 'Failed to start Ollama chat'}`);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing error boundary for window.api availability.

The method accesses window.api.setChatProvider but the entire Ollama flow depends on window.api being defined. If the preload script fails to load or api is undefined for any reason, calling window.api.setChatProvider at line 338 will throw before the null check.

🔎 Proposed fix
     async handleStartOllamaChat() {
         try {
+            if (!window.api) {
+                this.setStatus('Error: API not available');
+                return;
+            }
+            
             // Set chat provider to Ollama
-            if (window.api && window.api.setChatProvider) {
+            if (window.api.setChatProvider) {
                 const result = await window.api.setChatProvider('ollama');

Comment on lines +366 to +388
handleClearAndRestart() {
// Clear the current session and responses
this.responses = [];
this.currentResponseIndex = -1;
this.startTime = null;

// Stop any ongoing capture if in assistant view
if (this.currentView === 'assistant' && window.cheddar) {
window.cheddar.stopCapture();
}

// Return to main view
this.currentView = 'main';
this.setStatus('Session cleared. Starting new session...');

// Request update to refresh the UI
this.requestUpdate();

// Automatically start a new session after a brief delay
setTimeout(() => {
this.handleStart();
}, 100);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix indentation for handleClearAndRestart method.

The method declaration has extra leading spaces causing inconsistent indentation with other methods in the class.

🔎 Proposed fix
-        handleClearAndRestart() {
+    handleClearAndRestart() {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
handleClearAndRestart() {
// Clear the current session and responses
this.responses = [];
this.currentResponseIndex = -1;
this.startTime = null;
// Stop any ongoing capture if in assistant view
if (this.currentView === 'assistant' && window.cheddar) {
window.cheddar.stopCapture();
}
// Return to main view
this.currentView = 'main';
this.setStatus('Session cleared. Starting new session...');
// Request update to refresh the UI
this.requestUpdate();
// Automatically start a new session after a brief delay
setTimeout(() => {
this.handleStart();
}, 100);
}
handleClearAndRestart() {
// Clear the current session and responses
this.responses = [];
this.currentResponseIndex = -1;
this.startTime = null;
// Stop any ongoing capture if in assistant view
if (this.currentView === 'assistant' && window.cheddar) {
window.cheddar.stopCapture();
}
// Return to main view
this.currentView = 'main';
this.setStatus('Session cleared. Starting new session...');
// Request update to refresh the UI
this.requestUpdate();
// Automatically start a new session after a brief delay
setTimeout(() => {
this.handleStart();
}, 100);
}
🤖 Prompt for AI Agents
In src/components/app/CheatingDaddyApp.js around lines 366 to 388, the
handleClearAndRestart() method has extra leading spaces causing inconsistent
indentation with other class methods; remove the extra leading spaces so the
method declaration and its body align with the class's method indentation,
ensure brackets and inner lines are indented one level consistently, and run the
project's formatter/linter to enforce the same style across the file.

Comment on lines +637 to +654
setupChatProviderListener() {
// Listen for localStorage changes for chat provider (from other windows/tabs)
window.addEventListener('storage', (e) => {
if (e.key === 'chatProvider') {
this.chatProvider = e.newValue || 'gemini';
this.requestUpdate();
}
});

// Also check periodically to catch direct localStorage changes in same window
this._providerCheckInterval = setInterval(() => {
const currentProvider = localStorage.getItem('chatProvider') || 'gemini';
if (currentProvider !== this.chatProvider) {
this.chatProvider = currentProvider;
this.requestUpdate();
}
}, 1000);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Memory leak: interval and storage listener are never cleaned up.

setupChatProviderListener creates both a storage event listener and a setInterval that are never cleared in disconnectedCallback. This causes:

  1. Memory leak from accumulating storage listeners on repeated mounts
  2. Interval continues running after component unmount
🔎 Proposed fix
     setupChatProviderListener() {
         // Listen for localStorage changes for chat provider (from other windows/tabs)
-        window.addEventListener('storage', (e) => {
+        this._onProviderStorageChange = (e) => {
             if (e.key === 'chatProvider') {
                 this.chatProvider = e.newValue || 'gemini';
                 this.requestUpdate();
             }
-        });
+        };
+        window.addEventListener('storage', this._onProviderStorageChange);
         
         // Also check periodically to catch direct localStorage changes in same window
         this._providerCheckInterval = setInterval(() => {
             const currentProvider = localStorage.getItem('chatProvider') || 'gemini';
             if (currentProvider !== this.chatProvider) {
                 this.chatProvider = currentProvider;
                 this.requestUpdate();
             }
         }, 1000);
     }

And in disconnectedCallback:

     disconnectedCallback() {
         super.disconnectedCallback();
+        
+        // Clean up provider listener and interval
+        if (this._onProviderStorageChange) {
+            window.removeEventListener('storage', this._onProviderStorageChange);
+        }
+        if (this._providerCheckInterval) {
+            clearInterval(this._providerCheckInterval);
+        }

         // Clean up IPC listeners
         if (window.require) {
🤖 Prompt for AI Agents
In src/components/views/AssistantView.js around lines 637 to 654,
setupChatProviderListener registers a window 'storage' event handler and a
setInterval but does not remove them on unmount; add cleanup in
disconnectedCallback to remove the storage listener and clear the interval.
Store the listener function (e.g. this._onStorageChange) and the interval id
(this._providerCheckInterval) when creating them, then in disconnectedCallback
call window.removeEventListener('storage', this._onStorageChange) and
clearInterval(this._providerCheckInterval) (and null them afterwards) to prevent
memory leaks and stop the background polling.

Comment on lines +632 to 642
// Check if we're in coding mode
const selectedMode = localStorage.getItem('selectedMode') || 'interview';

if (selectedMode === 'coding') {
// For coding mode, ONLY send screenshot - system prompt will handle it
await captureScreenshot(quality, true);
} else {
// For interview mode, just send screenshot
await captureScreenshot(quality, true);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Redundant if/else branches—both paths identical

Lines 635-641 branch on selectedMode, but both the coding and interview paths call captureScreenshot(quality, true) with no other differences. The comments suggest different handling was intended, but the implementation is the same.

Either:

  • Remove the conditional if no mode-specific logic is needed, or
  • Implement the differentiated behavior hinted at by the comments (e.g., different prompts, different processing).

As-is, the code is misleading and adds unnecessary complexity.

🤖 Prompt for AI Agents
In src/utils/renderer.js around lines 632 to 642, the if/else on selectedMode is
redundant because both branches call captureScreenshot(quality, true); either
remove the conditional and always call captureScreenshot(quality, true), or
implement the intended mode-specific behavior: for coding mode call
captureScreenshot(quality, true) and then apply the coding-specific handling
(e.g., attach a system prompt or different processing), while for interview mode
call captureScreenshot(quality, true) and apply the interview-specific handling;
update or remove the comments to reflect the chosen behavior.

Comment on lines 696 to +799
async function sendTextMessage(text) {
if (!text || text.trim().length === 0) {
console.warn('Cannot send empty text message');
return { success: false, error: 'Empty message' };
}

try {
const result = await ipcRenderer.invoke('send-text-message', text);
if (result.success) {
console.log('Text message sent successfully');
} else {
console.error('Failed to send text message:', result.error);
// Check if we're using Ollama - if so, send text-only message directly
const chatProvider = localStorage.getItem('chatProvider') || 'gemini';
if (chatProvider === 'ollama') {
console.log('Sending text-only message to Ollama...');
// Use the IPC handler directly for text-only messages with Ollama
const result = await ipcRenderer.invoke('send-text-message', text.trim());
return result;
}
return result;
} catch (error) {
console.error('Error sending text message:', error);
return { success: false, error: error.message };
}
}

// Conversation storage functions using IndexedDB
let conversationDB = null;

async function initConversationStorage() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('ConversationHistory', 1);

request.onerror = () => reject(request.error);
request.onsuccess = () => {
conversationDB = request.result;
resolve(conversationDB);
};

request.onupgradeneeded = event => {
const db = event.target.result;

// Create sessions store
if (!db.objectStoreNames.contains('sessions')) {
const sessionStore = db.createObjectStore('sessions', { keyPath: 'sessionId' });
sessionStore.createIndex('timestamp', 'timestamp', { unique: false });
}
};
});
}
// Capture screenshot and get base64 data (for Gemini mode)
console.log('Capturing screenshot with text message...');

async function saveConversationSession(sessionId, conversationHistory) {
if (!conversationDB) {
await initConversationStorage();
}
if (!mediaStream) {
console.error('No media stream available');
return { success: false, error: 'No media stream' };
}

const transaction = conversationDB.transaction(['sessions'], 'readwrite');
const store = transaction.objectStore('sessions');
// Lazy init of video element if needed
if (!hiddenVideo) {
hiddenVideo = document.createElement('video');
hiddenVideo.srcObject = mediaStream;
hiddenVideo.muted = true;
hiddenVideo.playsInline = true;
await hiddenVideo.play();

await new Promise(resolve => {
if (hiddenVideo.readyState >= 2) return resolve();
hiddenVideo.onloadedmetadata = () => resolve();
});

const sessionData = {
sessionId: sessionId,
timestamp: parseInt(sessionId),
conversationHistory: conversationHistory,
lastUpdated: Date.now(),
};
offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = hiddenVideo.videoWidth;
offscreenCanvas.height = hiddenVideo.videoHeight;
offscreenContext = offscreenCanvas.getContext('2d');
}

return new Promise((resolve, reject) => {
const request = store.put(sessionData);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
// Check if video is ready
if (hiddenVideo.readyState < 2) {
console.warn('Video not ready');
return { success: false, error: 'Video not ready' };
}

async function getConversationSession(sessionId) {
if (!conversationDB) {
await initConversationStorage();
}
offscreenContext.drawImage(hiddenVideo, 0, 0, offscreenCanvas.width, offscreenCanvas.height);

// Get quality setting
let qualityValue;
switch (currentImageQuality) {
case 'high':
qualityValue = 0.9;
break;
case 'medium':
qualityValue = 0.7;
break;
case 'low':
qualityValue = 0.5;
break;
default:
qualityValue = 0.7;
}

const transaction = conversationDB.transaction(['sessions'], 'readonly');
const store = transaction.objectStore('sessions');
// Convert canvas to base64
const blob = await new Promise(resolve => {
offscreenCanvas.toBlob(resolve, 'image/jpeg', qualityValue);
});

return new Promise((resolve, reject) => {
const request = store.get(sessionId);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
if (!blob) {
console.error('Failed to create blob');
return { success: false, error: 'Failed to create blob' };
}

async function getAllConversationSessions() {
if (!conversationDB) {
await initConversationStorage();
}
const reader = new FileReader();
const base64data = await new Promise((resolve, reject) => {
reader.onloadend = () => resolve(reader.result.split(',')[1]);
reader.onerror = reject;
reader.readAsDataURL(blob);
});

const transaction = conversationDB.transaction(['sessions'], 'readonly');
const store = transaction.objectStore('sessions');
const index = store.index('timestamp');

return new Promise((resolve, reject) => {
const request = index.getAll();
request.onerror = () => reject(request.error);
request.onsuccess = () => {
// Sort by timestamp descending (newest first)
const sessions = request.result.sort((a, b) => b.timestamp - a.timestamp);
resolve(sessions);
};
});
}
// Send both screenshot and text together in one request
const result = await ipcRenderer.invoke('send-screenshot-with-text', {
imageData: base64data,
text: text.trim()
});

// Listen for conversation data from main process
ipcRenderer.on('save-conversation-turn', async (event, data) => {
try {
await saveConversationSession(data.sessionId, data.fullHistory);
console.log('Conversation session saved:', data.sessionId);
if (result.success) {
// Track image tokens
const imageTokens = tokenTracker.calculateImageTokens(offscreenCanvas.width, offscreenCanvas.height);
tokenTracker.addTokens(imageTokens, 'image');
console.log('Screenshot + text sent successfully in one request');
} else {
console.error('Failed to send screenshot with text:', result.error);
}
return result;
} catch (error) {
console.error('Error saving conversation session:', error);
console.error('Error sending text message with screenshot:', error);
return { success: false, error: error.message };
}
});

// Initialize conversation storage when renderer loads
initConversationStorage().catch(console.error);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

sendTextMessage requires mediaStream for Gemini, breaking text-only chat

Lines 715-718 return an error if mediaStream is unavailable, but this is called for all Gemini text messages (including when the user types in the assistant view without starting screen capture). This breaks text-only chat unless capture is active.

Additionally, the function name sendTextMessage doesn't convey that it captures and sends a screenshot alongside the text. Consider:

  • Renaming to sendTextMessageWithScreenshot for clarity, or
  • Making the screenshot optional and falling back to text-only if mediaStream is unavailable, or
  • Documenting the mediaStream requirement prominently.

For Ollama (lines 704-710), text-only works correctly.

Comment on lines +23 to +51
function checkFile(filePath, checks) {
const fullPath = path.join(projectRoot, 'src', filePath);

if (!fs.existsSync(fullPath)) {
console.log(`❌ File not found: ${filePath}`);
checks.failed++;
return false;
}

const content = fs.readFileSync(fullPath, 'utf8');

let allChecksPassed = true;

checks.forEach(check => {
const found = check.regex ? check.regex.test(content) : content.includes(check.text);

if (found) {
console.log(`✅ ${check.description}`);
checks.passed++;
} else {
console.log(`❌ ${check.description}`);
console.log(` Expected: ${check.text || check.regex}`);
checks.failed++;
allChecksPassed = false;
}
});

return allChecksPassed;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Parameter shadows outer checks object, breaking counters

Line 23 declares a parameter named checks, which shadows the outer checks object (line 17) that tracks passed, failed, and warnings counts. Inside checkFile, lines 28, 42, etc. reference checks.failed++ and checks.passed++, but these now refer to the array parameter, not the outer counter object. This breaks the verification logic.

Rename the parameter to avoid shadowing:

🔎 Proposed fix
-function checkFile(filePath, checks) {
+function checkFile(filePath, fileChecks) {
     const fullPath = path.join(projectRoot, 'src', filePath);
     
     if (!fs.existsSync(fullPath)) {
         console.log(`❌ File not found: ${filePath}`);
         checks.failed++;
         return false;
     }
     
     const content = fs.readFileSync(fullPath, 'utf8');
     
     let allChecksPassed = true;
     
-    checks.forEach(check => {
+    fileChecks.forEach(check => {
         const found = check.regex ? check.regex.test(content) : content.includes(check.text);
         
         if (found) {
             console.log(`✅ ${check.description}`);
             checks.passed++;
         } else {
             console.log(`❌ ${check.description}`);
             console.log(`   Expected: ${check.text || check.regex}`);
             checks.failed++;
             allChecksPassed = false;
         }
     });
     
     return allChecksPassed;
 }
🤖 Prompt for AI Agents
In verify-ollama-integration.js around lines 23 to 51 the function parameter
named "checks" shadows the outer counters object (defined around line 17),
causing checks.passed/failed increments to operate on the wrong value; rename
the function parameter (e.g., to "fileChecks" or "checksList"), update the
forEach and any references inside the function to use that new name, and leave
the outer "checks" counter object referenced when incrementing
checks.passed/checks.failed so the global counters are updated correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants