Tests / Test passed: 868
Save the "Show OCR/UI-tree bounding boxes" checkbox state to localStorage so it persists across page refreshes in the task detail view.
252 lines
11 KiB
HTML
252 lines
11 KiB
HTML
{% extends "base.html" %}
|
|
{% block styles %}
|
|
{{ super() }}
|
|
.task-back { margin-top: 0; }
|
|
.timeline-step { border: 1px solid #c8d0d6; background: #fff; padding: 1rem; margin-bottom: 1rem; }
|
|
.step-heading { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; align-items: baseline; margin-bottom: 0.75rem; }
|
|
.step-heading p { margin: 0; color: #4d5a63; }
|
|
.evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; margin-bottom: 1rem; }
|
|
.evidence-pane { margin: 0; min-width: 0; }
|
|
.evidence-pane h3 { font-size: 1rem; margin: 0 0 0.35rem; }
|
|
.screenshot-frame { min-height: 6rem; border: 1px solid #c8d0d6; background: #f8fafb; display: grid; place-items: center; overflow: hidden; color: #5e6b73; position: relative; }
|
|
.screenshot-frame img { display: block; width: 100%; height: auto; }
|
|
.overlay-svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
|
|
.overlay-svg .overlay-box { fill: none; stroke-width: 2; vector-effect: non-scaling-stroke; }
|
|
.overlay-svg .overlay-box.source-ui { stroke: #1e88e5; }
|
|
.overlay-svg .overlay-box.source-ocr { stroke: #fb8c00; }
|
|
.overlay-svg .overlay-boxes { display: none; }
|
|
.overlay-svg.show-boxes .overlay-boxes { display: inline; }
|
|
.overlay-svg .action-tap { fill: #e53935; fill-opacity: 0.25; stroke: #e53935; stroke-width: 2; vector-effect: non-scaling-stroke; }
|
|
.overlay-svg .action-swipe-line { stroke: #e53935; stroke-width: 3; vector-effect: non-scaling-stroke; fill: none; }
|
|
.overlay-svg .action-swipe-dot { fill: #e53935; }
|
|
.overlay-toggle { margin-bottom: 1rem; }
|
|
.step-details { margin-top: 0.75rem; }
|
|
.step-details summary { cursor: pointer; font-weight: 600; }
|
|
.step-details pre { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0.65rem 0 0; padding: 0.65rem; border: 1px solid #d5dce0; background: #f8fafb; }
|
|
.operation-grid { display: grid; grid-template-columns: minmax(7rem, 0.4fr) minmax(0, 1fr); gap: 0.35rem 0.75rem; margin: 0.65rem 0 0; }
|
|
.operation-grid dt { font-weight: 600; }
|
|
.operation-grid dd { margin: 0; overflow-wrap: anywhere; }
|
|
.observation-list, .ui-tree-nodes { margin: 0.65rem 0 0; padding-left: 1.25rem; }
|
|
.observation-list li, .ui-tree-nodes li { margin-bottom: 0.45rem; overflow-wrap: anywhere; }
|
|
.observation-list span, .ui-tree-nodes span { color: #4d5a63; margin-left: 0.4rem; }
|
|
.ui-tree-nodes code { overflow-wrap: anywhere; }
|
|
@media (max-width: 640px) {
|
|
.evidence-grid { grid-template-columns: minmax(0, 1fr); }
|
|
.timeline-step { padding: 0.75rem; }
|
|
}
|
|
{% endblock %}
|
|
{% block body %}
|
|
<p class="task-back"><a href="/tasks">← Back to executions</a></p>
|
|
<h1>Execution</h1>
|
|
<table>
|
|
<thead><tr><th>Field</th><th>Value</th></tr></thead>
|
|
<tbody>{% for row in task_rows %}<tr><td>{{ row[0] }}</td><td>{{ row[1] }}</td></tr>{% endfor %}</tbody>
|
|
</table>
|
|
<h2>Timeline</h2>
|
|
{% if not timeline_steps %}
|
|
<p>No timeline records.</p>
|
|
{% else %}
|
|
<p class="overlay-toggle">
|
|
<label><input type="checkbox" id="overlay-boxes-toggle"> Show OCR/UI-tree bounding boxes on before-action screenshots</label>
|
|
</p>
|
|
{% endif %}
|
|
{% for step in timeline_steps %}
|
|
<section class="timeline-step">
|
|
<div class="step-heading">
|
|
<strong>Step {{ step.index }}</strong>
|
|
<p>{{ step.timestamp }}</p>
|
|
</div>
|
|
<div class="evidence-grid">
|
|
<figure class="evidence-pane">
|
|
<h3>Before action</h3>
|
|
<div class="screenshot-frame">
|
|
{% if step.before_screenshot_src %}
|
|
<img src="{{ step.before_screenshot_src }}" alt="Screenshot before action">
|
|
{% if step.overlay.width and step.overlay.height %}
|
|
<svg class="overlay-svg" data-step-overlay
|
|
viewBox="0 0 {{ step.overlay.width }} {{ step.overlay.height }}"
|
|
preserveAspectRatio="none"></svg>
|
|
<script type="application/json" class="step-overlay-data">{{ {"overlay": step.overlay, "tool_call": step.tool_call} | tojson }}</script>
|
|
{% endif %}
|
|
{% else %}
|
|
<span>No screenshot</span>
|
|
{% endif %}
|
|
</div>
|
|
</figure>
|
|
<figure class="evidence-pane">
|
|
<h3>After action</h3>
|
|
<div class="screenshot-frame">
|
|
{% if step.after_screenshot_src %}
|
|
<img src="{{ step.after_screenshot_src }}" alt="Screenshot after action">
|
|
{% else %}
|
|
<span>No screenshot</span>
|
|
{% endif %}
|
|
</div>
|
|
</figure>
|
|
</div>
|
|
<details class="step-details" open>
|
|
<summary>Operation</summary>
|
|
<dl class="operation-grid">
|
|
<dt>Action</dt><dd>{{ step.tool_call.get("action") or "-" }}</dd>
|
|
<dt>Description</dt><dd>{{ step.tool_call.get("description") or "-" }}</dd>
|
|
</dl>
|
|
<pre>{{ step.tool_call | tojson(indent=2) }}</pre>
|
|
</details>
|
|
<details class="step-details">
|
|
<summary>Result</summary>
|
|
<pre>{{ step.result | tojson(indent=2) }}</pre>
|
|
</details>
|
|
{% if step.prompt %}
|
|
<details class="step-details">
|
|
<summary>Planner prompt</summary>
|
|
<pre>{{ step.prompt }}</pre>
|
|
</details>
|
|
{% endif %}
|
|
{% if step.ocr_results %}
|
|
<details class="step-details">
|
|
<summary>OCR results ({{ step.ocr_results|length }})</summary>
|
|
<ul class="observation-list">
|
|
{% for ocr in step.ocr_results %}
|
|
<li>
|
|
<strong>{{ ocr.get("text") or "-" }}</strong>
|
|
<span>{{ ocr.get("bounds") | tojson }}</span>
|
|
{% if ocr.get("confidence") is not none %}<span>confidence {{ "%.3f" | format(ocr.get("confidence")) }}</span>{% endif %}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</details>
|
|
{% endif %}
|
|
{% if step.ui_tree_nodes %}
|
|
<details class="step-details">
|
|
<summary>UI tree ({{ step.ui_tree_nodes|length }} normalized nodes)</summary>
|
|
<ul class="ui-tree-nodes">
|
|
{% for node in step.ui_tree_nodes %}
|
|
<li>
|
|
<strong>{{ node.get("type") or "unknown" }}</strong>
|
|
<span>{{ node.get("text") or node.get("id") or "-" }}</span>
|
|
<code>{{ node.get("bounds") | tojson }}</code>
|
|
{% if node.get("confidence") is not none %}<span>confidence {{ "%.3f" | format(node.get("confidence")) }}</span>{% endif %}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</details>
|
|
{% endif %}
|
|
</section>
|
|
{% endfor %}
|
|
<script>
|
|
(function () {
|
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
|
|
function rect(bounds, className) {
|
|
const el = document.createElementNS(SVG_NS, "rect");
|
|
el.setAttribute("x", bounds.x);
|
|
el.setAttribute("y", bounds.y);
|
|
el.setAttribute("width", bounds.width);
|
|
el.setAttribute("height", bounds.height);
|
|
el.setAttribute("class", className);
|
|
return el;
|
|
}
|
|
|
|
function buildBoxesGroup(elements) {
|
|
const group = document.createElementNS(SVG_NS, "g");
|
|
group.setAttribute("class", "overlay-boxes");
|
|
(elements || []).forEach(function (element) {
|
|
const bounds = element.bounds;
|
|
if (!bounds) return;
|
|
const source = element.source === "ui" ? "source-ui" : "source-ocr";
|
|
const box = rect(bounds, "overlay-box " + source);
|
|
const label = element.text || element.id || "";
|
|
if (label) {
|
|
const title = document.createElementNS(SVG_NS, "title");
|
|
title.textContent = label;
|
|
box.appendChild(title);
|
|
}
|
|
group.appendChild(box);
|
|
});
|
|
return group;
|
|
}
|
|
|
|
function buildActionGroup(toolCall) {
|
|
const group = document.createElementNS(SVG_NS, "g");
|
|
group.setAttribute("class", "overlay-action");
|
|
if (!toolCall) return group;
|
|
const action = toolCall.action;
|
|
const args = toolCall.args || {};
|
|
if (action === "tap" && isFinite(args.x) && isFinite(args.y)) {
|
|
const circle = document.createElementNS(SVG_NS, "circle");
|
|
circle.setAttribute("cx", args.x);
|
|
circle.setAttribute("cy", args.y);
|
|
circle.setAttribute("r", 14);
|
|
circle.setAttribute("class", "action-tap");
|
|
group.appendChild(circle);
|
|
} else if (
|
|
action === "swipe" &&
|
|
isFinite(args.start_x) &&
|
|
isFinite(args.start_y) &&
|
|
isFinite(args.end_x) &&
|
|
isFinite(args.end_y)
|
|
) {
|
|
const line = document.createElementNS(SVG_NS, "line");
|
|
line.setAttribute("x1", args.start_x);
|
|
line.setAttribute("y1", args.start_y);
|
|
line.setAttribute("x2", args.end_x);
|
|
line.setAttribute("y2", args.end_y);
|
|
line.setAttribute("class", "action-swipe-line");
|
|
group.appendChild(line);
|
|
|
|
const dot = document.createElementNS(SVG_NS, "circle");
|
|
dot.setAttribute("r", 8);
|
|
dot.setAttribute("class", "action-swipe-dot");
|
|
const motion = document.createElementNS(SVG_NS, "animateMotion");
|
|
motion.setAttribute("dur", "1.2s");
|
|
motion.setAttribute("repeatCount", "indefinite");
|
|
motion.setAttribute(
|
|
"path",
|
|
"M" + args.start_x + "," + args.start_y + " L" + args.end_x + "," + args.end_y
|
|
);
|
|
dot.appendChild(motion);
|
|
group.appendChild(dot);
|
|
}
|
|
return group;
|
|
}
|
|
|
|
document.querySelectorAll("svg.overlay-svg[data-step-overlay]").forEach(function (svg) {
|
|
const dataScript = svg.nextElementSibling;
|
|
if (!dataScript || !dataScript.classList.contains("step-overlay-data")) return;
|
|
let payload;
|
|
try {
|
|
payload = JSON.parse(dataScript.textContent);
|
|
} catch (err) {
|
|
return;
|
|
}
|
|
svg.appendChild(buildBoxesGroup(payload.overlay && payload.overlay.elements));
|
|
svg.appendChild(buildActionGroup(payload.tool_call));
|
|
});
|
|
|
|
const toggle = document.getElementById("overlay-boxes-toggle");
|
|
if (toggle) {
|
|
const STORAGE_KEY = "task-detail-overlay-boxes-visible";
|
|
|
|
// 页面加载时恢复之前的选择
|
|
const savedState = localStorage.getItem(STORAGE_KEY);
|
|
if (savedState !== null) {
|
|
const shouldShow = savedState === "true";
|
|
toggle.checked = shouldShow;
|
|
document.querySelectorAll("svg.overlay-svg").forEach(function (svg) {
|
|
svg.classList.toggle("show-boxes", shouldShow);
|
|
});
|
|
}
|
|
|
|
toggle.addEventListener("change", function () {
|
|
const isChecked = toggle.checked;
|
|
// 保存状态到 localStorage
|
|
localStorage.setItem(STORAGE_KEY, String(isChecked));
|
|
document.querySelectorAll("svg.overlay-svg").forEach(function (svg) {
|
|
svg.classList.toggle("show-boxes", isChecked);
|
|
});
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
{% endblock %}
|