docs: add latex rendering implementation plan
This commit is contained in:
@@ -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<string, string>
|
||||
}
|
||||
|
||||
const markdownItKatex: MarkdownIt.PluginWithOptions<MarkdownItKatexOptions>
|
||||
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 <fixed-files>
|
||||
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.
|
||||
Reference in New Issue
Block a user