// Show any JSON value well: images and audio carried as base64 or URLs, chat transcripts, tables for lists of
// records, long text as text, nested objects as key/value rows that fold. Used for environment observations, Task
// API tasks and the rows of custom environments. Everything is escaped; media only from data: or https: URLs.
import { esc, fmt, md } from "./util.js";
const IMG_KEY = /(image|img|png|jpe?g|frame|screenshot|pixels|photo|thumbnail|render)/i;
const AUDIO_KEY = /(audio|wav|speech|sound|waveform)/i;
const B64 = /^[A-Za-z0-9+/=\s]+$/;
const IMG_URL = /^https:\/\/[^\s"'<>]+\.(png|jpe?g|gif|webp|svg|avif)(\?[^\s"'<>]*)?$/i;
const AUDIO_URL = /^https:\/\/[^\s"'<>]+\.(wav|mp3|ogg|flac|m4a)(\?[^\s"'<>]*)?$/i;
function mediaOf(key, v, ctx) {
if (typeof v !== "string" || v.length < 16) return null;
if (key === "media_url" && /^https:\/\/[^\s"'<>]+$/.test(v)) {
if (/^audio\/[\w.+-]+$/.test(ctx.mime || "")) return ``;
if (/^image\/(png|jpeg|gif|webp|avif)$/.test(ctx.mime || "")) return ``;
if (/^video\/(mp4|webm|ogg)$/.test(ctx.mime || "")) return ``;
}
if (v.startsWith("data:image/") && /^data:image\/(png|jpe?g|gif|webp);base64,/.test(v)) return `
`;
if (v.startsWith("data:audio/") && /^data:audio\/[\w.+-]+;base64,/.test(v)) return ``;
if (IMG_URL.test(v)) return `
`;
if (AUDIO_URL.test(v)) return ``;
if (v.length > 200 && B64.test(v.slice(0, 400))) {
if (IMG_KEY.test(key)) {
const fmtName = String(ctx.image_format || ctx.format || "png").toLowerCase().replace("jpg", "jpeg");
return `
`;
}
if (AUDIO_KEY.test(key)) return ``;
return `base64, ${fmt.format(Math.round(v.length * 0.75 / 1024))} KB`;
}
return null;
}
const isMessage = (x) => x && typeof x === "object" && !Array.isArray(x) && typeof x.role === "string" && ("content" in x || "tool_calls" in x);
const scalar = (x) => x == null || ["string", "number", "boolean"].includes(typeof x);
function text(s, key) {
if (s === "") return `empty`;
if (s.length > 160 || s.includes("\n")) {
const looksMd = /(^|\n)(#{1,4} |[-*] |\d+\. |```)/.test(s);
return looksMd && s.length < 40_000 ? `
${esc(s.length > 60_000 ? s.slice(0, 60_000) + "\n… (cut)" : s)}`;
}
return `${esc(s)}`;
}
// a long message shows its first lines and a "Show all" toggle (a checkbox: no script needed in a rendered string)
let clipN = 0;
const clip = (html, len) => (len < 1500 ? html : `${esc(typeof c.function?.arguments === "string" ? c.function.arguments.slice(0, 2000) : JSON.stringify(c.arguments ?? c.function?.arguments ?? {}).slice(0, 2000))}and ${fmt.format(msgs.length - 200)} more messages
` : ""}| ${esc(k)} | `).join("")}
|---|
| ${scalar(r[k]) ? cell(r[k]) : value(r[k], k, depth + 1)} | `).join("")}
${fmt.format(rows.length - 100)} more rows
` : ""}${esc(String(x))}`);
export function value(v, key = "", depth = 0, ctx = {}) {
if (depth > 7) return `…`;
if (v == null) return `null`;
if (typeof v === "boolean") return `${v}`;
if (typeof v === "number") return `${esc(Number.isInteger(v) ? fmt.format(v) : String(+v.toFixed(6)))}`;
if (typeof v === "string") return mediaOf(key, v, ctx) || text(v, key);
if (Array.isArray(v)) {
if (!v.length) return `empty list`;
if (v.every(isMessage)) return chat(v, depth);
if (v.every(scalar) && v.length <= 60 && v.every((x) => String(x).length < 60)) return `${v.map((x) => `${esc(String(x))}`).join("")}`;
if (v.length > 1 && v.every((x) => x && typeof x === "object" && !Array.isArray(x)) && v.slice(0, 20).every((x) => Object.values(x).filter((y) => !scalar(y)).length <= 1)) return table(v, depth);
return `