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]>
93 lines
3.0 KiB
TypeScript
93 lines
3.0 KiB
TypeScript
type Params = {
|
|
text: string;
|
|
/**
|
|
* Names by id; a ref to anything missing reads "a bot" / "a workspace".
|
|
*/
|
|
bot_names: Record<number, string>;
|
|
workspace_names: Record<number, string>;
|
|
/**
|
|
* How many levels the message's own headings drop, so they sit under
|
|
* the heading of the message they are in. Capped at `######`.
|
|
*/
|
|
heading_offset: number;
|
|
};
|
|
|
|
/**
|
|
* A message's markdown as it reads outside the app: its refs in words
|
|
* (`[bot:3]` as `@Ada`, `[workspace:2]` as `#Launch`, `[computer:3]` as
|
|
* "computer 3", a folder as its path, a graphic and a secret by number only, never
|
|
* a secret's value) and its headings dropped by
|
|
* `heading_offset`. Code blocks are left exactly as they are.
|
|
*/
|
|
export default function exportMessageText({
|
|
text,
|
|
bot_names,
|
|
workspace_names,
|
|
heading_offset,
|
|
}: Params) {
|
|
let fence: string | null = null;
|
|
|
|
return text
|
|
.split("\n")
|
|
.map((line) => {
|
|
const marker = line.match(/^\s{0,3}(`{3,}|~{3,})/)?.[1];
|
|
|
|
if (fence) {
|
|
if (marker && marker[0] == fence[0] && marker.length >= fence.length) {
|
|
fence = null;
|
|
}
|
|
|
|
return line;
|
|
}
|
|
|
|
if (marker) {
|
|
fence = marker;
|
|
return line;
|
|
}
|
|
|
|
return wordRefs({ line, bot_names, workspace_names }).replace(
|
|
/^(\s{0,3})(#{1,6})(?=\s|$)/,
|
|
(_, indent: string, hashes: string) =>
|
|
`${indent}${"#".repeat(Math.min(6, hashes.length + heading_offset))}`,
|
|
);
|
|
})
|
|
.join("\n")
|
|
.trim();
|
|
}
|
|
|
|
/**
|
|
* One line's refs in words. Refs that are already markdown links
|
|
* (`[bot:3](...)`) are left alone, as the chat leaves them.
|
|
*/
|
|
function wordRefs({
|
|
line,
|
|
bot_names,
|
|
workspace_names,
|
|
}: {
|
|
line: string;
|
|
bot_names: Record<number, string>;
|
|
workspace_names: Record<number, string>;
|
|
}) {
|
|
return line
|
|
.replace(/\[bot:(\d+)\](?!\()/g, (_, id: string) => {
|
|
const name = bot_names[Number(id)];
|
|
|
|
return name ? `@${name}` : "a bot";
|
|
})
|
|
.replace(/\[workspace:(\d+)\](?!\()/g, (_, id: string) => {
|
|
const name = workspace_names[Number(id)];
|
|
|
|
return name ? `#${name}` : "a workspace";
|
|
})
|
|
.replace(/\[computer:(\d+)\](?!\()/g, "computer $1")
|
|
.replace(/\[take-over:(\d+):([a-z]+)\](?!\()/g, "(take over computer $1, $2)")
|
|
.replace(/\[proposal:(\d+)\](?!\()/g, "proposal $1")
|
|
.replace(/\[hook-proposal:(\d+)\](?!\()/g, "hook proposal $1")
|
|
.replace(/\[secret:(\d+)\](?!\()/g, "(secret $1, hidden)")
|
|
.replace(/\[secret-request:(\d+)\](?!\()/g, "(secret request $1)")
|
|
.replace(/\[graphic:(\d+)\](?!\()/g, "(graphic $1)")
|
|
.replace(/\[file:(\d+)\](?!\()/g, "file $1")
|
|
.replace(/\[folder:[a-z]+:\d+:([^\]\n]+)\](?!\()/g, "`$1`")
|
|
.replace(/\[folder:([a-z]+):(\d+)\](?!\()/g, "the $1 $2 folder");
|
|
}
|