"""
JavaScript 处理模块 - 前端交互逻辑
"""
# 主要的 JavaScript 函数定义
JS_FUNC = """
() => {
if (window.__rosaDemoInit) return;
window.__rosaDemoInit = true;
const rootId = "rosa-vis";
const stepsId = "steps_json";
const speedId = "speed_slider";
const baseDelay = 650;
const transferPauseMs = 200;
const transferMs = 750;
const debugAnim = true;
let runToken = 0;
let linkLayer = null;
let activeTokenEl = null;
const themeToggleId = "theme_toggle";
const themeClass = "rosa-theme-dark";
const themeStorageKey = "rosa-theme";
function getAppRoot() {
const app = document.querySelector("gradio-app");
if (app && app.shadowRoot) return app.shadowRoot;
return document;
}
function findInputById(id, selector) {
const root = getAppRoot();
const direct = root.querySelector(`#${id}`) || document.getElementById(id);
if (!direct) return null;
if (direct.matches && direct.matches(selector)) return direct;
const nested = direct.querySelector(selector);
if (nested) return nested;
if (direct.shadowRoot) {
const shadowEl = direct.shadowRoot.querySelector(selector);
if (shadowEl) return shadowEl;
}
return null;
}
function getStepsBox() {
return findInputById(stepsId, "textarea, input");
}
function applyThemeClass(isDark) {
const root = getAppRoot();
const container = root.querySelector(".gradio-container");
const host = root.host || document.documentElement;
[container, host, document.documentElement, document.body].forEach((el) => {
if (!el || !el.classList) return;
el.classList.toggle(themeClass, isDark);
});
}
function getSystemDarkPreference() {
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
}
function resolveInitialTheme() {
try {
const persisted = window.localStorage ? localStorage.getItem(themeStorageKey) : null;
if (persisted === "dark") return true;
if (persisted === "light") return false;
} catch (err) {
console.warn("[ROSA] theme localStorage unavailable", err);
}
// 没有持久化的选择,跟随系统
return getSystemDarkPreference();
}
function initThemeToggle() {
const isDark = resolveInitialTheme();
applyThemeClass(isDark);
const toggle = findInputById(themeToggleId, "input[type='checkbox']");
if (!toggle) return;
if (!toggle.dataset.rosaThemeBound) {
toggle.dataset.rosaThemeBound = "1";
toggle.addEventListener("change", () => {
const nextDark = !!toggle.checked;
applyThemeClass(nextDark);
try {
if (window.localStorage) {
localStorage.setItem(themeStorageKey, nextDark ? "dark" : "light");
}
} catch (err) {
console.warn("[ROSA] theme localStorage write failed", err);
}
});
// 监听系统主题变化,如果用户没有手动选择过则自动跟随
if (window.matchMedia) {
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
try {
const persisted = window.localStorage ? localStorage.getItem(themeStorageKey) : null;
// 只有在没有手动选择过的情况下才跟随系统
if (!persisted) {
const sysDark = e.matches;
applyThemeClass(sysDark);
toggle.checked = sysDark;
}
} catch (err) {
console.warn("[ROSA] system theme change handler failed", err);
}
});
}
}
toggle.checked = isDark;
}
function safeInitThemeToggle() {
try {
initThemeToggle();
} catch (err) {
console.warn("[ROSA] theme init failed", err);
}
}
function getSpeed() {
const slider = findInputById(speedId, 'input[type="range"], input');
const value = slider ? parseFloat(slider.value) : 2;
if (!Number.isFinite(value) || value <= 0) return 1;
return value;
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function getOverlayHost() {
const root = getAppRoot();
if (root === document) return document.body;
return root;
}
function animateTransfer(fromEl, toEl, value, durationMs, onFinish) {
if (!fromEl || !toEl) {
if (debugAnim) {
console.warn("[ROSA] animateTransfer missing element", {
hasFrom: !!fromEl,
hasTo: !!toEl,
});
}
return;
}
const fromRect = fromEl.getBoundingClientRect();
const toRect = toEl.getBoundingClientRect();
if (!fromRect || !toRect) {
if (debugAnim) {
console.warn("[ROSA] animateTransfer missing rect", {
fromRect,
toRect,
});
}
return;
}
const bubble = document.createElement("div");
bubble.className = "v-float";
bubble.textContent = value != null ? String(value) : fromEl.textContent;
bubble.style.position = "fixed";
bubble.style.zIndex = "9999";
bubble.style.borderRadius = "8px";
bubble.style.background = "#f59e0b";
bubble.style.color = "#1f2937";
bubble.style.display = "flex";
bubble.style.alignItems = "center";
bubble.style.justifyContent = "center";
bubble.style.fontWeight = "600";
bubble.style.boxShadow = "0 10px 24px rgba(15, 23, 42, 0.2)";
bubble.style.pointerEvents = "none";
bubble.style.transform = "translate(0px, 0px) scale(1)";
bubble.style.left = `${fromRect.left}px`;
bubble.style.top = `${fromRect.top}px`;
bubble.style.width = `${fromRect.width}px`;
bubble.style.height = `${fromRect.height}px`;
const host = getOverlayHost();
if (debugAnim) {
console.log("[ROSA] animateTransfer", {
from: {
left: fromRect.left,
top: fromRect.top,
width: fromRect.width,
height: fromRect.height,
},
to: {
left: toRect.left,
top: toRect.top,
width: toRect.width,
height: toRect.height,
},
host: host === document.body ? "document.body" : "shadowRoot",
value,
});
if (fromRect.width === 0 || fromRect.height === 0 || toRect.width === 0 || toRect.height === 0) {
console.warn("[ROSA] animateTransfer zero rect", {
fromRect,
toRect,
});
}
}
host.appendChild(bubble);
const dx = toRect.left - fromRect.left;
const dy = toRect.top - fromRect.top;
const toTransform = `translate(${dx}px, ${dy}px) scale(0.95)`;
const duration =
Number.isFinite(durationMs) && durationMs > 0 ? durationMs : transferMs;
const startAnimation = () => {
if (bubble.animate) {
const anim = bubble.animate(
[
{ transform: "translate(0px, 0px) scale(1)" },
{ transform: toTransform },
],
{ duration, easing: "ease-out", fill: "forwards" }
);
anim.addEventListener("finish", () => {
if (onFinish) onFinish();
bubble.remove();
});
return;
}
bubble.style.transition = `transform ${duration}ms ease`;
bubble.style.willChange = "transform";
requestAnimationFrame(() => {
bubble.style.transform = toTransform;
});
setTimeout(() => {
if (onFinish) onFinish();
bubble.remove();
}, duration + 80);
};
bubble.getBoundingClientRect();
requestAnimationFrame(() => {
requestAnimationFrame(startAnimation);
});
}
function getCodeLines() {
const root = getAppRoot();
const container = root.querySelector("#rosa-code") || document.getElementById("rosa-code");
if (!container) return {};
const lines = {};
container.querySelectorAll(".code-line").forEach((line) => {
const index = parseInt(line.dataset.line, 10);
if (Number.isFinite(index)) {
lines[index] = line;
}
});
return lines;
}
function resetCodeHighlight(codeLines) {
Object.values(codeLines).forEach((line) => {
line.classList.remove("active");
});
}
function setActiveCodeLine(state, line) {
if (!state.codeLines) return;
if (Number.isInteger(state.activeLine) && state.codeLines[state.activeLine]) {
state.codeLines[state.activeLine].classList.remove("active");
}
if (Number.isInteger(line) && state.codeLines[line]) {
state.codeLines[line].classList.add("active");
state.activeLine = line;
}
}
function buildRow(label, values, className) {
const row = document.createElement("div");
row.className = `rosa-row ${className || ""}`;
const labelEl = document.createElement("div");
labelEl.className = "row-label";
labelEl.textContent = label;
const cellsEl = document.createElement("div");
cellsEl.className = "row-cells";
const cells = values.map((val, idx) => {
const cell = document.createElement("div");
cell.className = "cell";
cell.textContent = String(val);
cell.dataset.idx = String(idx);
cellsEl.appendChild(cell);
return cell;
});
row.appendChild(labelEl);
row.appendChild(cellsEl);
return { row, cells };
}
function getCodeToken(rootEl, tokenKey) {
const root = rootEl || getAppRoot();
return root.querySelector(`[data-token="${tokenKey}"]`);
}
function ensureLinkLayer(shellEl) {
const host = shellEl || getAppRoot().querySelector("#rosa-shell") || document.body;
if (linkLayer && linkLayer.host === host) return linkLayer;
if (linkLayer && linkLayer.svg) {
linkLayer.svg.remove();
}
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", "100%");
svg.setAttribute("height", "100%");
svg.style.position = "absolute";
svg.style.left = "0";
svg.style.top = "0";
svg.style.pointerEvents = "none";
svg.style.zIndex = "10000";
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
marker.setAttribute("id", "rosa-link-arrow");
marker.setAttribute("markerWidth", "8");
marker.setAttribute("markerHeight", "8");
marker.setAttribute("refX", "6");
marker.setAttribute("refY", "3");
marker.setAttribute("orient", "auto");
const markerPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
markerPath.setAttribute("d", "M0,0 L6,3 L0,6 Z");
markerPath.setAttribute("fill", "#94a3b8");
marker.appendChild(markerPath);
defs.appendChild(marker);
svg.appendChild(defs);
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("stroke", "#94a3b8");
path.setAttribute("stroke-width", "1.5");
path.setAttribute("fill", "none");
path.setAttribute("marker-end", "url(#rosa-link-arrow)");
path.style.display = "none";
svg.appendChild(path);
host.appendChild(svg);
linkLayer = { svg, path, host };
return linkLayer;
}
function showLink(state, fromEl, tokenKey) {
if (!state || !state.shellEl) return;
const tokenEl = getCodeToken(state.shellEl, tokenKey);
if (!fromEl || !tokenEl) return;
const shellRect = state.shellEl.getBoundingClientRect();
const fromRect = fromEl.getBoundingClientRect();
const toRect = tokenEl.getBoundingClientRect();
const link = ensureLinkLayer(state.shellEl);
let startX = fromRect.right - shellRect.left;
let endX = toRect.left - shellRect.left;
if (toRect.left < fromRect.left) {
startX = fromRect.left - shellRect.left;
endX = toRect.right - shellRect.left;
}
const startY = fromRect.top + fromRect.height / 2 - shellRect.top;
const endY = toRect.top + toRect.height / 2 - shellRect.top;
link.path.setAttribute("d", `M ${startX} ${startY} L ${endX} ${endY}`);
link.path.style.display = "block";
if (activeTokenEl && activeTokenEl !== tokenEl) {
activeTokenEl.classList.remove("active");
}
tokenEl.classList.add("active");
activeTokenEl = tokenEl;
}
function hideLink() {
if (linkLayer) {
linkLayer.path.style.display = "none";
}
if (activeTokenEl) {
activeTokenEl.classList.remove("active");
activeTokenEl = null;
}
}
function getRangeRect(cells, start, end) {
const rects = [];
for (let idx = start; idx <= end; idx += 1) {
const cell = cells[idx];
if (!cell) continue;
rects.push(cell.getBoundingClientRect());
}
if (!rects.length) return null;
let left = rects[0].left;
let right = rects[0].right;
let top = rects[0].top;
let bottom = rects[0].bottom;
rects.forEach((rect) => {
left = Math.min(left, rect.left);
right = Math.max(right, rect.right);
top = Math.min(top, rect.top);
bottom = Math.max(bottom, rect.bottom);
});
return {
left,
top,
right,
bottom,
width: right - left,
height: bottom - top,
};
}
function toLocalRect(rect, containerRect) {
return {
left: rect.left - containerRect.left,
top: rect.top - containerRect.top,
width: rect.width,
height: rect.height,
right: rect.right - containerRect.left,
bottom: rect.bottom - containerRect.top,
};
}
function padRect(rect, pad, bounds) {
const left = Math.max(0, rect.left - pad);
const top = Math.max(0, rect.top - pad);
const right = Math.min(bounds.width, rect.left + rect.width + pad);
const bottom = Math.min(bounds.height, rect.top + rect.height + pad);
return {
left,
top,
width: Math.max(0, right - left),
height: Math.max(0, bottom - top),
right,
bottom,
};
}
function createOverlayBox(layer, rect, label, className, labelClass) {
const box = document.createElement("div");
box.className = `overlay-box ${className || ""}`;
box.style.left = `${rect.left}px`;
box.style.top = `${rect.top}px`;
box.style.width = `${rect.width}px`;
box.style.height = `${rect.height}px`;
const labelEl = document.createElement("div");
labelEl.className = `overlay-label ${labelClass || ""}`;
labelEl.textContent = label;
box.appendChild(labelEl);
box.dataset.label = label;
layer.appendChild(box);
}
function clearHoverBoxes(state) {
if (state.overlayHover) {
state.overlayHover.innerHTML = "";
}
state.hoverBoxes = [];
hideLink();
}
function clearOverlay(state) {
if (state.overlayBoxes) {
state.overlayBoxes.innerHTML = "";
}
if (state.rayLine) {
state.rayLine.style.display = "none";
}
clearHoverBoxes(state);
}
function updateOverlay(state, step) {
const hasI = Number.isInteger(step.i);
const hasW = Number.isInteger(step.w);
const hasJ = Number.isInteger(step.j);
if (step.phase === "loop_i") {
state.lastOverlay = null;
}
const showOverlay = [
"loop_w",
"assign_t",
"loop_j",
"try",
"assign",
"break_inner",
"check",
"break_outer",
].includes(step.phase);
let overlay = null;
if (hasI && hasW) {
const sameWindow =
state.lastOverlay &&
state.lastOverlay.i === step.i &&
state.lastOverlay.w === step.w;
const jValue = hasJ ? step.j : (sameWindow ? state.lastOverlay.j : null);
state.lastOverlay = { i: step.i, w: step.w, j: jValue };
overlay = state.lastOverlay;
} else if (state.lastOverlay) {
overlay = state.lastOverlay;
}
clearOverlay(state);
if (!showOverlay) {
return;
}
if (!overlay) return;
const tStart = overlay.i - overlay.w + 1;
const tEnd = overlay.i;
const kStart = overlay.j;
const kEnd = Number.isInteger(overlay.j) ? overlay.j + overlay.w - 1 : null;
const tRect = getRangeRect(state.qCells, tStart, tEnd);
const kRect =
Number.isInteger(kStart) && Number.isInteger(kEnd)
? getRangeRect(state.kCells, kStart, kEnd)
: null;
const vIndex = Number.isInteger(kStart) ? kStart + overlay.w : null;
const vCell = Number.isInteger(vIndex) ? state.vCells[vIndex] : null;
const vRect = vCell ? vCell.getBoundingClientRect() : null;
const cardRect = state.cardEl.getBoundingClientRect();
const cardStyle = window.getComputedStyle(state.cardEl);
const borderLeft = parseFloat(cardStyle.borderLeftWidth) || 0;
const borderTop = parseFloat(cardStyle.borderTopWidth) || 0;
const borderRight = parseFloat(cardStyle.borderRightWidth) || 0;
const borderBottom = parseFloat(cardStyle.borderBottomHeight) || 0;
const originLeft = cardRect.left + borderLeft;
const originTop = cardRect.top + borderTop;
const boxPad = 4;
const bounds = {
width: cardRect.width - borderLeft - borderRight,
height: cardRect.height - borderTop - borderBottom,
};
const toOverlayRect = (rect) => ({
left: rect.left - originLeft,
top: rect.top - originTop,
width: rect.width,
height: rect.height,
right: rect.right - originLeft,
bottom: rect.bottom - originTop,
});
const isTry = step.phase === "try";
const tryClass = isTry ? (step.matched ? "try-match" : "try-miss") : "";
if (tRect) {
const local = padRect(toOverlayRect(tRect), boxPad, bounds);
createOverlayBox(state.overlayBoxes, local, "t", `t-box ${tryClass}`, "t-label");
const hoverBox = document.createElement("div");
hoverBox.className = "overlay-hover-box";
hoverBox.style.left = `${local.left}px`;
hoverBox.style.top = `${local.top}px`;
hoverBox.style.width = `${local.width}px`;
hoverBox.style.height = `${local.height}px`;
hoverBox.addEventListener("mouseenter", () => showLink(state, hoverBox, "t"));
hoverBox.addEventListener("mouseleave", hideLink);
state.overlayHover.appendChild(hoverBox);
state.hoverBoxes.push(hoverBox);
}
if (kRect) {
const local = padRect(toOverlayRect(kRect), boxPad, bounds);
createOverlayBox(state.overlayBoxes, local, "kkk[j:j+w]", `k-box ${tryClass}`, "k-label");
const hoverBox = document.createElement("div");
hoverBox.className = "overlay-hover-box";
hoverBox.style.left = `${local.left}px`;
hoverBox.style.top = `${local.top}px`;
hoverBox.style.width = `${local.width}px`;
hoverBox.style.height = `${local.height}px`;
hoverBox.addEventListener("mouseenter", () => showLink(state, hoverBox, "k"));
hoverBox.addEventListener("mouseleave", hideLink);
state.overlayHover.appendChild(hoverBox);
state.hoverBoxes.push(hoverBox);
}
if (step.phase === "assign" && kRect && vRect && state.rayLine) {
const kLocal = padRect(toOverlayRect(kRect), boxPad, bounds);
const vLocal = toOverlayRect(vRect);
const startX = kLocal.right;
const startY = kLocal.bottom;
const endX = vLocal.left + vLocal.width / 2;
const endY = vLocal.top + vLocal.height / 2;
state.rayLine.setAttribute("x1", startX);
state.rayLine.setAttribute("y1", startY);
state.rayLine.setAttribute("x2", endX);
state.rayLine.setAttribute("y2", endY);
state.rayLine.style.display = "block";
}
}
function render(data) {
const root = getAppRoot().querySelector(`#${rootId}`);
if (!root) return null;
root.innerHTML = "";
const card = document.createElement("div");
card.className = "rosa-card";
const legend = document.createElement("div");
legend.className = "rosa-legend";
legend.innerHTML = `
Current suffix (t)
k window (kkk[j:j+w])
Match (kkk[j:j+w]==t)
Read v (vvv[j+w])
Output (out)
`;
card.appendChild(legend);
const rowsWrap = document.createElement("div");
rowsWrap.className = "rosa-rows";
const indexRow = document.createElement("div");
indexRow.className = "rosa-row index-row";
const indexLabel = document.createElement("div");
indexLabel.className = "row-label";
indexLabel.textContent = "#";
const indexCells = document.createElement("div");
indexCells.className = "row-cells index-cells";
data.q.forEach((_, idx) => {
const cell = document.createElement("div");
cell.className = "index-cell";
cell.textContent = String(idx);
indexCells.appendChild(cell);
});
indexRow.appendChild(indexLabel);
indexRow.appendChild(indexCells);
rowsWrap.appendChild(indexRow);
const qRow = buildRow("q", data.q, "q-row");
const kRow = buildRow("k", data.k, "k-row");
const vRow = buildRow("v", data.v, "v-row");
const outRow = buildRow("out", data.q.map(() => "."), "out-row");
rowsWrap.appendChild(qRow.row);
rowsWrap.appendChild(kRow.row);
rowsWrap.appendChild(vRow.row);
rowsWrap.appendChild(outRow.row);
card.appendChild(rowsWrap);
const overlay = document.createElement("div");
overlay.className = "rosa-overlay";
const overlayRay = document.createElementNS("http://www.w3.org/2000/svg", "svg");
overlayRay.classList.add("overlay-ray");
const rayDefs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
const rayMarker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
rayMarker.setAttribute("id", "rosa-ray-head");
rayMarker.setAttribute("markerWidth", "8");
rayMarker.setAttribute("markerHeight", "8");
rayMarker.setAttribute("refX", "6");
rayMarker.setAttribute("refY", "3");
rayMarker.setAttribute("orient", "auto");
const rayMarkerPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
rayMarkerPath.setAttribute("d", "M0,0 L6,3 L0,6 Z");
rayMarkerPath.setAttribute("fill", "var(--rosa-cyan)");
rayMarker.appendChild(rayMarkerPath);
rayDefs.appendChild(rayMarker);
overlayRay.appendChild(rayDefs);
const rayLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
rayLine.classList.add("overlay-ray-line");
rayLine.setAttribute("marker-end", "url(#rosa-ray-head)");
rayLine.style.display = "none";
overlayRay.appendChild(rayLine);
const overlayBoxes = document.createElement("div");
overlayBoxes.className = "overlay-box-layer";
overlay.appendChild(overlayBoxes);
overlay.appendChild(overlayRay);
card.appendChild(overlay);
const overlayHover = document.createElement("div");
overlayHover.className = "overlay-hover-layer";
card.appendChild(overlayHover);
root.appendChild(card);
const codeLines = getCodeLines();
resetCodeHighlight(codeLines);
return {
shellEl: root.closest("#rosa-shell") || getAppRoot().querySelector("#rosa-shell"),
cardEl: card,
qCells: qRow.cells,
kCells: kRow.cells,
vCells: vRow.cells,
outCells: outRow.cells,
outFixed: new Set(),
outPending: new Set(),
overlayBoxes,
rayLine,
overlayHover,
hoverBoxes: [],
lastOverlay: null,
codeLines,
activeLine: null,
};
}
function clearHighlights(state) {
const holdActive =
state.vHold &&
Number.isInteger(state.vHold.index) &&
performance.now() < state.vHold.until;
const holdIndex = holdActive ? state.vHold.index : null;
const all = [...state.qCells, ...state.kCells, ...state.vCells, ...state.outCells];
all.forEach((cell) => {
cell.classList.remove("active", "suffix", "k-window", "v-pick", "out");
});
if (holdActive && state.vCells[holdIndex]) {
state.vCells[holdIndex].classList.add("v-pick");
}
}
function applyStep(state, step) {
clearHighlights(state);
const { qCells, kCells, vCells, outCells } = state;
if (Number.isInteger(step.line)) {
setActiveCodeLine(state, step.line);
}
const showWindow = [
"loop_w",
"assign_t",
"loop_j",
"try",
"assign",
"break_inner",
"check",
"break_outer",
].includes(step.phase);
if (showWindow && Number.isInteger(step.i) && qCells[step.i]) {
qCells[step.i].classList.add("active");
}
if (showWindow && Number.isInteger(step.w)) {
for (let idx = step.i - step.w + 1; idx <= step.i; idx += 1) {
if (qCells[idx]) qCells[idx].classList.add("suffix");
}
}
if (showWindow && Number.isInteger(step.j)) {
for (let idx = step.j; idx <= step.j + step.w - 1; idx += 1) {
if (kCells[idx]) kCells[idx].classList.add("k-window");
}
}
updateOverlay(state, step);
if (step.phase === "try") {
return;
}
if (step.phase === "assign") {
if (debugAnim) {
console.log("[ROSA] assign step", {
i: step.i,
j: step.j,
w: step.w,
v_index: step.v_index,
value: step.value,
});
}
if (Number.isInteger(step.v_index) && vCells[step.v_index]) {
vCells[step.v_index].classList.add("v-pick");
}
if (
Number.isInteger(step.v_index) &&
Number.isInteger(step.i) &&
vCells[step.v_index] &&
outCells[step.i]
) {
const speed = getSpeed();
const holdToken = state.runToken;
if (state.outPending) {
state.outPending.add(step.i);
}
const pauseMs = transferPauseMs / speed;
const durationMs = transferMs / speed;
const totalWait = pauseMs + durationMs;
if (state.outPending) {
state.outPending.add(step.i);
}
const startTransfer = () => {
if (state.runToken !== holdToken) return;
animateTransfer(vCells[step.v_index], outCells[step.i], step.value, durationMs, () => {
if (state.runToken !== holdToken) return;
if (state.outPending) {
state.outPending.delete(step.i);
}
if (state.outFixed) {
state.outFixed.add(step.i);
}
const outCell = outCells[step.i];
if (outCell) {
outCell.textContent = String(step.value);
outCell.classList.add("out-fixed", "filled");
}
});
};
if (pauseMs > 0) {
setTimeout(startTransfer, pauseMs);
} else {
startTransfer();
}
state.vHold = {
index: step.v_index,
until: performance.now() + totalWait,
};
const holdIndex = step.v_index;
setTimeout(() => {
if (state.runToken !== holdToken) return;
if (!state.vHold || state.vHold.index !== holdIndex) return;
if (performance.now() < state.vHold.until) return;
const cell = state.vCells[holdIndex];
if (cell) cell.classList.remove("v-pick");
}, totalWait + 60);
return totalWait;
}
return 0;
}
if (step.phase === "break_inner") {
return;
}
if (step.phase === "check") {
return;
}
if (step.phase === "break_outer") {
return;
}
if (step.phase === "output") {
if (
(state.outPending && state.outPending.has(step.i)) ||
(state.outFixed && state.outFixed.has(step.i))
) {
return;
}
if (outCells[step.i]) {
outCells[step.i].textContent = String(step.value);
outCells[step.i].classList.add("out", "out-fixed", "filled");
if (state.outFixed) {
state.outFixed.add(step.i);
}
}
return;
}
if (step.phase === "return") {
return;
}
}
async function play(state, steps, token) {
if (!steps || !steps.length) return;
for (let idx = 0; idx < steps.length; idx += 1) {
if (token !== runToken) return;
const extraWait = applyStep(state, steps[idx]);
const delay = baseDelay / getSpeed();
const waitMs = Math.max(delay, Number.isFinite(extraWait) ? extraWait : 0);
await sleep(waitMs);
}
}
function start(data) {
runToken += 1;
const token = runToken;
const state = render(data);
if (!state) return;
state.runToken = token;
state.vHold = null;
if (state.outFixed) state.outFixed.clear();
if (state.outPending) state.outPending.clear();
play(state, data.steps, token);
}
let lastValue = "";
safeInitThemeToggle();
setInterval(safeInitThemeToggle, 1200);
setInterval(() => {
const box = getStepsBox();
if (!box) return;
const value = box.value || "";
if (value && value !== lastValue) {
lastValue = value;
try {
const data = JSON.parse(value);
start(data);
} catch (err) {
console.error("Invalid ROSA steps payload", err);
}
}
}, 300);
}
"""
def get_js_boot(js_func: str) -> str:
"""获取 JavaScript 启动代码"""
return f"(function(){{ const init = {js_func}; init(); return init; }})()"