cheat-exam/src/components/app/MastermindApp.js
Илья Глазунов 06e178762d
Some checks failed
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled
Rename project from "Cheating Daddy" to "Mastermind" across all files, update version to 0.6.0, and implement migration functionality for users with existing configurations. Enhance onboarding experience with migration options and update relevant documentation.
2026-01-16 01:16:43 +03:00

641 lines
23 KiB
JavaScript

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 MastermindApp 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([
mastermind.storage.getConfig(),
mastermind.storage.getPreferences(),
mastermind.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') {
mastermind.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 mastermind.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 mastermind.initializeGemini(this.selectedProfile, this.selectedLanguage);
// Pass the screenshot interval as string (including 'manual' option)
mastermind.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 mastermind.storage.updatePreference('selectedProfile', profile);
}
async handleLanguageChange(language) {
this.selectedLanguage = language;
await mastermind.storage.updatePreference('selectedLanguage', language);
}
async handleScreenshotIntervalChange(interval) {
this.selectedScreenshotInterval = interval;
await mastermind.storage.updatePreference('selectedScreenshotInterval', interval);
}
async handleImageQualityChange(quality) {
this.selectedImageQuality = quality;
await mastermind.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.mastermind.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`
<onboarding-view .onComplete=${() => this.handleOnboardingComplete()} .onClose=${() => this.handleClose()}></onboarding-view>
`;
case 'main':
return html`
<main-view
.onStart=${() => this.handleStart()}
.onAPIKeyHelp=${() => this.handleAPIKeyHelp()}
.onLayoutModeChange=${layoutMode => this.handleLayoutModeChange(layoutMode)}
></main-view>
`;
case 'customize':
return html`
<customize-view
.selectedProfile=${this.selectedProfile}
.selectedLanguage=${this.selectedLanguage}
.selectedScreenshotInterval=${this.selectedScreenshotInterval}
.selectedImageQuality=${this.selectedImageQuality}
.layoutMode=${this.layoutMode}
.onProfileChange=${profile => this.handleProfileChange(profile)}
.onLanguageChange=${language => this.handleLanguageChange(language)}
.onScreenshotIntervalChange=${interval => this.handleScreenshotIntervalChange(interval)}
.onImageQualityChange=${quality => this.handleImageQualityChange(quality)}
.onLayoutModeChange=${layoutMode => this.handleLayoutModeChange(layoutMode)}
></customize-view>
`;
case 'help':
return html` <help-view .onExternalLinkClick=${url => this.handleExternalLinkClick(url)}></help-view> `;
case 'history':
return html` <history-view></history-view> `;
case 'assistant':
return html`
<assistant-view
.responses=${this.responses}
.currentResponseIndex=${this.currentResponseIndex}
.selectedProfile=${this.selectedProfile}
.aiProvider=${this.aiProvider}
.onSendText=${message => 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();
}}
></assistant-view>
`;
default:
return html`<div>Unknown view: ${this.currentView}</div>`;
}
}
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`
<div class="window-container">
<div class="container">
<app-header
.currentView=${this.currentView}
.statusText=${this.statusText}
.startTime=${this.startTime}
.aiProvider=${this.aiProvider}
.modelInfo=${this.modelInfo}
.onCustomizeClick=${() => this.handleCustomizeClick()}
.onHelpClick=${() => this.handleHelpClick()}
.onHistoryClick=${() => this.handleHistoryClick()}
.onCloseClick=${() => this.handleClose()}
.onBackClick=${() => this.handleBackClick()}
.onHideToggleClick=${() => this.handleHideToggle()}
?isClickThrough=${this._isClickThrough}
></app-header>
<div class="${mainContentClass}">
<div class="view-container">${this.renderCurrentView()}</div>
</div>
</div>
${this.showScreenPicker
? html`
<screen-picker-dialog
?visible=${this.showScreenPicker}
.sources=${this.screenSources}
@source-selected=${this.handleSourceSelected}
@cancelled=${this.handlePickerCancelled}
></screen-picker-dialog>
`
: ''}
</div>
`;
}
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 mastermind.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('mastermind-app', MastermindApp);