Gate local Gemma behind explicit terms acceptance
Build and publish frontend / publish (push) Successful in 45s

This commit is contained in:
Ein Anderssono
2026-10-19 12:03:51 +02:00
parent bb317a9366
commit b57c4c075f
6 changed files with 93 additions and 3 deletions
+52 -2
View File
@@ -116,6 +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>
const auth = document.querySelector('#auth');
const play = document.querySelector('#play');
@@ -162,6 +163,7 @@
line.dataset.role = role;
line.textContent = `${options.speaker || (role === 'user' ? 'Du' : 'Berättelsen')}: ${content}`;
transcript.append(line);
if (role === 'user' || role === 'assistant') window.localGameAI?.record({type: 'dialogue', role, content});
line.scrollIntoView({block: 'end'});
if (role === 'assistant') {
latestAssistantText = content;
@@ -206,6 +208,28 @@
const authVoiceStatus = document.querySelector('#auth-voice-status');
if (authVoiceStatus && !auth.hidden && message) authVoiceStatus.textContent = message;
}
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;
statusLine.replaceChildren(document.createTextNode(`Lokal AI: ${detail.text || ''}`));
if (detail.type === 'terms-required') {
statusLine.append(document.createTextNode(' Läs dokumenten: '));
for (const [label, url] of [['Spelvillkor för Gemma', detail.user_terms], ['Gemma-villkor', detail.terms], ['policy för förbjuden användning', detail.policy], ['modellinformation', detail.notice]]) {
if (!url) continue;
const link = document.createElement('a');
link.href = url; link.target = '_blank'; link.rel = 'noopener noreferrer';
link.textContent = label;
statusLine.append(link, document.createTextNode('. '));
}
statusLine.append(document.createElement('br'), document.createTextNode(detail.command || ''));
}
});
function updateSpeechProgress(message = '') {
const container = document.querySelector('#speech-progress-container');
const progress = document.querySelector('#speech-progress');
@@ -700,6 +724,7 @@
currentAccount = await request('/api/me');
gameState = await request('/api/world');
showPlay();
if (!currentAccount.is_admin) window.localGameAI?.start(currentAccount.id);
if (!gameState.character) {
transcript.replaceChildren();
appendInitialPrompt();
@@ -866,6 +891,7 @@
presenceTimer = null;
playerMessagesPrimed = false;
seenPlayerMessageIds.clear();
window.localGameAI?.stop();
await request('/api/logout', {method: 'POST'});
if (location.pathname !== '/admin') appendLine('assistant', 'Du har loggat ut. Logga in igen för att fortsätta spela.');
characterDraft = null;
@@ -918,6 +944,12 @@
document.querySelector('#dry-run-action').disabled = !enabled;
}
async function handleGameMessage(message, actionMode = 'normal') {
if (actionMode === 'normal' && /^lokal ai(?:\s|$)/i.test(message.trim())) {
const response = await window.localGameAI?.settings(message);
appendLine('user', message);
appendLine('assistant', response || 'Kommandon: “lokal ai villkor”, “lokal ai godkänn villkor”, “lokal ai avvisa villkor”, “lokal ai på/av”, “lokal ai loggar på/av”, “lokal ai exportera loggar” och “lokal ai radera loggar”.');
return;
}
if (/^logga ut[.!]?$/i.test(message.trim())) {
appendLine('user', message);
await handleLogout();
@@ -946,13 +978,26 @@
appendLine('user', message, actionMode === 'dry_run' ? {speaker: 'Torrkörning'} : actionMode === 'redo' ? {speaker: 'Gör om'} : {});
messageField.disabled = true;
try {
let clientAssistance = null;
if (actionMode === 'normal' && currentAccount && !currentAccount.is_admin && window.localGameAI) {
try {
const history = [...transcript.querySelectorAll('li')].map(line => line.textContent).filter(Boolean);
const assisted = await window.localGameAI.assist(message, history, gameState?.world || {});
clientAssistance = {
intent_hint: String(assisted.intent_hint || '').slice(0, 500),
warning: String(assisted.warning || '').slice(0, 500),
excerpts: Array.isArray(assisted.excerpts) ? assisted.excerpts.filter(item => typeof item === 'string' && item.length <= 500 && history.some(line => line.includes(item))).slice(0, 3) : [],
};
if (clientAssistance.warning) appendLine('system', `Lokal textkontroll: ${clientAssistance.warning}`, {speaker: 'Lokal AI', speak: false});
} catch (_) { /* Local assistance is optional; the original input always proceeds. */ }
}
let result;
for (let attempt = 0; attempt < 2; attempt += 1) {
try {
result = await request('/api/dialogue', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({message, expected_world_version: gameState.world.version, idempotency_key: idempotencyKey, forget_previous_turn: actionMode !== 'normal', dry_run: actionMode === 'dry_run'}),
body: JSON.stringify({message, expected_world_version: gameState.world.version, idempotency_key: idempotencyKey, forget_previous_turn: actionMode !== 'normal', dry_run: actionMode === 'dry_run', client_assistance: clientAssistance}),
});
break;
} catch (error) {
@@ -987,7 +1032,12 @@
refreshActionTools();
} catch (error) {
messageField.value = message;
appendLine('assistant', `${explainDialogueError(error)} Ditt meddelande ligger kvar i inmatningen.`);
try {
const history = [...transcript.querySelectorAll('li')].map(line => line.textContent).filter(Boolean);
const localReply = await window.localGameAI?.offline(message, history, gameState?.world || {});
if (localReply) appendLine('assistant', `Lokalt, osparat förslag: ${localReply}`, {speaker: 'Lokal hjälp'});
else appendLine('assistant', `${explainDialogueError(error)} Ditt meddelande ligger kvar i inmatningen.`);
} catch (_) { appendLine('assistant', `${explainDialogueError(error)} Ditt meddelande ligger kvar i inmatningen.`); }
}
messageField.disabled = false;
messageField.focus();