1190 lines
34 KiB
JavaScript
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);
|