Mastermind/src/components/views/AssistantView.js
2026-02-16 22:29:30 +03:00

1190 lines
34 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: 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`
<div class="response-container" id="responseContainer"></div>
${hasMultipleResponses || hasResponse
? html`
<div class="response-nav">
${hasMultipleResponses
? html`
<button
class="nav-btn"
@click=${this.navigateToPreviousResponse}
?disabled=${this.currentResponseIndex <= 0}
title="Previous response"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z"
clip-rule="evenodd"
/>
</svg>
</button>
<span class="response-counter"
>${this.currentResponseIndex + 1} of
${this.responses.length}</span
>
<button
class="nav-btn"
@click=${this.navigateToNextResponse}
?disabled=${this.currentResponseIndex >=
this.responses.length - 1}
title="Next response"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
</svg>
</button>
`
: ""}
${hasResponse
? html`
<button
class="expand-btn"
@click=${this.handleExpandResponse}
?disabled=${this.isExpanding}
title="Expand this response with more detail"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.168l3.71-3.938a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z"
clip-rule="evenodd"
/>
</svg>
${this.isExpanding ? "Expanding..." : "Expand"}
</button>
`
: ""}
</div>
`
: ""}
<div class="input-bar">
<div class="input-bar-inner">
<input
type="text"
id="textInput"
placeholder="Type a message..."
@keydown=${this.handleTextKeydown}
/>
</div>
<button
class="analyze-btn ${this.isAnalyzing ? "analyzing" : ""}"
@click=${this.handleScreenAnswer}
>
<canvas class="analyze-canvas"></canvas>
<span class="analyze-btn-content">
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 3v7h6l-8 11v-7H5z"
/>
</svg>
Analyze Screen
</span>
</button>
</div>
`;
}
}
customElements.define("assistant-view", AssistantView);