);
};
// Quiet byline above an AI response. Replaces the heavy "chip + verified" card-header
// from the previous draft so the design reads as a chatbot first.
const Byline = ({ agent, reason, filters }) => {
const [showFilters, setShowFilters] = useStateR(false);
const A = window.OMNI_DATA.AGENTS.find(a => a.id === agent);
return (
Asked {A.name}·{reason}
{showFilters && setShowFilters(false)}/>}
);
};
// Map our backend's agent short-name (registry.py) to the OMNI_DATA.AGENTS chip id.
const ROUTED_AGENT_TO_CHIP = {
mydashboard: 'mydash',
sales: 'sales',
otif: 'otif',
inventory: 'inventory',
ar: 'ar',
agri: 'agri',
orderbook: 'orderbook',
lost_tree: 'lost_tree',
};
const fmtCost = (c) => (c == null ? '—' : '$' + Number(c).toFixed(c < 0.01 ? 6 : 4));
const fmtTokens = (n) => (n == null ? '—' : Number(n).toLocaleString());
// Parse a $20,481,443.56 / 31.8% / 1,234 / **$69.75M** / -$1.44M style cell into a Number.
const parseNumericCell = (s) => {
if (s == null) return null;
// Strip markdown bold/emphasis, currency, commas, spaces, leading/trailing punct
let cleaned = String(s).replace(/\*+/g, '').replace(/[\s$,`_]/g, '').replace(/%$/, '');
if (!cleaned) return null;
// Handle magnitude suffixes: 12.5M, 3.4K, 1.2B, 4.1T
const m = cleaned.match(/^(-?\d+\.?\d*)(K|M|B|T|k|m|b|t)?$/);
if (!m) {
const n = Number(cleaned);
return Number.isFinite(n) ? n : null;
}
const num = Number(m[1]);
const suffix = (m[2] || '').toLowerCase();
const mult = { k: 1e3, m: 1e6, b: 1e9, t: 1e12 }[suffix] || 1;
return Number.isFinite(num) ? num * mult : null;
};
// Pull the first markdown table whose last column is numeric and use it as a chart.
const extractChartFromMarkdown = (md) => {
if (!md || !window.marked) return null;
try {
const tokens = window.marked.lexer(md);
for (const tok of tokens) {
if (tok.type !== 'table') continue;
const headers = tok.header.map(h => h.text);
const rows = tok.rows.map(r => r.map(c => c.text));
if (rows.length < 2 || headers.length < 2) continue;
// Find a numeric column — prefer last col, fallback to scanning all
let valCol = -1;
for (let c = headers.length - 1; c >= 1; c--) {
const nums = rows.map(r => parseNumericCell(r[c])).filter(v => v != null);
if (nums.length >= rows.length - 1) { valCol = c; break; }
}
if (valCol === -1) continue;
const stripMd = (s) => String(s || '').replace(/\*+/g, '').replace(/`+/g, '').trim();
const series = rows
.map(r => ({ lbl: stripMd(r[0]), v: parseNumericCell(r[valCol]) }))
.filter(p => p.v != null && p.lbl);
// Skip if last row is a "Total" row — that's a summary, not a category
const filtered = series[series.length - 1]?.lbl.toLowerCase().includes('total')
? series.slice(0, -1)
: series;
if (filtered.length < 2 || filtered.length > 12) continue;
return { title: stripMd(headers[valCol]) + ' by ' + stripMd(headers[0]), series: filtered };
}
} catch (e) { /* swallow */ }
return null;
};
const AutoChart = ({ data }) => {
if (!data || !data.series || data.series.length === 0) return null;
const max = Math.max(...data.series.map(p => Math.abs(p.v)));
// Detect if title hints at currency or percentage
const isPct = /(%|percent|rate|share|ratio)/i.test(data.title);
const fmt = (n) => {
if (isPct) return n.toFixed(1) + '%';
if (Math.abs(n) >= 1_000_000) return '$' + (n / 1_000_000).toFixed(1) + 'M';
if (Math.abs(n) >= 1_000) return '$' + (n / 1_000).toFixed(1) + 'K';
return '$' + n.toLocaleString();
};
return (