Conversation
* 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.
…sed-on-codebase Add repo guidelines
…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
…odel to gemini-3-pro
…matical responses
📝 WalkthroughWalkthroughAdds 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
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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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-containerhaswidth: 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 returnfalsefor clicks inside the shadow DOM when the event originates from the document level, becausee.targetis 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
commitTranscriptmethod passesSTT_CONFIG.languageCodewhich may not reflect the language used during streaming ifstartStreamingwas called with a differentlanguageCodeparameter. Use an instance property set duringstartStreaming.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.jsdefinessilenceThreshold: 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: 600butsrc/utils/vad.jsdefines it as200(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.
- The HTML indentation is broken - the
<p>tag alignment is inconsistent with the surrounding markup.- 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-napiis imported but never used.Line 99 imports
ref-napibut 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:beforeunloadhandler cannot await async functions.The
beforeunloadevent handler is markedasyncand awaitsshutdownPseudoLiveSystem(), but browsers do not wait for promises inbeforeunloadhandlers. The shutdown may not complete before the page unloads.🔎 Proposed fix
For Electron apps, consider using
window.addEventListener('close')via IPC or the main processwill-quitevent 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: Duplicateorchestrator-errorlistener may cause issues.A listener for
orchestrator-erroris 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-expfor 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 likegemini-2.5-flashorgemini-3-pro.src/components/app/CheatingDaddyApp.js-334-356 (1)
334-356: Indentation issue inhandleClearAndRestartmethod 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-settingIPC has no handler on the main process
handleVADChangeinvokes:await ipcRenderer.invoke('update-vad-setting', this.vadEnabled);but the main process only defines an
update-vad-modehandler insetupGeminiIpcHandlers; there is noipcMain.handle('update-vad-setting', ...). This will causeipcRenderer.invoketo 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
localStorageuntil 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"androle="listbox"on the containerrole="option"on each optionaria-expandedstate@keydownhandler for Enter, Escape, and Arrow key navigationPRODUCTION_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
maintainTopPositioninterval runs every 2 seconds indefinitely. While the cleanup onclosedis correct, consider:
- Using a longer interval (e.g., 5 seconds) since this is a fallback mechanism
- 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
executeJavaScriptis 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
initializeStreamingConnectionmethod is a placeholder. Consider either implementing proper streaming via@google-cloud/speechor 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.cssexists 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.,
textorplaintext) 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
VADProcessoris 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
getElapsedTimelogic instead of importing it fromAppHeader.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
clearSessionshortcut is rendered inline using a ternary onisMacOS, but it's not included in thekeybindsobject (lines 252-268). This means:
- It won't be customizable via Settings like other shortcuts
- 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.
setIntervalat 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()inshutdownPseudoLiveSystem().INTEGRATION_GUIDE.js (1)
77-78: Variable shadowing in example code.The function parameter is named
modeland line 77 also usesmodelfor 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
TokenTrackerclass is re-implemented in this test file instead of importing fromrenderer.js. If the production code changes, these tests may pass while the actual implementation is broken (or vice versa).Consider extracting
TokenTrackerto a separate module that can be imported by bothrenderer.jsand 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, orplaintext) 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
getSystemPromptaccepts 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 ifflushStorageDatathrows repeatedly.Using
event.preventDefault()followed byapp.exit()is correct, but ifflushStorageData()fails, the error is caught and logged, thenapp.exit()is called. This is fine, but consider thatbefore-quitmay 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: DuplicateinterimResultsconfiguration.
interimResults: trueis set both inSTREAMING_CONFIG(line 39) and again when callingstreamingRecognize(line 132). The spread operator already includes it fromSTREAMING_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,
handleStreamErrorstops the stream and logs the error, but there's no callback to notify the caller about the failure. Consider adding anonErrorcallback similar toonTranscriptComplete.🔎 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
float32ToPCM16function is nearly identical to the conversion inpseudoLiveOrchestrator.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 usesnavigator.platformwhich is deprecated.
navigator.platformis deprecated and may be removed in future browser versions. Consider usingnavigator.userAgentData.platformwith a fallback, or the existingcheddar.isMacOSif 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 = isMacsrc/preload.js (1)
189-191: Thesendmethod lacks channel validation.Unlike the
onmethod which validates channels, thesendmethod 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:initializeGeminidefaults prevent localStorage mode/model from ever being usedBecause the function has default parameters
mode = 'interview'andmodel = '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/selectedModelinlocalStorage.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 optionsThe Exam Assistant description says:
“uses Gemini API 2.5 Flash or 2.5 Pro …”
but the actual options are
gemini-2.5-flashandgemini-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 behaviorThe new
send-image-contentandsend-screenshot-with-texthandlers 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-textpath 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
⛔ Files ignored due to path filters (3)
package-lock.jsonis excluded by!**/package-lock.jsonsrc/assets/katex-0.16.9.min.jsis excluded by!**/*.min.jssrc/assets/models/500px-Google_Gemini_icon_2025.svg.pngis excluded by!**/*.png
📒 Files selected for processing (48)
.gitignoreAGENTS.mdARCHITECTURE.mdIMPLEMENTATION_COMPLETE.mdIMPLEMENTATION_SUMMARY.mdINTEGRATION_GUIDE.jsPRODUCTION_IMPLEMENTATION.mdPSEUDO_LIVE_INTEGRATION.jsPSEUDO_LIVE_README.mdQUICK_START.mdREADME.mdSTT_COMPARISON.mdTESTING_GUIDE.mdforge.config.jspackage.jsonsrc/__tests__/geminiConversation.test.jssrc/__tests__/languages.test.jssrc/__tests__/prompts.test.jssrc/__tests__/timer.test.jssrc/__tests__/tokenTracking.test.jssrc/__tests__/vad.test.jssrc/assets/katex-0.16.9.min.csssrc/components/app/AppHeader.jssrc/components/app/CheatingDaddyApp.jssrc/components/common/CustomDropdown.jssrc/components/index.jssrc/components/views/AdvancedView.jssrc/components/views/AssistantView.jssrc/components/views/CustomizeView.jssrc/components/views/HelpView.jssrc/components/views/HistoryView.jssrc/components/views/MainView.jssrc/config.jssrc/index.htmlsrc/index.jssrc/preload.jssrc/script.jssrc/utils/gemini.jssrc/utils/geminiSTT.jssrc/utils/googleSpeechSTT.jssrc/utils/prompts.jssrc/utils/pseudoLiveOrchestrator.jssrc/utils/renderer.jssrc/utils/speechToText.jssrc/utils/stealthFeatures.jssrc/utils/vad.jssrc/utils/window.jssrc/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)
| ```bash | ||
| npm install @google-cloud/speech --save | ||
| ``` | ||
|
|
There was a problem hiding this comment.
🧩 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 -20Repository: 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 -50Repository: 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.
| 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(); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
wc -l src/components/views/AssistantView.jsRepository: 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 20Repository: sohzm/cheating-daddy
Length of output: 1271
🏁 Script executed:
cat -n src/components/views/AssistantView.js | grep -n "setupVADModeListener" -A 30Repository: sohzm/cheating-daddy
Length of output: 1500
🏁 Script executed:
cat -n src/components/views/AssistantView.js | grep -n "addEventListener.*storage" -B 5 -A 15Repository: sohzm/cheating-daddy
Length of output: 984
🏁 Script executed:
find . -name "*.js" -type f | xargs grep -l "localStorage.setItem.*vadMode" | head -20Repository: sohzm/cheating-daddy
Length of output: 132
🏁 Script executed:
find . -name "*Customize*" -o -name "*Settings*" | head -20Repository: sohzm/cheating-daddy
Length of output: 103
🏁 Script executed:
grep -r "localStorage.setItem.*vadMode" --include="*.js" -B 2 -A 2Repository: sohzm/cheating-daddy
Length of output: 681
🏁 Script executed:
cat -n src/components/views/CustomizeView.js | grep -n "handleVADModeChange" -B 3 -A 10Repository: sohzm/cheating-daddy
Length of output: 2127
🏁 Script executed:
grep -r "addEventListener.*storage" --include="*.js" -B 2 -A 10Repository: 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 -5Repository: 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:
-
Functional bug: The
storageevent only fires in other browsing contexts (tabs/windows), not in the document that callslocalStorage.setItem(). IfCustomizeView(which callslocalStorage.setItem('vadMode', ...)at line 951) andAssistantVieware 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. -
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 onthis, 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.
| // Handle Squirrel events manually to prevent desktop shortcuts | ||
| if (require('electron').app) { | ||
| const squirrelCommand = process.argv[1]; | ||
| if (handleSquirrelEvent(squirrelCommand)) { | ||
| return; | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| // 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 }; | ||
| } | ||
| }); |
There was a problem hiding this comment.
🧩 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 -50Repository: 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 -30Repository: 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.
| off: (channel, callback) => { | ||
| if (callback) { | ||
| ipcRenderer.removeListener(channel, callback); | ||
| } else { | ||
| ipcRenderer.removeAllListeners(channel); | ||
| } | ||
| }, |
There was a problem hiding this comment.
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.
| 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}`); | ||
| } | ||
| } |
There was a problem hiding this comment.
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:
- Use REST API directly with
?key=API_KEYquery parameter - 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.
| 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.
| updateLanguage(languageCode) { | ||
| console.log(`[STT] Updating language to: ${languageCode}`); | ||
| STT_CONFIG.languageCode = languageCode; | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| // 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; | ||
| } |
There was a problem hiding this comment.
🧩 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 -20Repository: 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.jsRepository: sohzm/cheating-daddy
Length of output: 1659
🏁 Script executed:
# Get broader context of the handleCommit/onCommit function
sed -n '350,420p' src/utils/pseudoLiveOrchestrator.jsRepository: 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 5Repository: 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 3Repository: 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.
| // 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() | ||
| }); |
There was a problem hiding this comment.
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.
| // 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.
| // Public method to update configuration | ||
| updateConfig(newConfig) { | ||
| Object.assign(VAD_CONFIG, newConfig); | ||
| console.log('VAD configuration updated:', VAD_CONFIG); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
There was a problem hiding this comment.
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
microphoneEnabledalone, butmicrophoneEnabledis only set when VAD is active. When VAD is disabled or fails to load,microphoneEnabledstaysfalse, 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]offmethod cannot remove listeners registered viaonThe past review comment remains valid. Line 164 wraps the callback in a new function
(_, data) => callback(data), but line 177 tries to remove the originalcallback. Since the wrapper is a different reference,removeListenerwon'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]geminiSessionRefundefined in error/close handlers;lastSessionParamscleared before useThe past review comment remains valid:
Undefined
geminiSessionRef: Lines 558 and 626 referencegeminiSessionRef, which is not in scope withininitializeGeminiSession. This will throw aReferenceError. Useglobal.geminiSessionRefinstead (set at line 1310), and guard against it beingundefined.
lastSessionParamscleared prematurely: Lines 551-552 (onerror) setlastSessionParams = nulland then checkif (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
setupVADModeListeneris never removed indisconnectedCallback, causing memory leaks on repeated component mounts. Additionally, thestorageevent 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 lookupsLines 173-174 unconditionally read
modeandmodelfrom localStorage, overriding the function arguments. The ternarymode || localStorage.getItem(...)will use localStorage whenevermodeis falsy (includingundefined), but the parameters have default values in the signature, so callers passing explicit values will be ignored if they passundefined.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 onlynull/undefinedtrigger the fallback, preserving explicit empty strings if that's ever needed.
847-856: Direct access tovadProcessor.audioBuffercouples to internal stateLine 849 checks
vadProcessor.audioBufferto 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 oncommit()to be a no-op when there's no audio.src/utils/ollama.js (1)
11-21: Consider adding timeouts to fetch callsThe
fetchcalls 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 whitelistLines 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 requestLines 259-263 use
Promise.raceto implement a 3-second timeout. If the timeout wins, the underlyinggenerateContentcall continues running, potentially wasting API quota and resources.Consider using
AbortControllerto 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
getLocalizedGreetingmethod contains a large object with 30+ translations, making this component harder to maintain. Consider extracting to a separatelocalization.jsmodule.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
DELIVERABLES.mdIMPLEMENTATION_SUMMARY.mdOLLAMA_INTEGRATION_COMPLETE.mdOLLAMA_INTEGRATION_COMPLETE_FINAL.mdQUICKSTART.mdsrc/components/app/CheatingDaddyApp.jssrc/components/views/AssistantView.jssrc/components/views/CustomizeView.jssrc/components/views/MainView.jssrc/preload.jssrc/utils/gemini.jssrc/utils/ollama.jssrc/utils/renderer.jsverify-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
connectedCallbackand correctly removed indisconnectedCallback, 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 legacycustomPromptkey 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
extractCodeBlocksmethod 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
!importantto override highlight.js defaults. The language-specific enhancements for JavaScript, Python, HTML, CSS, SQL, and JSON are well-organized.
| 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'}`); | ||
| } | ||
| } |
There was a problem hiding this comment.
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');| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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); | ||
| } |
There was a problem hiding this comment.
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:
- Memory leak from accumulating storage listeners on repeated mounts
- 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.
| // 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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); | ||
| } |
There was a problem hiding this comment.
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
sendTextMessageWithScreenshotfor clarity, or - Making the screenshot optional and falling back to text-only if
mediaStreamis unavailable, or - Documenting the mediaStream requirement prominently.
For Ollama (lines 704-710), text-only works correctly.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
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:
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:
enableSpeakerDiarizationminSpeakerCountThis caused early JSON validation failures.
2. Live API Access Is Gated
Inspection via
GET /v1beta/modelsrevealed:bidiGenerateContent*-live-*models were enabled➡️ 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:
Audio must be converted to text before reasoning, which is how Copilot-style tools work.
✅ Implemented Solution: Pseudo-Live Architecture
Instead of:
The system uses:
This approach:
🧠 Final Architecture
⏱️ Latency Characteristics
Typical timings:
Total end-to-end latency: ~400–800 ms
⚙️ Important Implementation Details
?,.)🏁 Final Verdict
This pseudo-live architecture:
🙌 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
Documentation
Refactor
Tests
✏️ Tip: You can customize this high-level summary in your review settings.