@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
155 lines • 7.12 kB
JavaScript
const PALETTE = ['#2563eb', '#16a34a', '#f59e0b', '#dc2626', '#7c3aed', '#0891b2'];
function escapeXml(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
function cell(value) {
return value === null || value === undefined ? '' : String(value);
}
/** A round axis maximum: 1, 2, 2.5 or 5 times a power of ten, at least `value`. */
function niceMax(value) {
if (value <= 0)
return 1;
const power = 10 ** Math.floor(Math.log10(value));
for (const step of [1, 2, 2.5, 5, 10])
if (value <= step * power)
return step * power;
return 10 * power;
}
function formatNumber(value) {
if (Math.abs(value) >= 1_000_000)
return `${+(value / 1_000_000).toFixed(1)}M`;
if (Math.abs(value) >= 10_000)
return `${+(value / 1000).toFixed(1)}k`;
return `${+value.toFixed(2)}`;
}
/** The builtin `Chart` as an SVG document: axes, gridlines, bars or lines, a legend for 2+ series. */
export function chartSvg(props, options = {}) {
const width = options.width ?? 800;
const height = options.height ?? 450;
const colors = options.colors ?? PALETTE;
const fg = options.foreground ?? '#1f2328';
const font = escapeXml(options.fontFamily ??
"Inter, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'DejaVu Sans', 'Liberation Sans', sans-serif");
const series = (props.series ?? []).slice(0, 6);
const data = (props.data ?? []).slice(0, 60);
const value = (point, key) => {
const number = Number(point[key]);
return Number.isFinite(number) ? number : 0;
};
const top = props.title ? 56 : 24;
const legend = series.length > 1 ? 28 : 0;
const left = 64;
const right = 24;
const bottom = 56 + legend;
const plotW = width - left - right;
const plotH = height - top - bottom;
const values = data.flatMap((point) => series.map((each) => value(point, each.key)));
const max = niceMax(Math.max(0, ...values));
const min = Math.min(0, ...values);
const span = max - min || 1;
const y = (v) => top + plotH - ((v - min) / span) * plotH;
const out = [
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" font-family="${font}">`,
`<rect width="100%" height="100%" fill="${escapeXml(options.background ?? '#ffffff')}"/>`,
];
if (props.title) {
out.push(`<text x="${left}" y="34" font-size="26" font-weight="600" fill="${fg}">${escapeXml(props.title)}</text>`);
}
for (let i = 0; i <= 4; i++) {
const v = min + (span * i) / 4;
const gy = y(v);
out.push(`<line x1="${left}" x2="${left + plotW}" y1="${gy}" y2="${gy}" stroke="${fg}" stroke-opacity="0.12"/>`, `<text x="${left - 8}" y="${gy + 4}" font-size="15" text-anchor="end" fill="${fg}" fill-opacity="0.7">${escapeXml(formatNumber(v))}${props.unit ? escapeXml(` ${props.unit}`) : ''}</text>`);
}
const n = Math.max(1, data.length);
const slot = plotW / n;
const labelEvery = Math.max(1, Math.ceil(n / 12));
data.forEach((point, index) => {
if (index % labelEvery !== 0)
return;
const label = cell(point[props.xKey]).slice(0, 14);
out.push(`<text x="${left + slot * index + slot / 2}" y="${top + plotH + 20}" font-size="15" text-anchor="middle" fill="${fg}" fill-opacity="0.8">${escapeXml(label)}</text>`);
});
if (props.type === 'line') {
series.forEach((each, s) => {
const points = data.map((point, index) => `${left + slot * index + slot / 2},${y(value(point, each.key))}`);
const color = colors[s % colors.length];
out.push(`<polyline fill="none" stroke="${color}" stroke-width="3" stroke-linejoin="round" points="${points.join(' ')}"/>`);
for (const p of points) {
const [cx, cy] = p.split(',');
out.push(`<circle cx="${cx}" cy="${cy}" r="3.5" fill="${color}"/>`);
}
});
}
else {
const groupW = slot * 0.75;
const barW = groupW / Math.max(1, series.length);
data.forEach((point, index) => {
series.forEach((each, s) => {
const v = value(point, each.key);
const x = left + slot * index + (slot - groupW) / 2 + barW * s;
const y0 = y(Math.max(0, min));
const y1 = y(v);
out.push(`<rect x="${x}" y="${Math.min(y0, y1)}" width="${Math.max(1, barW - 2)}" height="${Math.abs(y0 - y1)}" rx="2" fill="${colors[s % colors.length]}"/>`);
});
});
}
out.push(`<line x1="${left}" x2="${left + plotW}" y1="${y(Math.max(0, min))}" y2="${y(Math.max(0, min))}" stroke="${fg}" stroke-opacity="0.4"/>`);
if (legend > 0) {
let x = left;
const ly = height - 18;
series.forEach((each, s) => {
const label = escapeXml(each.label || each.key);
out.push(`<rect x="${x}" y="${ly - 10}" width="12" height="12" rx="2" fill="${colors[s % colors.length]}"/>`, `<text x="${x + 18}" y="${ly}" font-size="15" fill="${fg}">${label}</text>`);
x += 34 + label.length * 9;
});
}
out.push('</svg>');
return out.join('');
}
let resvg;
async function loadResvg() {
const id = '@resvg/resvg-js';
resvg ??= import(/* @vite-ignore */ id).catch((error) => {
resvg = undefined;
throw new Error(`genui: chartImages() needs the optional peer dependency @resvg/resvg-js (npm i @resvg/resvg-js): ${error instanceof Error ? error.message : String(error)}`);
});
return resvg;
}
/** SVG → PNG bytes, with `@resvg/resvg-js`. */
export async function svgToPng(svg, options = {}) {
const { Resvg } = await loadResvg();
const renderer = new Resvg(svg, {
...(options.width !== undefined ? { fitTo: { mode: 'width', value: options.width } } : {}),
font: {
loadSystemFonts: true,
...(options.fontFiles !== undefined ? { fontFiles: [...options.fontFiles] } : {}),
},
});
return renderer.render().asPng();
}
/**
* Charts as PNG images on a channel (`genui({ channels: { whatsapp: { chartImages: chartImages() } } })`).
* A chart the renderer cannot draw (resvg missing, bad props) falls back to its text summary.
*/
export function chartImages(options = {}) {
const components = Object.freeze([...(options.components ?? ['Chart'])]);
return {
components,
async render(component, props) {
if (!components.includes(component))
return null;
const chart = props;
if (!Array.isArray(chart.data) || !Array.isArray(chart.series) || chart.data.length === 0)
return null;
const png = await svgToPng(chartSvg(chart, options), {
...(options.fontFiles !== undefined ? { fontFiles: options.fontFiles } : {}),
});
return { data: png, contentType: 'image/png' };
},
};
}
//# sourceMappingURL=chart-image.js.map