// Rows of RL training data (NeMo Gym, Verifiers, verl / SkyRL; app/envs/convo.py reads them): the conversation as the // policy gets it, and the tools it may call. Text is shown whole and escaped; long parts (a system prompt, reasoning, a // tool's output) fold under a line that says how long they are, so nothing scrolls inside the page. // transcript {turns: [{kind: message|reasoning|call|output|other, role, text, name?, args?, output?, images?}], // next?: {label, text}, note?} // tools {tools: [{name, kind, description, built_in, params: [{name, type, required, description, enum?, // default?, params?}]}], open?} // fields {rows: [{key, text, code}]}: a row's other fields, label over value; long values fold import { esc, fmt, md } from "../util.js"; import { icon } from "../icons.js"; export function render(kind, data) { return ({ transcript, tools, fields }[kind] || (() => ""))(data || {}); } const LONG = 1400, TALL = 18; // past this many characters or lines, a system prompt, reasoning or tool output folds const long = (s) => s.length > LONG || (s.match(/\n/g) || []).length > TALL; const chars = (s) => `${fmt.format(s.length)} characters`; const ROLE = { system: "System", developer: "Developer", user: "User", assistant: "Assistant", tool: "Tool", other: "Other" }; const text = (s, cls = "") => (s ? `
${esc(s)}
` : `

empty

`); const images = (list) => (list || []).map((u) => `an image in the conversation`).join(""); const peek = (s) => esc(s.replace(/\s+/g, " ").trim().slice(0, 140)); function fold(label, meta, inner, open = false) { return `
${icon("chevronRight", 13, "chev")}${label}${meta}
${inner}
`; } // ── the conversation ───────────────────────────────────────────────────────── function part(t) { if (t.kind === "reasoning") return fold("Reasoning", t.text ? `${chars(t.text)} · ${peek(t.text)}` : "empty", text(t.text)); if (t.kind === "call") { const args = t.args && t.args !== "{}" ? `
${esc(t.args)}
` : `

no arguments

