Skip to content

Commit dbb571c

Browse files
committed
test: comprehensive test suite — 12 new spec files (108 tests)
- video-player.spec.js: 13 unit tests (URL detection, HTML builders, embed grid) - tts-engine.spec.js: 8 unit tests (TTS API, state management) - speech-commands.spec.js: 10 unit tests (STT DOM, language selector) - file-converters.spec.js: 9 unit tests (MD/CSV/JSON/XML/HTML import) - stock-widget.spec.js: 8 unit tests (TradingView, sandbox, double-render) - video-embed-render.spec.js: 5 integration tests (embed grid pipeline) - model-registry-e2e.spec.js: 8 integration tests (AI_MODELS registry) - regression-recent.spec.js: 12 regression tests (recent bug fixes) - module-init-perf.spec.js: 7 performance tests (module load timing) - static-analysis.spec.js: 6 quality tests (ESLint, file size, eval) - code-smell-extended.spec.js: 8 quality tests (IIFE, workers, HTTPS) - security-extended.spec.js: 14 security tests (XSS, sandbox, privacy) - README: test count 191 → 299, added release note - Total: 299 Playwright tests across all categories
1 parent f7ca256 commit dbb571c

37 files changed

+5246
-68
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
| **💾 Disk Workspace** | Folder-backed storage via File System Access API — "Open Folder" in sidebar header; `.md` files read/written directly to disk; `.textagent/workspace.json` manifest; debounced autosave ("💾 Saved to disk" indicator); refresh from disk for external edits; disconnect to revert to localStorage; auto-reconnect on reload via IndexedDB handles; unified action modal for rename/duplicate/delete with confirmation; Chromium-only (hidden in unsupported browsers) |
4848
| **📈 Finance Dashboard** | Stock/crypto/index dashboard templates with live TradingView charts; dynamic grid via `data-var-prefix` (add/remove tickers in `@variables` table, grid auto-adjusts); configurable chart range (`1M`, `12M`, `36M`), interval (`D`, `W`, `M`), and EMA period (default 52); interactive 1M/1Y/3Y range + 52D/52W/52M EMA toggle buttons; `@variables` table persists after ⚡ Vars for re-editing; JS code block generates grid HTML from variables |
4949
| **Extras** | Auto-save (localStorage + cloud), table of contents, image paste, 106+ templates (12 categories: AI, Agents, Coding, Creative, Documentation, Finance, Maths, PPT, Project, Quiz, Tables, Technical), template variable substitution (`$(varName)` with auto-detect), table spreadsheet tools (sort, filter, stats, chart, add row/col, inline cell edit, CSV/MD export), content statistics, modular codebase (13+ JS modules), fully responsive mobile UI with scrollable Quick Action Bar (Files, Search, TOC, Share, Copy, Tools, AI, Model, Upload, Help) and formatting toolbar, multi-file workspace sidebar, compact header mode with collapsible Tools dropdown (Presentation, Zen, Word Wrap, Focus, Voice, Dark Mode, Preview Theme), Clear All / Clear Selection buttons (undoable via Ctrl+Z) |
50-
| **Dev Tooling** | ESLint + Prettier (lint, format:check), Playwright test suite — 191 tests across smoke, feature, integration, dev, performance, and QA categories (import, export, share, view-mode, editor, email-to-self, secure share, startup timing, export integrity, persistence, module loading, disk workspace, context memory, exec engine, build validation, load-time, accessibility), pre-commit changelog enforcement, GitHub Actions CI |
50+
| **Dev Tooling** | ESLint + Prettier (lint, format:check), Playwright test suite — 299 tests across smoke, feature, integration, dev, regression, performance, quality, and security categories (import, export, share, view-mode, editor, email-to-self, secure share, startup timing, export integrity, persistence, module loading, disk workspace, context memory, exec engine, build validation, load-time, accessibility, video player, TTS, STT, file converters, stock widget, embed grid, model registry, static analysis, code smell, XSS hardening), pre-commit changelog enforcement, GitHub Actions CI |
5151

5252
## 🤖 AI Assistant
5353

@@ -456,6 +456,7 @@ TextAgent has undergone significant evolution since its inception. What started
456456

