461 lines
22 KiB
JavaScript
461 lines
22 KiB
JavaScript
(() => {
|
||
"use strict";
|
||
|
||
if (window.self !== window.top || new URLSearchParams(window.location.search).has("embedded")) {
|
||
document.body.classList.add("embedded");
|
||
}
|
||
|
||
const STATUS_LABELS = {
|
||
waiting: "等待",
|
||
running: "运行中",
|
||
pending_confirmation: "待确认",
|
||
success: "成功",
|
||
failed: "失败",
|
||
canceled: "取消",
|
||
blocked: "阻塞",
|
||
published: "已发布",
|
||
ready: "待发布",
|
||
baseline: "基线固件",
|
||
};
|
||
const STAGE_ICONS = {
|
||
instruction: "file-input",
|
||
requirements: "list-checks",
|
||
baseline: "git-compare-arrows",
|
||
codegen: "code-2",
|
||
build: "hammer",
|
||
test: "shield-check",
|
||
package: "package-check",
|
||
publish: "cloud-upload",
|
||
};
|
||
const ACTIVE_STATES = new Set(["waiting", "running", "pending_confirmation"]);
|
||
const CAPABILITY_ORDER = ["时间控制", "位置控制", "座椅通风", "座椅加热"];
|
||
const FALLBACK_BASELINES = {
|
||
time_only: ["时间控制"],
|
||
};
|
||
const FALLBACK_PIPELINE = [
|
||
["instruction", "指令输入"], ["requirements", "需求解析"],
|
||
["baseline", "基线检查"], ["codegen", "代码生成"],
|
||
["build", "编译构建"], ["test", "测试验证"],
|
||
["package", "固件打包"], ["publish", "发布服务器"],
|
||
].map(([id, name]) => ({ id, name, status: "waiting", summary: "等待执行" }));
|
||
|
||
const ui = {
|
||
instruction: document.querySelector("#instructionInput"),
|
||
baseline: document.querySelector("#baselineSelect"),
|
||
baselineCapabilities: document.querySelector("#baselineCapabilities"),
|
||
targetCapabilities: document.querySelector("#targetCapabilities"),
|
||
start: document.querySelector("#startTaskButton"),
|
||
cancel: document.querySelector("#cancelTaskButton"),
|
||
retry: document.querySelector("#retryTaskButton"),
|
||
server: document.querySelector("#serverState"),
|
||
serverName: document.querySelector("#otaServerName"),
|
||
release: document.querySelector("#releaseResult"),
|
||
pipeline: document.querySelector("#pipeline"),
|
||
logs: document.querySelector("#logStream"),
|
||
scrollLogs: document.querySelector("#scrollLogButton"),
|
||
status: document.querySelector("#taskStatusBadge"),
|
||
metrics: document.querySelector("#taskMetrics"),
|
||
inspector: document.querySelector("#inspectorContent"),
|
||
artifacts: document.querySelector("#artifactTableBody"),
|
||
modal: document.querySelector("#detailModal"),
|
||
modalTitle: document.querySelector("#modalTitle"),
|
||
modalBody: document.querySelector("#modalBody"),
|
||
modalClose: document.querySelector("#closeModalButton"),
|
||
modalCloseFooter: document.querySelector("#modalCloseButton"),
|
||
toasts: document.querySelector("#toastRegion"),
|
||
};
|
||
const model = {
|
||
snapshot: { tasks: [], artifacts: [], releases: [], pipeline: FALLBACK_PIPELINE },
|
||
selectedTaskId: null,
|
||
inspectorTab: "changes",
|
||
pending: false,
|
||
pollTimer: null,
|
||
lastLogCount: 0,
|
||
};
|
||
|
||
function escapeHtml(value) {
|
||
return String(value ?? "")
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """)
|
||
.replaceAll("'", "'");
|
||
}
|
||
|
||
async function api(path, options = {}) {
|
||
const response = await fetch(path, {
|
||
credentials: "same-origin",
|
||
headers: { "Content-Type": "application/json", ...(options.headers || {}) },
|
||
...options,
|
||
});
|
||
const contentType = response.headers.get("content-type") || "";
|
||
const payload = contentType.includes("application/json") ? await response.json() : null;
|
||
if (!response.ok) throw new Error(payload?.message || `请求失败(${response.status})`);
|
||
return payload;
|
||
}
|
||
|
||
function selectedTask() {
|
||
return model.snapshot.tasks.find((task) => task.id === model.selectedTaskId)
|
||
|| model.snapshot.tasks[0]
|
||
|| null;
|
||
}
|
||
|
||
function baselineCapabilities() {
|
||
const baseline = model.snapshot.baselines?.find((item) => item.id === ui.baseline.value);
|
||
return baseline?.capabilities || FALLBACK_BASELINES[ui.baseline.value] || FALLBACK_BASELINES.time_only;
|
||
}
|
||
|
||
function parseInstructionCapabilities(instruction) {
|
||
const lowered = String(instruction || "").toLowerCase();
|
||
const aliases = {
|
||
"时间控制": ["时间", "定时", "timed"],
|
||
"位置控制": ["位置", "position"],
|
||
"座椅通风": ["通风", "vent"],
|
||
"座椅加热": ["加热", "heater", "heat"],
|
||
};
|
||
return CAPABILITY_ORDER.filter((capability) => aliases[capability].some((alias) => lowered.includes(alias)));
|
||
}
|
||
|
||
function updateCapabilityPreview(task = null) {
|
||
const baseline = baselineCapabilities();
|
||
const taskMatchesDraft = task
|
||
&& task.instruction === ui.instruction.value.trim()
|
||
&& (task.baseline_id || "time_only") === ui.baseline.value;
|
||
const target = taskMatchesDraft
|
||
? task.target_capabilities
|
||
: CAPABILITY_ORDER.filter((item) => baseline.includes(item) || parseInstructionCapabilities(ui.instruction.value).includes(item));
|
||
ui.baselineCapabilities.textContent = baseline.join("+");
|
||
ui.targetCapabilities.textContent = (target?.length ? target : baseline).join("+");
|
||
}
|
||
|
||
function formatDate(value) {
|
||
if (!value) return "—";
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return value;
|
||
return new Intl.DateTimeFormat("zh-CN", {
|
||
month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit",
|
||
hour12: false,
|
||
}).format(date).replaceAll("/", "-");
|
||
}
|
||
|
||
function formatClock(value) {
|
||
if (!value) return "--:--:--";
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return "--:--:--";
|
||
return new Intl.DateTimeFormat("zh-CN", {
|
||
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
|
||
}).format(date);
|
||
}
|
||
|
||
function formatDuration(milliseconds) {
|
||
if (milliseconds === null || milliseconds === undefined) return "—";
|
||
if (milliseconds < 1000) return `${milliseconds} 毫秒`;
|
||
const seconds = milliseconds / 1000;
|
||
return seconds < 60 ? `${seconds.toFixed(seconds < 10 ? 1 : 0)} 秒` : `${Math.floor(seconds / 60)} 分 ${Math.round(seconds % 60)} 秒`;
|
||
}
|
||
|
||
function formatSize(bytes) {
|
||
const value = Number(bytes || 0);
|
||
if (value < 1024) return `${value} 字节`;
|
||
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} 千字节`;
|
||
return `${(value / 1024 / 1024).toFixed(2)} 兆字节`;
|
||
}
|
||
|
||
function toast(message, type = "info") {
|
||
const element = document.createElement("div");
|
||
element.className = `toast ${type}`;
|
||
element.textContent = message;
|
||
ui.toasts.append(element);
|
||
window.setTimeout(() => element.remove(), 4200);
|
||
}
|
||
|
||
function refreshIcons() {
|
||
if (window.lucide) window.lucide.createIcons({ attrs: { "stroke-width": 1.8 } });
|
||
}
|
||
|
||
function renderPipeline(task) {
|
||
const stages = task?.stages || model.snapshot.pipeline?.map((stage) => ({ ...stage, status: "waiting" })) || FALLBACK_PIPELINE;
|
||
ui.pipeline.innerHTML = stages.map((stage) => `
|
||
<li class="pipeline-stage" data-status="${escapeHtml(stage.status || "waiting")}" title="${escapeHtml(stage.summary || "")}">
|
||
<span class="stage-node"><i data-lucide="${STAGE_ICONS[stage.id] || "circle"}"></i></span>
|
||
<strong class="stage-name">${escapeHtml(stage.name)}</strong>
|
||
<span class="stage-state">${escapeHtml(STATUS_LABELS[stage.status] || stage.status || "等待")}${stage.duration_ms !== null && stage.duration_ms !== undefined ? ` · ${escapeHtml(formatDuration(stage.duration_ms))}` : ""}</span>
|
||
</li>`).join("");
|
||
}
|
||
|
||
function renderRelease(task) {
|
||
let icon = "cloud-upload";
|
||
let label = "尚未开始";
|
||
let state = "idle";
|
||
if (task) {
|
||
label = task.message || STATUS_LABELS[task.status] || task.status;
|
||
state = task.status;
|
||
if (task.status === "success") { icon = "circle-check-big"; state = "success"; }
|
||
else if (task.status === "running") icon = "loader-circle";
|
||
else if (task.status === "failed") icon = "circle-x";
|
||
else if (task.status === "canceled") icon = "ban";
|
||
}
|
||
ui.release.dataset.state = state;
|
||
ui.release.innerHTML = `<span class="result-icon"><i data-lucide="${icon}"></i></span><div><small>当前交付状态</small><strong>${escapeHtml(label)}</strong></div>`;
|
||
}
|
||
|
||
function renderLogs(task) {
|
||
const logs = task?.logs || [];
|
||
if (!logs.length) {
|
||
ui.logs.innerHTML = `<div class="empty-state"><i data-lucide="terminal-square"></i><strong>暂无执行记录</strong><span>创建开发任务后,这里将实时显示各阶段输出。</span></div>`;
|
||
model.lastLogCount = 0;
|
||
return;
|
||
}
|
||
const nearBottom = ui.logs.scrollHeight - ui.logs.scrollTop - ui.logs.clientHeight < 80;
|
||
ui.logs.innerHTML = logs.map((entry) => `
|
||
<article class="log-entry" data-level="${escapeHtml(entry.level)}">
|
||
<time class="log-time">${escapeHtml(formatClock(entry.time))}</time>
|
||
<span class="log-dot"></span>
|
||
<div class="log-copy"><strong>${escapeHtml(entry.title)}</strong><p>${escapeHtml(entry.message)}</p></div>
|
||
</article>`).join("");
|
||
if (nearBottom || logs.length > model.lastLogCount) ui.logs.scrollTop = ui.logs.scrollHeight;
|
||
model.lastLogCount = logs.length;
|
||
}
|
||
|
||
function renderInspector(task) {
|
||
const status = task?.status || "waiting";
|
||
ui.status.dataset.status = status;
|
||
ui.status.textContent = STATUS_LABELS[status] || status;
|
||
const fileCount = task?.file_changes?.length || 0;
|
||
ui.metrics.innerHTML = `
|
||
<div><dt>开始时间</dt><dd>${escapeHtml(formatDate(task?.started_at))}</dd></div>
|
||
<div><dt>结束时间</dt><dd>${escapeHtml(formatDate(task?.finished_at))}</dd></div>
|
||
<div><dt>实际耗时</dt><dd>${escapeHtml(formatDuration(task?.duration_ms))}</dd></div>
|
||
<div><dt>文件变更</dt><dd>${fileCount ? `${fileCount} 个文件` : "—"}</dd></div>`;
|
||
document.querySelectorAll("[data-inspector-tab]").forEach((button) => {
|
||
button.classList.toggle("active", button.dataset.inspectorTab === model.inspectorTab);
|
||
});
|
||
if (!task) {
|
||
ui.inspector.innerHTML = `<div class="empty-inline">暂无任务数据</div>`;
|
||
return;
|
||
}
|
||
if (model.inspectorTab === "build") {
|
||
ui.inspector.innerHTML = task.build_output?.length
|
||
? `<pre class="build-output">${escapeHtml(task.build_output.join("\n"))}</pre>`
|
||
: `<div class="empty-inline">尚无编译输出</div>`;
|
||
return;
|
||
}
|
||
if (model.inspectorTab === "tests") {
|
||
ui.inspector.innerHTML = task.test_results?.length
|
||
? task.test_results.map((test) => `<div class="test-item"><span class="test-mark"><i data-lucide="check"></i></span><div class="test-copy"><strong>${escapeHtml(test.name)}</strong><span>${escapeHtml(test.summary)}</span></div></div>`).join("")
|
||
: `<div class="empty-inline">测试尚未执行</div>`;
|
||
return;
|
||
}
|
||
ui.inspector.innerHTML = task.file_changes?.length
|
||
? task.file_changes.map((change) => `<div class="change-item"><span class="change-type">${change.type === "added" ? "新增" : "修改"}</span><div class="change-copy"><strong>${escapeHtml(change.path)}</strong><span>${escapeHtml(change.summary)}</span></div></div>`).join("")
|
||
: `<div class="empty-inline">尚无文件变更</div>`;
|
||
}
|
||
|
||
function renderArtifacts() {
|
||
const artifacts = model.snapshot.artifacts || [];
|
||
if (!artifacts.length) {
|
||
ui.artifacts.innerHTML = `<tr><td colspan="6"><div class="empty-table">尚未生成固件产物</div></td></tr>`;
|
||
return;
|
||
}
|
||
ui.artifacts.innerHTML = artifacts.map((artifact) => `
|
||
<tr>
|
||
<td><div class="software-name"><strong>${escapeHtml(artifact.software_name)}</strong><span>${escapeHtml(artifact.version)}</span></div></td>
|
||
<td><div class="cap-tags">${(artifact.capabilities || []).map((capability) => `<span>${escapeHtml(capability)}</span>`).join("")}</div></td>
|
||
<td>${escapeHtml(formatDate(artifact.build_time))}</td>
|
||
<td><div class="file-cell"><strong>${escapeHtml(artifact.firmware_filename)}</strong><span>${escapeHtml(formatSize(artifact.size_bytes))} · <span class="hash-value" title="${escapeHtml(artifact.sha256)}">${escapeHtml(artifact.sha256)}</span></span></div></td>
|
||
<td><span class="status-badge" data-status="${escapeHtml(artifact.status)}">${escapeHtml(STATUS_LABELS[artifact.status] || artifact.status)}</span></td>
|
||
<td><div class="table-actions">
|
||
<button type="button" title="查看详情" data-artifact-action="detail" data-artifact-id="${artifact.id}">详情</button>
|
||
<button type="button" title="查看能力差异" data-artifact-action="capabilities" data-artifact-id="${artifact.id}">能力</button>
|
||
<button type="button" title="查看代码差异" data-artifact-action="code" data-artifact-id="${artifact.id}">代码</button>
|
||
<button type="button" title="查看验证记录" data-artifact-action="validation" data-artifact-id="${artifact.id}">验证</button>
|
||
<a href="${escapeHtml(artifact.download_url)}" title="下载固件" aria-label="下载固件" download><i data-lucide="download"></i></a>
|
||
<button class="publish-action" type="button" title="发布到服务器" data-artifact-action="publish" data-artifact-id="${artifact.id}"><i data-lucide="cloud-upload"></i></button>
|
||
${artifact.is_baseline ? `<span class="baseline-protection" title="时间控制基线固件受保护"><i data-lucide="shield-check"></i>受保护</span>` : `<button class="delete-action" type="button" title="删除固件产物" data-artifact-action="delete" data-artifact-id="${artifact.id}"><i data-lucide="trash-2"></i></button>`}
|
||
${artifact.status === "published" ? `<button class="notify-action" type="button" title="通知车端" aria-label="通知车端" data-artifact-action="notify" data-artifact-id="${artifact.id}"><i data-lucide="bell"></i></button>` : ""}
|
||
</div></td>
|
||
</tr>`).join("");
|
||
}
|
||
|
||
function render() {
|
||
const task = selectedTask();
|
||
if (task) model.selectedTaskId = task.id;
|
||
renderPipeline(task);
|
||
renderRelease(task);
|
||
renderLogs(task);
|
||
renderInspector(task);
|
||
renderArtifacts();
|
||
updateCapabilityPreview(task);
|
||
const active = task && ACTIVE_STATES.has(task.status);
|
||
ui.start.disabled = model.pending || Boolean(active);
|
||
ui.cancel.disabled = model.pending || !active;
|
||
ui.retry.disabled = model.pending || !task || !["failed", "canceled", "blocked"].includes(task.status);
|
||
ui.start.innerHTML = model.pending ? `<i data-lucide="loader-circle"></i>正在提交` : `<i data-lucide="play"></i>开始生成`;
|
||
refreshIcons();
|
||
}
|
||
|
||
async function refreshState({ quiet = false } = {}) {
|
||
try {
|
||
model.snapshot = await api("/api/firmware-workbench/state");
|
||
if (!model.selectedTaskId && model.snapshot.tasks?.length) model.selectedTaskId = model.snapshot.tasks[0].id;
|
||
ui.server.classList.add("online");
|
||
ui.server.innerHTML = `<span></span>发布服务已连接`;
|
||
ui.serverName.textContent = model.snapshot.ota_server?.name || "车机升级服务器";
|
||
render();
|
||
} catch (error) {
|
||
ui.server.classList.remove("online");
|
||
ui.server.innerHTML = `<span></span>发布服务连接失败`;
|
||
if (!quiet) toast(error.message, "error");
|
||
} finally {
|
||
schedulePoll();
|
||
}
|
||
}
|
||
|
||
function schedulePoll() {
|
||
window.clearTimeout(model.pollTimer);
|
||
const active = model.snapshot.tasks?.some((task) => ACTIVE_STATES.has(task.status));
|
||
model.pollTimer = window.setTimeout(() => refreshState({ quiet: true }), active ? 700 : 5000);
|
||
}
|
||
|
||
async function startTask() {
|
||
const instruction = ui.instruction.value.trim();
|
||
if (!instruction) { toast("请输入开发指令。", "error"); ui.instruction.focus(); return; }
|
||
model.pending = true;
|
||
render();
|
||
try {
|
||
const result = await api("/api/firmware-workbench/tasks", {
|
||
method: "POST", body: JSON.stringify({ instruction, baseline_id: ui.baseline.value }),
|
||
});
|
||
model.selectedTaskId = result.task.id;
|
||
toast("开发任务已创建,流水线开始执行。", "success");
|
||
await refreshState({ quiet: true });
|
||
} catch (error) {
|
||
toast(error.message, "error");
|
||
} finally {
|
||
model.pending = false;
|
||
render();
|
||
}
|
||
}
|
||
|
||
async function taskAction(action) {
|
||
const task = selectedTask();
|
||
if (!task) return;
|
||
model.pending = true;
|
||
render();
|
||
try {
|
||
await api(`/api/firmware-workbench/tasks/${encodeURIComponent(task.id)}/${action}`, { method: "POST", body: "{}" });
|
||
toast(action === "cancel" ? "取消请求已提交。" : "任务已重新进入流水线。", "success");
|
||
await refreshState({ quiet: true });
|
||
} catch (error) {
|
||
toast(error.message, "error");
|
||
} finally {
|
||
model.pending = false;
|
||
render();
|
||
}
|
||
}
|
||
|
||
function appendPrompt(text) {
|
||
const current = ui.instruction.value.trim();
|
||
if (current.includes(text)) return;
|
||
ui.instruction.value = current ? `${current.replace(/[。;;]?$/, "")};${text}。` : `${text}。`;
|
||
ui.instruction.focus();
|
||
updateCapabilityPreview();
|
||
}
|
||
|
||
function showModal(title, content) {
|
||
ui.modalTitle.textContent = title;
|
||
ui.modalBody.innerHTML = content;
|
||
ui.modal.hidden = false;
|
||
document.body.style.overflow = "hidden";
|
||
refreshIcons();
|
||
ui.modalClose.focus();
|
||
}
|
||
|
||
function closeModal() {
|
||
ui.modal.hidden = true;
|
||
document.body.style.overflow = "";
|
||
}
|
||
|
||
async function artifactAction(button) {
|
||
const artifact = model.snapshot.artifacts.find((item) => item.id === button.dataset.artifactId);
|
||
if (!artifact) return;
|
||
const action = button.dataset.artifactAction;
|
||
if (action === "delete") {
|
||
if (!window.confirm(`确认删除“${artifact.firmware_filename}”及其服务器发布记录吗?此操作不可恢复。`)) return;
|
||
button.disabled = true;
|
||
try {
|
||
const result = await api(`/api/firmware-workbench/artifacts/${artifact.id}`, { method: "DELETE" });
|
||
toast(result.message, "success");
|
||
await refreshState({ quiet: true });
|
||
} catch (error) { toast(error.message, "error"); }
|
||
finally { button.disabled = false; }
|
||
return;
|
||
}
|
||
if (action === "publish") {
|
||
button.disabled = true;
|
||
try {
|
||
const release = await api(`/api/firmware-workbench/artifacts/${artifact.id}/publish`, { method: "POST", body: "{}" });
|
||
toast(`${release.message}(${release.id})`, "success");
|
||
await refreshState({ quiet: true });
|
||
} catch (error) { toast(error.message, "error"); }
|
||
finally { button.disabled = false; }
|
||
return;
|
||
}
|
||
if (action === "notify") {
|
||
button.disabled = true;
|
||
try {
|
||
const result = await api(`/api/firmware-workbench/artifacts/${artifact.id}/notify`, { method: "POST", body: "{}" });
|
||
toast(`已向车端发送 ${result.published_count || result.burst_count || 3} 帧通知。`, "success");
|
||
await refreshState({ quiet: true });
|
||
} catch (error) { toast(error.message, "error"); }
|
||
finally { button.disabled = false; }
|
||
return;
|
||
}
|
||
if (action === "detail") {
|
||
showModal("固件产物详情", `<div class="detail-grid">
|
||
<div class="detail-field"><span>软件名称</span><strong>${escapeHtml(artifact.software_name)}</strong></div>
|
||
<div class="detail-field"><span>版本</span><strong>${escapeHtml(artifact.version)}</strong></div>
|
||
<div class="detail-field"><span>固件包</span><strong>${escapeHtml(artifact.firmware_filename)}</strong></div>
|
||
<div class="detail-field"><span>构建时间</span><strong>${escapeHtml(formatDate(artifact.build_time))}</strong></div>
|
||
<div class="detail-field"><span>固件包大小</span><strong>${escapeHtml(formatSize(artifact.size_bytes))}</strong></div>
|
||
<div class="detail-field"><span>发布状态</span><strong>${escapeHtml(STATUS_LABELS[artifact.status] || artifact.status)}</strong></div>
|
||
<div class="detail-field"><span>SHA-256(固件包)</span><strong>${escapeHtml(artifact.sha256)}</strong></div>
|
||
<div class="detail-field"><span>SHA-256(HEX 镜像)</span><strong>${escapeHtml(artifact.image_sha256)}</strong></div>
|
||
</div>`);
|
||
} else if (action === "capabilities") {
|
||
const diff = artifact.capability_diff || {};
|
||
showModal("能力差异", `<div class="diff-columns">
|
||
<div class="diff-panel"><h3>固件基线</h3>${(artifact.baseline_capabilities || []).map((item) => `<p>• ${escapeHtml(item)}</p>`).join("")}</div>
|
||
<div class="diff-panel added"><h3>目标固件变化</h3>${(diff.added || []).map((item) => `<p>+ ${escapeHtml(item)}</p>`).join("")}${(diff.retained || []).map((item) => `<p>✓ 保留${escapeHtml(item)}</p>`).join("") || "<p>无新增能力</p>"}</div>
|
||
</div>`);
|
||
} else if (action === "code") {
|
||
showModal("代码差异", `<pre class="code-block">${escapeHtml(artifact.code_diff || "暂无代码差异记录")}</pre>`);
|
||
} else if (action === "validation") {
|
||
showModal("验证记录", (artifact.validation_records || []).map((test) => `<div class="test-item"><span class="test-mark"><i data-lucide="check"></i></span><div class="test-copy"><strong>${escapeHtml(test.name)}</strong><span>${escapeHtml(test.summary)} · ${escapeHtml(formatDate(test.finished_at))}</span></div></div>`).join("") || `<div class="empty-inline">暂无验证记录</div>`);
|
||
}
|
||
}
|
||
|
||
ui.start.addEventListener("click", startTask);
|
||
ui.instruction.addEventListener("input", () => updateCapabilityPreview());
|
||
ui.baseline.addEventListener("change", () => updateCapabilityPreview());
|
||
ui.cancel.addEventListener("click", () => taskAction("cancel"));
|
||
ui.retry.addEventListener("click", () => taskAction("retry"));
|
||
ui.scrollLogs.addEventListener("click", () => { ui.logs.scrollTop = ui.logs.scrollHeight; });
|
||
document.querySelectorAll("[data-prompt]").forEach((button) => button.addEventListener("click", () => appendPrompt(button.dataset.prompt)));
|
||
document.querySelectorAll("[data-inspector-tab]").forEach((button) => button.addEventListener("click", () => { model.inspectorTab = button.dataset.inspectorTab; renderInspector(selectedTask()); refreshIcons(); }));
|
||
ui.artifacts.addEventListener("click", (event) => {
|
||
const button = event.target.closest("[data-artifact-action]");
|
||
if (button) artifactAction(button);
|
||
});
|
||
ui.modalClose.addEventListener("click", closeModal);
|
||
ui.modalCloseFooter.addEventListener("click", closeModal);
|
||
ui.modal.addEventListener("click", (event) => { if (event.target === ui.modal) closeModal(); });
|
||
document.addEventListener("keydown", (event) => { if (event.key === "Escape" && !ui.modal.hidden) closeModal(); });
|
||
|
||
renderPipeline(null);
|
||
renderRelease(null);
|
||
updateCapabilityPreview();
|
||
refreshIcons();
|
||
refreshState();
|
||
})();
|