multimodalart's picture
Squashing commit
4450790 verified
/**
* Progress bar web component.
*/
import { SERVICE as PROMPT_SERVICE, type PromptExecution } from "rgthree/common/prompt_service.js";
import { createElement } from "./utils_dom.js";
/**
* The progress bar web component.
*/
export class RgthreeProgressBar extends HTMLElement {
static NAME = "rgthree-progress-bar";
static create(): RgthreeProgressBar {
return document.createElement(RgthreeProgressBar.NAME) as RgthreeProgressBar;
}
private shadow: ShadowRoot | null = null;
private progressNodesEl!: HTMLDivElement;
private progressStepsEl!: HTMLDivElement;
private progressTextEl!: HTMLSpanElement;
private currentPromptExecution: PromptExecution | null = null;
private readonly onProgressUpdateBound = this.onProgressUpdate.bind(this);
private connected: boolean = false;
/** The currentNodeId so outside callers can see what we're currently executing against. */
get currentNodeId() {
const prompt = this.currentPromptExecution;
const nodeId = prompt?.errorDetails?.node_id || prompt?.currentlyExecuting?.nodeId;
return nodeId || null;
}
constructor() {
super();
}
private onProgressUpdate(e: CustomEvent<{ queue: number; prompt: PromptExecution }>) {
if (!this.connected) return;
const prompt = e.detail.prompt;
this.currentPromptExecution = prompt;
if (prompt?.errorDetails) {
let progressText = `${prompt.errorDetails?.exception_type} ${
prompt.errorDetails?.node_id || ""
} ${prompt.errorDetails?.node_type || ""}`;
this.progressTextEl.innerText = progressText;
this.progressNodesEl.classList.add("-error");
this.progressStepsEl.classList.add("-error");
return;
}
if (prompt?.currentlyExecuting) {
this.progressNodesEl.classList.remove("-error");
this.progressStepsEl.classList.remove("-error");
const current = prompt?.currentlyExecuting;
let progressText = `(${e.detail.queue}) `;
// Sometimes we may get status updates for a workflow that was already running. In that case
// we don't know totalNodes.
if (!prompt.totalNodes) {
progressText += `??%`;
this.progressNodesEl.style.width = `0%`;
} else {
const percent = (prompt.executedNodeIds.length / prompt.totalNodes) * 100;
this.progressNodesEl.style.width = `${Math.max(2, percent)}%`;
// progressText += `Node ${prompt.executedNodeIds.length + 1} of ${prompt.totalNodes || "?"}`;
progressText += `${Math.round(percent)}%`;
}
let nodeLabel = current.nodeLabel?.trim();
let stepsLabel = "";
if (current.step != null && current.maxSteps) {
const percent = (current.step / current.maxSteps) * 100;
this.progressStepsEl.style.width = `${percent}%`;
// stepsLabel += `Step ${current.step} of ${current.maxSteps}`;
if (current.pass > 1 || current.maxPasses != null) {
stepsLabel += `#${current.pass}`;
if (current.maxPasses && current.maxPasses > 0) {
stepsLabel += `/${current.maxPasses}`;
}
stepsLabel += ` - `;
}
stepsLabel += `${Math.round(percent)}%`;
}
if (nodeLabel || stepsLabel) {
progressText += ` - ${nodeLabel || "???"}${stepsLabel ? ` (${stepsLabel})` : ""}`;
}
if (!stepsLabel) {
this.progressStepsEl.style.width = `0%`;
}
this.progressTextEl.innerText = progressText;
} else {
if (e?.detail.queue) {
this.progressTextEl.innerText = `(${e.detail.queue}) Running... in another tab`;
} else {
this.progressTextEl.innerText = "Idle";
}
this.progressNodesEl.style.width = `0%`;
this.progressStepsEl.style.width = `0%`;
}
}
connectedCallback() {
if (!this.connected) {
PROMPT_SERVICE.addEventListener(
"progress-update",
this.onProgressUpdateBound as EventListener,
);
this.connected = true;
}
// We were already connected, so we just need to reset.
if (this.shadow) {
this.progressTextEl.innerText = "Idle";
this.progressNodesEl.style.width = `0%`;
this.progressStepsEl.style.width = `0%`;
return;
}
this.shadow = this.attachShadow({ mode: "open" });
const sheet = new CSSStyleSheet();
sheet.replaceSync(`
:host {
position: relative;
overflow: hidden;
box-sizing: border-box;
background: var(--rgthree-progress-bg-color);
--rgthree-progress-bg-color: rgba(23, 23, 23, 0.9);
--rgthree-progress-nodes-bg-color: rgb(0, 128, 0);
--rgthree-progress-steps-bg-color: rgb(0, 128, 0);
--rgthree-progress-error-bg-color: rgb(128, 0, 0);
--rgthree-progress-text-color: #fff;
}
:host * {
box-sizing: inherit;
}
:host > div.bar {
background: var(--rgthree-progress-nodes-bg-color);
position: absolute;
left: 0;
top: 0;
width: 0%;
height: 50%;
z-index: 1;
transition: width 50ms ease-in-out;
}
:host > div.bar + div.bar {
background: var(--rgthree-progress-steps-bg-color);
top: 50%;
height: 50%;
z-index: 2;
}
:host > div.bar.-error {
background: var(--rgthree-progress-error-bg-color);
}
:host > .overlay {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 5;
background: linear-gradient(to bottom, rgba(255,255,255,0.25), rgba(0,0,0,0.25));
mix-blend-mode: overlay;
}
:host > span {
position: relative;
z-index: 4;
text-align: left;
font-size: inherit;
height: 100%;
font-family: sans-serif;
text-shadow: 1px 1px 0px #000;
display: flex;
flex-direction: row;
padding: 0 6px;
align-items: center;
justify-content: start;
color: var(--rgthree-progress-text-color);
text-shadow: black 0px 0px 2px;
}
:host > div.bar[style*="width: 0%"]:first-child,
:host > div.bar[style*="width:0%"]:first-child {
height: 0%;
}
:host > div.bar[style*="width: 0%"]:first-child + div,
:host > div.bar[style*="width:0%"]:first-child + div {
bottom: 0%;
}
`);
this.shadow.adoptedStyleSheets = [sheet];
const overlayEl = createElement(`div.overlay[part="overlay"]`, { parent: this.shadow });
this.progressNodesEl = createElement(`div.bar[part="progress-nodes"]`, { parent: this.shadow });
this.progressStepsEl = createElement(`div.bar[part="progress-steps"]`, { parent: this.shadow });
this.progressTextEl = createElement(`span[part="text"]`, { text: "Idle", parent: this.shadow });
}
disconnectedCallback() {
this.connected = false;
PROMPT_SERVICE.removeEventListener(
"progress-update",
this.onProgressUpdateBound as EventListener,
);
}
}
customElements.define(RgthreeProgressBar.NAME, RgthreeProgressBar);