Files
byre/web/src/preview.ts
T

127 lines
3.3 KiB
TypeScript

import { get, type ByreEvent, type Schemas } from "./api.js";
import { errorText, h, replace } from "./dom.js";
import { onEach, state } from "./state.js";
type Preview = Schemas["Preview"];
const chosen = new Map<string, string>();
export function previewURL(port: number): string {
return `https://${location.hostname}:${port}/`;
}
export function mountPreview(root: HTMLElement): () => void {
const head = h("header", { class: "preview-header" });
const body = h("div", { class: "preview-body" });
let previews: Preview[] | undefined;
let error = "";
let target = 0;
let framed = "";
let disposed = false;
const current = (): Preview | undefined => {
const taskId = state.selectedTaskId;
const name = taskId ? chosen.get(taskId) : undefined;
return previews?.find((p) => p.name === name) ?? previews?.[0];
};
const load = async () => {
const taskId = state.selectedTaskId;
if (!taskId) {
render();
return;
}
const started = ++target;
try {
const fresh = await get("/api/tasks/{taskId}/previews", { path: { taskId } });
if (started === target) {
previews = fresh;
error = "";
}
} catch (err) {
if (started === target) {
previews = undefined;
error = errorText(err);
}
}
if (!disposed && started === target) {
render();
}
};
const render = () => {
const taskId = state.selectedTaskId;
const shown = current();
replace(
head,
h(
"nav",
{ class: "preview-names" },
...(previews ?? []).map((p) => {
const b = h("button", { type: "button", class: p === shown ? "preview-name active" : "preview-name" }, p.name, h("span", { class: "preview-port" }, ` :${p.port}`));
b.onclick = () => {
if (taskId) {
chosen.set(taskId, p.name);
}
render();
};
return b;
}),
),
shown ? h("a", { class: "preview-open", href: previewURL(shown.port), target: "_blank", rel: "noopener" }, "open ↗") : null,
);
if (!taskId) {
framed = "";
replace(body, h("div", { class: "empty" }, "No task selected."));
return;
}
if (error) {
framed = "";
replace(body, h("div", { class: "empty error-text" }, error));
return;
}
if (!previews) {
framed = "";
replace(body, h("div", { class: "empty" }, "Loading…"));
return;
}
if (!shown) {
framed = "";
replace(body, h("div", { class: "empty" }, "Nothing exposed. The task's agent shows a running server here with expose_port."));
return;
}
const url = previewURL(shown.port);
if (url === framed) {
return;
}
framed = url;
replace(body, h("iframe", { class: "preview-frame", src: url, title: shown.name }));
};
const offs = [
onEach(["selection"], () => {
target++;
previews = undefined;
error = "";
render();
void load();
}),
onEach(["transcript"], (fresh) => {
if ((fresh as ByreEvent[]).some((ev) => ev.kind === "preview.exposed")) {
framed = "";
void load();
}
}),
];
replace(root, h("div", { class: "preview-view" }, head, body));
render();
void load();
return () => {
disposed = true;
for (const off of offs) {
off();
}
};
}