|
|
|
@@ -29,6 +29,21 @@
|
|
|
|
|
#admin-content pre { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 32rem; overflow: auto; }
|
|
|
|
|
#admin-toolbar { display: flex; flex-wrap: wrap; gap: .5rem; margin-block: 1rem; }
|
|
|
|
|
#player-search { display: flex; flex-wrap: wrap; align-items: end; gap: .5rem; }
|
|
|
|
|
body.debug-layout { max-width: none; margin: 0; padding: .5rem; height: 100vh; overflow: hidden; }
|
|
|
|
|
#play-layout.debug-enabled { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: .75rem; height: calc(100vh - 4rem); }
|
|
|
|
|
#play-layout.debug-enabled #play, #play-layout.debug-enabled #debug-panel { min-width: 0; height: 100%; overflow: auto; padding: 1rem; box-sizing: border-box; }
|
|
|
|
|
#play-layout.debug-enabled #debug-panel { border-inline-start: 1px solid currentColor; }
|
|
|
|
|
#debug-panel h2 { margin-block-start: 0; }
|
|
|
|
|
#debug-trace { display: grid; gap: .6rem; }
|
|
|
|
|
#debug-trace details { border: 1px solid currentColor; border-radius: .3rem; padding: .5rem; }
|
|
|
|
|
#debug-trace summary { cursor: pointer; overflow-wrap: anywhere; }
|
|
|
|
|
#debug-trace pre { white-space: pre-wrap; overflow-wrap: anywhere; font: .8rem/1.45 ui-monospace, monospace; }
|
|
|
|
|
@media (max-width: 800px) {
|
|
|
|
|
body.debug-layout { height: auto; overflow: auto; }
|
|
|
|
|
#play-layout.debug-enabled { grid-template-columns: minmax(0, 1fr); height: auto; }
|
|
|
|
|
#play-layout.debug-enabled #play { min-height: 55vh; }
|
|
|
|
|
#play-layout.debug-enabled #debug-panel { min-height: 45vh; border-inline-start: 0; border-block-start: 1px solid currentColor; }
|
|
|
|
|
}
|
|
|
|
|
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
|
|
|
|
[hidden] { display: none !important; }
|
|
|
|
|
footer { margin-top: 2rem; border-top: 1px solid; padding-top: .5rem; color: GrayText; font-size: .875rem; }
|
|
|
|
@@ -48,6 +63,7 @@
|
|
|
|
|
<div><button type="submit" name="intent" value="login">Logga in</button> <button type="submit" name="intent" value="register">Skapa konto</button></div>
|
|
|
|
|
</form>
|
|
|
|
|
</section>
|
|
|
|
|
<div id="play-layout">
|
|
|
|
|
<section id="play" aria-label="Spelets dialog" hidden>
|
|
|
|
|
<h1 class="sr-only">Dialog</h1>
|
|
|
|
|
<ol id="dialogue" aria-label="Dialoghistorik" role="log" aria-live="polite" aria-relevant="additions text"></ol>
|
|
|
|
@@ -64,6 +80,14 @@
|
|
|
|
|
<output id="speech-progress-value" for="speech-progress">Väntar på nedladdning</output>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
<aside id="debug-panel" aria-labelledby="debug-heading" hidden>
|
|
|
|
|
<h2 id="debug-heading">Beslutsspårning</h2>
|
|
|
|
|
<p>Administratörsvy för ditt eget spelkonto. Promptar, modelltrafik, regelresultat och Rust-commit visas från revisionsloggad API-data. Hemligheter och credentials ska vara maskerade av servern.</p>
|
|
|
|
|
<button type="button" id="debug-refresh">Uppdatera spår</button>
|
|
|
|
|
<p id="debug-status" role="status" aria-live="polite">Hämtar spårning…</p>
|
|
|
|
|
<div id="debug-trace" aria-label="Kronologiskt beslutsspår"></div>
|
|
|
|
|
</aside>
|
|
|
|
|
</div>
|
|
|
|
|
<section id="admin-view" aria-labelledby="admin-heading" hidden>
|
|
|
|
|
<h1 id="admin-heading">Administration</h1>
|
|
|
|
|
<p>Privilegierad läsning loggas. Känsligt spelarinnehåll visas bara här för administratörer.</p>
|
|
|
|
@@ -84,6 +108,10 @@
|
|
|
|
|
<script>
|
|
|
|
|
const auth = document.querySelector('#auth');
|
|
|
|
|
const play = document.querySelector('#play');
|
|
|
|
|
const playLayout = document.querySelector('#play-layout');
|
|
|
|
|
const debugPanel = document.querySelector('#debug-panel');
|
|
|
|
|
const debugTrace = document.querySelector('#debug-trace');
|
|
|
|
|
const debugStatus = document.querySelector('#debug-status');
|
|
|
|
|
const adminView = document.querySelector('#admin-view');
|
|
|
|
|
const adminContent = document.querySelector('#admin-content');
|
|
|
|
|
const transcript = document.querySelector('#dialogue');
|
|
|
|
@@ -93,6 +121,8 @@
|
|
|
|
|
let characterDraft = null;
|
|
|
|
|
let worldDraft = null;
|
|
|
|
|
let gameState = null;
|
|
|
|
|
let currentAccount = null;
|
|
|
|
|
let debugRefreshBusy = false;
|
|
|
|
|
let submitBusy = false;
|
|
|
|
|
let pendingDialogue = null;
|
|
|
|
|
let latestAssistantText = '';
|
|
|
|
@@ -412,6 +442,9 @@
|
|
|
|
|
function showAuth() {
|
|
|
|
|
stage = 'auth';
|
|
|
|
|
play.hidden = true;
|
|
|
|
|
debugPanel.hidden = true;
|
|
|
|
|
playLayout.classList.remove('debug-enabled');
|
|
|
|
|
document.body.classList.remove('debug-layout');
|
|
|
|
|
adminView.hidden = true;
|
|
|
|
|
auth.hidden = false;
|
|
|
|
|
document.querySelector('#auth-heading').textContent = location.pathname === '/admin' ? 'Administratörsinloggning' : 'Svensk livssimulator';
|
|
|
|
@@ -426,6 +459,10 @@
|
|
|
|
|
auth.hidden = true;
|
|
|
|
|
adminView.hidden = true;
|
|
|
|
|
play.hidden = false;
|
|
|
|
|
const showDebug = currentAccount?.is_admin === true;
|
|
|
|
|
debugPanel.hidden = !showDebug;
|
|
|
|
|
playLayout.classList.toggle('debug-enabled', showDebug);
|
|
|
|
|
document.body.classList.toggle('debug-layout', showDebug);
|
|
|
|
|
refreshMicrophone();
|
|
|
|
|
messageField.focus();
|
|
|
|
|
}
|
|
|
|
@@ -435,6 +472,9 @@
|
|
|
|
|
voiceController.stopAll();
|
|
|
|
|
auth.hidden = true;
|
|
|
|
|
play.hidden = true;
|
|
|
|
|
debugPanel.hidden = true;
|
|
|
|
|
playLayout.classList.remove('debug-enabled');
|
|
|
|
|
document.body.classList.remove('debug-layout');
|
|
|
|
|
adminView.hidden = false;
|
|
|
|
|
document.querySelector('#version-footer').hidden = true;
|
|
|
|
|
}
|
|
|
|
@@ -598,8 +638,7 @@
|
|
|
|
|
body: JSON.stringify({input: message, current_draft: worldDraft}),
|
|
|
|
|
});
|
|
|
|
|
worldDraft = proposal.draft;
|
|
|
|
|
const senses = worldDraft.location_senses;
|
|
|
|
|
appendLine('assistant', `${proposal.message}\n\nVärld: ${worldDraft.name}\nStartplats: ${worldDraft.location_name}. ${worldDraft.location_description}\nLjud: ${senses.sound}\nDoft: ${senses.smell}\nLuft och temperatur: ${senses.air_and_temperature}\nBeröring och ytor: ${senses.touch_and_surfaces}\nOrientering: ${senses.spatial_orientation}\nStartbelopp: ${worldDraft.starting_balance_sek} kronor. ${worldDraft.offer_name} kostar ${worldDraft.offer_price_sek} kronor.\n\nSkriv ändringar eller “godkänn”.`);
|
|
|
|
|
appendLine('assistant', `${proposal.message}\n\nVärld: ${worldDraft.name}\nStartplats: ${worldDraft.location_name}. ${worldDraft.location_description}\nStartbelopp: ${worldDraft.starting_balance_sek} kronor. ${worldDraft.offer_name} kostar ${worldDraft.offer_price_sek} kronor.\n\nSkriv ändringar eller “godkänn”.`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
appendLine('assistant', `Förslaget kunde inte tas fram: ${error.message}. Skriv “försök igen” när du vill försöka på nytt.`);
|
|
|
|
|
}
|
|
|
|
@@ -637,16 +676,17 @@
|
|
|
|
|
voiceController?.speak(latestAssistantText, {role: 'narrator', voiceIdentity: 'neutral'});
|
|
|
|
|
} else if (lines.length === 0 && gameState?.world) {
|
|
|
|
|
const place = gameState.world.location;
|
|
|
|
|
const senses = place.senses || {};
|
|
|
|
|
appendLine('assistant', `Du börjar på ${place.name}. ${place.description} Ljud: ${senses.sound || 'Inget särskilt ljud beskrivet.'} Doft: ${senses.smell || 'Ingen särskild doft beskriven.'} Luft och temperatur: ${senses.air_and_temperature || 'Inte beskrivet.'} Beröring och ytor: ${senses.touch_and_surfaces || 'Inte beskrivet.'} Orientering: ${senses.spatial_orientation || 'Inte beskrivet.'}`);
|
|
|
|
|
appendLine('assistant', `Du börjar på ${place.name}. ${place.description}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
async function openGame() {
|
|
|
|
|
currentAccount = await request('/api/me');
|
|
|
|
|
gameState = await request('/api/world');
|
|
|
|
|
showPlay();
|
|
|
|
|
if (!gameState.character) {
|
|
|
|
|
transcript.replaceChildren();
|
|
|
|
|
appendInitialPrompt();
|
|
|
|
|
if (currentAccount.is_admin) await refreshDecisionTrace();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
characterDraft = null;
|
|
|
|
@@ -654,13 +694,66 @@
|
|
|
|
|
worldDraft = null;
|
|
|
|
|
appendLine('assistant', 'Nu föreslår jag en startplats som passar biografin. Du kan ändra förslaget innan något sparas. Skriv “börja”.');
|
|
|
|
|
setStage('world-draft');
|
|
|
|
|
if (currentAccount.is_admin) await refreshDecisionTrace();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await loadSavedDialogue();
|
|
|
|
|
if (currentAccount.is_admin) await refreshDecisionTrace();
|
|
|
|
|
setStage('game');
|
|
|
|
|
startPlayerPresence();
|
|
|
|
|
announcePlayerChatHelp();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function traceTimestamp(record) {
|
|
|
|
|
return record.recorded_at || record.requested_at || record.completed_at || '';
|
|
|
|
|
}
|
|
|
|
|
function renderDecisionTrace(data) {
|
|
|
|
|
const groups = new Map();
|
|
|
|
|
for (const [kind, records] of Object.entries({events: data.events || [], ai_interactions: data.ai_interactions || [], player_messages: data.player_messages || []})) {
|
|
|
|
|
for (const record of records) {
|
|
|
|
|
const key = record.correlation_id || `${kind}:${record.event_id || record.id || traceTimestamp(record)}`;
|
|
|
|
|
if (!groups.has(key)) groups.set(key, {correlation_id: key, events: [], ai_interactions: [], player_messages: [], latest: ''});
|
|
|
|
|
const group = groups.get(key);
|
|
|
|
|
group[kind].push(record);
|
|
|
|
|
const time = traceTimestamp(record);
|
|
|
|
|
if (time > group.latest) group.latest = time;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const ordered = [...groups.values()].sort((a, b) => b.latest.localeCompare(a.latest));
|
|
|
|
|
debugTrace.replaceChildren();
|
|
|
|
|
if (ordered.length === 0) {
|
|
|
|
|
const empty = document.createElement('p');
|
|
|
|
|
empty.textContent = 'Ingen sparad spårning för kontot ännu.';
|
|
|
|
|
debugTrace.append(empty);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
for (const group of ordered) {
|
|
|
|
|
const details = document.createElement('details');
|
|
|
|
|
details.open = debugTrace.childElementCount === 0;
|
|
|
|
|
const summary = document.createElement('summary');
|
|
|
|
|
const eventNames = group.events.map(event => `${event.event_type} (${event.commit_status || 'okänt utfall'})`).join(', ');
|
|
|
|
|
const prompts = group.ai_interactions.map(interaction => `${interaction.prompt_id} v${interaction.prompt_version}: ${interaction.outcome}`).join(', ');
|
|
|
|
|
summary.textContent = `${group.latest || 'Tid saknas'} · ${group.correlation_id}${eventNames ? ` · Rust: ${eventNames}` : ''}${prompts ? ` · AI: ${prompts}` : ''}`;
|
|
|
|
|
const pre = document.createElement('pre');
|
|
|
|
|
pre.textContent = JSON.stringify(group, null, 2);
|
|
|
|
|
details.append(summary, pre);
|
|
|
|
|
debugTrace.append(details);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
async function refreshDecisionTrace() {
|
|
|
|
|
if (!currentAccount?.is_admin || debugRefreshBusy || debugPanel.hidden) return;
|
|
|
|
|
debugRefreshBusy = true;
|
|
|
|
|
debugStatus.textContent = 'Hämtar revisionsloggad spårning…';
|
|
|
|
|
try {
|
|
|
|
|
const trace = await request('/api/admin/self/trace?limit=200');
|
|
|
|
|
renderDecisionTrace(trace);
|
|
|
|
|
debugStatus.textContent = `Visar ${debugTrace.childElementCount} korrelerade händelsegrupper. Uppdaterad ${new Date().toLocaleTimeString('sv-SE')}.`;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
debugStatus.textContent = `Spårningen kunde inte hämtas: ${error.message}`;
|
|
|
|
|
} finally {
|
|
|
|
|
debugRefreshBusy = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
function announcePlayerChatHelp() {
|
|
|
|
|
const key = 'ai-spel.player-chat-help.v1';
|
|
|
|
|
try {
|
|
|
|
@@ -822,6 +915,7 @@
|
|
|
|
|
} finally {
|
|
|
|
|
submitBusy = false;
|
|
|
|
|
refreshMicrophone();
|
|
|
|
|
if (currentAccount?.is_admin) await refreshDecisionTrace();
|
|
|
|
|
if (!play.hidden && !messageField.disabled) messageField.focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -922,6 +1016,7 @@
|
|
|
|
|
finally { adminContent.setAttribute('aria-busy', 'false'); }
|
|
|
|
|
}));
|
|
|
|
|
document.querySelector('#admin-logout').addEventListener('click', handleLogout);
|
|
|
|
|
document.querySelector('#debug-refresh').addEventListener('click', refreshDecisionTrace);
|
|
|
|
|
messageField.addEventListener('keydown', (event) => {
|
|
|
|
|
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|