`; const out = "output" in t ? (long(t.output || "") ? fold("Output", `${chars(t.output)} · ${peek(t.output)}`, `
${esc(t.output)}
${images(t.images)}`) : `
Output
${t.output ? `
${esc(t.output)}
` : `

empty

`}${images(t.images)}`) : ""; return `
${icon("wrench", 12)}Calls ${esc(t.name)}
${args}${out}
`; } if (t.kind === "output") { const body = `
${esc(t.text)}
${images(t.images)}`; return long(t.text || "") ? fold("Output", `${chars(t.text)} · ${peek(t.text)}`, body) : body; } if (t.kind === "other") return `
${esc(t.text)}
`; if (t.role === "system" || t.role === "developer") { return t.text && long(t.text) ? fold(`${ROLE[t.role]} prompt`, chars(t.text), text(t.text)) + images(t.images) : text(t.text) + images(t.images); } // the assistant writes Markdown; what it's given is shown as it is if (t.role === "assistant" && t.text) return `
${md(t.text)}
${images(t.images)}`; return text(t.text) + images(t.images); } const KEEP = 4; // a long conversation shows its opening and its last turns; the middle folds function turnHtml(g) { return `
  • ${esc(ROLE[g.side] || g.side)}${g.name ? ` ${esc(g.name)}` : ""}
    ${g.items.map(part).join("")}
  • `; } function tally(groups) { const items = groups.flatMap((g) => g.items); const n = (f) => items.filter(f).length; return [[n((t) => t.kind === "message" && t.role === "user"), "user message"], [n((t) => t.kind === "message" && t.role === "assistant"), "assistant message"], [n((t) => t.kind === "call"), "tool call"]].filter(([k]) => k).map(([k, w]) => `${fmt.format(k)} ${w}${k === 1 ? "" : "s"}`).join(", "); } function transcript(d) { const ts = d.turns || []; // consecutive turns of one side (the assistant's reasoning, words and tool calls) are one block const groups = []; ts.forEach((t) => { const side = t.kind === "output" ? "tool" : t.kind === "other" ? "other" : t.role; const g = groups[groups.length - 1]; if (g && g.side === side && side === "assistant") g.items.push(t); else groups.push({ side, name: t.name && t.kind === "message" ? t.name : "", items: [t] }); }); const n = (k, role) => ts.filter((t) => t.kind === k && (!role || t.role === role)).length; const counts = [[n("message", "user"), "user message"], [n("message", "assistant"), "assistant message"], [n("call"), "tool call"], [n("reasoning"), "reasoning step"]] .filter(([c]) => c).map(([c, w]) => `${fmt.format(c)} ${w}${c === 1 ? "" : "s"}`).join(" · "); // the opening (system prompt and the first request) and the last few turns stay open; a long middle folds const firstUser = groups.findIndex((g) => g.side === "user"); const head = firstUser < 0 ? 1 : firstUser + 1; const tailFrom = Math.max(head, groups.length - KEEP); const middle = groups.slice(head, tailFrom); const body = middle.length >= 3 ? [...groups.slice(0, head).map(turnHtml), `
  • ${icon("chevronRight", 13, "chev")}${fmt.format(middle.length)} earlier turns${tally(middle)}
      ${middle.map(turnHtml).join("")}
  • `, ...groups.slice(tailFrom).map(turnHtml)] : groups.map(turnHtml); return `${d.note ? `

    ${esc(d.note)}

    ` : ""} ${counts && ts.length > 2 ? `

    ${counts}

    ` : ""}
      ${body.join("")} ${d.next ? `
    1. ${esc(d.next.label || "Next")}

      ${esc(d.next.text || "")}

    2. ` : ""}
    `; } // ── the tools ──────────────────────────────────────────────────────────────── const sentence = (s) => { const m = String(s || "").replace(/\s+/g, " ").match(/^.{0,160}?[.!?](\s|$)/); return m ? m[0].trim() : String(s || "").replace(/\s+/g, " ").slice(0, 160); }; function params(list) { return `
    ${list.map((p) => `
    ${esc(p.name)}${p.required ? `required` : ""}
    ${p.type ? `${esc(p.type)}` : ""}${p.description ? ` ${esc(p.description)}` : ""} ${p.enum ? `
    One of ${p.enum.map((x) => `${esc(x)}`).join(" ")}
    ` : ""} ${p.default != null ? `
    Default ${esc(p.default)}
    ` : ""} ${p.params?.length ? params(p.params) : ""}
    `).join("")}
    `; } function tools(d) { const list = d.tools || []; if (!list.length) return `

    No tools: the policy answers in words only.

    `; const open = d.open ?? list.length <= 3; return `
    ${list.map((t) => { const sig = t.built_in ? "" : `(${t.params.map((p) => esc(p.name) + (p.required ? "" : "?")).join(", ")})`; return `
    ${icon("chevronRight", 13, "chev")}${esc(t.name)}${sig} ${t.built_in ? `built-in ${esc(t.kind)} tool` : esc(sentence(t.description))}
    ${t.description ? `

    ${esc(t.description)}

    ` : ""}${t.params.length ? params(t.params) : `

    no parameters

    `}
    `; }).join("")}
    `; } // ── a row's other fields ───────────────────────────────────────────────────── function fields(d) { const rows = d.rows || []; if (!rows.length) return ""; const val = (r) => { const body = r.code ? `
    ${esc(r.text)}
    ` : text(r.text); if (long(r.text)) return fold(r.code ? "Show" : "Show all", `${chars(r.text)} · ${peek(r.text)}`, body); return r.code && !r.text.includes("\n") ? `${esc(r.text)}` : body; }; return `
    ${rows.map((r) => `
    ${esc(r.key)}
    ${val(r)}
    `).join("")}
    `; }