457457
| Date | Commits | Feature / Update |
458458
|------|---------|-----------------|
459+
| **2026-03-12** | — | 🧪 **Comprehensive Test Suite** — 12 new Playwright spec files (108 tests) across 5 categories targeting past 3 days of code changes: **Functional** — unit tests for video player (URL detection, HTML builders, embed grid), TTS engine (API surface, state), speech commands (DOM elements, language selector), file converters (MD/CSV/JSON/XML/HTML import), stock widget (rendering, sandbox, double-render prevention); integration tests for embed grid pipeline and AI_MODELS registry. **Regression** — 12 tests pinning recent bug fixes (file upload crash, template confirmation, stock variable, embed rendering, mermaid stability, dark mode, XSS). **Performance** — module init timing (TTS/STT/video/stock/converter < 5–8s), complex render < 5s, embed grid < 3s. **Static Analysis** — ESLint, file size < 100KB, debugger/eval detection, CSS !important audit, IIFE patterns, worker files, HTTPS enforcement. **Security** — embed grid XSS (javascript:/data: URI), video player HTML escaping, YouTube privacy mode, TradingView sandbox, Vimeo DNT, link security, CSP validation. Total test count: 299 |
459460
| **2026-03-12** || 🎤 **Voxtral STT**[Voxtral Mini 3B](https://huggingface.co/textagent/Voxtral-Mini-3B-2507-ONNX) as primary speech-to-text engine on WebGPU (~2.7 GB, q4, 13 languages, streaming partial output via `TextStreamer`); Whisper Large V3 Turbo as WASM fallback (~800 MB, q8); `voxtral-worker.js` new WebWorker with `VoxtralForConditionalGeneration` + `VoxtralProcessor`; `speechToText.js` WebGPU detection + dual-worker routing; download consent popup (`showSttConsentPopup`) with model name/size/privacy info before first download; `STT_CONSENTED` localStorage key; model duplicated to `textagent/` HuggingFace org with `onnx-community/` fallback |
460461
| **2026-03-12** || 🛡️ **Code Audit Fixes** — sandboxed `jsAdapter` in `exec-sandbox.js` (was raw `eval()` on main thread, now iframe-sandboxed); `mirror-models.sh` model IDs updated to `textagent`, Kokoro v1.0→v1.1-zh, GitLab refs removed; Whisper speech worker forwarded user's language selection instead of hardcoded English; shared `ai-worker-common.js` module extracts `TOKEN_LIMITS` + `buildMessages()` from 3 workers; cloud workers load as ES modules |
461462
| **2026-03-12** || 🏠 **Model Hosting Migration** — all 7 ONNX models (Qwen 3.5 0.8B/2B/4B, Qwen 3 4B Thinking, Whisper Large V3 Turbo, Kokoro 82M v1.0/v1.1-zh) duplicated to self-owned [`textagent` HuggingFace org](https://huggingface.co/textagent); model IDs updated from `onnx-community/` to `textagent/` across all workers; automatic fallback to `onnx-community/` namespace if textagent models unavailable; GitLab mirror removed from runtime code |

ai-worker-docling.js

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/**
2+
* AI Worker — Granite Docling 258M (IBM) — Document OCR
3+
*
4+
* Converts document images to structured Markdown/HTML using
5+
* IBM's Granite Docling vision-language model via Transformers.js.
6+
*
7+
* Uses AutoModelForVision2Seq + AutoProcessor from Transformers.js.
8+
* Supports WebGPU acceleration.
9+
*
10+
* Message interface:
11+
* setModelId → configure model ID before loading
12+
* load → download and initialise model
13+
* process → run document OCR on an image
14+
* ping/pong → health check
15+
*/
16+
17+
const TRANSFORMERS_URL = "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.0.0-next.6";
18+
19+
// Model config
20+
let MODEL_ID = "onnx-community/granite-docling-258M-ONNX";
21+
let MODEL_LABEL = "Granite Docling 258M";
22+
23+
// Dynamically loaded modules
24+
let AutoProcessor = null;
25+
let AutoModelForVision2Seq = null;
26+
let load_image = null;
27+
let TextStreamer = null;
28+
29+
// Runtime state
30+
let processor = null;
31+
let model = null;
32+
let device = "wasm"; // will upgrade to webgpu if available
33+
34+
/**
35+
* Initialize the model: load processor + model
36+
*/
37+
async function loadModel() {
38+
try {
39+
// 1. Import Transformers.js
40+
if (!AutoProcessor) {
41+
self.postMessage({ type: "status", message: "Loading AI libraries..." });
42+
try {
43+
const transformers = await import(TRANSFORMERS_URL);
44+
AutoProcessor = transformers.AutoProcessor;
45+
AutoModelForVision2Seq = transformers.AutoModelForVision2Seq;
46+
load_image = transformers.load_image;
47+
TextStreamer = transformers.TextStreamer;
48+
} catch (importError) {
49+
self.postMessage({
50+
type: "error",
51+
message: `Failed to load AI libraries: ${importError.message}`,
52+
});
53+
return;
54+
}
55+
}
56+
57+
// 2. Check WebGPU
58+
if (typeof navigator !== "undefined" && navigator.gpu) {
59+
const adapter = await navigator.gpu.requestAdapter();
60+
if (adapter) device = "webgpu";
61+
}
62+
63+
// 3. Load processor
64+
self.postMessage({ type: "status", message: `Loading ${MODEL_LABEL} processor...` });
65+
processor = await AutoProcessor.from_pretrained(MODEL_ID, {
66+
progress_callback: (progress) => {
67+
if (progress.status === "progress") {
68+
self.postMessage({
69+
type: "progress",
70+
file: progress.file || "processor",
71+
loaded: progress.loaded || 0,
72+
total: progress.total || 0,
73+
progress: progress.progress || 0,
74+
});
75+
} else if (progress.status === "initiate") {
76+
self.postMessage({
77+
type: "status",
78+
message: `Downloading ${progress.file || "model"}...`,
79+
});
80+
}
81+
},
82+
});
83+
84+
// 4. Load model
85+
self.postMessage({ type: "status", message: `Loading ${MODEL_LABEL} model (${device.toUpperCase()})...` });
86+
model = await AutoModelForVision2Seq.from_pretrained(MODEL_ID, {
87+
dtype: "fp32",
88+
device: device,
89+
progress_callback: (progress) => {
90+
if (progress.status === "progress") {
91+
self.postMessage({
92+
type: "progress",
93+
file: progress.file || "model",
94+
loaded: progress.loaded || 0,
95+
total: progress.total || 0,
96+
progress: progress.progress || 0,
97+
});
98+
} else if (progress.status === "initiate") {
99+
self.postMessage({
100+
type: "status",
101+
message: `Downloading ${progress.file || "model"}...`,
102+
});
103+
}
104+
},
105+
});
106+
107+
self.postMessage({ type: "loaded", device: device });
108+
} catch (error) {
109+
self.postMessage({
110+
type: "error",
111+
message: `Failed to load Docling model: ${error.message}`,
112+
});
113+
}
114+
}
115+
116+
/**
117+
* Process a document image and convert to structured text
118+
* @param {object} options
119+
* @param {string} options.imageData - Base64 data URL or URL to the image
120+
* @param {string} options.outputFormat - 'docling', 'markdown', 'html', or 'text'
121+
* @param {boolean} options.doImageSplitting - Split image into patches for more accuracy
122+
* @param {string} options.messageId
123+
*/
124+
async function processDocument({ imageData, outputFormat = 'docling', doImageSplitting = false, messageId }) {
125+
if (!model || !processor) {
126+
self.postMessage({
127+
type: "error",
128+
message: "Model not loaded. Please wait for the model to finish loading.",
129+
messageId,
130+
});
131+
return;
132+
}
133+
134+
try {
135+
self.postMessage({ type: "status", message: "Processing document...", messageId });
136+
137+
// Load image
138+
const image = await load_image(imageData);
139+
140+
// Build prompt based on output format
141+
let promptText = "Convert this page to docling.";
142+
if (outputFormat === 'markdown') {
143+
promptText = "Convert this page to markdown.";
144+
} else if (outputFormat === 'html') {
145+
promptText = "Convert this page to html.";
146+
} else if (outputFormat === 'text') {
147+
promptText = "Extract all text from this page.";
148+
}
149+
150+
// Create messages
151+
const messages = [
152+
{
153+
role: "user",
154+
content: [
155+
{ type: "image" },
156+
{ type: "text", text: promptText },
157+
],
158+
},
159+
];
160+
161+
// Apply chat template and process inputs
162+
const text = processor.apply_chat_template(messages, { add_generation_prompt: true });
163+
const inputs = await processor(text, [image], {
164+
do_image_splitting: doImageSplitting,
165+
});
166+
167+
// Generate with streaming
168+
const generated_ids = await model.generate({
169+
...inputs,
170+
max_new_tokens: 4096,
171+
streamer: new TextStreamer(processor.tokenizer, {
172+
skip_prompt: true,
173+
skip_special_tokens: false,
174+
callback_function: (token) => {
175+
self.postMessage({ type: "token", token: token, messageId });
176+
},
177+
}),
178+
});
179+
180+
// Decode final output
181+
const generated_texts = processor.batch_decode(
182+
generated_ids.slice(null, [inputs.input_ids.dims.at(-1), null]),
183+
{ skip_special_tokens: true },
184+
);
185+
186+
const result = generated_texts[0] || "";
187+
188+
self.postMessage({
189+
type: "complete",
190+
text: result,
191+
messageId,
192+
});
193+
} catch (error) {
194+
self.postMessage({
195+
type: "error",
196+
message: `Document processing failed: ${error.message}`,
197+
messageId,
198+
});
199+
}
200+
}
201+
202+
// Listen for messages from the main thread
203+
self.addEventListener("message", async (event) => {
204+
const { type, messageId } = event.data;
205+
206+
switch (type) {
207+
case "setModelId":
208+
MODEL_ID = event.data.modelId || MODEL_ID;
209+
MODEL_LABEL = event.data.modelLabel || MODEL_LABEL;
210+
break;
211+
case "load":
212+
await loadModel();
213+
break;
214+
case "process":
215+
await processDocument(event.data);
216+
break;
217+
// Also support 'generate' for compatibility with the standard worker interface
218+
case "generate": {
219+
const attachments = event.data.attachments || [];
220+
const imageAtt = attachments.find(a => a.type === 'image');
221+
if (imageAtt) {
222+
await processDocument({
223+
imageData: imageAtt.data,
224+
outputFormat: 'markdown',
225+
doImageSplitting: false,
226+
messageId,
227+
});
228+
} else {
229+
self.postMessage({
230+
type: "error",
231+
message: "Granite Docling requires a document image. Please attach an image.",
232+
messageId,
233+
});
234+
}
235+
break;
236+
}
237+
case "ping":
238+
self.postMessage({ type: "pong" });
239+
break;
240+
default:
241+
console.warn("Unknown message type:", type);
242+
}
243+
});

0 commit comments

Comments
 (0)