Show player-facing processing feedback
Build and publish frontend / publish (push) Successful in 47s

This commit is contained in:
Ein Anderssono
2026-10-19 12:37:04 +02:00
parent 3cf91781f5
commit f464bd5490
5 changed files with 74 additions and 11 deletions
+48 -8
View File
@@ -116,7 +116,7 @@
<footer id="version-footer">Frontend <span id="frontend-version">__FRONTEND_VERSION__</span> · Statik <span id="static-version">hämtar version…</span> · API <span id="api-version">hämtar version…</span></footer>
<script src="/piper-runtime.js?v=__FRONTEND_VERSION__"></script>
<script src="/voice.js?v=__FRONTEND_VERSION__"></script>
<script src="/static/wllama-gemma3-v1/local-ai.js"></script>
<script src="/static/wllama-gemma3-v2/local-ai.js"></script>
<script>
const auth = document.querySelector('#auth');
const play = document.querySelector('#play');
@@ -174,6 +174,24 @@
}
return line;
}
let activityStatusLine = null;
function setActivityStatus(message) {
if (!message) return;
if (!activityStatusLine?.isConnected) {
activityStatusLine = document.createElement('li');
activityStatusLine.dataset.role = 'system';
activityStatusLine.dataset.activityStatus = 'true';
activityStatusLine.setAttribute('role', 'status');
activityStatusLine.setAttribute('aria-live', 'polite');
transcript.append(activityStatusLine);
}
activityStatusLine.textContent = `Berättelsen: ${message}`;
activityStatusLine.scrollIntoView({block: 'end'});
}
function clearActivityStatus() {
activityStatusLine?.remove();
activityStatusLine = null;
}
function biographyContextText(context) {
if (!context || typeof context !== 'object') return '';
const details = [];
@@ -210,13 +228,17 @@
}
window.addEventListener('local-ai-status', event => {
if (!currentAccount || currentAccount.is_admin || play.hidden) return;
let statusLine = transcript.querySelector('li[data-local-ai-status]');
if (!statusLine) {
const detail = typeof event.detail === 'string' ? {text: event.detail} : event.detail;
statusLine = appendLine(detail.type === 'terms-required' ? 'assistant' : 'system', detail.text || '', {speaker: 'Lokal AI', speak: detail.type === 'terms-required'});
statusLine.dataset.localAiStatus = 'true';
}
const detail = typeof event.detail === 'string' ? {text: event.detail} : event.detail;
const statusAttribute = detail.type === 'activity' ? 'data-local-ai-activity' : 'data-local-ai-status';
let statusLine = transcript.querySelector(`li[${statusAttribute}]`);
if (!statusLine) {
statusLine = appendLine(detail.type === 'terms-required' ? 'assistant' : 'system', detail.text || '', {
speaker: 'Lokal AI', speak: detail.type === 'terms-required',
});
statusLine.setAttribute(statusAttribute, 'true');
statusLine.setAttribute('role', 'status');
statusLine.setAttribute('aria-live', 'polite');
}
statusLine.replaceChildren(document.createTextNode(`Lokal AI: ${detail.text || ''}`));
if (detail.type === 'terms-required') {
statusLine.append(document.createTextNode(' Läs dokumenten: '));
@@ -670,15 +692,17 @@
stage = 'world-start';
messageField.disabled = true;
appendLine('user', message);
appendLine('assistant', 'Jag tar fram ett förslag på startplats och startläge…');
setActivityStatus('Berättaren ber AI ta fram ett förslag på startvärld och en plats som passar din biografi.');
try {
const proposal = await request('/api/world/draft', {
method: 'POST', headers: {'content-type': 'application/json'},
body: JSON.stringify({input: message, current_draft: worldDraft}),
});
worldDraft = proposal.draft;
clearActivityStatus();
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) {
clearActivityStatus();
appendLine('assistant', `Förslaget kunde inte tas fram: ${error.message}. Skriv “försök igen” när du vill försöka på nytt.`);
}
setStage('world-draft');
@@ -692,6 +716,7 @@
}
appendLine('user', message);
messageField.disabled = true;
setActivityStatus('Sparar det godkända världsförslaget och skapar din startposition.');
try {
gameState = await request('/api/world/start', {
method: 'POST', headers: {'content-type': 'application/json'}, body: JSON.stringify(worldDraft),
@@ -699,8 +724,10 @@
worldDraft = null;
await loadSavedDialogue();
setStage('game');
clearActivityStatus();
appendLine('assistant', 'Du kan fråga om platsen, dina saker eller din ekonomi, vänta, handla, prata med någon som finns här eller skriva vart du vill gå.');
} catch (error) {
clearActivityStatus();
appendLine('assistant', `Världsförslaget kunde inte sparas: ${error.message}. Förslaget finns kvar så att du kan försöka igen.`);
}
messageField.disabled = false;
@@ -924,6 +951,7 @@
}
appendLine('user', message);
messageField.disabled = true;
setActivityStatus('Berättaren ber AI skapa eller uppdatera ett biografiförslag utifrån det du skrev.');
try {
const proposal = await request('/api/character/draft', {
method: 'POST',
@@ -931,8 +959,10 @@
body: JSON.stringify({input: message, current_draft: characterDraft}),
});
characterDraft = proposal.draft;
clearActivityStatus();
appendLine('assistant', `${proposal.message}\n\nFörslag: ${characterDraft.name}, ${characterDraft.age} år.\n${characterDraft.biography}${biographyContextText(characterDraft.life_context)}\n\nSkriv ändringar eller “godkänn”.`);
} catch (error) {
clearActivityStatus();
appendLine('assistant', `Jag kunde inte ta fram ett biografiförslag: ${error.message}. Försök igen när tjänsten svarar.`);
}
messageField.disabled = false;
@@ -977,6 +1007,11 @@
if (actionMode === 'normal') pendingDialogue = {message, idempotencyKey};
appendLine('user', message, actionMode === 'dry_run' ? {speaker: 'Torrkörning'} : actionMode === 'redo' ? {speaker: 'Gör om'} : {});
messageField.disabled = true;
setActivityStatus(actionMode === 'dry_run'
? 'Torrkör handlingen utan att spara eller göra framsteg.'
: actionMode === 'redo'
? 'Gör om den senaste handlingen med aktuella regler.'
: 'Berättaren arbetar med din avsikt. Den avgör om en plats, scen eller person behöver etableras och fortsätter sedan berättelsen.');
try {
let clientAssistance = null;
if (actionMode === 'normal' && currentAccount && !currentAccount.is_admin && window.localGameAI) {
@@ -1017,7 +1052,11 @@
if (refreshed.world) gameState = refreshed;
} catch (_) { /* The committed dialogue stays successful if this optional view refresh fails. */ }
}
clearActivityStatus();
const speakingNpc = result.status !== 'dry_run' && ['speak', 'meet'].includes(result.intent) ? gameState.world.npc : null;
for (const change of result.world_changes || []) {
appendLine('system', change, {speaker: 'Världsändring', speak: false});
}
for (const ruleName of result.debug_rules_created || []) {
appendLine('assistant', ruleName, {speaker: 'Nya regler', speak: false});
}
@@ -1031,6 +1070,7 @@
}
refreshActionTools();
} catch (error) {
clearActivityStatus();
messageField.value = message;
try {
const history = [...transcript.querySelectorAll('li')].map(line => line.textContent).filter(Boolean);