From 0a433012ef8e41edf4e0a2efa1bffe4fa924d742 Mon Sep 17 00:00:00 2001 From: ijbo Date: Thu, 14 May 2026 19:01:01 +0900 Subject: [PATCH 1/2] feat: Tools category + iOS-style BODMAS calculator template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New Tools template category (bi-tools icon) with Calculator as first template - iOS-style two-line display: dim expression above, large result below - Both lines contenteditable — click expression to edit and re-evaluate live - Full BODMAS support: ( ) ^ ÷ × − +, with x^y mapped to JS ** for correct precedence - Strict allowlist safeEval ([-+*/().\d^]+) — no eval, no identifiers, alert(1) → Error - History side panel: editable expr/result cells, ↩ to send back to display, Clear button - Paste support for full expressions (12*7+3, (2+3)^2*4, 1,234.5) - Operator-aware backspace, keyboard support, unicode operator normalization - Renderer round-trip fix: entity strings built via String.fromCharCode to survive textContent extraction - 10 new Playwright cases (calculator-tool.spec.js) + Tools-pill assertion in template-loading.spec.js --- README.md | 9 +- changelogs/CHANGELOG-tools-calculator.md | 80 ++++ js/modal-templates.js | 1 + js/templates.js | 5 +- js/templates/tools.js | 475 +++++++++++++++++++++++ src/main.js | 1 + tests/feature/calculator-tool.spec.js | 274 +++++++++++++ tests/feature/template-loading.spec.js | 26 ++ 8 files changed, 866 insertions(+), 5 deletions(-) create mode 100644 changelogs/CHANGELOG-tools-calculator.md create mode 100644 js/templates/tools.js create mode 100644 tests/feature/calculator-tool.spec.js diff --git a/README.md b/README.md index 1e9507a..ad456b0 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ | **✉️ Email to Self** | Send documents directly to your inbox from the share modal — email address input with `.md` file attached + share link; powered by Google Apps Script (free, 100 emails/day); Cloudflare Turnstile CAPTCHA verification; dual rate limiting (100/day global + 7/day per recipient); loading state + success/error feedback; email persisted in localStorage; zero third-party dependencies | | **💾 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) | | **📈 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`), EMA period (default 52), and card size via `data-height`; single cards auto-expand to full width; 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 | -| **Extras** | Auto-save (localStorage + cloud), table of contents, image paste, 137+ templates (16 categories: AI, Agents, API Explorer, Coding, Creative, Documentation, Finance, Games, Maths, PPT, Project, Quiz, Science, Skills, Tables, Technical), AI Model Manager template (local model reference with sizes, privacy, and capabilities), 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), auto-naming (Untitled files derive name from first 10 content characters) | +| **Extras** | Auto-save (localStorage + cloud), table of contents, image paste, 138+ templates (17 categories: AI, Agents, API Explorer, Coding, Creative, Documentation, Finance, Games, Maths, PPT, Project, Quiz, Science, Skills, Tables, Technical, Tools), AI Model Manager template (local model reference with sizes, privacy, and capabilities), 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), auto-naming (Untitled files derive name from first 10 content characters) | | **Dev Tooling** | ESLint + Prettier (lint, format:check), Playwright test suite — 592 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, exec-jsx, build validation, load-time, accessibility, video player, TTS, STT, file converters, stock widget, embed grid, model registry, model tag, game tag, draw docgen, readonly mode, excalidraw library, help mode, page view, table tools, API tag, Linux tag, template loading, inline rename, presentation, static analysis, code smell, XSS hardening, Florence-2 model, Docling model, GLM-OCR model, TTS download), Firestore rules validation (21 tests), automated security scanner (13 checks, 3 severity tiers), pre-commit changelog + security enforcement, GitHub Actions CI | | **🎥 RecStudio** | Full-screen screen & camera recorder with 4 modes (Screen only, Screen + Camera, Camera only, Whiteboard); Canvas-based compositing at 1920×1080 / 60fps; interactive teleprompter (draggable, resizable, font size A−/A+ 10–48px, scroll speed ◁/▷ 0.5x–5x, play/pause scroll, 3-level transparency toggle with readable text on any background); whiteboard with 7 tools (Pen, Highlighter, Eraser, Line, Rectangle, Ellipse, Text), 10 colors, undo/redo; PiP webcam with shape selector (Circle/Square/Full/Off); device selection dropdowns; countdown timer; recording timer; post-recording review + WebM download; all client-side via MediaRecorder + Canvas APIs | @@ -115,7 +115,7 @@ Import files directly — they're auto-converted to Markdown client-side: ### AI Writing Assistant — Local & Cloud Models ![AI Assistant panel with model selector, action chips, and three-column layout](assets/ai-assistant.png) -### Templates Gallery — 137+ Templates, 15 Categories +### Templates Gallery — 138+ Templates, 16 Categories ![Templates modal with category tabs, search, and template cards including Games](assets/templates-gallery.png) ### LaTeX Math & Mermaid Diagrams @@ -162,9 +162,9 @@ Import files directly — they're auto-converted to Markdown client-side:
-📄 Templates Gallery — 137+ Templates, 15 Categories +📄 Templates Gallery — 138+ Templates, 16 Categories -**Start any document in seconds.** Browse 137+ professionally designed templates across 15 categories: AI, Agents, API Explorer, Coding, Creative, Documentation, Finance, Games, Maths, PPT, Project, Quiz, Skills, Tables, and Technical. AI-powered templates include `{{AI:}}` tags for one-click document generation, the API Explorer lists 1400+ public APIs with click-to-try `{{API:}}` blocks, and the Games category features 8 instant pre-built games. +**Start any document in seconds.** Browse 138+ professionally designed templates across 16 categories: AI, Agents, API Explorer, Coding, Creative, Documentation, Finance, Games, Maths, PPT, Project, Quiz, Skills, Tables, Technical, and Tools. AI-powered templates include `{{AI:}}` tags for one-click document generation, the API Explorer lists 1400+ public APIs with click-to-try `{{API:}}` blocks, and the Games category features 8 instant pre-built games. Templates Gallery — browsing categories and loading AI Business Proposal template @@ -549,6 +549,7 @@ TextAgent has undergone significant evolution since its inception. What started | Date | Commits | Feature / Update | |------|---------|-----------------:| +| **2026-05-14** | — | 🧮 **Tools Category + iOS-Style BODMAS Calculator** — new **Tools** template category (`bi-tools` icon) with a fully working calculator as the first template; iOS-style two-line display (small dim expression line above, large result line below); both lines `contenteditable` — click the expression to edit it and re-evaluate live; full **BODMAS** support with visible buttons `(`, `)`, `xʸ`, `÷`, `×`, `−`, `+`, plus `AC`, `±`, `%`, `⌫`, `.`, `=`; live running total updates the result line while typing; **History side panel** lists every calculation as `expr = result` with both cells editable — edits re-evaluate and push the latest result back to the main display, `↩` button sends any row's result to the display, `Clear` empties history; paste support for numbers and full expressions (`12*7+3`, `(2+3)^2*4`, `1,234.5`); unicode operator normalization (`×`, `÷`, `−`, commas); strict allowlist `safeEval` (`^[-+*/().\d^]+$`) with `^` translated to JS `**` for right-associative exponent precedence — no `eval()`, no identifiers, `alert(1)` rejected as `Error`; operator-aware backspace (cancels pending operator or removes one char); keyboard support for digits, `.`, `+ - * / ^ ( )`, `Backspace`, `Enter`/`=`, `Escape`; renderer round-trip safe — embedded `escapeHtml` builds entity strings via `String.fromCharCode(38)` so they survive `
.textContent` extraction; `js/templates/tools.js` (~475 lines, single `html-autorun` block) + `tools` category pill in `modal-templates.js` + icon/color mapping in `templates.js` + dynamic import in `src/main.js`; Playwright assertion for Tools pill + Calculator card |
 | **2026-04-15** | — | 🎙️ **Podcast Generation System** — new `{{@Podcast:}}` document tag for AI-powered multi-speaker podcast creation; 3-phase pipeline (web research via Jina API → AI script generation with `[Speaker]` markers → Kokoro TTS multi-speaker audio synthesis); configurable styles (debate, interview, chat, lecture, storytelling); `parseScript()` speaker segmentation; `createWavBlob()` Float32Array→WAV encoder; real-time progress UI with phase indicators; WAV audio download; **Podcast Marketplace** with 15+ curated templates across 5 categories (Tech, Science, Business, Creative, Education); search/filter, template cards with metadata; `podcast-docgen.js` (~1046 lines) + `podcast-marketplace.js` (~923 lines) + `css/podcast-docgen.css` + `css/podcast-marketplace.css` + `js/templates/podcasts.js` |
 | **2026-04-15** | — | 🔧 **TTS Worker Multi-Speaker Fix** — fixed critical bug where Web Worker silently dropped `speak-multi` messages after async `init` handler completed; root cause: service worker (`sw.js`) used cache-first strategy for `.js` files, serving stale `tts-worker.js` indefinitely; fix: (1) extracted `processMultiSegments()` as standalone async function, (2) bundled segments with `init` message via `pendingSegments` field for same-handler-execution processing, (3) added cache-busting `?v=` param to worker URL, (4) excluded worker files from service worker caching, (5) bumped `CACHE_NAME` v2→v3, (6) added `worker.onerror` handler, (7) per-chunk 90s timeout, event loop yields, voice pre-fetch phase, heartbeat logger, version stamping |
 | **2026-04-08** | — | ⭐ **Star on GitHub Button** — new gold/amber gradient pill button in header next to Issues button linking to the GitHub repo for starring; `.star-github-pill` CSS class with dark mode variant; fixed Issues button inline styles that prevented proper `.help-mode-pill` rendering |
diff --git a/changelogs/CHANGELOG-tools-calculator.md b/changelogs/CHANGELOG-tools-calculator.md
new file mode 100644
index 0000000..d5fceaf
--- /dev/null
+++ b/changelogs/CHANGELOG-tools-calculator.md
@@ -0,0 +1,80 @@
+# Tools Category — iOS-Style BODMAS Calculator
+
+- Added new **Tools** template category with `bi-tools` icon and `technical` color group
+- Added `Calculator` template — iOS-style BODMAS calculator rendered as an `html-autorun` block
+- Two-line display: small dim expression line above, large result line below (matches iOS Calculator)
+- Both lines are editable: click either line to edit, press Enter or blur to re-evaluate
+- Live running total: while typing operands, the result line shows the partial chain evaluated so far
+- Full BODMAS support with visible buttons: `(`, `)`, `xʸ` (power), `÷`, `×`, `−`, `+`, `.`, `±`, `%`, `AC`, `⌫`, `=`
+- History side panel lists every calculation as `expr = result`; both cells editable, edits re-evaluate live
+- `↩` button on each history row sends that row's result back to the main display
+- Latest history-row edit propagates back to the calculator's main display automatically
+- Paste support: paste a number or full expression (`12*7+3`, `(2+3)^2*4`, `1,234.5`) into the result line; auto-evaluated
+- Unicode operator normalization: `×`, `÷`, `−` and comma-separated numbers normalised before evaluation
+- Strict allowlist evaluator (`safeEval`): only `0-9 . + - * / ( ) ^` accepted; `^` translated to JS `**` for right-associative precedence
+- No `eval()` / identifiers / property access — attempts like `alert(1)` are rejected as `Error`
+- Backspace is operator-aware: cancels a pending operator if the last press was an operator, otherwise removes one character
+- Keyboard support: digits, `.`, `+ - * / ^ ( )`, `Backspace`, `Enter`/`=`, `Escape`
+- Service-worker round-trip safe: HTML entity strings in the embedded script are built via `String.fromCharCode(38)` so they survive `
.textContent` extraction by the renderer
+- Added Playwright assertion that the Tools category pill exists and the Calculator card appears under it
+
+---
+
+## Summary
+Added a new **Tools** template category with a working iOS-style calculator as its first template. The calculator is a single `html-autorun` block — markdown ships the source, the existing renderer auto-executes it in a sandboxed iframe. Full BODMAS precedence (brackets, exponent, division/multiplication, addition/subtraction) via a strict regex-allowlist `safeEval`. Editable expression line, editable history rows, and a live-evaluating running total on the result line, all mirroring iOS Calculator behaviour.
+
+---
+
+## 1. Tools Template Module
+**Files:** `js/templates/tools.js` (new, 475 lines)
+**What:** A new template file registering `window.__MDV_TEMPLATES_TOOLS` with one template (`Calculator`). The entire calculator — CSS for the iOS rounded-button look, two-line display markup, button grid, BODMAS evaluator, chain state machine, history panel, paste/keyboard/edit handlers — is embedded as a single `html-autorun` code fence inside a template literal. The existing renderer (`js/renderer.js`) handles auto-execution; no new infrastructure required.
+**Impact:** Users open the Templates modal → click **Tools** → click **Calculator** and the editor loads a doc that renders a live calculator in the preview pane.
+
+## 2. Template System Registration
+**Files:** `js/templates.js`, `js/modal-templates.js`, `src/main.js`
+**What:**
+- `templates.js` — appended `window.__MDV_TEMPLATES_TOOLS` to the `MARKDOWN_TEMPLATES` concatenation; mapped `tools` category to `technical` color group and `bi-tools` Bootstrap icon (in both `getCategoryIconClass()` and `getCategoryIcon()`).
+- `modal-templates.js` — added the `` pill to the category bar.
+- `src/main.js` — added `import('../js/templates/tools.js')` to the Phase 3c parallel template-loading `Promise.all`.
+**Impact:** "Tools" appears in the category bar of the Templates modal with a wrench-and-screwdriver icon, and the Calculator card shows up there (and under "All").
+
+## 3. BODMAS Evaluator
+**Files:** `js/templates/tools.js`
+**What:** Inside the iframe sandbox, `safeEval(expr)` enforces a strict allowlist `^[-+*/().\d^]+$` and rejects runs of more than two of `+`, `/`, or `^`. The `^` operator is translated to JavaScript's `**` before evaluation, preserving right-associative exponent precedence (`2^3^2 = 512`, not `64`). The evaluator runs the sanitised string inside an inner `Function('"use strict";return (' + src + ')')()` and discards any non-finite result. Divide-by-zero and overflow produce `Error` on the display, never `Infinity` or `NaN`.
+**Impact:** Operator precedence is correct out of the box (`2 + 3 * 4 = 14`, `3 * 2 ^ 3 = 24`, `(2+3)^2 * 4 = 100`, `((1+2)*(3+4))^2 = 441`) without writing a precedence parser.
+
+## 4. iOS-Style Two-Line Display
+**Files:** `js/templates/tools.js`
+**What:** The display is a flex column containing a 18px dim `expr-line` and a 56px bright `result-line`. `show()` writes both based on state:
+- typing an operand → top: chain so far, bottom: current operand
+- pending after an operator → top: chain with trailing op, bottom: live running total (partial `safeEval` of the chain so far)
+- after `=` → top: `" ="` from the just-pushed history entry, bottom: result
+Both `
`s are `contenteditable`, with paste/Enter/blur handlers that call `applyPasted()` or `commitExprEdit()` respectively. The expression line strips a trailing `= ` before re-evaluating, so users can edit `(2+3)^2*4 =` directly into `100/(4+1)` and get `20`. +**Impact:** Looks and behaves like the iOS Calculator app, but every value remains editable — the expression line, the result line, and every row in the history side panel. + +## 5. History Side Panel with Live Re-evaluation +**Files:** `js/templates/tools.js` +**What:** Each completed calculation (whether via `=`, paste, or expression-line edit) appends an entry to a `history` array. `renderHistory()` paints rows in newest-first order; each row has a `contenteditable` `.expr` and `.res` cell plus a `↩` "send to display" button. A capturing `blur` listener on the history list re-evaluates the row when its `.expr` changes (or trusts the manually-edited `.res`), then — if the edited row is the latest — pushes the new result back to the main display. The `clear-h` button empties the history. Adjacent duplicates are merged to avoid noise. +**Impact:** Full edit history with two-way data flow: change a step you took half an hour ago and the current result updates accordingly. + +## 6. Renderer Round-Trip Hardening +**Files:** `js/templates/tools.js` +**What:** The renderer reads the `html-autorun` source via `codeEl.textContent` from the `
` it built during markdown parsing. Any HTML entities written as literals in the source (`&`, `<`, `"`, `'`) get decoded back to `&`, `<`, `"`, `'` during extraction, corrupting the embedded `escapeHtml` function. The fix builds those entity strings dynamically at runtime: `var amp = String.fromCharCode(38) + 'amp;'` etc. These survive the `textContent` round-trip intact.
+**Impact:** Prevents a silent syntax-error class that would break any template embedding JS with literal HTML-entity strings.
+
+## 7. Tests
+**Files:** `tests/feature/template-loading.spec.js`
+**What:** Added a new Playwright case `tools category pill exists and Calculator template loads` that opens the template modal via `M.openTemplateModal()`, asserts the `data-category="tools"` pill exists, clicks it, and verifies at least one card matching `/Calculator/i` appears in the filtered grid.
+**Impact:** Catches future regressions of the Tools category registration (e.g. dropped from `concat`, missing pill button, missing template import in `main.js`).
+
+---
+
+## Files Changed (5 total)
+
+| File | Lines Changed | Type |
+|------|:---:|------|
+| `js/templates/tools.js` | +475 | New: iOS-style BODMAS calculator template (`html-autorun` block) |
+| `tests/feature/template-loading.spec.js` | +26 | Playwright: Tools pill + Calculator card |
+| `js/templates.js` | +4 −1 | `MDV_TEMPLATES_TOOLS` concat + icon/color mapping for `tools` |
+| `js/modal-templates.js` | +1 | `` pill |
+| `src/main.js` | +1 | `import('../js/templates/tools.js')` in Phase 3c |
diff --git a/js/modal-templates.js b/js/modal-templates.js
index 0ef50ba..b78e86d 100644
--- a/js/modal-templates.js
+++ b/js/modal-templates.js
@@ -797,6 +797,7 @@ const chatResponse = await openai.chat.completions.create({
                     
                     
                     
+                    
                 
diff --git a/js/templates.js b/js/templates.js index af7c0d9..001f756 100644 --- a/js/templates.js +++ b/js/templates.js @@ -35,7 +35,8 @@ window.__MDV_TEMPLATES_SANKEY_GALLERY || [], window.__MDV_TEMPLATES_PARALLEL_GALLERY || [], window.__MDV_TEMPLATES_GRAPH_GALLERY || [], - window.__MDV_TEMPLATES_PODCASTS || [] + window.__MDV_TEMPLATES_PODCASTS || [], + window.__MDV_TEMPLATES_TOOLS || [] ); @@ -72,6 +73,7 @@ case 'charts': return 'technical'; case 'science': return 'technical'; case 'podcasts': return 'creative'; + case 'tools': return 'technical'; default: return 'doc'; } } @@ -97,6 +99,7 @@ case 'charts': return 'bi-bar-chart-line'; case 'science': return 'bi-atom'; case 'podcasts': return 'bi-mic-fill'; + case 'tools': return 'bi-tools'; default: return 'bi-file-earmark'; } } diff --git a/js/templates/tools.js b/js/templates/tools.js new file mode 100644 index 0000000..63112f4 --- /dev/null +++ b/js/templates/tools.js @@ -0,0 +1,475 @@ +// ============================================ +// templates/tools.js — Tools Templates +// ============================================ +window.__MDV_TEMPLATES_TOOLS = [ + { + name: 'Calculator', + category: 'tools', + icon: 'bi-calculator', + description: 'Interactive calculator widget — runs live in the preview', + content: `# 🧮 Calculator + +A working calculator with full **BODMAS** support — Brackets, Orders (powers), Division/Multiplication, Addition/Subtraction — all evaluated with correct precedence. Type or click; supports + − × ÷ ^ ( ), decimals, percent, sign-flip, and clear. **You can also paste a number or a full expression** (e.g. \`12*7+3\` or \`(2+3)^2\`) into the display. + +A **history panel** on the right lists every calculation. Edit the expression *or* the number on either side of \`=\` and the row re-evaluates live — the latest result is pushed back to the main display. + +\`\`\`html-autorun + + + + + + +
+
+
+
0
+
+
Tip: paste a number or expression (e.g. 12*7+3)
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+

