// renderer.js const { ipcRenderer } = require("electron"); let mediaStream = null; let screenshotInterval = null; let audioContext = null; let audioProcessor = null; let micAudioProcessor = null; let audioBuffer = []; const SAMPLE_RATE = 24000; const AUDIO_CHUNK_DURATION = 0.1; // seconds 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 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); }, // 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); }, // 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"); }, // 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 } }; }, }; // Cache for preferences to avoid async calls in hot paths let preferencesCache = null; async function loadPreferencesCache() { 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; } 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); } 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") { 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); }); 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"; 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, ); } // 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", ); 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); } } } 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); let audioBuffer = []; const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION; 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); await ipcRenderer.invoke("send-mic-audio-content", { data: base64Data, mimeType: "audio/pcm;rate=24000", }); } }; micSource.connect(micProcessor); micProcessor.connect(micAudioContext.destination); // 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); let audioBuffer = []; const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION; 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); await ipcRenderer.invoke("send-audio-content", { data: base64Data, mimeType: "audio/pcm;rate=24000", }); } }; 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); let audioBuffer = []; const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION; 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); await ipcRenderer.invoke("send-audio-content", { data: base64Data, mimeType: "audio/pcm;rate=24000", }); } }; source.connect(audioProcessor); audioProcessor.connect(audioContext.destination); } 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(); 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; }); if (isBlank) { console.warn("Screenshot appears to be blank/black"); } 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 } offscreenCanvas.toBlob( async (blob) => { if (!blob) { console.error("Failed to create blob from canvas"); return; } const reader = new FileReader(); reader.onloadend = async () => { const base64data = reader.result.split(",")[1]; // 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, }); 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 = `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; } const reader = new FileReader(); reader.onloadend = async () => { const base64data = reader.result.split(",")[1]; 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, ); } // Expose functions to global scope for external access window.captureManualScreenshot = captureManualScreenshot; function stopCapture() { if (screenshotInterval) { clearInterval(screenshotInterval); screenshotInterval = null; } if (audioProcessor) { audioProcessor.disconnect(); audioProcessor = null; } // Clean up microphone audio processor (Linux only) if (micAudioProcessor) { micAudioProcessor.disconnect(); micAudioProcessor = null; } if (audioContext) { audioContext.close(); audioContext = 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); }); } // 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" }; } 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); } }); // 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); } }); // 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); } }); // Listen for emergency erase command from main process 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(); 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"); // ============ 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)", }, }, 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 }; }, 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; // Determine if theme is light or dark const lightThemes = ["light", "sepia"]; const isLightTheme = lightThemes.includes(themeName); document.body.setAttribute("data-theme-type", isLightTheme ? "light" : "dark"); // 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"), // Element access element: () => cheatingDaddyApp, e: () => cheatingDaddyApp, // 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), // Core functionality initializeGemini, initializeLocal, startCapture, stopCapture, sendTextMessage, expandLastResponse, handleShortcut, // Storage API storage, // Theme API theme, // Refresh preferences cache (call after updating preferences) refreshPreferencesCache: loadPreferencesCache, // 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()); } else { theme.load(); }