feat(chat): render LaTeX math formulas in assistant messages (#886)
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Haze <hazeone@users.noreply.github.com>
This commit is contained in:
@@ -98,7 +98,7 @@ ClawXは公式の**OpenClaw**コアを直接ベースに構築されています
|
||||
インストールから最初のAIインタラクションまで、すべてのセットアップを直感的なグラフィカルインターフェースで完了できます。ターミナルコマンド不要、YAMLファイル不要、環境変数の探索も不要です。
|
||||
|
||||
### 💬 インテリジェントチャットインターフェース
|
||||
モダンなチャット体験を通じてAIエージェントとコミュニケーションできます。複数の会話コンテキスト、メッセージ履歴、Markdownによるリッチコンテンツレンダリングに加え、マルチエージェント構成ではメイン入力欄の `@agent` から対象エージェントへ直接ルーティングできます。
|
||||
モダンなチャット体験を通じてAIエージェントとコミュニケーションできます。複数の会話コンテキスト、メッセージ履歴、Markdownによるリッチコンテンツレンダリング(GitHub 風テーブルや KaTeX による LaTeX 数式 `$インライン$`、`$$ブロック$$`、`\(インライン\)`、`\[ブロック\]` を含む)に加え、マルチエージェント構成ではメイン入力欄の `@agent` から対象エージェントへ直接ルーティングできます。
|
||||
`@agent` で別のエージェントを選ぶと、ClawX はデフォルトエージェントを経由せず、そのエージェント自身の会話コンテキストへ直接切り替えます。各エージェントのワークスペースは既定で分離されていますが、より強い実行時分離は OpenClaw の sandbox 設定に依存します。
|
||||
各 Agent は `provider/model` の実行時設定を個別に上書きできます。上書きしていない Agent は引き続きグローバルの既定モデルを継承します。
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ We are committed to maintaining strict alignment with the upstream OpenClaw proj
|
||||
Complete the entire setup—from installation to your first AI interaction—through an intuitive graphical interface. No terminal commands, no YAML files, no environment variable hunting.
|
||||
|
||||
### 💬 Intelligent Chat Interface
|
||||
Communicate with AI agents through a modern chat experience. Support for multiple conversation contexts, message history, rich content rendering with Markdown, and direct `@agent` routing in the main composer for multi-agent setups.
|
||||
Communicate with AI agents through a modern chat experience. Support for multiple conversation contexts, message history, rich content rendering with Markdown (including GitHub-flavored tables and KaTeX-powered LaTeX math: `$inline$`, `$$block$$`, `\(inline\)`, and `\[block\]`), and direct `@agent` routing in the main composer for multi-agent setups.
|
||||
When you target another agent with `@agent`, ClawX switches into that agent's own conversation context directly instead of relaying through the default agent. Agent workspaces stay separate by default, and stronger isolation depends on OpenClaw sandbox settings.
|
||||
Each agent can also override its own `provider/model` runtime setting; agents without overrides continue inheriting the global default model.
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ ClawX построен непосредственно на официально
|
||||
Весь процесс — от установки до первого взаимодействия с AI — выполняется через интуитивный графический интерфейс. Без терминальных команд, без YAML-файлов, без поиска переменных окружения.
|
||||
|
||||
### 💬 Интеллектуальный интерфейс чата
|
||||
Общайтесь с AI-агентами через современный чат. Поддержка нескольких контекстов разговора, истории сообщений, рендеринга Markdown и прямая маршрутизация через `@agent` в главном поле ввода для мультиагентных конфигураций.
|
||||
Общайтесь с AI-агентами через современный чат. Поддержка нескольких контекстов разговора, истории сообщений, рендеринга Markdown (включая таблицы GitHub-flavored и математические формулы LaTeX через KaTeX: `$строчные$`, `$$блочные$$`, `\(строчные\)` и `\[блочные\]`) и прямая маршрутизация через `@agent` в главном поле ввода для мультиагентных конфигураций.
|
||||
При выборе другого агента через `@agent` ClawX переключается непосредственно в контекст этого агента вместо ретрансляции через агента по умолчанию. Рабочие пространства агентов по умолчанию разделены, но более строгая изоляция зависит от настроек песочницы OpenClaw.
|
||||
Каждый агент может переопределить свои настройки `provider/model`; агенты без переопределения продолжают наследовать глобальную модель по умолчанию.
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ ClawX 直接基于官方 **OpenClaw** 核心构建。无需单独安装,我们
|
||||
从安装到第一次 AI 对话,全程通过直观的图形界面完成。无需终端命令,无需 YAML 文件,无需到处寻找环境变量。
|
||||
|
||||
### 💬 智能聊天界面
|
||||
通过现代化的聊天体验与 AI 智能体交互。支持多会话上下文、消息历史记录、Markdown 富文本渲染,以及在多 Agent 场景下通过主输入框中的 `@agent` 直接路由到目标智能体。
|
||||
通过现代化的聊天体验与 AI 智能体交互。支持多会话上下文、消息历史记录、Markdown 富文本渲染(包括 GitHub 风格表格以及由 KaTeX 渲染的 LaTeX 数学公式:`$行内$`、`$$块级$$`、`\(行内\)` 和 `\[块级\]`),以及在多 Agent 场景下通过主输入框中的 `@agent` 直接路由到目标智能体。
|
||||
当你使用 `@agent` 选择其他智能体时,ClawX 会直接切换到该智能体自己的对话上下文,而不是经过默认智能体转发。各 Agent 工作区默认彼此分离,但更强的运行时隔离仍取决于 OpenClaw 的 sandbox 配置。
|
||||
每个 Agent 还可以单独覆盖自己的 `provider/model` 运行时设置;未覆盖的 Agent 会继续继承全局默认模型。
|
||||
|
||||
|
||||
@@ -75,10 +75,13 @@
|
||||
"clawhub": "^0.5.0",
|
||||
"electron-store": "^11.0.2",
|
||||
"electron-updater": "^6.8.3",
|
||||
"katex": "^0.16.45",
|
||||
"lru-cache": "^11.2.6",
|
||||
"ms": "^2.1.3",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"posthog-node": "^5.28.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"ws": "^8.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
197
pnpm-lock.yaml
generated
197
pnpm-lock.yaml
generated
@@ -24,6 +24,9 @@ importers:
|
||||
electron-updater:
|
||||
specifier: ^6.8.3
|
||||
version: 6.8.3
|
||||
katex:
|
||||
specifier: ^0.16.45
|
||||
version: 0.16.45
|
||||
lru-cache:
|
||||
specifier: ^11.2.6
|
||||
version: 11.2.7
|
||||
@@ -36,6 +39,12 @@ importers:
|
||||
posthog-node:
|
||||
specifier: ^5.28.0
|
||||
version: 5.28.5
|
||||
rehype-katex:
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1
|
||||
remark-math:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
ws:
|
||||
specifier: ^8.19.0
|
||||
version: 8.20.0
|
||||
@@ -2783,6 +2792,9 @@ packages:
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
|
||||
|
||||
'@types/katex@0.16.8':
|
||||
resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==}
|
||||
|
||||
'@types/keyv@3.1.4':
|
||||
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
|
||||
|
||||
@@ -3471,6 +3483,10 @@ packages:
|
||||
resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
commander@8.3.0:
|
||||
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
commander@9.5.0:
|
||||
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
|
||||
engines: {node: ^12.20.0 || >=14}
|
||||
@@ -4339,15 +4355,39 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hast-util-from-dom@5.0.1:
|
||||
resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==}
|
||||
|
||||
hast-util-from-html-isomorphic@2.0.0:
|
||||
resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==}
|
||||
|
||||
hast-util-from-html@2.0.3:
|
||||
resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
|
||||
|
||||
hast-util-from-parse5@8.0.3:
|
||||
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
|
||||
|
||||
hast-util-is-element@3.0.0:
|
||||
resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
|
||||
|
||||
hast-util-parse-selector@4.0.0:
|
||||
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
|
||||
|
||||
hast-util-to-html@9.0.5:
|
||||
resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.6:
|
||||
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
|
||||
|
||||
hast-util-to-text@4.0.2:
|
||||
resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==}
|
||||
|
||||
hast-util-whitespace@3.0.0:
|
||||
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
|
||||
|
||||
hastscript@9.0.1:
|
||||
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
|
||||
|
||||
hermes-estree@0.25.1:
|
||||
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
|
||||
|
||||
@@ -4681,6 +4721,10 @@ packages:
|
||||
resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
katex@0.16.45:
|
||||
resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==}
|
||||
hasBin: true
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
@@ -4894,6 +4938,9 @@ packages:
|
||||
mdast-util-gfm@3.1.0:
|
||||
resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
|
||||
|
||||
mdast-util-math@3.0.0:
|
||||
resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==}
|
||||
|
||||
mdast-util-mdx-expression@2.0.1:
|
||||
resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
|
||||
|
||||
@@ -4957,6 +5004,9 @@ packages:
|
||||
micromark-extension-gfm@3.0.0:
|
||||
resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
|
||||
|
||||
micromark-extension-math@3.1.0:
|
||||
resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==}
|
||||
|
||||
micromark-factory-destination@2.0.1:
|
||||
resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
|
||||
|
||||
@@ -5462,6 +5512,9 @@ packages:
|
||||
parse5@6.0.1:
|
||||
resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
|
||||
|
||||
parse5@7.3.0:
|
||||
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||
|
||||
parse5@8.0.0:
|
||||
resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==}
|
||||
|
||||
@@ -5901,9 +5954,15 @@ packages:
|
||||
regex@6.1.0:
|
||||
resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
|
||||
|
||||
rehype-katex@7.0.1:
|
||||
resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==}
|
||||
|
||||
remark-gfm@4.0.1:
|
||||
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
|
||||
|
||||
remark-math@6.0.0:
|
||||
resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==}
|
||||
|
||||
remark-parse@11.0.0:
|
||||
resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
|
||||
|
||||
@@ -6505,12 +6564,18 @@ packages:
|
||||
resolution: {integrity: sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
unist-util-find-after@5.0.0:
|
||||
resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==}
|
||||
|
||||
unist-util-is@6.0.1:
|
||||
resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
|
||||
|
||||
unist-util-position@5.0.0:
|
||||
resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
|
||||
|
||||
unist-util-remove-position@5.0.0:
|
||||
resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==}
|
||||
|
||||
unist-util-stringify-position@4.0.0:
|
||||
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
|
||||
|
||||
@@ -6596,6 +6661,9 @@ packages:
|
||||
resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==}
|
||||
engines: {node: '>=0.6.0'}
|
||||
|
||||
vfile-location@5.0.3:
|
||||
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
|
||||
|
||||
vfile-message@4.0.3:
|
||||
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
|
||||
|
||||
@@ -6699,6 +6767,9 @@ packages:
|
||||
wcwidth@1.0.1:
|
||||
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
||||
|
||||
web-namespaces@2.0.1:
|
||||
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
||||
|
||||
web-streams-polyfill@3.3.3:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -10046,6 +10117,8 @@ snapshots:
|
||||
'@types/ms': 2.1.0
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/katex@0.16.8': {}
|
||||
|
||||
'@types/keyv@3.1.4':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
@@ -10876,6 +10949,8 @@ snapshots:
|
||||
|
||||
commander@5.1.0: {}
|
||||
|
||||
commander@8.3.0: {}
|
||||
|
||||
commander@9.5.0:
|
||||
optional: true
|
||||
|
||||
@@ -11942,6 +12017,47 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hast-util-from-dom@5.0.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
hastscript: 9.0.1
|
||||
web-namespaces: 2.0.1
|
||||
|
||||
hast-util-from-html-isomorphic@2.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-from-dom: 5.0.1
|
||||
hast-util-from-html: 2.0.3
|
||||
unist-util-remove-position: 5.0.0
|
||||
|
||||
hast-util-from-html@2.0.3:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
devlop: 1.1.0
|
||||
hast-util-from-parse5: 8.0.3
|
||||
parse5: 7.3.0
|
||||
vfile: 6.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
hast-util-from-parse5@8.0.3:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.3
|
||||
devlop: 1.1.0
|
||||
hastscript: 9.0.1
|
||||
property-information: 7.1.0
|
||||
vfile: 6.0.3
|
||||
vfile-location: 5.0.3
|
||||
web-namespaces: 2.0.1
|
||||
|
||||
hast-util-is-element@3.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hast-util-parse-selector@4.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hast-util-to-html@9.0.5:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
@@ -11976,10 +12092,25 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
hast-util-to-text@4.0.2:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.3
|
||||
hast-util-is-element: 3.0.0
|
||||
unist-util-find-after: 5.0.0
|
||||
|
||||
hast-util-whitespace@3.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hastscript@9.0.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
comma-separated-tokens: 2.0.3
|
||||
hast-util-parse-selector: 4.0.0
|
||||
property-information: 7.1.0
|
||||
space-separated-tokens: 2.0.2
|
||||
|
||||
hermes-estree@0.25.1: {}
|
||||
|
||||
hermes-parser@0.25.1:
|
||||
@@ -12327,6 +12458,10 @@ snapshots:
|
||||
|
||||
jwt-decode@4.0.0: {}
|
||||
|
||||
katex@0.16.45:
|
||||
dependencies:
|
||||
commander: 8.3.0
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
@@ -12608,6 +12743,18 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-math@3.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
longest-streak: 3.1.0
|
||||
mdast-util-from-markdown: 2.0.3
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
unist-util-remove-position: 5.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-mdx-expression@2.0.1:
|
||||
dependencies:
|
||||
'@types/estree-jsx': 1.0.5
|
||||
@@ -12767,6 +12914,16 @@ snapshots:
|
||||
micromark-util-combine-extensions: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-math@3.1.0:
|
||||
dependencies:
|
||||
'@types/katex': 0.16.8
|
||||
devlop: 1.1.0
|
||||
katex: 0.16.45
|
||||
micromark-factory-space: 2.0.1
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-factory-destination@2.0.1:
|
||||
dependencies:
|
||||
micromark-util-character: 2.1.1
|
||||
@@ -13423,6 +13580,10 @@ snapshots:
|
||||
|
||||
parse5@6.0.1: {}
|
||||
|
||||
parse5@7.3.0:
|
||||
dependencies:
|
||||
entities: 6.0.1
|
||||
|
||||
parse5@8.0.0:
|
||||
dependencies:
|
||||
entities: 6.0.1
|
||||
@@ -13847,6 +14008,16 @@ snapshots:
|
||||
dependencies:
|
||||
regex-utilities: 2.3.0
|
||||
|
||||
rehype-katex@7.0.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/katex': 0.16.8
|
||||
hast-util-from-html-isomorphic: 2.0.0
|
||||
hast-util-to-text: 4.0.2
|
||||
katex: 0.16.45
|
||||
unist-util-visit-parents: 6.0.2
|
||||
vfile: 6.0.3
|
||||
|
||||
remark-gfm@4.0.1:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
@@ -13858,6 +14029,15 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
remark-math@6.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-math: 3.0.0
|
||||
micromark-extension-math: 3.1.0
|
||||
unified: 11.0.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
remark-parse@11.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
@@ -14546,6 +14726,11 @@ snapshots:
|
||||
dependencies:
|
||||
imurmurhash: 0.1.4
|
||||
|
||||
unist-util-find-after@5.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
unist-util-is: 6.0.1
|
||||
|
||||
unist-util-is@6.0.1:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
@@ -14554,6 +14739,11 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
unist-util-remove-position@5.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
unist-util-visit: 5.1.0
|
||||
|
||||
unist-util-stringify-position@4.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
@@ -14629,6 +14819,11 @@ snapshots:
|
||||
extsprintf: 1.4.1
|
||||
optional: true
|
||||
|
||||
vfile-location@5.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
vfile: 6.0.3
|
||||
|
||||
vfile-message@4.0.3:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
@@ -14697,6 +14892,8 @@ snapshots:
|
||||
dependencies:
|
||||
defaults: 1.0.4
|
||||
|
||||
web-namespaces@2.0.1: {}
|
||||
|
||||
web-streams-polyfill@3.3.3: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { HashRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import './i18n';
|
||||
import './styles/globals.css';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import { initializeDefaultTransports } from './lib/api-client';
|
||||
|
||||
initializeDefaultTransports();
|
||||
|
||||
@@ -7,6 +7,8 @@ import { useState, useCallback, useEffect, memo } from 'react';
|
||||
import { Sparkles, Copy, Check, ChevronDown, ChevronRight, Wrench, FileText, Film, Music, FileArchive, File, X, FolderOpen, ZoomIn, Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -40,6 +42,36 @@ interface ChatMessageProps {
|
||||
|
||||
interface ExtractedImage { url?: string; data?: string; mimeType: string; }
|
||||
|
||||
/**
|
||||
* Normalize LaTeX delimiters so `remark-math` can detect them.
|
||||
*
|
||||
* Many LLMs emit LaTeX using `\(` / `\)` for inline math and `\[` / `\]`
|
||||
* for block math (OpenAI style), which are NOT recognized by remark-math.
|
||||
* remark-math only parses `$...$` and `$$...$$`.
|
||||
*
|
||||
* We convert the backslash-paren/bracket forms to dollar-sign forms so the
|
||||
* math is rendered regardless of which convention the model uses.
|
||||
*
|
||||
* Transformations are skipped inside fenced/inline code spans to avoid
|
||||
* clobbering code samples that legitimately contain `\(` etc.
|
||||
*/
|
||||
function normalizeLatexDelimiters(input: string): string {
|
||||
if (!input || (input.indexOf('\\(') === -1 && input.indexOf('\\[') === -1)) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const parts = input.split(/(```[\s\S]*?```|`[^`\n]*`)/g);
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
if (!part) continue;
|
||||
if (part.startsWith('```') || part.startsWith('`')) continue;
|
||||
let next = part.replace(/\\\[([\s\S]+?)\\\]/g, (_m, body: string) => `\n$$\n${body.trim()}\n$$\n`);
|
||||
next = next.replace(/\\\(([\s\S]+?)\\\)/g, (_m, body: string) => `$${body}$`);
|
||||
parts[i] = next;
|
||||
}
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
/** Resolve an ExtractedImage to a displayable src string, or null if not possible. */
|
||||
function imageSrc(img: ExtractedImage): string | null {
|
||||
if (img.url) return img.url;
|
||||
@@ -368,7 +400,8 @@ function MessageBubble({
|
||||
) : (
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none break-words break-all">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[[rehypeKatex, { strict: false, throwOnError: false, output: 'html' }]]}
|
||||
components={{
|
||||
code({ className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
@@ -397,7 +430,7 @@ function MessageBubble({
|
||||
},
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
{normalizeLatexDelimiters(text)}
|
||||
</ReactMarkdown>
|
||||
{isStreaming && (
|
||||
<span className="inline-block w-2 h-4 bg-foreground/50 animate-pulse ml-0.5" />
|
||||
@@ -426,7 +459,12 @@ function ThinkingBlock({ content }: { content: string }) {
|
||||
{expanded && (
|
||||
<div className="px-3 pb-3 text-muted-foreground">
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none opacity-75">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[[rehypeKatex, { strict: false, throwOnError: false, output: 'html' }]]}
|
||||
>
|
||||
{normalizeLatexDelimiters(content)}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
109
tests/e2e/chat-latex-rendering.spec.ts
Normal file
109
tests/e2e/chat-latex-rendering.spec.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { closeElectronApp, expect, getStableWindow, installIpcMocks, test } from './fixtures/electron';
|
||||
|
||||
const SESSION_KEY = 'agent:main:main';
|
||||
|
||||
function stableStringify(value: unknown): string {
|
||||
if (value == null || typeof value !== 'object') return JSON.stringify(value);
|
||||
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
||||
const entries = Object.entries(value as Record<string, unknown>)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`);
|
||||
return `{${entries.join(',')}}`;
|
||||
}
|
||||
|
||||
const seededHistory = [
|
||||
{
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: 'Show me Einstein\'s mass-energy equivalence and a definite integral.' }],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: [
|
||||
'Sure! Einstein famously wrote $E=mc^2$, and the quadratic formula is \\(x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}\\).',
|
||||
'',
|
||||
'A definite integral:',
|
||||
'',
|
||||
'$$',
|
||||
'\\int_0^1 x\\,dx = \\frac{1}{2}',
|
||||
'$$',
|
||||
'',
|
||||
'And a sum with bracket-style block math:',
|
||||
'',
|
||||
'\\[\\sum_{i=1}^n i = \\frac{n(n+1)}{2}\\]',
|
||||
].join('\n'),
|
||||
}],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
];
|
||||
|
||||
test.describe('ClawX chat LaTeX rendering', () => {
|
||||
test('renders KaTeX markup for $...$, $$...$$, \\(...\\) and \\[...\\] delimiters', async ({ launchElectronApp }) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
|
||||
try {
|
||||
await installIpcMocks(app, {
|
||||
gatewayStatus: { state: 'running', port: 18789, pid: 12345 },
|
||||
gatewayRpc: {
|
||||
[stableStringify(['sessions.list', {}])]: {
|
||||
success: true,
|
||||
result: {
|
||||
sessions: [{ key: SESSION_KEY, displayName: 'main' }],
|
||||
},
|
||||
},
|
||||
[stableStringify(['chat.history', { sessionKey: SESSION_KEY, limit: 200 }])]: {
|
||||
success: true,
|
||||
result: { messages: seededHistory },
|
||||
},
|
||||
[stableStringify(['chat.history', { sessionKey: SESSION_KEY, limit: 1000 }])]: {
|
||||
success: true,
|
||||
result: { messages: seededHistory },
|
||||
},
|
||||
},
|
||||
hostApi: {
|
||||
[stableStringify(['/api/gateway/status', 'GET'])]: {
|
||||
ok: true,
|
||||
data: {
|
||||
status: 200,
|
||||
ok: true,
|
||||
json: { state: 'running', port: 18789, pid: 12345 },
|
||||
},
|
||||
},
|
||||
[stableStringify(['/api/agents', 'GET'])]: {
|
||||
ok: true,
|
||||
data: {
|
||||
status: 200,
|
||||
ok: true,
|
||||
json: {
|
||||
success: true,
|
||||
agents: [{ id: 'main', name: 'main' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const page = await getStableWindow(app);
|
||||
try {
|
||||
await page.reload();
|
||||
} catch (error) {
|
||||
if (!String(error).includes('ERR_FILE_NOT_FOUND')) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
|
||||
// Wait for a KaTeX inline rendering to appear.
|
||||
await expect(page.locator('.katex').first()).toBeVisible({ timeout: 30_000 });
|
||||
// Inline math: $E=mc^2$
|
||||
await expect(page.locator('.katex').filter({ hasText: /E\s*=\s*mc/ }).first()).toBeVisible();
|
||||
// Display math: both $$...$$ and \[...\] forms produce .katex-display blocks.
|
||||
await expect(page.locator('.katex-display')).toHaveCount(2);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -30,3 +30,52 @@ describe('ChatMessage attachment dedupe', () => {
|
||||
expect(screen.getByAltText('artifact.png')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ChatMessage LaTeX rendering', () => {
|
||||
it('renders inline `$...$` math with KaTeX', () => {
|
||||
const message: RawMessage = {
|
||||
role: 'assistant',
|
||||
content: 'Mass-energy equivalence: $E=mc^2$ is famous.',
|
||||
};
|
||||
const { container } = render(<ChatMessage message={message} />);
|
||||
expect(container.querySelector('.katex')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('renders display `$$...$$` math as a block', () => {
|
||||
const message: RawMessage = {
|
||||
role: 'assistant',
|
||||
content: 'Definite integral:\n\n$$\n\\int_0^1 x\\,dx = \\frac{1}{2}\n$$\n',
|
||||
};
|
||||
const { container } = render(<ChatMessage message={message} />);
|
||||
expect(container.querySelector('.katex-display')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('renders `\\(...\\)` inline math (OpenAI-style escaping)', () => {
|
||||
const message: RawMessage = {
|
||||
role: 'assistant',
|
||||
content: 'Quadratic formula: \\(x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}\\).',
|
||||
};
|
||||
const { container } = render(<ChatMessage message={message} />);
|
||||
expect(container.querySelector('.katex')).not.toBeNull();
|
||||
expect(container.querySelector('.katex-display')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders `\\[...\\]` block math (OpenAI-style escaping)', () => {
|
||||
const message: RawMessage = {
|
||||
role: 'assistant',
|
||||
content: 'Sum formula:\n\n\\[\\sum_{i=1}^n i = \\frac{n(n+1)}{2}\\]',
|
||||
};
|
||||
const { container } = render(<ChatMessage message={message} />);
|
||||
expect(container.querySelector('.katex-display')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('does not rewrite `\\(` inside code fences', () => {
|
||||
const message: RawMessage = {
|
||||
role: 'assistant',
|
||||
content: 'Code sample:\n\n```\nprintf("\\(hello\\)")\n```\n',
|
||||
};
|
||||
const { container } = render(<ChatMessage message={message} />);
|
||||
expect(container.textContent).toContain('\\(hello\\)');
|
||||
expect(container.querySelector('.katex')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user