History

+
+
No calculations yet. Press = or paste an expression.
+
+
+ + + +\`\`\` +` + } +]; diff --git a/src/main.js b/src/main.js index df21e9c..cf1c63f 100644 --- a/src/main.js +++ b/src/main.js @@ -190,6 +190,7 @@ async function loadModules() { import('../js/templates/charts-parallel-gallery.js'), import('../js/templates/charts-graph-gallery.js'), import('../js/templates/podcasts.js'), + import('../js/templates/tools.js'), ]); await import('../js/templates.js'); diff --git a/tests/feature/calculator-tool.spec.js b/tests/feature/calculator-tool.spec.js new file mode 100644 index 0000000..1f79ecd --- /dev/null +++ b/tests/feature/calculator-tool.spec.js @@ -0,0 +1,274 @@ +// @ts-check +import { test, expect } from '@playwright/test'; + +/** + * Tools → Calculator template — end-to-end behavioural tests. + * + * The calculator is shipped as a single `html-autorun` block inside the + * Tools template. We load the template content into the editor, wait for + * the renderer to mount it as a sandboxed iframe, then drive the buttons + * from inside that iframe and assert on the two-line display + history + * side panel. + */ + +const EDITOR_SELECTOR = '#markdown-editor'; + +test.describe('Tools — Calculator template', () => { + test.setTimeout(60_000); + + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.waitForSelector(EDITOR_SELECTOR, { state: 'visible' }); + await page.waitForFunction(() => { + const w = /** @type {any} */ (window); + return w.MDView + && typeof w.MDView.openTemplateModal === 'function' + && Array.isArray(w.__MDV_TEMPLATES_TOOLS) + && w.__MDV_TEMPLATES_TOOLS.length > 0; + }, null, { timeout: 20_000 }); + + // Inject the Calculator template directly into the editor — sidesteps + // the "discard current content?" confirm modal that the UI flow uses. + await page.evaluate(() => { + const w = /** @type {any} */ (window); + const tpl = w.__MDV_TEMPLATES_TOOLS[0]; + const editor = /** @type {HTMLTextAreaElement} */ ( + document.getElementById('markdown-editor') + ); + editor.value = tpl.content; + editor.dispatchEvent(new Event('input', { bubbles: true })); + if (w.MDView && typeof w.MDView.renderMarkdown === 'function') { + w.MDView.renderMarkdown(); + } + }); + + // Wait for the html-autorun iframe to mount and its script to wire up. + await page.waitForFunction(() => { + const iframe = /** @type {HTMLIFrameElement|null} */ ( + document.querySelector('.executable-html-container[data-autorun] iframe') + ); + if (!iframe || !iframe.contentDocument) return false; + return !!iframe.contentDocument.getElementById('result-line') + && !!iframe.contentDocument.querySelector('button[data-num="7"]'); + }, null, { timeout: 15_000 }); + }); + + /** Helper: run a sequence of button selectors inside the calculator iframe + * and return the {top, bot} display state after the last click. + */ + async function press(page, ...selectors) { + return await page.evaluate((sels) => { + const iframe = /** @type {HTMLIFrameElement} */ ( + document.querySelector('.executable-html-container[data-autorun] iframe') + ); + const idoc = /** @type {Document} */ (iframe.contentDocument); + for (const s of sels) { + const btn = /** @type {HTMLButtonElement} */ (idoc.querySelector(s)); + btn.click(); + } + return { + top: /** @type {HTMLElement} */ (idoc.getElementById('expr-line')).textContent, + bot: /** @type {HTMLElement} */ (idoc.getElementById('result-line')).textContent, + }; + }, selectors); + } + + async function clearAll(page) { + await page.evaluate(() => { + const iframe = /** @type {HTMLIFrameElement} */ ( + document.querySelector('.executable-html-container[data-autorun] iframe') + ); + const idoc = /** @type {Document} */ (iframe.contentDocument); + /** @type {HTMLButtonElement} */ (idoc.querySelector('button[data-act="clear"]')).click(); + const clr = idoc.getElementById('clear-h'); + if (clr) /** @type {HTMLButtonElement} */ (clr).click(); + }); + } + + test('chains operands but only collapses to result on =', async ({ page }) => { + await clearAll(page); + // 33 + 43 + 56 + 10 = + const out = await press(page, + 'button[data-num="3"]', 'button[data-num="3"]', + 'button[data-op="+"]', + 'button[data-num="4"]', 'button[data-num="3"]', + 'button[data-op="+"]', + 'button[data-num="5"]', 'button[data-num="6"]', + 'button[data-op="+"]', + 'button[data-num="1"]', 'button[data-num="0"]', + 'button[data-act="eq"]', + ); + expect(out.bot).toBe('142'); + expect(out.top).toBe('33+43+56+10 ='); + }); + + test('BODMAS: multiplication binds tighter than addition', async ({ page }) => { + await clearAll(page); + // 2 + 3 * 4 = 14 (not 20) + const out = await press(page, + 'button[data-num="2"]', 'button[data-op="+"]', + 'button[data-num="3"]', 'button[data-op="*"]', + 'button[data-num="4"]', 'button[data-act="eq"]', + ); + expect(out.bot).toBe('14'); + }); + + test('BODMAS: brackets override precedence', async ({ page }) => { + await clearAll(page); + // (2 + 3) * 4 = 20 + const out = await press(page, + 'button[data-bracket="("]', + 'button[data-num="2"]', 'button[data-op="+"]', 'button[data-num="3"]', + 'button[data-bracket=")"]', + 'button[data-op="*"]', 'button[data-num="4"]', + 'button[data-act="eq"]', + ); + expect(out.bot).toBe('20'); + }); + + test('BODMAS: power outranks multiplication', async ({ page }) => { + await clearAll(page); + // 3 * 2 ^ 3 = 24, not (3*2)^3 = 216 + const out = await press(page, + 'button[data-num="3"]', 'button[data-op="*"]', + 'button[data-num="2"]', 'button[data-op="^"]', + 'button[data-num="3"]', 'button[data-act="eq"]', + ); + expect(out.bot).toBe('24'); + }); + + test('paste evaluates a full BODMAS expression', async ({ page }) => { + await clearAll(page); + const out = await page.evaluate(() => { + const iframe = /** @type {HTMLIFrameElement} */ ( + document.querySelector('.executable-html-container[data-autorun] iframe') + ); + const idoc = /** @type {Document} */ (iframe.contentDocument); + const win = /** @type {Window} */ (iframe.contentWindow); + const resultLine = /** @type {HTMLElement} */ (idoc.getElementById('result-line')); + const ev = new win.Event('paste', { bubbles: true, cancelable: true }); + Object.defineProperty(ev, 'clipboardData', { + value: { getData: () => '(2+3)^2*4' }, + }); + resultLine.dispatchEvent(ev); + return { + top: /** @type {HTMLElement} */ (idoc.getElementById('expr-line')).textContent, + bot: resultLine.textContent, + }; + }); + expect(out.bot).toBe('100'); + }); + + test('paste rejects unsafe input (no eval injection)', async ({ page }) => { + await clearAll(page); + const bot = await page.evaluate(() => { + const iframe = /** @type {HTMLIFrameElement} */ ( + document.querySelector('.executable-html-container[data-autorun] iframe') + ); + const idoc = /** @type {Document} */ (iframe.contentDocument); + const win = /** @type {Window} */ (iframe.contentWindow); + const resultLine = /** @type {HTMLElement} */ (idoc.getElementById('result-line')); + const ev = new win.Event('paste', { bubbles: true, cancelable: true }); + Object.defineProperty(ev, 'clipboardData', { + value: { getData: () => 'alert(1)' }, + }); + resultLine.dispatchEvent(ev); + return resultLine.textContent; + }); + expect(bot).toBe('Error'); + }); + + test('editing the expression line re-evaluates and updates the result', async ({ page }) => { + await clearAll(page); + // First build (2+3)^2*4 = 100 + await press(page, + 'button[data-bracket="("]', + 'button[data-num="2"]', 'button[data-op="+"]', 'button[data-num="3"]', + 'button[data-bracket=")"]', + 'button[data-op="^"]', 'button[data-num="2"]', + 'button[data-op="*"]', 'button[data-num="4"]', + 'button[data-act="eq"]', + ); + + // Edit the small expression line to 100/(4+1) and blur — bottom should become 20 + const after = await page.evaluate(() => { + const iframe = /** @type {HTMLIFrameElement} */ ( + document.querySelector('.executable-html-container[data-autorun] iframe') + ); + const idoc = /** @type {Document} */ (iframe.contentDocument); + const win = /** @type {Window} */ (iframe.contentWindow); + const expr = /** @type {HTMLElement} */ (idoc.getElementById('expr-line')); + expr.focus(); + expr.textContent = '100/(4+1)'; + expr.dispatchEvent(new win.FocusEvent('blur', { bubbles: true })); + return { + top: expr.textContent, + bot: /** @type {HTMLElement} */ (idoc.getElementById('result-line')).textContent, + }; + }); + expect(after.top).toBe('100/(4+1) ='); + expect(after.bot).toBe('20'); + }); + + test('editing a history row re-evaluates and pushes result back to display', async ({ page }) => { + await clearAll(page); + // Build 7 * 8 = 56 so we have one history row + await press(page, + 'button[data-num="7"]', 'button[data-op="*"]', 'button[data-num="8"]', + 'button[data-act="eq"]', + ); + + // Edit the row's expression to "9*9" and confirm the display becomes 81 + const after = await page.evaluate(() => { + const iframe = /** @type {HTMLIFrameElement} */ ( + document.querySelector('.executable-html-container[data-autorun] iframe') + ); + const idoc = /** @type {Document} */ (iframe.contentDocument); + const win = /** @type {Window} */ (iframe.contentWindow); + const row = /** @type {HTMLElement} */ (idoc.querySelector('#hist-list .row')); + const exprCell = /** @type {HTMLElement} */ (row.querySelector('.expr')); + exprCell.focus(); + exprCell.textContent = '9*9'; + exprCell.dispatchEvent(new win.FocusEvent('blur', { bubbles: true })); + return /** @type {HTMLElement} */ (idoc.getElementById('result-line')).textContent; + }); + expect(after).toBe('81'); + }); + + test('backspace cancels a pending operator', async ({ page }) => { + await clearAll(page); + // Press 5, then + — pending operator, display shows 5 + await press(page, 'button[data-num="5"]', 'button[data-op="+"]'); + // Press ⌫ — operator should be cancelled, display back to 5 with no chain + const out = await press(page, 'button[data-act="back"]'); + expect(out.bot).toBe('5'); + expect(out.top).toBe(''); + }); + + test('AC clears state and history (button-driven Clear)', async ({ page }) => { + await clearAll(page); + await press(page, + 'button[data-num="2"]', 'button[data-op="+"]', 'button[data-num="3"]', + 'button[data-act="eq"]', + ); + await page.evaluate(() => { + const iframe = /** @type {HTMLIFrameElement} */ ( + document.querySelector('.executable-html-container[data-autorun] iframe') + ); + const idoc = /** @type {Document} */ (iframe.contentDocument); + /** @type {HTMLButtonElement} */ (idoc.querySelector('button[data-act="clear"]')).click(); + }); + const state = await page.evaluate(() => { + const iframe = /** @type {HTMLIFrameElement} */ ( + document.querySelector('.executable-html-container[data-autorun] iframe') + ); + const idoc = /** @type {Document} */ (iframe.contentDocument); + return { + top: /** @type {HTMLElement} */ (idoc.getElementById('expr-line')).textContent, + bot: /** @type {HTMLElement} */ (idoc.getElementById('result-line')).textContent, + }; + }); + expect(state.bot).toBe('0'); + expect(state.top).toBe(''); + }); +}); diff --git a/tests/feature/template-loading.spec.js b/tests/feature/template-loading.spec.js index 6c9a689..bc42a71 100644 --- a/tests/feature/template-loading.spec.js +++ b/tests/feature/template-loading.spec.js @@ -101,6 +101,32 @@ test.describe('Template System', () => { expect(filteredCount).toBeGreaterThanOrEqual(1); }); + test('tools category pill exists and Calculator template loads', async ({ page }) => { + await page.evaluate(() => { + /** @type {any} */ (window).MDView.openTemplateModal(); + }); + await page.waitForTimeout(500); + + const toolsPillExists = await page.evaluate(() => { + return !!document.querySelector('#template-categories .template-cat-btn[data-category="tools"]'); + }); + expect(toolsPillExists).toBe(true); + + await page.evaluate(() => { + const btn = /** @type {HTMLButtonElement} */ ( + document.querySelector('#template-categories .template-cat-btn[data-category="tools"]') + ); + btn.click(); + }); + await page.waitForTimeout(400); + + const calcCount = await page.evaluate(() => { + const cards = document.querySelectorAll('#template-modal .template-card'); + return Array.from(cards).filter((c) => /Calculator/i.test(c.textContent || '')).length; + }); + expect(calcCount).toBeGreaterThanOrEqual(1); + }); + test('closeTemplateModal closes the modal', async ({ page }) => { await page.evaluate(() => { /** @type {any} */ (window).MDView.openTemplateModal(); From 54e716299e9ce58d94f0babaaea2d9ab34a17851 Mon Sep 17 00:00:00 2001 From: ijbo Date: Thu, 14 May 2026 19:08:23 +0900 Subject: [PATCH 2/2] chore: move remaining root-level changelogs into changelogs/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI hook (changelog-location) fails when a CHANGELOG-*.md file is at the repo root. Four were identical duplicates of files already in changelogs/, so the root copies were removed. One (pretext-performance) only existed at root, so it was moved. - rm CHANGELOG-annotate-pretext-reflow.md (duplicate of changelogs/ copy) - rm CHANGELOG-chart-bugfixes.md (duplicate) - rm CHANGELOG-podcast-system.md (duplicate, flagged by CI on the calculator PR) - rm CHANGELOG-share-link-loader.md (duplicate) - mv CHANGELOG-pretext-performance.md → changelogs/ --- CHANGELOG-annotate-pretext-reflow.md | 90 ------------------- CHANGELOG-chart-bugfixes.md | 54 ----------- CHANGELOG-podcast-system.md | 82 ----------------- CHANGELOG-share-link-loader.md | 38 -------- .../CHANGELOG-pretext-performance.md | 0 5 files changed, 264 deletions(-) delete mode 100644 CHANGELOG-annotate-pretext-reflow.md delete mode 100644 CHANGELOG-chart-bugfixes.md delete mode 100644 CHANGELOG-podcast-system.md delete mode 100644 CHANGELOG-share-link-loader.md rename CHANGELOG-pretext-performance.md => changelogs/CHANGELOG-pretext-performance.md (100%) diff --git a/CHANGELOG-annotate-pretext-reflow.md b/CHANGELOG-annotate-pretext-reflow.md deleted file mode 100644 index ff2c33c..0000000 --- a/CHANGELOG-annotate-pretext-reflow.md +++ /dev/null @@ -1,90 +0,0 @@ -# Annotate DocGen — Pretext Scanline Reflow Engine - -- New `{{Annotate:}}` DocGen tag with canvas-based freehand annotation overlay -- New `@text:` source mode renders text on-canvas with Pretext-style scanline reflow -- Scanline engine (`reflowText`, `getFreeIntervals`) samples 1px pixel rows from an offscreen mask canvas to compute per-line free intervals -- Real-time text reflow: as user draws, each stroke updates the mask and text re-routes live around strokes -- `layoutNextLine()` pattern used per-row: per-line width narrows when a stroke occupies that row's x-range -- Offscreen mask canvas approach: strokes drawn at full resolution, scanline reads `getImageData(0, y, width, 1)` — O(width) per row, ~0.5–1.5ms total -- Added `↩ Undo` button with stroke history stack (Ctrl+Z equivalent) -- Added `🗑 Clear` button (no confirm dialog) with undo stack support -- Added `📥 PNG` button — exports composite canvas (strokes + text) as PNG download -- Added `📖 Present` button — calls `M.setViewMode('preview')` to hide editor and enter live reading/drawing mode -- `Present` auto-scrolls annotation card into view after mode switch -- Removed blocking `confirm()` dialog from Clear; instant clear with undo restore -- Canvas auto-resizes to match `ann-source-text-reflow` element height or defaults to 320px -- `redrawAll` handles dual-layer rendering: text layer first, strokes on top -- Fixed: `data-text` attribute stripped by DOMPurify — added `data-text`, `data-reflow` to `ADD_ATTR` whitelist in `renderer.js` -- Fixed: Text now stored as hidden `` textContent inside the reflow div — textContent always survives DOMPurify regardless of attribute whitelist -- Fixed: `initCanvas` reads from `textSpan.textContent` first (bulletproof), falls back to `dataset.text` -- Fixed: `M.insertAtCursor` replaced with `M.setViewMode('preview')` for the insert action — annotation stays live and drawable instead of becoming a static image -- Fixed: CORS/SecurityError on external images handled with try/catch and fallback to annotation-only export -- Fixed: Image insertion uses `gen-img:ID` registry pattern (same as `draw-docgen.js`) to bypass DOMPurify's stripping of `data:` URLs -- Added `.ann-present-btn` CSS with green gradient, `font-weight: 600`, and hover transform -- Added `.ann-reflow-text` CSS: visually hidden span (position absolute, 1×1px clip) so stored text is invisible but accessible to canvas reader -- New `public/pretext-reflow-demo.html` — 4-tab interactive demo: Float Image, Draw Exclusion, Both Together, How It Works -- Float Image tab: image float left/right with width %, top offset sliders, image picker (picsum.photos), and own-image upload -- Draw Exclusion tab: side-by-side DOM (text buried) vs Pretext (scanline reflow) comparison -- Both Together tab: image float + freehand strokes combined, both exclude text simultaneously -- How It Works tab: annotated `layoutNextLine()` code explanation with image and scanline approach -- New `css/annotate-docgen.css` with full card UI, toolbar, color swatches, size slider, dark mode -- New `js/annotate-docgen.js` (~710 lines): parser, canvas init, scanline engine, stroke management, export, present - ---- - -## Summary - -Integrates a real-time, pixel-precise Pretext-style text reflow engine into the `{{Annotate:}}` DocGen tag. Text flows live around freehand annotations using a scanline mask canvas algorithm (O(width) per row, ~0.5–1.5ms/frame). The "Present" button transitions TextAgent to preview-only mode, keeping the annotation card fully drawable while reading. A companion interactive demo (`pretext-reflow-demo.html`) illustrates all three reflow scenarios: image float, freehand, and both combined. - ---- - -## 1. Annotate DocGen Tag — New `{{Annotate:}}` tag - -**Files:** `js/annotate-docgen.js`, `css/annotate-docgen.css` -**What:** Full DocGen card with toolbar (pen/highlighter/eraser/line/arrow/rect/circle), color swatches, size slider, canvas overlay over any `@text:`, `@img:`, or `@url:` source. Undo/Clear/PNG/Present buttons in header. -**Impact:** Authors can write `{{Annotate: Title @text: body text}}` in markdown and get a live, interactive annotation canvas with real-time Pretext text reflow. - ---- - -## 2. Scanline Reflow Engine - -**Files:** `js/annotate-docgen.js` (`reflowText`, `getFreeIntervals`, `buildMask`) -**What:** Offscreen mask canvas accumulates stroke paths. Per text row, `getImageData(0, y, width, 1)` scans a single pixel row for occupied x-ranges. `getFreeIntervals` returns free segments. `reflowText` packs words into free intervals using `canvas.measureText()` for width arithmetic — exactly the Pretext `layoutNextLine()` pattern. -**Impact:** Text wraps with pixel precision around any freehand shape drawn by the user. Performance: ~0.5–1.5ms per full reflow at typical canvas sizes. - ---- - -## 3. DOMPurify Fix — `data-text` Whitelist + Hidden Span - -**Files:** `js/renderer.js`, `js/annotate-docgen.js`, `css/annotate-docgen.css` -**What:** `data-text` and `data-reflow` added to `ADD_ATTR` in `renderer.js`. Additionally, text now stored inside a hidden `` (position absolute, 1×1px clip) as textContent — survives DOMPurify with zero whitelisting needed. `initCanvas` reads textContent first, `dataset.text` as fallback. -**Impact:** Fixed core bug where `@text:` content was silently dropped by DOMPurify, leaving a blank canvas with no text rendered. - ---- - -## 4. Present Mode — Live Reading + Drawing - -**Files:** `js/annotate-docgen.js`, `css/annotate-docgen.css` -**What:** "📖 Present" button replaces "Insert". Calls `M.setViewMode('preview')` to hide the editor and expand preview to full width. Annotation card remains a live canvas — user reads and draws simultaneously. Falls back to manual CSS hide of `.editor-pane` if `M.setViewMode` is unavailable. -**Impact:** Completes the author workflow: write → annotate → present. The annotation is never "frozen" as a static image — it stays interactive. - ---- - -## 5. Pretext Reflow Demo - -**Files:** `public/pretext-reflow-demo.html` -**What:** Self-contained 4-tab demo showing: (1) float image with `layoutNextLine()` per-line width narrowing, (2) freehand scanline reflow vs DOM comparison, (3) image + strokes combined, (4) API explainer. Image picker uses picsum.photos; own-image upload supported. Performance metrics shown live. -**Impact:** Demonstrates all three `layoutNextLine()` reflow patterns in an interactive standalone page. Accessible at `/pretext-reflow-demo.html`. - ---- - -## Files Changed (6 total) - -| File | Lines Changed | Type | -|------|:---:|------| -| `js/annotate-docgen.js` | +710 | New module — full Annotate DocGen | -| `css/annotate-docgen.css` | +340 | New stylesheet — card, toolbar, reflow badge | -| `js/renderer.js` | +2 | DOMPurify ADD_ATTR: added `data-text`, `data-reflow` | -| `src/main.js` | +5 | Phase lazy-load registration for annotate-docgen | -| `public/pretext-reflow-demo.html` | +720 | New interactive Pretext reflow demo | -| `public/annotate-example.md` | +12 | Example markdown using `{{Annotate:}}` tag | diff --git a/CHANGELOG-chart-bugfixes.md b/CHANGELOG-chart-bugfixes.md deleted file mode 100644 index 78e0cdf..0000000 --- a/CHANGELOG-chart-bugfixes.md +++ /dev/null @@ -1,54 +0,0 @@ -# Chart Bug Fixes — 6 Bugs Resolved - -- Fixed: ECharts memory leak — old chart instances never disposed on re-render, causing growing memory usage -- Fixed: `@code` brace-depth tracker desynced on braces inside string literals and comments -- Fixed: `stripTypeScript` regex mangled valid JS (e.g. `{ type: "string" }` had `: "string"` removed) -- Fixed: "Add Series" insert position used fragile `- 2` offset instead of scanning for `}}` -- Fixed: Area-style applied unintentionally to stacked line charts via confusing boolean logic -- Fixed: Block index desync risk when `transformChartMarkdown` re-parsed `fullMatch` substrings - ---- - -## Summary -Fixed 6 bugs in the ECharts `{{Chart:}}` DocGen tag system: a memory leak from undisposed chart instances, brace-depth tracking that ignored strings/comments, TypeScript stripping that could corrupt valid JS object values, a fragile insert offset in the Add Series feature, confusing area-style default logic, and a re-parsing pattern that risked block index desync. - ---- - -## 1. ECharts Memory Leak Fix -**Files:** `js/renderer.js` -**What:** Added `M._activeCharts` array to track all `ec.init()` instances. At the start of each render cycle, old instances are `dispose()`d before creating new ones. All 3 `ec.init()` call sites (async code mode, sync code mode, declarative JSON mode) now push to this array. -**Impact:** Prevents growing memory usage and stale ResizeObserver callbacks during long editing sessions. - -## 2. String/Comment-Aware Brace Tracking -**Files:** `js/chart-docgen.js` -**What:** Replaced naive character-by-character `{`/`}` counting with a scanner that skips braces inside `"`, `'`, `` ` `` string literals, `//` line comments, and `/* */` block comments. Applied in both `parseChartBlocks` (existing) and the new `parseConfigFromBody` helper. -**Impact:** Prevents parsing failures when ECharts code contains unbalanced braces in strings like `formatter: "{value}%"`. - -## 3. TypeScript Stripping Regex Fix -**Files:** `js/chart-docgen.js` -**What:** Anchored the type annotation removal regex to `let/const/var` declarations with a `(?=\s*=)` lookahead, preventing it from matching `: "string"` in object values like `{ type: "string" }`. -**Impact:** Prevents silent code corruption when ECharts templates contain common JS patterns. - -## 4. Robust "Add Series" Insert Position -**Files:** `js/chart-docgen.js` -**What:** Replaced `blocks[idx].end - 2` with `text.lastIndexOf('}}', blocks[idx].end - 1)` plus a safety check ensuring the found position is within the block. -**Impact:** Correctly inserts new series content before `}}` regardless of whitespace or newlines. - -## 5. Simplified Area-Style Logic -**Files:** `js/chart-docgen.js` -**What:** Changed `if (cfg.area || (type === 'line' && cfg.area !== false && cfg.stack))` to `if (cfg.area === true)`. -**Impact:** Area fill now only applied when explicitly set via `@area: true`, removing confusing implicit behavior. - -## 6. Block Index Desync Prevention -**Files:** `js/chart-docgen.js` -**What:** Extracted `parseConfigFromBody(body)` as a shared helper function. `transformChartMarkdown` now calls this directly on `rb.body` instead of re-parsing `rb.fullMatch` via `parseChartBlocks()`, which could return multiple blocks if `fullMatch` contained nested `{{Chart:}}` references. -**Impact:** Eliminates risk of wrong chart being deleted/modified when documents contain chart documentation. - ---- - -## Files Changed (2 total) - -| File | Lines Changed | Type | -|------|:---:|------| -| `js/chart-docgen.js` | +99 −6 | Bug fixes 1-4, 5-6 | -| `js/renderer.js` | +10 −0 | Memory leak fix (Bug 5) | diff --git a/CHANGELOG-podcast-system.md b/CHANGELOG-podcast-system.md deleted file mode 100644 index d0b19cc..0000000 --- a/CHANGELOG-podcast-system.md +++ /dev/null @@ -1,82 +0,0 @@ -# Podcast Generation System & TTS Worker Fixes - -- Added `{{@Podcast:}}` tag for AI-powered podcast generation from any document content -- Built multi-speaker script generation with configurable styles (debate, interview, chat, lecture, storytelling) -- Integrated Kokoro TTS multi-speaker synthesis with voice pre-fetching and per-chunk progress -- Created podcast marketplace UI with pre-built podcast templates and search/filter -- Added podcast template system with 15+ curated templates across tech, science, business categories -- Built WAV audio creation from Float32Array TTS output with download support -- Added real-time podcast generation progress UI with phase indicators (research → script → audio → done) -- Extracted `processMultiSegments()` as standalone async function in TTS worker for reliable synthesis -- Fixed: TTS worker message delivery bug — bundled segments with `init` message to process in same handler execution -- Fixed: Service worker cache-first strategy serving stale `tts-worker.js` — added cache-busting `?v=` param -- Fixed: Worker files now excluded from service worker `shouldCacheResponse()` caching -- Bumped service worker cache version from `v2` → `v3` to force cache invalidation -- Added `worker.onerror` handler on main thread to catch worker-level errors -- Added TTS worker version identifier (`TTS_WORKER_VERSION`) with startup logging -- Added `_pendingMultiSegments` backup mechanism in `status: ready` handler -- Added per-chunk 90s timeout via `Promise.race` to prevent infinite synthesis hangs -- Added event loop yields (`setTimeout(0)`) between WASM calls so `postMessage` flushes -- Added voice pre-fetch phase before synthesis to separate network vs WASM issues -- Added heartbeat logger (10s interval) during multi-speaker synthesis -- Added detailed timestamped logging across `textToSpeech.js`, `tts-worker.js`, `podcast-docgen.js` -- Added help mode entries for podcast generation feature -- Added podcast renderer integration in `renderer.js` for `{{@Podcast:}}` tag processing - ---- - -## Summary -Complete podcast generation system: users write `{{@Podcast: topic}}` in any document and get an AI-generated multi-speaker podcast with web research, script writing, and Kokoro TTS audio synthesis. Also fixed a critical TTS worker bug where the service worker's cache-first strategy served stale worker code, and the Web Worker silently dropped `speak-multi` messages sent after the async `init` handler completed. - ---- - -## 1. Podcast Document Generator (`{{@Podcast:}}` Tag) -**Files:** `js/podcast-docgen.js`, `css/podcast-docgen.css` -**What:** New IIFE component that intercepts `{{@Podcast: topic}}` tags in rendered markdown. Performs 3-phase generation: (1) web search research via Jina API, (2) AI script generation with `[Speaker]` markers, (3) Kokoro TTS multi-speaker audio synthesis. Includes `parseScript()` for speaker segmentation, `createWavBlob()` for audio encoding, and real-time progress UI with phase indicators. -**Impact:** Users can generate full podcast episodes from any topic directly in their documents — no external tools needed. - -## 2. Podcast Marketplace -**Files:** `js/podcast-marketplace.js`, `css/podcast-marketplace.css`, `js/templates/podcasts.js` -**What:** Built a browsable marketplace UI with 15+ curated podcast templates across categories (Tech, Science, Business, Creative, Education). Includes search/filter, category tabs, template cards with metadata (duration, speakers, style), and one-click generation. Templates define speaker count, style, custom prompts, and voice assignments. -**Impact:** Users can browse and generate podcasts from pre-built templates without writing prompts. - -## 3. TTS Worker Multi-Speaker Fix (Critical Bug) -**Files:** `js/tts-worker.js`, `js/textToSpeech.js` -**What:** The Web Worker silently dropped `speak-multi` messages sent after the async `init` handler completed. Root cause: service worker served cached `tts-worker.js` via cache-first strategy, AND the worker couldn't reliably process a second `postMessage` after `init`. Fix: (1) extracted `processMultiSegments()` as standalone function, (2) bundled segments with `init` message via `pendingSegments` field, (3) worker processes segments inline at end of init handler, (4) added cache-busting `?v=` param to worker URL, (5) added `_pendingMultiSegments` backup dispatch from `status: ready` handler. -**Impact:** Podcast TTS synthesis now works reliably — previously it hung forever after model loaded. - -## 4. Service Worker Cache Fix -**Files:** `sw.js` -**What:** Bumped `CACHE_NAME` from `textagent-v2` to `textagent-v3` to invalidate stale caches. Added exclusion for `*worker*` files in `shouldCacheResponse()` so worker JS is always fetched fresh. This prevents the cache-first strategy from serving outdated worker code. -**Impact:** Future worker code changes take effect immediately without manual cache clearing. - -## 5. TTS Synthesis Robustness -**Files:** `js/tts-worker.js`, `js/textToSpeech.js` -**What:** Added per-chunk 90s timeout (`Promise.race`), event loop yields between WASM calls (`await setTimeout(0)`), voice pre-fetch phase, heartbeat logger, `worker.onerror` handler, and version stamping. Failed chunks are skipped gracefully instead of aborting the entire podcast. -**Impact:** Audio synthesis is more resilient — provides real-time progress, detects hangs, and degrades gracefully on failures. - -## 6. Integration & UI Updates -**Files:** `index.html`, `js/renderer.js`, `js/templates.js`, `js/modal-templates.js`, `js/help-mode.js`, `src/main.js` -**What:** Added podcast module imports in `main.js`, podcast tag processing in `renderer.js`, marketplace modal in `modal-templates.js`, help entries in `help-mode.js`, and toolbar button in `templates.js`. Updated `index.html` with podcast CSS imports. -**Impact:** Podcast features are fully integrated into the TextAgent UI with discoverable entry points. - ---- - -## Files Changed (14 total) - -| File | Lines Changed | Type | -|------|:---:|------| -| `js/podcast-docgen.js` | +1046 | New: podcast generation engine | -| `js/podcast-marketplace.js` | +923 | New: marketplace UI | -| `css/podcast-marketplace.css` | +730 | New: marketplace styles | -| `css/podcast-docgen.css` | +406 | New: podcast player styles | -| `js/templates/podcasts.js` | +279 | New: podcast templates | -| `js/textToSpeech.js` | +205 −30 | Multi-speaker fix, worker caching, error handling | -| `js/tts-worker.js` | +203 −0 | processMultiSegments, bundled init, version stamp | -| `index.html` | +30 −23 | CSS imports, podcast integration | -| `js/renderer.js` | +12 −1 | Podcast tag processing | -| `js/help-mode.js` | +11 | Podcast help entries | -| `src/main.js` | +9 | Module imports | -| `js/templates.js` | +4 −1 | Toolbar button | -| `sw.js` | +3 −1 | Cache version bump, worker exclusion | -| `js/modal-templates.js` | +1 | Marketplace modal | diff --git a/CHANGELOG-share-link-loader.md b/CHANGELOG-share-link-loader.md deleted file mode 100644 index 2418c23..0000000 --- a/CHANGELOG-share-link-loader.md +++ /dev/null @@ -1,38 +0,0 @@ -# Share Link Loading Overlay — Eliminate Flash of Bare UI - -- Added full-screen loading overlay (`#share-loading-overlay`) that shows instantly when a share URL is detected -- Overlay activates before any JS or CSS loads via inline `