Pretty print fenced JSON in admin AI replies
Build and publish frontend / publish (push) Successful in 1m14s

This commit is contained in:
Ein Anderssono
2026-10-24 03:37:36 +02:00
parent 1ba98b998d
commit 652afb7673
3 changed files with 63 additions and 1 deletions
+44 -1
View File
@@ -59,6 +59,9 @@
.json-long-text { margin: .2rem .5rem .5rem; padding: .5rem; max-height: 24rem; overflow: auto; background: #e2e8f0; white-space: pre-wrap; overflow-wrap: anywhere; }
.json-raw-value { margin-inline-start: .5rem; color: #475569; font-size: .9em; }
.json-raw-value > .json-long-text { border-inline-start: 2px solid #94a3b8; }
.admin-ai-response { margin: .35rem 0 .75rem; }
.admin-ai-response-text { white-space: pre-wrap; overflow-wrap: anywhere; }
.admin-ai-response-code { max-width: 100%; overflow: auto; padding: .65rem; border-inline-start: 2px solid #94a3b8; white-space: pre-wrap; overflow-wrap: anywhere; }
@media (max-width: 42rem) { .json-entry { grid-template-columns: 1fr; gap: .05rem; } }
@media (prefers-color-scheme: dark) {
.json-view { border-color: #475569; background: #111827; color: #e5e7eb; }
@@ -842,6 +845,43 @@
renderJsonTree(adminElement('div', undefined, details), value);
return details;
}
function parseAiResponseSegments(response) {
const text = String(response ?? '');
const fencedJson = /```json\s*([\s\S]*?)```/gi;
const segments = [];
let cursor = 0;
for (const match of text.matchAll(fencedJson)) {
const index = match.index ?? cursor;
if (index > cursor) segments.push({type: 'text', value: text.slice(cursor, index)});
const source = match[1].trim();
try {
segments.push({type: 'json', value: JSON.parse(source)});
} catch (_) {
segments.push({type: 'code', value: source});
}
cursor = index + match[0].length;
}
if (cursor < text.length || segments.length === 0) segments.push({type: 'text', value: text.slice(cursor)});
return segments;
}
function renderAdminAiResponse(parent, response) {
const segments = parseAiResponseSegments(response);
if (!segments.some(segment => segment.type === 'json' || segment.type === 'code')) return false;
const output = adminElement('section', undefined, parent);
output.className = 'admin-ai-response';
for (const segment of segments) {
if (segment.type === 'json') {
adminJson('JSON-svar · formaterat', segment.value, output);
} else if (segment.type === 'code') {
const code = adminElement('pre', segment.value, output);
code.className = 'admin-ai-response-code';
} else if (segment.value.trim()) {
const text = adminElement('p', decodeEscapedSequences(segment.value.trim()), output);
text.className = 'admin-ai-response-text';
}
}
return true;
}
async function loadAdminPlayers(query = '', offset = 0) {
adminOffset = offset;
adminContent.replaceChildren();
@@ -969,8 +1009,11 @@
}
for (const interaction of group.interactions) {
if (!interaction.response) continue;
const button = adminElement('button', `AI-svar · ${interaction.prompt_id} · ${interaction.outcome}: ${interaction.response}`, card);
const responseHasJson = parseAiResponseSegments(interaction.response).some(segment => segment.type === 'json' || segment.type === 'code');
const responseButtonLabel = `AI-svar · ${interaction.prompt_id} · ${interaction.outcome}${responseHasJson ? ' · JSON' : `: ${interaction.response}`}`;
const button = adminElement('button', responseButtonLabel, card);
button.type = 'button'; button.setAttribute('aria-expanded', 'false');
if (responseHasJson) renderAdminAiResponse(card, interaction.response);
const detail = adminElement('section', undefined, card); detail.hidden = true;
button.addEventListener('click', () => {
detail.hidden = !detail.hidden; button.setAttribute('aria-expanded', String(!detail.hidden));