import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js'; import { AppHeader } from './AppHeader.js'; import { MainView } from '../views/MainView.js'; import { CustomizeView } from '../views/CustomizeView.js'; import { HelpView } from '../views/HelpView.js'; import { HistoryView } from '../views/HistoryView.js'; import { AssistantView } from '../views/AssistantView.js'; import { OnboardingView } from '../views/OnboardingView.js'; import { ScreenPickerDialog } from '../views/ScreenPickerDialog.js'; export class CheatingDaddyApp extends LitElement { static styles = css` * { box-sizing: border-box; font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; margin: 0px; padding: 0px; cursor: default; user-select: none; } :host { display: block; width: 100%; height: 100vh; background-color: var(--background-transparent); color: var(--text-color); } .window-container { height: 100vh; overflow: hidden; background: var(--bg-primary); } .container { display: flex; flex-direction: column; height: 100%; } .main-content { flex: 1; padding: var(--main-content-padding); overflow-y: auto; background: var(--main-content-background); } .main-content.with-border { border-top: none; } .main-content.assistant-view { padding: 12px; } .main-content.onboarding-view { padding: 0; background: transparent; } .main-content.settings-view, .main-content.help-view, .main-content.history-view { padding: 0; } .view-container { opacity: 1; height: 100%; } .view-container.entering { opacity: 0; } ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); } `; static properties = { currentView: { type: String }, statusText: { type: String }, startTime: { type: Number }, isRecording: { type: Boolean }, sessionActive: { type: Boolean }, selectedProfile: { type: String }, selectedLanguage: { type: String }, responses: { type: Array }, currentResponseIndex: { type: Number }, selectedScreenshotInterval: { type: String }, selectedImageQuality: { type: String }, layoutMode: { type: String }, _viewInstances: { type: Object, state: true }, _isClickThrough: { state: true }, _awaitingNewResponse: { state: true }, shouldAnimateResponse: { type: Boolean }, _storageLoaded: { state: true }, aiProvider: { type: String }, modelInfo: { type: Object }, showScreenPicker: { type: Boolean }, screenSources: { type: Array }, }; constructor() { super(); // Set defaults - will be overwritten by storage this.currentView = 'main'; // Will check onboarding after storage loads this.statusText = ''; this.startTime = null; this.isRecording = false; this.sessionActive = false; this.selectedProfile = 'interview'; this.selectedLanguage = 'en-US'; this.selectedScreenshotInterval = '5'; this.selectedImageQuality = 'medium'; this.layoutMode = 'normal'; this.responses = []; this.currentResponseIndex = -1; this._viewInstances = new Map(); this._isClickThrough = false; this._awaitingNewResponse = false; this._currentResponseIsComplete = true; this.shouldAnimateResponse = false; this._storageLoaded = false; this.aiProvider = 'gemini'; this.modelInfo = { model: '', visionModel: '', whisperModel: '' }; this.showScreenPicker = false; this.screenSources = []; // Load from storage this._loadFromStorage(); } async _loadFromStorage() { try { const [config, prefs, openaiSdkCreds] = await Promise.all([ cheatingDaddy.storage.getConfig(), cheatingDaddy.storage.getPreferences(), cheatingDaddy.storage.getOpenAISDKCredentials(), ]); // Check onboarding status this.currentView = config.onboarded ? 'main' : 'onboarding'; // Apply background appearance (color + transparency) this.applyBackgroundAppearance(prefs.backgroundColor ?? '#1e1e1e', prefs.backgroundTransparency ?? 0.8); // Load preferences this.selectedProfile = prefs.selectedProfile || 'interview'; this.selectedLanguage = prefs.selectedLanguage || 'en-US'; this.selectedScreenshotInterval = prefs.selectedScreenshotInterval || '5'; this.selectedImageQuality = prefs.selectedImageQuality || 'medium'; this.layoutMode = config.layout || 'normal'; // Load AI provider and model info this.aiProvider = prefs.aiProvider || 'gemini'; this.modelInfo = { model: openaiSdkCreds.model || 'gpt-4o', visionModel: openaiSdkCreds.visionModel || 'gpt-4o', whisperModel: openaiSdkCreds.whisperModel || 'whisper-1', }; this._storageLoaded = true; this.updateLayoutMode(); this.requestUpdate(); } catch (error) { console.error('Error loading from storage:', error); this._storageLoaded = true; this.requestUpdate(); } } 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), }; } applyBackgroundAppearance(backgroundColor, alpha) { const root = document.documentElement; const baseRgb = this.hexToRgb(backgroundColor); // Generate color variants based on the base color const secondary = this.lightenColor(baseRgb, 7); const tertiary = this.lightenColor(baseRgb, 15); const hover = this.lightenColor(baseRgb, 20); 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})`); } // Keep old function name for backwards compatibility applyBackgroundTransparency(alpha) { this.applyBackgroundAppearance('#1e1e1e', alpha); } connectedCallback() { super.connectedCallback(); // Apply layout mode to document root this.updateLayoutMode(); // Set up IPC listeners if needed if (window.require) { const { ipcRenderer } = window.require('electron'); ipcRenderer.on('new-response', (_, response) => { this.addNewResponse(response); }); ipcRenderer.on('update-response', (_, response) => { this.updateCurrentResponse(response); }); ipcRenderer.on('update-status', (_, status) => { this.setStatus(status); }); ipcRenderer.on('click-through-toggled', (_, isEnabled) => { this._isClickThrough = isEnabled; }); ipcRenderer.on('reconnect-failed', (_, data) => { this.addNewResponse(data.message); }); } } disconnectedCallback() { super.disconnectedCallback(); if (window.require) { const { ipcRenderer } = window.require('electron'); ipcRenderer.removeAllListeners('new-response'); ipcRenderer.removeAllListeners('update-response'); ipcRenderer.removeAllListeners('update-status'); ipcRenderer.removeAllListeners('click-through-toggled'); ipcRenderer.removeAllListeners('reconnect-failed'); } } setStatus(text) { this.statusText = text; // Mark response as complete when we get certain status messages if (text.includes('Ready') || text.includes('Listening') || text.includes('Error')) { this._currentResponseIsComplete = true; console.log('[setStatus] Marked current response as complete'); } } addNewResponse(response) { // Add a new response entry (first word of a new AI response) this.responses = [...this.responses, response]; this.currentResponseIndex = this.responses.length - 1; this._awaitingNewResponse = false; console.log('[addNewResponse] Added:', response); this.requestUpdate(); } updateCurrentResponse(response) { // Update the current response in place (streaming subsequent words) if (this.responses.length > 0) { this.responses = [...this.responses.slice(0, -1), response]; console.log('[updateCurrentResponse] Updated to:', response); } else { // Fallback: if no responses exist, add as new this.addNewResponse(response); } this.requestUpdate(); } // Header event handlers handleCustomizeClick() { this.currentView = 'customize'; this.requestUpdate(); } handleHelpClick() { this.currentView = 'help'; this.requestUpdate(); } handleHistoryClick() { this.currentView = 'history'; this.requestUpdate(); } async handleClose() { if (this.currentView === 'customize' || this.currentView === 'help' || this.currentView === 'history') { this.currentView = 'main'; } else if (this.currentView === 'assistant') { cheatingDaddy.stopCapture(); // Close the session if (window.require) { const { ipcRenderer } = window.require('electron'); await ipcRenderer.invoke('close-session'); } this.sessionActive = false; this.currentView = 'main'; console.log('Session closed'); } else { // Quit the entire application if (window.require) { const { ipcRenderer } = window.require('electron'); await ipcRenderer.invoke('quit-application'); } } } async handleHideToggle() { if (window.require) { const { ipcRenderer } = window.require('electron'); await ipcRenderer.invoke('toggle-window-visibility'); } } // Main view event handlers async handleStart() { // check if api key is empty do nothing const apiKey = await cheatingDaddy.storage.getApiKey(); if (!apiKey || apiKey === '') { // Trigger the red blink animation on the API key input const mainView = this.shadowRoot.querySelector('main-view'); if (mainView && mainView.triggerApiKeyError) { mainView.triggerApiKeyError(); } return; } await cheatingDaddy.initializeGemini(this.selectedProfile, this.selectedLanguage); // Pass the screenshot interval as string (including 'manual' option) cheatingDaddy.startCapture(this.selectedScreenshotInterval, this.selectedImageQuality); this.responses = []; this.currentResponseIndex = -1; this.startTime = Date.now(); this.currentView = 'assistant'; } async handleAPIKeyHelp() { if (window.require) { const { ipcRenderer } = window.require('electron'); await ipcRenderer.invoke('open-external', 'https://cheatingdaddy.com/help/api-key'); } } // Customize view event handlers async handleProfileChange(profile) { this.selectedProfile = profile; await cheatingDaddy.storage.updatePreference('selectedProfile', profile); } async handleLanguageChange(language) { this.selectedLanguage = language; await cheatingDaddy.storage.updatePreference('selectedLanguage', language); } async handleScreenshotIntervalChange(interval) { this.selectedScreenshotInterval = interval; await cheatingDaddy.storage.updatePreference('selectedScreenshotInterval', interval); } async handleImageQualityChange(quality) { this.selectedImageQuality = quality; await cheatingDaddy.storage.updatePreference('selectedImageQuality', quality); } handleBackClick() { this.currentView = 'main'; this.requestUpdate(); } // Help view event handlers async handleExternalLinkClick(url) { if (window.require) { const { ipcRenderer } = window.require('electron'); await ipcRenderer.invoke('open-external', url); } } // Assistant view event handlers async handleSendText(message) { const result = await window.cheatingDaddy.sendTextMessage(message); if (!result.success) { console.error('Failed to send message:', result.error); this.setStatus('Error sending message: ' + result.error); } else { this.setStatus('Message sent...'); this._awaitingNewResponse = true; } } handleResponseIndexChanged(e) { this.currentResponseIndex = e.detail.index; this.shouldAnimateResponse = false; this.requestUpdate(); } // Onboarding event handlers handleOnboardingComplete() { this.currentView = 'main'; } updated(changedProperties) { super.updated(changedProperties); // Only notify main process of view change if the view actually changed if (changedProperties.has('currentView') && window.require) { const { ipcRenderer } = window.require('electron'); ipcRenderer.send('view-changed', this.currentView); // Add a small delay to smooth out the transition const viewContainer = this.shadowRoot?.querySelector('.view-container'); if (viewContainer) { viewContainer.classList.add('entering'); requestAnimationFrame(() => { viewContainer.classList.remove('entering'); }); } } if (changedProperties.has('layoutMode')) { this.updateLayoutMode(); } } renderCurrentView() { // Only re-render the view if it hasn't been cached or if critical properties changed const viewKey = `${this.currentView}-${this.selectedProfile}-${this.selectedLanguage}`; switch (this.currentView) { case 'onboarding': return html` this.handleOnboardingComplete()} .onClose=${() => this.handleClose()}> `; case 'main': return html` this.handleStart()} .onAPIKeyHelp=${() => this.handleAPIKeyHelp()} .onLayoutModeChange=${layoutMode => this.handleLayoutModeChange(layoutMode)} > `; case 'customize': return html` this.handleProfileChange(profile)} .onLanguageChange=${language => this.handleLanguageChange(language)} .onScreenshotIntervalChange=${interval => this.handleScreenshotIntervalChange(interval)} .onImageQualityChange=${quality => this.handleImageQualityChange(quality)} .onLayoutModeChange=${layoutMode => this.handleLayoutModeChange(layoutMode)} > `; case 'help': return html` this.handleExternalLinkClick(url)}> `; case 'history': return html` `; case 'assistant': return html` this.handleSendText(message)} .shouldAnimateResponse=${this.shouldAnimateResponse} @response-index-changed=${this.handleResponseIndexChanged} @response-animation-complete=${() => { this.shouldAnimateResponse = false; this._currentResponseIsComplete = true; console.log('[response-animation-complete] Marked current response as complete'); this.requestUpdate(); }} > `; default: return html`
Unknown view: ${this.currentView}
`; } } render() { const viewClassMap = { assistant: 'assistant-view', onboarding: 'onboarding-view', customize: 'settings-view', help: 'help-view', history: 'history-view', }; const mainContentClass = `main-content ${viewClassMap[this.currentView] || 'with-border'}`; return html`
this.handleCustomizeClick()} .onHelpClick=${() => this.handleHelpClick()} .onHistoryClick=${() => this.handleHistoryClick()} .onCloseClick=${() => this.handleClose()} .onBackClick=${() => this.handleBackClick()} .onHideToggleClick=${() => this.handleHideToggle()} ?isClickThrough=${this._isClickThrough} >
${this.renderCurrentView()}
${this.showScreenPicker ? html` ` : ''}
`; } updateLayoutMode() { // Apply or remove compact layout class to document root if (this.layoutMode === 'compact') { document.documentElement.classList.add('compact-layout'); } else { document.documentElement.classList.remove('compact-layout'); } } async handleLayoutModeChange(layoutMode) { this.layoutMode = layoutMode; await cheatingDaddy.storage.updateConfig('layout', layoutMode); this.updateLayoutMode(); // Notify main process about layout change for window resizing if (window.require) { try { const { ipcRenderer } = window.require('electron'); await ipcRenderer.invoke('update-sizes'); } catch (error) { console.error('Failed to update sizes in main process:', error); } } this.requestUpdate(); } async showScreenPickerDialog() { const { ipcRenderer } = window.require('electron'); const result = await ipcRenderer.invoke('get-screen-sources'); if (result.success) { this.screenSources = result.sources; this.showScreenPicker = true; return new Promise(resolve => { this._screenPickerResolve = resolve; }); } else { console.error('Failed to get screen sources:', result.error); return { cancelled: true }; } } async handleSourceSelected(event) { const { source } = event.detail; const { ipcRenderer } = window.require('electron'); // Tell main process which source was selected await ipcRenderer.invoke('set-selected-source', source.id); this.showScreenPicker = false; if (this._screenPickerResolve) { this._screenPickerResolve({ source }); this._screenPickerResolve = null; } } handlePickerCancelled() { this.showScreenPicker = false; if (this._screenPickerResolve) { this._screenPickerResolve({ cancelled: true }); this._screenPickerResolve = null; } } } customElements.define('cheating-daddy-app', CheatingDaddyApp);