huge refactor

This commit is contained in:
Илья Глазунов
2026-02-14 04:17:46 +03:00
parent 3a8d9705a2
commit 310b6b3fbd
37 changed files with 5899 additions and 8218 deletions
+201 -484
View File
@@ -18,29 +18,6 @@ let currentImageQuality = 'medium'; // Store current image quality for manual sc
const isLinux = process.platform === 'linux';
const isMacOS = process.platform === 'darwin';
const isWindows = process.platform === 'win32';
// Send logs to main process for file logging
function logToMain(level, ...args) {
const message = args
.map(arg => {
if (typeof arg === 'object') {
try {
return JSON.stringify(arg);
} catch {
return String(arg);
}
}
return String(arg);
})
.join(' ');
ipcRenderer.send('renderer-log', { level, message });
// Also log to console
if (level === 'error') console.error(...args);
else if (level === 'warn') console.warn(...args);
else console.log(...args);
}
// ============ STORAGE API ============
// Wrapper for IPC-based storage access
@@ -72,19 +49,12 @@ const storage = {
async setApiKey(apiKey) {
return ipcRenderer.invoke('storage:set-api-key', apiKey);
},
async getOpenAICredentials() {
const result = await ipcRenderer.invoke('storage:get-openai-credentials');
return result.success ? result.data : {};
async getGroqApiKey() {
const result = await ipcRenderer.invoke('storage:get-groq-api-key');
return result.success ? result.data : '';
},
async setOpenAICredentials(config) {
return ipcRenderer.invoke('storage:set-openai-credentials', config);
},
async getOpenAISDKCredentials() {
const result = await ipcRenderer.invoke('storage:get-openai-sdk-credentials');
return result.success ? result.data : {};
},
async setOpenAISDKCredentials(config) {
return ipcRenderer.invoke('storage:set-openai-sdk-credentials', config);
async setGroqApiKey(groqApiKey) {
return ipcRenderer.invoke('storage:set-groq-api-key', groqApiKey);
},
// Preferences
@@ -136,18 +106,7 @@ const storage = {
async getTodayLimits() {
const result = await ipcRenderer.invoke('storage:get-today-limits');
return result.success ? result.data : { flash: { count: 0 }, flashLite: { count: 0 } };
},
// Migration
hasOldConfig() {
// Note: This is synchronous in the main process, but we need to use invoke which is async
// So we'll make this return a promise
return ipcRenderer.invoke('storage:has-old-config').then(result => (result.success ? result.data : false));
},
async migrateFromOldConfig() {
const result = await ipcRenderer.invoke('storage:migrate-from-old-config');
return result.success ? result.data : false;
},
}
};
// Cache for preferences to avoid async calls in hot paths
@@ -182,23 +141,39 @@ function arrayBufferToBase64(buffer) {
}
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 success = await ipcRenderer.invoke('initialize-ai-session', prefs.customPrompt || '', profile, language);
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) {
mastermind.setStatus('Live');
cheatingDaddy.setStatus('Local AI Live');
return true;
} else {
mastermind.setStatus('Error: Failed to initialize AI session Gemini');
cheatingDaddy.setStatus('error');
return false;
}
}
// Listen for status updates
ipcRenderer.on('update-status', (event, status) => {
console.log('Status update:', status);
mastermind.setStatus(status);
});
ipcRenderer.on('push-to-talk-toggle', () => {
ipcRenderer.send('push-to-talk-toggle');
cheatingDaddy.setStatus(status);
});
async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'medium') {
@@ -315,21 +290,7 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
console.log('Linux capture started - system audio:', mediaStream.getAudioTracks().length > 0, 'microphone mode:', audioMode);
} else {
// Windows - show custom screen picker first
logToMain('info', '=== Starting Windows audio capture ===');
mastermind.setStatus('Choose screen to share...');
// Show screen picker dialog
const appElement = document.querySelector('mastermind-app');
const pickerResult = await appElement.showScreenPickerDialog();
if (pickerResult.cancelled) {
mastermind.setStatus('Cancelled');
return;
}
mastermind.setStatus('Starting capture...');
// Windows - use display media with loopback for system audio
mediaStream = await navigator.mediaDevices.getDisplayMedia({
video: {
frameRate: 1,
@@ -345,29 +306,10 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
},
});
const audioTracks = mediaStream.getAudioTracks();
const videoTracks = mediaStream.getVideoTracks();
console.log('Windows capture started with loopback audio');
logToMain('info', 'Windows capture result:', {
hasVideo: videoTracks.length > 0,
hasAudio: audioTracks.length > 0,
audioTrackInfo: audioTracks.map(t => ({
label: t.label,
enabled: t.enabled,
muted: t.muted,
readyState: t.readyState,
settings: t.getSettings(),
})),
});
if (audioTracks.length === 0) {
logToMain('warn', 'WARNING: No audio tracks! User must check "Share audio" in screen picker dialog');
mastermind.setStatus('Warning: No audio - enable "Share audio" checkbox');
} else {
logToMain('info', 'Audio track acquired, setting up processing...');
// Setup audio processing for Windows loopback audio only
setupWindowsLoopbackProcessing();
}
// Setup audio processing for Windows loopback audio only
setupWindowsLoopbackProcessing();
if (audioMode === 'mic_only' || audioMode === 'both') {
let micStream = null;
@@ -400,21 +342,7 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
console.log('Manual mode enabled - screenshots will be captured on demand only');
} catch (err) {
console.error('Error starting capture:', err);
// Provide more helpful error messages based on error type
let errorMessage = err.message || 'Failed to start capture';
if (errorMessage.toLowerCase().includes('timeout')) {
errorMessage = 'Screen capture timed out. Please try again and select a screen quickly.';
} else if (errorMessage.toLowerCase().includes('permission') || errorMessage.toLowerCase().includes('denied')) {
errorMessage = 'Screen capture permission denied. Please grant screen recording permission in System Settings.';
} else if (errorMessage.toLowerCase().includes('not found') || errorMessage.toLowerCase().includes('no sources')) {
errorMessage = 'No screen sources found. Please ensure a display is connected.';
} else if (errorMessage.toLowerCase().includes('aborted') || errorMessage.toLowerCase().includes('cancel')) {
errorMessage = 'Screen selection was cancelled. Please try again.';
}
mastermind.setStatus('Error: ' + errorMessage);
cheatingDaddy.setStatus('error');
}
}
@@ -483,75 +411,32 @@ function setupLinuxSystemAudioProcessing() {
function setupWindowsLoopbackProcessing() {
// Setup audio processing for Windows loopback audio only
logToMain('info', 'Setting up Windows loopback audio processing...');
audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
const source = audioContext.createMediaStreamSource(mediaStream);
audioProcessor = audioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
try {
audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
let audioBuffer = [];
const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
logToMain('info', 'AudioContext created:', {
state: audioContext.state,
sampleRate: audioContext.sampleRate,
});
audioProcessor.onaudioprocess = async e => {
const inputData = e.inputBuffer.getChannelData(0);
audioBuffer.push(...inputData);
// Resume AudioContext if suspended (Chrome policy)
if (audioContext.state === 'suspended') {
logToMain('warn', 'AudioContext suspended, attempting resume...');
audioContext
.resume()
.then(() => {
logToMain('info', 'AudioContext resumed successfully');
})
.catch(err => {
logToMain('error', 'Failed to resume AudioContext:', err.message);
});
// 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',
});
}
};
const source = audioContext.createMediaStreamSource(mediaStream);
audioProcessor = audioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
let audioBuffer = [];
const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
let chunkCount = 0;
let totalSamples = 0;
audioProcessor.onaudioprocess = async e => {
const inputData = e.inputBuffer.getChannelData(0);
audioBuffer.push(...inputData);
totalSamples += inputData.length;
// 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',
});
chunkCount++;
// Log progress every 100 chunks (~10 seconds)
if (chunkCount === 1) {
logToMain('info', 'First audio chunk sent to AI');
mastermind.setStatus('Listening...');
} else if (chunkCount % 100 === 0) {
// Calculate max amplitude to check if we're getting real audio
const maxAmp = Math.max(...chunk.map(Math.abs));
logToMain('info', `Audio progress: ${chunkCount} chunks, maxAmplitude: ${maxAmp.toFixed(4)}`);
}
}
};
source.connect(audioProcessor);
audioProcessor.connect(audioContext.destination);
logToMain('info', 'Windows audio processing pipeline connected');
} catch (err) {
logToMain('error', 'Error setting up Windows audio:', err.message, err.stack);
mastermind.setStatus('Audio error: ' + err.message);
}
source.connect(audioProcessor);
audioProcessor.connect(audioContext.destination);
}
async function captureScreenshot(imageQuality = 'medium', isManual = false) {
@@ -646,183 +531,10 @@ async function captureScreenshot(imageQuality = 'medium', isManual = false) {
);
}
const MANUAL_SCREENSHOT_PROMPT = `You are an expert AI assistant analyzing a screenshot. Your task is to understand what the user needs help with and provide the most useful response.
**ANALYSIS APPROACH:**
1. First, identify what's shown on the screen (code editor, math problem, website, document, exam, etc.)
2. Determine what the user likely needs (explanation, solution, answer, debugging help, etc.)
3. Provide a direct, actionable response
**RESPONSE GUIDELINES BY CONTEXT:**
**If it's CODE (LeetCode, HackerRank, coding interview, IDE):**
- Identify the programming language and problem type
- Provide a brief explanation of the approach (2-3 bullet points max)
- Give the complete, working code solution
- Include time/space complexity if relevant
- If there's an error, explain the fix
**If it's MATH or SCIENCE:**
- Show step-by-step solution
- Use proper mathematical notation with LaTeX ($..$ for inline, $$...$$ for blocks)
- Provide the final answer clearly marked
- Include any relevant formulas used
**If it's MCQ/EXAM/QUIZ:**
- State the correct answer immediately and clearly (e.g., "**Answer: B**")
- Provide brief justification (1-2 sentences)
- If multiple questions visible, answer all of them
**If it's a DOCUMENT/ARTICLE/WEBSITE:**
- Summarize the key information
- Answer any specific questions if apparent
- Highlight important points
**If it's a FORM/APPLICATION:**
- Help fill in the required information
- Suggest appropriate responses
- Point out any issues or missing fields
**If it's an ERROR/DEBUG scenario:**
- Identify the error type and cause
- Provide the fix immediately
- Explain briefly why it occurred
**FORMAT REQUIREMENTS:**
- Use **markdown** for formatting
- Use **bold** for key answers and important points
- Use code blocks with language specification for code
- Be concise but complete - no unnecessary explanations
- No pleasantries or filler text - get straight to the answer
**CRITICAL:** Provide the complete answer. Don't ask for clarification - make reasonable assumptions and deliver value immediately.`;
// ============ REGION SELECTION ============
// Uses a separate fullscreen window to allow selection outside the app window
async function startRegionSelection() {
console.log('Starting region selection...');
if (!mediaStream) {
console.error('No media stream available. Please start capture first.');
mastermind?.addNewResponse('Please start screen capture first before selecting a region.');
return;
}
// Ensure video is ready
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();
});
// Initialize canvas
offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = hiddenVideo.videoWidth;
offscreenCanvas.height = hiddenVideo.videoHeight;
offscreenContext = offscreenCanvas.getContext('2d');
}
if (hiddenVideo.readyState < 2) {
console.warn('Video not ready yet');
return;
}
// Capture current screen to show in selection window
offscreenContext.drawImage(hiddenVideo, 0, 0, offscreenCanvas.width, offscreenCanvas.height);
const screenshotDataUrl = offscreenCanvas.toDataURL('image/jpeg', 0.9);
// Request main process to create selection window
const result = await ipcRenderer.invoke('start-region-selection', { screenshotDataUrl });
if (result.success && result.rect) {
// Capture the selected region from the screenshot
await captureRegionFromScreenshot(result.rect, screenshotDataUrl);
} else if (result.cancelled) {
console.log('Region selection cancelled');
} else if (result.error) {
console.error('Region selection error:', result.error);
}
}
async function captureRegionFromScreenshot(rect, screenshotDataUrl) {
console.log('Capturing region from screenshot:', rect);
// Load the screenshot into an image
const img = new Image();
img.src = screenshotDataUrl;
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
});
// Calculate scale factor (screenshot might have different resolution than display)
// The selection coordinates are in screen pixels, we need to map to image pixels
const scaleX = img.naturalWidth / window.screen.width;
const scaleY = img.naturalHeight / window.screen.height;
// Scale the selection rectangle
const scaledRect = {
left: Math.round(rect.left * scaleX),
top: Math.round(rect.top * scaleY),
width: Math.round(rect.width * scaleX),
height: Math.round(rect.height * scaleY),
};
// Create canvas for the cropped region
const cropCanvas = document.createElement('canvas');
cropCanvas.width = scaledRect.width;
cropCanvas.height = scaledRect.height;
const cropContext = cropCanvas.getContext('2d');
// Draw only the selected region
cropContext.drawImage(img, scaledRect.left, scaledRect.top, scaledRect.width, scaledRect.height, 0, 0, scaledRect.width, scaledRect.height);
// Convert to blob and send
cropCanvas.toBlob(
async blob => {
if (!blob) {
console.error('Failed to create blob from cropped region');
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;
}
const result = await ipcRenderer.invoke('send-image-content', {
data: base64data,
prompt: MANUAL_SCREENSHOT_PROMPT,
});
if (result.success) {
console.log(`Region capture response completed from ${result.model}`);
} else {
console.error('Failed to get region capture response:', result.error);
mastermind.addNewResponse(`Error: ${result.error}`);
}
};
reader.readAsDataURL(blob);
},
'image/jpeg',
0.9
);
}
// Expose to global scope
window.startRegionSelection = startRegionSelection;
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');
@@ -859,21 +571,33 @@ async function captureManualScreenshot(imageQuality = null) {
return;
}
offscreenContext.drawImage(hiddenVideo, 0, 0, offscreenCanvas.width, offscreenCanvas.height);
// 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.9;
qualityValue = 0.85;
break;
case 'medium':
qualityValue = 0.7;
qualityValue = 0.6;
break;
case 'low':
qualityValue = 0.5;
qualityValue = 0.4;
break;
default:
qualityValue = 0.7;
qualityValue = 0.6;
}
offscreenCanvas.toBlob(
@@ -892,6 +616,8 @@ async function captureManualScreenshot(imageQuality = null) {
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,
@@ -903,7 +629,7 @@ async function captureManualScreenshot(imageQuality = null) {
// Response already displayed via streaming events (new-response/update-response)
} else {
console.error('Failed to get image response:', result.error);
mastermind.addNewResponse(`Error: ${result.error}`);
cheatingDaddy.addNewResponse(`Error: ${result.error}`);
}
};
reader.readAsDataURL(blob);
@@ -996,7 +722,7 @@ ipcRenderer.on('save-session-context', async (event, data) => {
try {
await storage.saveSession(data.sessionId, {
profile: data.profile,
customPrompt: data.customPrompt,
customPrompt: data.customPrompt
});
console.log('Session context saved:', data.sessionId, 'profile:', data.profile);
} catch (error) {
@@ -1010,7 +736,7 @@ ipcRenderer.on('save-screen-analysis', async (event, data) => {
await storage.saveSession(data.sessionId, {
screenAnalysisHistory: data.fullHistory,
profile: data.profile,
customPrompt: data.customPrompt,
customPrompt: data.customPrompt
});
console.log('Screen analysis saved:', data.sessionId);
} catch (error) {
@@ -1026,11 +752,11 @@ ipcRenderer.on('clear-sensitive-data', async () => {
// Handle shortcuts based on current view
function handleShortcut(shortcutKey) {
const currentView = mastermind.getCurrentView();
const currentView = cheatingDaddy.getCurrentView();
if (shortcutKey === 'ctrl+enter' || shortcutKey === 'cmd+enter') {
if (currentView === 'main') {
mastermind.element().handleStart();
cheatingDaddy.element().handleStart();
} else {
captureManualScreenshot();
}
@@ -1038,108 +764,82 @@ function handleShortcut(shortcutKey) {
}
// Create reference to the main app element
const mastermindApp = document.querySelector('mastermind-app');
const cheatingDaddyApp = document.querySelector('cheating-daddy-app');
// ============ THEME SYSTEM ============
const theme = {
themes: {
dark: {
background: '#1e1e1e',
text: '#e0e0e0',
textSecondary: '#a0a0a0',
textMuted: '#6b6b6b',
border: '#333333',
accent: '#ffffff',
btnPrimaryBg: '#ffffff',
btnPrimaryText: '#000000',
btnPrimaryHover: '#e0e0e0',
tooltipBg: '#1a1a1a',
tooltipText: '#ffffff',
keyBg: 'rgba(255,255,255,0.1)',
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)',
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)',
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)',
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)'
},
nord: {
background: '#2e3440',
text: '#eceff4',
textSecondary: '#d8dee9',
textMuted: '#4c566a',
border: '#3b4252',
accent: '#88c0d0',
btnPrimaryBg: '#88c0d0',
btnPrimaryText: '#2e3440',
btnPrimaryHover: '#8fbcbb',
tooltipBg: '#3b4252',
tooltipText: '#eceff4',
keyBg: 'rgba(136,192,208,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)'
},
dracula: {
background: '#282a36',
text: '#f8f8f2',
textSecondary: '#bd93f9',
textMuted: '#6272a4',
border: '#44475a',
accent: '#ff79c6',
btnPrimaryBg: '#ff79c6',
btnPrimaryText: '#282a36',
btnPrimaryHover: '#ff92d0',
tooltipBg: '#44475a',
tooltipText: '#f8f8f2',
keyBg: 'rgba(255,121,198,0.15)',
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)'
},
abyss: {
background: '#0a0a0a',
text: '#d4d4d4',
textSecondary: '#808080',
textMuted: '#505050',
border: '#1a1a1a',
accent: '#ffffff',
btnPrimaryBg: '#ffffff',
btnPrimaryText: '#0a0a0a',
btnPrimaryHover: '#d4d4d4',
tooltipBg: '#141414',
tooltipText: '#d4d4d4',
keyBg: 'rgba(255,255,255,0.08)',
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)'
},
},
@@ -1155,33 +855,33 @@ const theme = {
light: 'Light',
midnight: 'Midnight Blue',
sepia: 'Sepia',
nord: 'Nord',
dracula: 'Dracula',
abyss: 'Abyss',
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],
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 };
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),
b: Math.min(255, rgb.b + amount)
};
},
@@ -1189,7 +889,7 @@ const theme = {
return {
r: Math.max(0, rgb.r - amount),
g: Math.max(0, rgb.g - amount),
b: Math.max(0, rgb.b - amount),
b: Math.max(0, rgb.b - amount)
};
},
@@ -1201,20 +901,31 @@ const theme = {
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, 7);
const tertiary = adjust(baseRgb, 15);
const hover = adjust(baseRgb, 20);
const secondary = adjust(baseRgb, 10);
const tertiary = adjust(baseRgb, 22);
const hover = adjust(baseRgb, 28);
root.style.setProperty('--header-background', `rgba(${baseRgb.r}, ${baseRgb.g}, ${baseRgb.b}, ${alpha})`);
root.style.setProperty('--main-content-background', `rgba(${baseRgb.r}, ${baseRgb.g}, ${baseRgb.b}, ${alpha})`);
root.style.setProperty('--bg-primary', `rgba(${baseRgb.r}, ${baseRgb.g}, ${baseRgb.b}, ${alpha})`);
root.style.setProperty('--bg-secondary', `rgba(${secondary.r}, ${secondary.g}, ${secondary.b}, ${alpha})`);
root.style.setProperty('--bg-tertiary', `rgba(${tertiary.r}, ${tertiary.g}, ${tertiary.b}, ${alpha})`);
root.style.setProperty('--bg-hover', `rgba(${hover.r}, ${hover.g}, ${hover.b}, ${alpha})`);
root.style.setProperty('--input-background', `rgba(${tertiary.r}, ${tertiary.g}, ${tertiary.b}, ${alpha})`);
root.style.setProperty('--input-focus-background', `rgba(${tertiary.r}, ${tertiary.g}, ${tertiary.b}, ${alpha})`);
root.style.setProperty('--hover-background', `rgba(${hover.r}, ${hover.g}, ${hover.b}, ${alpha})`);
root.style.setProperty('--scrollbar-background', `rgba(${baseRgb.r}, ${baseRgb.g}, ${baseRgb.b}, ${alpha})`);
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) {
@@ -1222,14 +933,19 @@ const theme = {
this.current = themeName;
const root = document.documentElement;
// Text colors
root.style.setProperty('--text-color', colors.text);
// 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);
// Border colors
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);
// Misc
root.style.setProperty('--placeholder-color', colors.textMuted);
root.style.setProperty('--scrollbar-thumb', colors.border);
root.style.setProperty('--scrollbar-thumb-hover', colors.textMuted);
@@ -1269,29 +985,30 @@ const theme = {
async save(themeName) {
await storage.updatePreference('theme', themeName);
this.apply(themeName);
},
}
};
// Consolidated mastermind object - all functions in one place
const mastermind = {
// Consolidated cheatingDaddy object - all functions in one place
const cheatingDaddy = {
// App version
getVersion: async () => ipcRenderer.invoke('get-app-version'),
// Element access
element: () => mastermindApp,
e: () => mastermindApp,
element: () => cheatingDaddyApp,
e: () => cheatingDaddyApp,
// App state functions - access properties directly from the app element
getCurrentView: () => mastermindApp.currentView,
getLayoutMode: () => mastermindApp.layoutMode,
getCurrentView: () => cheatingDaddyApp.currentView,
getLayoutMode: () => cheatingDaddyApp.layoutMode,
// Status and response functions
setStatus: text => mastermindApp.setStatus(text),
addNewResponse: response => mastermindApp.addNewResponse(response),
updateCurrentResponse: response => mastermindApp.updateCurrentResponse(response),
setStatus: text => cheatingDaddyApp.setStatus(text),
addNewResponse: response => cheatingDaddyApp.addNewResponse(response),
updateCurrentResponse: response => cheatingDaddyApp.updateCurrentResponse(response),
// Core functionality
initializeGemini,
initializeLocal,
startCapture,
stopCapture,
sendTextMessage,
@@ -1312,7 +1029,7 @@ const mastermind = {
};
// Make it globally available
window.mastermind = mastermind;
window.cheatingDaddy = cheatingDaddy;
// Load theme after DOM is ready
if (document.readyState === 'loading') {