initial commit

This commit is contained in:
Илья Глазунов
2026-01-14 22:57:19 +03:00
commit beb9034cd4
50 changed files with 17257 additions and 0 deletions
+636
View File
@@ -0,0 +1,636 @@
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
export class AssistantView extends LitElement {
static styles = css`
:host {
height: 100%;
display: flex;
flex-direction: column;
}
* {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
cursor: default;
}
.response-container {
height: calc(100% - 50px);
overflow-y: auto;
font-size: var(--response-font-size, 16px);
line-height: 1.6;
background: var(--bg-primary);
padding: 12px;
scroll-behavior: smooth;
user-select: text;
cursor: text;
}
.response-container * {
user-select: text;
cursor: text;
}
.response-container a {
cursor: pointer;
}
/* Word display (no animation) */
.response-container [data-word] {
display: inline-block;
}
/* Markdown styling */
.response-container h1,
.response-container h2,
.response-container h3,
.response-container h4,
.response-container h5,
.response-container h6 {
margin: 1em 0 0.5em 0;
color: var(--text-color);
font-weight: 600;
}
.response-container h1 { font-size: 1.6em; }
.response-container h2 { font-size: 1.4em; }
.response-container h3 { font-size: 1.2em; }
.response-container h4 { font-size: 1.1em; }
.response-container h5 { font-size: 1em; }
.response-container h6 { font-size: 0.9em; }
.response-container p {
margin: 0.6em 0;
color: var(--text-color);
}
.response-container ul,
.response-container ol {
margin: 0.6em 0;
padding-left: 1.5em;
color: var(--text-color);
}
.response-container li {
margin: 0.3em 0;
}
.response-container blockquote {
margin: 0.8em 0;
padding: 0.5em 1em;
border-left: 2px solid var(--border-default);
background: var(--bg-secondary);
}
.response-container code {
background: var(--bg-tertiary);
padding: 0.15em 0.4em;
border-radius: 3px;
font-family: 'SF Mono', Monaco, monospace;
font-size: 0.85em;
}
.response-container pre {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 12px;
overflow-x: auto;
margin: 0.8em 0;
}
.response-container pre code {
background: none;
padding: 0;
}
.response-container a {
color: var(--text-color);
text-decoration: underline;
text-underline-offset: 2px;
}
.response-container strong,
.response-container b {
font-weight: 600;
}
.response-container hr {
border: none;
border-top: 1px solid var(--border-color);
margin: 1.5em 0;
}
.response-container table {
border-collapse: collapse;
width: 100%;
margin: 0.8em 0;
}
.response-container th,
.response-container td {
border: 1px solid var(--border-color);
padding: 8px;
text-align: left;
}
.response-container th {
background: var(--bg-secondary);
font-weight: 600;
}
.response-container::-webkit-scrollbar {
width: 8px;
}
.response-container::-webkit-scrollbar-track {
background: transparent;
}
.response-container::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 4px;
}
.response-container::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-hover);
}
.text-input-container {
display: flex;
gap: 8px;
margin-top: 8px;
align-items: center;
}
.text-input-container input {
flex: 1;
background: transparent;
color: var(--text-color);
border: none;
border-bottom: 1px solid var(--border-color);
padding: 8px 4px;
border-radius: 0;
font-size: 13px;
}
.text-input-container input:focus {
outline: none;
border-bottom-color: var(--text-color);
}
.text-input-container input::placeholder {
color: var(--placeholder-color);
}
.nav-button {
background: transparent;
color: var(--text-secondary);
border: none;
padding: 6px;
border-radius: 3px;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.1s ease;
}
.nav-button:hover {
background: var(--hover-background);
color: var(--text-color);
}
.nav-button:disabled {
opacity: 0.3;
}
.nav-button svg {
width: 18px;
height: 18px;
stroke: currentColor;
}
.response-counter {
font-size: 11px;
color: var(--text-muted);
white-space: nowrap;
min-width: 50px;
text-align: center;
font-family: 'SF Mono', Monaco, monospace;
}
.screen-answer-btn {
display: flex;
align-items: center;
gap: 6px;
background: var(--btn-primary-bg, #ffffff);
color: var(--btn-primary-text, #000000);
border: none;
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
}
.screen-answer-btn:hover {
background: var(--btn-primary-hover, #f0f0f0);
}
.screen-answer-btn svg {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.screen-answer-btn .usage-count {
font-size: 11px;
opacity: 0.7;
font-family: 'SF Mono', Monaco, monospace;
}
.screen-answer-btn-wrapper {
position: relative;
}
.screen-answer-btn-wrapper .tooltip {
position: absolute;
bottom: 100%;
right: 0;
margin-bottom: 8px;
background: var(--tooltip-bg, #1a1a1a);
color: var(--tooltip-text, #ffffff);
padding: 8px 12px;
border-radius: 6px;
font-size: 11px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.15s ease, visibility 0.15s ease;
pointer-events: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 100;
}
.screen-answer-btn-wrapper .tooltip::after {
content: '';
position: absolute;
top: 100%;
right: 16px;
border: 6px solid transparent;
border-top-color: var(--tooltip-bg, #1a1a1a);
}
.screen-answer-btn-wrapper:hover .tooltip {
opacity: 1;
visibility: visible;
}
.tooltip-row {
display: flex;
justify-content: space-between;
gap: 16px;
margin-bottom: 4px;
}
.tooltip-row:last-child {
margin-bottom: 0;
}
.tooltip-label {
opacity: 0.7;
}
.tooltip-value {
font-family: 'SF Mono', Monaco, monospace;
}
.tooltip-note {
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid rgba(255,255,255,0.1);
opacity: 0.5;
font-size: 10px;
}
`;
static properties = {
responses: { type: Array },
currentResponseIndex: { type: Number },
selectedProfile: { type: String },
onSendText: { type: Function },
shouldAnimateResponse: { type: Boolean },
flashCount: { type: Number },
flashLiteCount: { type: Number },
};
constructor() {
super();
this.responses = [];
this.currentResponseIndex = -1;
this.selectedProfile = 'interview';
this.onSendText = () => {};
this.flashCount = 0;
this.flashLiteCount = 0;
}
getProfileNames() {
return {
interview: 'Job Interview',
sales: 'Sales Call',
meeting: 'Business Meeting',
presentation: 'Presentation',
negotiation: 'Negotiation',
exam: 'Exam Assistant',
};
}
getCurrentResponse() {
const profileNames = this.getProfileNames();
return this.responses.length > 0 && this.currentResponseIndex >= 0
? this.responses[this.currentResponseIndex]
: `Hey, Im listening to your ${profileNames[this.selectedProfile] || 'session'}?`;
}
renderMarkdown(content) {
// Check if marked is available
if (typeof window !== 'undefined' && window.marked) {
try {
// Configure marked for better security and formatting
window.marked.setOptions({
breaks: true,
gfm: true,
sanitize: false, // We trust the AI responses
});
let rendered = window.marked.parse(content);
rendered = this.wrapWordsInSpans(rendered);
return rendered;
} catch (error) {
console.warn('Error parsing markdown:', error);
return content; // Fallback to plain text
}
}
console.log('Marked not available, using plain text');
return content; // Fallback if marked is not available
}
wrapWordsInSpans(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const tagsToSkip = ['PRE'];
function wrap(node) {
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() && !tagsToSkip.includes(node.parentNode.tagName)) {
const words = node.textContent.split(/(\s+)/);
const frag = document.createDocumentFragment();
words.forEach(word => {
if (word.trim()) {
const span = document.createElement('span');
span.setAttribute('data-word', '');
span.textContent = word;
frag.appendChild(span);
} else {
frag.appendChild(document.createTextNode(word));
}
});
node.parentNode.replaceChild(frag, node);
} else if (node.nodeType === Node.ELEMENT_NODE && !tagsToSkip.includes(node.tagName)) {
Array.from(node.childNodes).forEach(wrap);
}
}
Array.from(doc.body.childNodes).forEach(wrap);
return doc.body.innerHTML;
}
getResponseCounter() {
return this.responses.length > 0 ? `${this.currentResponseIndex + 1}/${this.responses.length}` : '';
}
navigateToPreviousResponse() {
if (this.currentResponseIndex > 0) {
this.currentResponseIndex--;
this.dispatchEvent(
new CustomEvent('response-index-changed', {
detail: { index: this.currentResponseIndex },
})
);
this.requestUpdate();
}
}
navigateToNextResponse() {
if (this.currentResponseIndex < this.responses.length - 1) {
this.currentResponseIndex++;
this.dispatchEvent(
new CustomEvent('response-index-changed', {
detail: { index: this.currentResponseIndex },
})
);
this.requestUpdate();
}
}
scrollResponseUp() {
const container = this.shadowRoot.querySelector('.response-container');
if (container) {
const scrollAmount = container.clientHeight * 0.3; // Scroll 30% of container height
container.scrollTop = Math.max(0, container.scrollTop - scrollAmount);
}
}
scrollResponseDown() {
const container = this.shadowRoot.querySelector('.response-container');
if (container) {
const scrollAmount = container.clientHeight * 0.3; // Scroll 30% of container height
container.scrollTop = Math.min(container.scrollHeight - container.clientHeight, container.scrollTop + scrollAmount);
}
}
connectedCallback() {
super.connectedCallback();
// Load limits on mount
this.loadLimits();
// Set up IPC listeners for keyboard shortcuts
if (window.require) {
const { ipcRenderer } = window.require('electron');
this.handlePreviousResponse = () => {
console.log('Received navigate-previous-response message');
this.navigateToPreviousResponse();
};
this.handleNextResponse = () => {
console.log('Received navigate-next-response message');
this.navigateToNextResponse();
};
this.handleScrollUp = () => {
console.log('Received scroll-response-up message');
this.scrollResponseUp();
};
this.handleScrollDown = () => {
console.log('Received scroll-response-down message');
this.scrollResponseDown();
};
ipcRenderer.on('navigate-previous-response', this.handlePreviousResponse);
ipcRenderer.on('navigate-next-response', this.handleNextResponse);
ipcRenderer.on('scroll-response-up', this.handleScrollUp);
ipcRenderer.on('scroll-response-down', this.handleScrollDown);
}
}
disconnectedCallback() {
super.disconnectedCallback();
// Clean up IPC listeners
if (window.require) {
const { ipcRenderer } = window.require('electron');
if (this.handlePreviousResponse) {
ipcRenderer.removeListener('navigate-previous-response', this.handlePreviousResponse);
}
if (this.handleNextResponse) {
ipcRenderer.removeListener('navigate-next-response', this.handleNextResponse);
}
if (this.handleScrollUp) {
ipcRenderer.removeListener('scroll-response-up', this.handleScrollUp);
}
if (this.handleScrollDown) {
ipcRenderer.removeListener('scroll-response-down', this.handleScrollDown);
}
}
}
async handleSendText() {
const textInput = this.shadowRoot.querySelector('#textInput');
if (textInput && textInput.value.trim()) {
const message = textInput.value.trim();
textInput.value = ''; // Clear input
await this.onSendText(message);
}
}
handleTextKeydown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.handleSendText();
}
}
async loadLimits() {
if (window.cheatingDaddy?.storage?.getTodayLimits) {
const limits = await window.cheatingDaddy.storage.getTodayLimits();
this.flashCount = limits.flash?.count || 0;
this.flashLiteCount = limits.flashLite?.count || 0;
}
}
getTotalUsed() {
return this.flashCount + this.flashLiteCount;
}
getTotalAvailable() {
return 40; // 20 flash + 20 flash-lite
}
async handleScreenAnswer() {
if (window.captureManualScreenshot) {
window.captureManualScreenshot();
// Reload limits after a short delay to catch the update
setTimeout(() => this.loadLimits(), 1000);
}
}
scrollToBottom() {
setTimeout(() => {
const container = this.shadowRoot.querySelector('.response-container');
if (container) {
container.scrollTop = container.scrollHeight;
}
}, 0);
}
firstUpdated() {
super.firstUpdated();
this.updateResponseContent();
}
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('responses') || changedProperties.has('currentResponseIndex')) {
this.updateResponseContent();
}
}
updateResponseContent() {
console.log('updateResponseContent called');
const container = this.shadowRoot.querySelector('#responseContainer');
if (container) {
const currentResponse = this.getCurrentResponse();
console.log('Current response:', currentResponse);
const renderedResponse = this.renderMarkdown(currentResponse);
console.log('Rendered response:', renderedResponse);
container.innerHTML = renderedResponse;
// Show all words immediately (no animation)
if (this.shouldAnimateResponse) {
this.dispatchEvent(new CustomEvent('response-animation-complete', { bubbles: true, composed: true }));
}
} else {
console.log('Response container not found');
}
}
render() {
const responseCounter = this.getResponseCounter();
return html`
<div class="response-container" id="responseContainer"></div>
<div class="text-input-container">
<button class="nav-button" @click=${this.navigateToPreviousResponse} ?disabled=${this.currentResponseIndex <= 0}>
<svg width="24px" height="24px" stroke-width="1.7" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 6L9 12L15 18" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</button>
${this.responses.length > 0 ? html`<span class="response-counter">${responseCounter}</span>` : ''}
<button class="nav-button" @click=${this.navigateToNextResponse} ?disabled=${this.currentResponseIndex >= this.responses.length - 1}>
<svg width="24px" height="24px" stroke-width="1.7" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 6L15 12L9 18" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</button>
<input type="text" id="textInput" placeholder="Type a message to the AI..." @keydown=${this.handleTextKeydown} />
<div class="screen-answer-btn-wrapper">
<div class="tooltip">
<div class="tooltip-row">
<span class="tooltip-label">Flash</span>
<span class="tooltip-value">${this.flashCount}/20</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Flash Lite</span>
<span class="tooltip-value">${this.flashLiteCount}/20</span>
</div>
<div class="tooltip-note">Resets every 24 hours</div>
</div>
<button class="screen-answer-btn" @click=${this.handleScreenAnswer}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path d="M15.98 1.804a1 1 0 0 0-1.96 0l-.24 1.192a1 1 0 0 1-.784.785l-1.192.238a1 1 0 0 0 0 1.962l1.192.238a1 1 0 0 1 .785.785l.238 1.192a1 1 0 0 0 1.962 0l.238-1.192a1 1 0 0 1 .785-.785l1.192-.238a1 1 0 0 0 0-1.962l-1.192-.238a1 1 0 0 1-.785-.785l-.238-1.192ZM6.949 5.684a1 1 0 0 0-1.898 0l-.683 2.051a1 1 0 0 1-.633.633l-2.051.683a1 1 0 0 0 0 1.898l2.051.684a1 1 0 0 1 .633.632l.683 2.051a1 1 0 0 0 1.898 0l.683-2.051a1 1 0 0 1 .633-.633l2.051-.683a1 1 0 0 0 0-1.898l-2.051-.683a1 1 0 0 1-.633-.633L6.95 5.684ZM13.949 13.684a1 1 0 0 0-1.898 0l-.184.551a1 1 0 0 1-.632.633l-.551.183a1 1 0 0 0 0 1.898l.551.183a1 1 0 0 1 .633.633l.183.551a1 1 0 0 0 1.898 0l.184-.551a1 1 0 0 1 .632-.633l.551-.183a1 1 0 0 0 0-1.898l-.551-.184a1 1 0 0 1-.633-.632l-.183-.551Z" />
</svg>
<span>Analyze screen</span>
<span class="usage-count">(${this.getTotalUsed()}/${this.getTotalAvailable()})</span>
</button>
</div>
</div>
`;
}
}
customElements.define('assistant-view', AssistantView);
File diff suppressed because it is too large Load Diff
+450
View File
@@ -0,0 +1,450 @@
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
import { resizeLayout } from '../../utils/windowResize.js';
export class HelpView extends LitElement {
static styles = css`
* {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
cursor: default;
user-select: none;
}
:host {
display: block;
padding: 0;
}
.help-container {
display: flex;
flex-direction: column;
}
.option-group {
padding: 16px 12px;
border-bottom: 1px solid var(--border-color);
}
.option-group:last-child {
border-bottom: none;
}
.option-label {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
.description {
color: var(--text-secondary);
font-size: 12px;
line-height: 1.4;
user-select: text;
cursor: text;
}
.description strong {
color: var(--text-color);
font-weight: 500;
}
.link {
color: var(--text-color);
text-decoration: underline;
text-underline-offset: 2px;
cursor: pointer;
}
.key {
background: var(--bg-tertiary);
color: var(--text-color);
border: 1px solid var(--border-color);
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-family: 'SF Mono', Monaco, monospace;
font-weight: 500;
margin: 0 1px;
white-space: nowrap;
}
.keyboard-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
margin-top: 8px;
}
.keyboard-group {
padding: 10px 0;
border-bottom: 1px solid var(--border-color);
}
.keyboard-group:last-child {
border-bottom: none;
}
.keyboard-group-title {
font-weight: 600;
font-size: 12px;
color: var(--text-color);
margin-bottom: 8px;
}
.shortcut-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
font-size: 11px;
}
.shortcut-description {
color: var(--text-secondary);
}
.shortcut-keys {
display: flex;
gap: 2px;
}
.profiles-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 8px;
margin-top: 8px;
}
.profile-item {
padding: 8px 0;
border-bottom: 1px solid var(--border-color);
}
.profile-item:last-child {
border-bottom: none;
}
.profile-name {
font-weight: 500;
font-size: 12px;
color: var(--text-color);
margin-bottom: 2px;
}
.profile-description {
font-size: 11px;
color: var(--text-muted);
line-height: 1.3;
}
.community-links {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.community-link {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: transparent;
border: 1px solid var(--border-color);
border-radius: 3px;
color: var(--text-color);
font-size: 11px;
font-weight: 500;
transition: background 0.1s ease;
cursor: pointer;
}
.community-link:hover {
background: var(--hover-background);
}
.community-link svg {
width: 14px;
height: 14px;
flex-shrink: 0;
}
.usage-steps {
counter-reset: step-counter;
}
.usage-step {
counter-increment: step-counter;
position: relative;
padding-left: 24px;
margin-bottom: 8px;
font-size: 11px;
line-height: 1.4;
color: var(--text-secondary);
}
.usage-step::before {
content: counter(step-counter);
position: absolute;
left: 0;
top: 0;
width: 16px;
height: 16px;
background: var(--bg-tertiary);
color: var(--text-color);
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 600;
}
.usage-step strong {
color: var(--text-color);
}
`;
static properties = {
onExternalLinkClick: { type: Function },
keybinds: { type: Object },
};
constructor() {
super();
this.onExternalLinkClick = () => {};
this.keybinds = this.getDefaultKeybinds();
this._loadKeybinds();
}
async _loadKeybinds() {
try {
const keybinds = await cheatingDaddy.storage.getKeybinds();
if (keybinds) {
this.keybinds = { ...this.getDefaultKeybinds(), ...keybinds };
this.requestUpdate();
}
} catch (error) {
console.error('Error loading keybinds:', error);
}
}
connectedCallback() {
super.connectedCallback();
// Resize window for this view
resizeLayout();
}
getDefaultKeybinds() {
const isMac = cheatingDaddy.isMacOS || navigator.platform.includes('Mac');
return {
moveUp: isMac ? 'Alt+Up' : 'Ctrl+Up',
moveDown: isMac ? 'Alt+Down' : 'Ctrl+Down',
moveLeft: isMac ? 'Alt+Left' : 'Ctrl+Left',
moveRight: isMac ? 'Alt+Right' : 'Ctrl+Right',
toggleVisibility: isMac ? 'Cmd+\\' : 'Ctrl+\\',
toggleClickThrough: isMac ? 'Cmd+M' : 'Ctrl+M',
nextStep: isMac ? 'Cmd+Enter' : 'Ctrl+Enter',
previousResponse: isMac ? 'Cmd+[' : 'Ctrl+[',
nextResponse: isMac ? 'Cmd+]' : 'Ctrl+]',
scrollUp: isMac ? 'Cmd+Shift+Up' : 'Ctrl+Shift+Up',
scrollDown: isMac ? 'Cmd+Shift+Down' : 'Ctrl+Shift+Down',
};
}
formatKeybind(keybind) {
return keybind.split('+').map(key => html`<span class="key">${key}</span>`);
}
handleExternalLinkClick(url) {
this.onExternalLinkClick(url);
}
render() {
const isMacOS = cheatingDaddy.isMacOS || false;
const isLinux = cheatingDaddy.isLinux || false;
return html`
<div class="help-container">
<div class="option-group">
<div class="option-label">
<span>Community & Support</span>
</div>
<div class="community-links">
<div class="community-link" @click=${() => this.handleExternalLinkClick('https://cheatingdaddy.com')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 11.9976C14 9.5059 11.683 7 8.85714 7C8.52241 7 7.41904 7.00001 7.14286 7.00001C4.30254 7.00001 2 9.23752 2 11.9976C2 14.376 3.70973 16.3664 6 16.8714C6.36756 16.9525 6.75006 16.9952 7.14286 16.9952"></path>
<path d="M10 11.9976C10 14.4893 12.317 16.9952 15.1429 16.9952C15.4776 16.9952 16.581 16.9952 16.8571 16.9952C19.6975 16.9952 22 14.7577 22 11.9976C22 9.6192 20.2903 7.62884 18 7.12383C17.6324 7.04278 17.2499 6.99999 16.8571 6.99999"></path>
</svg>
Website
</div>
<div class="community-link" @click=${() => this.handleExternalLinkClick('https://github.com/sohzm/cheating-daddy')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 22.0268V19.1568C16.0375 18.68 15.9731 18.2006 15.811 17.7506C15.6489 17.3006 15.3929 16.8902 15.06 16.5468C18.2 16.1968 21.5 15.0068 21.5 9.54679C21.4997 8.15062 20.9627 6.80799 20 5.79679C20.4558 4.5753 20.4236 3.22514 19.91 2.02679C19.91 2.02679 18.73 1.67679 16 3.50679C13.708 2.88561 11.292 2.88561 8.99999 3.50679C6.26999 1.67679 5.08999 2.02679 5.08999 2.02679C4.57636 3.22514 4.54413 4.5753 4.99999 5.79679C4.03011 6.81549 3.49251 8.17026 3.49999 9.57679C3.49999 14.9968 6.79998 16.1868 9.93998 16.5768C9.61098 16.9168 9.35725 17.3222 9.19529 17.7667C9.03334 18.2112 8.96679 18.6849 8.99999 19.1568V22.0268"></path>
<path d="M9 20.0267C6 20.9999 3.5 20.0267 2 17.0267"></path>
</svg>
GitHub
</div>
<div class="community-link" @click=${() => this.handleExternalLinkClick('https://discord.gg/GCBdubnXfJ')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5.5 16C10.5 18.5 13.5 18.5 18.5 16"></path>
<path d="M15.5 17.5L16.5 19.5C16.5 19.5 20.6713 18.1717 22 16C22 15 22.5301 7.85339 19 5.5C17.5 4.5 15 4 15 4L14 6H12"></path>
<path d="M8.52832 17.5L7.52832 19.5C7.52832 19.5 3.35699 18.1717 2.02832 16C2.02832 15 1.49823 7.85339 5.02832 5.5C6.52832 4.5 9.02832 4 9.02832 4L10.0283 6H12.0283"></path>
<path d="M8.5 14C7.67157 14 7 13.1046 7 12C7 10.8954 7.67157 10 8.5 10C9.32843 10 10 10.8954 10 12C10 13.1046 9.32843 14 8.5 14Z"></path>
<path d="M15.5 14C14.6716 14 14 13.1046 14 12C14 10.8954 14.6716 10 15.5 10C16.3284 10 17 10.8954 17 12C17 13.1046 16.3284 14 15.5 14Z"></path>
</svg>
Discord
</div>
</div>
</div>
<div class="option-group">
<div class="option-label">
<span>Keyboard Shortcuts</span>
</div>
<div class="keyboard-section">
<div class="keyboard-group">
<div class="keyboard-group-title">Window Movement</div>
<div class="shortcut-item">
<span class="shortcut-description">Move window up</span>
<div class="shortcut-keys">${this.formatKeybind(this.keybinds.moveUp)}</div>
</div>
<div class="shortcut-item">
<span class="shortcut-description">Move window down</span>
<div class="shortcut-keys">${this.formatKeybind(this.keybinds.moveDown)}</div>
</div>
<div class="shortcut-item">
<span class="shortcut-description">Move window left</span>
<div class="shortcut-keys">${this.formatKeybind(this.keybinds.moveLeft)}</div>
</div>
<div class="shortcut-item">
<span class="shortcut-description">Move window right</span>
<div class="shortcut-keys">${this.formatKeybind(this.keybinds.moveRight)}</div>
</div>
</div>
<div class="keyboard-group">
<div class="keyboard-group-title">Window Control</div>
<div class="shortcut-item">
<span class="shortcut-description">Toggle click-through mode</span>
<div class="shortcut-keys">${this.formatKeybind(this.keybinds.toggleClickThrough)}</div>
</div>
<div class="shortcut-item">
<span class="shortcut-description">Toggle window visibility</span>
<div class="shortcut-keys">${this.formatKeybind(this.keybinds.toggleVisibility)}</div>
</div>
</div>
<div class="keyboard-group">
<div class="keyboard-group-title">AI Actions</div>
<div class="shortcut-item">
<span class="shortcut-description">Take screenshot and ask for next step</span>
<div class="shortcut-keys">${this.formatKeybind(this.keybinds.nextStep)}</div>
</div>
</div>
<div class="keyboard-group">
<div class="keyboard-group-title">Response Navigation</div>
<div class="shortcut-item">
<span class="shortcut-description">Previous response</span>
<div class="shortcut-keys">${this.formatKeybind(this.keybinds.previousResponse)}</div>
</div>
<div class="shortcut-item">
<span class="shortcut-description">Next response</span>
<div class="shortcut-keys">${this.formatKeybind(this.keybinds.nextResponse)}</div>
</div>
<div class="shortcut-item">
<span class="shortcut-description">Scroll response up</span>
<div class="shortcut-keys">${this.formatKeybind(this.keybinds.scrollUp)}</div>
</div>
<div class="shortcut-item">
<span class="shortcut-description">Scroll response down</span>
<div class="shortcut-keys">${this.formatKeybind(this.keybinds.scrollDown)}</div>
</div>
</div>
<div class="keyboard-group">
<div class="keyboard-group-title">Text Input</div>
<div class="shortcut-item">
<span class="shortcut-description">Send message to AI</span>
<div class="shortcut-keys"><span class="key">Enter</span></div>
</div>
<div class="shortcut-item">
<span class="shortcut-description">New line in text input</span>
<div class="shortcut-keys"><span class="key">Shift</span><span class="key">Enter</span></div>
</div>
</div>
</div>
<div class="description" style="margin-top: 12px; text-align: center;">
You can customize these shortcuts in Settings.
</div>
</div>
<div class="option-group">
<div class="option-label">
<span>How to Use</span>
</div>
<div class="usage-steps">
<div class="usage-step"><strong>Start a Session:</strong> Enter your Gemini API key and click "Start Session"</div>
<div class="usage-step"><strong>Customize:</strong> Choose your profile and language in the settings</div>
<div class="usage-step">
<strong>Position Window:</strong> Use keyboard shortcuts to move the window to your desired location
</div>
<div class="usage-step">
<strong>Click-through Mode:</strong> Use ${this.formatKeybind(this.keybinds.toggleClickThrough)} to make the window
click-through
</div>
<div class="usage-step"><strong>Get AI Help:</strong> The AI will analyze your screen and audio to provide assistance</div>
<div class="usage-step"><strong>Text Messages:</strong> Type questions or requests to the AI using the text input</div>
<div class="usage-step">
<strong>Navigate Responses:</strong> Use ${this.formatKeybind(this.keybinds.previousResponse)} and
${this.formatKeybind(this.keybinds.nextResponse)} to browse through AI responses
</div>
</div>
</div>
<div class="option-group">
<div class="option-label">
<span>Supported Profiles</span>
</div>
<div class="profiles-grid">
<div class="profile-item">
<div class="profile-name">Job Interview</div>
<div class="profile-description">Get help with interview questions and responses</div>
</div>
<div class="profile-item">
<div class="profile-name">Sales Call</div>
<div class="profile-description">Assistance with sales conversations and objection handling</div>
</div>
<div class="profile-item">
<div class="profile-name">Business Meeting</div>
<div class="profile-description">Support for professional meetings and discussions</div>
</div>
<div class="profile-item">
<div class="profile-name">Presentation</div>
<div class="profile-description">Help with presentations and public speaking</div>
</div>
<div class="profile-item">
<div class="profile-name">Negotiation</div>
<div class="profile-description">Guidance for business negotiations and deals</div>
</div>
<div class="profile-item">
<div class="profile-name">Exam Assistant</div>
<div class="profile-description">Academic assistance for test-taking and exam questions</div>
</div>
</div>
</div>
<div class="option-group">
<div class="option-label">
<span>Audio Input</span>
</div>
<div class="description">The AI listens to conversations and provides contextual assistance based on what it hears.</div>
</div>
</div>
`;
}
}
customElements.define('help-view', HelpView);
+649
View File
@@ -0,0 +1,649 @@
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
import { resizeLayout } from '../../utils/windowResize.js';
export class HistoryView extends LitElement {
static styles = css`
* {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
cursor: default;
user-select: none;
}
:host {
height: 100%;
display: flex;
flex-direction: column;
width: 100%;
}
.history-container {
height: 100%;
display: flex;
flex-direction: column;
}
.sessions-list {
flex: 1;
overflow-y: auto;
}
.session-item {
padding: 12px;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: background 0.1s ease;
}
.session-item:hover {
background: var(--hover-background);
}
.session-item.selected {
background: var(--bg-secondary);
}
.session-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.session-date {
font-size: 12px;
font-weight: 500;
color: var(--text-color);
}
.session-time {
font-size: 11px;
color: var(--text-muted);
font-family: 'SF Mono', Monaco, monospace;
}
.session-preview {
font-size: 11px;
color: var(--text-muted);
line-height: 1.3;
}
.conversation-view {
flex: 1;
overflow-y: auto;
background: var(--bg-primary);
padding: 12px 0;
user-select: text;
cursor: text;
}
.message {
margin-bottom: 8px;
padding: 8px 12px;
border-left: 2px solid transparent;
font-size: 12px;
line-height: 1.4;
background: var(--bg-secondary);
user-select: text;
cursor: text;
white-space: pre-wrap;
word-wrap: break-word;
}
.message.user {
border-left-color: #3b82f6;
}
.message.ai {
border-left-color: #ef4444;
}
.back-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 12px 12px 12px 12px;
border-bottom: 1px solid var(--border-color);
}
.back-button {
background: transparent;
color: var(--text-color);
border: 1px solid var(--border-color);
padding: 6px 12px;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: background 0.1s ease;
}
.back-button:hover {
background: var(--hover-background);
}
.legend {
display: flex;
gap: 12px;
align-items: center;
}
.legend-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 10px;
color: var(--text-muted);
}
.legend-dot {
width: 8px;
height: 2px;
}
.legend-dot.user {
background-color: #3b82f6;
}
.legend-dot.ai {
background-color: #ef4444;
}
.legend-dot.screen {
background-color: #22c55e;
}
.session-context {
padding: 8px 12px;
margin-bottom: 8px;
background: var(--bg-tertiary);
border-radius: 4px;
font-size: 11px;
}
.session-context-row {
display: flex;
gap: 8px;
margin-bottom: 4px;
}
.session-context-row:last-child {
margin-bottom: 0;
}
.context-label {
color: var(--text-muted);
min-width: 80px;
}
.context-value {
color: var(--text-color);
font-weight: 500;
}
.custom-prompt-value {
color: var(--text-secondary);
font-style: italic;
word-break: break-word;
white-space: pre-wrap;
}
.view-tabs {
display: flex;
gap: 0;
border-bottom: 1px solid var(--border-color);
margin-bottom: 8px;
}
.view-tab {
background: transparent;
color: var(--text-muted);
border: none;
padding: 8px 16px;
font-size: 11px;
font-weight: 500;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.1s ease;
}
.view-tab:hover {
color: var(--text-color);
}
.view-tab.active {
color: var(--text-color);
border-bottom-color: var(--text-color);
}
.message.screen {
border-left-color: #22c55e;
}
.analysis-meta {
font-size: 10px;
color: var(--text-muted);
margin-bottom: 4px;
font-family: 'SF Mono', Monaco, monospace;
}
.empty-state {
text-align: center;
color: var(--text-muted);
font-size: 12px;
margin-top: 32px;
}
.empty-state-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
color: var(--text-secondary);
}
.loading {
text-align: center;
color: var(--text-muted);
font-size: 12px;
margin-top: 32px;
}
.sessions-list::-webkit-scrollbar,
.conversation-view::-webkit-scrollbar {
width: 8px;
}
.sessions-list::-webkit-scrollbar-track,
.conversation-view::-webkit-scrollbar-track {
background: transparent;
}
.sessions-list::-webkit-scrollbar-thumb,
.conversation-view::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 4px;
}
.sessions-list::-webkit-scrollbar-thumb:hover,
.conversation-view::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-hover);
}
.tabs-container {
display: flex;
gap: 0;
margin-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.tab {
background: transparent;
color: var(--text-muted);
border: none;
padding: 8px 16px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: color 0.1s ease;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.tab:hover {
color: var(--text-color);
}
.tab.active {
color: var(--text-color);
border-bottom-color: var(--text-color);
}
.saved-response-item {
padding: 12px 0;
border-bottom: 1px solid var(--border-color);
}
.saved-response-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 6px;
}
.saved-response-profile {
font-size: 11px;
font-weight: 500;
color: var(--text-secondary);
text-transform: capitalize;
}
.saved-response-date {
font-size: 10px;
color: var(--text-muted);
font-family: 'SF Mono', Monaco, monospace;
}
.saved-response-content {
font-size: 12px;
color: var(--text-color);
line-height: 1.4;
user-select: text;
cursor: text;
}
.delete-button {
background: transparent;
color: var(--text-muted);
border: none;
padding: 4px;
border-radius: 3px;
cursor: pointer;
transition: all 0.1s ease;
}
.delete-button:hover {
background: rgba(241, 76, 76, 0.1);
color: var(--error-color);
}
`;
static properties = {
sessions: { type: Array },
selectedSession: { type: Object },
loading: { type: Boolean },
activeTab: { type: String },
};
constructor() {
super();
this.sessions = [];
this.selectedSession = null;
this.loading = true;
this.activeTab = 'conversation'; // 'conversation' or 'screen'
this.loadSessions();
}
connectedCallback() {
super.connectedCallback();
// Resize window for this view
resizeLayout();
}
async loadSessions() {
try {
this.loading = true;
this.sessions = await cheatingDaddy.storage.getAllSessions();
} catch (error) {
console.error('Error loading conversation sessions:', error);
this.sessions = [];
} finally {
this.loading = false;
this.requestUpdate();
}
}
async loadSelectedSession(sessionId) {
try {
const session = await cheatingDaddy.storage.getSession(sessionId);
if (session) {
this.selectedSession = session;
this.requestUpdate();
}
} catch (error) {
console.error('Error loading session:', error);
}
}
formatDate(timestamp) {
const date = new Date(timestamp);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}
formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
});
}
formatTimestamp(timestamp) {
const date = new Date(timestamp);
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
getSessionPreview(session) {
const parts = [];
if (session.messageCount > 0) {
parts.push(`${session.messageCount} messages`);
}
if (session.screenAnalysisCount > 0) {
parts.push(`${session.screenAnalysisCount} screen analysis`);
}
if (session.profile) {
const profileNames = this.getProfileNames();
parts.push(profileNames[session.profile] || session.profile);
}
return parts.length > 0 ? parts.join(' • ') : 'Empty session';
}
handleSessionClick(session) {
this.loadSelectedSession(session.sessionId);
}
handleBackClick() {
this.selectedSession = null;
this.activeTab = 'conversation';
}
handleTabClick(tab) {
this.activeTab = tab;
}
getProfileNames() {
return {
interview: 'Job Interview',
sales: 'Sales Call',
meeting: 'Business Meeting',
presentation: 'Presentation',
negotiation: 'Negotiation',
exam: 'Exam Assistant',
};
}
renderSessionsList() {
if (this.loading) {
return html`<div class="loading">Loading conversation history...</div>`;
}
if (this.sessions.length === 0) {
return html`
<div class="empty-state">
<div class="empty-state-title">No conversations yet</div>
<div>Start a session to see your conversation history here</div>
</div>
`;
}
return html`
<div class="sessions-list">
${this.sessions.map(
session => html`
<div class="session-item" @click=${() => this.handleSessionClick(session)}>
<div class="session-header">
<div class="session-date">${this.formatDate(session.createdAt)}</div>
<div class="session-time">${this.formatTime(session.createdAt)}</div>
</div>
<div class="session-preview">${this.getSessionPreview(session)}</div>
</div>
`
)}
</div>
`;
}
renderContextContent() {
const { profile, customPrompt } = this.selectedSession;
const profileNames = this.getProfileNames();
if (!profile && !customPrompt) {
return html`<div class="empty-state">No profile context available</div>`;
}
return html`
<div class="session-context">
${profile ? html`
<div class="session-context-row">
<span class="context-label">Profile:</span>
<span class="context-value">${profileNames[profile] || profile}</span>
</div>
` : ''}
${customPrompt ? html`
<div class="session-context-row">
<span class="context-label">Custom Prompt:</span>
<span class="custom-prompt-value">${customPrompt}</span>
</div>
` : ''}
</div>
`;
}
renderConversationContent() {
const { conversationHistory } = this.selectedSession;
// Flatten the conversation turns into individual messages
const messages = [];
if (conversationHistory) {
conversationHistory.forEach(turn => {
if (turn.transcription) {
messages.push({
type: 'user',
content: turn.transcription,
timestamp: turn.timestamp,
});
}
if (turn.ai_response) {
messages.push({
type: 'ai',
content: turn.ai_response,
timestamp: turn.timestamp,
});
}
});
}
if (messages.length === 0) {
return html`<div class="empty-state">No conversation data available</div>`;
}
return messages.map(message => html`<div class="message ${message.type}">${message.content}</div>`);
}
renderScreenAnalysisContent() {
const { screenAnalysisHistory } = this.selectedSession;
if (!screenAnalysisHistory || screenAnalysisHistory.length === 0) {
return html`<div class="empty-state">No screen analysis data available</div>`;
}
return screenAnalysisHistory.map(analysis => html`
<div class="message screen"><div class="analysis-meta">${this.formatTimestamp(analysis.timestamp)}${analysis.model || 'unknown model'}</div>${analysis.response}</div>
`);
}
renderConversationView() {
if (!this.selectedSession) return html``;
const { conversationHistory, screenAnalysisHistory, profile, customPrompt } = this.selectedSession;
const hasConversation = conversationHistory && conversationHistory.length > 0;
const hasScreenAnalysis = screenAnalysisHistory && screenAnalysisHistory.length > 0;
const hasContext = profile || customPrompt;
return html`
<div class="back-header">
<button class="back-button" @click=${this.handleBackClick}>
<svg
width="16px"
height="16px"
stroke-width="1.7"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
color="currentColor"
>
<path d="M15 6L9 12L15 18" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
Back to Sessions
</button>
<div class="legend">
<div class="legend-item">
<div class="legend-dot user"></div>
<span>Them</span>
</div>
<div class="legend-item">
<div class="legend-dot ai"></div>
<span>Suggestion</span>
</div>
<div class="legend-item">
<div class="legend-dot screen"></div>
<span>Screen</span>
</div>
</div>
</div>
<div class="view-tabs">
<button
class="view-tab ${this.activeTab === 'conversation' ? 'active' : ''}"
@click=${() => this.handleTabClick('conversation')}
>
Conversation ${hasConversation ? `(${conversationHistory.length})` : ''}
</button>
<button
class="view-tab ${this.activeTab === 'screen' ? 'active' : ''}"
@click=${() => this.handleTabClick('screen')}
>
Screen ${hasScreenAnalysis ? `(${screenAnalysisHistory.length})` : ''}
</button>
<button
class="view-tab ${this.activeTab === 'context' ? 'active' : ''}"
@click=${() => this.handleTabClick('context')}
>
Context ${hasContext ? '' : '(empty)'}
</button>
</div>
<div class="conversation-view">
${this.activeTab === 'conversation'
? this.renderConversationContent()
: this.activeTab === 'screen'
? this.renderScreenAnalysisContent()
: this.renderContextContent()}
</div>
`;
}
render() {
if (this.selectedSession) {
return html`<div class="history-container">${this.renderConversationView()}</div>`;
}
return html`
<div class="history-container">
${this.renderSessionsList()}
</div>
`;
}
}
customElements.define('history-view', HistoryView);
+241
View File
@@ -0,0 +1,241 @@
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
import { resizeLayout } from '../../utils/windowResize.js';
export class MainView extends LitElement {
static styles = css`
* {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
cursor: default;
user-select: none;
}
.welcome {
font-size: 20px;
margin-bottom: 6px;
font-weight: 500;
color: var(--text-color);
margin-top: auto;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 16px;
}
.input-group input {
flex: 1;
}
input {
background: var(--input-background);
color: var(--text-color);
border: 1px solid var(--border-color);
padding: 10px 12px;
width: 100%;
border-radius: 3px;
font-size: 13px;
transition: border-color 0.1s ease;
}
input:focus {
outline: none;
border-color: var(--border-default);
}
input::placeholder {
color: var(--placeholder-color);
}
/* Red blink animation for empty API key */
input.api-key-error {
animation: blink-red 0.6s ease-in-out;
border-color: var(--error-color);
}
@keyframes blink-red {
0%, 100% {
border-color: var(--border-color);
}
50% {
border-color: var(--error-color);
background: rgba(241, 76, 76, 0.1);
}
}
.start-button {
background: var(--start-button-background);
color: var(--start-button-color);
border: none;
padding: 10px 16px;
border-radius: 3px;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
display: flex;
align-items: center;
gap: 8px;
transition: background 0.1s ease;
}
.start-button:hover {
background: var(--start-button-hover-background);
}
.start-button.initializing {
opacity: 0.5;
cursor: not-allowed;
}
.start-button.initializing:hover {
background: var(--start-button-background);
}
.shortcut-hint {
font-size: 11px;
color: var(--text-muted);
font-family: 'SF Mono', Monaco, monospace;
}
.description {
color: var(--text-secondary);
font-size: 13px;
margin-bottom: 20px;
line-height: 1.5;
}
.link {
color: var(--text-color);
text-decoration: underline;
cursor: pointer;
text-underline-offset: 2px;
}
.link:hover {
color: var(--text-color);
}
:host {
height: 100%;
display: flex;
flex-direction: column;
width: 100%;
max-width: 480px;
}
`;
static properties = {
onStart: { type: Function },
onAPIKeyHelp: { type: Function },
isInitializing: { type: Boolean },
onLayoutModeChange: { type: Function },
showApiKeyError: { type: Boolean },
};
constructor() {
super();
this.onStart = () => {};
this.onAPIKeyHelp = () => {};
this.isInitializing = false;
this.onLayoutModeChange = () => {};
this.showApiKeyError = false;
this.boundKeydownHandler = this.handleKeydown.bind(this);
this.apiKey = '';
this._loadApiKey();
}
async _loadApiKey() {
this.apiKey = await cheatingDaddy.storage.getApiKey();
this.requestUpdate();
}
connectedCallback() {
super.connectedCallback();
window.electron?.ipcRenderer?.on('session-initializing', (event, isInitializing) => {
this.isInitializing = isInitializing;
});
// Add keyboard event listener for Ctrl+Enter (or Cmd+Enter on Mac)
document.addEventListener('keydown', this.boundKeydownHandler);
// Resize window for this view
resizeLayout();
}
disconnectedCallback() {
super.disconnectedCallback();
window.electron?.ipcRenderer?.removeAllListeners('session-initializing');
// Remove keyboard event listener
document.removeEventListener('keydown', this.boundKeydownHandler);
}
handleKeydown(e) {
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const isStartShortcut = isMac ? e.metaKey && e.key === 'Enter' : e.ctrlKey && e.key === 'Enter';
if (isStartShortcut) {
e.preventDefault();
this.handleStartClick();
}
}
async handleInput(e) {
this.apiKey = e.target.value;
await cheatingDaddy.storage.setApiKey(e.target.value);
// Clear error state when user starts typing
if (this.showApiKeyError) {
this.showApiKeyError = false;
}
}
handleStartClick() {
if (this.isInitializing) {
return;
}
this.onStart();
}
handleAPIKeyHelpClick() {
this.onAPIKeyHelp();
}
// Method to trigger the red blink animation
triggerApiKeyError() {
this.showApiKeyError = true;
// Remove the error class after 1 second
setTimeout(() => {
this.showApiKeyError = false;
}, 1000);
}
getStartButtonText() {
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const shortcut = isMac ? 'Cmd+Enter' : 'Ctrl+Enter';
return html`Start <span class="shortcut-hint">${shortcut}</span>`;
}
render() {
return html`
<div class="welcome">Welcome</div>
<div class="input-group">
<input
type="password"
placeholder="Enter your Gemini API Key"
.value=${this.apiKey}
@input=${this.handleInput}
class="${this.showApiKeyError ? 'api-key-error' : ''}"
/>
<button @click=${this.handleStartClick} class="start-button ${this.isInitializing ? 'initializing' : ''}">
${this.getStartButtonText()}
</button>
</div>
<p class="description">
dont have an api key?
<span @click=${this.handleAPIKeyHelpClick} class="link">get one here</span>
</p>
`;
}
}
customElements.define('main-view', MainView);
+592
View File
@@ -0,0 +1,592 @@
import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';
export class OnboardingView extends LitElement {
static styles = css`
* {
font-family:
'Inter',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif;
cursor: default;
user-select: none;
margin: 0;
padding: 0;
box-sizing: border-box;
}
:host {
display: block;
height: 100%;
width: 100%;
position: fixed;
top: 0;
left: 0;
overflow: hidden;
}
.onboarding-container {
position: relative;
width: 100%;
height: 100%;
background: #0a0a0a;
overflow: hidden;
}
.close-button {
position: absolute;
top: 12px;
right: 12px;
z-index: 10;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
color: rgba(255, 255, 255, 0.6);
}
.close-button:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.9);
}
.close-button svg {
width: 16px;
height: 16px;
}
.gradient-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.content-wrapper {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 60px;
z-index: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding: 32px 48px;
max-width: 500px;
color: #e5e5e5;
overflow: hidden;
}
.slide-icon {
width: 48px;
height: 48px;
margin-bottom: 16px;
opacity: 0.9;
display: block;
}
.slide-title {
font-size: 28px;
font-weight: 600;
margin-bottom: 12px;
color: #ffffff;
line-height: 1.3;
}
.slide-content {
font-size: 16px;
line-height: 1.5;
margin-bottom: 24px;
color: #b8b8b8;
font-weight: 400;
}
.context-textarea {
width: 100%;
height: 100px;
padding: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: #e5e5e5;
font-size: 14px;
font-family: inherit;
resize: vertical;
transition: all 0.2s ease;
margin-bottom: 24px;
}
.context-textarea::placeholder {
color: rgba(255, 255, 255, 0.4);
font-size: 14px;
}
.context-textarea:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.08);
}
.feature-list {
max-width: 100%;
}
.feature-item {
display: flex;
align-items: center;
margin-bottom: 12px;
font-size: 15px;
color: #b8b8b8;
}
.feature-icon {
font-size: 16px;
margin-right: 12px;
opacity: 0.8;
}
.navigation {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border-top: 1px solid rgba(255, 255, 255, 0.05);
height: 60px;
box-sizing: border-box;
}
.nav-button {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #e5e5e5;
padding: 8px 16px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 36px;
min-height: 36px;
}
.nav-button:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.2);
}
.nav-button:active {
transform: scale(0.98);
}
.nav-button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.nav-button:disabled:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.1);
transform: none;
}
.progress-dots {
display: flex;
gap: 12px;
align-items: center;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
transition: all 0.2s ease;
cursor: pointer;
}
.dot:hover {
background: rgba(255, 255, 255, 0.4);
}
.dot.active {
background: rgba(255, 255, 255, 0.8);
transform: scale(1.2);
}
`;
static properties = {
currentSlide: { type: Number },
contextText: { type: String },
onComplete: { type: Function },
onClose: { type: Function },
};
constructor() {
super();
this.currentSlide = 0;
this.contextText = '';
this.onComplete = () => {};
this.onClose = () => {};
this.canvas = null;
this.ctx = null;
this.animationId = null;
// Transition properties
this.isTransitioning = false;
this.transitionStartTime = 0;
this.transitionDuration = 800; // 800ms fade duration
this.previousColorScheme = null;
// Subtle dark color schemes for each slide
this.colorSchemes = [
// Slide 1 - Welcome (Very dark purple/gray)
[
[25, 25, 35], // Dark gray-purple
[20, 20, 30], // Darker gray
[30, 25, 40], // Slightly purple
[15, 15, 25], // Very dark
[35, 30, 45], // Muted purple
[10, 10, 20], // Almost black
],
// Slide 2 - Privacy (Dark blue-gray)
[
[20, 25, 35], // Dark blue-gray
[15, 20, 30], // Darker blue-gray
[25, 30, 40], // Slightly blue
[10, 15, 25], // Very dark blue
[30, 35, 45], // Muted blue
[5, 10, 20], // Almost black
],
// Slide 3 - Context (Dark neutral)
[
[25, 25, 25], // Neutral dark
[20, 20, 20], // Darker neutral
[30, 30, 30], // Light dark
[15, 15, 15], // Very dark
[35, 35, 35], // Lighter dark
[10, 10, 10], // Almost black
],
// Slide 4 - Features (Dark green-gray)
[
[20, 30, 25], // Dark green-gray
[15, 25, 20], // Darker green-gray
[25, 35, 30], // Slightly green
[10, 20, 15], // Very dark green
[30, 40, 35], // Muted green
[5, 15, 10], // Almost black
],
// Slide 5 - Complete (Dark warm gray)
[
[30, 25, 20], // Dark warm gray
[25, 20, 15], // Darker warm
[35, 30, 25], // Slightly warm
[20, 15, 10], // Very dark warm
[40, 35, 30], // Muted warm
[15, 10, 5], // Almost black
],
];
}
firstUpdated() {
this.canvas = this.shadowRoot.querySelector('.gradient-canvas');
this.ctx = this.canvas.getContext('2d');
this.resizeCanvas();
this.startGradientAnimation();
window.addEventListener('resize', () => this.resizeCanvas());
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
window.removeEventListener('resize', () => this.resizeCanvas());
}
resizeCanvas() {
if (!this.canvas) return;
const rect = this.getBoundingClientRect();
this.canvas.width = rect.width;
this.canvas.height = rect.height;
}
startGradientAnimation() {
if (!this.ctx) return;
const animate = timestamp => {
this.drawGradient(timestamp);
this.animationId = requestAnimationFrame(animate);
};
animate(0);
}
drawGradient(timestamp) {
if (!this.ctx || !this.canvas) return;
const { width, height } = this.canvas;
let colors = this.colorSchemes[this.currentSlide];
// Handle color scheme transitions
if (this.isTransitioning && this.previousColorScheme) {
const elapsed = timestamp - this.transitionStartTime;
const progress = Math.min(elapsed / this.transitionDuration, 1);
// Use easing function for smoother transition
const easedProgress = this.easeInOutCubic(progress);
colors = this.interpolateColorSchemes(this.previousColorScheme, this.colorSchemes[this.currentSlide], easedProgress);
// End transition when complete
if (progress >= 1) {
this.isTransitioning = false;
this.previousColorScheme = null;
}
}
const time = timestamp * 0.0005; // Much slower animation
// Create moving gradient with subtle flow
const flowX = Math.sin(time * 0.7) * width * 0.3;
const flowY = Math.cos(time * 0.5) * height * 0.2;
const gradient = this.ctx.createLinearGradient(flowX, flowY, width + flowX * 0.5, height + flowY * 0.5);
// Very subtle color variations with movement
colors.forEach((color, index) => {
const offset = index / (colors.length - 1);
const wave = Math.sin(time + index * 0.3) * 0.05; // Very subtle wave
const r = Math.max(0, Math.min(255, color[0] + wave * 5));
const g = Math.max(0, Math.min(255, color[1] + wave * 5));
const b = Math.max(0, Math.min(255, color[2] + wave * 5));
gradient.addColorStop(offset, `rgb(${r}, ${g}, ${b})`);
});
// Fill with moving gradient
this.ctx.fillStyle = gradient;
this.ctx.fillRect(0, 0, width, height);
// Add a second layer with radial gradient for more depth
const centerX = width * 0.5 + Math.sin(time * 0.3) * width * 0.15;
const centerY = height * 0.5 + Math.cos(time * 0.4) * height * 0.1;
const radius = Math.max(width, height) * 0.8;
const radialGradient = this.ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
// Very subtle radial overlay
radialGradient.addColorStop(0, `rgba(${colors[0][0] + 10}, ${colors[0][1] + 10}, ${colors[0][2] + 10}, 0.1)`);
radialGradient.addColorStop(0.5, `rgba(${colors[2][0]}, ${colors[2][1]}, ${colors[2][2]}, 0.05)`);
radialGradient.addColorStop(
1,
`rgba(${colors[colors.length - 1][0]}, ${colors[colors.length - 1][1]}, ${colors[colors.length - 1][2]}, 0.03)`
);
this.ctx.globalCompositeOperation = 'overlay';
this.ctx.fillStyle = radialGradient;
this.ctx.fillRect(0, 0, width, height);
this.ctx.globalCompositeOperation = 'source-over';
}
nextSlide() {
if (this.currentSlide < 4) {
this.startColorTransition(this.currentSlide + 1);
} else {
this.completeOnboarding();
}
}
prevSlide() {
if (this.currentSlide > 0) {
this.startColorTransition(this.currentSlide - 1);
}
}
startColorTransition(newSlide) {
this.previousColorScheme = [...this.colorSchemes[this.currentSlide]];
this.currentSlide = newSlide;
this.isTransitioning = true;
this.transitionStartTime = performance.now();
}
// Interpolate between two color schemes
interpolateColorSchemes(scheme1, scheme2, progress) {
return scheme1.map((color1, index) => {
const color2 = scheme2[index];
return [
color1[0] + (color2[0] - color1[0]) * progress,
color1[1] + (color2[1] - color1[1]) * progress,
color1[2] + (color2[2] - color1[2]) * progress,
];
});
}
// Easing function for smooth transitions
easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
handleContextInput(e) {
this.contextText = e.target.value;
}
async handleClose() {
if (window.require) {
const { ipcRenderer } = window.require('electron');
await ipcRenderer.invoke('quit-application');
}
}
async completeOnboarding() {
if (this.contextText.trim()) {
await cheatingDaddy.storage.updatePreference('customPrompt', this.contextText.trim());
}
await cheatingDaddy.storage.updateConfig('onboarded', true);
this.onComplete();
}
getSlideContent() {
const slides = [
{
icon: 'assets/onboarding/welcome.svg',
title: 'Welcome to Cheating Daddy',
content:
'Your AI assistant that listens and watches, then provides intelligent suggestions automatically during interviews and meetings.',
},
{
icon: 'assets/onboarding/security.svg',
title: 'Completely Private',
content: 'Invisible to screen sharing apps and recording software. Your secret advantage stays completely hidden from others.',
},
{
icon: 'assets/onboarding/context.svg',
title: 'Add Your Context',
content: 'Share relevant information to help the AI provide better, more personalized assistance.',
showTextarea: true,
},
{
icon: 'assets/onboarding/customize.svg',
title: 'Additional Features',
content: '',
showFeatures: true,
},
{
icon: 'assets/onboarding/ready.svg',
title: 'Ready to Go',
content: 'Add your Gemini API key in settings and start getting AI-powered assistance in real-time.',
},
];
return slides[this.currentSlide];
}
render() {
const slide = this.getSlideContent();
return html`
<div class="onboarding-container">
<button class="close-button" @click=${this.handleClose} title="Close">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
</svg>
</button>
<canvas class="gradient-canvas"></canvas>
<div class="content-wrapper">
<img class="slide-icon" src="${slide.icon}" alt="${slide.title} icon" />
<div class="slide-title">${slide.title}</div>
<div class="slide-content">${slide.content}</div>
${slide.showTextarea
? html`
<textarea
class="context-textarea"
placeholder="Paste your resume, job description, or any relevant context here..."
.value=${this.contextText}
@input=${this.handleContextInput}
></textarea>
`
: ''}
${slide.showFeatures
? html`
<div class="feature-list">
<div class="feature-item">
<span class="feature-icon">-</span>
Customize AI behavior and responses
</div>
<div class="feature-item">
<span class="feature-icon">-</span>
Review conversation history
</div>
<div class="feature-item">
<span class="feature-icon">-</span>
Adjust capture settings and intervals
</div>
</div>
`
: ''}
</div>
<div class="navigation">
<button class="nav-button" @click=${this.prevSlide} ?disabled=${this.currentSlide === 0}>
<svg width="16px" height="16px" stroke-width="2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 6L9 12L15 18" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</button>
<div class="progress-dots">
${[0, 1, 2, 3, 4].map(
index => html`
<div
class="dot ${index === this.currentSlide ? 'active' : ''}"
@click=${() => {
if (index !== this.currentSlide) {
this.startColorTransition(index);
}
}}
></div>
`
)}
</div>
<button class="nav-button" @click=${this.nextSlide}>
${this.currentSlide === 4
? 'Get Started'
: html`
<svg width="16px" height="16px" stroke-width="2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 6L15 12L9 18" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
`}
</button>
</div>
</div>
`;
}
}
customElements.define('onboarding-view', OnboardingView);