""" 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; }})()"