Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
File size: 27,351 Bytes
da5cba1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 | // The MiMo release's own views (app/envs/mimo.py), as the MiMo RL Environment Explorer draws them: the agent's prompt,
// the systems a General task works through and their data, its workspace files with previews, the grader in full
// (hidden tests, the crash to reproduce, rubric checks and the judge prompt, the screenshot judge's bands, the music
// scorer's features), and the reward design across the release.
// render(kind, data, ctx) -> HTML wire(root, ctx): the clicks (previews, tables, the prompt's fold and copy)
import { $, $$, api, esc, fmt, md, bytes, sk, toast, openModal, progress, emptyState, table, sheetGrid } from "../util.js";
import { icon, DOMAIN_ICON, FILE_ICON } from "../icons.js";
const enc = (s) => s.split("/").map(encodeURIComponent).join("/");
const KIND_LABEL = { spreadsheet: "Excel", document: "Word", pdf: "PDF", slides: "PowerPoint", image: "image", web: "web page", text: "text", archive: "archive" };
const SHAPE_IN = { both: "the systems' databases and the workspace files", db: "the systems' databases", workspace: "the workspace files" };
const SHAPE_ACT = { none: "change nothing: its final answer is what gets graded", mutate_db: "update records in the systems' databases",
edit_workspace: "edit files in the workspace", both: "update the databases and edit workspace files" };
const pre = (t) => `<pre class="code-block wrap"><code>${esc(t)}</code></pre>`;
const pct = (x) => `${Math.round(x * 100)}%`;
const disclose = (label, inner) => `<details style="margin-top:12px"><summary class="disclose">${icon("chevronRight", 14, "chev")}${esc(label)}</summary>${inner}</details>`;
const fmtNum = (x) => (x == null ? "" : Math.abs(x) >= 100 ? x.toFixed(0) : Math.abs(x) >= 1 ? x.toFixed(2) : x.toFixed(3));
export function render(kind, data, ctx) {
return ({ prompt, systems, workspace, grading, rewards }[kind] || (() => ""))(data, ctx);
}
// ββ the agent's first message ββββββββββββββββββββββββββββββββββββββββββββββββ
function prompt(p) {
const chars = p.parts.reduce((n, [, t]) => n + t.length, 0);
const rows = [p.harness
? ["Harness", `OpenCode ${esc(p.harness.version)}, which adds its own system prompt and tool definitions`]
: ["Harness", "None. One chat completion: this message is the whole conversation, with no system prompt and no tools"]];
if (p.harness) rows.push(["Tools", `OpenCode's own${p.systems ? `, plus the tools of the ${p.systems} systems over MCP` : ""}; web search and web fetch are denied`]);
if (p.steps) rows.push(["Step limit", `${fmt.format(p.steps)} model calls`]);
if (p.timeout_min) rows.push(["Time limit", `${p.timeout_min} minutes`]);
if (p.max_tokens) rows.push(["Reply length", `up to ${fmt.format(p.max_tokens)} tokens${p.harness ? " per model call" : ""}`]);
const body = p.parts.map(([kind, text]) => kind === "task" && p.task_is_brief
? `<button class="pr-task" type="button" data-pr-expand title="Show it here">βΉthe task above, ${fmt.format(text.length)} charactersβΊ</button><span hidden>${esc(text)}</span>`
: esc(text)).join("");
return `<dl class="kv">${rows.map(([k, x]) => `<dt>${k}</dt><dd>${x}</dd>`).join("")}</dl>
<div class="pr-h"><span class="sec-label">The message Β· ${fmt.format(chars)} characters</span>
<button class="btn sm" type="button" data-pr-copy>${icon("copy", 13)}Copy</button></div>
<pre class="code-block wrap pr" data-pr-text="${esc(p.parts.map(([, t]) => t).join(""))}">${body}</pre>
<p class="muted xs" style="margin-top:8px">Limits are the training harness's defaults; the run panel can change them.</p>`;
}
// ββ systems: MCP servers and their databases βββββββββββββββββββββββββββββββββ
function systems(list) {
return `<p class="muted sm" style="margin-bottom:12px">The agent can only reach this data through the tools below. Open a table to see the rows it starts with.</p>
<div class="systems">${list.map((s, i) => `
<details class="sys" ${i === 0 ? "open" : ""}>
<summary>${icon("chevronRight", 14, "chev")}<b>${esc(s.label)}</b><span class="meta">${s.tools.length} tools Β· ${s.tables.length} tables Β· ${s.tables.reduce((n, t) => n + t.rows, 0).toLocaleString()} rows</span></summary>
<div class="sys-in">
<div><div class="sec-label">Tools</div>
<ul class="tools">${s.tools.map((t) => `<li><code class="sig">${esc(t.name)}(<span>${t.params.map((p) => esc(p.name) + (p.default != null ? "?" : "")).join(", ")}</span>)</code>
${t.doc ? `<p>${esc(t.doc)}</p>` : ""}</li>`).join("")}</ul></div>
<div><div class="sec-label">Data</div>
<div class="tables">${s.tables.map((t) => `<button class="tbl-btn" type="button" data-sys="${esc(s.name)}" data-label="${esc(s.label)}" data-table="${esc(t.table)}">${icon("table", 15)}
<b>${esc(t.table)}</b><span>${t.rows.toLocaleString()} Γ ${t.columns.length}</span></button>`).join("")}</div></div>
</div>
</details>`).join("")}</div>`;
}
// ββ workspace files ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function workspace(files) {
return `<div class="files-grid">${files.map((f) => `<button class="file k-${esc(f.kind)}" type="button" data-path="${esc(f.path)}" data-kind="${esc(f.kind)}" title="${esc(f.path)}">
<span class="fi">${icon(FILE_ICON[f.kind] || "file", 16)}</span><span class="fx"><span class="fn">${esc(f.path).replace(/([_/.-])/g, "$1<wbr>")}</span><span class="fs">${esc(KIND_LABEL[f.kind] || f.kind)} Β· ${bytes(f.size)}</span></span></button>`).join("")}</div>`;
}
// ββ the grader βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function diffHtml(patch) {
return patch.split("\n").slice(0, 4000).map((l) => {
const cls = l.startsWith("+++") || l.startsWith("---") || l.startsWith("diff ") ? "dh" : l.startsWith("+") ? "da" : l.startsWith("-") ? "dd" : l.startsWith("@@") ? "dc" : "";
return `<span class="${cls}">${esc(l)}</span>`;
}).join("\n");
}
function grading(g) {
let body = `<p class="lead">${esc(g.summary)}</p>`;
if (g.steps) body += `<ol class="steps">${g.steps.map((s) => `<li><span>${md(s).replace(/^<p>|<\/p>$/g, "")}</span></li>`).join("")}</ol>`;
if (g.formula) body += `<p class="formula"><code>${esc(g.formula)}</code></p>`;
if (g.kind === "tests") {
body += `<div class="sec-label" style="margin-top:18px">Hidden tests</div><div class="tbl"><table><thead><tr><th>File</th><th>Lines</th><th></th></tr></thead><tbody>${g.files.map((f) =>
`<tr><td><code>${esc(f.path)}</code></td><td class="pm"><span class="a">+${f.added}</span> <span class="d">β${f.removed}</span></td><td>${f.new ? '<span class="chip">new</span>' : ""}</td></tr>`).join("")}</tbody></table></div>`;
if (g.script) body += disclose("Test command script", `<pre class="code-block"><code>${esc(g.script)}</code></pre>`);
if (g.patch) body += disclose(`Full hidden test patch (${g.patch.length.toLocaleString()} characters)`, `<pre class="code-block diff">${diffHtml(g.patch)}</pre>`);
}
if (g.kind === "crash" && g.expected) {
const x = g.expected;
body += `<div class="target">${[["Sanitizer", esc(x.sanitizer)], ["Bug class", esc(x.error_type)], ["Function", `<code>${esc(x.function)}</code>`], ["File", `<code>${esc(x.file)}</code>`]]
.map(([k, val]) => `<div class="stat"><span>${k}</span><b>${val}</b></div>`).join("")}</div>`;
}
if (g.kind === "rubric") {
const shape = [["Facts come from", SHAPE_IN[g.shape?.input]], ["The agent must", SHAPE_ACT[g.shape?.act]]].filter(([, x]) => x);
if (shape.length) body += `<dl class="kv" style="margin-top:14px">${shape.map(([k, x]) => `<dt>${k}</dt><dd>${esc(x)}</dd>`).join("")}</dl>`;
if (g.rules) body += `<ul class="rules">${g.rules.map((r) => `<li>${esc(r)}</li>`).join("")}</ul>`;
// verify.py counts a check with no weight as weight 1 (weights are relative, not percentages)
const wt = (c) => (c.weight == null ? 1 : Number(c.weight));
const tot = g.checks.reduce((s, c) => s + wt(c), 0) || 1;
const top = Math.max(...g.checks.map(wt)) || 1;
body += `<div class="sec-label" style="margin-top:18px">${g.checks.length} checks, weighted</div><ol class="checks">${g.checks.map((c) => {
const share = Math.round((wt(c) / tot) * 100);
return `<li><div class="ck-top"><span class="tier t-${esc(c.tier)}">${esc(c.tier || "")}</span>
<span>${c.method === "llm" ? `judged by a model${c.files?.length ? ` Β· reads ${esc(c.files.join(", "))}` : ""}` : "checked by code"}</span>
<span class="w" title="Share of the reward"><i><b style="width:${Math.round((wt(c) / top) * 100)}%"></b></i>${share}%</span></div>
<p>${esc(c.question || c.id)}</p></li>`; }).join("")}</ol><p class="muted xs" style="margin-top:10px">The answer each check expects is not shown.</p>`;
const bare = g.checks.filter((c) => c.weight == null);
if (bare.length && bare.length < g.checks.length) {
const set = g.checks.filter((c) => c.weight != null).reduce((s, c) => s + Number(c.weight), 0);
body += `<p class="muted xs" style="margin-top:6px">The weights given add up to ${set.toFixed(2).replace(/\.?0+$/, "")}, and ${bare.length === 1 ? "one check has none" : `${bare.length} checks have none`}.
verify.py counts a missing weight as 1, so ${bare.length === 1 ? "that check decides" : "those checks decide"} ${pct(bare.length / tot)} of the reward.</p>`;
}
const j = g.judge;
if (j) {
const how = `<p class="muted xs" style="margin:8px 0">The judge gets one such message per model-judged check. This is the one for <code>${esc(j.check)}</code>, with the expected answer withheld.</p>`;
if (j.english) body += disclose("The judge prompt, in English", how + pre(j.english));
body += disclose("The judge prompt as sent, in Chinese", (j.english ? "" : how) + pre(j.original));
}
if (g.grader) body += `<p class="muted xs" style="margin-top:10px">${esc(g.grader)}</p>`;
}
if (g.kind === "terminal") {
body += `<div class="sec-label" style="margin-top:18px">Hidden test files</div>${table([["File", "Size"], ...g.files.map((f) => [f.path, bytes(f.size)])])}`;
if (g.script) body += disclose("tests/test.sh", `<pre class="code-block"><code>${esc(g.script)}</code></pre>`);
if (g.tests) body += disclose("tests/test_outputs.py", `<pre class="code-block"><code>${esc(g.tests)}</code></pre>`);
}
if (g.kind === "visual") {
const parts = [["visual", "Visual quality", "mean of five criteria"], ["query_fulfillment", "Brief fulfilment", ""], ["premium_assets", "Asset quality", ""]];
body += `<div class="parts">${parts.map(([key, name, how]) => {
const ds = g.dims.filter((d) => d.group === key);
return ds.length ? `<div class="rpart"><div class="rpart-h"><b>${name}</b><span>β
of the score${how ? ` Β· ${how}` : ""}</span></div>
<dl>${ds.map((d) => `<div><dt>${esc(d.label)}</dt><dd>${esc(d.desc)}</dd></div>`).join("")}</dl></div>` : "";
}).join("")}</div>`;
const j = g.judge;
if (j?.rules) body += `<div class="sec-label">The judge's instructions</div><ul class="rules" style="margin-top:0">${j.rules.map((r) => `<li>${esc(r)}</li>`).join("")}</ul>`;
if (j?.bands) body += disclose("Scoring bands for each criterion, from the judge prompt", `<div class="bands">${g.dims.map((d) => j.bands[d.key]
? `<div><b>${esc(d.label)}</b><ol>${j.bands[d.key].map((t, i) => `<li><span>${j.edges[i]}</span><span>${esc(t)}</span></li>`).join("")}</ol></div>` : "").join("")}</div>`);
if (j?.why) body += `<div class="sec-label">Why these weights</div><ul class="rules" style="margin-top:0">${j.why.map((r) => `<li>${esc(r)}</li>`).join("")}</ul>`;
body += `<div class="sec-label">In training</div><p class="sm muted">${esc(j?.training || g.note)}</p>`;
if (j?.original) body += disclose("The judge prompt as sent, in Chinese, with this task's brief", `<p class="muted xs" style="margin:8px 0">Sent with the full-page screenshot. The brief is cut to its first ${fmt.format(j.query_cap)} characters.</p>${pre(j.original)}`);
}
if (g.kind === "music") {
const groups = {};
g.features.forEach((f) => (groups[f.group] ||= []).push(f));
body += `<div class="note-box gated">${icon("alert")}<span><b>Validity gate first.</b> No notation errors, fewer than 10 bars of the wrong length, no blank lines in the tune,
and one instrument per MIDI channel. A piece that fails any of these scores 0.</span></div>
<div class="feat-groups">${Object.entries(groups).map(([gname, fs]) => `<div><div class="sec-label">${esc(gname)}${g.weights?.[gname] != null ? ` Β· ${pct(g.weights[gname])}` : ""}</div>
${fs.map((f) => `<div class="feat"><b>${esc(f.label || f.name)}</b><span>${f.rule === "band" ? "within the human range" : f.rule === "high" ? "the higher the better" : "the lower the better"} Β· <code>${esc(f.name)}</code></span>
${f.band ? `<em>${fmtNum(f.band[0])} β ${fmtNum(f.band[1])}</em>` : ""}</div>`).join("")}</div>`).join("")}</div>`;
if (g.curves) body += `<div class="sec-label">How a feature scores</div><dl class="kv">${[["Within the human range", g.curves.band], ["The higher the better", g.curves.high],
["The lower the better", g.curves.low], ["Histogram similarity", g.histograms]].filter(([, x]) => x).map(([k, x]) => `<dt>${k}</dt><dd>${esc(x)}</dd>`).join("")}</dl>
<p class="muted xs" style="margin-top:8px">Group weights are the share of the 85% that the six groups make up together.</p>`;
}
if (g.not_scored) body += `<dl class="kv" style="margin-top:16px"><dt>Not scored</dt><dd>${esc(g.not_scored)}${/^Never/.test(g.not_scored) ? "" : " A rollout that isn't scored has no reward, rather than 0."}</dd></dl>`;
body += `<p class="xs" style="margin-top:14px"><a href="/d/XiaomiMiMo/MiMo-V2.6-RL-oss?rewards=1">Every kind of task side by side, and how the rubrics are built across the dataset</a></p>`;
return body;
}
// ββ reward design, across the release ββββββββββββββββββββββββββββββββββββββββ
const VERL = "https://github.com/XiaomiMiMo/verl/tree/a2ad9f6";
const MIMOAGENT = "https://github.com/XiaomiMiMo/mimoagent/tree/467f0a1";
const KINDS = [
{ k: "code", d: "code", name: "Code", reward: "1 if the hidden tests pass, otherwise 0", range: "0 or 1",
by: "The task's hidden tests, applied after the agent finishes and run in its own image", judge: "β",
ns: "The tests can't be applied, or the files they touch can't be reset", train: "Same" },
{ k: "cyber", d: "cyber", name: "Cyber", reward: "1 if the last proof of concept crashes in the expected function, otherwise 0", range: "0 or 1",
by: "A root-owned server runs the real fuzz target on it, under the sanitizer", judge: "β", ns: "β", train: "Same" },
{ k: "general-workplace", d: "general", name: "General, workplace", reward: "Ξ£ weight Γ score Γ· Ξ£ weight, over the task's rubric checks", range: "0 to 1",
by: "Code checks on the systems' databases and the workspace, and a text judge: one 0-or-1 call per check", judge: "text",
ns: "The judge can't be reached, or one of the task's systems stops responding", train: "The same verify.py; it first looks for Xiaomi's internal grader, not released" },
{ k: "general-terminal", d: "general", name: "General, terminal", reward: "1 if every test passes, otherwise 0", range: "0 or 1",
by: "The task's pytest suite, copied in after the agent finishes", judge: "β",
ns: "The tests crash before grading, or write no reward", train: "No reference harness in the release; follows the Terminal-Bench convention" },
{ k: "webdev", d: "webdev", name: "Webdev", reward: "mean(visual, brief fulfilment, asset quality); visual is the mean of five criteria", range: "0 to 1",
by: "A vision judge scores a full-page render against a five-band scale per criterion", judge: "vision",
ns: "The page can't be rendered, or the judge can't be reached", train: "Different: a relative pick among up to 8 sibling rollouts, minus a deduction for missing the brief" },
{ k: "music", d: "music", name: "Music", reward: "(0.85 Γ weighted feature groups + 0.15 Γ histogram similarity) Γ· 100", range: "0 to 1",
by: "abc2midi, then 18 features compared with the range of human music", judge: "β",
ns: "Never; a piece that can't be read or measured scores 0", train: "Same" },
];
function rewards({ kinds, stats }, ctx) {
const share = (x, of) => (of ? `${Math.round((x / of) * 100)}%` : "β");
const n = (x) => fmt.format(x || 0);
const g = stats.general;
const rows = KINDS.map((k) => {
const c = kinds[k.k] || { count: 0 };
return `<tr><td><span class="rw-dom">${icon(DOMAIN_ICON[k.d], 13)}${esc(k.name)}</span>${c.example ? `<br><a class="xs" href="/t/${enc(ctx.spec)}/${encodeURIComponent(c.example)}">an example</a>` : ""}</td>
<td class="num nw">${n(c.count)}</td><td>${esc(k.reward)}</td><td class="nw">${esc(k.range)}</td><td>${esc(k.by)}</td><td class="nw">${esc(k.judge)}</td>
<td>${esc(k.ns)}</td><td>${esc(k.train)}</td></tr>`;
}).join("");
const m = g.method || {}, t = g.tier || {}, mix = g.mix || {}, inp = g.input || {}, act = g.act || {};
const judged = m.llm || 0;
const facts = [
["Checks", `${n(g.checks)} across ${n(g.tasks)} tasks, ${g.per_task.min} to ${g.per_task.max} per task (median ${g.per_task.median})`],
["Judged by a model", `${n(judged)} (${share(judged, g.checks)}), each in its own call, answered 0 or 1`],
["Checked by code", `${n(m.rule)} (${share(m.rule, g.checks)}), scored 0 to 1 against the databases and the workspace`],
["Tasks", `${n(mix.judge_only)} graded by the judge alone, ${n(mix.both)} by code and the judge, ${n(mix.code_only)} by code alone`],
["Judge's share of a task's reward", `median ${share(g.judge_share_median, 1)}`],
["Tiers", `critical ${n(t.critical)} (${share(t.critical, g.checks)}), important ${n(t.important)} (${share(t.important, g.checks)}), sanity ${n(t.sanity)} (${share(t.sanity, g.checks)}). A label only: tiers don't enter the formula`],
["Weights", `set in ${n(g.weighted_tasks)} tasks; the rest weight every check equally (a check without a weight counts 1)`],
...(g.mixed_weights ? [["Mixed weights", `${n(g.mixed_weights)} tasks give some checks weights that add up to about 1 and leave others without one, which count 1 each.
In ${n(g.mixed_heavy)} of them the unweighted checks decide 40% or more of the reward`]] : []),
["Facts come from", `the systems' databases in ${n(inp.db)} tasks, the workspace files in ${n(inp.workspace)}, both in ${n(inp.both)}`],
["The agent must", `change nothing in ${n(act.none)} tasks (its answer is graded), update databases in ${n(act.mutate_db)}, edit workspace files in ${n(act.edit_workspace)}, both in ${n(act.both)}`],
["What the judge reads", (g.judged_files || []).map(([f, c]) => `${esc(f)} for ${n(c)} checks`).join(", ") + ", as extracted text, up to 20,000 characters per file"],
["Gate and tamper checks", `supported by verify.py, used by ${n(g.uses_gates)} tasks`],
];
return `<div class="sec-label">By kind of task</div>
<div class="tbl rw-tbl"><table><thead><tr><th>Task</th><th class="num nw">Tasks</th><th>Reward</th><th class="nw">Range</th><th>Graded by</th><th class="nw">Judge</th>
<th>Not scored when</th><th>In training</th></tr></thead><tbody>${rows}</tbody></table></div>
<div class="sec-label" style="margin-top:18px">General rubrics, across the dataset <span class="faint">counted from every task's verifier_meta.json</span></div>
<dl class="kv">${facts.map(([k, x]) => `<dt>${esc(k)}</dt><dd>${x}</dd>`).join("")}</dl>
<div class="sec-label" style="margin-top:18px">Everywhere</div>
<ul class="rules" style="margin:0">
<li>A failure that isn't the model's (the testbed, the render, the judge) is not scored: the rollout has no reward, rather than 0. Xiaomi's harness masks these the same way.</li>
<li>Nothing that grades a task is in the sandbox while the agent works. Hidden tests, rubrics and the expected crash are uploaded after it finishes.</li>
<li>The judge prompts are sent in Chinese, unchanged, so scores stay comparable with Xiaomi's. Each task's Grading section has its prompt with an English translation.</li>
<li>Expected answers are never shown: not the rubric answers, not the code that checks them, not the Cyber proof of concept.</li>
<li>Graders follow <a href="${VERL}" target="_blank" rel="noopener">XiaomiMiMo/verl a2ad9f6</a> and <a href="${MIMOAGENT}" target="_blank" rel="noopener">mimoagent 467f0a1</a>; the vendored files are unmodified.</li>
</ul>`;
}
// ββ clicks: the prompt, previews, tables βββββββββββββββββββββββββββββββββββββ
export function wire(root, ctx) {
root.addEventListener("click", async (ev) => {
const fold = ev.target.closest("[data-pr-expand]");
if (fold) { fold.nextElementSibling.hidden = false; fold.remove(); return; }
const cp = ev.target.closest("[data-pr-copy]");
if (cp) {
const text = cp.closest(".bl-custom")?.querySelector("[data-pr-text]")?.dataset.prText || "";
try { await navigator.clipboard.writeText(text); toast("Message copied"); } catch { toast("Couldn't copy"); }
return;
}
const f = ev.target.closest(".bl-custom .file[data-path]");
if (f) return previewFile(ctx, f.dataset.path, f.dataset.kind);
const t = ev.target.closest(".bl-custom .tbl-btn");
if (t) return previewTable(ctx, t.dataset.sys, t.dataset.table, t.dataset.label);
});
}
const dataUrl = (ctx, part, params) => `/api/env/${enc(ctx.spec)}/data?${new URLSearchParams({ ref: ctx.ref, part, ...params })}`;
const rawUrl = (ctx, path) => `/api/env/${enc(ctx.spec)}/raw?${new URLSearchParams({ ref: ctx.ref, f: path })}`;
const previewSkeleton = (kind) => kind === "spreadsheet"
? `<div style="display:flex;gap:6px;margin-bottom:12px">${sk.box(28, "width:90px;border-radius:7px")}${sk.box(28, "width:70px;border-radius:7px")}</div>${sk.box(360, "border-radius:10px")}`
: `<div style="max-width:860px;margin:0 auto;display:grid;gap:10px">${sk.line(40, 18)}${sk.lines(100, 96, 90, 98, 70)}<div style="height:8px"></div>${sk.lines(94, 100, 88, 60)}</div>`;
async function previewFile(ctx, path, kind) {
const raw = rawUrl(ctx, path);
const body = openModal(path, previewSkeleton(kind), { raw, kind });
if (kind === "image") { body.innerHTML = `<div class="pv-img"><img src="${esc(raw)}" alt="${esc(path)}"></div>`; return; }
if (kind === "web") { body.innerHTML = `<iframe class="pv-frame" sandbox src="${esc(raw)}" title="${esc(path)}"></iframe>`; return; }
let p;
try { p = await progress.wrap(api(dataUrl(ctx, "preview", { path }))); }
catch (e) { body.innerHTML = emptyState("alert", "Couldn't open this file", esc(e.message)); return; }
if (!body.isConnected || $("#modal").hidden) return;
if (p.type === "sheets") {
body.innerHTML = `<div class="pv-tabs" role="tablist">${p.sheets.map((s, i) => `<button role="tab" data-i="${i}" aria-selected="${i === 0}">${icon("sheet", 13)}${esc(s.name)}${s.dims ? `<em>${esc(s.dims)}</em>` : ""}</button>`).join("")}</div><div id="pv-sheet"></div>`;
const show = (i) => {
$("#pv-sheet", body).innerHTML = sheetGrid(p.sheets[i].rows) + (p.sheets[i].rows.length >= 60 ? `<p class="pv-foot">Showing the first 60 rows and 24 columns. Open the original for the whole workbook.</p>` : "");
$$(".pv-tabs button", body).forEach((b) => b.setAttribute("aria-selected", String(b.dataset.i == i)));
};
body.querySelector(".pv-tabs").addEventListener("click", (e) => { const b = e.target.closest("button"); if (b) show(+b.dataset.i); });
show(0);
} else if (p.type === "document") {
body.innerHTML = `<div class="pv-doc prose">${p.blocks.map((b) => b.table ? table(b.table, { header: true }) : b.h ? `<h${Math.min(b.h + 2, 5)}>${esc(b.text)}</h${Math.min(b.h + 2, 5)}>` : `<p>${esc(b.text)}</p>`).join("")}</div>`;
} else if (p.type === "slides") {
body.innerHTML = `${p.repeated?.length ? `<div class="note-box" style="margin-bottom:14px">${icon("info")}<span>On every slide, not repeated below: ${p.repeated.map((t) => `<b>${esc(t)}</b>`).join(" Β· ")}</span></div>` : ""}
<div class="pv-slides">${p.slides.map((s) => `<article class="slide"><span class="n">${s.n}</span><div>
<b>${esc(s.title || "Untitled slide")}</b>${s.text.map((t) => `<p>${esc(t).replace(/\n/g, "<br>")}</p>`).join("")}
${(s.tables || []).map((t) => table(t)).join("")}</div></article>`).join("")}</div>`;
} else if (p.type === "pdf") {
body.innerHTML = `<div class="pv-tabs"><button data-m="view" aria-selected="true">${icon("doc", 13)}Document</button><button data-m="text" aria-selected="false">${icon("list", 13)}Text <em>${p.page_count} pages</em></button></div>
<div id="pv-pdf"><iframe class="pv-frame" src="${esc(raw)}" title="${esc(path)}"></iframe></div>`;
body.querySelector(".pv-tabs").addEventListener("click", (e) => {
const b = e.target.closest("button"); if (!b) return;
$$(".pv-tabs button", body).forEach((x) => x.setAttribute("aria-selected", String(x === b)));
$("#pv-pdf", body).innerHTML = b.dataset.m === "view" ? `<iframe class="pv-frame" src="${esc(raw)}" title="${esc(path)}"></iframe>`
: `<div class="pv-doc">${p.pages.map((pg) => `<div class="sec-label">Page ${pg.n}</div><pre class="code-block wrap">${esc(pg.text)}</pre>`).join("")}</div>`;
});
} else if (p.type === "text") body.innerHTML = `<pre class="code-block wrap" style="max-height:none">${esc(p.text)}</pre>`;
else if (p.type === "archive") body.innerHTML = table([["Entry", "Size"], ...p.entries.map((x) => [x.name, bytes(x.size)])]);
else if (p.type === "error") body.innerHTML = emptyState("alert", "Couldn't preview this file", esc(p.error), `<a class="btn" href="${esc(raw)}&download=1">Download</a>`);
else body.innerHTML = emptyState("file", "No preview for this file type", "", `<a class="btn" href="${esc(raw)}&download=1">Download</a>`);
}
async function previewTable(ctx, sys, tbl, label) {
const body = openModal(`${label || sys} Β· ${tbl}`, sk.box(420, "border-radius:10px"), { kind: "table" });
try {
const d = await progress.wrap(api(dataUrl(ctx, "table", { system: sys, table: tbl, limit: 100 })));
if ($("#modal").hidden) return;
body.innerHTML = table([d.columns, ...d.rows.map((r) => r.map((c) => (c == null ? "" : String(c))))]) + `<p class="pv-foot">${d.rows.length >= 100 ? "First 100 rows" : `${d.rows.length} row${d.rows.length === 1 ? "" : "s"}`} Β· ${d.columns.length} columns Β· the state of this database when the agent starts</p>`;
} catch (e) { body.innerHTML = emptyState("alert", "Couldn't load this table", esc(e.message)); }
}
|