var setHtml = function(el, html) { if (el) el.innerHTML = html; };
var BASE = '';
var pages = ['overview','sessions','context','memory','skills','agents','scheduler','integrations','metrics','efficiency','recommendations','wallet','settings','workspace'];
var titles = { overview: 'Overview', sessions: 'Sessions', context: 'Context', memory: 'Memory', skills: 'Skills & Plugins', agents: 'Agents', scheduler: 'Scheduler', integrations: 'Integrations', metrics: 'Observability', efficiency: 'Prompt Performance', wallet: 'Wallet', settings: 'Settings', workspace: 'Workspace' };
function esc(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); }
function formatMemContent(raw) {
if (!raw) return '<span style="color:var(--muted)">(empty)</span>';
var s = String(raw).trim();
if ((s.charAt(0) === '{' || s.charAt(0) === '[') && s.length > 2) {
try {
var obj = JSON.parse(s);
return formatMemValue(obj, 0);
} catch(_) { }
}
return formatMemText(s);
}
function formatMemValue(val, depth) {
if (val === null || val === undefined) return '<span style="color:var(--muted)">null</span>';
if (typeof val === 'boolean') return '<span style="color:' + (val ? '#22c55e' : '#ef4444') + '">' + val + '</span>';
if (typeof val === 'number') return '<span style="color:var(--accent)">' + val + '</span>';
if (typeof val === 'string') {
if (val.length === 0) return '<span style="color:var(--muted)">(empty)</span>';
if ((val.charAt(0) === '{' || val.charAt(0) === '[') && val.length > 2) {
try { return formatMemValue(JSON.parse(val), depth); } catch(_) {}
}
var display = esc(val.length > 300 ? val.substring(0, 300) + '\u2026' : val);
return '<span style="color:var(--text);word-break:break-word">' + display.replace(/\n/g, '<br>') + '</span>';
}
if (Array.isArray(val)) {
if (val.length === 0) return '<span style="color:var(--muted)">(empty list)</span>';
if (val.every(function(v) { return typeof v !== 'object' || v === null; })) {
return '<span style="color:var(--text)">' + val.map(function(v) { return esc(String(v)); }).join(', ') + '</span>';
}
return '<div style="display:flex;flex-direction:column;gap:4px;margin-top:2px">' + val.map(function(item, i) {
return '<div style="padding:4px 6px;border-left:2px solid var(--border-ghost);margin-left:' + (depth * 8) + 'px">' + formatMemValue(item, depth + 1) + '</div>';
}).join('') + '</div>';
}
var keys = Object.keys(val);
if (keys.length === 0) return '<span style="color:var(--muted)">(empty object)</span>';
var indent = depth > 0 ? 'margin-left:' + (depth * 8) + 'px;' : '';
return '<table style="border-collapse:collapse;font-size:0.78rem;width:100%;' + indent + '">' + keys.map(function(k) {
return '<tr style="border-bottom:1px solid var(--border-ghost)">'
+ '<td style="padding:3px 10px 3px 0;color:var(--accent);font-family:var(--mono);white-space:nowrap;vertical-align:top;font-size:0.75rem">' + esc(k) + '</td>'
+ '<td style="padding:3px 0;color:var(--text);word-break:break-word">' + formatMemValue(val[k], depth + 1) + '</td>'
+ '</tr>';
}).join('') + '</table>';
}
function formatMemText(s) {
var segments = [];
var remaining = s;
while (remaining.length > 0) {
var jsonStart = -1;
for (var ci = 0; ci < remaining.length; ci++) {
var ch = remaining.charAt(ci);
if (ch === '{' || ch === '[') {
var depth2 = 0; var close = ch === '{' ? '}' : ']'; var inStr = false; var esc2 = false;
for (var j = ci; j < remaining.length; j++) {
var c2 = remaining.charAt(j);
if (esc2) { esc2 = false; continue; }
if (c2 === '\\' && inStr) { esc2 = true; continue; }
if (c2 === '"') { inStr = !inStr; continue; }
if (inStr) continue;
if (c2 === ch) depth2++;
if (c2 === close) { depth2--; if (depth2 === 0) {
var candidate = remaining.substring(ci, j + 1);
try { var parsed = JSON.parse(candidate); if (typeof parsed === 'object' && parsed !== null) { jsonStart = ci; break; } } catch(_) {}
break;
}}
}
if (jsonStart >= 0) break;
}
}
if (jsonStart < 0) { segments.push({ type: 'text', value: remaining }); break; }
if (jsonStart > 0) segments.push({ type: 'text', value: remaining.substring(0, jsonStart) });
var d3 = 0; var cl3 = remaining.charAt(jsonStart) === '{' ? '}' : ']'; var inS3 = false; var es3 = false; var end3 = jsonStart;
for (var k = jsonStart; k < remaining.length; k++) {
var c3 = remaining.charAt(k);
if (es3) { es3 = false; continue; }
if (c3 === '\\' && inS3) { es3 = true; continue; }
if (c3 === '"') { inS3 = !inS3; continue; }
if (inS3) continue;
if (c3 === remaining.charAt(jsonStart)) d3++;
if (c3 === cl3) { d3--; if (d3 === 0) { end3 = k + 1; break; } }
}
var jsonStr = remaining.substring(jsonStart, end3);
try { segments.push({ type: 'json', value: JSON.parse(jsonStr) }); } catch(_) { segments.push({ type: 'text', value: jsonStr }); }
remaining = remaining.substring(end3);
}
var html = '<div style="font-size:0.8rem;line-height:1.45">';
segments.forEach(function(seg) {
if (seg.type === 'json') {
html += '<div style="margin:6px 0;padding:6px 8px;border-left:2px solid var(--accent);background:rgba(255,255,255,0.02);border-radius:0 4px 4px 0">' + formatMemValue(seg.value, 0) + '</div>';
} else {
var text = esc(seg.value.trim());
if (text.length === 0) return;
text = text.replace(/\n/g, '<br>');
text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html += '<div>' + text + '</div>';
}
});
html += '</div>';
if (s.length > 1500) {
var uid = 'mem-expand-' + Math.random().toString(36).substring(2, 8);
return '<div>'
+ '<div id="' + uid + '-short" style="max-height:200px;overflow:hidden">' + html + '</div>'
+ '<span onclick="var sh=document.getElementById(\'' + uid + '-short\');var fl=document.getElementById(\'' + uid + '-full\');if(fl.style.display===\'none\'){fl.style.display=\'block\';sh.style.display=\'none\';this.textContent=\'\u25BC Show less\'}else{fl.style.display=\'none\';sh.style.display=\'block\';this.textContent=\'\u25B6 Show more\'}" style="font-size:0.72rem;color:var(--accent);cursor:pointer;display:inline-block;margin-top:4px">\u25B6 Show more</span>'
+ '<div id="' + uid + '-full" style="display:none">' + html + '</div>'
+ '</div>';
}
return html;
}
function uiIcon(name) {
switch (name) {
case 'refresh':
return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M13 5V2h-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/><path d="M13 2A6 6 0 1 0 14 8" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
case 'download':
return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 2v7" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><path d="M5.5 7.5L8 10l2.5-2.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 12.5h11" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
case 'spark':
return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 1.5l1.7 3.4 3.8.6-2.7 2.6.6 3.7L8 10.3l-3.4 1.8.6-3.7L2.5 5.5l3.8-.6L8 1.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>';
case 'power':
return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 1.8v5.2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M11.5 3.7a5 5 0 1 1-7 0" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
case 'trash':
return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M2.5 4h11" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><path d="M6 4V2.8h4V4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><path d="M4.2 4l.8 9h6l.8-9" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>';
case 'plus':
return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
case 'send':
return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M2 8l12-5-3.5 10-2.7-4.2L2 8z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>';
case 'search':
return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><circle cx="7" cy="7" r="4.5" stroke="currentColor" stroke-width="1.3"/><path d="M10.4 10.4L14 14" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>';
case 'back':
return '<svg class="ui-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M7 3L2.5 8 7 13" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 8h10" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>';
default:
return '';
}
}
function uiBtnLabel(iconName, label) { return uiIcon(iconName) + '<span>' + label + '</span>'; }
function copyIdBtn(id) { return '<button class="copy-id-btn" data-copy-id="' + esc(id) + '" title="Copy session ID"><svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><rect x="5" y="5" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1.3"/><path d="M3 11V3a1 1 0 011-1h8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg></button>'; }
function truncate(s, n) { return (s && s.length > n) ? s.slice(0, n) + '\u2026' : (s || ''); }
function clamp01(v) {
var n = Number(v);
if (!isFinite(n)) return 0;
if (n < 0) return 0;
if (n > 1) return 1;
return n;
}
function round2(v) {
return Math.round(Number(v || 0) * 100) / 100;
}
function normalizeRoutingProfile(profile, preferredKey) {
var p = {
correctness: clamp01(profile && profile.correctness),
cost: clamp01(profile && profile.cost),
speed: clamp01(profile && profile.speed)
};
var keys = ['correctness', 'cost', 'speed'];
var total = p.correctness + p.cost + p.speed;
if (total <= 1.0 + 1e-9) {
return { correctness: round2(p.correctness), cost: round2(p.cost), speed: round2(p.speed) };
}
if (preferredKey && keys.indexOf(preferredKey) !== -1) {
var otherSum = 0;
keys.forEach(function(k) { if (k !== preferredKey) otherSum += p[k]; });
p[preferredKey] = Math.max(0, Math.min(p[preferredKey], 1 - otherSum));
return { correctness: round2(p.correctness), cost: round2(p.cost), speed: round2(p.speed) };
}
if (total > 0) {
var scale = 1 / total;
p.correctness *= scale;
p.cost *= scale;
p.speed *= scale;
}
return { correctness: round2(p.correctness), cost: round2(p.cost), speed: round2(p.speed) };
}
function routingProfileTotal(profile) {
return round2(clamp01(profile && profile.correctness) + clamp01(profile && profile.cost) + clamp01(profile && profile.speed));
}
function deriveRoutingProfile(cfg) {
var correctness = clamp01(cfg && cfg.accuracy_floor);
var costWeight = cfg && cfg.cost_weight;
var cost = clamp01(costWeight == null ? (cfg && cfg.cost_aware ? 0.5 : 0.0) : costWeight);
var confidence = clamp01(cfg && cfg.confidence_threshold != null ? cfg.confidence_threshold : 0.9);
var speed = clamp01((0.95 - confidence) / 0.35);
return normalizeRoutingProfile({
correctness: round2(correctness),
cost: round2(cost),
speed: round2(speed)
});
}
function projectRoutingPatchFromProfile(profile) {
var p = {
correctness: clamp01(profile && profile.correctness),
cost: clamp01(profile && profile.cost),
speed: clamp01(profile && profile.speed)
};
return {
models: {
routing: {
accuracy_floor: round2(p.correctness),
cost_aware: p.cost > 0.01,
cost_weight: round2(p.cost),
confidence_threshold: round2(0.95 - (0.35 * p.speed)),
estimated_output_tokens: Math.max(200, Math.min(1200, Math.round(1200 - (800 * p.speed))))
}
}
};
}
function normalizeContextBudget(budget) {
var levels = ['L0', 'L1', 'L2', 'L3'];
var normalized = {
l0: Math.max(1000, parseInt(budget && budget.l0, 10) || 4000),
l1: Math.max(2000, parseInt(budget && budget.l1, 10) || 8000),
l2: Math.max(4000, parseInt(budget && budget.l2, 10) || 16000),
l3: Math.max(8000, parseInt(budget && budget.l3, 10) || 32000),
channel_minimum: String((budget && budget.channel_minimum) || 'L1').toUpperCase()
};
if (levels.indexOf(normalized.channel_minimum) === -1) normalized.channel_minimum = 'L1';
return normalized;
}
function renderRoutingSpiderSvg(profile) {
var values = [clamp01(profile.correctness), clamp01(profile.cost), clamp01(profile.speed)];
var labels = ['Correctness', 'Cost', 'Speed'];
var cx = 110, cy = 110, r = 78;
var grid = '';
[0.25, 0.5, 0.75, 1.0].forEach(function(level) {
var pts = [];
for (var i = 0; i < 3; i++) {
var angle = (-Math.PI / 2) + (i * (2 * Math.PI / 3));
pts.push((cx + Math.cos(angle) * r * level).toFixed(1) + ',' + (cy + Math.sin(angle) * r * level).toFixed(1));
}
grid += '<polygon points="' + pts.join(' ') + '" fill="none" stroke="var(--border)" stroke-width="1"/>';
});
var axes = '';
var labelEls = '';
var polyPts = [];
for (var i = 0; i < 3; i++) {
var ang = (-Math.PI / 2) + (i * (2 * Math.PI / 3));
var ox = cx + Math.cos(ang) * r;
var oy = cy + Math.sin(ang) * r;
axes += '<line x1="' + cx + '" y1="' + cy + '" x2="' + ox.toFixed(1) + '" y2="' + oy.toFixed(1) + '" stroke="var(--border)"/>';
var lx = cx + Math.cos(ang) * (r + 18);
var ly = cy + Math.sin(ang) * (r + 18);
labelEls += '<text x="' + lx.toFixed(1) + '" y="' + ly.toFixed(1) + '" text-anchor="middle" fill="var(--muted)" style="font-size:10px;font-family:var(--font)">' + labels[i] + '</text>';
var px = cx + Math.cos(ang) * r * values[i];
var py = cy + Math.sin(ang) * r * values[i];
polyPts.push(px.toFixed(1) + ',' + py.toFixed(1));
}
return '<svg viewBox="0 0 220 220" width="220" height="220" aria-label="routing profile spider graph">'
+ grid + axes
+ '<polygon points="' + polyPts.join(' ') + '" fill="rgba(193,128,255,0.25)" stroke="var(--accent)" stroke-width="2"/>'
+ labelEls
+ '</svg>';
}
function renderModelDecisionGraph(events, focusTurnId, focusModel, focusEdge) {
var MAX_GRAPH_CANDIDATES = 18;
var rows = Array.isArray(events) ? events.slice() : [];
if (!rows.length) {
return '<div class="card" style="margin-bottom:1rem"><div class="card-title">Task Model Decision Graph</div><div style="color:var(--muted)">No model selection events yet.</div></div>';
}
var byTurn = {};
rows.forEach(function(ev) {
var tid = String((ev && ev.turn_id) || '').trim();
if (!tid) return;
byTurn[tid] = ev;
});
var selectedTurn = focusTurnId && byTurn[focusTurnId] ? focusTurnId : String(rows[0].turn_id || '');
var selectedEvent = byTurn[selectedTurn] || rows[0] || {};
var selectedModel = String(selectedEvent.selected_model || '').trim();
var candidates = Array.isArray(selectedEvent.candidates) ? selectedEvent.candidates : [];
var nodeMap = {};
candidates.forEach(function(c) {
var model = String((c && c.model) || '').trim();
if (!model) return;
var score = typeof c.metascore === 'number' ? c.metascore : null;
nodeMap[model] = {
model: model,
score: score,
usable: c && c.usable !== false
};
});
if (selectedModel && !nodeMap[selectedModel]) {
nodeMap[selectedModel] = { model: selectedModel, score: null, usable: true };
}
var sortedNodes = Object.keys(nodeMap).sort(function(a, b) {
if (a === selectedModel) return -1;
if (b === selectedModel) return 1;
var sa = nodeMap[a] && typeof nodeMap[a].score === 'number' ? nodeMap[a].score : -Infinity;
var sb = nodeMap[b] && typeof nodeMap[b].score === 'number' ? nodeMap[b].score : -Infinity;
if (sa !== sb) return sb - sa;
return a.localeCompare(b);
});
var hiddenCount = Math.max(0, sortedNodes.length - MAX_GRAPH_CANDIDATES);
var nodes = sortedNodes.slice(0, MAX_GRAPH_CANDIDATES);
if (selectedModel && nodes.indexOf(selectedModel) === -1) {
nodes[nodes.length - 1] = selectedModel;
}
if (!nodes.length) return '';
var width = 680, height = 320;
var selectedX = 530, selectedY = height / 2;
var candidateX = 220;
var pos = {};
var candidateModels = nodes.filter(function(model) { return model !== selectedModel; });
if (selectedModel) pos[selectedModel] = { x: selectedX, y: selectedY };
candidateModels.forEach(function(model, idx) {
var y;
if (candidateModels.length === 1) {
y = selectedY;
} else {
var pad = 28;
var span = Math.max(10, (height - (pad * 2)));
y = pad + ((span * idx) / (candidateModels.length - 1));
}
pos[model] = { x: candidateX, y: y };
});
var edges = {};
candidateModels.forEach(function(model) {
if (!selectedModel || model === selectedModel) return;
var key = model + '→' + selectedModel;
edges[key] = { key: key, from: model, to: selectedModel, count: 1 };
});
var edgeKeys = Object.keys(edges);
var edgeEls = edgeKeys.map(function(k) {
var e = edges[k];
var a = pos[e.from], b = pos[e.to];
if (!a || !b) return '';
var active = focusEdge && focusEdge === e.key;
var faded = (focusModel && e.from !== focusModel && e.to !== focusModel) || (focusEdge && !active);
var cls = 'model-graph-edge' + (active ? ' active' : '') + (faded ? ' model-graph-faded' : '');
return '<line class="' + cls + '" data-edge-key="' + esc(e.key) + '" x1="' + a.x.toFixed(1) + '" y1="' + a.y.toFixed(1) + '" x2="' + b.x.toFixed(1) + '" y2="' + b.y.toFixed(1) + '" stroke-width="1.4"><title>' + esc(e.key) + '</title></line>';
}).join('');
var laneGuides = '';
if (candidateModels.length > 1) {
var yMin = pos[candidateModels[0]].y;
var yMax = pos[candidateModels[candidateModels.length - 1]].y;
laneGuides = '<line x1="' + candidateX + '" y1="' + yMin.toFixed(1) + '" x2="' + candidateX + '" y2="' + yMax.toFixed(1) + '" stroke="var(--border)" stroke-opacity="0.45" stroke-dasharray="2 3" />';
}
var nodeEls = nodes.map(function(model) {
var p = pos[model];
if (!p) return '';
var active = (focusModel && focusModel === model) || (!focusModel && model === selectedModel);
var faded = focusModel && !active;
var unusable = nodeMap[model] && nodeMap[model].usable === false;
var cls = 'model-graph-node' + (active ? ' active' : '') + (faded ? ' model-graph-faded' : '') + (unusable ? ' model-graph-node-unusable' : '');
var short = truncate(model.split('/').pop() || model, 22);
var score = nodeMap[model].score;
var scoreTxt = typeof score === 'number' ? ' · score ' + score.toFixed(3) : '';
var selectedTxt = model === selectedModel ? ' · selected' : '';
var r = model === selectedModel ? 14 : 10;
var labelX = model === selectedModel ? p.x : (p.x - 14);
var labelY = model === selectedModel ? (p.y + 28) : (p.y + 3);
var anchor = model === selectedModel ? 'middle' : 'end';
return '<g data-node-model="' + esc(model) + '" style="cursor:pointer">'
+ '<circle class="' + cls + '" cx="' + p.x.toFixed(1) + '" cy="' + p.y.toFixed(1) + '" r="' + r + '"><title>' + esc(model + selectedTxt + scoreTxt) + '</title></circle>'
+ '<text class="model-graph-node-label" text-anchor="' + anchor + '" x="' + labelX.toFixed(1) + '" y="' + labelY.toFixed(1) + '">' + esc(short) + '</text>'
+ '</g>';
}).join('');
var options = rows.slice(0, 80).map(function(ev) {
var tid = String(ev.turn_id || '');
var stamp = String(ev.created_at || '').replace('T', ' ').replace('Z', '');
var excerpt = truncate(String(ev.user_excerpt || ''), 44);
var label = truncate(tid, 8) + (excerpt ? ' · ' + excerpt : '') + (stamp ? ' · ' + stamp : '');
return '<option value="' + esc(tid) + '"' + (tid === selectedTurn ? ' selected' : '') + '>' + esc(label) + '</option>';
}).join('');
var detail = 'Task turn: ' + selectedTurn + '. Selected model: ' + (selectedModel || 'unknown') + '. Displaying top ' + nodes.length + ' candidate models' + (hiddenCount > 0 ? ' (' + hiddenCount + ' hidden).' : '.');
if (focusEdge && edges[focusEdge]) {
detail = 'Task turn: ' + selectedTurn + '. Candidate edge: ' + edges[focusEdge].from + ' → ' + edges[focusEdge].to + '.';
} else if (focusModel) {
var n = nodeMap[focusModel];
if (n) detail = 'Task turn: ' + selectedTurn + '. Focused model: ' + focusModel + (typeof n.score === 'number' ? ' (metascore ' + n.score.toFixed(3) + ').' : '.');
}
return '<div class="card" style="margin-bottom:1rem;padding-bottom:0.75rem">'
+ '<div class="card-title">Task Model Decision Graph</div>'
+ '<div style="font-size:0.75rem;color:var(--muted);margin-bottom:0.5rem">Candidate models and chosen model for a single task. Pick a task turn to inspect that decision.</div>'
+ '<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;margin-bottom:0.55rem"><label for="model-graph-task-select" style="font-size:0.75rem;color:var(--muted)">Task turn</label><select id="model-graph-task-select" class="select" style="min-width:320px;max-width:100%">' + options + '</select></div>'
+ '<div class="model-graph-wrap">'
+ '<svg class="model-graph-svg" viewBox="0 0 680 320" preserveAspectRatio="xMidYMid meet">'
+ '<text x="36" y="22" fill="var(--muted)" style="font-size:10px;font-family:var(--font-mono)">Candidates (ranked)</text>'
+ '<text x="487" y="22" fill="var(--muted)" style="font-size:10px;font-family:var(--font-mono)">Selected</text>'
+ laneGuides + edgeEls + nodeEls
+ '</svg></div>'
+ '<div id="model-graph-detail" class="model-graph-detail">' + esc(detail) + '</div>'
+ '<div style="margin-top:0.5rem"><button class="btn secondary" id="model-graph-clear-focus" style="font-size:0.6875rem;padding:0.22rem 0.55rem">Clear focus</button></div>'
+ '</div>';
}
var AGENT_DISPLAY_NAME = '';
function setAgentDisplayName(name) {
var value = String(name || '').trim();
if (value) AGENT_DISPLAY_NAME = value;
}
function sessionAssistantLabel(session, fallback) {
var fallbackLabel = fallback || 'assistant';
if (!session) return AGENT_DISPLAY_NAME || fallbackLabel;
var candidate = (session.agent_name || session.agent_id || '').toString().trim();
if (candidate && candidate.toLowerCase() !== 'default') return candidate;
if (AGENT_DISPLAY_NAME) return AGENT_DISPLAY_NAME;
return fallbackLabel;
}
function sanitizeMarkdownUrl(url) {
var raw = String(url || '').trim();
if (!raw) return null;
var lowered = raw.toLowerCase();
if (lowered.indexOf('javascript:') === 0 || lowered.indexOf('data:') === 0 || lowered.indexOf('vbscript:') === 0) return null;
if (lowered.indexOf('http://') === 0 || lowered.indexOf('https://') === 0 || lowered.indexOf('mailto:') === 0) return raw;
if (raw.charAt(0) === '/' || raw.charAt(0) === '#') return raw;
return null;
}
function renderInlineMarkdown(text) {
var src = String(text || '');
if (!src) return '';
var html = esc(src);
var inlineCodes = [];
html = html.replace(/`([^`]+)`/g, function(_, code) {
var token = '\x01IC' + inlineCodes.length + '\x01';
inlineCodes.push('<code>' + code + '</code>');
return token;
});
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(_, label, url) {
var safe = sanitizeMarkdownUrl(url);
var safeLabel = renderInlineMarkdown(label);
if (!safe) return safeLabel;
return '<a href="' + esc(safe) + '" target="_blank" rel="noopener noreferrer">' + safeLabel + '</a>';
});
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');
html = html.replace(/\*([^*\n]+)\*/g, '<em>$1</em>');
html = html.replace(/_([^_\n]+)_/g, '<em>$1</em>');
html = html.replace(/~~([^~]+)~~/g, '<del>$1</del>');
html = html.replace(/~([^~\n]+)~/g, '<del>$1</del>');
html = html.replace(/\x01IC(\d+)\x01/g, function(_, idx) {
var n = Number(idx);
return inlineCodes[n] || '';
});
return html;
}
function renderSafeMarkdownImpl(input) {
var src = String(input || '');
if (!src) return '';
var codeBlocks = [];
src = src.replace(/```([^\n`]*)\n?([\s\S]*?)```/g, function(_, lang, code) {
var token = '__MD_CODE_BLOCK_' + codeBlocks.length + '__';
var language = String(lang || '').trim().toLowerCase().replace(/[^a-z0-9_-]/g, '');
var classAttr = language ? ' class="language-' + language + '"' : '';
codeBlocks.push('<pre><code' + classAttr + '>' + esc(code) + '</code></pre>');
return token;
});
var lines = src.replace(/\r\n/g, '\n').split('\n');
var htmlParts = [];
var paragraph = [];
var inUl = false;
var inOl = false;
var inQuote = false;
function flushParagraph() {
if (!paragraph.length) return;
htmlParts.push('<p>' + paragraph.map(renderInlineMarkdown).join('<br>') + '</p>');
paragraph = [];
}
function closeLists() {
if (inUl) { htmlParts.push('</ul>'); inUl = false; }
if (inOl) { htmlParts.push('</ol>'); inOl = false; }
}
function closeQuote() {
if (inQuote) { htmlParts.push('</blockquote>'); inQuote = false; }
}
for (var i = 0; i < lines.length; i++) {
var raw = lines[i];
var trimmed = raw.trim();
var tokenOnly = /^__MD_CODE_BLOCK_\d+__$/.test(trimmed);
if (!trimmed) {
flushParagraph();
closeLists();
closeQuote();
continue;
}
if (tokenOnly) {
flushParagraph();
closeLists();
closeQuote();
htmlParts.push(trimmed);
continue;
}
var headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
flushParagraph();
closeLists();
closeQuote();
var level = headingMatch[1].length;
htmlParts.push('<h' + level + '>' + renderInlineMarkdown(headingMatch[2]) + '</h' + level + '>');
continue;
}
if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
flushParagraph();
closeLists();
closeQuote();
htmlParts.push('<hr>');
continue;
}
if (trimmed.charAt(0) === '|' && i + 1 < lines.length && /^\|[\s\-:|]+\|$/.test(lines[i + 1].trim())) {
flushParagraph();
closeLists();
closeQuote();
var headerCells = trimmed.split('|').slice(1, -1).map(function(c) { return c.trim(); });
var tbl = '<table class="md-table"><thead><tr>';
for (var h = 0; h < headerCells.length; h++) {
tbl += '<th>' + renderInlineMarkdown(headerCells[h]) + '</th>';
}
tbl += '</tr></thead><tbody>';
i += 2; while (i < lines.length && lines[i].trim().charAt(0) === '|') {
var cells = lines[i].trim().split('|').slice(1, -1).map(function(c) { return c.trim(); });
tbl += '<tr>';
for (var c = 0; c < cells.length; c++) {
tbl += '<td>' + renderInlineMarkdown(cells[c]) + '</td>';
}
tbl += '</tr>';
i++;
}
tbl += '</tbody></table>';
htmlParts.push(tbl);
i--; continue;
}
var quoteMatch = trimmed.match(/^>\s?(.*)$/);
if (quoteMatch) {
flushParagraph();
closeLists();
if (!inQuote) { htmlParts.push('<blockquote>'); inQuote = true; }
htmlParts.push('<p>' + renderInlineMarkdown(quoteMatch[1]) + '</p>');
continue;
}
closeQuote();
var ulMatch = trimmed.match(/^[-*+]\s+(.+)$/);
if (ulMatch) {
flushParagraph();
if (inOl) { htmlParts.push('</ol>'); inOl = false; }
if (!inUl) { htmlParts.push('<ul>'); inUl = true; }
htmlParts.push('<li>' + renderInlineMarkdown(ulMatch[1]) + '</li>');
continue;
}
var olMatch = trimmed.match(/^\d+\.\s+(.+)$/);
if (olMatch) {
flushParagraph();
if (inUl) { htmlParts.push('</ul>'); inUl = false; }
if (!inOl) { htmlParts.push('<ol>'); inOl = true; }
htmlParts.push('<li>' + renderInlineMarkdown(olMatch[1]) + '</li>');
continue;
}
closeLists();
paragraph.push(trimmed);
}
flushParagraph();
closeLists();
closeQuote();
var html = htmlParts.join('\n');
html = html.replace(/__MD_CODE_BLOCK_(\d+)__/g, function(_, idx) {
var n = Number(idx);
return codeBlocks[n] || '';
});
return html;
}
function renderSafeMarkdown(input) {
try {
return renderSafeMarkdownImpl(input);
} catch (err) {
if (typeof console !== 'undefined' && console.error) { console.error('renderSafeMarkdown', err); }
var full = String(input || '');
var snippet = full.slice(0, 1200);
return '<p class="md-render-fallback" style="color:var(--warning);font-size:0.8125rem">This message could not be formatted safely for the dashboard. Showing escaped source:</p>'
+ '<pre style="white-space:pre-wrap;word-break:break-word;font-size:0.75rem;max-height:14rem;overflow:auto;border:1px solid var(--border);padding:0.5rem;border-radius:4px">'
+ esc(snippet) + (full.length > 1200 ? '\n…' : '') + '</pre>';
}
}