diff --git a/docs/plans/2026-05-23-latex-rendering.md b/docs/plans/2026-05-23-latex-rendering.md new file mode 100644 index 0000000..bc9aad4 --- /dev/null +++ b/docs/plans/2026-05-23-latex-rendering.md @@ -0,0 +1,490 @@ +# LaTeX Rendering Implementation Plan + +> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. + +**Goal:** Add reliable LaTeX/math rendering to Hermes Web UI chat Markdown messages so formulas render visually instead of appearing as raw TeX. + +**Architecture:** Keep the existing `markdown-it` renderer in `MarkdownRenderer.vue` and add a KaTeX-backed math plugin there. Load KaTeX styles globally once, cover inline and block math in component tests, then verify with a production build and a local Web UI smoke test. + +**Tech Stack:** Vue 3, TypeScript, Vite, markdown-it, KaTeX, Vitest, @vue/test-utils. + +--- + +## Acceptance Criteria + +- Chat messages render inline math like `$x^2 + y^2 = z^2$` as KaTeX HTML. +- Chat messages render block math like `$$\n\\int_0^1 x^2 dx = \\frac{1}{3}\n$$` as display KaTeX HTML. +- Existing Markdown features still work: code fences, Mermaid fences, headings, local file links, mentions. +- Math inside fenced code blocks stays literal and is not rendered. +- Invalid/unsupported math syntax does not crash the chat renderer. +- `npm run test -- tests/client/markdown-rendering.test.ts` passes. +- `npm run build` passes. +- Local `hermes-web-ui.service` can be restarted onto the built fork and displays formulas in the real panel. + +## Recommended Dependency Choice + +Use `markdown-it-katex` + `katex` because the app already uses `markdown-it`, and this is the smallest change surface. + +Install as runtime dependencies, not dev-only, because the renderer runs in the browser bundle: + +```bash +npm install katex markdown-it-katex +npm install -D @types/katex +``` + +If TypeScript reports no module declaration for `markdown-it-katex`, add a local declaration file instead of weakening `tsconfig`. + +--- + +## Task 1: Add KaTeX dependencies + +**Objective:** Make the required renderer and styles available to the client bundle. + +**Files:** +- Modify: `package.json` +- Modify: `package-lock.json` or lockfile generated by npm if present + +**Step 1: Install packages** + +Run from repo root: + +```bash +cd /home/werserk/2-kira/hermes-web-ui +npm install katex markdown-it-katex +npm install -D @types/katex +``` + +**Expected:** `package.json` includes `katex` and `markdown-it-katex`; lockfile is updated. + +**Step 2: Verify dependency tree** + +```bash +npm ls katex markdown-it-katex +``` + +**Expected:** both packages resolve without `UNMET DEPENDENCY`. + +**Step 3: Commit** + +```bash +git add package.json package-lock.json +git commit -m "chore: add latex rendering dependencies" +``` + +--- + +## Task 2: Add TypeScript module declaration if needed + +**Objective:** Keep TypeScript strict and avoid `any` leaking into the renderer setup. + +**Files:** +- Create if needed: `packages/client/src/types/markdown-it-katex.d.ts` + +**Step 1: Run typecheck to discover whether declaration is needed** + +```bash +npm run build +``` + +**Expected before implementation:** build may still fail later because code is not changed yet. For this task, only check whether TypeScript knows the `markdown-it-katex` module after it is imported in Task 3. + +**Step 2: If TS2307/TS7016 appears, create declaration** + +Create `packages/client/src/types/markdown-it-katex.d.ts`: + +```ts +declare module 'markdown-it-katex' { + import type MarkdownIt from 'markdown-it' + + interface MarkdownItKatexOptions { + throwOnError?: boolean + errorColor?: string + strict?: boolean | string | ((errorCode: string) => boolean | string) + output?: 'html' | 'mathml' | 'htmlAndMathml' + trust?: boolean | ((context: unknown) => boolean) + macros?: Record + } + + const markdownItKatex: MarkdownIt.PluginWithOptions + export default markdownItKatex +} +``` + +**Step 3: Commit only if file was needed** + +```bash +git add packages/client/src/types/markdown-it-katex.d.ts +git commit -m "chore: add markdown-it-katex types" +``` + +--- + +## Task 3: Wire KaTeX into MarkdownRenderer + +**Objective:** Render math in the same path that currently renders Markdown chat messages. + +**Files:** +- Modify: `packages/client/src/components/hermes/chat/MarkdownRenderer.vue` + +**Step 1: Import the plugin** + +Near existing Markdown imports: + +```ts +import markdownItKatex from 'markdown-it-katex' +``` + +**Step 2: Register the plugin after creating `md`** + +After the `new MarkdownItConstructor(...)` block and before custom fence renderer setup: + +```ts +md.use(markdownItKatex, { + throwOnError: false, + errorColor: '#cc3344', + output: 'htmlAndMathml', +}) +``` + +**Step 3: Preserve existing Mermaid fence behavior** + +Do not move or remove this existing code: + +```ts +const defaultFenceRenderer = md.renderer.rules.fence?.bind(md.renderer.rules) + +md.renderer.rules.fence = (tokens, idx, options, env, self) => { + const token = tokens[idx] + if (isMermaidFence(token.info)) { + return renderMermaidPlaceholder(token.content) + } + + if (defaultFenceRenderer) { + return defaultFenceRenderer(tokens, idx, options, env, self) + } + + return self.renderToken(tokens, idx, options) +} +``` + +**Step 4: Run focused test suite** + +```bash +npm run test -- tests/client/markdown-rendering.test.ts +``` + +**Expected:** existing tests still pass or only fail because new math tests are not yet added. + +**Step 5: Commit** + +```bash +git add packages/client/src/components/hermes/chat/MarkdownRenderer.vue +git commit -m "feat: enable latex rendering in chat markdown" +``` + +--- + +## Task 4: Load and tune KaTeX CSS + +**Objective:** Ensure rendered math is visually correct in the Web UI. + +**Files:** +- Modify: `packages/client/src/main.ts` +- Optionally modify: `packages/client/src/styles/global.scss` + +**Step 1: Import KaTeX CSS once globally** + +In `packages/client/src/main.ts`, add after global styles import: + +```ts +import 'katex/dist/katex.min.css' +``` + +Current import area should become: + +```ts +import App from './App.vue' +import './styles/global.scss' +import 'katex/dist/katex.min.css' +``` + +**Step 2: Add Hermes-specific layout tweaks only if needed** + +If visual smoke test shows cramped block equations, append to `packages/client/src/styles/global.scss`: + +```scss +.markdown-body { + .katex-display { + overflow-x: auto; + overflow-y: hidden; + padding: 0.25rem 0; + } +} +``` + +Do not restyle KaTeX itself unless there is a concrete visual bug. + +**Step 3: Commit** + +```bash +git add packages/client/src/main.ts packages/client/src/styles/global.scss +git commit -m "style: load katex styles for markdown math" +``` + +--- + +## Task 5: Add regression tests for math rendering + +**Objective:** Prove formulas render, code fences stay literal, and malformed math is safe. + +**Files:** +- Modify: `tests/client/markdown-rendering.test.ts` + +**Step 1: Add inline math test** + +Inside `describe('MarkdownRenderer', () => { ... })`: + +```ts +it('renders inline latex math with katex', () => { + const wrapper = mount(MarkdownRenderer, { + props: { + content: 'Pythagoras: $x^2 + y^2 = z^2$.', + }, + }) + + const body = wrapper.find('.markdown-body') + expect(body.find('.katex').exists()).toBe(true) + expect(body.html()).toContain('x') + expect(body.html()).toContain('z') + expect(body.text()).not.toContain('$x^2 + y^2 = z^2$') +}) +``` + +**Step 2: Add block math test** + +```ts +it('renders display latex math with katex', () => { + const wrapper = mount(MarkdownRenderer, { + props: { + content: '$$\n\\int_0^1 x^2 dx = \\frac{1}{3}\n$$', + }, + }) + + const body = wrapper.find('.markdown-body') + expect(body.find('.katex-display').exists()).toBe(true) + expect(body.find('.katex').exists()).toBe(true) + expect(body.text()).not.toContain('$$') +}) +``` + +**Step 3: Add fenced-code non-rendering test** + +```ts +it('does not render latex inside fenced code blocks', () => { + const wrapper = mount(MarkdownRenderer, { + props: { + content: '```md\n$x^2 + y^2 = z^2$\n```', + }, + }) + + expect(wrapper.find('.markdown-body').find('.katex').exists()).toBe(false) + expect(wrapper.find('code.hljs').text()).toContain('$x^2 + y^2 = z^2$') +}) +``` + +**Step 4: Add invalid math safety test** + +```ts +it('keeps rendering when latex syntax is invalid', () => { + const wrapper = mount(MarkdownRenderer, { + props: { + content: 'Before $\\notacommand{ after', + }, + }) + + expect(wrapper.find('.markdown-body').text()).toContain('Before') +}) +``` + +**Step 5: Run focused tests** + +```bash +npm run test -- tests/client/markdown-rendering.test.ts +``` + +**Expected:** all tests in the file pass. + +**Step 6: Commit** + +```bash +git add tests/client/markdown-rendering.test.ts +git commit -m "test: cover latex rendering in markdown" +``` + +--- + +## Task 6: Run build and broader regression checks + +**Objective:** Catch TypeScript, Vite, and existing client regressions before deploying locally. + +**Files:** +- No source changes expected unless checks fail. + +**Step 1: Run focused tests** + +```bash +npm run test -- tests/client/markdown-rendering.test.ts tests/client/markdown-rendering-mermaid-import-timeout.test.ts +``` + +**Expected:** pass. + +**Step 2: Run all tests if practical** + +```bash +npm run test +``` + +**Expected:** pass. If unrelated pre-existing failures appear, record them in the PR body with evidence. + +**Step 3: Run production build** + +```bash +npm run build +``` + +**Expected:** pass and update `dist/` only as build output. Do not commit `dist/` unless the package/release workflow requires it. + +**Step 4: Commit fixes only if needed** + +```bash +git add +git commit -m "fix: stabilize latex markdown rendering" +``` + +--- + +## Task 7: Deploy to Maxim's local Hermes Web UI instance + +**Objective:** Start using the feature on `https://hermes.kiraproject.ru/` before upstream PR review completes. + +**Files:** +- Local runtime install under npm/global prefix may change outside the repo. +- Do not commit generated deployment artifacts unless intentionally part of the repo. + +**Step 1: Build from the fork checkout** + +```bash +cd /home/werserk/2-kira/hermes-web-ui +npm run build +``` + +**Step 2: Replace currently installed package using npm link or local global install** + +Preferred reversible approach: + +```bash +cd /home/werserk/2-kira/hermes-web-ui +npm install -g --prefix /home/werserk/.npm-global . +``` + +**Step 3: Restart the Web UI service** + +```bash +systemctl --user restart hermes-web-ui.service +systemctl --user status hermes-web-ui.service --no-pager --lines=20 +``` + +**Step 4: Smoke test HTTP response** + +```bash +curl -sS -I --max-time 5 http://127.0.0.1:8648/ | head -20 +``` + +**Expected:** HTTP 200/304 or another successful static response, not connection refused. + +**Step 5: Browser smoke test** + +Open the chat panel and send/render a message containing: + +```md +Inline: $x^2 + y^2 = z^2$ + +Block: +$$ +\\int_0^1 x^2 dx = \\frac{1}{3} +$$ + +Code fence should stay literal: +```md +$x^2 + y^2 = z^2$ +``` +``` + +**Expected:** first two formulas render visually; fenced formula stays literal. + +--- + +## Task 8: Prepare upstream PR + +**Objective:** Make the contribution easy for upstream maintainers to review. + +**Files:** +- No code changes required unless PR polish finds an issue. + +**Step 1: Push branch** + +```bash +git push origin feat/latex-rendering +``` + +**Step 2: Create PR draft** + +```bash +gh pr create \ + --repo EKKOLearnAI/hermes-web-ui \ + --head kira-project-lab:feat/latex-rendering \ + --base main \ + --title "feat(chat): render LaTeX math in Markdown messages" \ + --body "$(cat <<'EOF' +## Summary +- Adds KaTeX-backed LaTeX rendering to chat Markdown messages. +- Supports inline `$...$` and display `$$...$$` math. +- Keeps math inside fenced code blocks literal. + +## Test Plan +- [ ] npm run test -- tests/client/markdown-rendering.test.ts +- [ ] npm run test -- tests/client/markdown-rendering-mermaid-import-timeout.test.ts +- [ ] npm run build +- [ ] Manual browser smoke test with inline, block, and fenced math + +## Notes +This uses the existing markdown-it rendering path and keeps HTML disabled. +EOF +)" \ + --draft +``` + +**Step 3: Attach visual proof** + +Add before/after screenshots in the PR body or a PR comment: + +- Before: raw `$...$` / `$$...$$` text. +- After: rendered KaTeX inline and display equations. + +--- + +## Risks and Guardrails + +- **Delimiter ambiguity:** `$` is common in shell snippets and prices. The fenced-code test protects code blocks, but inline text like `cost is $5 and $6` may be parsed depending on plugin behavior. If this is noisy, switch to `markdown-it-texmath` with explicit delimiter options or configure delimiters to prioritize `\(...\)` / `\[...\]`. +- **Security:** keep `html: false` in `markdown-it`; do not enable arbitrary HTML. KaTeX should run with `trust: false` unless a clear need appears. +- **Bundle size:** KaTeX adds client bundle weight. Acceptable for this feature, but mention it in PR if maintainers ask. +- **CSS scope:** KaTeX CSS is global. Import once in `main.ts`; avoid duplicating it inside component styles. +- **Invalid math:** `throwOnError: false` should prevent chat crashes. Regression test required. + +## Definition of Done + +- Branch `feat/latex-rendering` contains committed implementation. +- Local Web UI renders inline and block formulas. +- Focused Markdown tests pass. +- Production build passes. +- PR draft exists against `EKKOLearnAI/hermes-web-ui:main` or is ready to create.