Spaces:
Running
Running
| """ | |
| 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 = ` | |
| <span class="legend-item"><span class="legend-dot legend-suffix"></span>Current suffix (t)</span> | |
| <span class="legend-item"><span class="legend-dot legend-window"></span>k window (kkk[j:j+w])</span> | |
| <span class="legend-item"><span class="legend-dot legend-match"></span>Match (kkk[j:j+w]==t)</span> | |
| <span class="legend-item"><span class="legend-dot legend-v"></span>Read v (vvv[j+w])</span> | |
| <span class="legend-item"><span class="legend-dot legend-out"></span>Output (out)</span> | |
| `; | |
| 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; }})()" | |