Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6680dd88d6 | ||
|
|
652afb7673 |
@@ -2,5 +2,5 @@ apiVersion: v2
|
|||||||
name: ai-spel-frontend
|
name: ai-spel-frontend
|
||||||
description: Static web frontend for the Swedish life simulator
|
description: Static web frontend for the Swedish life simulator
|
||||||
type: application
|
type: application
|
||||||
version: 0.1.76
|
version: 0.1.77
|
||||||
appVersion: "0.1.76"
|
appVersion: "0.1.77"
|
||||||
+44
-1
@@ -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-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 { margin-inline-start: .5rem; color: #475569; font-size: .9em; }
|
||||||
.json-raw-value > .json-long-text { border-inline-start: 2px solid #94a3b8; }
|
.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; }
|
||||||
@@ -842,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();
|
||||||
@@ -969,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));
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ grep -q 'function renderJsonTree' index.html
|
|||||||
grep -q 'function decodeEscapedSequences' index.html
|
grep -q 'function decodeEscapedSequences' index.html
|
||||||
grep -q 'decodeEscapedSequences(item)' index.html
|
grep -q 'decodeEscapedSequences(item)' index.html
|
||||||
grep -q "Visa lagrad text med escape-tecken" index.html
|
grep -q "Visa lagrad text med escape-tecken" index.html
|
||||||
|
grep -q 'function parseAiResponseSegments' index.html
|
||||||
|
grep -q 'function renderAdminAiResponse' index.html
|
||||||
|
grep -q "JSON-svar · formaterat" index.html
|
||||||
|
grep -q 'admin-ai-response-code' index.html
|
||||||
grep -q "'Öppna alla block'" index.html
|
grep -q "'Öppna alla block'" index.html
|
||||||
grep -q "'Fäll ihop nästlade block'" index.html
|
grep -q "'Fäll ihop nästlade block'" index.html
|
||||||
grep -q "nested.className = 'json-node'" index.html
|
grep -q "nested.className = 'json-node'" index.html
|
||||||
|
|||||||
@@ -79,3 +79,18 @@ test('admin JSON text decodes common JSON and YAML escapes while retaining the s
|
|||||||
assert.equal(decode(String.raw`\U0001F642`), '🙂');
|
assert.equal(decode(String.raw`\U0001F642`), '🙂');
|
||||||
assert.equal(decode(String.raw`okänt: \q`), String.raw`okänt: \q`);
|
assert.equal(decode(String.raw`okänt: \q`), String.raw`okänt: \q`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('fenced JSON in AI replies is parsed for readable formatting', () => {
|
||||||
|
const parser = page.match(/function parseAiResponseSegments\(response\) \{[\s\S]*?\n \}/);
|
||||||
|
assert.ok(parser, 'AI response segment parser should be present');
|
||||||
|
assert.match(page, /renderAdminAiResponse\(card, interaction\.response\)/);
|
||||||
|
assert.match(page, /JSON-svar · formaterat/);
|
||||||
|
assert.match(page, /admin-ai-response-code/);
|
||||||
|
const parseSegments = new Function(`${parser[0]}; return parseAiResponseSegments;`)();
|
||||||
|
const segments = parseSegments('dialogue-turn · completed: ```json { "intent": "speak", "reply": "Rad 1\\nRad 2" }```');
|
||||||
|
|
||||||
|
assert.equal(segments[0].type, 'text');
|
||||||
|
assert.equal(segments[0].value, 'dialogue-turn · completed: ');
|
||||||
|
assert.equal(segments[1].type, 'json');
|
||||||
|
assert.deepEqual(segments[1].value, {intent: 'speak', reply: 'Rad 1\nRad 2'});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user