+ `;
+ }
- renderLanguageSection() {
- return html`
-
- `;
- }
-
- renderAppearanceSection() {
- return html`
-
- `;
- }
-
- renderKeyboardSection() {
- return html`
-
- `;
- }
-
- renderPrivacySection() {
- return html`
-
-
-
Settings
- ${this.renderAISection()}
- ${this.renderAudioSection()}
- ${this.renderLanguageSection()}
- ${this.renderAppearanceSection()}
- ${this.renderKeyboardSection()}
- ${this.renderPrivacySection()}
-
+ renderAppearanceSection() {
+ return html`
+
+ `;
+ }
+
+ renderKeyboardSection() {
+ return html`
+
+ Keyboard Shortcuts
+ ${this.getKeybindActions().map(
+ (action) => html`
+
+ ${action.name}
+
+
+ `,
+ )}
+
+
+
+
+ `;
+ }
+
+ renderPrivacySection() {
+ return html`
+
+ Privacy and Data
+
+
+
+
+ ${this.clearStatusMessage
+ ? html`
+
+ ${this.clearStatusMessage}
+
+ `
+ : ""}
+
+ `;
+ }
+
+ render() {
+ return html`
+
+
+
Settings
+ ${this.renderAISection()} ${this.renderAudioSection()}
+ ${this.renderLanguageSection()} ${this.renderAppearanceSection()}
+ ${this.renderKeyboardSection()} ${this.renderPrivacySection()}
+
+
+ `;
+ }
}
-customElements.define('customize-view', CustomizeView);
+customElements.define("customize-view", CustomizeView);
diff --git a/src/utils/gemini.js b/src/utils/gemini.js
index 5d034e3..3645e4e 100644
--- a/src/utils/gemini.js
+++ b/src/utils/gemini.js
@@ -1,30 +1,38 @@
-const { GoogleGenAI, Modality } = require('@google/genai');
-const { BrowserWindow, ipcMain } = require('electron');
-const { spawn } = require('child_process');
-const { saveDebugAudio } = require('../audioUtils');
-const { getSystemPrompt } = require('./prompts');
-const { getAvailableModel, incrementLimitCount, getApiKey, getGroqApiKey, getOpenAICompatibleConfig, incrementCharUsage, getModelForToday } = require('../storage');
-const OpenAI = require('openai');
+const { GoogleGenAI, Modality } = require("@google/genai");
+const { BrowserWindow, ipcMain } = require("electron");
+const { spawn } = require("child_process");
+const { saveDebugAudio } = require("../audioUtils");
+const { getSystemPrompt } = require("./prompts");
+const {
+ getAvailableModel,
+ incrementLimitCount,
+ getApiKey,
+ getGroqApiKey,
+ getOpenAICompatibleConfig,
+ incrementCharUsage,
+ getModelForToday,
+} = require("../storage");
+const OpenAI = require("openai");
// Lazy-loaded to avoid circular dependency (localai.js imports from gemini.js)
let _localai = null;
function getLocalAi() {
- if (!_localai) _localai = require('./localai');
- return _localai;
+ if (!_localai) _localai = require("./localai");
+ return _localai;
}
// Provider mode: 'byok' or 'local'
-let currentProviderMode = 'byok';
+let currentProviderMode = "byok";
// Response provider: 'gemini', 'groq', or 'openai-compatible'
-let currentResponseProvider = 'gemini';
+let currentResponseProvider = "gemini";
// Groq conversation history for context
let groqConversationHistory = [];
// Conversation tracking variables
let currentSessionId = null;
-let currentTranscription = '';
+let currentTranscription = "";
let conversationHistory = [];
let screenAnalysisHistory = [];
let currentProfile = null;
@@ -33,22 +41,21 @@ let isInitializingSession = false;
let currentSystemPrompt = null;
function formatSpeakerResults(results) {
- let text = '';
- for (const result of results) {
- if (result.transcript && result.speakerId) {
- const speakerLabel = result.speakerId === 1 ? 'Interviewer' : 'Candidate';
- text += `[${speakerLabel}]: ${result.transcript}\n`;
- }
+ let text = "";
+ for (const result of results) {
+ if (result.transcript && result.speakerId) {
+ const speakerLabel = result.speakerId === 1 ? "Interviewer" : "Candidate";
+ text += `[${speakerLabel}]: ${result.transcript}\n`;
}
- return text;
+ }
+ return text;
}
module.exports.formatSpeakerResults = formatSpeakerResults;
// Audio capture variables
let systemAudioProc = null;
-let messageBuffer = '';
-
+let messageBuffer = "";
// Reconnection variables
let isUserClosing = false;
@@ -58,127 +65,138 @@ const MAX_RECONNECT_ATTEMPTS = 3;
const RECONNECT_DELAY = 2000;
function sendToRenderer(channel, data) {
- const windows = BrowserWindow.getAllWindows();
- if (windows.length > 0) {
- windows[0].webContents.send(channel, data);
- }
+ const windows = BrowserWindow.getAllWindows();
+ if (windows.length > 0) {
+ windows[0].webContents.send(channel, data);
+ }
}
// Build context message for session restoration
function buildContextMessage() {
- const lastTurns = conversationHistory.slice(-20);
- const validTurns = lastTurns.filter(turn => turn.transcription?.trim() && turn.ai_response?.trim());
+ const lastTurns = conversationHistory.slice(-20);
+ const validTurns = lastTurns.filter(
+ (turn) => turn.transcription?.trim() && turn.ai_response?.trim(),
+ );
- if (validTurns.length === 0) return null;
+ if (validTurns.length === 0) return null;
- const contextLines = validTurns.map(turn =>
- `[Interviewer]: ${turn.transcription.trim()}\n[Your answer]: ${turn.ai_response.trim()}`
- );
+ const contextLines = validTurns.map(
+ (turn) =>
+ `[Interviewer]: ${turn.transcription.trim()}\n[Your answer]: ${turn.ai_response.trim()}`,
+ );
- return `Session reconnected. Here's the conversation so far:\n\n${contextLines.join('\n\n')}\n\nContinue from here.`;
+ return `Session reconnected. Here's the conversation so far:\n\n${contextLines.join("\n\n")}\n\nContinue from here.`;
}
// Conversation management functions
function initializeNewSession(profile = null, customPrompt = null) {
- currentSessionId = Date.now().toString();
- currentTranscription = '';
- conversationHistory = [];
- screenAnalysisHistory = [];
- groqConversationHistory = [];
- currentProfile = profile;
- currentCustomPrompt = customPrompt;
- console.log('New conversation session started:', currentSessionId, 'profile:', profile);
+ currentSessionId = Date.now().toString();
+ currentTranscription = "";
+ conversationHistory = [];
+ screenAnalysisHistory = [];
+ groqConversationHistory = [];
+ currentProfile = profile;
+ currentCustomPrompt = customPrompt;
+ console.log(
+ "New conversation session started:",
+ currentSessionId,
+ "profile:",
+ profile,
+ );
- // Save initial session with profile context
- if (profile) {
- sendToRenderer('save-session-context', {
- sessionId: currentSessionId,
- profile: profile,
- customPrompt: customPrompt || ''
- });
- }
+ // Save initial session with profile context
+ if (profile) {
+ sendToRenderer("save-session-context", {
+ sessionId: currentSessionId,
+ profile: profile,
+ customPrompt: customPrompt || "",
+ });
+ }
}
function saveConversationTurn(transcription, aiResponse) {
- if (!currentSessionId) {
- initializeNewSession();
- }
+ if (!currentSessionId) {
+ initializeNewSession();
+ }
- const conversationTurn = {
- timestamp: Date.now(),
- transcription: transcription.trim(),
- ai_response: aiResponse.trim(),
- };
+ const conversationTurn = {
+ timestamp: Date.now(),
+ transcription: transcription.trim(),
+ ai_response: aiResponse.trim(),
+ };
- conversationHistory.push(conversationTurn);
- console.log('Saved conversation turn:', conversationTurn);
+ conversationHistory.push(conversationTurn);
+ console.log("Saved conversation turn:", conversationTurn);
- // Send to renderer to save in IndexedDB
- sendToRenderer('save-conversation-turn', {
- sessionId: currentSessionId,
- turn: conversationTurn,
- fullHistory: conversationHistory,
- });
+ // Send to renderer to save in IndexedDB
+ sendToRenderer("save-conversation-turn", {
+ sessionId: currentSessionId,
+ turn: conversationTurn,
+ fullHistory: conversationHistory,
+ });
}
function saveScreenAnalysis(prompt, response, model) {
- if (!currentSessionId) {
- initializeNewSession();
- }
+ if (!currentSessionId) {
+ initializeNewSession();
+ }
- const analysisEntry = {
- timestamp: Date.now(),
- prompt: prompt,
- response: response.trim(),
- model: model
- };
+ const analysisEntry = {
+ timestamp: Date.now(),
+ prompt: prompt,
+ response: response.trim(),
+ model: model,
+ };
- screenAnalysisHistory.push(analysisEntry);
- console.log('Saved screen analysis:', analysisEntry);
+ screenAnalysisHistory.push(analysisEntry);
+ console.log("Saved screen analysis:", analysisEntry);
- // Send to renderer to save
- sendToRenderer('save-screen-analysis', {
- sessionId: currentSessionId,
- analysis: analysisEntry,
- fullHistory: screenAnalysisHistory,
- profile: currentProfile,
- customPrompt: currentCustomPrompt
- });
+ // Send to renderer to save
+ sendToRenderer("save-screen-analysis", {
+ sessionId: currentSessionId,
+ analysis: analysisEntry,
+ fullHistory: screenAnalysisHistory,
+ profile: currentProfile,
+ customPrompt: currentCustomPrompt,
+ });
}
function getCurrentSessionData() {
- return {
- sessionId: currentSessionId,
- history: conversationHistory,
- };
+ return {
+ sessionId: currentSessionId,
+ history: conversationHistory,
+ };
}
async function getEnabledTools() {
- const tools = [];
+ const tools = [];
- // Check if Google Search is enabled (default: true)
- const googleSearchEnabled = await getStoredSetting('googleSearchEnabled', 'true');
- console.log('Google Search enabled:', googleSearchEnabled);
+ // Check if Google Search is enabled (default: true)
+ const googleSearchEnabled = await getStoredSetting(
+ "googleSearchEnabled",
+ "true",
+ );
+ console.log("Google Search enabled:", googleSearchEnabled);
- if (googleSearchEnabled === 'true') {
- tools.push({ googleSearch: {} });
- console.log('Added Google Search tool');
- } else {
- console.log('Google Search tool disabled');
- }
+ if (googleSearchEnabled === "true") {
+ tools.push({ googleSearch: {} });
+ console.log("Added Google Search tool");
+ } else {
+ console.log("Google Search tool disabled");
+ }
- return tools;
+ return tools;
}
async function getStoredSetting(key, defaultValue) {
- try {
- const windows = BrowserWindow.getAllWindows();
- if (windows.length > 0) {
- // Wait a bit for the renderer to be ready
- await new Promise(resolve => setTimeout(resolve, 100));
+ try {
+ const windows = BrowserWindow.getAllWindows();
+ if (windows.length > 0) {
+ // Wait a bit for the renderer to be ready
+ await new Promise((resolve) => setTimeout(resolve, 100));
- // Try to get setting from renderer process localStorage
- const value = await windows[0].webContents.executeJavaScript(`
+ // Try to get setting from renderer process localStorage
+ const value = await windows[0].webContents.executeJavaScript(`
(function() {
try {
if (typeof localStorage === 'undefined') {
@@ -194,1037 +212,1202 @@ async function getStoredSetting(key, defaultValue) {
}
})()
`);
- return value;
- }
- } catch (error) {
- console.error('Error getting stored setting for', key, ':', error.message);
+ return value;
}
- console.log('Using default value for', key, ':', defaultValue);
- return defaultValue;
+ } catch (error) {
+ console.error("Error getting stored setting for", key, ":", error.message);
+ }
+ console.log("Using default value for", key, ":", defaultValue);
+ return defaultValue;
}
// helper to check if groq has been configured
function hasGroqKey() {
- const key = getGroqApiKey();
- return key && key.trim() != ''
+ const key = getGroqApiKey();
+ return key && key.trim() != "";
}
// helper to check if OpenAI-compatible API has been configured
function hasOpenAICompatibleConfig() {
- const config = getOpenAICompatibleConfig();
- return config.apiKey && config.apiKey.trim() !== '' &&
- config.baseUrl && config.baseUrl.trim() !== '' &&
- config.model && config.model.trim() !== '';
+ const config = getOpenAICompatibleConfig();
+ return (
+ config.apiKey &&
+ config.apiKey.trim() !== "" &&
+ config.baseUrl &&
+ config.baseUrl.trim() !== "" &&
+ config.model &&
+ config.model.trim() !== ""
+ );
}
-function trimConversationHistoryForGemma(history, maxChars=42000) {
- if(!history || history.length === 0) return [];
- let totalChars = 0;
- const trimmed = [];
+function trimConversationHistoryForGemma(history, maxChars = 42000) {
+ if (!history || history.length === 0) return [];
+ let totalChars = 0;
+ const trimmed = [];
- for(let i = history.length - 1; i >= 0; i--) {
- const turn = history[i];
- const turnChars = (turn.content || '').length;
+ for (let i = history.length - 1; i >= 0; i--) {
+ const turn = history[i];
+ const turnChars = (turn.content || "").length;
- if(totalChars + turnChars > maxChars) break;
- totalChars += turnChars;
- trimmed.unshift(turn);
- }
- return trimmed;
+ if (totalChars + turnChars > maxChars) break;
+ totalChars += turnChars;
+ trimmed.unshift(turn);
+ }
+ return trimmed;
}
function stripThinkingTags(text) {
- return text.replace(/
[\s\S]*?<\/think>/g, '').trim();
+ return text.replace(/[\s\S]*?<\/think>/g, "").trim();
}
async function sendToGroq(transcription) {
- const groqApiKey = getGroqApiKey();
- if (!groqApiKey) {
- console.log('No Groq API key configured, skipping Groq response');
- return;
- }
+ const groqApiKey = getGroqApiKey();
+ if (!groqApiKey) {
+ console.log("No Groq API key configured, skipping Groq response");
+ return;
+ }
- if (!transcription || transcription.trim() === '') {
- console.log('Empty transcription, skipping Groq');
- return;
- }
+ if (!transcription || transcription.trim() === "") {
+ console.log("Empty transcription, skipping Groq");
+ return;
+ }
- const modelToUse = getModelForToday();
- if (!modelToUse) {
- console.log('All Groq daily limits exhausted');
- sendToRenderer('update-status', 'Groq limits reached for today');
- return;
- }
+ const modelToUse = getModelForToday();
+ if (!modelToUse) {
+ console.log("All Groq daily limits exhausted");
+ sendToRenderer("update-status", "Groq limits reached for today");
+ return;
+ }
- console.log(`Sending to Groq (${modelToUse}):`, transcription.substring(0, 100) + '...');
+ console.log(
+ `Sending to Groq (${modelToUse}):`,
+ transcription.substring(0, 100) + "...",
+ );
- groqConversationHistory.push({
- role: 'user',
- content: transcription.trim()
- });
+ groqConversationHistory.push({
+ role: "user",
+ content: transcription.trim(),
+ });
- if (groqConversationHistory.length > 20) {
- groqConversationHistory = groqConversationHistory.slice(-20);
- }
+ if (groqConversationHistory.length > 20) {
+ groqConversationHistory = groqConversationHistory.slice(-20);
+ }
- try {
- const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${groqApiKey}`,
- 'Content-Type': 'application/json'
+ try {
+ const response = await fetch(
+ "https://api.groq.com/openai/v1/chat/completions",
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${groqApiKey}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ model: modelToUse,
+ messages: [
+ {
+ role: "system",
+ content: currentSystemPrompt || "You are a helpful assistant.",
},
- body: JSON.stringify({
- model: modelToUse,
- messages: [
- { role: 'system', content: currentSystemPrompt || 'You are a helpful assistant.' },
- ...groqConversationHistory
- ],
- stream: true,
- temperature: 0.7,
- max_tokens: 1024
- })
- });
+ ...groqConversationHistory,
+ ],
+ stream: true,
+ temperature: 0.7,
+ max_tokens: 1024,
+ }),
+ },
+ );
- if (!response.ok) {
- const errorText = await response.text();
- console.error('Groq API error:', response.status, errorText);
- sendToRenderer('update-status', `Groq error: ${response.status}`);
- return;
- }
-
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let fullText = '';
- let isFirst = true;
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
-
- const chunk = decoder.decode(value, { stream: true });
- const lines = chunk.split('\n').filter(line => line.trim() !== '');
-
- for (const line of lines) {
- if (line.startsWith('data: ')) {
- const data = line.slice(6);
- if (data === '[DONE]') continue;
-
- try {
- const json = JSON.parse(data);
- const token = json.choices?.[0]?.delta?.content || '';
- if (token) {
- fullText += token;
- const displayText = stripThinkingTags(fullText);
- if (displayText) {
- sendToRenderer(isFirst ? 'new-response' : 'update-response', displayText);
- isFirst = false;
- }
- }
- } catch (parseError) {
- // Skip invalid JSON chunks
- }
- }
- }
- }
-
- const cleanedResponse = stripThinkingTags(fullText);
- const modelKey = modelToUse.split('/').pop();
-
- const systemPromptChars = (currentSystemPrompt || 'You are a helpful assistant.').length;
- const historyChars = groqConversationHistory.reduce((sum, msg) => sum + (msg.content || '').length, 0);
- const inputChars = systemPromptChars + historyChars;
- const outputChars = cleanedResponse.length;
-
- incrementCharUsage('groq', modelKey, inputChars + outputChars);
-
- if (cleanedResponse) {
- groqConversationHistory.push({
- role: 'assistant',
- content: cleanedResponse
- });
-
- saveConversationTurn(transcription, cleanedResponse);
- }
-
- console.log(`Groq response completed (${modelToUse})`);
- sendToRenderer('update-status', 'Listening...');
-
- } catch (error) {
- console.error('Error calling Groq API:', error);
- sendToRenderer('update-status', 'Groq error: ' + error.message);
+ if (!response.ok) {
+ const errorText = await response.text();
+ console.error("Groq API error:", response.status, errorText);
+ sendToRenderer("update-status", `Groq error: ${response.status}`);
+ return;
}
+
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let fullText = "";
+ let isFirst = true;
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ const chunk = decoder.decode(value, { stream: true });
+ const lines = chunk.split("\n").filter((line) => line.trim() !== "");
+
+ for (const line of lines) {
+ if (line.startsWith("data: ")) {
+ const data = line.slice(6);
+ if (data === "[DONE]") continue;
+
+ try {
+ const json = JSON.parse(data);
+ const token = json.choices?.[0]?.delta?.content || "";
+ if (token) {
+ fullText += token;
+ const displayText = stripThinkingTags(fullText);
+ if (displayText) {
+ sendToRenderer(
+ isFirst ? "new-response" : "update-response",
+ displayText,
+ );
+ isFirst = false;
+ }
+ }
+ } catch (parseError) {
+ // Skip invalid JSON chunks
+ }
+ }
+ }
+ }
+
+ const cleanedResponse = stripThinkingTags(fullText);
+ const modelKey = modelToUse.split("/").pop();
+
+ const systemPromptChars = (
+ currentSystemPrompt || "You are a helpful assistant."
+ ).length;
+ const historyChars = groqConversationHistory.reduce(
+ (sum, msg) => sum + (msg.content || "").length,
+ 0,
+ );
+ const inputChars = systemPromptChars + historyChars;
+ const outputChars = cleanedResponse.length;
+
+ incrementCharUsage("groq", modelKey, inputChars + outputChars);
+
+ if (cleanedResponse) {
+ groqConversationHistory.push({
+ role: "assistant",
+ content: cleanedResponse,
+ });
+
+ saveConversationTurn(transcription, cleanedResponse);
+ }
+
+ console.log(`Groq response completed (${modelToUse})`);
+ sendToRenderer("update-status", "Listening...");
+ } catch (error) {
+ console.error("Error calling Groq API:", error);
+ sendToRenderer("update-status", "Groq error: " + error.message);
+ }
}
async function sendToOpenAICompatible(transcription) {
- const config = getOpenAICompatibleConfig();
-
- if (!config.apiKey || !config.baseUrl || !config.model) {
- console.log('OpenAI-compatible API not fully configured');
- return;
- }
+ const config = getOpenAICompatibleConfig();
- if (!transcription || transcription.trim() === '') {
- console.log('Empty transcription, skipping OpenAI-compatible API');
- return;
- }
+ if (!config.apiKey || !config.baseUrl || !config.model) {
+ console.log("OpenAI-compatible API not fully configured");
+ return;
+ }
- console.log(`Sending to OpenAI-compatible API (${config.model}):`, transcription.substring(0, 100) + '...');
+ if (!transcription || transcription.trim() === "") {
+ console.log("Empty transcription, skipping OpenAI-compatible API");
+ return;
+ }
- groqConversationHistory.push({
- role: 'user',
- content: transcription.trim()
+ console.log(
+ `Sending to OpenAI-compatible API (${config.model}):`,
+ transcription.substring(0, 100) + "...",
+ );
+
+ groqConversationHistory.push({
+ role: "user",
+ content: transcription.trim(),
+ });
+
+ if (groqConversationHistory.length > 20) {
+ groqConversationHistory = groqConversationHistory.slice(-20);
+ }
+
+ try {
+ const client = new OpenAI({
+ apiKey: config.apiKey,
+ baseURL: config.baseUrl.trim().replace(/\/$/, ""),
+ dangerouslyAllowBrowser: false,
});
- if (groqConversationHistory.length > 20) {
- groqConversationHistory = groqConversationHistory.slice(-20);
+ console.log(`Using OpenAI-compatible base URL: ${config.baseUrl}`);
+
+ const stream = await client.chat.completions.create({
+ model: config.model,
+ messages: [
+ {
+ role: "system",
+ content: currentSystemPrompt || "You are a helpful assistant.",
+ },
+ ...groqConversationHistory,
+ ],
+ stream: true,
+ temperature: 0.7,
+ max_tokens: 2048,
+ });
+
+ let fullText = "";
+ let isFirst = true;
+
+ for await (const chunk of stream) {
+ const content = chunk.choices?.[0]?.delta?.content;
+
+ if (content) {
+ fullText += content;
+ sendToRenderer(isFirst ? "new-response" : "update-response", fullText);
+ isFirst = false;
+ }
}
- try {
- const client = new OpenAI({
- apiKey: config.apiKey,
- baseURL: config.baseUrl.trim().replace(/\/$/, ''),
- dangerouslyAllowBrowser: false
- });
-
- console.log(`Using OpenAI-compatible base URL: ${config.baseUrl}`);
-
- const stream = await client.chat.completions.create({
- model: config.model,
- messages: [
- { role: 'system', content: currentSystemPrompt || 'You are a helpful assistant.' },
- ...groqConversationHistory
- ],
- stream: true,
- temperature: 0.7,
- max_tokens: 2048
- });
-
- let fullText = '';
- let isFirst = true;
-
- for await (const chunk of stream) {
- const content = chunk.choices?.[0]?.delta?.content;
-
- if (content) {
- fullText += content;
- sendToRenderer(isFirst ? 'new-response' : 'update-response', fullText);
- isFirst = false;
- }
- }
-
- // Clean up tags if present (for DeepSeek-style reasoning models)
- const cleanText = stripThinkingTags(fullText);
- if (cleanText !== fullText) {
- sendToRenderer('update-response', cleanText);
- }
-
- if (fullText.trim()) {
- groqConversationHistory.push({
- role: 'assistant',
- content: fullText.trim()
- });
-
- if (groqConversationHistory.length > 40) {
- groqConversationHistory = groqConversationHistory.slice(-40);
- }
-
- saveConversationTurn(transcription, fullText);
- }
-
- console.log(`OpenAI-compatible API response completed (${config.model})`);
- sendToRenderer('update-status', 'Listening...');
-
- } catch (error) {
- console.error('Error calling OpenAI-compatible API:', error);
- sendToRenderer('update-status', 'OpenAI API error: ' + error.message);
+ // Clean up tags if present (for DeepSeek-style reasoning models)
+ const cleanText = stripThinkingTags(fullText);
+ if (cleanText !== fullText) {
+ sendToRenderer("update-response", cleanText);
}
+
+ if (fullText.trim()) {
+ groqConversationHistory.push({
+ role: "assistant",
+ content: fullText.trim(),
+ });
+
+ if (groqConversationHistory.length > 40) {
+ groqConversationHistory = groqConversationHistory.slice(-40);
+ }
+
+ saveConversationTurn(transcription, fullText);
+ }
+
+ console.log(`OpenAI-compatible API response completed (${config.model})`);
+ sendToRenderer("update-status", "Listening...");
+ } catch (error) {
+ console.error("Error calling OpenAI-compatible API:", error);
+ sendToRenderer("update-status", "OpenAI API error: " + error.message);
+ }
}
async function sendToGemma(transcription) {
- const apiKey = getApiKey();
- if (!apiKey) {
- console.log('No Gemini API key configured');
- return;
- }
+ const apiKey = getApiKey();
+ if (!apiKey) {
+ console.log("No Gemini API key configured");
+ return;
+ }
- if (!transcription || transcription.trim() === '') {
- console.log('Empty transcription, skipping Gemma');
- return;
- }
+ if (!transcription || transcription.trim() === "") {
+ console.log("Empty transcription, skipping Gemma");
+ return;
+ }
- console.log('Sending to Gemma:', transcription.substring(0, 100) + '...');
+ console.log("Sending to Gemma:", transcription.substring(0, 100) + "...");
- groqConversationHistory.push({
- role: 'user',
- content: transcription.trim()
+ groqConversationHistory.push({
+ role: "user",
+ content: transcription.trim(),
+ });
+
+ const trimmedHistory = trimConversationHistoryForGemma(
+ groqConversationHistory,
+ 42000,
+ );
+
+ try {
+ const ai = new GoogleGenAI({ apiKey: apiKey });
+
+ const messages = trimmedHistory.map((msg) => ({
+ role: msg.role === "assistant" ? "model" : "user",
+ parts: [{ text: msg.content }],
+ }));
+
+ const systemPrompt = currentSystemPrompt || "You are a helpful assistant.";
+ const messagesWithSystem = [
+ { role: "user", parts: [{ text: systemPrompt }] },
+ {
+ role: "model",
+ parts: [{ text: "Understood. I will follow these instructions." }],
+ },
+ ...messages,
+ ];
+
+ const response = await ai.models.generateContentStream({
+ model: "gemma-3-27b-it",
+ contents: messagesWithSystem,
});
- const trimmedHistory = trimConversationHistoryForGemma(groqConversationHistory, 42000);
+ let fullText = "";
+ let isFirst = true;
- try {
- const ai = new GoogleGenAI({ apiKey: apiKey });
-
- const messages = trimmedHistory.map(msg => ({
- role: msg.role === 'assistant' ? 'model' : 'user',
- parts: [{ text: msg.content }]
- }));
-
- const systemPrompt = currentSystemPrompt || 'You are a helpful assistant.';
- const messagesWithSystem = [
- { role: 'user', parts: [{ text: systemPrompt }] },
- { role: 'model', parts: [{ text: 'Understood. I will follow these instructions.' }] },
- ...messages
- ];
-
- const response = await ai.models.generateContentStream({
- model: 'gemma-3-27b-it',
- contents: messagesWithSystem,
- });
-
- let fullText = '';
- let isFirst = true;
-
- for await (const chunk of response) {
- const chunkText = chunk.text;
- if (chunkText) {
- fullText += chunkText;
- sendToRenderer(isFirst ? 'new-response' : 'update-response', fullText);
- isFirst = false;
- }
- }
-
- const systemPromptChars = (currentSystemPrompt || 'You are a helpful assistant.').length;
- const historyChars = trimmedHistory.reduce((sum, msg) => sum + (msg.content || '').length, 0);
- const inputChars = systemPromptChars + historyChars;
- const outputChars = fullText.length;
-
- incrementCharUsage('gemini', 'gemma-3-27b-it', inputChars + outputChars);
-
- if (fullText.trim()) {
- groqConversationHistory.push({
- role: 'assistant',
- content: fullText.trim()
- });
-
- if (groqConversationHistory.length > 40) {
- groqConversationHistory = groqConversationHistory.slice(-40);
- }
-
- saveConversationTurn(transcription, fullText);
- }
-
- console.log('Gemma response completed');
- sendToRenderer('update-status', 'Listening...');
-
- } catch (error) {
- console.error('Error calling Gemma API:', error);
- sendToRenderer('update-status', 'Gemma error: ' + error.message);
+ for await (const chunk of response) {
+ const chunkText = chunk.text;
+ if (chunkText) {
+ fullText += chunkText;
+ sendToRenderer(isFirst ? "new-response" : "update-response", fullText);
+ isFirst = false;
+ }
}
+
+ const systemPromptChars = (
+ currentSystemPrompt || "You are a helpful assistant."
+ ).length;
+ const historyChars = trimmedHistory.reduce(
+ (sum, msg) => sum + (msg.content || "").length,
+ 0,
+ );
+ const inputChars = systemPromptChars + historyChars;
+ const outputChars = fullText.length;
+
+ incrementCharUsage("gemini", "gemma-3-27b-it", inputChars + outputChars);
+
+ if (fullText.trim()) {
+ groqConversationHistory.push({
+ role: "assistant",
+ content: fullText.trim(),
+ });
+
+ if (groqConversationHistory.length > 40) {
+ groqConversationHistory = groqConversationHistory.slice(-40);
+ }
+
+ saveConversationTurn(transcription, fullText);
+ }
+
+ console.log("Gemma response completed");
+ sendToRenderer("update-status", "Listening...");
+ } catch (error) {
+ console.error("Error calling Gemma API:", error);
+ sendToRenderer("update-status", "Gemma error: " + error.message);
+ }
}
-async function initializeGeminiSession(apiKey, customPrompt = '', profile = 'interview', language = 'en-US', isReconnect = false) {
- if (isInitializingSession) {
- console.log('Session initialization already in progress');
- return false;
- }
+async function initializeGeminiSession(
+ apiKey,
+ customPrompt = "",
+ profile = "interview",
+ language = "en-US",
+ isReconnect = false,
+) {
+ if (isInitializingSession) {
+ console.log("Session initialization already in progress");
+ return false;
+ }
- isInitializingSession = true;
- if (!isReconnect) {
- sendToRenderer('session-initializing', true);
- }
+ isInitializingSession = true;
+ if (!isReconnect) {
+ sendToRenderer("session-initializing", true);
+ }
- // Store params for reconnection
- if (!isReconnect) {
- sessionParams = { apiKey, customPrompt, profile, language };
- reconnectAttempts = 0;
- }
-
- // Load response provider preference
- if (!isReconnect) {
- const { getPreferences } = require('../storage');
- const prefs = getPreferences();
- currentResponseProvider = prefs.responseProvider || 'gemini';
- console.log('🔧 Response provider set to:', currentResponseProvider);
- }
+ // Store params for reconnection
+ if (!isReconnect) {
+ sessionParams = { apiKey, customPrompt, profile, language };
+ reconnectAttempts = 0;
+ }
- const client = new GoogleGenAI({
- vertexai: false,
- apiKey: apiKey,
- httpOptions: { apiVersion: 'v1alpha' },
+ // Load response provider preference
+ if (!isReconnect) {
+ const { getPreferences } = require("../storage");
+ const prefs = getPreferences();
+ currentResponseProvider = prefs.responseProvider || "gemini";
+ console.log("🔧 Response provider set to:", currentResponseProvider);
+ }
+
+ const client = new GoogleGenAI({
+ vertexai: false,
+ apiKey: apiKey,
+ httpOptions: { apiVersion: "v1alpha" },
+ });
+
+ // Get enabled tools first to determine Google Search status
+ const enabledTools = await getEnabledTools();
+ const googleSearchEnabled = enabledTools.some((tool) => tool.googleSearch);
+
+ const systemPrompt = getSystemPrompt(
+ profile,
+ customPrompt,
+ googleSearchEnabled,
+ );
+ currentSystemPrompt = systemPrompt; // Store for Groq
+
+ // Initialize new conversation session only on first connect
+ if (!isReconnect) {
+ initializeNewSession(profile, customPrompt);
+ }
+
+ try {
+ const session = await client.live.connect({
+ model: "gemini-2.5-flash-native-audio-preview-09-2025",
+ callbacks: {
+ onopen: function () {
+ sendToRenderer("update-status", "Live session connected");
+ },
+ onmessage: function (message) {
+ console.log("----------------", message);
+
+ // Handle input transcription (what was spoken)
+ if (message.serverContent?.inputTranscription?.results) {
+ const transcribed = formatSpeakerResults(
+ message.serverContent.inputTranscription.results,
+ );
+ console.log("Got transcription (results):", transcribed);
+ currentTranscription += transcribed;
+ } else if (message.serverContent?.inputTranscription?.text) {
+ const text = message.serverContent.inputTranscription.text;
+ if (text.trim() !== "") {
+ console.log("Got transcription (text):", text);
+ currentTranscription += text;
+ }
+ }
+
+ // DISABLED: Gemini's outputTranscription - using Groq for faster responses instead
+ // if (message.serverContent?.outputTranscription?.text) { ... }
+
+ if (message.serverContent?.generationComplete) {
+ console.log(
+ "Generation complete. Current transcription:",
+ `"${currentTranscription}"`,
+ );
+ if (currentTranscription.trim() !== "") {
+ // Use explicit user choice for response provider
+ if (currentResponseProvider === "openai-compatible") {
+ if (hasOpenAICompatibleConfig()) {
+ console.log(
+ "Sending to OpenAI-compatible API (user selected)",
+ );
+ sendToOpenAICompatible(currentTranscription);
+ } else {
+ console.log(
+ "OpenAI-compatible selected but not configured, falling back to Gemini",
+ );
+ sendToGemma(currentTranscription);
+ }
+ } else if (currentResponseProvider === "groq") {
+ if (hasGroqKey()) {
+ console.log("Sending to Groq (user selected)");
+ sendToGroq(currentTranscription);
+ } else {
+ console.log(
+ "Groq selected but not configured, falling back to Gemini",
+ );
+ sendToGemma(currentTranscription);
+ }
+ } else {
+ console.log("Sending to Gemini (user selected)");
+ sendToGemma(currentTranscription);
+ }
+ currentTranscription = "";
+ } else {
+ console.log("Transcription is empty, not sending to LLM");
+ }
+ messageBuffer = "";
+ }
+
+ if (message.serverContent?.turnComplete) {
+ console.log("Turn complete");
+ sendToRenderer("update-status", "Listening...");
+ }
+ },
+ onerror: function (e) {
+ console.log("Session error:", e.message);
+ sendToRenderer("update-status", "Error: " + e.message);
+ },
+ onclose: function (e) {
+ console.log("Session closed:", e.reason);
+
+ // Don't reconnect if user intentionally closed
+ if (isUserClosing) {
+ isUserClosing = false;
+ sendToRenderer("update-status", "Session closed");
+ return;
+ }
+
+ // Attempt reconnection
+ if (sessionParams && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
+ attemptReconnect();
+ } else {
+ sendToRenderer("update-status", "Session closed");
+ }
+ },
+ },
+ config: {
+ responseModalities: [Modality.AUDIO],
+ proactivity: { proactiveAudio: false },
+ outputAudioTranscription: {},
+ inputAudioTranscription: {},
+ tools: enabledTools,
+ contextWindowCompression: { slidingWindow: {} },
+ speechConfig: { languageCode: language },
+ systemInstruction: {
+ parts: [{ text: systemPrompt }],
+ },
+ },
});
- // Get enabled tools first to determine Google Search status
- const enabledTools = await getEnabledTools();
- const googleSearchEnabled = enabledTools.some(tool => tool.googleSearch);
-
- const systemPrompt = getSystemPrompt(profile, customPrompt, googleSearchEnabled);
- currentSystemPrompt = systemPrompt; // Store for Groq
-
- // Initialize new conversation session only on first connect
+ isInitializingSession = false;
if (!isReconnect) {
- initializeNewSession(profile, customPrompt);
+ sendToRenderer("session-initializing", false);
}
-
- try {
- const session = await client.live.connect({
- model: 'gemini-2.5-flash-native-audio-preview-09-2025',
- callbacks: {
- onopen: function () {
- sendToRenderer('update-status', 'Live session connected');
- },
- onmessage: function (message) {
- console.log('----------------', message);
-
- // Handle input transcription (what was spoken)
- if (message.serverContent?.inputTranscription?.results) {
- const transcribed = formatSpeakerResults(message.serverContent.inputTranscription.results);
- console.log('Got transcription (results):', transcribed);
- currentTranscription += transcribed;
- } else if (message.serverContent?.inputTranscription?.text) {
- const text = message.serverContent.inputTranscription.text;
- if (text.trim() !== '') {
- console.log('Got transcription (text):', text);
- currentTranscription += text;
- }
- }
-
- // DISABLED: Gemini's outputTranscription - using Groq for faster responses instead
- // if (message.serverContent?.outputTranscription?.text) { ... }
-
- if (message.serverContent?.generationComplete) {
- console.log('Generation complete. Current transcription:', `"${currentTranscription}"`);
- if (currentTranscription.trim() !== '') {
- // Use explicit user choice for response provider
- if (currentResponseProvider === 'openai-compatible') {
- if (hasOpenAICompatibleConfig()) {
- console.log('Sending to OpenAI-compatible API (user selected)');
- sendToOpenAICompatible(currentTranscription);
- } else {
- console.log('OpenAI-compatible selected but not configured, falling back to Gemini');
- sendToGemma(currentTranscription);
- }
- } else if (currentResponseProvider === 'groq') {
- if (hasGroqKey()) {
- console.log('Sending to Groq (user selected)');
- sendToGroq(currentTranscription);
- } else {
- console.log('Groq selected but not configured, falling back to Gemini');
- sendToGemma(currentTranscription);
- }
- } else {
- console.log('Sending to Gemini (user selected)');
- sendToGemma(currentTranscription);
- }
- currentTranscription = '';
- } else {
- console.log('Transcription is empty, not sending to LLM');
- }
- messageBuffer = '';
- }
-
- if (message.serverContent?.turnComplete) {
- console.log('Turn complete');
- sendToRenderer('update-status', 'Listening...');
- }
- },
- onerror: function (e) {
- console.log('Session error:', e.message);
- sendToRenderer('update-status', 'Error: ' + e.message);
- },
- onclose: function (e) {
- console.log('Session closed:', e.reason);
-
- // Don't reconnect if user intentionally closed
- if (isUserClosing) {
- isUserClosing = false;
- sendToRenderer('update-status', 'Session closed');
- return;
- }
-
- // Attempt reconnection
- if (sessionParams && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
- attemptReconnect();
- } else {
- sendToRenderer('update-status', 'Session closed');
- }
- },
- },
- config: {
- responseModalities: [Modality.AUDIO],
- proactivity: { proactiveAudio: false },
- outputAudioTranscription: {},
- inputAudioTranscription: {},
- tools: enabledTools,
- contextWindowCompression: { slidingWindow: {} },
- speechConfig: { languageCode: language },
- systemInstruction: {
- parts: [{ text: systemPrompt }],
- },
- },
- });
-
- isInitializingSession = false;
- if (!isReconnect) {
- sendToRenderer('session-initializing', false);
- }
- return session;
- } catch (error) {
- console.error('Failed to initialize Gemini session:', error);
- isInitializingSession = false;
- if (!isReconnect) {
- sendToRenderer('session-initializing', false);
- }
- return null;
+ return session;
+ } catch (error) {
+ console.error("Failed to initialize Gemini session:", error);
+ isInitializingSession = false;
+ if (!isReconnect) {
+ sendToRenderer("session-initializing", false);
}
+ return null;
+ }
}
async function attemptReconnect() {
- reconnectAttempts++;
- console.log(`Reconnection attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`);
+ reconnectAttempts++;
+ console.log(
+ `Reconnection attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`,
+ );
- // Clear stale buffers
- messageBuffer = '';
- currentTranscription = '';
- // Don't reset groqConversationHistory to preserve context across reconnects
+ // Clear stale buffers
+ messageBuffer = "";
+ currentTranscription = "";
+ // Don't reset groqConversationHistory to preserve context across reconnects
- sendToRenderer('update-status', `Reconnecting... (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
+ sendToRenderer(
+ "update-status",
+ `Reconnecting... (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`,
+ );
- // Wait before attempting
- await new Promise(resolve => setTimeout(resolve, RECONNECT_DELAY));
+ // Wait before attempting
+ await new Promise((resolve) => setTimeout(resolve, RECONNECT_DELAY));
- try {
- const session = await initializeGeminiSession(
- sessionParams.apiKey,
- sessionParams.customPrompt,
- sessionParams.profile,
- sessionParams.language,
- true // isReconnect
- );
+ try {
+ const session = await initializeGeminiSession(
+ sessionParams.apiKey,
+ sessionParams.customPrompt,
+ sessionParams.profile,
+ sessionParams.language,
+ true, // isReconnect
+ );
- if (session && global.geminiSessionRef) {
- global.geminiSessionRef.current = session;
+ if (session && global.geminiSessionRef) {
+ global.geminiSessionRef.current = session;
- // Restore context from conversation history via text message
- const contextMessage = buildContextMessage();
- if (contextMessage) {
- try {
- console.log('Restoring conversation context...');
- await session.sendRealtimeInput({ text: contextMessage });
- } catch (contextError) {
- console.error('Failed to restore context:', contextError);
- // Continue without context - better than failing
- }
- }
-
- // Don't reset reconnectAttempts here - let it reset on next fresh session
- sendToRenderer('update-status', 'Reconnected! Listening...');
- console.log('Session reconnected successfully');
- return true;
+ // Restore context from conversation history via text message
+ const contextMessage = buildContextMessage();
+ if (contextMessage) {
+ try {
+ console.log("Restoring conversation context...");
+ await session.sendRealtimeInput({ text: contextMessage });
+ } catch (contextError) {
+ console.error("Failed to restore context:", contextError);
+ // Continue without context - better than failing
}
- } catch (error) {
- console.error(`Reconnection attempt ${reconnectAttempts} failed:`, error);
- }
+ }
- // If we still have attempts left, try again
- if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
- return attemptReconnect();
+ // Don't reset reconnectAttempts here - let it reset on next fresh session
+ sendToRenderer("update-status", "Reconnected! Listening...");
+ console.log("Session reconnected successfully");
+ return true;
}
+ } catch (error) {
+ console.error(`Reconnection attempt ${reconnectAttempts} failed:`, error);
+ }
- // Max attempts reached - notify frontend
- console.log('Max reconnection attempts reached');
- sendToRenderer('reconnect-failed', {
- message: 'Tried 3 times to reconnect. Must be upstream/network issues. Try restarting or download updated app from site.',
- });
- sessionParams = null;
- return false;
+ // If we still have attempts left, try again
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
+ return attemptReconnect();
+ }
+
+ // Max attempts reached - notify frontend
+ console.log("Max reconnection attempts reached");
+ sendToRenderer("reconnect-failed", {
+ message:
+ "Tried 3 times to reconnect. Must be upstream/network issues. Try restarting or download updated app from site.",
+ });
+ sessionParams = null;
+ return false;
}
function killExistingSystemAudioDump() {
- return new Promise(resolve => {
- console.log('Checking for existing SystemAudioDump processes...');
+ return new Promise((resolve) => {
+ console.log("Checking for existing SystemAudioDump processes...");
- // Kill any existing SystemAudioDump processes
- const killProc = spawn('pkill', ['-f', 'SystemAudioDump'], {
- stdio: 'ignore',
- });
-
- killProc.on('close', code => {
- if (code === 0) {
- console.log('Killed existing SystemAudioDump processes');
- } else {
- console.log('No existing SystemAudioDump processes found');
- }
- resolve();
- });
-
- killProc.on('error', err => {
- console.log('Error checking for existing processes (this is normal):', err.message);
- resolve();
- });
-
- // Timeout after 2 seconds
- setTimeout(() => {
- killProc.kill();
- resolve();
- }, 2000);
+ // Kill any existing SystemAudioDump processes
+ const killProc = spawn("pkill", ["-f", "SystemAudioDump"], {
+ stdio: "ignore",
});
+
+ killProc.on("close", (code) => {
+ if (code === 0) {
+ console.log("Killed existing SystemAudioDump processes");
+ } else {
+ console.log("No existing SystemAudioDump processes found");
+ }
+ resolve();
+ });
+
+ killProc.on("error", (err) => {
+ console.log(
+ "Error checking for existing processes (this is normal):",
+ err.message,
+ );
+ resolve();
+ });
+
+ // Timeout after 2 seconds
+ setTimeout(() => {
+ killProc.kill();
+ resolve();
+ }, 2000);
+ });
}
async function startMacOSAudioCapture(geminiSessionRef) {
- if (process.platform !== 'darwin') return false;
+ if (process.platform !== "darwin") return false;
- // Kill any existing SystemAudioDump processes first
- await killExistingSystemAudioDump();
+ // Kill any existing SystemAudioDump processes first
+ await killExistingSystemAudioDump();
- console.log('Starting macOS audio capture with SystemAudioDump...');
+ console.log("Starting macOS audio capture with SystemAudioDump...");
- const { app } = require('electron');
- const path = require('path');
+ const { app } = require("electron");
+ const path = require("path");
- let systemAudioPath;
- if (app.isPackaged) {
- systemAudioPath = path.join(process.resourcesPath, 'SystemAudioDump');
- } else {
- systemAudioPath = path.join(__dirname, '../assets', 'SystemAudioDump');
+ let systemAudioPath;
+ if (app.isPackaged) {
+ systemAudioPath = path.join(process.resourcesPath, "SystemAudioDump");
+ } else {
+ systemAudioPath = path.join(__dirname, "../assets", "SystemAudioDump");
+ }
+
+ console.log("SystemAudioDump path:", systemAudioPath);
+
+ const spawnOptions = {
+ stdio: ["ignore", "pipe", "pipe"],
+ env: {
+ ...process.env,
+ },
+ };
+
+ systemAudioProc = spawn(systemAudioPath, [], spawnOptions);
+
+ if (!systemAudioProc.pid) {
+ console.error("Failed to start SystemAudioDump");
+ return false;
+ }
+
+ console.log("SystemAudioDump started with PID:", systemAudioProc.pid);
+
+ const CHUNK_DURATION = 0.1;
+ const SAMPLE_RATE = 24000;
+ const BYTES_PER_SAMPLE = 2;
+ const CHANNELS = 2;
+ const CHUNK_SIZE = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_DURATION;
+
+ let audioBuffer = Buffer.alloc(0);
+
+ systemAudioProc.stdout.on("data", (data) => {
+ audioBuffer = Buffer.concat([audioBuffer, data]);
+
+ while (audioBuffer.length >= CHUNK_SIZE) {
+ const chunk = audioBuffer.slice(0, CHUNK_SIZE);
+ audioBuffer = audioBuffer.slice(CHUNK_SIZE);
+
+ const monoChunk = CHANNELS === 2 ? convertStereoToMono(chunk) : chunk;
+
+ if (currentProviderMode === "local") {
+ getLocalAi().processLocalAudio(monoChunk);
+ } else {
+ const base64Data = monoChunk.toString("base64");
+ sendAudioToGemini(base64Data, geminiSessionRef);
+ }
+
+ if (process.env.DEBUG_AUDIO) {
+ console.log(`Processed audio chunk: ${chunk.length} bytes`);
+ saveDebugAudio(monoChunk, "system_audio");
+ }
}
- console.log('SystemAudioDump path:', systemAudioPath);
-
- const spawnOptions = {
- stdio: ['ignore', 'pipe', 'pipe'],
- env: {
- ...process.env,
- },
- };
-
- systemAudioProc = spawn(systemAudioPath, [], spawnOptions);
-
- if (!systemAudioProc.pid) {
- console.error('Failed to start SystemAudioDump');
- return false;
+ const maxBufferSize = SAMPLE_RATE * BYTES_PER_SAMPLE * 1;
+ if (audioBuffer.length > maxBufferSize) {
+ audioBuffer = audioBuffer.slice(-maxBufferSize);
}
+ });
- console.log('SystemAudioDump started with PID:', systemAudioProc.pid);
+ systemAudioProc.stderr.on("data", (data) => {
+ console.error("SystemAudioDump stderr:", data.toString());
+ });
- const CHUNK_DURATION = 0.1;
- const SAMPLE_RATE = 24000;
- const BYTES_PER_SAMPLE = 2;
- const CHANNELS = 2;
- const CHUNK_SIZE = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_DURATION;
+ systemAudioProc.on("close", (code) => {
+ console.log("SystemAudioDump process closed with code:", code);
+ systemAudioProc = null;
+ });
- let audioBuffer = Buffer.alloc(0);
+ systemAudioProc.on("error", (err) => {
+ console.error("SystemAudioDump process error:", err);
+ systemAudioProc = null;
+ });
- systemAudioProc.stdout.on('data', data => {
- audioBuffer = Buffer.concat([audioBuffer, data]);
-
- while (audioBuffer.length >= CHUNK_SIZE) {
- const chunk = audioBuffer.slice(0, CHUNK_SIZE);
- audioBuffer = audioBuffer.slice(CHUNK_SIZE);
-
- const monoChunk = CHANNELS === 2 ? convertStereoToMono(chunk) : chunk;
-
- if (currentProviderMode === 'local') {
- getLocalAi().processLocalAudio(monoChunk);
- } else {
- const base64Data = monoChunk.toString('base64');
- sendAudioToGemini(base64Data, geminiSessionRef);
- }
-
- if (process.env.DEBUG_AUDIO) {
- console.log(`Processed audio chunk: ${chunk.length} bytes`);
- saveDebugAudio(monoChunk, 'system_audio');
- }
- }
-
- const maxBufferSize = SAMPLE_RATE * BYTES_PER_SAMPLE * 1;
- if (audioBuffer.length > maxBufferSize) {
- audioBuffer = audioBuffer.slice(-maxBufferSize);
- }
- });
-
- systemAudioProc.stderr.on('data', data => {
- console.error('SystemAudioDump stderr:', data.toString());
- });
-
- systemAudioProc.on('close', code => {
- console.log('SystemAudioDump process closed with code:', code);
- systemAudioProc = null;
- });
-
- systemAudioProc.on('error', err => {
- console.error('SystemAudioDump process error:', err);
- systemAudioProc = null;
- });
-
- return true;
+ return true;
}
function convertStereoToMono(stereoBuffer) {
- const samples = stereoBuffer.length / 4;
- const monoBuffer = Buffer.alloc(samples * 2);
+ const samples = stereoBuffer.length / 4;
+ const monoBuffer = Buffer.alloc(samples * 2);
- for (let i = 0; i < samples; i++) {
- const leftSample = stereoBuffer.readInt16LE(i * 4);
- monoBuffer.writeInt16LE(leftSample, i * 2);
- }
+ for (let i = 0; i < samples; i++) {
+ const leftSample = stereoBuffer.readInt16LE(i * 4);
+ monoBuffer.writeInt16LE(leftSample, i * 2);
+ }
- return monoBuffer;
+ return monoBuffer;
}
function stopMacOSAudioCapture() {
- if (systemAudioProc) {
- console.log('Stopping SystemAudioDump...');
- systemAudioProc.kill('SIGTERM');
- systemAudioProc = null;
- }
+ if (systemAudioProc) {
+ console.log("Stopping SystemAudioDump...");
+ systemAudioProc.kill("SIGTERM");
+ systemAudioProc = null;
+ }
}
async function sendAudioToGemini(base64Data, geminiSessionRef) {
- if (!geminiSessionRef.current) return;
+ if (!geminiSessionRef.current) return;
- try {
- process.stdout.write('.');
- await geminiSessionRef.current.sendRealtimeInput({
- audio: {
- data: base64Data,
- mimeType: 'audio/pcm;rate=24000',
- },
- });
- } catch (error) {
- console.error('Error sending audio to Gemini:', error);
- }
+ try {
+ process.stdout.write(".");
+ await geminiSessionRef.current.sendRealtimeInput({
+ audio: {
+ data: base64Data,
+ mimeType: "audio/pcm;rate=24000",
+ },
+ });
+ } catch (error) {
+ console.error("Error sending audio to Gemini:", error);
+ }
}
async function sendImageToGeminiHttp(base64Data, prompt) {
- // Get available model based on rate limits
- const model = getAvailableModel();
+ // Get available model based on rate limits
+ const model = getAvailableModel();
- const apiKey = getApiKey();
- if (!apiKey) {
- return { success: false, error: 'No API key configured' };
+ const apiKey = getApiKey();
+ if (!apiKey) {
+ return { success: false, error: "No API key configured" };
+ }
+
+ try {
+ const ai = new GoogleGenAI({ apiKey: apiKey });
+
+ const contents = [
+ {
+ inlineData: {
+ mimeType: "image/jpeg",
+ data: base64Data,
+ },
+ },
+ { text: prompt },
+ ];
+
+ console.log(`Sending image to ${model} (streaming)...`);
+ const response = await ai.models.generateContentStream({
+ model: model,
+ contents: contents,
+ config: {
+ systemInstruction: currentSystemPrompt || undefined,
+ },
+ });
+
+ // Increment count after successful call
+ incrementLimitCount(model);
+
+ // Stream the response
+ let fullText = "";
+ let isFirst = true;
+ for await (const chunk of response) {
+ const chunkText = chunk.text;
+ if (chunkText) {
+ fullText += chunkText;
+ // Send to renderer - new response for first chunk, update for subsequent
+ sendToRenderer(isFirst ? "new-response" : "update-response", fullText);
+ isFirst = false;
+ }
}
- try {
- const ai = new GoogleGenAI({ apiKey: apiKey });
+ console.log(`Image response completed from ${model}`);
- const contents = [
- {
- inlineData: {
- mimeType: 'image/jpeg',
- data: base64Data,
- },
- },
- { text: prompt },
- ];
+ // Save screen analysis to history
+ saveScreenAnalysis(prompt, fullText, model);
- console.log(`Sending image to ${model} (streaming)...`);
- const response = await ai.models.generateContentStream({
- model: model,
- contents: contents,
- });
-
- // Increment count after successful call
- incrementLimitCount(model);
-
- // Stream the response
- let fullText = '';
- let isFirst = true;
- for await (const chunk of response) {
- const chunkText = chunk.text;
- if (chunkText) {
- fullText += chunkText;
- // Send to renderer - new response for first chunk, update for subsequent
- sendToRenderer(isFirst ? 'new-response' : 'update-response', fullText);
- isFirst = false;
- }
- }
-
- console.log(`Image response completed from ${model}`);
-
- // Save screen analysis to history
- saveScreenAnalysis(prompt, fullText, model);
-
- return { success: true, text: fullText, model: model };
- } catch (error) {
- console.error('Error sending image to Gemini HTTP:', error);
- return { success: false, error: error.message };
- }
+ return { success: true, text: fullText, model: model };
+ } catch (error) {
+ console.error("Error sending image to Gemini HTTP:", error);
+ return { success: false, error: error.message };
+ }
}
function setupGeminiIpcHandlers(geminiSessionRef) {
- // Store the geminiSessionRef globally for reconnection access
- global.geminiSessionRef = geminiSessionRef;
+ // Store the geminiSessionRef globally for reconnection access
+ global.geminiSessionRef = geminiSessionRef;
- ipcMain.handle('initialize-gemini', async (event, apiKey, customPrompt, profile = 'interview', language = 'en-US') => {
- currentProviderMode = 'byok';
- const session = await initializeGeminiSession(apiKey, customPrompt, profile, language);
- if (session) {
- geminiSessionRef.current = session;
- return true;
- }
- return false;
- });
+ ipcMain.handle(
+ "initialize-gemini",
+ async (
+ event,
+ apiKey,
+ customPrompt,
+ profile = "interview",
+ language = "en-US",
+ ) => {
+ currentProviderMode = "byok";
+ const session = await initializeGeminiSession(
+ apiKey,
+ customPrompt,
+ profile,
+ language,
+ );
+ if (session) {
+ geminiSessionRef.current = session;
+ return true;
+ }
+ return false;
+ },
+ );
- ipcMain.handle('initialize-local', async (event, ollamaHost, ollamaModel, whisperModel, profile, customPrompt) => {
- currentProviderMode = 'local';
- const success = await getLocalAi().initializeLocalSession(ollamaHost, ollamaModel, whisperModel, profile, customPrompt);
- if (!success) {
- currentProviderMode = 'byok';
- }
- return success;
- });
+ ipcMain.handle(
+ "initialize-local",
+ async (
+ event,
+ ollamaHost,
+ ollamaModel,
+ whisperModel,
+ profile,
+ customPrompt,
+ ) => {
+ currentProviderMode = "local";
+ const success = await getLocalAi().initializeLocalSession(
+ ollamaHost,
+ ollamaModel,
+ whisperModel,
+ profile,
+ customPrompt,
+ );
+ if (!success) {
+ currentProviderMode = "byok";
+ }
+ return success;
+ },
+ );
- ipcMain.handle('send-audio-content', async (event, { data, mimeType }) => {
- if (currentProviderMode === 'local') {
- try {
- const pcmBuffer = Buffer.from(data, 'base64');
- getLocalAi().processLocalAudio(pcmBuffer);
- return { success: true };
- } catch (error) {
- console.error('Error sending local audio:', error);
- return { success: false, error: error.message };
- }
- }
- if (!geminiSessionRef.current) return { success: false, error: 'No active Gemini session' };
+ ipcMain.handle("send-audio-content", async (event, { data, mimeType }) => {
+ if (currentProviderMode === "local") {
+ try {
+ const pcmBuffer = Buffer.from(data, "base64");
+ getLocalAi().processLocalAudio(pcmBuffer);
+ return { success: true };
+ } catch (error) {
+ console.error("Error sending local audio:", error);
+ return { success: false, error: error.message };
+ }
+ }
+ if (!geminiSessionRef.current)
+ return { success: false, error: "No active Gemini session" };
+ try {
+ process.stdout.write(".");
+ await geminiSessionRef.current.sendRealtimeInput({
+ audio: { data: data, mimeType: mimeType },
+ });
+ return { success: true };
+ } catch (error) {
+ console.error("Error sending system audio:", error);
+ return { success: false, error: error.message };
+ }
+ });
+
+ // Handle microphone audio on a separate channel
+ ipcMain.handle(
+ "send-mic-audio-content",
+ async (event, { data, mimeType }) => {
+ if (currentProviderMode === "local") {
try {
- process.stdout.write('.');
- await geminiSessionRef.current.sendRealtimeInput({
- audio: { data: data, mimeType: mimeType },
- });
- return { success: true };
+ const pcmBuffer = Buffer.from(data, "base64");
+ getLocalAi().processLocalAudio(pcmBuffer);
+ return { success: true };
} catch (error) {
- console.error('Error sending system audio:', error);
- return { success: false, error: error.message };
+ console.error("Error sending local mic audio:", error);
+ return { success: false, error: error.message };
}
- });
+ }
+ if (!geminiSessionRef.current)
+ return { success: false, error: "No active Gemini session" };
+ try {
+ process.stdout.write(",");
+ await geminiSessionRef.current.sendRealtimeInput({
+ audio: { data: data, mimeType: mimeType },
+ });
+ return { success: true };
+ } catch (error) {
+ console.error("Error sending mic audio:", error);
+ return { success: false, error: error.message };
+ }
+ },
+ );
- // Handle microphone audio on a separate channel
- ipcMain.handle('send-mic-audio-content', async (event, { data, mimeType }) => {
- if (currentProviderMode === 'local') {
- try {
- const pcmBuffer = Buffer.from(data, 'base64');
- getLocalAi().processLocalAudio(pcmBuffer);
- return { success: true };
- } catch (error) {
- console.error('Error sending local mic audio:', error);
- return { success: false, error: error.message };
- }
+ ipcMain.handle("send-image-content", async (event, { data, prompt }) => {
+ try {
+ if (!data || typeof data !== "string") {
+ console.error("Invalid image data received");
+ return { success: false, error: "Invalid image data" };
+ }
+
+ const buffer = Buffer.from(data, "base64");
+
+ if (buffer.length < 1000) {
+ console.error(`Image buffer too small: ${buffer.length} bytes`);
+ return { success: false, error: "Image buffer too small" };
+ }
+
+ process.stdout.write("!");
+
+ if (currentProviderMode === "local") {
+ const result = await getLocalAi().sendLocalImage(data, prompt);
+ return result;
+ }
+
+ // Use HTTP API instead of realtime session
+ const result = await sendImageToGeminiHttp(data, prompt);
+ return result;
+ } catch (error) {
+ console.error("Error sending image:", error);
+ return { success: false, error: error.message };
+ }
+ });
+
+ ipcMain.handle("send-text-message", async (event, text) => {
+ if (!text || typeof text !== "string" || text.trim().length === 0) {
+ return { success: false, error: "Invalid text message" };
+ }
+
+ if (currentProviderMode === "local") {
+ try {
+ console.log("Sending text to local Ollama:", text);
+ return await getLocalAi().sendLocalText(text.trim());
+ } catch (error) {
+ console.error("Error sending local text:", error);
+ return { success: false, error: error.message };
+ }
+ }
+
+ if (!geminiSessionRef.current)
+ return { success: false, error: "No active Gemini session" };
+
+ try {
+ console.log("Sending text message:", text);
+
+ // Use explicit user choice for response provider
+ if (currentResponseProvider === "openai-compatible") {
+ if (hasOpenAICompatibleConfig()) {
+ sendToOpenAICompatible(text.trim());
+ } else {
+ sendToGemma(text.trim());
}
- if (!geminiSessionRef.current) return { success: false, error: 'No active Gemini session' };
- try {
- process.stdout.write(',');
- await geminiSessionRef.current.sendRealtimeInput({
- audio: { data: data, mimeType: mimeType },
- });
- return { success: true };
- } catch (error) {
- console.error('Error sending mic audio:', error);
- return { success: false, error: error.message };
+ } else if (currentResponseProvider === "groq") {
+ if (hasGroqKey()) {
+ sendToGroq(text.trim());
+ } else {
+ sendToGemma(text.trim());
}
- });
+ } else {
+ sendToGemma(text.trim());
+ }
- ipcMain.handle('send-image-content', async (event, { data, prompt }) => {
- try {
- if (!data || typeof data !== 'string') {
- console.error('Invalid image data received');
- return { success: false, error: 'Invalid image data' };
- }
+ await geminiSessionRef.current.sendRealtimeInput({ text: text.trim() });
+ return { success: true };
+ } catch (error) {
+ console.error("Error sending text:", error);
+ return { success: false, error: error.message };
+ }
+ });
- const buffer = Buffer.from(data, 'base64');
+ // Expand last response — continues the conversation, not a re-generation
+ ipcMain.handle(
+ "expand-last-response",
+ async (event, { previousResponse, originalContext }) => {
+ if (!previousResponse || typeof previousResponse !== "string") {
+ return { success: false, error: "No previous response to expand" };
+ }
- if (buffer.length < 1000) {
- console.error(`Image buffer too small: ${buffer.length} bytes`);
- return { success: false, error: 'Image buffer too small' };
- }
+ const expansionPrompt = `You previously gave this brief answer:\n\n---\n${previousResponse.trim()}\n---\n\nNow expand on your previous answer. Continue your thought and provide a thorough, comprehensive, detailed explanation. Do NOT repeat the brief version — build on it and go deeper. If there is code involved, provide the complete working solution with detailed comments and explain time/space complexity. If there are edge cases, discuss them. Be as detailed as needed.`;
- process.stdout.write('!');
+ try {
+ console.log("Expanding last response...");
- if (currentProviderMode === 'local') {
- const result = await getLocalAi().sendLocalImage(data, prompt);
- return result;
- }
-
- // Use HTTP API instead of realtime session
- const result = await sendImageToGeminiHttp(data, prompt);
- return result;
- } catch (error) {
- console.error('Error sending image:', error);
- return { success: false, error: error.message };
- }
- });
-
- ipcMain.handle('send-text-message', async (event, text) => {
- if (!text || typeof text !== 'string' || text.trim().length === 0) {
- return { success: false, error: 'Invalid text message' };
+ if (currentProviderMode === "local") {
+ return await getLocalAi().sendLocalText(expansionPrompt);
}
- if (currentProviderMode === 'local') {
- try {
- console.log('Sending text to local Ollama:', text);
- return await getLocalAi().sendLocalText(text.trim());
- } catch (error) {
- console.error('Error sending local text:', error);
- return { success: false, error: error.message };
- }
+ // Ensure the previous assistant response is in conversation history
+ // so the LLM sees full context when sendToGroq/sendToGemma add the user message
+ const lastEntry =
+ groqConversationHistory[groqConversationHistory.length - 1];
+ if (!lastEntry || lastEntry.role !== "assistant") {
+ groqConversationHistory.push({
+ role: "assistant",
+ content: previousResponse.trim(),
+ });
}
- if (!geminiSessionRef.current) return { success: false, error: 'No active Gemini session' };
-
- try {
- console.log('Sending text message:', text);
-
- // Use explicit user choice for response provider
- if (currentResponseProvider === 'openai-compatible') {
- if (hasOpenAICompatibleConfig()) {
- sendToOpenAICompatible(text.trim());
- } else {
- sendToGemma(text.trim());
- }
- } else if (currentResponseProvider === 'groq') {
- if (hasGroqKey()) {
- sendToGroq(text.trim());
- } else {
- sendToGemma(text.trim());
- }
- } else {
- sendToGemma(text.trim());
- }
-
- await geminiSessionRef.current.sendRealtimeInput({ text: text.trim() });
- return { success: true };
- } catch (error) {
- console.error('Error sending text:', error);
- return { success: false, error: error.message };
- }
- });
-
- ipcMain.handle('start-macos-audio', async event => {
- if (process.platform !== 'darwin') {
- return {
- success: false,
- error: 'macOS audio capture only available on macOS',
- };
+ // Route to the same provider as normal messages
+ // (each send function pushes the user message to groqConversationHistory itself)
+ if (currentResponseProvider === "openai-compatible") {
+ if (hasOpenAICompatibleConfig()) {
+ sendToOpenAICompatible(expansionPrompt);
+ } else {
+ sendToGemma(expansionPrompt);
+ }
+ } else if (currentResponseProvider === "groq") {
+ if (hasGroqKey()) {
+ sendToGroq(expansionPrompt);
+ } else {
+ sendToGemma(expansionPrompt);
+ }
+ } else {
+ sendToGemma(expansionPrompt);
}
- try {
- const success = await startMacOSAudioCapture(geminiSessionRef);
- return { success };
- } catch (error) {
- console.error('Error starting macOS audio capture:', error);
- return { success: false, error: error.message };
- }
- });
+ return { success: true };
+ } catch (error) {
+ console.error("Error expanding response:", error);
+ return { success: false, error: error.message };
+ }
+ },
+ );
- ipcMain.handle('stop-macos-audio', async event => {
- try {
- stopMacOSAudioCapture();
- return { success: true };
- } catch (error) {
- console.error('Error stopping macOS audio capture:', error);
- return { success: false, error: error.message };
- }
- });
+ ipcMain.handle("start-macos-audio", async (event) => {
+ if (process.platform !== "darwin") {
+ return {
+ success: false,
+ error: "macOS audio capture only available on macOS",
+ };
+ }
- ipcMain.handle('close-session', async event => {
- try {
- stopMacOSAudioCapture();
+ try {
+ const success = await startMacOSAudioCapture(geminiSessionRef);
+ return { success };
+ } catch (error) {
+ console.error("Error starting macOS audio capture:", error);
+ return { success: false, error: error.message };
+ }
+ });
- if (currentProviderMode === 'local') {
- getLocalAi().closeLocalSession();
- currentProviderMode = 'byok';
- return { success: true };
- }
+ ipcMain.handle("stop-macos-audio", async (event) => {
+ try {
+ stopMacOSAudioCapture();
+ return { success: true };
+ } catch (error) {
+ console.error("Error stopping macOS audio capture:", error);
+ return { success: false, error: error.message };
+ }
+ });
- // Set flag to prevent reconnection attempts
- isUserClosing = true;
- sessionParams = null;
+ ipcMain.handle("close-session", async (event) => {
+ try {
+ stopMacOSAudioCapture();
- // Cleanup session
- if (geminiSessionRef.current) {
- await geminiSessionRef.current.close();
- geminiSessionRef.current = null;
- }
+ if (currentProviderMode === "local") {
+ getLocalAi().closeLocalSession();
+ currentProviderMode = "byok";
+ return { success: true };
+ }
- return { success: true };
- } catch (error) {
- console.error('Error closing session:', error);
- return { success: false, error: error.message };
- }
- });
+ // Set flag to prevent reconnection attempts
+ isUserClosing = true;
+ sessionParams = null;
- // Conversation history IPC handlers
- ipcMain.handle('get-current-session', async event => {
- try {
- return { success: true, data: getCurrentSessionData() };
- } catch (error) {
- console.error('Error getting current session:', error);
- return { success: false, error: error.message };
- }
- });
+ // Cleanup session
+ if (geminiSessionRef.current) {
+ await geminiSessionRef.current.close();
+ geminiSessionRef.current = null;
+ }
- ipcMain.handle('start-new-session', async event => {
- try {
- initializeNewSession();
- return { success: true, sessionId: currentSessionId };
- } catch (error) {
- console.error('Error starting new session:', error);
- return { success: false, error: error.message };
- }
- });
+ return { success: true };
+ } catch (error) {
+ console.error("Error closing session:", error);
+ return { success: false, error: error.message };
+ }
+ });
- ipcMain.handle('update-google-search-setting', async (event, enabled) => {
- try {
- console.log('Google Search setting updated to:', enabled);
- // The setting is already saved in localStorage by the renderer
- // This is just for logging/confirmation
- return { success: true };
- } catch (error) {
- console.error('Error updating Google Search setting:', error);
- return { success: false, error: error.message };
- }
- });
+ // Conversation history IPC handlers
+ ipcMain.handle("get-current-session", async (event) => {
+ try {
+ return { success: true, data: getCurrentSessionData() };
+ } catch (error) {
+ console.error("Error getting current session:", error);
+ return { success: false, error: error.message };
+ }
+ });
- // OpenAI-compatible API configuration handlers
- ipcMain.handle('set-openai-compatible-config', async (event, apiKey, baseUrl, model) => {
- try {
- const { setOpenAICompatibleConfig } = require('../storage');
- setOpenAICompatibleConfig(apiKey, baseUrl, model);
- console.log('OpenAI-compatible config saved:', { baseUrl, model: model.substring(0, 30) });
- return { success: true };
- } catch (error) {
- console.error('Error setting OpenAI-compatible config:', error);
- return { success: false, error: error.message };
- }
- });
+ ipcMain.handle("start-new-session", async (event) => {
+ try {
+ initializeNewSession();
+ return { success: true, sessionId: currentSessionId };
+ } catch (error) {
+ console.error("Error starting new session:", error);
+ return { success: false, error: error.message };
+ }
+ });
- ipcMain.handle('get-openai-compatible-config', async (event) => {
- try {
- const config = getOpenAICompatibleConfig();
- return { success: true, config };
- } catch (error) {
- console.error('Error getting OpenAI-compatible config:', error);
- return { success: false, error: error.message };
- }
- });
+ ipcMain.handle("update-google-search-setting", async (event, enabled) => {
+ try {
+ console.log("Google Search setting updated to:", enabled);
+ // The setting is already saved in localStorage by the renderer
+ // This is just for logging/confirmation
+ return { success: true };
+ } catch (error) {
+ console.error("Error updating Google Search setting:", error);
+ return { success: false, error: error.message };
+ }
+ });
+
+ // OpenAI-compatible API configuration handlers
+ ipcMain.handle(
+ "set-openai-compatible-config",
+ async (event, apiKey, baseUrl, model) => {
+ try {
+ const { setOpenAICompatibleConfig } = require("../storage");
+ setOpenAICompatibleConfig(apiKey, baseUrl, model);
+ console.log("OpenAI-compatible config saved:", {
+ baseUrl,
+ model: model.substring(0, 30),
+ });
+ return { success: true };
+ } catch (error) {
+ console.error("Error setting OpenAI-compatible config:", error);
+ return { success: false, error: error.message };
+ }
+ },
+ );
+
+ ipcMain.handle("get-openai-compatible-config", async (event) => {
+ try {
+ const config = getOpenAICompatibleConfig();
+ return { success: true, config };
+ } catch (error) {
+ console.error("Error getting OpenAI-compatible config:", error);
+ return { success: false, error: error.message };
+ }
+ });
}
module.exports = {
- initializeGeminiSession,
- getEnabledTools,
- getStoredSetting,
- sendToRenderer,
- initializeNewSession,
- saveConversationTurn,
- getCurrentSessionData,
- killExistingSystemAudioDump,
- startMacOSAudioCapture,
- convertStereoToMono,
- stopMacOSAudioCapture,
- sendAudioToGemini,
- sendImageToGeminiHttp,
- setupGeminiIpcHandlers,
- formatSpeakerResults,
- hasOpenAICompatibleConfig,
- sendToOpenAICompatible,
+ initializeGeminiSession,
+ getEnabledTools,
+ getStoredSetting,
+ sendToRenderer,
+ initializeNewSession,
+ saveConversationTurn,
+ getCurrentSessionData,
+ killExistingSystemAudioDump,
+ startMacOSAudioCapture,
+ convertStereoToMono,
+ stopMacOSAudioCapture,
+ sendAudioToGemini,
+ sendImageToGeminiHttp,
+ setupGeminiIpcHandlers,
+ formatSpeakerResults,
+ hasOpenAICompatibleConfig,
+ sendToOpenAICompatible,
};
diff --git a/src/utils/prompts.js b/src/utils/prompts.js
index b7160b4..d93e375 100644
--- a/src/utils/prompts.js
+++ b/src/utils/prompts.js
@@ -1,21 +1,45 @@
-const profilePrompts = {
- interview: {
- intro: `You are an AI-powered interview assistant, designed to act as a discreet on-screen teleprompter. Your mission is to help the user excel in their job interview by providing concise, impactful, and ready-to-speak answers or key talking points. Analyze the ongoing interview dialogue and, crucially, the 'User-provided context' below.`,
-
- formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
+const responseModeFormats = {
+ brief: `**RESPONSE FORMAT REQUIREMENTS:**
- Keep responses SHORT and CONCISE (1-3 sentences max)
- Use **markdown formatting** for better readability
- Use **bold** for key points and emphasis
- Use bullet points (-) for lists when appropriate
-- Focus on the most essential information only`,
+- Focus on the most essential information only
+- EXCEPTION: If a coding/algorithm task is detected, ALWAYS provide the complete working code (see CODING TASKS below)`,
- searchUsage: `**SEARCH TOOL USAGE:**
+ detailed: `**RESPONSE FORMAT REQUIREMENTS:**
+- Provide a THOROUGH and COMPREHENSIVE response with full explanations
+- Use **markdown formatting** for better readability
+- Use **bold** for key points and emphasis
+- Use headers (##) to organize sections when appropriate
+- Use bullet points (-) for lists when appropriate
+- Include relevant context, edge cases, and reasoning
+- For technical topics, explain the "why" behind each point
+- No length restriction — be as detailed as needed to fully answer the question`,
+};
+
+const codingAwareness = `**CODING TASKS — CRITICAL INSTRUCTION:**
+When the interviewer/questioner asks to solve a coding problem, implement an algorithm, debug code, do a live coding exercise, open an IDE and write code, or any task that requires a code solution:
+- You MUST provide the ACTUAL COMPLETE WORKING CODE SOLUTION
+- NEVER respond with meta-advice like "now you should write code" or "prepare to implement" or "think about the approach"
+- NEVER say "open your IDE" or "start coding" — instead, GIVE THE CODE
+- In brief mode: provide 2-3 bullet approach points, then the FULL working code with comments
+- In detailed mode: explain approach, time/space complexity, edge cases, then the FULL working code with comments
+- Include the programming language name in the code fence (e.g. \`\`\`python, \`\`\`javascript)
+- If the language is not specified, default to Python
+- The code must be complete, runnable, and correct`;
+
+const profilePrompts = {
+ interview: {
+ intro: `You are an AI-powered interview assistant, designed to act as a discreet on-screen teleprompter. Your mission is to help the user excel in their job interview by providing concise, impactful, and ready-to-speak answers or key talking points. Analyze the ongoing interview dialogue and, crucially, the 'User-provided context' below.`,
+
+ searchUsage: `**SEARCH TOOL USAGE:**
- If the interviewer mentions **recent events, news, or current trends** (anything from the last 6 months), **ALWAYS use Google search** to get up-to-date information
- If they ask about **company-specific information, recent acquisitions, funding, or leadership changes**, use Google search first
- If they mention **new technologies, frameworks, or industry developments**, search for the latest information
- After searching, provide a **concise, informed response** based on the real-time data`,
- content: `Focus on delivering the most essential information the user needs. Your suggestions should be direct and immediately usable.
+ content: `Focus on delivering the most essential information the user needs. Your suggestions should be direct and immediately usable.
To help the user 'crack' the interview in their specific field:
1. Heavily rely on the 'User-provided context' (e.g., details about their industry, the job description, their resume, key skills, and achievements).
@@ -32,27 +56,20 @@ You: "I've been working with React for 4 years, building everything from simple
Interviewer: "Why do you want to work here?"
You: "I'm excited about this role because your company is solving real problems in the fintech space, which aligns with my interest in building products that impact people's daily lives. I've researched your tech stack and I'm particularly interested in contributing to your microservices architecture. Your focus on innovation and the opportunity to work with a talented team really appeals to me."`,
- outputInstructions: `**OUTPUT INSTRUCTIONS:**
+ outputInstructions: `**OUTPUT INSTRUCTIONS:**
Provide only the exact words to say in **markdown format**. No coaching, no "you should" statements, no explanations - just the direct response the candidate can speak immediately. Keep it **short and impactful**.`,
- },
+ },
- sales: {
- intro: `You are a sales call assistant. Your job is to provide the exact words the salesperson should say to prospects during sales calls. Give direct, ready-to-speak responses that are persuasive and professional.`,
+ sales: {
+ intro: `You are a sales call assistant. Your job is to provide the exact words the salesperson should say to prospects during sales calls. Give direct, ready-to-speak responses that are persuasive and professional.`,
- formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
-- Keep responses SHORT and CONCISE (1-3 sentences max)
-- Use **markdown formatting** for better readability
-- Use **bold** for key points and emphasis
-- Use bullet points (-) for lists when appropriate
-- Focus on the most essential information only`,
-
- searchUsage: `**SEARCH TOOL USAGE:**
+ searchUsage: `**SEARCH TOOL USAGE:**
- If the prospect mentions **recent industry trends, market changes, or current events**, **ALWAYS use Google search** to get up-to-date information
- If they reference **competitor information, recent funding news, or market data**, search for the latest information first
- If they ask about **new regulations, industry reports, or recent developments**, use search to provide accurate data
- After searching, provide a **concise, informed response** that demonstrates current market knowledge`,
- content: `Examples:
+ content: `Examples:
Prospect: "Tell me about your product"
You: "Our platform helps companies like yours reduce operational costs by 30% while improving efficiency. We've worked with over 500 businesses in your industry, and they typically see ROI within the first 90 days. What specific operational challenges are you facing right now?"
@@ -63,27 +80,20 @@ You: "Three key differentiators set us apart: First, our implementation takes ju
Prospect: "I need to think about it"
You: "I completely understand this is an important decision. What specific concerns can I address for you today? Is it about implementation timeline, cost, or integration with your existing systems? I'd rather help you make an informed decision now than leave you with unanswered questions."`,
- outputInstructions: `**OUTPUT INSTRUCTIONS:**
+ outputInstructions: `**OUTPUT INSTRUCTIONS:**
Provide only the exact words to say in **markdown format**. Be persuasive but not pushy. Focus on value and addressing objections directly. Keep responses **short and impactful**.`,
- },
+ },
- meeting: {
- intro: `You are a meeting assistant. Your job is to provide the exact words to say during professional meetings, presentations, and discussions. Give direct, ready-to-speak responses that are clear and professional.`,
+ meeting: {
+ intro: `You are a meeting assistant. Your job is to provide the exact words to say during professional meetings, presentations, and discussions. Give direct, ready-to-speak responses that are clear and professional.`,
- formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
-- Keep responses SHORT and CONCISE (1-3 sentences max)
-- Use **markdown formatting** for better readability
-- Use **bold** for key points and emphasis
-- Use bullet points (-) for lists when appropriate
-- Focus on the most essential information only`,
-
- searchUsage: `**SEARCH TOOL USAGE:**
+ searchUsage: `**SEARCH TOOL USAGE:**
- If participants mention **recent industry news, regulatory changes, or market updates**, **ALWAYS use Google search** for current information
- If they reference **competitor activities, recent reports, or current statistics**, search for the latest data first
- If they discuss **new technologies, tools, or industry developments**, use search to provide accurate insights
- After searching, provide a **concise, informed response** that adds value to the discussion`,
- content: `Examples:
+ content: `Examples:
Participant: "What's the status on the project?"
You: "We're currently on track to meet our deadline. We've completed 75% of the deliverables, with the remaining items scheduled for completion by Friday. The main challenge we're facing is the integration testing, but we have a plan in place to address it."
@@ -94,27 +104,20 @@ You: "Absolutely. We're currently at 80% of our allocated budget with 20% of the
Participant: "What are the next steps?"
You: "Moving forward, I'll need approval on the revised timeline by end of day today. Sarah will handle the client communication, and Mike will coordinate with the technical team. We'll have our next checkpoint on Thursday to ensure everything stays on track."`,
- outputInstructions: `**OUTPUT INSTRUCTIONS:**
+ outputInstructions: `**OUTPUT INSTRUCTIONS:**
Provide only the exact words to say in **markdown format**. Be clear, concise, and action-oriented in your responses. Keep it **short and impactful**.`,
- },
+ },
- presentation: {
- intro: `You are a presentation coach. Your job is to provide the exact words the presenter should say during presentations, pitches, and public speaking events. Give direct, ready-to-speak responses that are engaging and confident.`,
+ presentation: {
+ intro: `You are a presentation coach. Your job is to provide the exact words the presenter should say during presentations, pitches, and public speaking events. Give direct, ready-to-speak responses that are engaging and confident.`,
- formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
-- Keep responses SHORT and CONCISE (1-3 sentences max)
-- Use **markdown formatting** for better readability
-- Use **bold** for key points and emphasis
-- Use bullet points (-) for lists when appropriate
-- Focus on the most essential information only`,
-
- searchUsage: `**SEARCH TOOL USAGE:**
+ searchUsage: `**SEARCH TOOL USAGE:**
- If the audience asks about **recent market trends, current statistics, or latest industry data**, **ALWAYS use Google search** for up-to-date information
- If they reference **recent events, new competitors, or current market conditions**, search for the latest information first
- If they inquire about **recent studies, reports, or breaking news** in your field, use search to provide accurate data
- After searching, provide a **concise, credible response** with current facts and figures`,
- content: `Examples:
+ content: `Examples:
Audience: "Can you explain that slide again?"
You: "Of course. This slide shows our three-year growth trajectory. The blue line represents revenue, which has grown 150% year over year. The orange bars show our customer acquisition, doubling each year. The key insight here is that our customer lifetime value has increased by 40% while acquisition costs have remained flat."
@@ -125,27 +128,20 @@ You: "Great question. Our competitive advantage comes down to three core strengt
Audience: "How do you plan to scale?"
You: "Our scaling strategy focuses on three pillars. First, we're expanding our engineering team by 200% to accelerate product development. Second, we're entering three new markets next quarter. Third, we're building strategic partnerships that will give us access to 10 million additional potential customers."`,
- outputInstructions: `**OUTPUT INSTRUCTIONS:**
+ outputInstructions: `**OUTPUT INSTRUCTIONS:**
Provide only the exact words to say in **markdown format**. Be confident, engaging, and back up claims with specific numbers or facts when possible. Keep responses **short and impactful**.`,
- },
+ },
- negotiation: {
- intro: `You are a negotiation assistant. Your job is to provide the exact words to say during business negotiations, contract discussions, and deal-making conversations. Give direct, ready-to-speak responses that are strategic and professional.`,
+ negotiation: {
+ intro: `You are a negotiation assistant. Your job is to provide the exact words to say during business negotiations, contract discussions, and deal-making conversations. Give direct, ready-to-speak responses that are strategic and professional.`,
- formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
-- Keep responses SHORT and CONCISE (1-3 sentences max)
-- Use **markdown formatting** for better readability
-- Use **bold** for key points and emphasis
-- Use bullet points (-) for lists when appropriate
-- Focus on the most essential information only`,
-
- searchUsage: `**SEARCH TOOL USAGE:**
+ searchUsage: `**SEARCH TOOL USAGE:**
- If they mention **recent market pricing, current industry standards, or competitor offers**, **ALWAYS use Google search** for current benchmarks
- If they reference **recent legal changes, new regulations, or market conditions**, search for the latest information first
- If they discuss **recent company news, financial performance, or industry developments**, use search to provide informed responses
- After searching, provide a **strategic, well-informed response** that leverages current market intelligence`,
- content: `Examples:
+ content: `Examples:
Other party: "That price is too high"
You: "I understand your concern about the investment. Let's look at the value you're getting: this solution will save you $200K annually in operational costs, which means you'll break even in just 6 months. Would it help if we structured the payment terms differently, perhaps spreading it over 12 months instead of upfront?"
@@ -156,27 +152,20 @@ You: "I appreciate your directness. We want this to work for both parties. Our c
Other party: "We're considering other options"
You: "That's smart business practice. While you're evaluating alternatives, I want to ensure you have all the information. Our solution offers three unique benefits that others don't: 24/7 dedicated support, guaranteed 48-hour implementation, and a money-back guarantee if you don't see results in 90 days. How important are these factors in your decision?"`,
- outputInstructions: `**OUTPUT INSTRUCTIONS:**
+ outputInstructions: `**OUTPUT INSTRUCTIONS:**
Provide only the exact words to say in **markdown format**. Focus on finding win-win solutions and addressing underlying concerns. Keep responses **short and impactful**.`,
- },
+ },
- exam: {
- intro: `You are an exam assistant designed to help students pass tests efficiently. Your role is to provide direct, accurate answers to exam questions with minimal explanation - just enough to confirm the answer is correct.`,
+ exam: {
+ intro: `You are an exam assistant designed to help students pass tests efficiently. Your role is to provide direct, accurate answers to exam questions with minimal explanation - just enough to confirm the answer is correct.`,
- formatRequirements: `**RESPONSE FORMAT REQUIREMENTS:**
-- Keep responses SHORT and CONCISE (1-2 sentences max)
-- Use **markdown formatting** for better readability
-- Use **bold** for the answer choice/result
-- Focus on the most essential information only
-- Provide only brief justification for correctness`,
-
- searchUsage: `**SEARCH TOOL USAGE:**
+ searchUsage: `**SEARCH TOOL USAGE:**
- If the question involves **recent information, current events, or updated facts**, **ALWAYS use Google search** for the latest data
- If they reference **specific dates, statistics, or factual information** that might be outdated, search for current information
- If they ask about **recent research, new theories, or updated methodologies**, search for the latest information
- After searching, provide **direct, accurate answers** with minimal explanation`,
- content: `Focus on providing efficient exam assistance that helps students pass tests quickly.
+ content: `Focus on providing efficient exam assistance that helps students pass tests quickly.
**Key Principles:**
1. **Answer the question directly** - no unnecessary explanations
@@ -196,30 +185,62 @@ You: "**Question**: Which of the following is a primary color? A) Green B) Red C
Question: "Solve for x: 2x + 5 = 13"
You: "**Question**: Solve for x: 2x + 5 = 13 **Answer**: x = 4 **Why**: Subtract 5 from both sides: 2x = 8, then divide by 2: x = 4."`,
- outputInstructions: `**OUTPUT INSTRUCTIONS:**
+ outputInstructions: `**OUTPUT INSTRUCTIONS:**
Provide direct exam answers in **markdown format**. Include the question text, the correct answer choice, and a brief justification. Focus on efficiency and accuracy. Keep responses **short and to the point**.`,
- },
+ },
};
-function buildSystemPrompt(promptParts, customPrompt = '', googleSearchEnabled = true) {
- const sections = [promptParts.intro, '\n\n', promptParts.formatRequirements];
+function buildSystemPrompt(
+ promptParts,
+ customPrompt = "",
+ googleSearchEnabled = true,
+ responseMode = "brief",
+) {
+ const formatReqs =
+ responseModeFormats[responseMode] || responseModeFormats.brief;
+ const sections = [
+ promptParts.intro,
+ "\n\n",
+ formatReqs,
+ "\n\n",
+ codingAwareness,
+ ];
- // Only add search usage section if Google Search is enabled
- if (googleSearchEnabled) {
- sections.push('\n\n', promptParts.searchUsage);
- }
+ // Only add search usage section if Google Search is enabled
+ if (googleSearchEnabled) {
+ sections.push("\n\n", promptParts.searchUsage);
+ }
- sections.push('\n\n', promptParts.content, '\n\nUser-provided context\n-----\n', customPrompt, '\n-----\n\n', promptParts.outputInstructions);
+ sections.push(
+ "\n\n",
+ promptParts.content,
+ "\n\nUser-provided context\n-----\n",
+ customPrompt,
+ "\n-----\n\n",
+ promptParts.outputInstructions,
+ );
- return sections.join('');
+ return sections.join("");
}
-function getSystemPrompt(profile, customPrompt = '', googleSearchEnabled = true) {
- const promptParts = profilePrompts[profile] || profilePrompts.interview;
- return buildSystemPrompt(promptParts, customPrompt, googleSearchEnabled);
+function getSystemPrompt(
+ profile,
+ customPrompt = "",
+ googleSearchEnabled = true,
+ responseMode = "brief",
+) {
+ const promptParts = profilePrompts[profile] || profilePrompts.interview;
+ return buildSystemPrompt(
+ promptParts,
+ customPrompt,
+ googleSearchEnabled,
+ responseMode,
+ );
}
module.exports = {
- profilePrompts,
- getSystemPrompt,
+ profilePrompts,
+ responseModeFormats,
+ codingAwareness,
+ getSystemPrompt,
};
diff --git a/src/utils/renderer.js b/src/utils/renderer.js
index b07a51b..4041a88 100644
--- a/src/utils/renderer.js
+++ b/src/utils/renderer.js
@@ -1,5 +1,5 @@
// renderer.js
-const { ipcRenderer } = require('electron');
+const { ipcRenderer } = require("electron");
let mediaStream = null;
let screenshotInterval = null;
@@ -14,1033 +14,1185 @@ const BUFFER_SIZE = 4096; // Increased buffer size for smoother audio
let hiddenVideo = null;
let offscreenCanvas = null;
let offscreenContext = null;
-let currentImageQuality = 'medium'; // Store current image quality for manual screenshots
+let currentImageQuality = "medium"; // Store current image quality for manual screenshots
-const isLinux = process.platform === 'linux';
-const isMacOS = process.platform === 'darwin';
+const isLinux = process.platform === "linux";
+const isMacOS = process.platform === "darwin";
// ============ STORAGE API ============
// Wrapper for IPC-based storage access
const storage = {
- // Config
- async getConfig() {
- const result = await ipcRenderer.invoke('storage:get-config');
- return result.success ? result.data : {};
- },
- async setConfig(config) {
- return ipcRenderer.invoke('storage:set-config', config);
- },
- async updateConfig(key, value) {
- return ipcRenderer.invoke('storage:update-config', key, value);
- },
+ // Config
+ async getConfig() {
+ const result = await ipcRenderer.invoke("storage:get-config");
+ return result.success ? result.data : {};
+ },
+ async setConfig(config) {
+ return ipcRenderer.invoke("storage:set-config", config);
+ },
+ async updateConfig(key, value) {
+ return ipcRenderer.invoke("storage:update-config", key, value);
+ },
- // Credentials
- async getCredentials() {
- const result = await ipcRenderer.invoke('storage:get-credentials');
- return result.success ? result.data : {};
- },
- async setCredentials(credentials) {
- return ipcRenderer.invoke('storage:set-credentials', credentials);
- },
- async getApiKey() {
- const result = await ipcRenderer.invoke('storage:get-api-key');
- return result.success ? result.data : '';
- },
- async setApiKey(apiKey) {
- return ipcRenderer.invoke('storage:set-api-key', apiKey);
- },
- async getGroqApiKey() {
- const result = await ipcRenderer.invoke('storage:get-groq-api-key');
- return result.success ? result.data : '';
- },
- async setGroqApiKey(groqApiKey) {
- return ipcRenderer.invoke('storage:set-groq-api-key', groqApiKey);
- },
- async getOpenAICompatibleConfig() {
- const result = await ipcRenderer.invoke('get-openai-compatible-config');
- return result.success ? result.config : { apiKey: '', baseUrl: '', model: '' };
- },
- async setOpenAICompatibleConfig(apiKey, baseUrl, model) {
- return ipcRenderer.invoke('set-openai-compatible-config', apiKey, baseUrl, model);
- },
+ // Credentials
+ async getCredentials() {
+ const result = await ipcRenderer.invoke("storage:get-credentials");
+ return result.success ? result.data : {};
+ },
+ async setCredentials(credentials) {
+ return ipcRenderer.invoke("storage:set-credentials", credentials);
+ },
+ async getApiKey() {
+ const result = await ipcRenderer.invoke("storage:get-api-key");
+ return result.success ? result.data : "";
+ },
+ async setApiKey(apiKey) {
+ return ipcRenderer.invoke("storage:set-api-key", apiKey);
+ },
+ async getGroqApiKey() {
+ const result = await ipcRenderer.invoke("storage:get-groq-api-key");
+ return result.success ? result.data : "";
+ },
+ async setGroqApiKey(groqApiKey) {
+ return ipcRenderer.invoke("storage:set-groq-api-key", groqApiKey);
+ },
+ async getOpenAICompatibleConfig() {
+ const result = await ipcRenderer.invoke("get-openai-compatible-config");
+ return result.success
+ ? result.config
+ : { apiKey: "", baseUrl: "", model: "" };
+ },
+ async setOpenAICompatibleConfig(apiKey, baseUrl, model) {
+ return ipcRenderer.invoke(
+ "set-openai-compatible-config",
+ apiKey,
+ baseUrl,
+ model,
+ );
+ },
- // Preferences
- async getPreferences() {
- const result = await ipcRenderer.invoke('storage:get-preferences');
- return result.success ? result.data : {};
- },
- async setPreferences(preferences) {
- return ipcRenderer.invoke('storage:set-preferences', preferences);
- },
- async updatePreference(key, value) {
- return ipcRenderer.invoke('storage:update-preference', key, value);
- },
+ // Preferences
+ async getPreferences() {
+ const result = await ipcRenderer.invoke("storage:get-preferences");
+ return result.success ? result.data : {};
+ },
+ async setPreferences(preferences) {
+ return ipcRenderer.invoke("storage:set-preferences", preferences);
+ },
+ async updatePreference(key, value) {
+ return ipcRenderer.invoke("storage:update-preference", key, value);
+ },
- // Keybinds
- async getKeybinds() {
- const result = await ipcRenderer.invoke('storage:get-keybinds');
- return result.success ? result.data : null;
- },
- async setKeybinds(keybinds) {
- return ipcRenderer.invoke('storage:set-keybinds', keybinds);
- },
+ // Keybinds
+ async getKeybinds() {
+ const result = await ipcRenderer.invoke("storage:get-keybinds");
+ return result.success ? result.data : null;
+ },
+ async setKeybinds(keybinds) {
+ return ipcRenderer.invoke("storage:set-keybinds", keybinds);
+ },
- // Sessions (History)
- async getAllSessions() {
- const result = await ipcRenderer.invoke('storage:get-all-sessions');
- return result.success ? result.data : [];
- },
- async getSession(sessionId) {
- const result = await ipcRenderer.invoke('storage:get-session', sessionId);
- return result.success ? result.data : null;
- },
- async saveSession(sessionId, data) {
- return ipcRenderer.invoke('storage:save-session', sessionId, data);
- },
- async deleteSession(sessionId) {
- return ipcRenderer.invoke('storage:delete-session', sessionId);
- },
- async deleteAllSessions() {
- return ipcRenderer.invoke('storage:delete-all-sessions');
- },
+ // Sessions (History)
+ async getAllSessions() {
+ const result = await ipcRenderer.invoke("storage:get-all-sessions");
+ return result.success ? result.data : [];
+ },
+ async getSession(sessionId) {
+ const result = await ipcRenderer.invoke("storage:get-session", sessionId);
+ return result.success ? result.data : null;
+ },
+ async saveSession(sessionId, data) {
+ return ipcRenderer.invoke("storage:save-session", sessionId, data);
+ },
+ async deleteSession(sessionId) {
+ return ipcRenderer.invoke("storage:delete-session", sessionId);
+ },
+ async deleteAllSessions() {
+ return ipcRenderer.invoke("storage:delete-all-sessions");
+ },
- // Clear all
- async clearAll() {
- return ipcRenderer.invoke('storage:clear-all');
- },
+ // Clear all
+ async clearAll() {
+ return ipcRenderer.invoke("storage:clear-all");
+ },
- // Limits
- async getTodayLimits() {
- const result = await ipcRenderer.invoke('storage:get-today-limits');
- return result.success ? result.data : { flash: { count: 0 }, flashLite: { count: 0 } };
- }
+ // Limits
+ async getTodayLimits() {
+ const result = await ipcRenderer.invoke("storage:get-today-limits");
+ return result.success
+ ? result.data
+ : { flash: { count: 0 }, flashLite: { count: 0 } };
+ },
};
// Cache for preferences to avoid async calls in hot paths
let preferencesCache = null;
async function loadPreferencesCache() {
- preferencesCache = await storage.getPreferences();
- return preferencesCache;
+ preferencesCache = await storage.getPreferences();
+ return preferencesCache;
}
// Initialize preferences cache
loadPreferencesCache();
function convertFloat32ToInt16(float32Array) {
- const int16Array = new Int16Array(float32Array.length);
- for (let i = 0; i < float32Array.length; i++) {
- // Improved scaling to prevent clipping
- const s = Math.max(-1, Math.min(1, float32Array[i]));
- int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
- }
- return int16Array;
+ const int16Array = new Int16Array(float32Array.length);
+ for (let i = 0; i < float32Array.length; i++) {
+ // Improved scaling to prevent clipping
+ const s = Math.max(-1, Math.min(1, float32Array[i]));
+ int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
+ }
+ return int16Array;
}
function arrayBufferToBase64(buffer) {
- let binary = '';
- const bytes = new Uint8Array(buffer);
- const len = bytes.byteLength;
- for (let i = 0; i < len; i++) {
- binary += String.fromCharCode(bytes[i]);
- }
- return btoa(binary);
+ let binary = "";
+ const bytes = new Uint8Array(buffer);
+ const len = bytes.byteLength;
+ for (let i = 0; i < len; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return btoa(binary);
}
-async function initializeGemini(profile = 'interview', language = 'en-US') {
- const apiKey = await storage.getApiKey();
- if (apiKey) {
- const prefs = await storage.getPreferences();
- const success = await ipcRenderer.invoke('initialize-gemini', apiKey, prefs.customPrompt || '', profile, language);
- if (success) {
- cheatingDaddy.setStatus('Live');
- } else {
- cheatingDaddy.setStatus('error');
- }
- }
-}
-
-async function initializeLocal(profile = 'interview') {
+async function initializeGemini(profile = "interview", language = "en-US") {
+ const apiKey = await storage.getApiKey();
+ if (apiKey) {
const prefs = await storage.getPreferences();
- const ollamaHost = prefs.ollamaHost || 'http://127.0.0.1:11434';
- const ollamaModel = prefs.ollamaModel || 'llama3.1';
- const whisperModel = prefs.whisperModel || 'Xenova/whisper-small';
- const customPrompt = prefs.customPrompt || '';
-
- const success = await ipcRenderer.invoke('initialize-local', ollamaHost, ollamaModel, whisperModel, profile, customPrompt);
+ const success = await ipcRenderer.invoke(
+ "initialize-gemini",
+ apiKey,
+ prefs.customPrompt || "",
+ profile,
+ language,
+ );
if (success) {
- cheatingDaddy.setStatus('Local AI Live');
- return true;
+ cheatingDaddy.setStatus("Live");
} else {
- cheatingDaddy.setStatus('error');
- return false;
+ cheatingDaddy.setStatus("error");
}
+ }
+}
+
+async function initializeLocal(profile = "interview") {
+ const prefs = await storage.getPreferences();
+ const ollamaHost = prefs.ollamaHost || "http://127.0.0.1:11434";
+ const ollamaModel = prefs.ollamaModel || "llama3.1";
+ const whisperModel = prefs.whisperModel || "Xenova/whisper-small";
+ const customPrompt = prefs.customPrompt || "";
+
+ const success = await ipcRenderer.invoke(
+ "initialize-local",
+ ollamaHost,
+ ollamaModel,
+ whisperModel,
+ profile,
+ customPrompt,
+ );
+ if (success) {
+ cheatingDaddy.setStatus("Local AI Live");
+ return true;
+ } else {
+ cheatingDaddy.setStatus("error");
+ return false;
+ }
}
// Listen for status updates
-ipcRenderer.on('update-status', (event, status) => {
- console.log('Status update:', status);
- cheatingDaddy.setStatus(status);
+ipcRenderer.on("update-status", (event, status) => {
+ console.log("Status update:", status);
+ cheatingDaddy.setStatus(status);
});
-async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'medium') {
- // Store the image quality for manual screenshots
- currentImageQuality = imageQuality;
+async function startCapture(
+ screenshotIntervalSeconds = 5,
+ imageQuality = "medium",
+) {
+ // Store the image quality for manual screenshots
+ currentImageQuality = imageQuality;
- // Refresh preferences cache
- await loadPreferencesCache();
- const audioMode = preferencesCache.audioMode || 'speaker_only';
+ // Refresh preferences cache
+ await loadPreferencesCache();
+ const audioMode = preferencesCache.audioMode || "speaker_only";
- try {
- if (isMacOS) {
- // On macOS, use SystemAudioDump for audio and getDisplayMedia for screen
- console.log('Starting macOS capture with SystemAudioDump...');
+ try {
+ if (isMacOS) {
+ // On macOS, use SystemAudioDump for audio and getDisplayMedia for screen
+ console.log("Starting macOS capture with SystemAudioDump...");
- // Start macOS audio capture
- const audioResult = await ipcRenderer.invoke('start-macos-audio');
- if (!audioResult.success) {
- throw new Error('Failed to start macOS audio capture: ' + audioResult.error);
- }
+ // Start macOS audio capture
+ const audioResult = await ipcRenderer.invoke("start-macos-audio");
+ if (!audioResult.success) {
+ throw new Error(
+ "Failed to start macOS audio capture: " + audioResult.error,
+ );
+ }
- // Get screen capture for screenshots
- mediaStream = await navigator.mediaDevices.getDisplayMedia({
- video: {
- frameRate: 1,
- width: { ideal: 1920 },
- height: { ideal: 1080 },
- },
- audio: false, // Don't use browser audio on macOS
- });
+ // Get screen capture for screenshots
+ mediaStream = await navigator.mediaDevices.getDisplayMedia({
+ video: {
+ frameRate: 1,
+ width: { ideal: 1920 },
+ height: { ideal: 1080 },
+ },
+ audio: false, // Don't use browser audio on macOS
+ });
- console.log('macOS screen capture started - audio handled by SystemAudioDump');
+ console.log(
+ "macOS screen capture started - audio handled by SystemAudioDump",
+ );
- if (audioMode === 'mic_only' || audioMode === 'both') {
- let micStream = null;
- try {
- micStream = await navigator.mediaDevices.getUserMedia({
- audio: {
- sampleRate: SAMPLE_RATE,
- channelCount: 1,
- echoCancellation: true,
- noiseSuppression: true,
- autoGainControl: true,
- },
- video: false,
- });
- console.log('macOS microphone capture started');
- setupLinuxMicProcessing(micStream);
- } catch (micError) {
- console.warn('Failed to get microphone access on macOS:', micError);
- }
- }
- } else if (isLinux) {
- // Linux - use display media for screen capture and try to get system audio
- try {
- // First try to get system audio via getDisplayMedia (works on newer browsers)
- mediaStream = await navigator.mediaDevices.getDisplayMedia({
- video: {
- frameRate: 1,
- width: { ideal: 1920 },
- height: { ideal: 1080 },
- },
- audio: {
- sampleRate: SAMPLE_RATE,
- channelCount: 1,
- echoCancellation: false, // Don't cancel system audio
- noiseSuppression: false,
- autoGainControl: false,
- },
- });
-
- console.log('Linux system audio capture via getDisplayMedia succeeded');
-
- // Setup audio processing for Linux system audio
- setupLinuxSystemAudioProcessing();
- } catch (systemAudioError) {
- console.warn('System audio via getDisplayMedia failed, trying screen-only capture:', systemAudioError);
-
- // Fallback to screen-only capture
- mediaStream = await navigator.mediaDevices.getDisplayMedia({
- video: {
- frameRate: 1,
- width: { ideal: 1920 },
- height: { ideal: 1080 },
- },
- audio: false,
- });
- }
-
- // Additionally get microphone input for Linux based on audio mode
- if (audioMode === 'mic_only' || audioMode === 'both') {
- let micStream = null;
- try {
- micStream = await navigator.mediaDevices.getUserMedia({
- audio: {
- sampleRate: SAMPLE_RATE,
- channelCount: 1,
- echoCancellation: true,
- noiseSuppression: true,
- autoGainControl: true,
- },
- video: false,
- });
-
- console.log('Linux microphone capture started');
-
- // Setup audio processing for microphone on Linux
- setupLinuxMicProcessing(micStream);
- } catch (micError) {
- console.warn('Failed to get microphone access on Linux:', micError);
- // Continue without microphone if permission denied
- }
- }
-
- console.log('Linux capture started - system audio:', mediaStream.getAudioTracks().length > 0, 'microphone mode:', audioMode);
- } else {
- // Windows - use display media with loopback for system audio
- mediaStream = await navigator.mediaDevices.getDisplayMedia({
- video: {
- frameRate: 1,
- width: { ideal: 1920 },
- height: { ideal: 1080 },
- },
- audio: {
- sampleRate: SAMPLE_RATE,
- channelCount: 1,
- echoCancellation: true,
- noiseSuppression: true,
- autoGainControl: true,
- },
- });
-
- console.log('Windows capture started with loopback audio');
-
- // Setup audio processing for Windows loopback audio only
- setupWindowsLoopbackProcessing();
-
- if (audioMode === 'mic_only' || audioMode === 'both') {
- let micStream = null;
- try {
- micStream = await navigator.mediaDevices.getUserMedia({
- audio: {
- sampleRate: SAMPLE_RATE,
- channelCount: 1,
- echoCancellation: true,
- noiseSuppression: true,
- autoGainControl: true,
- },
- video: false,
- });
- console.log('Windows microphone capture started');
- setupLinuxMicProcessing(micStream);
- } catch (micError) {
- console.warn('Failed to get microphone access on Windows:', micError);
- }
- }
+ if (audioMode === "mic_only" || audioMode === "both") {
+ let micStream = null;
+ try {
+ micStream = await navigator.mediaDevices.getUserMedia({
+ audio: {
+ sampleRate: SAMPLE_RATE,
+ channelCount: 1,
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true,
+ },
+ video: false,
+ });
+ console.log("macOS microphone capture started");
+ setupLinuxMicProcessing(micStream);
+ } catch (micError) {
+ console.warn("Failed to get microphone access on macOS:", micError);
}
-
- console.log('MediaStream obtained:', {
- hasVideo: mediaStream.getVideoTracks().length > 0,
- hasAudio: mediaStream.getAudioTracks().length > 0,
- videoTrack: mediaStream.getVideoTracks()[0]?.getSettings(),
+ }
+ } else if (isLinux) {
+ // Linux - use display media for screen capture and try to get system audio
+ try {
+ // First try to get system audio via getDisplayMedia (works on newer browsers)
+ mediaStream = await navigator.mediaDevices.getDisplayMedia({
+ video: {
+ frameRate: 1,
+ width: { ideal: 1920 },
+ height: { ideal: 1080 },
+ },
+ audio: {
+ sampleRate: SAMPLE_RATE,
+ channelCount: 1,
+ echoCancellation: false, // Don't cancel system audio
+ noiseSuppression: false,
+ autoGainControl: false,
+ },
});
- // Manual mode only - screenshots captured on demand via shortcut
- console.log('Manual mode enabled - screenshots will be captured on demand only');
- } catch (err) {
- console.error('Error starting capture:', err);
- cheatingDaddy.setStatus('error');
+ console.log("Linux system audio capture via getDisplayMedia succeeded");
+
+ // Setup audio processing for Linux system audio
+ setupLinuxSystemAudioProcessing();
+ } catch (systemAudioError) {
+ console.warn(
+ "System audio via getDisplayMedia failed, trying screen-only capture:",
+ systemAudioError,
+ );
+
+ // Fallback to screen-only capture
+ mediaStream = await navigator.mediaDevices.getDisplayMedia({
+ video: {
+ frameRate: 1,
+ width: { ideal: 1920 },
+ height: { ideal: 1080 },
+ },
+ audio: false,
+ });
+ }
+
+ // Additionally get microphone input for Linux based on audio mode
+ if (audioMode === "mic_only" || audioMode === "both") {
+ let micStream = null;
+ try {
+ micStream = await navigator.mediaDevices.getUserMedia({
+ audio: {
+ sampleRate: SAMPLE_RATE,
+ channelCount: 1,
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true,
+ },
+ video: false,
+ });
+
+ console.log("Linux microphone capture started");
+
+ // Setup audio processing for microphone on Linux
+ setupLinuxMicProcessing(micStream);
+ } catch (micError) {
+ console.warn("Failed to get microphone access on Linux:", micError);
+ // Continue without microphone if permission denied
+ }
+ }
+
+ console.log(
+ "Linux capture started - system audio:",
+ mediaStream.getAudioTracks().length > 0,
+ "microphone mode:",
+ audioMode,
+ );
+ } else {
+ // Windows - use display media with loopback for system audio
+ mediaStream = await navigator.mediaDevices.getDisplayMedia({
+ video: {
+ frameRate: 1,
+ width: { ideal: 1920 },
+ height: { ideal: 1080 },
+ },
+ audio: {
+ sampleRate: SAMPLE_RATE,
+ channelCount: 1,
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true,
+ },
+ });
+
+ console.log("Windows capture started with loopback audio");
+
+ // Setup audio processing for Windows loopback audio only
+ setupWindowsLoopbackProcessing();
+
+ if (audioMode === "mic_only" || audioMode === "both") {
+ let micStream = null;
+ try {
+ micStream = await navigator.mediaDevices.getUserMedia({
+ audio: {
+ sampleRate: SAMPLE_RATE,
+ channelCount: 1,
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true,
+ },
+ video: false,
+ });
+ console.log("Windows microphone capture started");
+ setupLinuxMicProcessing(micStream);
+ } catch (micError) {
+ console.warn("Failed to get microphone access on Windows:", micError);
+ }
+ }
}
+
+ console.log("MediaStream obtained:", {
+ hasVideo: mediaStream.getVideoTracks().length > 0,
+ hasAudio: mediaStream.getAudioTracks().length > 0,
+ videoTrack: mediaStream.getVideoTracks()[0]?.getSettings(),
+ });
+
+ // Manual mode only - screenshots captured on demand via shortcut
+ console.log(
+ "Manual mode enabled - screenshots will be captured on demand only",
+ );
+ } catch (err) {
+ console.error("Error starting capture:", err);
+ cheatingDaddy.setStatus("error");
+ }
}
function setupLinuxMicProcessing(micStream) {
- // Setup microphone audio processing for Linux
- const micAudioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
- const micSource = micAudioContext.createMediaStreamSource(micStream);
- const micProcessor = micAudioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
+ // Setup microphone audio processing for Linux
+ const micAudioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
+ const micSource = micAudioContext.createMediaStreamSource(micStream);
+ const micProcessor = micAudioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
- let audioBuffer = [];
- const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
+ let audioBuffer = [];
+ const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
- micProcessor.onaudioprocess = async e => {
- const inputData = e.inputBuffer.getChannelData(0);
- audioBuffer.push(...inputData);
+ micProcessor.onaudioprocess = async (e) => {
+ const inputData = e.inputBuffer.getChannelData(0);
+ audioBuffer.push(...inputData);
- // Process audio in chunks
- while (audioBuffer.length >= samplesPerChunk) {
- const chunk = audioBuffer.splice(0, samplesPerChunk);
- const pcmData16 = convertFloat32ToInt16(chunk);
- const base64Data = arrayBufferToBase64(pcmData16.buffer);
+ // Process audio in chunks
+ while (audioBuffer.length >= samplesPerChunk) {
+ const chunk = audioBuffer.splice(0, samplesPerChunk);
+ const pcmData16 = convertFloat32ToInt16(chunk);
+ const base64Data = arrayBufferToBase64(pcmData16.buffer);
- await ipcRenderer.invoke('send-mic-audio-content', {
- data: base64Data,
- mimeType: 'audio/pcm;rate=24000',
- });
- }
- };
+ await ipcRenderer.invoke("send-mic-audio-content", {
+ data: base64Data,
+ mimeType: "audio/pcm;rate=24000",
+ });
+ }
+ };
- micSource.connect(micProcessor);
- micProcessor.connect(micAudioContext.destination);
+ micSource.connect(micProcessor);
+ micProcessor.connect(micAudioContext.destination);
- // Store processor reference for cleanup
- micAudioProcessor = micProcessor;
+ // Store processor reference for cleanup
+ micAudioProcessor = micProcessor;
}
function setupLinuxSystemAudioProcessing() {
- // Setup system audio processing for Linux (from getDisplayMedia)
- audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
- const source = audioContext.createMediaStreamSource(mediaStream);
- audioProcessor = audioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
+ // Setup system audio processing for Linux (from getDisplayMedia)
+ audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
+ const source = audioContext.createMediaStreamSource(mediaStream);
+ audioProcessor = audioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
- let audioBuffer = [];
- const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
+ let audioBuffer = [];
+ const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
- audioProcessor.onaudioprocess = async e => {
- const inputData = e.inputBuffer.getChannelData(0);
- audioBuffer.push(...inputData);
+ audioProcessor.onaudioprocess = async (e) => {
+ const inputData = e.inputBuffer.getChannelData(0);
+ audioBuffer.push(...inputData);
- // Process audio in chunks
- while (audioBuffer.length >= samplesPerChunk) {
- const chunk = audioBuffer.splice(0, samplesPerChunk);
- const pcmData16 = convertFloat32ToInt16(chunk);
- const base64Data = arrayBufferToBase64(pcmData16.buffer);
+ // Process audio in chunks
+ while (audioBuffer.length >= samplesPerChunk) {
+ const chunk = audioBuffer.splice(0, samplesPerChunk);
+ const pcmData16 = convertFloat32ToInt16(chunk);
+ const base64Data = arrayBufferToBase64(pcmData16.buffer);
- await ipcRenderer.invoke('send-audio-content', {
- data: base64Data,
- mimeType: 'audio/pcm;rate=24000',
- });
- }
- };
+ await ipcRenderer.invoke("send-audio-content", {
+ data: base64Data,
+ mimeType: "audio/pcm;rate=24000",
+ });
+ }
+ };
- source.connect(audioProcessor);
- audioProcessor.connect(audioContext.destination);
+ source.connect(audioProcessor);
+ audioProcessor.connect(audioContext.destination);
}
function setupWindowsLoopbackProcessing() {
- // Setup audio processing for Windows loopback audio only
- audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
- const source = audioContext.createMediaStreamSource(mediaStream);
- audioProcessor = audioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
+ // Setup audio processing for Windows loopback audio only
+ audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
+ const source = audioContext.createMediaStreamSource(mediaStream);
+ audioProcessor = audioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
- let audioBuffer = [];
- const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
+ let audioBuffer = [];
+ const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
- audioProcessor.onaudioprocess = async e => {
- const inputData = e.inputBuffer.getChannelData(0);
- audioBuffer.push(...inputData);
+ audioProcessor.onaudioprocess = async (e) => {
+ const inputData = e.inputBuffer.getChannelData(0);
+ audioBuffer.push(...inputData);
- // Process audio in chunks
- while (audioBuffer.length >= samplesPerChunk) {
- const chunk = audioBuffer.splice(0, samplesPerChunk);
- const pcmData16 = convertFloat32ToInt16(chunk);
- const base64Data = arrayBufferToBase64(pcmData16.buffer);
+ // Process audio in chunks
+ while (audioBuffer.length >= samplesPerChunk) {
+ const chunk = audioBuffer.splice(0, samplesPerChunk);
+ const pcmData16 = convertFloat32ToInt16(chunk);
+ const base64Data = arrayBufferToBase64(pcmData16.buffer);
- await ipcRenderer.invoke('send-audio-content', {
- data: base64Data,
- mimeType: 'audio/pcm;rate=24000',
- });
- }
- };
+ await ipcRenderer.invoke("send-audio-content", {
+ data: base64Data,
+ mimeType: "audio/pcm;rate=24000",
+ });
+ }
+ };
- source.connect(audioProcessor);
- audioProcessor.connect(audioContext.destination);
+ source.connect(audioProcessor);
+ audioProcessor.connect(audioContext.destination);
}
-async function captureScreenshot(imageQuality = 'medium', isManual = false) {
- console.log(`Capturing ${isManual ? 'manual' : 'automated'} screenshot...`);
- if (!mediaStream) return;
+async function captureScreenshot(imageQuality = "medium", isManual = false) {
+ console.log(`Capturing ${isManual ? "manual" : "automated"} screenshot...`);
+ if (!mediaStream) return;
- // Lazy init of video element
- if (!hiddenVideo) {
- hiddenVideo = document.createElement('video');
- hiddenVideo.srcObject = mediaStream;
- hiddenVideo.muted = true;
- hiddenVideo.playsInline = true;
- await hiddenVideo.play();
+ // Lazy init of video element
+ 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();
- });
-
- // Lazy init of canvas based on video dimensions
- offscreenCanvas = document.createElement('canvas');
- offscreenCanvas.width = hiddenVideo.videoWidth;
- offscreenCanvas.height = hiddenVideo.videoHeight;
- offscreenContext = offscreenCanvas.getContext('2d');
- }
-
- // Check if video is ready
- if (hiddenVideo.readyState < 2) {
- console.warn('Video not ready yet, skipping screenshot');
- return;
- }
-
- offscreenContext.drawImage(hiddenVideo, 0, 0, offscreenCanvas.width, offscreenCanvas.height);
-
- // Check if image was drawn properly by sampling a pixel
- const imageData = offscreenContext.getImageData(0, 0, 1, 1);
- const isBlank = imageData.data.every((value, index) => {
- // Check if all pixels are black (0,0,0) or transparent
- return index === 3 ? true : value === 0;
+ await new Promise((resolve) => {
+ if (hiddenVideo.readyState >= 2) return resolve();
+ hiddenVideo.onloadedmetadata = () => resolve();
});
- if (isBlank) {
- console.warn('Screenshot appears to be blank/black');
- }
+ // Lazy init of canvas based on video dimensions
+ offscreenCanvas = document.createElement("canvas");
+ offscreenCanvas.width = hiddenVideo.videoWidth;
+ offscreenCanvas.height = hiddenVideo.videoHeight;
+ offscreenContext = offscreenCanvas.getContext("2d");
+ }
- let qualityValue;
- switch (imageQuality) {
- case 'high':
- qualityValue = 0.9;
- break;
- case 'medium':
- qualityValue = 0.7;
- break;
- case 'low':
- qualityValue = 0.5;
- break;
- default:
- qualityValue = 0.7; // Default to medium
- }
+ // Check if video is ready
+ if (hiddenVideo.readyState < 2) {
+ console.warn("Video not ready yet, skipping screenshot");
+ return;
+ }
- offscreenCanvas.toBlob(
- async blob => {
- if (!blob) {
- console.error('Failed to create blob from canvas');
- return;
- }
+ offscreenContext.drawImage(
+ hiddenVideo,
+ 0,
+ 0,
+ offscreenCanvas.width,
+ offscreenCanvas.height,
+ );
- const reader = new FileReader();
- reader.onloadend = async () => {
- const base64data = reader.result.split(',')[1];
+ // Check if image was drawn properly by sampling a pixel
+ const imageData = offscreenContext.getImageData(0, 0, 1, 1);
+ const isBlank = imageData.data.every((value, index) => {
+ // Check if all pixels are black (0,0,0) or transparent
+ return index === 3 ? true : value === 0;
+ });
- // Validate base64 data
- if (!base64data || base64data.length < 100) {
- console.error('Invalid base64 data generated');
- return;
- }
+ if (isBlank) {
+ console.warn("Screenshot appears to be blank/black");
+ }
- const result = await ipcRenderer.invoke('send-image-content', {
- data: base64data,
- });
+ let qualityValue;
+ switch (imageQuality) {
+ case "high":
+ qualityValue = 0.9;
+ break;
+ case "medium":
+ qualityValue = 0.7;
+ break;
+ case "low":
+ qualityValue = 0.5;
+ break;
+ default:
+ qualityValue = 0.7; // Default to medium
+ }
- if (result.success) {
- console.log(`Image sent successfully (${offscreenCanvas.width}x${offscreenCanvas.height})`);
- } else {
- console.error('Failed to send image:', result.error);
- }
- };
- reader.readAsDataURL(blob);
- },
- 'image/jpeg',
- qualityValue
- );
-}
-
-const MANUAL_SCREENSHOT_PROMPT = `Help me on this page, give me the answer no bs, complete answer.
-So if its a code question, give me the approach in few bullet points, then the entire code. Also if theres anything else i need to know, tell me.
-If its a question about the website, give me the answer no bs, complete answer.
-If its a mcq question, give me the answer no bs, complete answer.`;
-
-async function captureManualScreenshot(imageQuality = null) {
- console.log('Manual screenshot triggered');
- const quality = imageQuality || currentImageQuality;
-
- if (!mediaStream) {
- console.error('No media stream available');
+ offscreenCanvas.toBlob(
+ async (blob) => {
+ if (!blob) {
+ console.error("Failed to create blob from canvas");
return;
- }
+ }
- // Lazy init of video element
- if (!hiddenVideo) {
- hiddenVideo = document.createElement('video');
- hiddenVideo.srcObject = mediaStream;
- hiddenVideo.muted = true;
- hiddenVideo.playsInline = true;
- await hiddenVideo.play();
+ const reader = new FileReader();
+ reader.onloadend = async () => {
+ const base64data = reader.result.split(",")[1];
- await new Promise(resolve => {
- if (hiddenVideo.readyState >= 2) return resolve();
- hiddenVideo.onloadedmetadata = () => resolve();
+ // Validate base64 data
+ if (!base64data || base64data.length < 100) {
+ console.error("Invalid base64 data generated");
+ return;
+ }
+
+ const result = await ipcRenderer.invoke("send-image-content", {
+ data: base64data,
});
- // Lazy init of canvas based on video dimensions
- offscreenCanvas = document.createElement('canvas');
- offscreenCanvas.width = hiddenVideo.videoWidth;
- offscreenCanvas.height = hiddenVideo.videoHeight;
- offscreenContext = offscreenCanvas.getContext('2d');
- }
+ if (result.success) {
+ console.log(
+ `Image sent successfully (${offscreenCanvas.width}x${offscreenCanvas.height})`,
+ );
+ } else {
+ console.error("Failed to send image:", result.error);
+ }
+ };
+ reader.readAsDataURL(blob);
+ },
+ "image/jpeg",
+ qualityValue,
+ );
+}
- // Check if video is ready
- if (hiddenVideo.readyState < 2) {
- console.warn('Video not ready yet, skipping screenshot');
+const MANUAL_SCREENSHOT_PROMPT = `Analyze this screen and help me. Give me the answer directly, no BS.
+- If it's a coding problem, algorithm challenge, or live coding exercise: provide 2-3 bullet approach points, then the COMPLETE WORKING CODE with comments. Never say "open your IDE" — give the actual code.
+- If it's a question about the website content, give me the direct answer.
+- If it's a multiple choice question, give me the correct answer with brief justification.
+- If there's anything else important on screen, tell me.`;
+
+async function captureManualScreenshot(imageQuality = null) {
+ console.log("Manual screenshot triggered");
+ const quality = imageQuality || currentImageQuality;
+
+ if (!mediaStream) {
+ console.error("No media stream available");
+ return;
+ }
+
+ // Lazy init of video element
+ 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();
+ });
+
+ // Lazy init of canvas based on video dimensions
+ offscreenCanvas = document.createElement("canvas");
+ offscreenCanvas.width = hiddenVideo.videoWidth;
+ offscreenCanvas.height = hiddenVideo.videoHeight;
+ offscreenContext = offscreenCanvas.getContext("2d");
+ }
+
+ // Check if video is ready
+ if (hiddenVideo.readyState < 2) {
+ console.warn("Video not ready yet, skipping screenshot");
+ return;
+ }
+
+ // Downscale to max 1280px wide for faster transfer — vision models don't need 4K
+ const MAX_WIDTH = 1280;
+ const srcW = hiddenVideo.videoWidth;
+ const srcH = hiddenVideo.videoHeight;
+ let destW = srcW;
+ let destH = srcH;
+ if (srcW > MAX_WIDTH) {
+ destW = MAX_WIDTH;
+ destH = Math.round(srcH * (MAX_WIDTH / srcW));
+ }
+ offscreenCanvas.width = destW;
+ offscreenCanvas.height = destH;
+ offscreenContext.drawImage(hiddenVideo, 0, 0, destW, destH);
+
+ let qualityValue;
+ switch (quality) {
+ case "high":
+ qualityValue = 0.85;
+ break;
+ case "medium":
+ qualityValue = 0.6;
+ break;
+ case "low":
+ qualityValue = 0.4;
+ break;
+ default:
+ qualityValue = 0.6;
+ }
+
+ offscreenCanvas.toBlob(
+ async (blob) => {
+ if (!blob) {
+ console.error("Failed to create blob from canvas");
return;
- }
+ }
- // Downscale to max 1280px wide for faster transfer — vision models don't need 4K
- const MAX_WIDTH = 1280;
- const srcW = hiddenVideo.videoWidth;
- const srcH = hiddenVideo.videoHeight;
- let destW = srcW;
- let destH = srcH;
- if (srcW > MAX_WIDTH) {
- destW = MAX_WIDTH;
- destH = Math.round(srcH * (MAX_WIDTH / srcW));
- }
- offscreenCanvas.width = destW;
- offscreenCanvas.height = destH;
- offscreenContext.drawImage(hiddenVideo, 0, 0, destW, destH);
+ const reader = new FileReader();
+ reader.onloadend = async () => {
+ const base64data = reader.result.split(",")[1];
- let qualityValue;
- switch (quality) {
- case 'high':
- qualityValue = 0.85;
- break;
- case 'medium':
- qualityValue = 0.6;
- break;
- case 'low':
- qualityValue = 0.4;
- break;
- default:
- qualityValue = 0.6;
- }
+ if (!base64data || base64data.length < 100) {
+ console.error("Invalid base64 data generated");
+ return;
+ }
- offscreenCanvas.toBlob(
- async blob => {
- if (!blob) {
- console.error('Failed to create blob from canvas');
- return;
- }
+ console.log(
+ `Sending image: ${destW}x${destH}, ~${Math.round(base64data.length / 1024)}KB`,
+ );
- const reader = new FileReader();
- reader.onloadend = async () => {
- const base64data = reader.result.split(',')[1];
+ // Send image with prompt to HTTP API (response streams via IPC events)
+ const result = await ipcRenderer.invoke("send-image-content", {
+ data: base64data,
+ prompt: MANUAL_SCREENSHOT_PROMPT,
+ });
- if (!base64data || base64data.length < 100) {
- console.error('Invalid base64 data generated');
- return;
- }
-
- console.log(`Sending image: ${destW}x${destH}, ~${Math.round(base64data.length / 1024)}KB`);
-
- // Send image with prompt to HTTP API (response streams via IPC events)
- const result = await ipcRenderer.invoke('send-image-content', {
- data: base64data,
- prompt: MANUAL_SCREENSHOT_PROMPT,
- });
-
- if (result.success) {
- console.log(`Image response completed from ${result.model}`);
- // Response already displayed via streaming events (new-response/update-response)
- } else {
- console.error('Failed to get image response:', result.error);
- cheatingDaddy.addNewResponse(`Error: ${result.error}`);
- }
- };
- reader.readAsDataURL(blob);
- },
- 'image/jpeg',
- qualityValue
- );
+ if (result.success) {
+ console.log(`Image response completed from ${result.model}`);
+ // Response already displayed via streaming events (new-response/update-response)
+ } else {
+ console.error("Failed to get image response:", result.error);
+ cheatingDaddy.addNewResponse(`Error: ${result.error}`);
+ }
+ };
+ reader.readAsDataURL(blob);
+ },
+ "image/jpeg",
+ qualityValue,
+ );
}
// Expose functions to global scope for external access
window.captureManualScreenshot = captureManualScreenshot;
function stopCapture() {
- if (screenshotInterval) {
- clearInterval(screenshotInterval);
- screenshotInterval = null;
- }
+ if (screenshotInterval) {
+ clearInterval(screenshotInterval);
+ screenshotInterval = null;
+ }
- if (audioProcessor) {
- audioProcessor.disconnect();
- audioProcessor = null;
- }
+ if (audioProcessor) {
+ audioProcessor.disconnect();
+ audioProcessor = null;
+ }
- // Clean up microphone audio processor (Linux only)
- if (micAudioProcessor) {
- micAudioProcessor.disconnect();
- micAudioProcessor = null;
- }
+ // Clean up microphone audio processor (Linux only)
+ if (micAudioProcessor) {
+ micAudioProcessor.disconnect();
+ micAudioProcessor = null;
+ }
- if (audioContext) {
- audioContext.close();
- audioContext = null;
- }
+ if (audioContext) {
+ audioContext.close();
+ audioContext = null;
+ }
- if (mediaStream) {
- mediaStream.getTracks().forEach(track => track.stop());
- mediaStream = null;
- }
+ if (mediaStream) {
+ mediaStream.getTracks().forEach((track) => track.stop());
+ mediaStream = null;
+ }
- // Stop macOS audio capture if running
- if (isMacOS) {
- ipcRenderer.invoke('stop-macos-audio').catch(err => {
- console.error('Error stopping macOS audio:', err);
- });
- }
+ // Stop macOS audio capture if running
+ if (isMacOS) {
+ ipcRenderer.invoke("stop-macos-audio").catch((err) => {
+ console.error("Error stopping macOS audio:", err);
+ });
+ }
- // Clean up hidden elements
- if (hiddenVideo) {
- hiddenVideo.pause();
- hiddenVideo.srcObject = null;
- hiddenVideo = null;
- }
- offscreenCanvas = null;
- offscreenContext = null;
+ // Clean up hidden elements
+ if (hiddenVideo) {
+ hiddenVideo.pause();
+ hiddenVideo.srcObject = null;
+ hiddenVideo = null;
+ }
+ offscreenCanvas = null;
+ offscreenContext = null;
}
// Send text message to Gemini
async function sendTextMessage(text) {
- if (!text || text.trim().length === 0) {
- console.warn('Cannot send empty text message');
- return { success: false, error: 'Empty message' };
- }
+ 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);
- }
- return result;
- } catch (error) {
- console.error('Error sending text message:', error);
- return { success: false, error: error.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);
}
+ return result;
+ } catch (error) {
+ console.error("Error sending text message:", error);
+ return { success: false, error: error.message };
+ }
+}
+
+// Expand the last response — continues the conversation with a detailed version
+async function expandLastResponse() {
+ const app = cheatingDaddyApp;
+ if (!app || app.responses.length === 0 || app.currentResponseIndex < 0) {
+ console.warn("No response to expand");
+ return { success: false, error: "No response to expand" };
+ }
+
+ const previousResponse = app.responses[app.currentResponseIndex];
+ if (!previousResponse || previousResponse.trim().length === 0) {
+ console.warn("Current response is empty");
+ return { success: false, error: "Current response is empty" };
+ }
+
+ try {
+ const result = await ipcRenderer.invoke("expand-last-response", {
+ previousResponse: previousResponse,
+ originalContext: "",
+ });
+
+ if (result.success) {
+ console.log("Expand request sent successfully");
+ } else {
+ console.error("Failed to expand response:", result.error);
+ }
+ return result;
+ } catch (error) {
+ console.error("Error expanding response:", error);
+ return { success: false, error: error.message };
+ }
}
// Listen for conversation data from main process and save to storage
-ipcRenderer.on('save-conversation-turn', async (event, data) => {
- try {
- await storage.saveSession(data.sessionId, { conversationHistory: data.fullHistory });
- console.log('Conversation session saved:', data.sessionId);
- } catch (error) {
- console.error('Error saving conversation session:', error);
- }
+ipcRenderer.on("save-conversation-turn", async (event, data) => {
+ try {
+ await storage.saveSession(data.sessionId, {
+ conversationHistory: data.fullHistory,
+ });
+ console.log("Conversation session saved:", data.sessionId);
+ } catch (error) {
+ console.error("Error saving conversation session:", error);
+ }
});
// Listen for session context (profile info) when session starts
-ipcRenderer.on('save-session-context', async (event, data) => {
- try {
- await storage.saveSession(data.sessionId, {
- profile: data.profile,
- customPrompt: data.customPrompt
- });
- console.log('Session context saved:', data.sessionId, 'profile:', data.profile);
- } catch (error) {
- console.error('Error saving session context:', error);
- }
+ipcRenderer.on("save-session-context", async (event, data) => {
+ try {
+ await storage.saveSession(data.sessionId, {
+ profile: data.profile,
+ customPrompt: data.customPrompt,
+ });
+ console.log(
+ "Session context saved:",
+ data.sessionId,
+ "profile:",
+ data.profile,
+ );
+ } catch (error) {
+ console.error("Error saving session context:", error);
+ }
});
// Listen for screen analysis responses (from ctrl+enter)
-ipcRenderer.on('save-screen-analysis', async (event, data) => {
- try {
- await storage.saveSession(data.sessionId, {
- screenAnalysisHistory: data.fullHistory,
- profile: data.profile,
- customPrompt: data.customPrompt
- });
- console.log('Screen analysis saved:', data.sessionId);
- } catch (error) {
- console.error('Error saving screen analysis:', error);
- }
+ipcRenderer.on("save-screen-analysis", async (event, data) => {
+ try {
+ await storage.saveSession(data.sessionId, {
+ screenAnalysisHistory: data.fullHistory,
+ profile: data.profile,
+ customPrompt: data.customPrompt,
+ });
+ console.log("Screen analysis saved:", data.sessionId);
+ } catch (error) {
+ console.error("Error saving screen analysis:", error);
+ }
});
// Listen for emergency erase command from main process
-ipcRenderer.on('clear-sensitive-data', async () => {
- console.log('Clearing all data...');
- await storage.clearAll();
+ipcRenderer.on("clear-sensitive-data", async () => {
+ console.log("Clearing all data...");
+ await storage.clearAll();
});
// Handle shortcuts based on current view
function handleShortcut(shortcutKey) {
- const currentView = cheatingDaddy.getCurrentView();
+ const currentView = cheatingDaddy.getCurrentView();
- if (shortcutKey === 'ctrl+enter' || shortcutKey === 'cmd+enter') {
- if (currentView === 'main') {
- cheatingDaddy.element().handleStart();
- } else {
- captureManualScreenshot();
- }
+ if (shortcutKey === "ctrl+enter" || shortcutKey === "cmd+enter") {
+ if (currentView === "main") {
+ cheatingDaddy.element().handleStart();
+ } else {
+ captureManualScreenshot();
}
+ }
}
// Create reference to the main app element
-const cheatingDaddyApp = document.querySelector('cheating-daddy-app');
+const cheatingDaddyApp = document.querySelector("cheating-daddy-app");
// ============ THEME SYSTEM ============
const theme = {
- themes: {
- dark: {
- background: '#101010',
- text: '#e0e0e0', textSecondary: '#a0a0a0', textMuted: '#6b6b6b',
- border: '#2a2a2a', accent: '#ffffff',
- btnPrimaryBg: '#ffffff', btnPrimaryText: '#000000', btnPrimaryHover: '#e0e0e0',
- tooltipBg: '#1a1a1a', tooltipText: '#ffffff',
- keyBg: 'rgba(255,255,255,0.1)'
- },
- light: {
- background: '#ffffff',
- text: '#1a1a1a', textSecondary: '#555555', textMuted: '#888888',
- border: '#e0e0e0', accent: '#000000',
- btnPrimaryBg: '#1a1a1a', btnPrimaryText: '#ffffff', btnPrimaryHover: '#333333',
- tooltipBg: '#1a1a1a', tooltipText: '#ffffff',
- keyBg: 'rgba(0,0,0,0.1)'
- },
- midnight: {
- background: '#0d1117',
- text: '#c9d1d9', textSecondary: '#8b949e', textMuted: '#6e7681',
- border: '#30363d', accent: '#58a6ff',
- btnPrimaryBg: '#58a6ff', btnPrimaryText: '#0d1117', btnPrimaryHover: '#79b8ff',
- tooltipBg: '#161b22', tooltipText: '#c9d1d9',
- keyBg: 'rgba(88,166,255,0.15)'
- },
- sepia: {
- background: '#f4ecd8',
- text: '#5c4b37', textSecondary: '#7a6a56', textMuted: '#998875',
- border: '#d4c8b0', accent: '#8b4513',
- btnPrimaryBg: '#5c4b37', btnPrimaryText: '#f4ecd8', btnPrimaryHover: '#7a6a56',
- tooltipBg: '#5c4b37', tooltipText: '#f4ecd8',
- keyBg: 'rgba(92,75,55,0.15)'
- },
- catppuccin: {
- background: '#1e1e2e',
- text: '#cdd6f4', textSecondary: '#a6adc8', textMuted: '#585b70',
- border: '#313244', accent: '#cba6f7',
- btnPrimaryBg: '#cba6f7', btnPrimaryText: '#1e1e2e', btnPrimaryHover: '#b4befe',
- tooltipBg: '#313244', tooltipText: '#cdd6f4',
- keyBg: 'rgba(203,166,247,0.12)'
- },
- gruvbox: {
- background: '#1d2021',
- text: '#ebdbb2', textSecondary: '#a89984', textMuted: '#665c54',
- border: '#3c3836', accent: '#fe8019',
- btnPrimaryBg: '#fe8019', btnPrimaryText: '#1d2021', btnPrimaryHover: '#fabd2f',
- tooltipBg: '#3c3836', tooltipText: '#ebdbb2',
- keyBg: 'rgba(254,128,25,0.12)'
- },
- rosepine: {
- background: '#191724',
- text: '#e0def4', textSecondary: '#908caa', textMuted: '#6e6a86',
- border: '#26233a', accent: '#ebbcba',
- btnPrimaryBg: '#ebbcba', btnPrimaryText: '#191724', btnPrimaryHover: '#f6c177',
- tooltipBg: '#26233a', tooltipText: '#e0def4',
- keyBg: 'rgba(235,188,186,0.12)'
- },
- solarized: {
- background: '#002b36',
- text: '#93a1a1', textSecondary: '#839496', textMuted: '#586e75',
- border: '#073642', accent: '#2aa198',
- btnPrimaryBg: '#2aa198', btnPrimaryText: '#002b36', btnPrimaryHover: '#268bd2',
- tooltipBg: '#073642', tooltipText: '#93a1a1',
- keyBg: 'rgba(42,161,152,0.12)'
- },
- tokyonight: {
- background: '#1a1b26',
- text: '#c0caf5', textSecondary: '#9aa5ce', textMuted: '#565f89',
- border: '#292e42', accent: '#7aa2f7',
- btnPrimaryBg: '#7aa2f7', btnPrimaryText: '#1a1b26', btnPrimaryHover: '#bb9af7',
- tooltipBg: '#292e42', tooltipText: '#c0caf5',
- keyBg: 'rgba(122,162,247,0.12)'
- },
+ themes: {
+ dark: {
+ background: "#101010",
+ text: "#e0e0e0",
+ textSecondary: "#a0a0a0",
+ textMuted: "#6b6b6b",
+ border: "#2a2a2a",
+ accent: "#ffffff",
+ btnPrimaryBg: "#ffffff",
+ btnPrimaryText: "#000000",
+ btnPrimaryHover: "#e0e0e0",
+ tooltipBg: "#1a1a1a",
+ tooltipText: "#ffffff",
+ keyBg: "rgba(255,255,255,0.1)",
},
-
- current: 'dark',
-
- get(name) {
- return this.themes[name] || this.themes.dark;
+ light: {
+ background: "#ffffff",
+ text: "#1a1a1a",
+ textSecondary: "#555555",
+ textMuted: "#888888",
+ border: "#e0e0e0",
+ accent: "#000000",
+ btnPrimaryBg: "#1a1a1a",
+ btnPrimaryText: "#ffffff",
+ btnPrimaryHover: "#333333",
+ tooltipBg: "#1a1a1a",
+ tooltipText: "#ffffff",
+ keyBg: "rgba(0,0,0,0.1)",
},
-
- getAll() {
- const names = {
- dark: 'Dark',
- light: 'Light',
- midnight: 'Midnight Blue',
- sepia: 'Sepia',
- catppuccin: 'Catppuccin Mocha',
- gruvbox: 'Gruvbox Dark',
- rosepine: 'Ros\u00e9 Pine',
- solarized: 'Solarized Dark',
- tokyonight: 'Tokyo Night'
- };
- return Object.keys(this.themes).map(key => ({
- value: key,
- name: names[key] || key,
- colors: this.themes[key]
- }));
+ midnight: {
+ background: "#0d1117",
+ text: "#c9d1d9",
+ textSecondary: "#8b949e",
+ textMuted: "#6e7681",
+ border: "#30363d",
+ accent: "#58a6ff",
+ btnPrimaryBg: "#58a6ff",
+ btnPrimaryText: "#0d1117",
+ btnPrimaryHover: "#79b8ff",
+ tooltipBg: "#161b22",
+ tooltipText: "#c9d1d9",
+ keyBg: "rgba(88,166,255,0.15)",
},
-
- hexToRgb(hex) {
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
- return result ? {
- r: parseInt(result[1], 16),
- g: parseInt(result[2], 16),
- b: parseInt(result[3], 16)
- } : { r: 30, g: 30, b: 30 };
+ sepia: {
+ background: "#f4ecd8",
+ text: "#5c4b37",
+ textSecondary: "#7a6a56",
+ textMuted: "#998875",
+ border: "#d4c8b0",
+ accent: "#8b4513",
+ btnPrimaryBg: "#5c4b37",
+ btnPrimaryText: "#f4ecd8",
+ btnPrimaryHover: "#7a6a56",
+ tooltipBg: "#5c4b37",
+ tooltipText: "#f4ecd8",
+ keyBg: "rgba(92,75,55,0.15)",
},
-
- lightenColor(rgb, amount) {
- return {
- r: Math.min(255, rgb.r + amount),
- g: Math.min(255, rgb.g + amount),
- b: Math.min(255, rgb.b + amount)
- };
+ catppuccin: {
+ background: "#1e1e2e",
+ text: "#cdd6f4",
+ textSecondary: "#a6adc8",
+ textMuted: "#585b70",
+ border: "#313244",
+ accent: "#cba6f7",
+ btnPrimaryBg: "#cba6f7",
+ btnPrimaryText: "#1e1e2e",
+ btnPrimaryHover: "#b4befe",
+ tooltipBg: "#313244",
+ tooltipText: "#cdd6f4",
+ keyBg: "rgba(203,166,247,0.12)",
},
-
- darkenColor(rgb, amount) {
- return {
- r: Math.max(0, rgb.r - amount),
- g: Math.max(0, rgb.g - amount),
- b: Math.max(0, rgb.b - amount)
- };
+ gruvbox: {
+ background: "#1d2021",
+ text: "#ebdbb2",
+ textSecondary: "#a89984",
+ textMuted: "#665c54",
+ border: "#3c3836",
+ accent: "#fe8019",
+ btnPrimaryBg: "#fe8019",
+ btnPrimaryText: "#1d2021",
+ btnPrimaryHover: "#fabd2f",
+ tooltipBg: "#3c3836",
+ tooltipText: "#ebdbb2",
+ keyBg: "rgba(254,128,25,0.12)",
},
-
- applyBackgrounds(backgroundColor, alpha = 0.8) {
- const root = document.documentElement;
- const baseRgb = this.hexToRgb(backgroundColor);
-
- // For light themes, darken; for dark themes, lighten
- const isLight = (baseRgb.r + baseRgb.g + baseRgb.b) / 3 > 128;
- const adjust = isLight ? this.darkenColor.bind(this) : this.lightenColor.bind(this);
-
- const secondary = adjust(baseRgb, 10);
- const tertiary = adjust(baseRgb, 22);
- const hover = adjust(baseRgb, 28);
-
- const bgBase = `rgba(${baseRgb.r}, ${baseRgb.g}, ${baseRgb.b}, ${alpha})`;
- const bgSurface = `rgba(${secondary.r}, ${secondary.g}, ${secondary.b}, ${alpha})`;
- const bgElevated = `rgba(${tertiary.r}, ${tertiary.g}, ${tertiary.b}, ${alpha})`;
- const bgHover = `rgba(${hover.r}, ${hover.g}, ${hover.b}, ${alpha})`;
-
- // New design tokens (used by components)
- root.style.setProperty('--bg-app', bgBase);
- root.style.setProperty('--bg-surface', bgSurface);
- root.style.setProperty('--bg-elevated', bgElevated);
- root.style.setProperty('--bg-hover', bgHover);
-
- // Legacy aliases
- root.style.setProperty('--header-background', bgBase);
- root.style.setProperty('--main-content-background', bgBase);
- root.style.setProperty('--bg-primary', bgBase);
- root.style.setProperty('--bg-secondary', bgSurface);
- root.style.setProperty('--bg-tertiary', bgElevated);
- root.style.setProperty('--input-background', bgElevated);
- root.style.setProperty('--input-focus-background', bgElevated);
- root.style.setProperty('--hover-background', bgHover);
- root.style.setProperty('--scrollbar-background', bgBase);
+ rosepine: {
+ background: "#191724",
+ text: "#e0def4",
+ textSecondary: "#908caa",
+ textMuted: "#6e6a86",
+ border: "#26233a",
+ accent: "#ebbcba",
+ btnPrimaryBg: "#ebbcba",
+ btnPrimaryText: "#191724",
+ btnPrimaryHover: "#f6c177",
+ tooltipBg: "#26233a",
+ tooltipText: "#e0def4",
+ keyBg: "rgba(235,188,186,0.12)",
},
-
- apply(themeName, alpha = 0.8) {
- const colors = this.get(themeName);
- this.current = themeName;
- const root = document.documentElement;
-
- // New design tokens (used by components)
- root.style.setProperty('--text-primary', colors.text);
- root.style.setProperty('--text-secondary', colors.textSecondary);
- root.style.setProperty('--text-muted', colors.textMuted);
- root.style.setProperty('--border', colors.border);
- root.style.setProperty('--border-strong', colors.accent);
- root.style.setProperty('--accent', colors.btnPrimaryBg);
- root.style.setProperty('--accent-hover', colors.btnPrimaryHover);
-
- // Legacy aliases
- root.style.setProperty('--text-color', colors.text);
- root.style.setProperty('--border-color', colors.border);
- root.style.setProperty('--border-default', colors.accent);
- root.style.setProperty('--placeholder-color', colors.textMuted);
- root.style.setProperty('--scrollbar-thumb', colors.border);
- root.style.setProperty('--scrollbar-thumb-hover', colors.textMuted);
- root.style.setProperty('--key-background', colors.keyBg);
- // Primary button
- root.style.setProperty('--btn-primary-bg', colors.btnPrimaryBg);
- root.style.setProperty('--btn-primary-text', colors.btnPrimaryText);
- root.style.setProperty('--btn-primary-hover', colors.btnPrimaryHover);
- // Start button (same as primary)
- root.style.setProperty('--start-button-background', colors.btnPrimaryBg);
- root.style.setProperty('--start-button-color', colors.btnPrimaryText);
- root.style.setProperty('--start-button-hover-background', colors.btnPrimaryHover);
- // Tooltip
- root.style.setProperty('--tooltip-bg', colors.tooltipBg);
- root.style.setProperty('--tooltip-text', colors.tooltipText);
- // Error color (stays constant)
- root.style.setProperty('--error-color', '#f14c4c');
- root.style.setProperty('--success-color', '#4caf50');
-
- // Also apply background colors from theme
- this.applyBackgrounds(colors.background, alpha);
+ solarized: {
+ background: "#002b36",
+ text: "#93a1a1",
+ textSecondary: "#839496",
+ textMuted: "#586e75",
+ border: "#073642",
+ accent: "#2aa198",
+ btnPrimaryBg: "#2aa198",
+ btnPrimaryText: "#002b36",
+ btnPrimaryHover: "#268bd2",
+ tooltipBg: "#073642",
+ tooltipText: "#93a1a1",
+ keyBg: "rgba(42,161,152,0.12)",
},
+ tokyonight: {
+ background: "#1a1b26",
+ text: "#c0caf5",
+ textSecondary: "#9aa5ce",
+ textMuted: "#565f89",
+ border: "#292e42",
+ accent: "#7aa2f7",
+ btnPrimaryBg: "#7aa2f7",
+ btnPrimaryText: "#1a1b26",
+ btnPrimaryHover: "#bb9af7",
+ tooltipBg: "#292e42",
+ tooltipText: "#c0caf5",
+ keyBg: "rgba(122,162,247,0.12)",
+ },
+ },
- async load() {
- try {
- const prefs = await storage.getPreferences();
- const themeName = prefs.theme || 'dark';
- const alpha = prefs.backgroundTransparency ?? 0.8;
- this.apply(themeName, alpha);
- return themeName;
- } catch (err) {
- this.apply('dark');
- return 'dark';
+ current: "dark",
+
+ get(name) {
+ return this.themes[name] || this.themes.dark;
+ },
+
+ getAll() {
+ const names = {
+ dark: "Dark",
+ light: "Light",
+ midnight: "Midnight Blue",
+ sepia: "Sepia",
+ catppuccin: "Catppuccin Mocha",
+ gruvbox: "Gruvbox Dark",
+ rosepine: "Ros\u00e9 Pine",
+ solarized: "Solarized Dark",
+ tokyonight: "Tokyo Night",
+ };
+ return Object.keys(this.themes).map((key) => ({
+ value: key,
+ name: names[key] || key,
+ colors: this.themes[key],
+ }));
+ },
+
+ hexToRgb(hex) {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result
+ ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16),
}
- },
+ : { r: 30, g: 30, b: 30 };
+ },
- async save(themeName) {
- await storage.updatePreference('theme', themeName);
- this.apply(themeName);
+ lightenColor(rgb, amount) {
+ return {
+ r: Math.min(255, rgb.r + amount),
+ g: Math.min(255, rgb.g + amount),
+ b: Math.min(255, rgb.b + amount),
+ };
+ },
+
+ darkenColor(rgb, amount) {
+ return {
+ r: Math.max(0, rgb.r - amount),
+ g: Math.max(0, rgb.g - amount),
+ b: Math.max(0, rgb.b - amount),
+ };
+ },
+
+ applyBackgrounds(backgroundColor, alpha = 0.8) {
+ const root = document.documentElement;
+ const baseRgb = this.hexToRgb(backgroundColor);
+
+ // For light themes, darken; for dark themes, lighten
+ const isLight = (baseRgb.r + baseRgb.g + baseRgb.b) / 3 > 128;
+ const adjust = isLight
+ ? this.darkenColor.bind(this)
+ : this.lightenColor.bind(this);
+
+ const secondary = adjust(baseRgb, 10);
+ const tertiary = adjust(baseRgb, 22);
+ const hover = adjust(baseRgb, 28);
+
+ const bgBase = `rgba(${baseRgb.r}, ${baseRgb.g}, ${baseRgb.b}, ${alpha})`;
+ const bgSurface = `rgba(${secondary.r}, ${secondary.g}, ${secondary.b}, ${alpha})`;
+ const bgElevated = `rgba(${tertiary.r}, ${tertiary.g}, ${tertiary.b}, ${alpha})`;
+ const bgHover = `rgba(${hover.r}, ${hover.g}, ${hover.b}, ${alpha})`;
+
+ // New design tokens (used by components)
+ root.style.setProperty("--bg-app", bgBase);
+ root.style.setProperty("--bg-surface", bgSurface);
+ root.style.setProperty("--bg-elevated", bgElevated);
+ root.style.setProperty("--bg-hover", bgHover);
+
+ // Legacy aliases
+ root.style.setProperty("--header-background", bgBase);
+ root.style.setProperty("--main-content-background", bgBase);
+ root.style.setProperty("--bg-primary", bgBase);
+ root.style.setProperty("--bg-secondary", bgSurface);
+ root.style.setProperty("--bg-tertiary", bgElevated);
+ root.style.setProperty("--input-background", bgElevated);
+ root.style.setProperty("--input-focus-background", bgElevated);
+ root.style.setProperty("--hover-background", bgHover);
+ root.style.setProperty("--scrollbar-background", bgBase);
+ },
+
+ apply(themeName, alpha = 0.8) {
+ const colors = this.get(themeName);
+ this.current = themeName;
+ const root = document.documentElement;
+
+ // New design tokens (used by components)
+ root.style.setProperty("--text-primary", colors.text);
+ root.style.setProperty("--text-secondary", colors.textSecondary);
+ root.style.setProperty("--text-muted", colors.textMuted);
+ root.style.setProperty("--border", colors.border);
+ root.style.setProperty("--border-strong", colors.accent);
+ root.style.setProperty("--accent", colors.btnPrimaryBg);
+ root.style.setProperty("--accent-hover", colors.btnPrimaryHover);
+
+ // Legacy aliases
+ root.style.setProperty("--text-color", colors.text);
+ root.style.setProperty("--border-color", colors.border);
+ root.style.setProperty("--border-default", colors.accent);
+ root.style.setProperty("--placeholder-color", colors.textMuted);
+ root.style.setProperty("--scrollbar-thumb", colors.border);
+ root.style.setProperty("--scrollbar-thumb-hover", colors.textMuted);
+ root.style.setProperty("--key-background", colors.keyBg);
+ // Primary button
+ root.style.setProperty("--btn-primary-bg", colors.btnPrimaryBg);
+ root.style.setProperty("--btn-primary-text", colors.btnPrimaryText);
+ root.style.setProperty("--btn-primary-hover", colors.btnPrimaryHover);
+ // Start button (same as primary)
+ root.style.setProperty("--start-button-background", colors.btnPrimaryBg);
+ root.style.setProperty("--start-button-color", colors.btnPrimaryText);
+ root.style.setProperty(
+ "--start-button-hover-background",
+ colors.btnPrimaryHover,
+ );
+ // Tooltip
+ root.style.setProperty("--tooltip-bg", colors.tooltipBg);
+ root.style.setProperty("--tooltip-text", colors.tooltipText);
+ // Error color (stays constant)
+ root.style.setProperty("--error-color", "#f14c4c");
+ root.style.setProperty("--success-color", "#4caf50");
+
+ // Also apply background colors from theme
+ this.applyBackgrounds(colors.background, alpha);
+ },
+
+ async load() {
+ try {
+ const prefs = await storage.getPreferences();
+ const themeName = prefs.theme || "dark";
+ const alpha = prefs.backgroundTransparency ?? 0.8;
+ this.apply(themeName, alpha);
+ return themeName;
+ } catch (err) {
+ this.apply("dark");
+ return "dark";
}
+ },
+
+ async save(themeName) {
+ await storage.updatePreference("theme", themeName);
+ this.apply(themeName);
+ },
};
// Consolidated cheatingDaddy object - all functions in one place
const cheatingDaddy = {
- // App version
- getVersion: async () => ipcRenderer.invoke('get-app-version'),
+ // App version
+ getVersion: async () => ipcRenderer.invoke("get-app-version"),
- // Element access
- element: () => cheatingDaddyApp,
- e: () => cheatingDaddyApp,
+ // Element access
+ element: () => cheatingDaddyApp,
+ e: () => cheatingDaddyApp,
- // App state functions - access properties directly from the app element
- getCurrentView: () => cheatingDaddyApp.currentView,
- getLayoutMode: () => cheatingDaddyApp.layoutMode,
+ // App state functions - access properties directly from the app element
+ getCurrentView: () => cheatingDaddyApp.currentView,
+ getLayoutMode: () => cheatingDaddyApp.layoutMode,
- // Status and response functions
- setStatus: text => cheatingDaddyApp.setStatus(text),
- addNewResponse: response => cheatingDaddyApp.addNewResponse(response),
- updateCurrentResponse: response => cheatingDaddyApp.updateCurrentResponse(response),
+ // Status and response functions
+ setStatus: (text) => cheatingDaddyApp.setStatus(text),
+ addNewResponse: (response) => cheatingDaddyApp.addNewResponse(response),
+ updateCurrentResponse: (response) =>
+ cheatingDaddyApp.updateCurrentResponse(response),
- // Core functionality
- initializeGemini,
- initializeLocal,
- startCapture,
- stopCapture,
- sendTextMessage,
- handleShortcut,
+ // Core functionality
+ initializeGemini,
+ initializeLocal,
+ startCapture,
+ stopCapture,
+ sendTextMessage,
+ expandLastResponse,
+ handleShortcut,
- // Storage API
- storage,
+ // Storage API
+ storage,
- // Theme API
- theme,
+ // Theme API
+ theme,
- // Refresh preferences cache (call after updating preferences)
- refreshPreferencesCache: loadPreferencesCache,
+ // Refresh preferences cache (call after updating preferences)
+ refreshPreferencesCache: loadPreferencesCache,
- // Platform detection
- isLinux: isLinux,
- isMacOS: isMacOS,
+ // Platform detection
+ isLinux: isLinux,
+ isMacOS: isMacOS,
};
// Make it globally available
window.cheatingDaddy = cheatingDaddy;
// Load theme after DOM is ready
-if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', () => theme.load());
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", () => theme.load());
} else {
- theme.load();
+ theme.load();
}
diff --git a/src/utils/window.js b/src/utils/window.js
index 7b9f533..cf7e8c5 100644
--- a/src/utils/window.js
+++ b/src/utils/window.js
@@ -1,368 +1,429 @@
-const { BrowserWindow, globalShortcut, ipcMain, screen } = require('electron');
-const path = require('node:path');
-const storage = require('../storage');
+const { BrowserWindow, globalShortcut, ipcMain, screen } = require("electron");
+const path = require("node:path");
+const storage = require("../storage");
let mouseEventsIgnored = false;
function createWindow(sendToRenderer, geminiSessionRef) {
- // Get layout preference (default to 'normal')
- let windowWidth = 1100;
- let windowHeight = 800;
+ // Get layout preference (default to 'normal')
+ let windowWidth = 1100;
+ let windowHeight = 800;
- const mainWindow = new BrowserWindow({
- width: windowWidth,
- height: windowHeight,
- frame: false,
- transparent: true,
- hasShadow: false,
- alwaysOnTop: true,
- webPreferences: {
- nodeIntegration: true,
- contextIsolation: false, // TODO: change to true
- backgroundThrottling: false,
- enableBlinkFeatures: 'GetDisplayMedia',
- webSecurity: true,
- allowRunningInsecureContent: false,
- },
- backgroundColor: '#00000000',
- });
+ const mainWindow = new BrowserWindow({
+ width: windowWidth,
+ height: windowHeight,
+ frame: false,
+ transparent: true,
+ hasShadow: false,
+ alwaysOnTop: true,
+ webPreferences: {
+ nodeIntegration: true,
+ contextIsolation: false, // TODO: change to true
+ backgroundThrottling: false,
+ enableBlinkFeatures: "GetDisplayMedia",
+ webSecurity: true,
+ allowRunningInsecureContent: false,
+ },
+ backgroundColor: "#00000000",
+ });
- const { session, desktopCapturer } = require('electron');
- session.defaultSession.setDisplayMediaRequestHandler(
- (request, callback) => {
- desktopCapturer.getSources({ types: ['screen'] }).then(sources => {
- callback({ video: sources[0], audio: 'loopback' });
- });
- },
- { useSystemPicker: true }
- );
+ const { session, desktopCapturer } = require("electron");
+ session.defaultSession.setDisplayMediaRequestHandler(
+ (request, callback) => {
+ desktopCapturer.getSources({ types: ["screen"] }).then((sources) => {
+ callback({ video: sources[0], audio: "loopback" });
+ });
+ },
+ { useSystemPicker: true },
+ );
- mainWindow.setResizable(false);
- mainWindow.setContentProtection(true);
- mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
+ mainWindow.setResizable(false);
+ mainWindow.setContentProtection(true);
+ mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
- // Hide from Windows taskbar
- if (process.platform === 'win32') {
- try {
- mainWindow.setSkipTaskbar(true);
- } catch (error) {
- console.warn('Could not hide from taskbar:', error.message);
- }
+ // Hide from Windows taskbar
+ if (process.platform === "win32") {
+ try {
+ mainWindow.setSkipTaskbar(true);
+ } catch (error) {
+ console.warn("Could not hide from taskbar:", error.message);
}
+ }
- // Hide from Mission Control on macOS
- if (process.platform === 'darwin') {
- try {
- mainWindow.setHiddenInMissionControl(true);
- } catch (error) {
- console.warn('Could not hide from Mission Control:', error.message);
- }
+ // Hide from Mission Control on macOS
+ if (process.platform === "darwin") {
+ try {
+ mainWindow.setHiddenInMissionControl(true);
+ } catch (error) {
+ console.warn("Could not hide from Mission Control:", error.message);
}
+ }
- // Center window at the top of the screen
- const primaryDisplay = screen.getPrimaryDisplay();
- const { width: screenWidth } = primaryDisplay.workAreaSize;
- const x = Math.floor((screenWidth - windowWidth) / 2);
- const y = 0;
- mainWindow.setPosition(x, y);
+ // Center window at the top of the screen
+ const primaryDisplay = screen.getPrimaryDisplay();
+ const { width: screenWidth } = primaryDisplay.workAreaSize;
+ const x = Math.floor((screenWidth - windowWidth) / 2);
+ const y = 0;
+ mainWindow.setPosition(x, y);
- if (process.platform === 'win32') {
- mainWindow.setAlwaysOnTop(true, 'screen-saver', 1);
- }
+ if (process.platform === "win32") {
+ mainWindow.setAlwaysOnTop(true, "screen-saver", 1);
+ }
- mainWindow.loadFile(path.join(__dirname, '../index.html'));
+ mainWindow.loadFile(path.join(__dirname, "../index.html"));
- // After window is created, initialize keybinds
- mainWindow.webContents.once('dom-ready', () => {
- setTimeout(() => {
- const defaultKeybinds = getDefaultKeybinds();
- let keybinds = defaultKeybinds;
+ // After window is created, initialize keybinds
+ mainWindow.webContents.once("dom-ready", () => {
+ setTimeout(() => {
+ const defaultKeybinds = getDefaultKeybinds();
+ let keybinds = defaultKeybinds;
- // Load keybinds from storage
- const savedKeybinds = storage.getKeybinds();
- if (savedKeybinds) {
- keybinds = { ...defaultKeybinds, ...savedKeybinds };
- }
+ // Load keybinds from storage
+ const savedKeybinds = storage.getKeybinds();
+ if (savedKeybinds) {
+ keybinds = { ...defaultKeybinds, ...savedKeybinds };
+ }
- updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessionRef);
- }, 150);
- });
+ updateGlobalShortcuts(
+ keybinds,
+ mainWindow,
+ sendToRenderer,
+ geminiSessionRef,
+ );
+ }, 150);
+ });
- setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef);
+ setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef);
- return mainWindow;
+ return mainWindow;
}
function getDefaultKeybinds() {
- const isMac = process.platform === 'darwin';
- return {
- moveUp: isMac ? 'Alt+Up' : 'Ctrl+Up',
- moveDown: isMac ? 'Alt+Down' : 'Ctrl+Down',
- moveLeft: isMac ? 'Alt+Left' : 'Ctrl+Left',
- moveRight: isMac ? 'Alt+Right' : 'Ctrl+Right',
- toggleVisibility: isMac ? 'Cmd+\\' : 'Ctrl+\\',
- toggleClickThrough: isMac ? 'Cmd+M' : 'Ctrl+M',
- nextStep: isMac ? 'Cmd+Enter' : 'Ctrl+Enter',
- previousResponse: isMac ? 'Cmd+[' : 'Ctrl+[',
- nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]',
- scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up',
- scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down',
- emergencyErase: isMac ? 'Cmd+Shift+E' : 'Ctrl+Shift+E',
- };
+ const isMac = process.platform === "darwin";
+ return {
+ moveUp: isMac ? "Alt+Up" : "Ctrl+Up",
+ moveDown: isMac ? "Alt+Down" : "Ctrl+Down",
+ moveLeft: isMac ? "Alt+Left" : "Ctrl+Left",
+ moveRight: isMac ? "Alt+Right" : "Ctrl+Right",
+ toggleVisibility: isMac ? "Cmd+\\" : "Ctrl+\\",
+ toggleClickThrough: isMac ? "Cmd+M" : "Ctrl+M",
+ nextStep: isMac ? "Cmd+Enter" : "Ctrl+Enter",
+ previousResponse: isMac ? "Cmd+[" : "Ctrl+[",
+ nextResponse: isMac ? "Cmd+]" : "Ctrl+]",
+ scrollUp: isMac ? "Cmd+Shift+Up" : "Ctrl+Shift+Up",
+ scrollDown: isMac ? "Cmd+Shift+Down" : "Ctrl+Shift+Down",
+ expandResponse: isMac ? "Cmd+E" : "Ctrl+E",
+ emergencyErase: isMac ? "Cmd+Shift+E" : "Ctrl+Shift+E",
+ };
}
-function updateGlobalShortcuts(keybinds, mainWindow, sendToRenderer, geminiSessionRef) {
- console.log('Updating global shortcuts with:', keybinds);
+function updateGlobalShortcuts(
+ keybinds,
+ mainWindow,
+ sendToRenderer,
+ geminiSessionRef,
+) {
+ console.log("Updating global shortcuts with:", keybinds);
- // Unregister all existing shortcuts
- globalShortcut.unregisterAll();
+ // Unregister all existing shortcuts
+ globalShortcut.unregisterAll();
- const primaryDisplay = screen.getPrimaryDisplay();
- const { width, height } = primaryDisplay.workAreaSize;
- const moveIncrement = Math.floor(Math.min(width, height) * 0.1);
+ const primaryDisplay = screen.getPrimaryDisplay();
+ const { width, height } = primaryDisplay.workAreaSize;
+ const moveIncrement = Math.floor(Math.min(width, height) * 0.1);
- // Register window movement shortcuts
- const movementActions = {
- moveUp: () => {
- if (!mainWindow.isVisible()) return;
- const [currentX, currentY] = mainWindow.getPosition();
- mainWindow.setPosition(currentX, currentY - moveIncrement);
- },
- moveDown: () => {
- if (!mainWindow.isVisible()) return;
- const [currentX, currentY] = mainWindow.getPosition();
- mainWindow.setPosition(currentX, currentY + moveIncrement);
- },
- moveLeft: () => {
- if (!mainWindow.isVisible()) return;
- const [currentX, currentY] = mainWindow.getPosition();
- mainWindow.setPosition(currentX - moveIncrement, currentY);
- },
- moveRight: () => {
- if (!mainWindow.isVisible()) return;
- const [currentX, currentY] = mainWindow.getPosition();
- mainWindow.setPosition(currentX + moveIncrement, currentY);
- },
- };
+ // Register window movement shortcuts
+ const movementActions = {
+ moveUp: () => {
+ if (!mainWindow.isVisible()) return;
+ const [currentX, currentY] = mainWindow.getPosition();
+ mainWindow.setPosition(currentX, currentY - moveIncrement);
+ },
+ moveDown: () => {
+ if (!mainWindow.isVisible()) return;
+ const [currentX, currentY] = mainWindow.getPosition();
+ mainWindow.setPosition(currentX, currentY + moveIncrement);
+ },
+ moveLeft: () => {
+ if (!mainWindow.isVisible()) return;
+ const [currentX, currentY] = mainWindow.getPosition();
+ mainWindow.setPosition(currentX - moveIncrement, currentY);
+ },
+ moveRight: () => {
+ if (!mainWindow.isVisible()) return;
+ const [currentX, currentY] = mainWindow.getPosition();
+ mainWindow.setPosition(currentX + moveIncrement, currentY);
+ },
+ };
- // Register each movement shortcut
- Object.keys(movementActions).forEach(action => {
- const keybind = keybinds[action];
- if (keybind) {
- try {
- globalShortcut.register(keybind, movementActions[action]);
- console.log(`Registered ${action}: ${keybind}`);
- } catch (error) {
- console.error(`Failed to register ${action} (${keybind}):`, error);
- }
- }
- });
-
- // Register toggle visibility shortcut
- if (keybinds.toggleVisibility) {
- try {
- globalShortcut.register(keybinds.toggleVisibility, () => {
- if (mainWindow.isVisible()) {
- mainWindow.hide();
- } else {
- mainWindow.showInactive();
- }
- });
- console.log(`Registered toggleVisibility: ${keybinds.toggleVisibility}`);
- } catch (error) {
- console.error(`Failed to register toggleVisibility (${keybinds.toggleVisibility}):`, error);
- }
+ // Register each movement shortcut
+ Object.keys(movementActions).forEach((action) => {
+ const keybind = keybinds[action];
+ if (keybind) {
+ try {
+ globalShortcut.register(keybind, movementActions[action]);
+ console.log(`Registered ${action}: ${keybind}`);
+ } catch (error) {
+ console.error(`Failed to register ${action} (${keybind}):`, error);
+ }
}
+ });
- // Register toggle click-through shortcut
- if (keybinds.toggleClickThrough) {
- try {
- globalShortcut.register(keybinds.toggleClickThrough, () => {
- mouseEventsIgnored = !mouseEventsIgnored;
- if (mouseEventsIgnored) {
- mainWindow.setIgnoreMouseEvents(true, { forward: true });
- console.log('Mouse events ignored');
- } else {
- mainWindow.setIgnoreMouseEvents(false);
- console.log('Mouse events enabled');
- }
- mainWindow.webContents.send('click-through-toggled', mouseEventsIgnored);
- });
- console.log(`Registered toggleClickThrough: ${keybinds.toggleClickThrough}`);
- } catch (error) {
- console.error(`Failed to register toggleClickThrough (${keybinds.toggleClickThrough}):`, error);
+ // Register toggle visibility shortcut
+ if (keybinds.toggleVisibility) {
+ try {
+ globalShortcut.register(keybinds.toggleVisibility, () => {
+ if (mainWindow.isVisible()) {
+ mainWindow.hide();
+ } else {
+ mainWindow.showInactive();
}
+ });
+ console.log(`Registered toggleVisibility: ${keybinds.toggleVisibility}`);
+ } catch (error) {
+ console.error(
+ `Failed to register toggleVisibility (${keybinds.toggleVisibility}):`,
+ error,
+ );
}
+ }
- // Register next step shortcut (either starts session or takes screenshot based on view)
- if (keybinds.nextStep) {
+ // Register toggle click-through shortcut
+ if (keybinds.toggleClickThrough) {
+ try {
+ globalShortcut.register(keybinds.toggleClickThrough, () => {
+ mouseEventsIgnored = !mouseEventsIgnored;
+ if (mouseEventsIgnored) {
+ mainWindow.setIgnoreMouseEvents(true, { forward: true });
+ console.log("Mouse events ignored");
+ } else {
+ mainWindow.setIgnoreMouseEvents(false);
+ console.log("Mouse events enabled");
+ }
+ mainWindow.webContents.send(
+ "click-through-toggled",
+ mouseEventsIgnored,
+ );
+ });
+ console.log(
+ `Registered toggleClickThrough: ${keybinds.toggleClickThrough}`,
+ );
+ } catch (error) {
+ console.error(
+ `Failed to register toggleClickThrough (${keybinds.toggleClickThrough}):`,
+ error,
+ );
+ }
+ }
+
+ // Register next step shortcut (either starts session or takes screenshot based on view)
+ if (keybinds.nextStep) {
+ try {
+ globalShortcut.register(keybinds.nextStep, async () => {
+ console.log("Next step shortcut triggered");
try {
- globalShortcut.register(keybinds.nextStep, async () => {
- console.log('Next step shortcut triggered');
- try {
- // Determine the shortcut key format
- const isMac = process.platform === 'darwin';
- const shortcutKey = isMac ? 'cmd+enter' : 'ctrl+enter';
+ // Determine the shortcut key format
+ const isMac = process.platform === "darwin";
+ const shortcutKey = isMac ? "cmd+enter" : "ctrl+enter";
- // Use the new handleShortcut function
- mainWindow.webContents.executeJavaScript(`
+ // Use the new handleShortcut function
+ mainWindow.webContents.executeJavaScript(`
cheatingDaddy.handleShortcut('${shortcutKey}');
`);
- } catch (error) {
- console.error('Error handling next step shortcut:', error);
- }
- });
- console.log(`Registered nextStep: ${keybinds.nextStep}`);
} catch (error) {
- console.error(`Failed to register nextStep (${keybinds.nextStep}):`, error);
+ console.error("Error handling next step shortcut:", error);
}
+ });
+ console.log(`Registered nextStep: ${keybinds.nextStep}`);
+ } catch (error) {
+ console.error(
+ `Failed to register nextStep (${keybinds.nextStep}):`,
+ error,
+ );
}
+ }
- // Register previous response shortcut
- if (keybinds.previousResponse) {
- try {
- globalShortcut.register(keybinds.previousResponse, () => {
- console.log('Previous response shortcut triggered');
- sendToRenderer('navigate-previous-response');
- });
- console.log(`Registered previousResponse: ${keybinds.previousResponse}`);
- } catch (error) {
- console.error(`Failed to register previousResponse (${keybinds.previousResponse}):`, error);
- }
- }
-
- // Register next response shortcut
- if (keybinds.nextResponse) {
- try {
- globalShortcut.register(keybinds.nextResponse, () => {
- console.log('Next response shortcut triggered');
- sendToRenderer('navigate-next-response');
- });
- console.log(`Registered nextResponse: ${keybinds.nextResponse}`);
- } catch (error) {
- console.error(`Failed to register nextResponse (${keybinds.nextResponse}):`, error);
- }
- }
-
- // Register scroll up shortcut
- if (keybinds.scrollUp) {
- try {
- globalShortcut.register(keybinds.scrollUp, () => {
- console.log('Scroll up shortcut triggered');
- sendToRenderer('scroll-response-up');
- });
- console.log(`Registered scrollUp: ${keybinds.scrollUp}`);
- } catch (error) {
- console.error(`Failed to register scrollUp (${keybinds.scrollUp}):`, error);
- }
- }
-
- // Register scroll down shortcut
- if (keybinds.scrollDown) {
- try {
- globalShortcut.register(keybinds.scrollDown, () => {
- console.log('Scroll down shortcut triggered');
- sendToRenderer('scroll-response-down');
- });
- console.log(`Registered scrollDown: ${keybinds.scrollDown}`);
- } catch (error) {
- console.error(`Failed to register scrollDown (${keybinds.scrollDown}):`, error);
- }
- }
-
- // Register emergency erase shortcut
- if (keybinds.emergencyErase) {
- try {
- globalShortcut.register(keybinds.emergencyErase, () => {
- console.log('Emergency Erase triggered!');
- if (mainWindow && !mainWindow.isDestroyed()) {
- mainWindow.hide();
-
- if (geminiSessionRef.current) {
- geminiSessionRef.current.close();
- geminiSessionRef.current = null;
- }
-
- sendToRenderer('clear-sensitive-data');
-
- setTimeout(() => {
- const { app } = require('electron');
- app.quit();
- }, 300);
- }
- });
- console.log(`Registered emergencyErase: ${keybinds.emergencyErase}`);
- } catch (error) {
- console.error(`Failed to register emergencyErase (${keybinds.emergencyErase}):`, error);
+ // Register previous response shortcut
+ if (keybinds.previousResponse) {
+ try {
+ globalShortcut.register(keybinds.previousResponse, () => {
+ console.log("Previous response shortcut triggered");
+ sendToRenderer("navigate-previous-response");
+ });
+ console.log(`Registered previousResponse: ${keybinds.previousResponse}`);
+ } catch (error) {
+ console.error(
+ `Failed to register previousResponse (${keybinds.previousResponse}):`,
+ error,
+ );
+ }
+ }
+
+ // Register next response shortcut
+ if (keybinds.nextResponse) {
+ try {
+ globalShortcut.register(keybinds.nextResponse, () => {
+ console.log("Next response shortcut triggered");
+ sendToRenderer("navigate-next-response");
+ });
+ console.log(`Registered nextResponse: ${keybinds.nextResponse}`);
+ } catch (error) {
+ console.error(
+ `Failed to register nextResponse (${keybinds.nextResponse}):`,
+ error,
+ );
+ }
+ }
+
+ // Register scroll up shortcut
+ if (keybinds.scrollUp) {
+ try {
+ globalShortcut.register(keybinds.scrollUp, () => {
+ console.log("Scroll up shortcut triggered");
+ sendToRenderer("scroll-response-up");
+ });
+ console.log(`Registered scrollUp: ${keybinds.scrollUp}`);
+ } catch (error) {
+ console.error(
+ `Failed to register scrollUp (${keybinds.scrollUp}):`,
+ error,
+ );
+ }
+ }
+
+ // Register scroll down shortcut
+ if (keybinds.scrollDown) {
+ try {
+ globalShortcut.register(keybinds.scrollDown, () => {
+ console.log("Scroll down shortcut triggered");
+ sendToRenderer("scroll-response-down");
+ });
+ console.log(`Registered scrollDown: ${keybinds.scrollDown}`);
+ } catch (error) {
+ console.error(
+ `Failed to register scrollDown (${keybinds.scrollDown}):`,
+ error,
+ );
+ }
+ }
+
+ // Register expand response shortcut
+ if (keybinds.expandResponse) {
+ try {
+ globalShortcut.register(keybinds.expandResponse, () => {
+ console.log("Expand response shortcut triggered");
+ sendToRenderer("expand-response");
+ });
+ console.log(`Registered expandResponse: ${keybinds.expandResponse}`);
+ } catch (error) {
+ console.error(
+ `Failed to register expandResponse (${keybinds.expandResponse}):`,
+ error,
+ );
+ }
+ }
+
+ // Register emergency erase shortcut
+ if (keybinds.emergencyErase) {
+ try {
+ globalShortcut.register(keybinds.emergencyErase, () => {
+ console.log("Emergency Erase triggered!");
+ if (mainWindow && !mainWindow.isDestroyed()) {
+ mainWindow.hide();
+
+ if (geminiSessionRef.current) {
+ geminiSessionRef.current.close();
+ geminiSessionRef.current = null;
+ }
+
+ sendToRenderer("clear-sensitive-data");
+
+ setTimeout(() => {
+ const { app } = require("electron");
+ app.quit();
+ }, 300);
}
+ });
+ console.log(`Registered emergencyErase: ${keybinds.emergencyErase}`);
+ } catch (error) {
+ console.error(
+ `Failed to register emergencyErase (${keybinds.emergencyErase}):`,
+ error,
+ );
}
+ }
}
function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
- ipcMain.on('view-changed', (event, view) => {
- if (!mainWindow.isDestroyed()) {
- const primaryDisplay = screen.getPrimaryDisplay();
- const { width: screenWidth } = primaryDisplay.workAreaSize;
+ ipcMain.on("view-changed", (event, view) => {
+ if (!mainWindow.isDestroyed()) {
+ const primaryDisplay = screen.getPrimaryDisplay();
+ const { width: screenWidth } = primaryDisplay.workAreaSize;
- if (view === 'assistant') {
- // Shrink window for live view
- const liveWidth = 850;
- const liveHeight = 400;
- const x = Math.floor((screenWidth - liveWidth) / 2);
- mainWindow.setSize(liveWidth, liveHeight);
- mainWindow.setPosition(x, 0);
- } else {
- // Restore full size
- const fullWidth = 1100;
- const fullHeight = 800;
- const x = Math.floor((screenWidth - fullWidth) / 2);
- mainWindow.setSize(fullWidth, fullHeight);
- mainWindow.setPosition(x, 0);
- mainWindow.setIgnoreMouseEvents(false);
- }
- }
- });
+ if (view === "assistant") {
+ // Shrink window for live view
+ const liveWidth = 850;
+ const liveHeight = 400;
+ const x = Math.floor((screenWidth - liveWidth) / 2);
+ mainWindow.setSize(liveWidth, liveHeight);
+ mainWindow.setPosition(x, 0);
+ } else {
+ // Restore full size
+ const fullWidth = 1100;
+ const fullHeight = 800;
+ const x = Math.floor((screenWidth - fullWidth) / 2);
+ mainWindow.setSize(fullWidth, fullHeight);
+ mainWindow.setPosition(x, 0);
+ mainWindow.setIgnoreMouseEvents(false);
+ }
+ }
+ });
- ipcMain.handle('window-minimize', () => {
- if (!mainWindow.isDestroyed()) {
- mainWindow.minimize();
- }
- });
+ ipcMain.handle("window-minimize", () => {
+ if (!mainWindow.isDestroyed()) {
+ mainWindow.minimize();
+ }
+ });
- ipcMain.on('update-keybinds', (event, newKeybinds) => {
- if (!mainWindow.isDestroyed()) {
- updateGlobalShortcuts(newKeybinds, mainWindow, sendToRenderer, geminiSessionRef);
- }
- });
+ ipcMain.on("update-keybinds", (event, newKeybinds) => {
+ if (!mainWindow.isDestroyed()) {
+ updateGlobalShortcuts(
+ newKeybinds,
+ mainWindow,
+ sendToRenderer,
+ geminiSessionRef,
+ );
+ }
+ });
- ipcMain.handle('toggle-window-visibility', async event => {
- try {
- if (mainWindow.isDestroyed()) {
- return { success: false, error: 'Window has been destroyed' };
- }
+ ipcMain.handle("toggle-window-visibility", async (event) => {
+ try {
+ if (mainWindow.isDestroyed()) {
+ return { success: false, error: "Window has been destroyed" };
+ }
- if (mainWindow.isVisible()) {
- mainWindow.hide();
- } else {
- mainWindow.showInactive();
- }
- return { success: true };
- } catch (error) {
- console.error('Error toggling window visibility:', error);
- return { success: false, error: error.message };
- }
- });
+ if (mainWindow.isVisible()) {
+ mainWindow.hide();
+ } else {
+ mainWindow.showInactive();
+ }
+ return { success: true };
+ } catch (error) {
+ console.error("Error toggling window visibility:", error);
+ return { success: false, error: error.message };
+ }
+ });
- ipcMain.handle('update-sizes', async event => {
- // With the sidebar layout, the window size is user-controlled.
- // This handler is kept for compatibility but is a no-op now.
- return { success: true };
- });
+ ipcMain.handle("update-sizes", async (event) => {
+ // With the sidebar layout, the window size is user-controlled.
+ // This handler is kept for compatibility but is a no-op now.
+ return { success: true };
+ });
}
module.exports = {
- createWindow,
- getDefaultKeybinds,
- updateGlobalShortcuts,
- setupWindowIpcHandlers,
+ createWindow,
+ getDefaultKeybinds,
+ updateGlobalShortcuts,
+ setupWindowIpcHandlers,
};