forked from retoor/devplacepy
147 lines
5.4 KiB
JavaScript
147 lines
5.4 KiB
JavaScript
// retoor <retoor@molodetz.nl>
|
|
|
|
import { Component } from "./Component.js";
|
|
import { Http } from "../Http.js";
|
|
import { Poller } from "../Poller.js";
|
|
|
|
const POLL_INTERVAL_MS = 3000;
|
|
const FEED_CAP = 600;
|
|
const RELOAD_DELAY_MS = 1500;
|
|
const TERMINAL_KINDS = ["done", "error"];
|
|
|
|
const KIND_ICONS = {
|
|
stage: "🧭",
|
|
log: "📋",
|
|
file: "📄",
|
|
signal: "🚩",
|
|
ai: "🤖",
|
|
image: "🖼️",
|
|
progress: "⏳",
|
|
score: "🏁",
|
|
report: "📝",
|
|
error: "❌",
|
|
done: "✅",
|
|
status: "📡",
|
|
};
|
|
const KIND_ICON_FALLBACK = "🔹";
|
|
|
|
export class AppIsslopRun extends Component {
|
|
connectedCallback() {
|
|
this._uid = this.attr("uid");
|
|
this._topic = this.attr("topic");
|
|
this._eventsUrl = this.attr("events-url", `/tools/isslop/${this._uid}/events`);
|
|
this._lastSeq = 0;
|
|
this._finished = false;
|
|
this._render();
|
|
this._status = this.querySelector("[data-isslop-status]");
|
|
this._bar = this.querySelector("[data-isslop-bar]");
|
|
this._feed = this.querySelector("[data-isslop-feed]");
|
|
this._subscribed = false;
|
|
this._subscribe();
|
|
this._poller = new Poller(() => this._poll(), POLL_INTERVAL_MS);
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (this._poller) this._poller.stop();
|
|
if (this._subscribed && window.app && window.app.pubsub) {
|
|
window.app.pubsub.unsubscribe(this._topic, this._onFrame);
|
|
}
|
|
}
|
|
|
|
_render() {
|
|
this.innerHTML = `
|
|
<div class="isslop-live-head">
|
|
<span class="isslop-live-status" data-isslop-status role="status" aria-live="polite">Waiting for the analyzer…</span>
|
|
</div>
|
|
<div class="isslop-progress"><div class="isslop-progress-bar" data-isslop-bar></div></div>
|
|
<ul class="isslop-feed" data-isslop-feed role="log" aria-live="polite"></ul>
|
|
`;
|
|
this._loader = document.createElement("li");
|
|
this._loader.className = "isslop-feed-item isslop-feed-loader";
|
|
this._loader.setAttribute("aria-hidden", "true");
|
|
this._loader.innerHTML = '<span class="isslop-loader-cursor"></span><span class="isslop-loader-text">working</span>';
|
|
this.querySelector("[data-isslop-feed]").appendChild(this._loader);
|
|
}
|
|
|
|
_subscribe() {
|
|
const app = window.app;
|
|
if (!this._topic || !app || !app.pubsub || typeof app.pubsub.subscribe !== "function") return;
|
|
this._onFrame = (frame) => this._apply(frame);
|
|
app.pubsub.subscribe(this._topic, this._onFrame);
|
|
this._subscribed = true;
|
|
}
|
|
|
|
async _poll() {
|
|
if (this._finished) return;
|
|
let payload;
|
|
try {
|
|
payload = await Http.getJson(`${this._eventsUrl}?after=${this._lastSeq}`);
|
|
} catch (error) {
|
|
return;
|
|
}
|
|
(payload.events || []).forEach((event) => this._apply(event));
|
|
if (!this._finished && payload.status === "failed" && this._lastSeq === 0) {
|
|
this._finish(false, "Analysis failed before producing events.");
|
|
}
|
|
}
|
|
|
|
_apply(event) {
|
|
if (!event || typeof event.seq !== "number") return;
|
|
if (event.seq > 0 && event.seq <= this._lastSeq) return;
|
|
if (event.seq > 0) this._lastSeq = event.seq;
|
|
if (event.kind === "stage") this._status.textContent = event.message;
|
|
if (event.kind === "progress" && event.data && event.data.total) {
|
|
const percent = Math.min(100, Math.round((event.data.current / event.data.total) * 100));
|
|
this._bar.style.width = `${percent}%`;
|
|
}
|
|
this._append(event);
|
|
if (TERMINAL_KINDS.includes(event.kind)) {
|
|
this._finish(event.kind === "done", event.message);
|
|
}
|
|
}
|
|
|
|
_append(event) {
|
|
const item = document.createElement("li");
|
|
item.className = `isslop-feed-item isslop-feed-${event.kind}`;
|
|
const icon = document.createElement("span");
|
|
icon.className = "isslop-feed-icon";
|
|
icon.textContent = KIND_ICONS[event.kind] || KIND_ICON_FALLBACK;
|
|
const text = document.createElement("span");
|
|
text.className = "isslop-feed-text";
|
|
text.textContent = event.message;
|
|
item.append(icon, text);
|
|
const thumb = event.data ? event.data.thumb : null;
|
|
if (thumb && /^[a-f0-9]{16}\.webp$/.test(thumb)) {
|
|
const preview = document.createElement("img");
|
|
preview.className = "isslop-feed-thumb";
|
|
preview.src = `/tools/isslop/${this._uid}/media/${thumb}`;
|
|
preview.alt = "";
|
|
preview.loading = "lazy";
|
|
item.appendChild(preview);
|
|
}
|
|
this._feed.appendChild(item);
|
|
this._feed.appendChild(this._loader);
|
|
while (this._feed.children.length > FEED_CAP) {
|
|
this._feed.removeChild(this._feed.firstChild);
|
|
}
|
|
this._feed.scrollTop = this._feed.scrollHeight;
|
|
}
|
|
|
|
_finish(success, message) {
|
|
if (this._finished) return;
|
|
this._finished = true;
|
|
if (this._loader) this._loader.remove();
|
|
if (this._poller) this._poller.stop();
|
|
this._bar.style.width = "100%";
|
|
this._bar.classList.toggle("isslop-progress-failed", !success);
|
|
this._status.textContent = success
|
|
? "Analysis complete, loading the report…"
|
|
: message || "Analysis failed.";
|
|
if (success) {
|
|
window.setTimeout(() => window.location.reload(), RELOAD_DELAY_MS);
|
|
}
|
|
}
|
|
}
|
|
|
|
customElements.define("dp-isslop-run", AppIsslopRun);
|