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: var(--font); cursor: default; } /* ── Response area ── */ .response-container { flex: 1; overflow-y: auto; font-size: var(--response-font-size, 15px); line-height: var(--line-height); background: var(--bg-app); padding: var(--space-sm) var(--space-md); scroll-behavior: smooth; user-select: text; cursor: text; color: var(--text-primary); } .response-container * { user-select: text; cursor: text; } .response-container a { cursor: pointer; } .response-container [data-word] { display: inline-block; } /* ── Markdown ── */ .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-primary); font-weight: var(--font-weight-semibold); } .response-container h1 { font-size: 1.5em; } .response-container h2 { font-size: 1.3em; } .response-container h3 { font-size: 1.15em; } .response-container h4 { font-size: 1.05em; } .response-container h5, .response-container h6 { font-size: 1em; } .response-container p { margin: 0.6em 0; color: var(--text-primary); } .response-container ul, .response-container ol { margin: 0.6em 0; padding-left: 1.5em; color: var(--text-primary); } .response-container li { margin: 0.3em 0; } .response-container blockquote { margin: 0.8em 0; padding: 0.5em 1em; border-left: 2px solid var(--border-strong); background: var(--bg-surface); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; } .response-container code { background: var(--bg-elevated); padding: 0.15em 0.4em; border-radius: var(--radius-sm); font-family: var(--font-mono); font-size: 0.85em; } .response-container pre { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--space-md); overflow-x: auto; margin: 0.8em 0; position: relative; } .response-container pre::before { content: attr(data-language); position: absolute; top: 0; right: 0; background: var(--bg-elevated); color: var(--text-secondary); padding: 4px 12px; font-size: var(--font-size-xs); font-family: var(--font-mono); border: 1px solid var(--border); border-top: none; border-right: none; border-bottom-left-radius: var(--radius-sm); text-transform: uppercase; letter-spacing: 0.5px; } .response-container pre code { background: none; padding: 0; font-family: var(--font-mono); font-size: 0.9em; line-height: 1.5; color: var(--text-primary); } /* ── Syntax highlighting for code blocks ── */ /* Default (Dark theme) */ .response-container .hljs { color: #c9d1d9; background: transparent; } .response-container .hljs-doctag, .response-container .hljs-keyword, .response-container .hljs-meta .hljs-keyword, .response-container .hljs-template-tag, .response-container .hljs-template-variable, .response-container .hljs-type, .response-container .hljs-variable.language_ { color: #ff7b72; } .response-container .hljs-title, .response-container .hljs-title.class_, .response-container .hljs-title.class_.inherited__, .response-container .hljs-title.function_ { color: #d2a8ff; } .response-container .hljs-attr, .response-container .hljs-attribute, .response-container .hljs-literal, .response-container .hljs-meta, .response-container .hljs-number, .response-container .hljs-operator, .response-container .hljs-selector-attr, .response-container .hljs-selector-class, .response-container .hljs-selector-id, .response-container .hljs-variable { color: #79c0ff; } .response-container .hljs-meta .hljs-string, .response-container .hljs-regexp, .response-container .hljs-string { color: #a5d6ff; } .response-container .hljs-built_in, .response-container .hljs-symbol { color: #ffa657; } .response-container .hljs-code, .response-container .hljs-comment, .response-container .hljs-formula { color: #8b949e; } .response-container .hljs-name, .response-container .hljs-quote, .response-container .hljs-selector-pseudo, .response-container .hljs-selector-tag { color: #7ee787; } .response-container .hljs-subst { color: #c9d1d9; } .response-container .hljs-section { color: #1f6feb; font-weight: 700; } .response-container .hljs-bullet { color: #f2cc60; } .response-container .hljs-emphasis { color: #c9d1d9; font-style: italic; } .response-container .hljs-strong { color: #c9d1d9; font-weight: 700; } .response-container .hljs-addition { color: #aff5b4; background-color: #033a16; } .response-container .hljs-deletion { color: #ffdcd7; background-color: #67060c; } /* Light theme syntax highlighting */ :host-context(body[data-theme-type="light"]) .response-container .hljs { color: #24292f; } :host-context(body[data-theme-type="light"]) .response-container .hljs-doctag, :host-context(body[data-theme-type="light"]) .response-container .hljs-keyword, :host-context(body[data-theme-type="light"]) .response-container .hljs-meta .hljs-keyword, :host-context(body[data-theme-type="light"]) .response-container .hljs-template-tag, :host-context(body[data-theme-type="light"]) .response-container .hljs-template-variable, :host-context(body[data-theme-type="light"]) .response-container .hljs-type, :host-context(body[data-theme-type="light"]) .response-container .hljs-variable.language_ { color: #cf222e; } :host-context(body[data-theme-type="light"]) .response-container .hljs-title, :host-context(body[data-theme-type="light"]) .response-container .hljs-title.class_, :host-context(body[data-theme-type="light"]) .response-container .hljs-title.class_.inherited__, :host-context(body[data-theme-type="light"]) .response-container .hljs-title.function_ { color: #8250df; } :host-context(body[data-theme-type="light"]) .response-container .hljs-attr, :host-context(body[data-theme-type="light"]) .response-container .hljs-attribute, :host-context(body[data-theme-type="light"]) .response-container .hljs-literal, :host-context(body[data-theme-type="light"]) .response-container .hljs-meta, :host-context(body[data-theme-type="light"]) .response-container .hljs-number, :host-context(body[data-theme-type="light"]) .response-container .hljs-operator, :host-context(body[data-theme-type="light"]) .response-container .hljs-selector-attr, :host-context(body[data-theme-type="light"]) .response-container .hljs-selector-class, :host-context(body[data-theme-type="light"]) .response-container .hljs-selector-id, :host-context(body[data-theme-type="light"]) .response-container .hljs-variable { color: #0550ae; } :host-context(body[data-theme-type="light"]) .response-container .hljs-meta .hljs-string, :host-context(body[data-theme-type="light"]) .response-container .hljs-regexp, :host-context(body[data-theme-type="light"]) .response-container .hljs-string { color: #0a3069; } :host-context(body[data-theme-type="light"]) .response-container .hljs-built_in, :host-context(body[data-theme-type="light"]) .response-container .hljs-symbol { color: #953800; } :host-context(body[data-theme-type="light"]) .response-container .hljs-code, :host-context(body[data-theme-type="light"]) .response-container .hljs-comment, :host-context(body[data-theme-type="light"]) .response-container .hljs-formula { color: #6e7781; } :host-context(body[data-theme-type="light"]) .response-container .hljs-name, :host-context(body[data-theme-type="light"]) .response-container .hljs-quote, :host-context(body[data-theme-type="light"]) .response-container .hljs-selector-pseudo, :host-context(body[data-theme-type="light"]) .response-container .hljs-selector-tag { color: #116329; } :host-context(body[data-theme-type="light"]) .response-container .hljs-subst { color: #24292f; } :host-context(body[data-theme-type="light"]) .response-container .hljs-section { color: #0969da; font-weight: 700; } :host-context(body[data-theme-type="light"]) .response-container .hljs-bullet { color: #953800; } :host-context(body[data-theme-type="light"]) .response-container .hljs-emphasis { color: #24292f; font-style: italic; } :host-context(body[data-theme-type="light"]) .response-container .hljs-strong { color: #24292f; font-weight: 700; } :host-context(body[data-theme-type="light"]) .response-container .hljs-addition { color: #116329; background-color: #dafbe1; } :host-context(body[data-theme-type="light"]) .response-container .hljs-deletion { color: #82071e; background-color: #ffebe9; } .response-container a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; } .response-container strong, .response-container b { font-weight: var(--font-weight-semibold); } .response-container hr { border: none; border-top: 1px solid var(--border); 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); padding: var(--space-sm); text-align: left; } .response-container th { background: var(--bg-surface); font-weight: var(--font-weight-semibold); } .response-container::-webkit-scrollbar { width: 6px; } .response-container::-webkit-scrollbar-track { background: transparent; } .response-container::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; } .response-container::-webkit-scrollbar-thumb:hover { background: #444444; } /* ── Response navigation strip ── */ .response-nav { display: flex; align-items: center; justify-content: center; gap: var(--space-sm); padding: var(--space-xs) var(--space-md); border-top: 1px solid var(--border); background: var(--bg-app); } .nav-btn { background: none; border: none; color: var(--text-muted); cursor: pointer; padding: var(--space-xs); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; transition: color var(--transition); } .nav-btn:hover:not(:disabled) { color: var(--text-primary); } .nav-btn:disabled { opacity: 0.25; cursor: default; } .nav-btn svg { width: 14px; height: 14px; } .response-counter { font-size: var(--font-size-xs); color: var(--text-muted); font-family: var(--font-mono); min-width: 40px; text-align: center; } /* ── Bottom input bar ── */ .input-bar { display: flex; align-items: center; gap: var(--space-sm); padding: var(--space-md); background: var(--bg-app); } .input-bar-inner { display: flex; align-items: center; flex: 1; background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 100px; padding: 0 var(--space-md); height: 32px; transition: border-color var(--transition); } .input-bar-inner:focus-within { border-color: var(--accent); } .input-bar-inner input { flex: 1; background: none; color: var(--text-primary); border: none; padding: 0; font-size: var(--font-size-sm); font-family: var(--font); height: 100%; outline: none; } .input-bar-inner input::placeholder { color: var(--text-muted); } .analyze-btn { position: relative; background: var(--bg-elevated); border: 1px solid var(--border); color: var(--text-primary); cursor: pointer; font-size: var(--font-size-xs); font-family: var(--font-mono); white-space: nowrap; padding: var(--space-xs) var(--space-md); border-radius: 100px; height: 32px; display: flex; align-items: center; gap: 4px; transition: border-color 0.4s ease, background var(--transition); flex-shrink: 0; overflow: hidden; } .analyze-btn:hover:not(.analyzing) { border-color: var(--accent); background: var(--bg-surface); } .analyze-btn.analyzing { cursor: default; border-color: transparent; } .analyze-btn-content { display: flex; align-items: center; gap: 4px; transition: opacity 0.4s ease; z-index: 1; position: relative; } .analyze-btn.analyzing .analyze-btn-content { opacity: 0; } .analyze-canvas { position: absolute; inset: -1px; width: calc(100% + 2px); height: calc(100% + 2px); pointer-events: none; } /* ── Expand button ── */ .expand-bar { display: flex; align-items: center; justify-content: center; padding: var(--space-xs) var(--space-md); border-top: 1px solid var(--border); background: var(--bg-app); } .expand-btn { display: flex; align-items: center; gap: 4px; background: none; border: 1px solid var(--border); color: var(--text-secondary); cursor: pointer; font-size: var(--font-size-xs); font-family: var(--font-mono); padding: var(--space-xs) var(--space-md); border-radius: 100px; height: 26px; transition: color var(--transition), border-color var(--transition), background var(--transition); } .expand-btn:hover:not(:disabled) { color: var(--text-primary); border-color: var(--accent); background: var(--bg-surface); } .expand-btn:disabled { opacity: 0.4; cursor: default; } .expand-btn svg { width: 12px; height: 12px; } `; static properties = { responses: { type: Array }, currentResponseIndex: { type: Number }, selectedProfile: { type: String }, onSendText: { type: Function }, onExpandResponse: { type: Function }, shouldAnimateResponse: { type: Boolean }, isAnalyzing: { type: Boolean, state: true }, isExpanding: { type: Boolean, state: true }, }; constructor() { super(); this.responses = []; this.currentResponseIndex = -1; this.selectedProfile = "interview"; this.onSendText = () => {}; this.onExpandResponse = () => {}; this.isAnalyzing = false; this.isExpanding = false; this._animFrame = null; } 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] : `Listening to your ${profileNames[this.selectedProfile] || "session"}...`; } renderMarkdown(content) { if (typeof window !== "undefined" && window.marked) { try { // Configure marked to use highlight.js for syntax highlighting window.marked.setOptions({ breaks: true, gfm: true, sanitize: false, highlight: (code, lang) => { if (window.hljs && lang) { try { return window.hljs.highlight(code, { language: lang }).value; } catch (e) { // If language is not recognized, try auto-detection try { return window.hljs.highlightAuto(code).value; } catch (err) { return window.hljs.escapeHtml(code); } } } else if (window.hljs) { // Auto-detect language if not specified try { return window.hljs.highlightAuto(code).value; } catch (e) { return window.hljs.escapeHtml(code); } } return code; }, }); let rendered = window.marked.parse(content); rendered = this.wrapWordsInSpans(rendered); return rendered; } catch (error) { console.warn("Error parsing markdown:", error); return content; } } return content; } wrapWordsInSpans(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); const tagsToSkip = ["PRE", "CODE"]; 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; } applyCodeHighlighting(container) { if (!window.hljs) return; // Find all code blocks in the rendered content const codeBlocks = container.querySelectorAll("pre code"); codeBlocks.forEach((block) => { const pre = block.parentElement; if (!pre || pre.tagName !== "PRE") return; // Skip if already highlighted if (block.classList.contains("hljs")) { return; } const code = block.textContent; let lang = block.className.replace(/language-|lang-/, "") || ""; try { if (lang && window.hljs.getLanguage(lang)) { block.innerHTML = window.hljs.highlight(code, { language: lang }).value; } else { // Auto-detect language const result = window.hljs.highlightAuto(code); block.innerHTML = result.value; if (result.language && !lang) { lang = result.language; block.className = `language-${lang}`; } } block.classList.add("hljs"); // Set data-language attribute on pre tag for display if (lang) { pre.setAttribute("data-language", lang); } } catch (e) { console.warn("Error highlighting code block:", e); // Leave block as-is if highlighting fails } }); } 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; container.scrollTop = Math.max(0, container.scrollTop - scrollAmount); } } scrollResponseDown() { const container = this.shadowRoot.querySelector(".response-container"); if (container) { const scrollAmount = container.clientHeight * 0.3; container.scrollTop = Math.min( container.scrollHeight - container.clientHeight, container.scrollTop + scrollAmount, ); } } connectedCallback() { super.connectedCallback(); if (window.require) { const { ipcRenderer } = window.require("electron"); this.handlePreviousResponse = () => this.navigateToPreviousResponse(); this.handleNextResponse = () => this.navigateToNextResponse(); this.handleScrollUp = () => this.scrollResponseUp(); this.handleScrollDown = () => this.scrollResponseDown(); this.handleExpandHotkey = () => this.handleExpandResponse(); 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); ipcRenderer.on("expand-response", this.handleExpandHotkey); } } disconnectedCallback() { super.disconnectedCallback(); this._stopWaveformAnimation(); 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, ); if (this.handleExpandHotkey) ipcRenderer.removeListener("expand-response", this.handleExpandHotkey); } } async handleSendText() { const textInput = this.shadowRoot.querySelector("#textInput"); if (textInput && textInput.value.trim()) { const message = textInput.value.trim(); textInput.value = ""; await this.onSendText(message); } } handleTextKeydown(e) { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); this.handleSendText(); } } async handleScreenAnswer() { if (this.isAnalyzing) return; if (window.captureManualScreenshot) { this.isAnalyzing = true; this._responseCountWhenStarted = this.responses.length; window.captureManualScreenshot(); } } async handleExpandResponse() { if ( this.isExpanding || this.responses.length === 0 || this.currentResponseIndex < 0 ) return; this.isExpanding = true; this._responseCountWhenStarted = this.responses.length; await this.onExpandResponse(); } _startWaveformAnimation() { const canvas = this.shadowRoot.querySelector(".analyze-canvas"); if (!canvas) return; const ctx = canvas.getContext("2d"); const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; ctx.scale(dpr, dpr); const dangerColor = getComputedStyle(this).getPropertyValue("--danger").trim() || "#EF4444"; const startTime = performance.now(); const FADE_IN = 0.5; // seconds const PARTICLE_SPREAD = 4; // px inward from border const PARTICLE_COUNT = 250; // Pill perimeter helpers const w = rect.width; const h = rect.height; const r = h / 2; // pill radius = half height const straightLen = w - 2 * r; const arcLen = Math.PI * r; const perimeter = 2 * straightLen + 2 * arcLen; // Given a distance along the perimeter, return {x, y, nx, ny} (position + inward normal) const pointOnPerimeter = (d) => { d = ((d % perimeter) + perimeter) % perimeter; // Top straight: left to right if (d < straightLen) { return { x: r + d, y: 0, nx: 0, ny: 1 }; } d -= straightLen; // Right arc if (d < arcLen) { const angle = -Math.PI / 2 + (d / arcLen) * Math.PI; return { x: w - r + Math.cos(angle) * r, y: r + Math.sin(angle) * r, nx: -Math.cos(angle), ny: -Math.sin(angle), }; } d -= arcLen; // Bottom straight: right to left if (d < straightLen) { return { x: w - r - d, y: h, nx: 0, ny: -1 }; } d -= straightLen; // Left arc const angle = Math.PI / 2 + (d / arcLen) * Math.PI; return { x: r + Math.cos(angle) * r, y: r + Math.sin(angle) * r, nx: -Math.cos(angle), ny: -Math.sin(angle), }; }; // Pre-seed random offsets for stable particles const seeds = []; for (let i = 0; i < PARTICLE_COUNT; i++) { seeds.push({ pos: Math.random(), drift: Math.random(), depthSeed: Math.random(), }); } const draw = (now) => { const elapsed = (now - startTime) / 1000; const fade = Math.min(1, elapsed / FADE_IN); ctx.clearRect(0, 0, w, h); // ── Particle border ── ctx.fillStyle = dangerColor; for (let i = 0; i < PARTICLE_COUNT; i++) { const s = seeds[i]; const along = (s.pos + s.drift * elapsed * 0.03) * perimeter; const depth = s.depthSeed * PARTICLE_SPREAD; const density = 1 - depth / PARTICLE_SPREAD; if (Math.random() > density) continue; const p = pointOnPerimeter(along); const px = p.x + p.nx * depth; const py = p.y + p.ny * depth; const size = 0.8 + density * 0.6; ctx.globalAlpha = fade * density * 0.85; ctx.beginPath(); ctx.arc(px, py, size, 0, Math.PI * 2); ctx.fill(); } // ── Waveform ── const midY = h / 2; const waves = [ { freq: 3, amp: 0.35, speed: 2.5, opacity: 0.9, width: 1.8 }, { freq: 5, amp: 0.2, speed: 3.5, opacity: 0.5, width: 1.2 }, { freq: 7, amp: 0.12, speed: 5, opacity: 0.3, width: 0.8 }, ]; for (const wave of waves) { ctx.beginPath(); ctx.strokeStyle = dangerColor; ctx.globalAlpha = wave.opacity * fade; ctx.lineWidth = wave.width; ctx.lineCap = "round"; ctx.lineJoin = "round"; for (let x = 0; x <= w; x++) { const norm = x / w; const envelope = Math.sin(norm * Math.PI); const y = midY + Math.sin(norm * Math.PI * 2 * wave.freq + elapsed * wave.speed) * (midY * wave.amp) * envelope; if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); } ctx.globalAlpha = 1; this._animFrame = requestAnimationFrame(draw); }; this._animFrame = requestAnimationFrame(draw); } _stopWaveformAnimation() { if (this._animFrame) { cancelAnimationFrame(this._animFrame); this._animFrame = null; } const canvas = this.shadowRoot.querySelector(".analyze-canvas"); if (canvas) { const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); } } 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(); } if (changedProperties.has("isAnalyzing")) { if (this.isAnalyzing) { this._startWaveformAnimation(); } else { this._stopWaveformAnimation(); } } if ( changedProperties.has("responses") && (this.isAnalyzing || this.isExpanding) ) { if (this.responses.length > this._responseCountWhenStarted) { this.isAnalyzing = false; this.isExpanding = false; } } } updateResponseContent() { const container = this.shadowRoot.querySelector("#responseContainer"); if (container) { const currentResponse = this.getCurrentResponse(); const renderedResponse = this.renderMarkdown(currentResponse); container.innerHTML = renderedResponse; // Apply syntax highlighting to code blocks this.applyCodeHighlighting(container); if (this.shouldAnimateResponse) { this.dispatchEvent( new CustomEvent("response-animation-complete", { bubbles: true, composed: true, }), ); } } } render() { const hasMultipleResponses = this.responses.length > 1; const hasResponse = this.responses.length > 0 && this.currentResponseIndex >= 0; return html`
${hasMultipleResponses || hasResponse ? html`
${hasMultipleResponses ? html` ${this.currentResponseIndex + 1} of ${this.responses.length} ` : ""} ${hasResponse ? html` ` : ""}
` : ""}
`; } } customElements.define("assistant-view", AssistantView);