): void {
super.willUpdate(changedProperties);
if (changedProperties.has("messages") && this.messages !== undefined) {
this._cellController.bind(this.messages, MessagesSchema);
}
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this.#unmountTooltip();
}
#mountTooltip(): void {
if (this.#tooltip) return;
const el = document.createElement("div");
el.style.position = "fixed";
el.style.zIndex = "1001";
el.style.pointerEvents = "none";
el.dataset.ctMessageBeadsTooltip = "";
document.body.appendChild(el);
this.#tooltip = el;
}
#unmountTooltip(): void {
if (this.#tooltip) {
render(nothing, this.#tooltip);
this.#tooltip.remove();
this.#tooltip = null;
}
}
#showTooltip(msg: BuiltInLLMMessage, beadEl: HTMLElement): void {
this.#mountTooltip();
const label = beadLabel(msg);
const tpl = html`
${label}
`;
render(tpl, this.#tooltip!);
const rect = beadEl.getBoundingClientRect();
const panel = this.#tooltip!.querySelector(".tp") as HTMLElement;
if (!panel) return;
// Place offscreen for measurement, then position
panel.style.top = "-9999px";
panel.style.left = "-9999px";
requestAnimationFrame(() => {
const pr = panel.getBoundingClientRect();
let top = rect.top - pr.height - 4;
let left = rect.left + rect.width / 2 - pr.width / 2;
left = Math.max(4, Math.min(left, globalThis.innerWidth - pr.width - 4));
if (top < 4) top = rect.bottom + 4;
panel.style.top = `${Math.round(top)}px`;
panel.style.left = `${Math.round(left)}px`;
});
}
private _onBeadEnter = (e: MouseEvent, index: number) => {
const msgs = this._messagesValue;
if (!msgs?.[index]) return;
this.#showTooltip(msgs[index], e.currentTarget as HTMLElement);
};
private _onBeadClick = (_e: MouseEvent, index: number) => {
const msgs = this._messagesValue;
if (!msgs?.[index]) return;
// Future: show message detail on click
};
private _onBeadLeave = () => {
this.#unmountTooltip();
};
private _onRefineClick = () => {
this.emit("ct-refine", {});
};
override render() {
const msgs = this._messagesValue;
const hasMessages = msgs && msgs.length > 0;
this.toggleAttribute("has-messages", !!hasMessages);
if (!hasMessages) {
return this.pending
? html`
`
: html`
`;
}
const beads = msgs.map((msg, i) => {
const color = classifyMessage(msg);
return html`
this._onBeadEnter(e, i)}"
@mouseleave="${this._onBeadLeave}"
@click="${(e: MouseEvent) => this._onBeadClick(e, i)}"
>
`;
});
return html`
${this.label
? html`
${this.label}
`
: nothing} ${beads} ${this.pending
? html`
`
: html`
`}
`;
}
}
globalThis.customElements.define("ct-message-beads", CTMessageBeads);
export type { CTMessageBeads as CTMessageBeadsElement };