Files
Mastermind/src/utils/renderer.js
T

1204 lines
36 KiB
JavaScript

// 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();
}