Add bot graphics

Bots can now show a concept as a visual: create_graphic (with
update_graphic, read_graphic and list_graphics, on every run, no computer
needed) stores one HTML document in bot_graphics, per user like the
conversation, and the chat renders its [graphic:ID] ref as an inline card
sized to fit, with a large view and a page of its own (no rail).

Graphic HTML is treated as hostile: it is only served at /graphics/<id>
with a CSP sandbox allowing scripts under an origin of its own and no
network, loading only the vendored Chart.js and Mermaid from
/graphics-lib/. The document carries the app's theme vars and scrollbars,
reports its height to the thread, and a frame that navigates away is
closed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-27 08:11:15 +01:00
co-authored by Claude Opus 5.5
parent 51fd4d5538
commit bea656ade8
65 changed files with 1959 additions and 21 deletions
+58
View File
@@ -0,0 +1,58 @@
import { describe, expect, test } from "bun:test";
import graphicDocument from "./graphic-document";
const vars = {
light: { "--c-paper": "#fff" },
dark: { "--c-paper": "#000" },
fonts: { "--font-sans": "sans-serif" },
};
describe("graphicDocument", () => {
test("wraps a fragment in a themed document", () => {
const doc = graphicDocument({ html: "<svg></svg>", title: "A <b>", vars });
expect(doc.startsWith("<!DOCTYPE html><html><head>")).toBe(true);
expect(doc).toContain("<title>A &lt;b&gt;</title>");
expect(doc).toContain("--c-paper:#fff;");
expect(doc).toContain(":root.dark{color-scheme:dark;--c-paper:#000;}");
expect(doc).toContain("<body><svg></svg><script>");
expect(doc).toContain("cobble:graphic-size");
});
test("styles scrollbars in the theme's colours", () => {
const doc = graphicDocument({ html: "<p>x</p>", title: "T", vars });
expect(doc).toContain("::-webkit-scrollbar-thumb{background-color:var(--c-border-strong);");
expect(doc).toContain("scrollbar-color:var(--c-border-strong) transparent;");
});
test("puts the theme ahead of a whole document's own styles", () => {
const doc = graphicDocument({
html: `<!doctype html><html lang="en"><head><style>body{color:red}</style></head><body><p>Hi</p></body></html>`,
title: "T",
dark: true,
vars,
});
expect(doc.indexOf("--c-paper")).toBeLessThan(doc.indexOf("body{color:red}"));
expect(doc).toContain(`classList.toggle("dark", true)`);
expect(doc.indexOf("cobble:graphic-size")).toBeLessThan(doc.lastIndexOf("</body>"));
expect(doc.match(/<!doctype/gi)?.length).toBe(1);
});
test("gives a document without a head one", () => {
const doc = graphicDocument({ html: `<html><body>x</body></html>`, title: "T", vars });
expect(doc).toContain(`<html><head><meta charset="utf-8">`);
});
test("drops a refresh that would navigate the frame away", () => {
const doc = graphicDocument({
html: `<head><meta http-equiv="refresh" content="0;url=https://evil.test/?d=1"></head>`,
title: "T",
vars,
});
expect(doc).not.toContain("evil.test");
});
});