|
|
@@ -55,8 +55,13 @@
|
|
|
|
.json-value-null { color: #be123c; font-style: italic; }
|
|
|
|
.json-value-null { color: #be123c; font-style: italic; }
|
|
|
|
.json-empty { padding: .25rem .65rem; color: #475569; font-style: italic; }
|
|
|
|
.json-empty { padding: .25rem .65rem; color: #475569; font-style: italic; }
|
|
|
|
.json-long-value { border-inline-start: 2px solid #94a3b8; margin-inline-start: .5rem; }
|
|
|
|
.json-long-value { border-inline-start: 2px solid #94a3b8; margin-inline-start: .5rem; }
|
|
|
|
.json-long-value > summary { cursor: pointer; padding: .3rem .45rem; overflow-wrap: anywhere; }
|
|
|
|
.json-long-value > summary, .json-raw-value > summary { cursor: pointer; padding: .3rem .45rem; overflow-wrap: anywhere; }
|
|
|
|
.json-long-text { margin: .2rem .5rem .5rem; padding: .5rem; max-height: 24rem; overflow: auto; background: #e2e8f0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
|
|
.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 (max-width: 42rem) { .json-entry { grid-template-columns: 1fr; gap: .05rem; } }
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
|
|
.json-view { border-color: #475569; background: #111827; color: #e5e7eb; }
|
|
|
|
.json-view { border-color: #475569; background: #111827; color: #e5e7eb; }
|
|
|
@@ -69,6 +74,7 @@
|
|
|
|
.json-value-number { color: #c4b5fd; }
|
|
|
|
.json-value-number { color: #c4b5fd; }
|
|
|
|
.json-value-boolean { color: #fcd34d; }
|
|
|
|
.json-value-boolean { color: #fcd34d; }
|
|
|
|
.json-value-null { color: #fda4af; }
|
|
|
|
.json-value-null { color: #fda4af; }
|
|
|
|
|
|
|
|
.json-raw-value { color: #cbd5e1; }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#admin-toolbar { display: flex; flex-wrap: wrap; gap: .5rem; margin-block: 1rem; }
|
|
|
|
#admin-toolbar { display: flex; flex-wrap: wrap; gap: .5rem; margin-block: 1rem; }
|
|
|
|
#player-search { display: flex; flex-wrap: wrap; align-items: end; gap: .5rem; }
|
|
|
|
#player-search { display: flex; flex-wrap: wrap; align-items: end; gap: .5rem; }
|
|
|
@@ -656,6 +662,24 @@
|
|
|
|
parent.append(element);
|
|
|
|
parent.append(element);
|
|
|
|
return element;
|
|
|
|
return element;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function decodeEscapedSequences(text) {
|
|
|
|
|
|
|
|
const source = String(text).replace(/\\(?:\r\n|\n|\r)[ \t]*/g, '');
|
|
|
|
|
|
|
|
const escapes = {
|
|
|
|
|
|
|
|
'0': '\0', a: '\x07', b: '\b', t: '\t', n: '\n', v: '\x0b',
|
|
|
|
|
|
|
|
f: '\f', r: '\r', e: '\x1b', ' ': ' ', '"': '"', "'": "'",
|
|
|
|
|
|
|
|
'/': '/', '\\': '\\', N: '\u0085', _: '\u00a0', L: '\u2028', P: '\u2029',
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
return source.replace(/\\(U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[\s\S])/g, (sequence, escape) => {
|
|
|
|
|
|
|
|
if (Object.hasOwn(escapes, escape)) return escapes[escape];
|
|
|
|
|
|
|
|
if (/^x[0-9a-fA-F]{2}$/.test(escape)) return String.fromCharCode(Number.parseInt(escape.slice(1), 16));
|
|
|
|
|
|
|
|
if (/^u[0-9a-fA-F]{4}$/.test(escape)) return String.fromCharCode(Number.parseInt(escape.slice(1), 16));
|
|
|
|
|
|
|
|
if (/^U[0-9a-fA-F]{8}$/.test(escape)) {
|
|
|
|
|
|
|
|
const point = Number.parseInt(escape.slice(1), 16);
|
|
|
|
|
|
|
|
if (point <= 0x10ffff && !(point >= 0xd800 && point <= 0xdfff)) return String.fromCodePoint(point);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return sequence;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
function renderJsonTree(container, value) {
|
|
|
|
function renderJsonTree(container, value) {
|
|
|
|
container.className = 'json-view';
|
|
|
|
container.className = 'json-view';
|
|
|
|
container.setAttribute('aria-label', 'Utfällbart JSON-svar');
|
|
|
|
container.setAttribute('aria-label', 'Utfällbart JSON-svar');
|
|
|
@@ -677,7 +701,19 @@
|
|
|
|
const groupSummary = item => Array.isArray(item)
|
|
|
|
const groupSummary = item => Array.isArray(item)
|
|
|
|
? `Lista · ${item.length} ${item.length === 1 ? 'post' : 'poster'}`
|
|
|
|
? `Lista · ${item.length} ${item.length === 1 ? 'post' : 'poster'}`
|
|
|
|
: `Objekt · ${Object.keys(item).length} fält`;
|
|
|
|
: `Objekt · ${Object.keys(item).length} fält`;
|
|
|
|
|
|
|
|
const addRawText = (parent, value) => {
|
|
|
|
|
|
|
|
const raw = document.createElement('details');
|
|
|
|
|
|
|
|
raw.className = 'json-raw-value';
|
|
|
|
|
|
|
|
const rawSummary = document.createElement('summary');
|
|
|
|
|
|
|
|
rawSummary.textContent = 'Visa lagrad text med escape-tecken';
|
|
|
|
|
|
|
|
const rawText = document.createElement('div');
|
|
|
|
|
|
|
|
rawText.className = 'json-long-text';
|
|
|
|
|
|
|
|
rawText.textContent = value;
|
|
|
|
|
|
|
|
raw.append(rawSummary, rawText);
|
|
|
|
|
|
|
|
parent.append(raw);
|
|
|
|
|
|
|
|
};
|
|
|
|
const addLeaf = (parent, label, item) => {
|
|
|
|
const addLeaf = (parent, label, item) => {
|
|
|
|
|
|
|
|
const readable = typeof item === 'string' ? decodeEscapedSequences(item) : item;
|
|
|
|
if (typeof item === 'string' && item.length > 180) {
|
|
|
|
if (typeof item === 'string' && item.length > 180) {
|
|
|
|
const details = document.createElement('details');
|
|
|
|
const details = document.createElement('details');
|
|
|
|
details.className = 'json-long-value';
|
|
|
|
details.className = 'json-long-value';
|
|
|
@@ -685,13 +721,15 @@
|
|
|
|
summary.append(document.createTextNode(`${label ? `${label} · ` : ''}Text · ${item.length} tecken · `));
|
|
|
|
summary.append(document.createTextNode(`${label ? `${label} · ` : ''}Text · ${item.length} tecken · `));
|
|
|
|
const preview = document.createElement('span');
|
|
|
|
const preview = document.createElement('span');
|
|
|
|
preview.className = 'json-value-string';
|
|
|
|
preview.className = 'json-value-string';
|
|
|
|
preview.textContent = `${item.slice(0, 100)}${item.length > 100 ? '…' : ''}`;
|
|
|
|
const previewText = readable.slice(0, 100);
|
|
|
|
|
|
|
|
preview.textContent = `${previewText}${readable.length > 100 ? '…' : ''}`;
|
|
|
|
summary.append(preview);
|
|
|
|
summary.append(preview);
|
|
|
|
const fullText = document.createElement('div');
|
|
|
|
const fullText = document.createElement('div');
|
|
|
|
fullText.className = 'json-long-text';
|
|
|
|
fullText.className = 'json-long-text';
|
|
|
|
fullText.textContent = item;
|
|
|
|
fullText.textContent = readable;
|
|
|
|
details.append(summary, fullText);
|
|
|
|
details.append(summary, fullText);
|
|
|
|
parent.append(details);
|
|
|
|
parent.append(details);
|
|
|
|
|
|
|
|
if (readable !== item) addRawText(parent, item);
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const row = document.createElement('div');
|
|
|
|
const row = document.createElement('div');
|
|
|
@@ -705,9 +743,10 @@
|
|
|
|
const valueNode = document.createElement('span');
|
|
|
|
const valueNode = document.createElement('span');
|
|
|
|
const type = item === null ? 'null' : typeof item;
|
|
|
|
const type = item === null ? 'null' : typeof item;
|
|
|
|
valueNode.className = `json-value-${type === 'boolean' ? 'boolean' : type}`;
|
|
|
|
valueNode.className = `json-value-${type === 'boolean' ? 'boolean' : type}`;
|
|
|
|
valueNode.textContent = typeof item === 'string' ? JSON.stringify(item) : String(item);
|
|
|
|
valueNode.textContent = typeof readable === 'string' ? readable : String(readable);
|
|
|
|
row.append(valueNode);
|
|
|
|
row.append(valueNode);
|
|
|
|
parent.append(row);
|
|
|
|
parent.append(row);
|
|
|
|
|
|
|
|
if (typeof item === 'string' && readable !== item) addRawText(parent, item);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
const populate = (details, item, depth) => {
|
|
|
|
const populate = (details, item, depth) => {
|
|
|
|
if (details.dataset.loaded === 'true') return;
|
|
|
|
if (details.dataset.loaded === 'true') return;
|
|
|
@@ -806,6 +845,43 @@
|
|
|
|
renderJsonTree(adminElement('div', undefined, details), value);
|
|
|
|
renderJsonTree(adminElement('div', undefined, details), value);
|
|
|
|
return details;
|
|
|
|
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) {
|
|
|
|
async function loadAdminPlayers(query = '', offset = 0) {
|
|
|
|
adminOffset = offset;
|
|
|
|
adminOffset = offset;
|
|
|
|
adminContent.replaceChildren();
|
|
|
|
adminContent.replaceChildren();
|
|
|
@@ -933,8 +1009,11 @@
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (const interaction of group.interactions) {
|
|
|
|
for (const interaction of group.interactions) {
|
|
|
|
if (!interaction.response) continue;
|
|
|
|
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');
|
|
|
|
button.type = 'button'; button.setAttribute('aria-expanded', 'false');
|
|
|
|
|
|
|
|
if (responseHasJson) renderAdminAiResponse(card, interaction.response);
|
|
|
|
const detail = adminElement('section', undefined, card); detail.hidden = true;
|
|
|
|
const detail = adminElement('section', undefined, card); detail.hidden = true;
|
|
|
|
button.addEventListener('click', () => {
|
|
|
|
button.addEventListener('click', () => {
|
|
|
|
detail.hidden = !detail.hidden; button.setAttribute('aria-expanded', String(!detail.hidden));
|
|
|
|
detail.hidden = !detail.hidden; button.setAttribute('aria-expanded', String(!detail.hidden));
|
|
|
|