2 Commits
Author SHA1 Message Date
Gitea Actions 0c1bba2a22 Release 0.1.6 [auto-version-bump] 2026-10-19 12:40:04 +02:00
Ein Anderssono 214f3b5cbd Show local AI activity feedback
Build and publish static assets / publish (push) Successful in 8m21s
2026-10-19 12:36:43 +02:00
11 changed files with 70 additions and 48 deletions
+27 -27
View File
@@ -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/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/sv/sv_SE/nst/medium/sv_SE-nst-medium.onnx
test -s public/static/piper-wasm-v1/voices/ATTRIBUTION.txt 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-v2/manifest.json
test -s public/static/wllama-gemma3-v1/licenses/GEMMA_USER_TERMS.html test -s public/static/wllama-gemma3-v2/licenses/GEMMA_USER_TERMS.html
test -s public/static/wllama-gemma3-v1/licenses/GEMMA_TERMS.html test -s public/static/wllama-gemma3-v2/licenses/GEMMA_TERMS.html
test -s public/static/wllama-gemma3-v1/licenses/GEMMA_PROHIBITED_USE_POLICY.html test -s public/static/wllama-gemma3-v2/licenses/GEMMA_PROHIBITED_USE_POLICY.html
test -s public/static/wllama-gemma3-v1/licenses/GEMMA_NOTICE.txt test -s public/static/wllama-gemma3-v2/licenses/GEMMA_NOTICE.txt
test -s public/static/wllama-gemma3-v1/licenses/GEMMA_SOURCE.txt test -s public/static/wllama-gemma3-v2/licenses/GEMMA_SOURCE.txt
test -s public/static/wllama-gemma3-v1/licenses/WLLAMA_LICENSE.txt test -s public/static/wllama-gemma3-v2/licenses/WLLAMA_LICENSE.txt
test -s public/static/wllama-gemma3-v1/licenses/LLAMA_CPP_LICENSE.txt test -s public/static/wllama-gemma3-v2/licenses/LLAMA_CPP_LICENSE.txt
python3 - <<'PY' python3 - <<'PY'
import json 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"]["bytes"] == 806058240
assert m["model"]["sha256"] == "8ccc5cd1f1b3602548715ae25a66ed73fd5dc68a210412eea643eb20eb75a135" assert m["model"]["sha256"] == "8ccc5cd1f1b3602548715ae25a66ed73fd5dc68a210412eea643eb20eb75a135"
assert len(m["license"]["acceptance_id"]) == 64 assert len(m["license"]["acceptance_id"]) == 64
required = ["user_terms", "terms", "prohibited_use_policy", "notice", "source", "runtime", "llama_cpp_runtime"] required = ["user_terms", "terms", "prohibited_use_policy", "notice", "source", "runtime", "llama_cpp_runtime"]
assert all(m["license"][key].startswith("/static/") for key in required) assert all(m["license"][key].startswith("/static/") for key in required)
PY PY
test -s public/static/wllama-gemma3-v1/browser-assistance.yaml test -s public/static/wllama-gemma3-v2/browser-assistance.yaml
test -s public/static/wllama-gemma3-v1/local-ai.js test -s public/static/wllama-gemma3-v2/local-ai.js
test -s public/static/wllama-gemma3-v1/wllama/esm/index.js test -s public/static/wllama-gemma3-v2/wllama/esm/index.js
test -s public/static/wllama-gemma3-v1/wllama/esm/wasm/wllama.wasm test -s public/static/wllama-gemma3-v2/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 "$(stat -c %s public/static/wllama-gemma3-v2/models/gemma-3-1b-it-Q4_K_M.gguf)" = 806058240
grep -q 'version.json' Dockerfile grep -q 'version.json' Dockerfile
- name: Ensure Helm is available - name: Ensure Helm is available
@@ -174,21 +174,21 @@ jobs:
/static/piper-wasm-v1/onnx/ort-wasm-simd-threaded.wasm \ /static/piper-wasm-v1/onnx/ort-wasm-simd-threaded.wasm \
/static/piper-wasm-v1/piper/piper_phonemize.data \ /static/piper-wasm-v1/piper/piper_phonemize.data \
/static/piper-wasm-v1/voices/sv/sv_SE/nst/medium/sv_SE-nst-medium.onnx \ /static/piper-wasm-v1/voices/sv/sv_SE/nst/medium/sv_SE-nst-medium.onnx \
/static/wllama-gemma3-v1/manifest.json \ /static/wllama-gemma3-v2/manifest.json \
/static/wllama-gemma3-v1/browser-assistance.yaml \ /static/wllama-gemma3-v2/browser-assistance.yaml \
/static/wllama-gemma3-v1/local-ai.js \ /static/wllama-gemma3-v2/local-ai.js \
/static/wllama-gemma3-v1/wllama/esm/index.js \ /static/wllama-gemma3-v2/wllama/esm/index.js \
/static/wllama-gemma3-v1/wllama/esm/wasm/wllama.wasm \ /static/wllama-gemma3-v2/wllama/esm/wasm/wllama.wasm \
/static/wllama-gemma3-v1/models/gemma-3-1b-it-Q4_K_M.gguf; do /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 curl -fsSI "https://ai-spel.k3s.k0.nu$path" >/dev/null
done done
for path in \ for path in \
/static/wllama-gemma3-v1/licenses/GEMMA_TERMS.html \ /static/wllama-gemma3-v2/licenses/GEMMA_TERMS.html \
/static/wllama-gemma3-v1/licenses/GEMMA_USER_TERMS.html \ /static/wllama-gemma3-v2/licenses/GEMMA_USER_TERMS.html \
/static/wllama-gemma3-v1/licenses/GEMMA_PROHIBITED_USE_POLICY.html \ /static/wllama-gemma3-v2/licenses/GEMMA_PROHIBITED_USE_POLICY.html \
/static/wllama-gemma3-v1/licenses/GEMMA_NOTICE.txt \ /static/wllama-gemma3-v2/licenses/GEMMA_NOTICE.txt \
/static/wllama-gemma3-v1/licenses/GEMMA_SOURCE.txt \ /static/wllama-gemma3-v2/licenses/GEMMA_SOURCE.txt \
/static/wllama-gemma3-v1/licenses/WLLAMA_LICENSE.txt \ /static/wllama-gemma3-v2/licenses/WLLAMA_LICENSE.txt \
/static/wllama-gemma3-v1/licenses/LLAMA_CPP_LICENSE.txt; do /static/wllama-gemma3-v2/licenses/LLAMA_CPP_LICENSE.txt; do
curl -fsSI "https://ai-spel.k3s.k0.nu$path" >/dev/null curl -fsSI "https://ai-spel.k3s.k0.nu$path" >/dev/null
done done
+1 -1
View File
@@ -7,7 +7,7 @@
- Git release commit/tag, image, Helm chart, and manifest use the same version. - 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`. - 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/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. - 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. - 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. - No secrets or runtime network access.
+1 -1
View File
@@ -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/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/onnx/` — ONNX Runtime WASM and ES module loader.
- `/static/piper-wasm-v1/piper/` — Piper phonemizer WASM and data. - `/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. - 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.txt` — active deployment version, without long-lived caching.
- `/static/version.json` — active deployment version as `{"version":"<semver>"}`, generated from the same `VERSION` value used by the image and Helm chart and checked after deployment. - `/static/version.json` — active deployment version as `{"version":"<semver>"}`, generated from the same `VERSION` value used by the image and Helm chart and checked after deployment.
+1 -1
View File
@@ -1 +1 @@
0.1.5 0.1.6
+17 -2
View File
@@ -1,6 +1,6 @@
/* Browser-only, best-effort assistance. All executable/model/prompt assets come from our static service. */ /* Browser-only, best-effort assistance. All executable/model/prompt assets come from our static service. */
(function (global) { (function (global) {
const base = '/static/wllama-gemma3-v1/'; const base = '/static/wllama-gemma3-v2/';
const dbName = 'ai-spel-local-assistance-v1'; const dbName = 'ai-spel-local-assistance-v1';
let enginePromise; let enginePromise;
let loadedEngine; let loadedEngine;
@@ -161,12 +161,27 @@
} }
async function assist(message, transcript, worldSnapshot) { async function assist(message, transcript, worldSnapshot) {
if (!enabled || !loadedEngine || !await isAccepted()) return {}; 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) { async function offline(message, transcript, worldSnapshot) {
if (!enabled || !loadedEngine || !await isAccepted()) return ''; if (!enabled || !loadedEngine || !await isAccepted()) return '';
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); 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.'); 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) { async function settings(command) {
const normalized = command.trim().toLocaleLowerCase('sv'); const normalized = command.trim().toLocaleLowerCase('sv');
+2 -2
View File
@@ -2,5 +2,5 @@ apiVersion: v2
name: ai-spel-static name: ai-spel-static
description: Long-lived static Piper assets for the Swedish life simulator description: Long-lived static Piper assets for the Swedish life simulator
type: application type: application
version: 0.1.5 version: 0.1.6
appVersion: "0.1.5" appVersion: "0.1.6"
+3 -3
View File
@@ -21,21 +21,21 @@ server {
try_files $uri =404; try_files $uri =404;
} }
location = /static/wllama-gemma3-v1/manifest.json { location = /static/wllama-gemma3-v2/manifest.json {
default_type application/json; default_type application/json;
add_header Cache-Control "no-cache" always; add_header Cache-Control "no-cache" always;
add_header Cross-Origin-Resource-Policy "same-origin" always; add_header Cross-Origin-Resource-Policy "same-origin" always;
try_files $uri =404; try_files $uri =404;
} }
location = /static/wllama-gemma3-v1/local-ai.js { location = /static/wllama-gemma3-v2/local-ai.js {
default_type application/javascript; default_type application/javascript;
add_header Cache-Control "no-cache" always; add_header Cache-Control "no-cache" always;
add_header Cross-Origin-Resource-Policy "same-origin" always; add_header Cross-Origin-Resource-Policy "same-origin" always;
try_files $uri =404; try_files $uri =404;
} }
location ~ ^/static/wllama-gemma3-v1/licenses/ { location ~ ^/static/wllama-gemma3-v2/licenses/ {
types { types {
text/html html; text/html html;
text/plain txt; text/plain txt;
+1 -1
View File
@@ -10,7 +10,7 @@ const root = process.cwd();
const output = join(root, 'public', 'static', assetPrefix); const output = join(root, 'public', 'static', assetPrefix);
const onnxSource = join(root, 'node_modules', 'onnxruntime-web', 'dist'); const onnxSource = join(root, 'node_modules', 'onnxruntime-web', 'dist');
const piperSource = join(root, 'node_modules', '@diffusionstudio', 'piper-wasm', 'build'); 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 wllamaUrl = 'https://registry.npmjs.org/@wllama/wllama/-/wllama-3.8.1.tgz';
const wllamaSha512 = 'Pl7BEJhPU8OHnV/yhn3DFZYhLLOdSHh+4TND79WCftHnlUAdQPAsyc/CfIOY+4mVxvf5VW3Xw3Uf7BBfotS1gw=='; 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'; const gemmaUrl = 'https://huggingface.co/ggml-org/gemma-3-1b-it-GGUF/resolve/f9c28bcd85737ffc5aef028638d3341d49869c27/gemma-3-1b-it-Q4_K_M.gguf';
+4 -4
View File
@@ -12,14 +12,14 @@ grep -q 'application/wasm wasm;' nginx/default.conf
grep -Fq 'try_files $uri =404;' nginx/default.conf grep -Fq 'try_files $uri =404;' nginx/default.conf
grep -q 'version.json' Dockerfile grep -q 'version.json' Dockerfile
grep -q 'location = /static/version.json' nginx/default.conf 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-v2/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/local-ai.js' nginx/default.conf
grep -q 'Cache-Control "no-cache"' nginx/default.conf grep -q 'Cache-Control "no-cache"' nginx/default.conf
grep -q 'text/html html;' nginx/default.conf grep -q 'text/html html;' nginx/default.conf
grep -q 'application/octet-stream onnx data gguf;' 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 "client', 'local-ai.js" scripts/build-assets.mjs
grep -q 'wllama-gemma3-v1' scripts/build-assets.mjs grep -q 'wllama-gemma3-v2' scripts/build-assets.mjs
grep -q 'test -s public/static/wllama-gemma3-v1/local-ai.js' .gitea/workflows/publish.yaml 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 'lokal ai godkänn villkor' client/local-ai.js
grep -q 'acceptance_id' scripts/build-assets.mjs grep -q 'acceptance_id' scripts/build-assets.mjs
grep -q 'GEMMA_TERMS.html' .gitea/workflows/publish.yaml grep -q 'GEMMA_TERMS.html' .gitea/workflows/publish.yaml
+3 -3
View File
@@ -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 () => { 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'); 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('wllama-3.8.1.tgz'));
assert.ok(source.includes('gemma-3-1b-it-Q4_K_M.gguf')); assert.ok(source.includes('gemma-3-1b-it-Q4_K_M.gguf'));
assert.ok(source.includes('manifest.json')); 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.match(terms, /must include the use restrictions/);
assert.doesNotMatch(terms, /<script|google-signin|google-analytics/); assert.doesNotMatch(terms, /<script|google-signin|google-analytics/);
const nginx = fs.readFileSync(path.join(__dirname, '..', 'nginx', 'default.conf'), 'utf8'); const nginx = fs.readFileSync(path.join(__dirname, '..', 'nginx', 'default.conf'), 'utf8');
assert.match(nginx, /location = \/static\/wllama-gemma3-v1\/manifest\.json[\s\S]*?Cache-Control "no-cache"/); assert.match(nginx, /location = \/static\/wllama-gemma3-v2\/manifest\.json[\s\S]*?Cache-Control "no-cache"/);
assert.match(nginx, /location = \/static\/wllama-gemma3-v1\/local-ai\.js[\s\S]*?Cache-Control "no-cache"/); assert.match(nginx, /location = \/static\/wllama-gemma3-v2\/local-ai\.js[\s\S]*?Cache-Control "no-cache"/);
}); });
+8 -1
View File
@@ -8,7 +8,7 @@ const source = fs.readFileSync(path.join(__dirname, '..', 'client', 'local-ai.js
test('local AI runtime rejects assets outside the same-origin static service', () => { test('local AI runtime rejects assets outside the same-origin static service', () => {
assert.match(source, /parsed\.origin !== location\.origin/); assert.match(source, /parsed\.origin !== location\.origin/);
assert.match(source, /parsed\.pathname\.startsWith\('\/static\/'\)/); 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/); 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/); 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', () => { test('local diagnostics are opt-in, stored in IndexedDB, exportable, and deletable', () => {
assert.match(source, /ai-spel-local-ai-logs/); assert.match(source, /ai-spel-local-ai-logs/);
assert.match(source, /indexedDB\.open/); assert.match(source, /indexedDB\.open/);