refactor: replace react-syntax-highlighter with react-shiki

Consolidate markdown rendering logic into dedicated assistant-ui components and update syntax highlighting to use Shiki with GitHub themes. Removes dependency on react-markdown and react-syntax-highlighter in favor of @assistant-ui/react-markdown with Shiki-based highlighting.
This commit is contained in:
weishu
2025-12-18 12:08:59 +08:00
parent 35d959dba4
commit fd55750a12
8 changed files with 413 additions and 142 deletions
+31 -25
View File
@@ -77,8 +77,7 @@
"diff": "^7.0.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^16.1.0",
"react-shiki": "^0.9.1",
"react-textarea-autosize": "^8.5.9",
"remark-gfm": "^4.0.1",
"socket.io-client": "^4.8.1",
@@ -87,7 +86,6 @@
"devDependencies": {
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
@@ -444,6 +442,20 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.4", "", { "os": "win32", "cpu": "x64" }, "sha512-Sbx45u/Lbb5RyptSbX7/3deP+/lzEmZ0BTSHxwxN/IMOZDZf8S0AGo0hJD5n/LQssxb5Z3B4og4P2X6Dd8acCA=="],
"@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-OFx8fHAZuk7I42Z9YAdZ95To6jDePQ9Rnfbw9uSRTSbBhYBp1kEOKv/3jOimcj3VRUKusDYM6DswLauwfhboLg=="],
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-Yx3gy7xLzM0ZOjqoxciHjA7dAt5tyzJE3L4uQoM83agahy+PlW244XJSrmJRSBvGYELDhYXPacD4R/cauV5bzQ=="],
"@shikijs/langs": ["@shikijs/langs@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0" } }, "sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA=="],
"@shikijs/themes": ["@shikijs/themes@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0" } }, "sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ=="],
"@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@socket.io/bun-engine": ["@socket.io/bun-engine@0.1.0", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-B1z6GuAxZlfvjgaa3BHZBOfqHJNfnpebTw15p+Un1HuBL4YM7wUxO9sJa7K4NDTe7XbUBeqLIwTDA5tOOjffog=="],
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
@@ -494,16 +506,12 @@
"@types/parse-path": ["@types/parse-path@7.1.0", "", { "dependencies": { "parse-path": "*" } }, "sha512-EULJ8LApcVEPbrfND0cRQqutIOdiIgJ1Mgrhpy755r14xMohPTEpkV/k28SJvuOs9bHRFW8x+KeDAEPiGQPB9Q=="],
"@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="],
"@types/ps-list": ["@types/ps-list@6.2.1", "", { "dependencies": { "ps-list": "*" } }, "sha512-YGrVUmLaAu89QffaehCcCp5kphlbCf72+vDxnRyP1soC9y7xaatnoB0Fgo/tEGHk8teglcLwY6b4iHcyd/UWFA=="],
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/react-syntax-highlighter": ["@types/react-syntax-highlighter@15.5.13", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="],
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
"@types/tmp": ["@types/tmp@0.2.6", "", {}, "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA=="],
@@ -868,8 +876,6 @@
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"fault": ["fault@1.0.4", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
@@ -890,8 +896,6 @@
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
@@ -950,22 +954,18 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
"highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="],
"highlightjs-vue": ["highlightjs-vue@1.0.0", "", {}, "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA=="],
"hono": ["hono@4.11.1", "", {}, "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg=="],
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
@@ -1102,8 +1102,6 @@
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
"lowlight": ["lowlight@1.20.0", "", { "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" } }, "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw=="],
"lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
"macos-release": ["macos-release@3.4.0", "", {}, "sha512-wpGPwyg/xrSp4H4Db4xYSeAr6+cFQGHfspHzDUdYxswDnUW0L5Ov63UuJiSr8NMSpyaChO4u1n0MXUvVPtrN6A=="],
@@ -1272,6 +1270,10 @@
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
"oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="],
"open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
@@ -1352,8 +1354,6 @@
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
@@ -1396,9 +1396,9 @@
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
"react-shiki": ["react-shiki@0.9.1", "", { "dependencies": { "clsx": "^2.1.1", "dequal": "^2.0.3", "hast-util-to-jsx-runtime": "^2.3.6", "shiki": "^3.11.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@types/react": ">=16.8.0", "@types/react-dom": ">=16.8.0", "react": ">= 16.8.0", "react-dom": ">= 16.8.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ln1PnISi7WaSlheSBRdxVruVbU1zMUkCmxe+vmbIvZSsHdfvOF5NBOgf1h4cCr6OjdR0dLAxmPKcx3tobdyxVA=="],
"react-syntax-highlighter": ["react-syntax-highlighter@16.1.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", "refractor": "^5.0.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg=="],
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
"react-textarea-autosize": ["react-textarea-autosize@8.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A=="],
@@ -1410,7 +1410,11 @@
"rechoir": ["rechoir@0.6.2", "", { "dependencies": { "resolve": "^1.1.6" } }, "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw=="],
"refractor": ["refractor@5.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/prismjs": "^1.0.0", "hastscript": "^9.0.0", "parse-entities": "^4.0.0" } }, "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw=="],
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
"release-it": ["release-it@19.1.0", "", { "dependencies": { "@nodeutils/defaults-deep": "1.1.0", "@octokit/rest": "22.0.0", "@phun-ky/typeof": "2.0.3", "async-retry": "1.3.3", "c12": "3.3.1", "ci-info": "^4.3.0", "eta": "4.0.1", "git-url-parse": "16.1.0", "inquirer": "12.9.6", "issue-parser": "7.0.1", "lodash.merge": "4.6.2", "mime-types": "3.0.1", "new-github-release-url": "2.0.0", "open": "10.2.0", "ora": "9.0.0", "os-name": "6.1.0", "proxy-agent": "6.5.0", "semver": "7.7.2", "tinyglobby": "0.2.15", "undici": "6.21.3", "url-join": "5.0.0", "wildcard-match": "5.1.4", "yargs-parser": "21.1.1" }, "bin": { "release-it": "bin/release-it.js" } }, "sha512-XgAIZVKgHgOBUUoLFrUfXHG4cUDiXPWFW0AHIAgk17Tk9JbIjz9YyAU8MyQer7dWkfG5IpNU9QH8tViQZGfhaw=="],
@@ -1480,6 +1484,8 @@
"shelljs": ["shelljs@0.8.5", "", { "dependencies": { "glob": "^7.0.0", "interpret": "^1.0.0", "rechoir": "^0.6.2" }, "bin": { "shjs": "bin/shjs" } }, "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow=="],
"shiki": ["shiki@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/engine-javascript": "3.20.0", "@shikijs/engine-oniguruma": "3.20.0", "@shikijs/langs": "3.20.0", "@shikijs/themes": "3.20.0", "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg=="],
"shx": ["shx@0.3.4", "", { "dependencies": { "minimist": "^1.2.3", "shelljs": "^0.8.5" }, "bin": { "shx": "lib/cli.js" } }, "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
+1 -3
View File
@@ -19,8 +19,7 @@
"diff": "^7.0.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^16.1.0",
"react-shiki": "^0.9.1",
"react-textarea-autosize": "^8.5.9",
"remark-gfm": "^4.0.1",
"socket.io-client": "^4.8.1",
@@ -29,7 +28,6 @@
"devDependencies": {
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
@@ -1,35 +1,7 @@
import type { SyntaxHighlighterProps } from '@assistant-ui/react-markdown'
import { MarkdownTextPrimitive } from '@assistant-ui/react-markdown'
import { MessagePrimitive, useAssistantState } from '@assistant-ui/react'
import remarkGfm from 'remark-gfm'
import { CodeBlock } from '@/components/CodeBlock'
import { MarkdownText } from '@/components/assistant-ui/markdown-text'
import { HappyToolMessage } from '@/components/AssistantChat/messages/ToolMessage'
function MarkdownSyntaxHighlighter(props: SyntaxHighlighterProps) {
return (
<CodeBlock
code={props.code}
language={props.language}
showCopyButton={false}
/>
)
}
const MARKDOWN_PLUGINS = [remarkGfm]
const MARKDOWN_COMPONENTS = {
SyntaxHighlighter: MarkdownSyntaxHighlighter
} as const
function MarkdownText() {
return (
<MarkdownTextPrimitive
className="markdown-content text-sm"
remarkPlugins={MARKDOWN_PLUGINS}
components={MARKDOWN_COMPONENTS}
/>
)
}
const TOOL_COMPONENTS = {
Fallback: HappyToolMessage
} as const
+27 -43
View File
@@ -1,41 +1,20 @@
import type { Themes } from 'react-shiki/web'
import { useMemo, useState } from 'react'
import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/prism-async-light'
import oneDark from 'react-syntax-highlighter/dist/esm/styles/prism/one-dark'
import oneLight from 'react-syntax-highlighter/dist/esm/styles/prism/one-light'
import { useShikiHighlighter } from 'react-shiki/web'
import { getTelegramWebApp } from '@/hooks/useTelegram'
import { useTheme } from '@/hooks/useTheme'
type SyntaxHighlighterWithSupportedLanguages = typeof SyntaxHighlighter & {
supportedLanguages?: string[]
}
const SUPPORTED_LANGUAGE_LOOKUP = new Map(
((SyntaxHighlighter as SyntaxHighlighterWithSupportedLanguages).supportedLanguages ?? []).map(
(lang: string): [string, string] => [canonicalizeLanguage(lang), lang]
)
)
function canonicalizeLanguage(value: string): string {
return value
.trim()
.replace(/^language-/, '')
.toLowerCase()
.replace(/#/g, 'sharp')
.replace(/\+/g, 'p')
.replace(/[^a-z0-9]/g, '')
const SHIKI_THEMES: Themes = {
light: 'github-light',
dark: 'github-dark',
}
function normalizeLanguage(language?: string): string {
const raw = language?.trim()
if (!raw) return 'text'
const canonical = canonicalizeLanguage(raw)
if (!canonical || canonical === 'text' || canonical === 'plaintext' || canonical === 'txt') return 'text'
const supported = SUPPORTED_LANGUAGE_LOOKUP.get(canonical)
if (supported) return supported
return raw.startsWith('language-') ? raw.slice('language-'.length) : raw
const cleaned = raw.startsWith('language-') ? raw.slice('language-'.length) : raw
const canonical = cleaned.toLowerCase()
if (canonical === 'text' || canonical === 'plaintext' || canonical === 'txt') return 'text'
return cleaned
}
function safeCopyToClipboard(text: string): Promise<void> {
@@ -91,10 +70,15 @@ export function CodeBlock(props: {
}) {
const showCopyButton = props.showCopyButton ?? true
const normalizedLanguage = useMemo(() => normalizeLanguage(props.language), [props.language])
const { isDark } = useTheme()
const [copied, setCopied] = useState(false)
const highlighted = useShikiHighlighter(props.code, normalizedLanguage, SHIKI_THEMES, {
delay: 75,
outputFormat: 'react',
structure: 'inline',
})
const handleCopy = async () => {
try {
await safeCopyToClipboard(props.code)
@@ -106,8 +90,6 @@ export function CodeBlock(props: {
}
}
const syntaxTheme = isDark ? oneDark : oneLight
return (
<div className="relative overflow-hidden rounded-md bg-[var(--app-code-bg)]">
{showCopyButton ? (
@@ -121,16 +103,18 @@ export function CodeBlock(props: {
</button>
) : null}
<div className="overflow-auto p-2 pr-8 text-xs">
<SyntaxHighlighter
language={normalizedLanguage}
style={syntaxTheme}
PreTag="div"
customStyle={{ margin: 0, padding: 0, background: 'transparent' }}
>
{props.code}
</SyntaxHighlighter>
</div>
<pre className="overflow-auto p-2 pr-8 text-xs">
{typeof highlighted === 'string' ? (
<code
className="shiki font-mono"
dangerouslySetInnerHTML={{ __html: highlighted }}
/>
) : (
<code className="shiki font-mono">
{highlighted ?? props.code}
</code>
)}
</pre>
</div>
)
}
+22 -42
View File
@@ -1,50 +1,30 @@
import ReactMarkdown, { Components } from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { CodeBlock } from '@/components/CodeBlock'
function getLanguageFromClassName(className?: string): string | null {
if (!className) return null
for (const token of className.split(/\s+/g)) {
if (token.startsWith('language-')) {
return token.slice('language-'.length) || null
}
}
return null
}
const defaultComponents: Components = {
code({ className, children, ...props }) {
const language = getLanguageFromClassName(className) || 'text'
const code = String(children).replace(/\n$/, '')
if (language !== 'text' || code.includes('\n')) {
return (
<CodeBlock
code={code}
language={language}
showCopyButton={false}
/>
)
}
return <code className={className} {...props}>{children}</code>
},
}
import type { MarkdownTextPrimitiveProps } from '@assistant-ui/react-markdown'
import { MarkdownTextPrimitive } from '@assistant-ui/react-markdown'
import { TextMessagePartProvider } from '@assistant-ui/react'
import { MARKDOWN_PLUGINS, defaultComponents } from '@/components/assistant-ui/markdown-text'
import { cn } from '@/lib/utils'
interface MarkdownRendererProps {
content: string
components?: Components
components?: MarkdownTextPrimitiveProps['components']
}
export function MarkdownRenderer({ content, components }: MarkdownRendererProps) {
function MarkdownContent(props: MarkdownRendererProps) {
const mergedComponents = props.components
? { ...defaultComponents, ...props.components }
: defaultComponents
return (
<div className="markdown-content text-sm">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{ ...defaultComponents, ...components }}
>
{content}
</ReactMarkdown>
</div>
<TextMessagePartProvider text={props.content}>
<MarkdownTextPrimitive
remarkPlugins={MARKDOWN_PLUGINS}
components={mergedComponents}
className={cn('aui-md text-sm')}
/>
</TextMessagePartProvider>
)
}
export function MarkdownRenderer(props: MarkdownRendererProps) {
return <MarkdownContent {...props} />
}
@@ -0,0 +1,280 @@
import '@assistant-ui/react-markdown/styles/dot.css'
import type { ComponentPropsWithoutRef } from 'react'
import { useState } from 'react'
import {
MarkdownTextPrimitive,
unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
useIsMarkdownCodeBlock,
type CodeHeaderProps,
} from '@assistant-ui/react-markdown'
import remarkGfm from 'remark-gfm'
import { getTelegramWebApp } from '@/hooks/useTelegram'
import { cn } from '@/lib/utils'
import { SyntaxHighlighter } from '@/components/assistant-ui/shiki-highlighter'
export const MARKDOWN_PLUGINS = [remarkGfm]
function safeCopyToClipboard(text: string): Promise<void> {
if (navigator.clipboard?.writeText) {
return navigator.clipboard.writeText(text)
}
return Promise.reject(new Error('Clipboard API not available'))
}
function CopyIcon(props: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={props.className}
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
)
}
function CheckIcon(props: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={props.className}
>
<polyline points="20 6 9 17 4 12" />
</svg>
)
}
function CodeHeader(props: CodeHeaderProps) {
const [copied, setCopied] = useState(false)
const language = props.language && props.language !== 'unknown' ? props.language : ''
const handleCopy = async () => {
try {
await safeCopyToClipboard(props.code)
getTelegramWebApp()?.HapticFeedback?.notificationOccurred('success')
setCopied(true)
setTimeout(() => setCopied(false), 1500)
} catch {
getTelegramWebApp()?.HapticFeedback?.notificationOccurred('error')
}
}
return (
<div className="aui-md-codeheader flex items-center justify-between rounded-t-md bg-[var(--app-code-bg)] px-2 py-1">
<div className="min-w-0 flex-1 pr-2 text-xs font-mono text-[var(--app-hint)]">
{language}
</div>
<button
type="button"
onClick={handleCopy}
className="shrink-0 rounded p-1 text-[var(--app-hint)] hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)] transition-colors"
title="Copy"
>
{copied ? <CheckIcon /> : <CopyIcon />}
</button>
</div>
)
}
function Pre(props: ComponentPropsWithoutRef<'pre'>) {
return (
<pre
{...props}
className={cn(
'aui-md-pre overflow-auto rounded-b-md rounded-t-none bg-[var(--app-code-bg)] p-2 text-xs',
props.className
)}
/>
)
}
function Code(props: ComponentPropsWithoutRef<'code'>) {
const isCodeBlock = useIsMarkdownCodeBlock()
if (isCodeBlock) {
return (
<code
{...props}
className={cn('aui-md-codeblockcode font-mono', props.className)}
/>
)
}
return (
<code
{...props}
className={cn(
'aui-md-code rounded bg-[var(--app-inline-code-bg)] px-[0.3em] py-[0.1em] font-mono text-[0.9em]',
props.className
)}
/>
)
}
function A(props: ComponentPropsWithoutRef<'a'>) {
const rel = props.target === '_blank' ? (props.rel ?? 'noreferrer') : props.rel
return (
<a
{...props}
rel={rel}
className={cn('aui-md-a text-[var(--app-link)] underline', props.className)}
/>
)
}
function Paragraph(props: ComponentPropsWithoutRef<'p'>) {
return <p {...props} className={cn('aui-md-p leading-relaxed', props.className)} />
}
function Blockquote(props: ComponentPropsWithoutRef<'blockquote'>) {
return (
<blockquote
{...props}
className={cn(
'aui-md-blockquote border-l-4 border-[var(--app-hint)] pl-3 opacity-85',
props.className
)}
/>
)
}
function UnorderedList(props: ComponentPropsWithoutRef<'ul'>) {
return <ul {...props} className={cn('aui-md-ul list-disc pl-6', props.className)} />
}
function OrderedList(props: ComponentPropsWithoutRef<'ol'>) {
return <ol {...props} className={cn('aui-md-ol list-decimal pl-6', props.className)} />
}
function ListItem(props: ComponentPropsWithoutRef<'li'>) {
return <li {...props} className={cn('aui-md-li', props.className)} />
}
function Hr(props: ComponentPropsWithoutRef<'hr'>) {
return <hr {...props} className={cn('aui-md-hr border-[var(--app-divider)]', props.className)} />
}
function Table(props: ComponentPropsWithoutRef<'table'>) {
return <table {...props} className={cn('aui-md-table w-full border-collapse', props.className)} />
}
function Thead(props: ComponentPropsWithoutRef<'thead'>) {
return <thead {...props} className={cn('aui-md-thead', props.className)} />
}
function Tbody(props: ComponentPropsWithoutRef<'tbody'>) {
return <tbody {...props} className={cn('aui-md-tbody', props.className)} />
}
function Tr(props: ComponentPropsWithoutRef<'tr'>) {
return <tr {...props} className={cn('aui-md-tr', props.className)} />
}
function Th(props: ComponentPropsWithoutRef<'th'>) {
return (
<th
{...props}
className={cn(
'aui-md-th border border-[var(--app-border)] bg-[var(--app-subtle-bg)] px-2 py-1 text-left font-semibold',
props.className
)}
/>
)
}
function Td(props: ComponentPropsWithoutRef<'td'>) {
return <td {...props} className={cn('aui-md-td border border-[var(--app-border)] px-2 py-1', props.className)} />
}
function H1(props: ComponentPropsWithoutRef<'h1'>) {
return <h1 {...props} className={cn('aui-md-h1 mt-3 text-base font-semibold', props.className)} />
}
function H2(props: ComponentPropsWithoutRef<'h2'>) {
return <h2 {...props} className={cn('aui-md-h2 mt-3 text-sm font-semibold', props.className)} />
}
function H3(props: ComponentPropsWithoutRef<'h3'>) {
return <h3 {...props} className={cn('aui-md-h3 mt-2 text-sm font-semibold', props.className)} />
}
function H4(props: ComponentPropsWithoutRef<'h4'>) {
return <h4 {...props} className={cn('aui-md-h4 mt-2 text-sm font-semibold', props.className)} />
}
function H5(props: ComponentPropsWithoutRef<'h5'>) {
return <h5 {...props} className={cn('aui-md-h5 mt-2 text-sm font-semibold', props.className)} />
}
function H6(props: ComponentPropsWithoutRef<'h6'>) {
return <h6 {...props} className={cn('aui-md-h6 mt-2 text-sm font-semibold', props.className)} />
}
function Strong(props: ComponentPropsWithoutRef<'strong'>) {
return <strong {...props} className={cn('aui-md-strong font-semibold', props.className)} />
}
function Em(props: ComponentPropsWithoutRef<'em'>) {
return <em {...props} className={cn('aui-md-em italic', props.className)} />
}
function Image(props: ComponentPropsWithoutRef<'img'>) {
return <img {...props} className={cn('aui-md-img max-w-full rounded', props.className)} />
}
export const defaultComponents = memoizeMarkdownComponents({
SyntaxHighlighter,
CodeHeader,
pre: Pre,
code: Code,
h1: H1,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
h6: H6,
a: A,
p: Paragraph,
strong: Strong,
em: Em,
blockquote: Blockquote,
ul: UnorderedList,
ol: OrderedList,
li: ListItem,
hr: Hr,
table: Table,
thead: Thead,
tbody: Tbody,
tr: Tr,
th: Th,
td: Td,
img: Image,
} as const)
export function MarkdownText() {
return (
<MarkdownTextPrimitive
remarkPlugins={MARKDOWN_PLUGINS}
components={defaultComponents}
className={cn('aui-md text-sm')}
/>
)
}
@@ -0,0 +1,24 @@
import type { SyntaxHighlighterProps } from '@assistant-ui/react-markdown'
import ShikiHighlighter from 'react-shiki/web'
const SHIKI_THEMES = {
light: 'github-light',
dark: 'github-dark',
} as const
export function SyntaxHighlighter(props: SyntaxHighlighterProps) {
return (
<div className="aui-md-codeblock overflow-hidden rounded-b-md bg-[var(--app-code-bg)]">
<ShikiHighlighter
language={props.language}
theme={SHIKI_THEMES}
delay={75}
addDefaultStyles={false}
showLanguage={false}
as="div"
>
{props.code}
</ShikiHighlighter>
</div>
)
}
+27
View File
@@ -92,6 +92,33 @@ body {
.markdown-content table { border-collapse: collapse; width: 100%; }
.markdown-content th, .markdown-content td { border: 1px solid var(--app-border); padding: 0.25rem 0.5rem; }
/* assistant-ui markdown + shiki */
.aui-md-codeblock pre.shiki {
margin: 0;
padding: 0.5rem;
overflow: auto;
background-color: transparent !important;
font-size: 0.75rem;
}
.aui-md-codeblock pre.shiki code {
display: block;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.aui-md-codeblock .shiki,
.aui-md-codeblock .shiki span {
background-color: transparent !important;
}
html[data-theme="dark"] .shiki,
html[data-theme="dark"] .shiki span {
color: var(--shiki-dark) !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
/* Ultrathink rainbow sparkle effect - per-letter wave animation */
@keyframes rainbow-wave {
0% { color: #ff5555; } /* red */