From 214f3b5cbd48c15193e570c9f5fa85bb6715be2e Mon Sep 17 00:00:00 2001 From: Ein Anderssono Date: Mon, 19 Oct 2026 12:36:37 +0200 Subject: [PATCH] Show local AI activity feedback --- .gitea/workflows/publish.yaml | 54 +++++++++++++++++------------------ AGENTS.md | 2 +- README.md | 2 +- client/local-ai.js | 23 ++++++++++++--- nginx/default.conf | 6 ++-- scripts/build-assets.mjs | 2 +- scripts/check-static.sh | 8 +++--- tests/assets.test.cjs | 6 ++-- tests/local-ai.test.cjs | 9 +++++- 9 files changed, 67 insertions(+), 45 deletions(-) diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml index 4a8601e..e565348 100644 --- a/.gitea/workflows/publish.yaml +++ b/.gitea/workflows/publish.yaml @@ -40,28 +40,28 @@ jobs: test -s public/static/piper-wasm-v1/voices/sv/sv_SE/lisa/medium/sv_SE-lisa-medium.onnx test -s public/static/piper-wasm-v1/voices/sv/sv_SE/nst/medium/sv_SE-nst-medium.onnx test -s public/static/piper-wasm-v1/voices/ATTRIBUTION.txt - test -s public/static/wllama-gemma3-v1/manifest.json - test -s public/static/wllama-gemma3-v1/licenses/GEMMA_USER_TERMS.html - test -s public/static/wllama-gemma3-v1/licenses/GEMMA_TERMS.html - test -s public/static/wllama-gemma3-v1/licenses/GEMMA_PROHIBITED_USE_POLICY.html - test -s public/static/wllama-gemma3-v1/licenses/GEMMA_NOTICE.txt - test -s public/static/wllama-gemma3-v1/licenses/GEMMA_SOURCE.txt - test -s public/static/wllama-gemma3-v1/licenses/WLLAMA_LICENSE.txt - test -s public/static/wllama-gemma3-v1/licenses/LLAMA_CPP_LICENSE.txt + test -s public/static/wllama-gemma3-v2/manifest.json + test -s public/static/wllama-gemma3-v2/licenses/GEMMA_USER_TERMS.html + test -s public/static/wllama-gemma3-v2/licenses/GEMMA_TERMS.html + test -s public/static/wllama-gemma3-v2/licenses/GEMMA_PROHIBITED_USE_POLICY.html + test -s public/static/wllama-gemma3-v2/licenses/GEMMA_NOTICE.txt + test -s public/static/wllama-gemma3-v2/licenses/GEMMA_SOURCE.txt + test -s public/static/wllama-gemma3-v2/licenses/WLLAMA_LICENSE.txt + test -s public/static/wllama-gemma3-v2/licenses/LLAMA_CPP_LICENSE.txt python3 - <<'PY' import json - m = json.load(open("public/static/wllama-gemma3-v1/manifest.json", encoding="utf-8")) + m = json.load(open("public/static/wllama-gemma3-v2/manifest.json", encoding="utf-8")) assert m["model"]["bytes"] == 806058240 assert m["model"]["sha256"] == "8ccc5cd1f1b3602548715ae25a66ed73fd5dc68a210412eea643eb20eb75a135" assert len(m["license"]["acceptance_id"]) == 64 required = ["user_terms", "terms", "prohibited_use_policy", "notice", "source", "runtime", "llama_cpp_runtime"] assert all(m["license"][key].startswith("/static/") for key in required) PY - test -s public/static/wllama-gemma3-v1/browser-assistance.yaml - test -s public/static/wllama-gemma3-v1/local-ai.js - test -s public/static/wllama-gemma3-v1/wllama/esm/index.js - test -s public/static/wllama-gemma3-v1/wllama/esm/wasm/wllama.wasm - test "$(stat -c %s public/static/wllama-gemma3-v1/models/gemma-3-1b-it-Q4_K_M.gguf)" = 806058240 + test -s public/static/wllama-gemma3-v2/browser-assistance.yaml + test -s public/static/wllama-gemma3-v2/local-ai.js + test -s public/static/wllama-gemma3-v2/wllama/esm/index.js + test -s public/static/wllama-gemma3-v2/wllama/esm/wasm/wllama.wasm + test "$(stat -c %s public/static/wllama-gemma3-v2/models/gemma-3-1b-it-Q4_K_M.gguf)" = 806058240 grep -q 'version.json' Dockerfile - name: Ensure Helm is available @@ -174,21 +174,21 @@ jobs: /static/piper-wasm-v1/onnx/ort-wasm-simd-threaded.wasm \ /static/piper-wasm-v1/piper/piper_phonemize.data \ /static/piper-wasm-v1/voices/sv/sv_SE/nst/medium/sv_SE-nst-medium.onnx \ - /static/wllama-gemma3-v1/manifest.json \ - /static/wllama-gemma3-v1/browser-assistance.yaml \ - /static/wllama-gemma3-v1/local-ai.js \ - /static/wllama-gemma3-v1/wllama/esm/index.js \ - /static/wllama-gemma3-v1/wllama/esm/wasm/wllama.wasm \ - /static/wllama-gemma3-v1/models/gemma-3-1b-it-Q4_K_M.gguf; do + /static/wllama-gemma3-v2/manifest.json \ + /static/wllama-gemma3-v2/browser-assistance.yaml \ + /static/wllama-gemma3-v2/local-ai.js \ + /static/wllama-gemma3-v2/wllama/esm/index.js \ + /static/wllama-gemma3-v2/wllama/esm/wasm/wllama.wasm \ + /static/wllama-gemma3-v2/models/gemma-3-1b-it-Q4_K_M.gguf; do curl -fsSI "https://ai-spel.k3s.k0.nu$path" >/dev/null done for path in \ - /static/wllama-gemma3-v1/licenses/GEMMA_TERMS.html \ - /static/wllama-gemma3-v1/licenses/GEMMA_USER_TERMS.html \ - /static/wllama-gemma3-v1/licenses/GEMMA_PROHIBITED_USE_POLICY.html \ - /static/wllama-gemma3-v1/licenses/GEMMA_NOTICE.txt \ - /static/wllama-gemma3-v1/licenses/GEMMA_SOURCE.txt \ - /static/wllama-gemma3-v1/licenses/WLLAMA_LICENSE.txt \ - /static/wllama-gemma3-v1/licenses/LLAMA_CPP_LICENSE.txt; do + /static/wllama-gemma3-v2/licenses/GEMMA_TERMS.html \ + /static/wllama-gemma3-v2/licenses/GEMMA_USER_TERMS.html \ + /static/wllama-gemma3-v2/licenses/GEMMA_PROHIBITED_USE_POLICY.html \ + /static/wllama-gemma3-v2/licenses/GEMMA_NOTICE.txt \ + /static/wllama-gemma3-v2/licenses/GEMMA_SOURCE.txt \ + /static/wllama-gemma3-v2/licenses/WLLAMA_LICENSE.txt \ + /static/wllama-gemma3-v2/licenses/LLAMA_CPP_LICENSE.txt; do curl -fsSI "https://ai-spel.k3s.k0.nu$path" >/dev/null done diff --git a/AGENTS.md b/AGENTS.md index 4b5e418..4525df9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ - Git release commit/tag, image, Helm chart, and manifest use the same version. - Use Harbor project `ai-spel`, Argo CD project `ai-spel`, and Kubernetes namespace `ai-spel`. - `/static/piper-wasm-v1/` is immutable and cached for one year. Incompatible asset updates require a new path and coordinated frontend change. -- `/static/wllama-gemma3-v1/` serves the client adapter, pinned Wllama WebAssembly/runtime, the Gemma 3 1B Q4_K_M model, its manifest, and its YAML prompt. Browser runtime requests must remain same-origin under `/static/`; upstream URLs are allowed only in the static CI asset fetcher, never in client runtime code. Any changed adapter/model/runtime/prompt gets a new immutable path and coordinated frontend update. +- `/static/wllama-gemma3-v2/` serves the client adapter, pinned Wllama WebAssembly/runtime, the Gemma 3 1B Q4_K_M model, its manifest, and its YAML prompt. Browser runtime requests must remain same-origin under `/static/`; upstream URLs are allowed only in the static CI asset fetcher, never in client runtime code. Any changed adapter/model/runtime/prompt gets a new immutable path and coordinated frontend update. - Gemma publication is blocked unless the static release includes the applicable Gemma Terms of Use copy, prohibited-use policy reference, required Gemma notice, model provenance, and third-party runtime/license notices. The frontend must obtain explicit user acceptance before model download/use; do not publish or enable Gemma until both the legal resources and the acceptance gate are implemented and verified in CI. - Publish `/static/version.json` with `version` generated from `VERSION` in the same image as the Helm appVersion; keep it uncached and verify its value after rollout. The frontend footer displays this release independently from frontend and API versions. - No secrets or runtime network access. diff --git a/README.md b/README.md index db80490..9c7e36b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Assets under `/static/piper-wasm-v1/` are served with `Cache-Control: public, ma - `/static/piper-wasm-v1/voices/` — Alma, Lisa, and NST Swedish voices, plus attribution. - `/static/piper-wasm-v1/onnx/` — ONNX Runtime WASM and ES module loader. - `/static/piper-wasm-v1/piper/` — Piper phonemizer WASM and data. -- `/static/wllama-gemma3-v1/` — local AI client adapter, integrity-pinned Wllama WebAssembly runtime, Gemma 3 1B Q4_K_M (806 MB), static manifest, and YAML assistance prompt. The client downloads these only from this server and caches the model on-device. +- `/static/wllama-gemma3-v2/` — local AI client adapter, integrity-pinned Wllama WebAssembly runtime, Gemma 3 1B Q4_K_M (806 MB), static manifest, and YAML assistance prompt. The client downloads these only from this server and caches the model on-device. - Gemma distribution must also include the applicable Gemma Terms of Use copy, required notice, prohibited-use policy reference, source/conversion provenance, and all runtime/model license notices. The frontend must obtain explicit acceptance before download or use. Do not release the Gemma files until those resources and the client acceptance gate are implemented and checked by CI. - `/static/version.txt` — active deployment version, without long-lived caching. - `/static/version.json` — active deployment version as `{"version":""}`, generated from the same `VERSION` value used by the image and Helm chart and checked after deployment. diff --git a/client/local-ai.js b/client/local-ai.js index 541baa4..888202e 100644 --- a/client/local-ai.js +++ b/client/local-ai.js @@ -1,6 +1,6 @@ /* Browser-only, best-effort assistance. All executable/model/prompt assets come from our static service. */ (function (global) { - const base = '/static/wllama-gemma3-v1/'; + const base = '/static/wllama-gemma3-v2/'; const dbName = 'ai-spel-local-assistance-v1'; let enginePromise; let loadedEngine; @@ -161,12 +161,27 @@ } async function assist(message, transcript, worldSnapshot) { if (!enabled || !loadedEngine || !await isAccepted()) return {}; - return boundedInfer('prepare_turn', {player_message: message.slice(0, 1200), recent_dialogue: transcript.slice(-12).map(line => line.slice(0, 240)), world_snapshot: JSON.stringify(worldSnapshot).slice(0, 1200)}, 5000); + announce({type: 'activity', text: 'Lokal Gemma 3 körs på den här enheten för att förbereda handlingen.'}); + try { + const result = await boundedInfer('prepare_turn', {player_message: message.slice(0, 1200), recent_dialogue: transcript.slice(-12).map(line => line.slice(0, 240)), world_snapshot: JSON.stringify(worldSnapshot).slice(0, 1200)}, 5000); + announce({type: 'activity', text: 'Lokal Gemma 3 är klar. Originalhandlingen skickas nu vidare till berättartjänsten.'}); + return result; + } catch (error) { + announce({type: 'activity', text: 'Lokal Gemma 3 kunde inte förbereda handlingen. Originaltexten skickas ändå vidare till berättartjänsten.'}); + throw error; + } } async function offline(message, transcript, worldSnapshot) { if (!enabled || !loadedEngine || !await isAccepted()) return ''; - const result = await boundedInfer('offline_help', {player_message: message.slice(0, 1200), recent_dialogue: transcript.slice(-12).map(line => line.slice(0, 240)), world_snapshot: JSON.stringify(worldSnapshot).slice(0, 1200)}, 10000); - return String(result.offline_reply || 'Jag kan inte skapa ett lokalt förslag med den information som finns sparad.'); + announce({type: 'activity', text: 'Lokal Gemma 3 arbetar offline med ett osparat hjälpförslag.'}); + try { + const result = await boundedInfer('offline_help', {player_message: message.slice(0, 1200), recent_dialogue: transcript.slice(-12).map(line => line.slice(0, 240)), world_snapshot: JSON.stringify(worldSnapshot).slice(0, 1200)}, 10000); + announce({type: 'activity', text: 'Lokal Gemma 3 har skapat ett osparat hjälpförslag.'}); + return String(result.offline_reply || 'Jag kan inte skapa ett lokalt förslag med den information som finns sparad.'); + } catch (error) { + announce({type: 'activity', text: 'Lokal Gemma 3 kunde inte skapa ett offlineförslag.'}); + throw error; + } } async function settings(command) { const normalized = command.trim().toLocaleLowerCase('sv'); diff --git a/nginx/default.conf b/nginx/default.conf index 84703ef..625ec47 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -21,21 +21,21 @@ server { try_files $uri =404; } - location = /static/wllama-gemma3-v1/manifest.json { + location = /static/wllama-gemma3-v2/manifest.json { default_type application/json; add_header Cache-Control "no-cache" always; add_header Cross-Origin-Resource-Policy "same-origin" always; try_files $uri =404; } - location = /static/wllama-gemma3-v1/local-ai.js { + location = /static/wllama-gemma3-v2/local-ai.js { default_type application/javascript; add_header Cache-Control "no-cache" always; add_header Cross-Origin-Resource-Policy "same-origin" always; try_files $uri =404; } - location ~ ^/static/wllama-gemma3-v1/licenses/ { + location ~ ^/static/wllama-gemma3-v2/licenses/ { types { text/html html; text/plain txt; diff --git a/scripts/build-assets.mjs b/scripts/build-assets.mjs index 1a12e28..1ee7571 100644 --- a/scripts/build-assets.mjs +++ b/scripts/build-assets.mjs @@ -10,7 +10,7 @@ const root = process.cwd(); const output = join(root, 'public', 'static', assetPrefix); const onnxSource = join(root, 'node_modules', 'onnxruntime-web', 'dist'); const piperSource = join(root, 'node_modules', '@diffusionstudio', 'piper-wasm', 'build'); -const localAiPrefix = 'wllama-gemma3-v1'; +const localAiPrefix = 'wllama-gemma3-v2'; const wllamaUrl = 'https://registry.npmjs.org/@wllama/wllama/-/wllama-3.8.1.tgz'; const wllamaSha512 = 'Pl7BEJhPU8OHnV/yhn3DFZYhLLOdSHh+4TND79WCftHnlUAdQPAsyc/CfIOY+4mVxvf5VW3Xw3Uf7BBfotS1gw=='; const gemmaUrl = 'https://huggingface.co/ggml-org/gemma-3-1b-it-GGUF/resolve/f9c28bcd85737ffc5aef028638d3341d49869c27/gemma-3-1b-it-Q4_K_M.gguf'; diff --git a/scripts/check-static.sh b/scripts/check-static.sh index c8e4e5f..a1e8279 100755 --- a/scripts/check-static.sh +++ b/scripts/check-static.sh @@ -12,14 +12,14 @@ grep -q 'application/wasm wasm;' nginx/default.conf grep -Fq 'try_files $uri =404;' nginx/default.conf grep -q 'version.json' Dockerfile grep -q 'location = /static/version.json' nginx/default.conf -grep -q 'location = /static/wllama-gemma3-v1/manifest.json' nginx/default.conf -grep -q 'location = /static/wllama-gemma3-v1/local-ai.js' nginx/default.conf +grep -q 'location = /static/wllama-gemma3-v2/manifest.json' nginx/default.conf +grep -q 'location = /static/wllama-gemma3-v2/local-ai.js' nginx/default.conf grep -q 'Cache-Control "no-cache"' nginx/default.conf grep -q 'text/html html;' nginx/default.conf grep -q 'application/octet-stream onnx data gguf;' nginx/default.conf grep -q "client', 'local-ai.js" scripts/build-assets.mjs -grep -q 'wllama-gemma3-v1' scripts/build-assets.mjs -grep -q 'test -s public/static/wllama-gemma3-v1/local-ai.js' .gitea/workflows/publish.yaml +grep -q 'wllama-gemma3-v2' scripts/build-assets.mjs +grep -q 'test -s public/static/wllama-gemma3-v2/local-ai.js' .gitea/workflows/publish.yaml grep -q 'lokal ai godkänn villkor' client/local-ai.js grep -q 'acceptance_id' scripts/build-assets.mjs grep -q 'GEMMA_TERMS.html' .gitea/workflows/publish.yaml diff --git a/tests/assets.test.cjs b/tests/assets.test.cjs index 0a8e7a0..a073732 100644 --- a/tests/assets.test.cjs +++ b/tests/assets.test.cjs @@ -21,7 +21,7 @@ test('browser local AI prompt declares non-authoritative, bounded assistance tas test('local AI runtime and model are published only under the owned static origin', async () => { const source = fs.readFileSync(path.join(__dirname, '..', 'scripts', 'build-assets.mjs'), 'utf8'); - assert.ok(source.includes('wllama-gemma3-v1')); + assert.ok(source.includes('wllama-gemma3-v2')); assert.ok(source.includes('wllama-3.8.1.tgz')); assert.ok(source.includes('gemma-3-1b-it-Q4_K_M.gguf')); assert.ok(source.includes('manifest.json')); @@ -45,6 +45,6 @@ test('Gemma terms, prohibited-use policy, notice, provenance, and runtime licens assert.match(terms, /must include the use restrictions/); assert.doesNotMatch(terms, / { assert.match(source, /parsed\.origin !== location\.origin/); assert.match(source, /parsed\.pathname\.startsWith\('\/static\/'\)/); - assert.match(source, /\/static\/wllama-gemma3-v1\//); + assert.match(source, /\/static\/wllama-gemma3-v2\//); assert.doesNotMatch(source, /huggingface\.co|registry\.npmjs\.org/); }); @@ -19,6 +19,13 @@ test('local assistance includes preflight, bounded continuity, and unsaved offli assert.match(source, /Jag kan inte skapa ett lokalt förslag/); }); +test('local AI reports when on-device inference starts, finishes, or fails', () => { + assert.match(source, /type: 'activity', text: 'Lokal Gemma 3 körs på den här enheten/); + assert.match(source, /Lokal Gemma 3 är klar\. Originalhandlingen skickas nu vidare/); + assert.match(source, /Lokal Gemma 3 kunde inte förbereda handlingen/); + assert.match(source, /Lokal Gemma 3 arbetar offline med ett osparat hjälpförslag/); +}); + test('local diagnostics are opt-in, stored in IndexedDB, exportable, and deletable', () => { assert.match(source, /ai-spel-local-ai-logs/); assert.match(source, /indexedDB\.open/);