cheat-exam/src/components/views/AssistantView.js
Илья Глазунов 669c019fd8 prettier fix
2026-01-15 23:26:09 +03:00

719 lines
24 KiB
JavaScript

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;
}
.capture-buttons {
display: flex;
gap: 6px;
}
.region-select-btn {
display: flex;
align-items: center;
justify-content: center;
background: transparent;
color: var(--text-secondary);
border: 1px solid var(--border-color);
padding: 6px 10px;
border-radius: 20px;
font-size: 12px;
cursor: pointer;
transition: all 0.15s ease;
}
.region-select-btn:hover {
background: var(--hover-background);
color: var(--text-color);
border-color: var(--text-color);
}
.region-select-btn svg {
width: 16px;
height: 16px;
}
.region-select-btn span {
margin-left: 4px;
}
`;
static properties = {
responses: { type: Array },
currentResponseIndex: { type: Number },
selectedProfile: { type: String },
onSendText: { type: Function },
shouldAnimateResponse: { type: Boolean },
flashCount: { type: Number },
flashLiteCount: { type: Number },
aiProvider: { type: String },
};
constructor() {
super();
this.responses = [];
this.currentResponseIndex = -1;
this.selectedProfile = 'interview';
this.onSendText = () => {};
this.flashCount = 0;
this.flashLiteCount = 0;
this.aiProvider = 'gemini';
}
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);
}
}
handleRegionSelect() {
if (window.startRegionSelection) {
window.startRegionSelection();
// 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="capture-buttons">
<button class="region-select-btn" @click=${this.handleRegionSelect} title="Select region to analyze (like Win+Shift+S)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M4.25 2A2.25 2.25 0 0 0 2 4.25v2.5A.75.75 0 0 0 3.5 6.75v-2.5a.75.75 0 0 1 .75-.75h2.5A.75.75 0 0 0 6.75 2h-2.5Zm9.5 0a.75.75 0 0 0 0 1.5h2.5a.75.75 0 0 1 .75.75v2.5a.75.75 0 0 0 1.5 0v-2.5A2.25 2.25 0 0 0 16.25 2h-2.5ZM3.5 13.25a.75.75 0 0 0-1.5 0v2.5A2.25 2.25 0 0 0 4.25 18h2.5a.75.75 0 0 0 0-1.5h-2.5a.75.75 0 0 1-.75-.75v-2.5Zm13.5 0a.75.75 0 0 0 1.5 0v2.5A2.25 2.25 0 0 1 16.25 18h-2.5a.75.75 0 0 1 0-1.5h2.5a.75.75 0 0 0 .75-.75v-2.5Z"
clip-rule="evenodd"
/>
</svg>
<span>Select region</span>
</button>
<div class="screen-answer-btn-wrapper">
${this.aiProvider === 'gemini'
? html`
<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>Full screen</span>
${this.aiProvider === 'gemini'
? html`<span class="usage-count">(${this.getTotalUsed()}/${this.getTotalAvailable()})</span>`
: ''}
</button>
</div>
</div>
</div>
`;
}
}
customElements.define('assistant-view', AssistantView);