diff --git a/package-lock.json b/package-lock.json index 77ecd1f..7380ee0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,12 +15,14 @@ "@wecom/aibot-node-sdk": "^1.0.6", "cross-spawn": "^7.0.6", "dingtalk-stream": "^2.1.6-beta.1", + "dompurify": "^3.4.13", "fflate": "^0.8.3", "html-to-text": "^10.0.0", "i18next": "^25.10.10", "json5": "^2.2.3", "katex": "^0.16.47", "lucide-react": "^1.27.0", + "mermaid": "^11.16.1", "onnxruntime-web": "^1.23.2", "pdfjs-dist": "^6.2.108", "ppu-paddle-ocr": "^6.4.0", @@ -76,6 +78,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@antv/algorithm": { "version": "0.1.26", "resolved": "https://registry.npmjs.org/@antv/algorithm/-/algorithm-0.1.26.tgz", @@ -671,6 +686,12 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -684,6 +705,12 @@ "specificity": "bin/cli.js" } }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, "node_modules/@continuedev/cli": { "version": "1.5.47", "resolved": "https://registry.npmjs.org/@continuedev/cli/-/cli-1.5.47.tgz", @@ -1929,6 +1956,23 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", @@ -2285,6 +2329,15 @@ "node": ">=10" } }, + "node_modules/@mermaid-js/parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.0.tgz", + "integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", @@ -3337,24 +3390,111 @@ "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "license": "MIT" }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, "node_modules/@types/d3-dispatch": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", "license": "MIT" }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, "node_modules/@types/d3-dsv": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", @@ -3418,6 +3558,12 @@ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", "license": "MIT" }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, "node_modules/@types/d3-quadtree": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", @@ -3445,6 +3591,12 @@ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", "license": "MIT" }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, "node_modules/@types/d3-shape": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", @@ -3460,12 +3612,37 @@ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "license": "MIT" }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, "node_modules/@types/d3-timer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -3629,6 +3806,13 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -3884,6 +4068,16 @@ "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", "license": "ISC" }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/@vitejs/plugin-react": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", @@ -5120,6 +5314,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cross-dirname": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", @@ -5179,6 +5382,95 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/cytoscape": { + "version": "3.34.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.1.tgz", + "integrity": "sha512-Lr0RvH9H75y9ar8h9Toy6u4lxRSCcxUq+hHcQ26sVWo6BnaQp1gwEZOYqwuYTZhyW7npyKnNLP8oJ2p1/3OZ7g==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", @@ -5191,12 +5483,49 @@ "node": ">=12" } }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-binarytree": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", "license": "MIT" }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", @@ -5206,6 +5535,30 @@ "node": ">=12" } }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-dispatch": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", @@ -5215,6 +5568,19 @@ "node": ">=12" } }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-dsv": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", @@ -5399,6 +5765,15 @@ "node": ">=12" } }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-quadtree": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", @@ -5423,6 +5798,46 @@ "integrity": "sha512-PF8GWEL70cHHWpx2jUQXc68r1pyPHIA+St16muk/XRokETzlegj5LriNKg7o4LR0TySug4nHYPJNNRz/W+/Niw==", "license": "BSD-3-Clause" }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, "node_modules/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", @@ -5452,6 +5867,15 @@ "node": ">=12" } }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-shape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", @@ -5497,6 +5921,41 @@ "node": ">=12" } }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/dagre": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", @@ -5507,6 +5966,16 @@ "lodash": "^4.17.15" } }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -5536,6 +6005,12 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -5675,6 +6150,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5856,6 +6340,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -6245,6 +6738,17 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types" + ] + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -7254,6 +7758,12 @@ "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", "license": "ISC" }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -7789,6 +8299,16 @@ "node": ">= 4" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -8234,6 +8754,17 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/lazy-val": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", @@ -8381,6 +8912,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -8737,6 +9280,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mermaid": { + "version": "11.16.1", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.1.tgz", + "integrity": "sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.2.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.20", + "dompurify": "^3.3.3", + "es-toolkit": "^1.45.1", + "katex": "^0.16.45", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -10050,6 +10622,12 @@ "node": ">=6" } }, + "node_modules/package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "license": "MIT" + }, "node_modules/parchment": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz", @@ -10116,6 +10694,12 @@ "node": ">= 0.8" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10323,6 +10907,22 @@ "node": ">=14.19.0" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/postcss": { "version": "8.5.25", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", @@ -11194,6 +11794,12 @@ "node": ">=8.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.62.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.3.tgz", @@ -11239,6 +11845,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -11753,6 +12371,12 @@ "inline-style-parser": "0.2.7" } }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -11885,7 +12509,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -12036,6 +12659,15 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -12427,6 +13059,19 @@ "base64-arraybuffer": "^1.0.2" } }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 1bf1944..3cef25a 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,18 @@ { "from": "node_modules/katex/LICENSE", "to": "licenses/katex-MIT.txt" + }, + { + "from": "node_modules/mermaid/LICENSE", + "to": "licenses/mermaid-MIT.txt" + }, + { + "from": "node_modules/dompurify/LICENSE", + "to": "licenses/dompurify-Apache-2.0.txt" + }, + { + "from": "node_modules/dompurify/LICENSE-MPL", + "to": "licenses/dompurify-MPL-2.0.txt" } ], "win": { @@ -165,12 +177,14 @@ "@wecom/aibot-node-sdk": "^1.0.6", "cross-spawn": "^7.0.6", "dingtalk-stream": "^2.1.6-beta.1", + "dompurify": "^3.4.13", "fflate": "^0.8.3", "html-to-text": "^10.0.0", "i18next": "^25.10.10", "json5": "^2.2.3", "katex": "^0.16.47", "lucide-react": "^1.27.0", + "mermaid": "^11.16.1", "onnxruntime-web": "^1.23.2", "pdfjs-dist": "^6.2.108", "ppu-paddle-ocr": "^6.4.0", diff --git a/src/renderer/src/App.test.tsx b/src/renderer/src/App.test.tsx index dec34de..fcafe16 100644 --- a/src/renderer/src/App.test.tsx +++ b/src/renderer/src/App.test.tsx @@ -4314,6 +4314,82 @@ describe('App', () => { ).not.toBeInTheDocument() }) + it('excludes ignored heartbeat suggestions from the navigation badge', async () => { + const heartbeatId = '00000000-0000-4000-8000-000000000701' + const cancelledTaskId = '00000000-0000-4000-8000-000000000801' + const pendingTaskId = '00000000-0000-4000-8000-000000000802' + vi.mocked(api.heartbeats.list).mockResolvedValue([ + { + id: heartbeatId, + projectId, + name: '每日回顾', + timezone: 'Asia/Shanghai', + recurrence: { type: 'daily', localTime: '09:00' }, + enabled: true, + lookbackHours: 24, + retentionDays: 30, + nextRunAt: '2026-08-05T01:00:00.000Z', + createdAt: '2026-08-01T00:00:00.000Z', + updatedAt: '2026-08-01T00:00:00.000Z' + } + ]) + vi.mocked(api.heartbeats.history).mockResolvedValue({ + runs: [], + entries: [ + { + id: '00000000-0000-4000-8000-000000000901', + configId: heartbeatId, + runId: '00000000-0000-4000-8000-000000000902', + scheduledFor: '2026-08-04T01:00:00.000Z', + summary: '建议处理两个后续行动。', + highlights: [], + proposedMemoryIds: [], + followUpTaskIds: [cancelledTaskId, pendingTaskId], + createdAt: '2026-08-04T01:00:00.000Z' + } + ] + }) + vi.mocked(api.tasks.list).mockResolvedValue([ + { + id: cancelledTaskId, + projectId, + title: '已忽略建议', + instructions: '无需继续处理。', + origin: 'assistant', + status: 'cancelled', + createdAt: '2026-08-04T01:00:00.000Z' + }, + { + id: pendingTaskId, + projectId, + title: '待处理建议', + instructions: '继续处理此建议。', + origin: 'assistant', + status: 'paused', + createdAt: '2026-08-04T01:00:00.000Z' + } + ]) + + try { + render() + + expect( + await screen.findByLabelText('1 条待处理建议') + ).toBeInTheDocument() + expect( + screen.queryByLabelText('2 条待处理建议') + ).not.toBeInTheDocument() + } finally { + cleanup() + vi.mocked(api.heartbeats.list).mockResolvedValue([]) + vi.mocked(api.heartbeats.history).mockResolvedValue({ + runs: [], + entries: [] + }) + vi.mocked(api.tasks.list).mockResolvedValue([]) + } + }) + it('shows retryable page-local knowledge errors without an empty-state flash', async () => { vi.mocked(api.knowledge.getSnapshot).mockRejectedValueOnce( new Error('知识数据库暂时不可用') diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 4e93fbb..a9ee0d0 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -2120,7 +2120,9 @@ function App(): React.JSX.Element { ).length + assistantTasks.filter( (task) => - taskIds.has(task.id) && task.status !== 'completed' + taskIds.has(task.id) && + task.status !== 'completed' && + task.status !== 'cancelled' ).length ) }, [assistantMemories, assistantTasks, heartbeatEntries]) diff --git a/src/renderer/src/KnowledgeGraphChart.tsx b/src/renderer/src/KnowledgeGraphChart.tsx index 740f943..f8cb2d8 100644 --- a/src/renderer/src/KnowledgeGraphChart.tsx +++ b/src/renderer/src/KnowledgeGraphChart.tsx @@ -13,6 +13,7 @@ import type { KnowledgeGraphNode, KnowledgeGraphRelation } from '../../shared/contracts' +import { useDocumentTheme } from './use-document-theme' type ChartKnowledgeGraphNode = Omit< KnowledgeGraphNode, @@ -347,7 +348,7 @@ export function KnowledgeGraphChart({ () => graphRevision(nodes, relations), [nodes, relations] ) - const [themeRevision, setThemeRevision] = useState(0) + const documentTheme = useDocumentTheme() const [renderError, setRenderError] = useState() useEffect(() => { @@ -407,20 +408,6 @@ export function KnowledgeGraphChart({ }) }, [dataRevision, fitViewRequest, renderErrorFallback]) - useEffect(() => { - if (typeof MutationObserver !== 'function') { - return - } - const observer = new MutationObserver(() => { - setThemeRevision((revision) => revision + 1) - }) - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ['data-theme'] - }) - return () => observer.disconnect() - }, []) - useEffect(() => { const container = containerRef.current if (!container) { @@ -583,7 +570,7 @@ export function KnowledgeGraphChart({ locale, relationFallback, renderErrorFallback, - themeRevision + documentTheme ]) useEffect(() => { diff --git a/src/renderer/src/MarkdownRenderer.test.tsx b/src/renderer/src/MarkdownRenderer.test.tsx index d261dc0..311ac41 100644 --- a/src/renderer/src/MarkdownRenderer.test.tsx +++ b/src/renderer/src/MarkdownRenderer.test.tsx @@ -1,10 +1,45 @@ -import { cleanup, render, screen } from '@testing-library/react' -import { afterEach, describe, expect, it } from 'vitest' +import { + act, + cleanup, + fireEvent, + render, + screen, + waitFor +} from '@testing-library/react' +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi +} from 'vitest' import { changeUiLocale } from './i18n' import { MarkdownRenderer } from './MarkdownRenderer' +const mermaidMock = vi.hoisted(() => ({ + initialize: vi.fn(), + render: vi.fn() +})) + +vi.mock('mermaid', () => ({ + default: mermaidMock +})) + describe('MarkdownRenderer', () => { - afterEach(cleanup) + beforeEach(() => { + mermaidMock.render.mockResolvedValue({ + svg: ` + + ` + }) + }) + + afterEach(() => { + cleanup() + delete document.documentElement.dataset.theme + vi.clearAllMocks() + }) it('renders CommonMark and GitHub Flavored Markdown', () => { render( @@ -165,6 +200,282 @@ $\\href{javascript:alert(1)}{unsafe}$`} ).not.toBeInTheDocument() }) + it('renders Mermaid fences with strict bounded configuration', async () => { + const { container } = render( + {`\`\`\`mermaid +flowchart LR + Start --> Finish +\`\`\``} + ) + + expect( + screen.getByText('正在绘制 Mermaid 图表…') + ).toHaveAttribute('role', 'status') + const diagram = await screen.findByRole('region', { + name: 'Mermaid 图表,可横向滚动' + }) + + expect(diagram).toHaveAttribute('tabindex', '0') + expect(container.querySelector('#safe-node')).toBeInTheDocument() + expect(mermaidMock.render).toHaveBeenCalledWith( + expect.stringMatching(/^goodbuddy-mermaid-\d+-0$/u), + 'flowchart LR\n Start --> Finish' + ) + expect(mermaidMock.initialize).toHaveBeenCalledWith( + expect.objectContaining({ + htmlLabels: false, + maxEdges: 300, + maxTextSize: 20_000, + securityLevel: 'strict', + startOnLoad: false, + suppressErrorRendering: true, + theme: 'default' + }) + ) + }) + + it('shows source and opens an accessible zoomable Mermaid viewer', async () => { + render( +
+ {`\`\`\`mermaid +flowchart LR + Start --> Finish +\`\`\``} +
+ ) + + await screen.findByRole('region', { + name: 'Mermaid 图表,可横向滚动' + }) + const sourceButton = screen.getByRole('button', { + name: '查看源码' + }) + expect(sourceButton).toHaveAttribute('aria-expanded', 'false') + fireEvent.click(sourceButton) + expect( + screen.getByRole('button', { name: '隐藏源码' }) + ).toHaveAttribute('aria-expanded', 'true') + expect(screen.getByText(/Start --> Finish/u)).toBeInTheDocument() + + const viewerButton = screen.getByRole('button', { + name: '打开大图' + }) + viewerButton.focus() + fireEvent.click(viewerButton) + const dialog = screen.getByRole('dialog', { + name: 'Mermaid 大图' + }) + expect(dialog).toHaveAttribute('aria-modal', 'true') + expect( + screen.getByRole('button', { name: '关闭 Mermaid 大图' }) + ).toHaveFocus() + expect( + document.querySelector('.app-shell')?.inert + ).toBe(true) + + fireEvent.click(screen.getByRole('button', { name: '放大图表' })) + expect(screen.getByLabelText('当前缩放比例')).toHaveTextContent( + '125%' + ) + const canvas = screen.getByRole('region', { + name: '可缩放、可拖动的 Mermaid 图表' + }) + expect(canvas).toHaveAttribute('tabindex', '0') + canvas.focus() + expect(canvas).toHaveFocus() + fireEvent.keyDown(dialog, { key: 'Tab' }) + expect( + screen.getByRole('button', { name: '缩小图表' }) + ).toHaveFocus() + fireEvent.wheel(canvas, { deltaY: -100 }) + fireEvent.wheel(canvas, { deltaY: -100 }) + fireEvent.wheel(canvas, { deltaY: -100 }) + await waitFor(() => + expect(screen.getByLabelText('当前缩放比例')).toHaveTextContent( + '200%' + ) + ) + fireEvent.click(screen.getByRole('button', { name: '重置缩放' })) + expect(screen.getByLabelText('当前缩放比例')).toHaveTextContent( + '100%' + ) + + fireEvent.keyDown(dialog, { key: 'Escape' }) + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + await waitFor(() => expect(viewerButton).toHaveFocus()) + expect( + document.querySelector('.app-shell')?.inert + ).toBe(false) + }) + + it('closes Mermaid controls when streamed source changes', async () => { + const { rerender } = render( + {`\`\`\`mermaid +flowchart LR + A --> B +\`\`\``} + ) + await screen.findByRole('region', { + name: 'Mermaid 图表,可横向滚动' + }) + fireEvent.click(screen.getByRole('button', { name: '查看源码' })) + fireEvent.click(screen.getByRole('button', { name: '打开大图' })) + expect(screen.getByRole('dialog')).toBeInTheDocument() + + rerender( + {`\`\`\`mermaid +flowchart LR + A --> B --> C +\`\`\``} + ) + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + await screen.findByRole('region', { + name: 'Mermaid 图表,可横向滚动' + }) + expect( + screen.getByRole('button', { name: '查看源码' }) + ).toHaveAttribute('aria-expanded', 'false') + expect(screen.queryByText(/A --> B --> C/u)).not.toBeInTheDocument() + }) + + it('pans the large Mermaid diagram with pointer dragging', async () => { + render( + {`\`\`\`mermaid +flowchart LR + A --> B +\`\`\``} + ) + await screen.findByRole('region', { + name: 'Mermaid 图表,可横向滚动' + }) + fireEvent.click(screen.getByRole('button', { name: '打开大图' })) + const canvas = screen.getByRole('region', { + name: '可缩放、可拖动的 Mermaid 图表' + }) + const setPointerCapture = vi.fn() + const releasePointerCapture = vi.fn() + Object.defineProperties(canvas, { + setPointerCapture: { value: setPointerCapture }, + hasPointerCapture: { value: () => true }, + releasePointerCapture: { value: releasePointerCapture } + }) + canvas.scrollLeft = 30 + canvas.scrollTop = 40 + + fireEvent.pointerDown(canvas, { + button: 0, + clientX: 100, + clientY: 100, + pointerId: 9 + }) + fireEvent.pointerMove(canvas, { + clientX: 70, + clientY: 60, + pointerId: 9 + }) + + expect(setPointerCapture).toHaveBeenCalledWith(9) + expect(canvas).toHaveClass('mermaid-viewer__canvas--dragging') + expect(canvas.scrollLeft).toBe(60) + expect(canvas.scrollTop).toBe(80) + + fireEvent.pointerUp(canvas, { pointerId: 9 }) + expect(releasePointerCapture).toHaveBeenCalledWith(9) + expect(canvas).not.toHaveClass( + 'mermaid-viewer__canvas--dragging' + ) + }) + + it('sanitizes Mermaid SVG without enabling diagram interactions', async () => { + mermaidMock.render.mockResolvedValueOnce({ + svg: ` + + + + + + + ` + }) + + const { container } = render( + {`\`\`\`mermaid +flowchart LR + A --> B +\`\`\``} + ) + + await screen.findByRole('region', { + name: 'Mermaid 图表,可横向滚动' + }) + expect(container.querySelector('script')).not.toBeInTheDocument() + expect(container.querySelector('a')).not.toBeInTheDocument() + expect(container.querySelector('style')).not.toBeInTheDocument() + expect(container.querySelector('#unsafe-node')).not.toHaveAttribute( + 'onclick' + ) + expect(container.querySelector('#unsafe-node')).not.toHaveAttribute( + 'style' + ) + expect(container.querySelector('#safe-node')).toHaveAttribute( + 'fill', + 'url(#gradient)' + ) + }) + + it('rerenders Mermaid diagrams when the application theme changes', async () => { + render( + {`\`\`\`mermaid +sequenceDiagram + Alice->>Bob: Hello +\`\`\``} + ) + + await waitFor(() => + expect(mermaidMock.render).toHaveBeenCalledTimes(1) + ) + act(() => { + document.documentElement.dataset.theme = 'dark' + }) + await waitFor(() => + expect(mermaidMock.render).toHaveBeenCalledTimes(2) + ) + expect(mermaidMock.initialize).toHaveBeenLastCalledWith( + expect.objectContaining({ + darkMode: true, + theme: 'dark' + }) + ) + }) + + it('shows Mermaid source when rendering fails or input is oversized', async () => { + mermaidMock.render.mockRejectedValueOnce( + new Error('Invalid Mermaid syntax') + ) + const { rerender } = render( + {`\`\`\`mermaid +not a diagram +\`\`\``} + ) + + expect( + await screen.findByRole('alert') + ).toHaveTextContent('无法绘制 Mermaid 图表') + expect(screen.getByText('not a diagram')).toBeInTheDocument() + + rerender( + {`\`\`\`mermaid +${'A'.repeat(20_001)} +\`\`\``} + ) + expect( + await screen.findByRole('alert') + ).toHaveTextContent('无法绘制 Mermaid 图表') + expect(mermaidMock.render).toHaveBeenCalledTimes(1) + }) + it('updates table accessibility copy when the locale changes', async () => { render( {`| Name | diff --git a/src/renderer/src/MarkdownRenderer.tsx b/src/renderer/src/MarkdownRenderer.tsx index 4ffe144..9dbd2a7 100644 --- a/src/renderer/src/MarkdownRenderer.tsx +++ b/src/renderer/src/MarkdownRenderer.tsx @@ -1,4 +1,11 @@ -import { memo, useMemo } from 'react' +import { + Children, + isValidElement, + lazy, + memo, + Suspense, + useMemo +} from 'react' import rehypeKatex from 'rehype-katex' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' @@ -6,6 +13,12 @@ import remarkMath from 'remark-math' import type { Components } from 'react-markdown' import { useTranslation } from 'react-i18next' +const MermaidDiagram = lazy(() => + import('./MermaidDiagram').then((module) => ({ + default: module.MermaidDiagram + })) +) + const linkComponent: Components['a'] = ({ children, node, @@ -19,9 +32,46 @@ const linkComponent: Components['a'] = ({ ) } -function markdownComponents(tableAriaLabel: string): Components { +function markdownComponents( + tableAriaLabel: string, + mermaidLoadingLabel: string +): Components { return { a: linkComponent, + pre: ({ children, node, ...properties }) => { + void node + const child = Children.count(children) === 1 + ? Children.only(children) + : undefined + if ( + isValidElement<{ + children?: React.ReactNode + className?: string + }>(child) && + /(?:^|\s)language-mermaid(?:\s|$)/iu.test( + child.props.className ?? '' + ) + ) { + const source = String(child.props.children ?? '').replace( + /\n$/u, + '' + ) + return ( + +
+ {mermaidLoadingLabel} +
+ + } + > + +
+ ) + } + return
{children}
+ }, table: ({ children, node, ...properties }) => { void node return ( @@ -149,7 +199,11 @@ export const MarkdownRenderer = memo(function MarkdownRenderer({ }: MarkdownRendererProps): React.JSX.Element { const { t } = useTranslation('app') const components = useMemo( - () => markdownComponents(t('markdown.scrollableTable')), + () => + markdownComponents( + t('markdown.scrollableTable'), + t('markdown.mermaidLoading') + ), [t] ) diff --git a/src/renderer/src/MermaidDiagram.tsx b/src/renderer/src/MermaidDiagram.tsx new file mode 100644 index 0000000..64485c3 --- /dev/null +++ b/src/renderer/src/MermaidDiagram.tsx @@ -0,0 +1,617 @@ +import DOMPurify from 'dompurify' +import { + Code2, + Maximize2, + RotateCcw, + X, + ZoomIn, + ZoomOut +} from 'lucide-react' +import { + useEffect, + useId, + useMemo, + useRef, + useState +} from 'react' +import { createPortal } from 'react-dom' +import { useTranslation } from 'react-i18next' +import type { Mermaid, MermaidConfig } from 'mermaid' +import { activateModalFocus, trapTabFocus } from './dialog-focus' +import { useDocumentTheme } from './use-document-theme' +import './markdown-mermaid.css' + +const MAX_MERMAID_TEXT_SIZE = 20_000 +const MAX_MERMAID_EDGES = 300 +const MAX_MERMAID_DIMENSION = 4_096 +const MIN_VIEWER_ZOOM = 0.5 +const MAX_VIEWER_ZOOM = 3 +const VIEWER_ZOOM_STEP = 0.25 +const SVG_NAMESPACE = 'http://www.w3.org/2000/svg' +const unsafeCssPattern = + /(?:@import|\bexpression\s*\(|(?:https?|ftp|file|data|javascript|vbscript):|\/\/|-moz-binding|\bbehavior\s*:)/iu +const cssUrlPattern = /url\s*\(\s*(['"]?)(.*?)\1\s*\)/giu + +const forbiddenTags = [ + 'a', + 'animate', + 'animatemotion', + 'animatetransform', + 'discard', + 'foreignobject', + 'iframe', + 'image', + 'mpath', + 'set', + 'script' +] +const forbiddenAttributes = ['href', 'xlink:href'] +const sharedSanitizerConfig = { + ALLOW_DATA_ATTR: false, + ALLOW_UNKNOWN_PROTOCOLS: false, + FORBID_ATTR: forbiddenAttributes, + FORBID_TAGS: forbiddenTags +} as const +const secureConfigKeys = [ + 'secure', + 'securityLevel', + 'startOnLoad', + 'maxTextSize', + 'suppressErrorRendering', + 'maxEdges', + 'htmlLabels', + 'dompurifyConfig', + 'theme', + 'themeCSS', + 'themeVariables', + 'fontFamily', + 'altFontFamily', + 'flowchart' +] + +let mermaidModulePromise: Promise | undefined +let mermaidRenderQueue = Promise.resolve() +let mermaidDiagramSequence = 0 + +type MermaidRenderState = + | { + source: string + status: 'ready' + svg: string + theme: 'light' | 'dark' + } + | { + source: string + status: 'error' + theme: 'light' | 'dark' + } + +function loadMermaid(): Promise { + mermaidModulePromise ??= import('mermaid').then( + (module) => module.default + ) + return mermaidModulePromise +} + +function containsUnsafeCss(value: string): boolean { + const normalizedValue = value + .replace(/\/\*[\s\S]*?\*\//gu, '') + .replace( + /\\([0-9a-f]{1,6}\s?|.)/giu, + (_match, escaped: string) => { + const hex = escaped.trim() + return /^[0-9a-f]{1,6}$/iu.test(hex) + ? String.fromCodePoint(Number.parseInt(hex, 16)) + : escaped + } + ) + if (unsafeCssPattern.test(normalizedValue)) { + return true + } + cssUrlPattern.lastIndex = 0 + for ( + let match = cssUrlPattern.exec(normalizedValue); + match; + match = cssUrlPattern.exec(normalizedValue) + ) { + if (!match[2]?.trim().startsWith('#')) { + return true + } + } + return false +} + +function sanitizeMermaidSvg(svg: string): string { + const sanitized = DOMPurify.sanitize(svg, { + ...sharedSanitizerConfig, + USE_PROFILES: { + svg: true, + svgFilters: true + } + }) + const documentNode = new DOMParser().parseFromString( + sanitized, + 'image/svg+xml' + ) + const root = documentNode.documentElement + if ( + root.localName !== 'svg' || + root.namespaceURI !== SVG_NAMESPACE || + documentNode.querySelector('parsererror') + ) { + throw new Error('Mermaid returned invalid SVG') + } + + for (const element of [root, ...root.querySelectorAll('*')]) { + if ( + element.localName === 'style' && + containsUnsafeCss(element.textContent ?? '') + ) { + element.remove() + continue + } + for (const attribute of [...element.attributes]) { + const attributeName = attribute.name.toLowerCase() + if ( + attributeName.startsWith('on') || + forbiddenAttributes.includes(attributeName) || + containsUnsafeCss(attribute.value) + ) { + element.removeAttribute(attribute.name) + } + } + } + + const viewBox = root + .getAttribute('viewBox') + ?.trim() + .split(/[\s,]+/u) + .map(Number) + if ( + viewBox?.length === 4 && + viewBox.every(Number.isFinite) && + viewBox[2]! > 0 && + viewBox[3]! > 0 + ) { + const scale = Math.min( + 1, + MAX_MERMAID_DIMENSION / viewBox[2]!, + MAX_MERMAID_DIMENSION / viewBox[3]! + ) + root.setAttribute('width', String(Math.ceil(viewBox[2]! * scale))) + root.setAttribute('height', String(Math.ceil(viewBox[3]! * scale))) + root.style.removeProperty('max-width') + } + + return new XMLSerializer().serializeToString(root) +} + +function mermaidConfig(darkTheme: boolean): MermaidConfig { + return { + darkMode: darkTheme, + deterministicIds: true, + dompurifyConfig: sharedSanitizerConfig, + fontFamily: + '"Inter Variable", "Noto Sans SC Variable", "Segoe UI Variable", sans-serif', + htmlLabels: false, + logLevel: 'fatal', + maxEdges: MAX_MERMAID_EDGES, + maxTextSize: MAX_MERMAID_TEXT_SIZE, + secure: secureConfigKeys, + securityLevel: 'strict', + startOnLoad: false, + suppressErrorRendering: true, + theme: darkTheme ? 'dark' : 'default' + } +} + +function renderMermaid( + source: string, + id: string, + darkTheme: boolean, + shouldRender: () => boolean +): Promise { + if (source.length > MAX_MERMAID_TEXT_SIZE) { + return Promise.reject(new Error('Mermaid source is too large')) + } + + const render = mermaidRenderQueue.then(async () => { + if (!shouldRender()) { + throw new Error('Mermaid render was superseded') + } + const mermaid = await loadMermaid() + if (!shouldRender()) { + throw new Error('Mermaid render was superseded') + } + mermaid.initialize(mermaidConfig(darkTheme)) + const result = await mermaid.render(id, source) + return sanitizeMermaidSvg(result.svg) + }) + mermaidRenderQueue = render.then( + () => undefined, + () => undefined + ) + return render +} + +type MermaidDiagramProps = { + source: string +} + +type MermaidViewerProps = { + onClose: () => void + svg: string +} + +function clampViewerZoom(zoom: number): number { + return Math.min(MAX_VIEWER_ZOOM, Math.max(MIN_VIEWER_ZOOM, zoom)) +} + +function MermaidViewer({ + onClose, + svg +}: MermaidViewerProps): React.JSX.Element { + const { i18n, t } = useTranslation('app') + const titleId = useId() + const hintId = useId() + const dialogRef = useRef(null) + const closeRef = useRef(null) + const dragRef = useRef<{ + pointerId: number + startX: number + startY: number + scrollLeft: number + scrollTop: number + } | undefined>(undefined) + const [dragging, setDragging] = useState(false) + const [zoom, setZoom] = useState(1) + const pendingWheelZoomRef = useRef(0) + const zoomFrameRef = useRef(undefined) + const percentFormatter = useMemo( + () => + new Intl.NumberFormat(i18n.resolvedLanguage, { + maximumFractionDigits: 0, + style: 'percent' + }), + [i18n.resolvedLanguage] + ) + + useEffect(() => { + const deactivateModalFocus = activateModalFocus( + () => closeRef.current + ) + return () => { + if (zoomFrameRef.current !== undefined) { + cancelAnimationFrame(zoomFrameRef.current) + } + deactivateModalFocus() + } + }, []) + + const adjustZoom = (delta: number): void => { + setZoom((current) => clampViewerZoom(current + delta)) + } + + const queueWheelZoom = (delta: number): void => { + pendingWheelZoomRef.current += delta + if (zoomFrameRef.current !== undefined) { + return + } + zoomFrameRef.current = requestAnimationFrame(() => { + const pendingZoom = pendingWheelZoomRef.current + pendingWheelZoomRef.current = 0 + zoomFrameRef.current = undefined + adjustZoom(pendingZoom) + }) + } + + const finishDragging = ( + event: React.PointerEvent + ): void => { + if (dragRef.current?.pointerId !== event.pointerId) { + return + } + if (event.currentTarget.hasPointerCapture(event.pointerId)) { + event.currentTarget.releasePointerCapture(event.pointerId) + } + dragRef.current = undefined + setDragging(false) + } + + return createPortal( +
{ + if (event.target === event.currentTarget) { + onClose() + } + }} + > +
{ + if (event.key === 'Escape') { + event.preventDefault() + onClose() + return + } + trapTabFocus(event, dialogRef.current) + }} + ref={dialogRef} + role="dialog" + > +
+
+ {t('markdown.mermaidViewerTitle')} + + {t('markdown.mermaidViewerHint')} + +
+
+ + + {percentFormatter.format(zoom)} + + + + +
+
+
{ + if (event.button !== 0) { + return + } + event.preventDefault() + dragRef.current = { + pointerId: event.pointerId, + startX: event.clientX, + startY: event.clientY, + scrollLeft: event.currentTarget.scrollLeft, + scrollTop: event.currentTarget.scrollTop + } + event.currentTarget.setPointerCapture(event.pointerId) + setDragging(true) + }} + onPointerMove={(event) => { + const drag = dragRef.current + if (drag?.pointerId !== event.pointerId) { + return + } + event.preventDefault() + event.currentTarget.scrollLeft = + drag.scrollLeft - (event.clientX - drag.startX) + event.currentTarget.scrollTop = + drag.scrollTop - (event.clientY - drag.startY) + }} + onPointerUp={finishDragging} + onWheel={(event) => { + if (event.deltaY === 0) { + return + } + event.preventDefault() + queueWheelZoom( + event.deltaY < 0 + ? VIEWER_ZOOM_STEP + : -VIEWER_ZOOM_STEP + ) + }} + role="region" + tabIndex={0} + > +
+
+
+
, + document.body + ) +} + +export function MermaidDiagram({ + source +}: MermaidDiagramProps): React.JSX.Element { + const { t } = useTranslation('app') + const idRef = useRef( + `goodbuddy-mermaid-${++mermaidDiagramSequence}` + ) + const renderRevisionRef = useRef(0) + const sourceId = useId() + const documentTheme = useDocumentTheme() + const [renderState, setRenderState] = + useState() + const [visibleSource, setVisibleSource] = useState() + const [viewerSource, setViewerSource] = useState() + const sourceVisible = visibleSource === source + const viewerOpen = viewerSource === source + + useEffect(() => { + let active = true + const renderRevision = renderRevisionRef.current++ + const timeout = window.setTimeout(() => { + void renderMermaid( + source.trim(), + `${idRef.current}-${renderRevision}`, + documentTheme === 'dark', + () => active + ).then( + (svg) => { + if (active) { + setRenderState({ + source, + status: 'ready', + svg, + theme: documentTheme + }) + } + }, + () => { + if (active) { + setRenderState({ + source, + status: 'error', + theme: documentTheme + }) + } + } + ) + }, 120) + + return () => { + active = false + window.clearTimeout(timeout) + } + }, [documentTheme, source]) + + const currentRenderState = + renderState?.source === source && + renderState.theme === documentTheme + ? renderState + : undefined + const retainedSvg = + renderState?.status === 'ready' && + renderState.source === source + ? renderState.svg + : undefined + const svg = + currentRenderState?.status === 'ready' + ? currentRenderState.svg + : retainedSvg + + if (currentRenderState?.status === 'error') { + return ( +
+
+ {t('markdown.mermaidError')} +
+
+          {source}
+        
+
+ ) + } + + return ( +
+ {svg ? ( + <> +
+ + +
+ {!viewerOpen && ( +
+ )} + {sourceVisible && ( +
+              {source}
+            
+ )} + {viewerOpen && ( + setViewerSource(undefined)} + svg={svg} + /> + )} + + ) : ( +
+ {t('markdown.mermaidLoading')} +
+ )} +
+ ) +} diff --git a/src/renderer/src/dialog-focus.ts b/src/renderer/src/dialog-focus.ts index 53a8aca..d78a4a6 100644 --- a/src/renderer/src/dialog-focus.ts +++ b/src/renderer/src/dialog-focus.ts @@ -5,7 +5,7 @@ type TabKeyEvent = { } const focusableSelector = - 'button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled])' + 'button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])' export function trapTabFocus( event: TabKeyEvent, diff --git a/src/renderer/src/i18n/locales/en-US/app.ts b/src/renderer/src/i18n/locales/en-US/app.ts index e878b6e..d948830 100644 --- a/src/renderer/src/i18n/locales/en-US/app.ts +++ b/src/renderer/src/i18n/locales/en-US/app.ts @@ -473,6 +473,23 @@ export const app = { evidenceExcerpt: '{{source}}: {{excerpt}}' }, markdown: { - scrollableTable: 'Table, horizontally scrollable' + scrollableTable: 'Table, horizontally scrollable', + mermaidDiagram: 'Mermaid diagram, horizontally scrollable', + mermaidLoading: 'Rendering Mermaid diagram…', + mermaidError: + 'The Mermaid diagram could not be rendered. Its source is shown below.', + mermaidActions: 'Mermaid diagram actions', + mermaidViewSource: 'View source', + mermaidHideSource: 'Hide source', + mermaidOpenViewer: 'Open large diagram', + mermaidViewerTitle: 'Large Mermaid diagram', + mermaidViewerHint: + 'Use the mouse wheel or buttons to zoom, and drag the canvas to pan.', + mermaidViewerCanvas: 'Zoomable, pannable Mermaid diagram', + mermaidZoomOut: 'Zoom out diagram', + mermaidZoomIn: 'Zoom in diagram', + mermaidResetZoom: 'Reset zoom', + mermaidZoomLevel: 'Current zoom level', + mermaidCloseViewer: 'Close large Mermaid diagram' } } satisfies TranslationShape diff --git a/src/renderer/src/i18n/locales/zh-CN/app.ts b/src/renderer/src/i18n/locales/zh-CN/app.ts index ae81c24..b48f671 100644 --- a/src/renderer/src/i18n/locales/zh-CN/app.ts +++ b/src/renderer/src/i18n/locales/zh-CN/app.ts @@ -439,6 +439,21 @@ export const app = { evidenceExcerpt: '{{source}}:{{excerpt}}' }, markdown: { - scrollableTable: '表格,可横向滚动' + scrollableTable: '表格,可横向滚动', + mermaidDiagram: 'Mermaid 图表,可横向滚动', + mermaidLoading: '正在绘制 Mermaid 图表…', + mermaidError: '无法绘制 Mermaid 图表,已保留原始图表代码。', + mermaidActions: 'Mermaid 图表操作', + mermaidViewSource: '查看源码', + mermaidHideSource: '隐藏源码', + mermaidOpenViewer: '打开大图', + mermaidViewerTitle: 'Mermaid 大图', + mermaidViewerHint: '使用滚轮或按钮缩放,拖动画布浏览。', + mermaidViewerCanvas: '可缩放、可拖动的 Mermaid 图表', + mermaidZoomOut: '缩小图表', + mermaidZoomIn: '放大图表', + mermaidResetZoom: '重置缩放', + mermaidZoomLevel: '当前缩放比例', + mermaidCloseViewer: '关闭 Mermaid 大图' } } as const diff --git a/src/renderer/src/markdown-mermaid.css b/src/renderer/src/markdown-mermaid.css new file mode 100644 index 0000000..8439491 --- /dev/null +++ b/src/renderer/src/markdown-mermaid.css @@ -0,0 +1,205 @@ +.mermaid-diagram { + position: relative; + display: grid; + min-width: 0; + margin: var(--space-3) 0; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-control); + background: var(--surface-raised); +} + +.mermaid-diagram__actions { + position: absolute; + z-index: 1; + top: var(--space-2); + right: var(--space-2); + display: flex; + padding: 2px; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-control); + background: var(--surface-raised); + box-shadow: var(--shadow-card); + gap: 2px; +} + +.mermaid-diagram__actions .icon-button { + width: 30px; + height: 30px; +} + +.mermaid-diagram__viewport { + min-width: 0; + padding: var(--space-4); + overflow-x: auto; + overscroll-behavior-inline: contain; +} + +.mermaid-diagram__viewport:focus-visible { + outline: 2px solid var(--accent); + outline-offset: -2px; +} + +.mermaid-diagram__viewport > svg { + display: block; + min-width: min(100%, 320px); + max-width: none !important; + height: auto; + min-height: 80px; + margin-inline: auto; + font-family: var(--font-family-ui); +} + +.markdown-content .mermaid-diagram__source { + max-height: 320px; + margin: 0; + border: 0; + border-top: 1px solid var(--border-subtle); + border-radius: 0 0 var(--radius-control) var(--radius-control); + overflow: auto; +} + +.mermaid-diagram__loading, +.mermaid-diagram--error > figcaption { + padding: var(--space-3) var(--space-4); + color: var(--text-muted); + font-size: var(--font-caption); + line-height: 1.5; +} + +.mermaid-diagram--error { + border-color: var(--danger-border); + background: var(--danger-subtle); +} + +.mermaid-diagram--error > figcaption { + color: var(--danger-strong); +} + +.markdown-content .mermaid-diagram--error > pre { + margin: 0; + border: 0; + border-top: 1px solid var(--danger-border); + border-radius: 0 0 var(--radius-control) var(--radius-control); +} + +.mermaid-viewer-backdrop { + position: fixed; + z-index: 120; + display: grid; + padding: var(--space-4); + background: var(--overlay-backdrop); + inset: 38px 0 0; + place-items: center; +} + +.mermaid-viewer { + display: grid; + width: min(1280px, 100%); + height: min(860px, calc(100vh - 70px)); + min-height: 0; + border: 1px solid var(--border-default); + border-radius: var(--radius-card); + background: var(--surface-raised); + box-shadow: var(--shadow-dialog); + grid-template-rows: auto minmax(0, 1fr); +} + +.mermaid-viewer__header { + display: flex; + min-width: 0; + align-items: center; + padding: var(--space-3) var(--space-4); + border-bottom: 1px solid var(--border-subtle); + justify-content: space-between; + gap: var(--space-3); +} + +.mermaid-viewer__header > div:first-child { + display: grid; + min-width: 0; + gap: 2px; +} + +.mermaid-viewer__header strong { + color: var(--text-primary); + font-size: var(--font-section-title); +} + +.mermaid-viewer__header small { + color: var(--text-muted); + font-size: var(--font-caption); + line-height: 1.4; +} + +.mermaid-viewer__controls { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: var(--space-1); +} + +.mermaid-viewer__zoom { + min-width: 48px; + color: var(--text-secondary); + font-size: var(--font-caption); + font-variant-numeric: tabular-nums; + text-align: center; +} + +.mermaid-viewer__canvas { + display: grid; + min-width: 0; + min-height: 0; + padding: var(--space-6); + overflow: auto; + background: var(--surface-subtle); + cursor: grab; + overscroll-behavior: contain; + place-items: center; + touch-action: none; +} + +.mermaid-viewer__canvas--dragging { + cursor: grabbing; + user-select: none; +} + +.mermaid-viewer__canvas:focus-visible { + outline: 2px solid var(--accent); + outline-offset: -2px; +} + +.mermaid-viewer__diagram { + display: grid; + min-width: max-content; + min-height: max-content; + transform-origin: center; + place-items: center; +} + +.mermaid-viewer__diagram > svg { + display: block; + max-width: none !important; + height: auto; + font-family: var(--font-family-ui); +} + +@media (max-width: 720px) { + .mermaid-viewer-backdrop { + padding: var(--space-2); + } + + .mermaid-viewer { + height: calc(100vh - 54px); + } + + .mermaid-viewer__header { + align-items: flex-start; + flex-direction: column; + } + + .mermaid-viewer__controls { + width: 100%; + justify-content: flex-end; + } +} diff --git a/src/renderer/src/use-document-theme.ts b/src/renderer/src/use-document-theme.ts new file mode 100644 index 0000000..63f057c --- /dev/null +++ b/src/renderer/src/use-document-theme.ts @@ -0,0 +1,46 @@ +import { useSyncExternalStore } from 'react' +import type { ResolvedAppearanceTheme } from './theme' + +const listeners = new Set<() => void>() +let observer: MutationObserver | undefined + +function resolvedDocumentTheme(): ResolvedAppearanceTheme { + return document.documentElement.dataset.theme === 'dark' + ? 'dark' + : 'light' +} + +function notifyThemeListeners(): void { + for (const listener of listeners) { + listener() + } +} + +function subscribeToDocumentTheme(listener: () => void): () => void { + listeners.add(listener) + if ( + listeners.size === 1 && + typeof MutationObserver === 'function' + ) { + observer = new MutationObserver(notifyThemeListeners) + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-theme'] + }) + } + return () => { + listeners.delete(listener) + if (listeners.size === 0) { + observer?.disconnect() + observer = undefined + } + } +} + +export function useDocumentTheme(): ResolvedAppearanceTheme { + return useSyncExternalStore( + subscribeToDocumentTheme, + resolvedDocumentTheme, + resolvedDocumentTheme + ) +}