From b8a5e5b67ca7f188b258d01706d8df622d2d9df0 Mon Sep 17 00:00:00 2001 From: SOUMITRO-SAHA Date: Sun, 29 Sep 2024 20:49:59 +0530 Subject: [PATCH 1/5] Fix: Add Syntax Highlighting to Sidebar Code Snippets --- extensions/void/package-lock.json | 306 +++++++++++++++++- extensions/void/package.json | 4 +- .../void/src/sidebar/MarkdownRender.tsx | 129 +++++--- 3 files changed, 396 insertions(+), 43 deletions(-) diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json index 1073079a..47203796 100644 --- a/extensions/void/package-lock.json +++ b/extensions/void/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.1", "dependencies": { "@anthropic-ai/sdk": "^0.27.1", - "openai": "^4.57.0" + "openai": "^4.57.0", + "react-syntax-highlighter": "^15.5.0" }, "devDependencies": { "@eslint/js": "^9.9.1", @@ -19,6 +20,7 @@ "@types/node": "^22.5.1", "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", + "@types/react-syntax-highlighter": "^15.5.13", "@types/vscode": "1.92.0", "@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/parser": "^8.3.0", @@ -184,6 +186,18 @@ "node": ">=4" } }, + "node_modules/@babel/runtime": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -685,6 +699,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -2644,6 +2668,19 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -2815,6 +2852,14 @@ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/formdata-node": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", @@ -3106,6 +3151,16 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", @@ -3146,6 +3201,71 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/hastscript/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/hastscript/node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -3155,6 +3275,15 @@ "he": "bin/he" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -3937,8 +4066,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -4094,7 +4222,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -4102,6 +4229,20 @@ "loose-envify": "cli.js" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -5767,6 +5908,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -5842,7 +5992,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5895,6 +6044,22 @@ "react": ">=18" } }, + "node_modules/react-syntax-highlighter": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz", + "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -5952,6 +6117,128 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "license": "MIT", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -7472,6 +7759,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/extensions/void/package.json b/extensions/void/package.json index 43b34951..9553b286 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -111,6 +111,7 @@ "@types/node": "^22.5.1", "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", + "@types/react-syntax-highlighter": "^15.5.13", "@types/vscode": "1.92.0", "@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/parser": "^8.3.0", @@ -136,6 +137,7 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.27.1", - "openai": "^4.57.0" + "openai": "^4.57.0", + "react-syntax-highlighter": "^15.5.0" } } diff --git a/extensions/void/src/sidebar/MarkdownRender.tsx b/extensions/void/src/sidebar/MarkdownRender.tsx index e9cc2b96..4df719c3 100644 --- a/extensions/void/src/sidebar/MarkdownRender.tsx +++ b/extensions/void/src/sidebar/MarkdownRender.tsx @@ -1,34 +1,69 @@ -import React, { JSX, useState } from 'react'; -import { MarkedToken, Token, TokensList } from 'marked'; -import { awaitVSCodeResponse, getVSCodeAPI } from './getVscodeApi'; +import React, { JSX, useState } from "react"; +import { MarkedToken, Token, TokensList } from "marked"; +import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi"; +import SyntaxHighlighter from "react-syntax-highlighter"; +import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs"; // code block with Apply button at top -export const BlockCode = ({ text, disableApplyButton = false }: { text: string, disableApplyButton?: boolean }) => { - return
- {disableApplyButton ? null :
- -
} -
-
-				{text}
-			
+export const BlockCode = ({ + text, + language, + disableApplyButton = false, +}: { + text: string; + language?: string; + disableApplyButton?: boolean; +}) => { + const customStyle = { + ...atomOneDarkReasonable, + 'code[class*="language-"]': { + ...atomOneDarkReasonable['code[class*="language-"]'], + background: "none", + }, + }; + + return ( +
+ {disableApplyButton ? null : ( +
+ +
+ )} +
+ + {text} + +
-
-} + ); +}; const Render = ({ token }: { token: Token }) => { - // deal with built-in tokens first (assume marked token) - const t = token as MarkedToken + const t = token as MarkedToken; if (t.type === "space") { return {t.raw}; } if (t.type === "code") { - return + return ; } if (t.type === "heading") { @@ -42,7 +77,7 @@ const Render = ({ token }: { token: Token }) => { {t.header.map((cell: any, index: number) => ( - + {cell.raw} ))} @@ -52,7 +87,10 @@ const Render = ({ token }: { token: Token }) => { {t.rows.map((row: any[], rowIndex: number) => ( {row.map((cell: any, cellIndex: number) => ( - + {cell.raw} ))} @@ -72,11 +110,11 @@ const Render = ({ token }: { token: Token }) => { } if (t.type === "list") { - - const ListTag = t.ordered ? 'ol' : 'ul'; + const ListTag = t.ordered ? "ol" : "ul"; return ( - {t.items.map((item, index) => (
  • @@ -91,15 +129,23 @@ const Render = ({ token }: { token: Token }) => { } if (t.type === "paragraph") { - return

    - {t.tokens.map((token, index) => ( - - ))} -

    ; + return ( +

    + {t.tokens.map((token, index) => ( + + ))} +

    + ); } if (t.type === "html") { - return
    {``}{t.raw}{``}
    ; + return ( +
    +				{``}
    +				{t.raw}
    +				{``}
    +			
    + ); } if (t.type === "text" || t.type === "escape") { @@ -111,7 +157,11 @@ const Render = ({ token }: { token: Token }) => { } if (t.type === "link") { - return {t.text}; + return ( + + {t.text} + + ); } if (t.type === "image") { @@ -128,7 +178,11 @@ const Render = ({ token }: { token: Token }) => { // inline code if (t.type === "codespan") { - return {t.text}; + return ( + + {t.text} + + ); } if (t.type === "br") { @@ -139,12 +193,13 @@ const Render = ({ token }: { token: Token }) => { return {t.text}; } - // default - return
    - Unknown type: - {t.raw} -
    ; + return ( +
    + Unknown type: + {t.raw} +
    + ); }; const MarkdownRender = ({ tokens }: { tokens: TokensList }) => { From bed7929e489016797651150d3c10826ea4a67240 Mon Sep 17 00:00:00 2001 From: w1gs Date: Thu, 3 Oct 2024 21:24:53 -0400 Subject: [PATCH 2/5] Added error messages in UI --- extensions/void/src/common/sendLLMMessage.ts | 518 ++++++++++++------- extensions/void/src/sidebar/Sidebar.tsx | 20 +- 2 files changed, 338 insertions(+), 200 deletions(-) diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index 8f651de2..a1d85228 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -1,133 +1,173 @@ import Anthropic from '@anthropic-ai/sdk'; +import { Ollama } from 'ollama/browser'; import OpenAI from 'openai'; -import { Ollama } from 'ollama/browser' -import { getVSCodeAPI } from '../sidebar/getVscodeApi'; - // always compare these against package.json to make sure every setting in this type can actually be provided by the user export type ApiConfig = { anthropic: { - apikey: string, - model: string, - maxTokens: string - }, + apikey: string; + model: string; + maxTokens: string; + }; openai: { - apikey: string, - model: string, - }, + apikey: string; + model: string; + }; greptile: { - apikey: string, - githubPAT: string, + apikey: string; + githubPAT: string; repoinfo: { - remote: string, // e.g. 'github' - repository: string, // e.g. 'voideditor/void' - branch: string // e.g. 'main' - } - }, + remote: string; // e.g. 'github' + repository: string; // e.g. 'voideditor/void' + branch: string; // e.g. 'main' + }; + }; ollama: { - endpoint: string, - model: string - }, - whichApi: string -} - - - -type OnText = (newText: string, fullText: string) => void - -export type LLMMessage = { - role: 'user' | 'assistant', - content: string -} - -type SendLLMMessageFnTypeInternal = (params: { - messages: LLMMessage[], - onText: OnText, - onFinalMessage: (input: string) => void, - apiConfig: ApiConfig, -}) - => { - abort: () => void - } - -type SendLLMMessageFnTypeExternal = (params: { - messages: LLMMessage[], - onText: OnText, - onFinalMessage: (input: string) => void, - apiConfig: ApiConfig | null, -}) - => { - abort: () => void - } - - - - -// Claude -const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { - - const anthropic = new Anthropic({ apiKey: apiConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"] - - const stream = anthropic.messages.stream({ - model: apiConfig.anthropic.model, - max_tokens: parseInt(apiConfig.anthropic.maxTokens), - messages: messages, - }); - - let did_abort = false - - // when receive text - stream.on('text', (newText, fullText) => { - if (did_abort) return - onText(newText, fullText) - }) - - // when we get the final message on this stream (or when error/fail) - stream.on('finalMessage', (claude_response) => { - if (did_abort) return - // stringify the response's content - let content = claude_response.content.map(c => { if (c.type === 'text') { return c.text } }).join('\n'); - onFinalMessage(content) - }) - - - // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either - const abort = () => { - // stream.abort() // this doesnt appear to do anything, but it should try to stop claude from generating anymore - did_abort = true - } - - return { abort } - + endpoint: string; + model: string; + }; + whichApi: string; }; +type OnText = (newText: string, fullText: string) => void; +export type LLMMessage = { + role: 'user' | 'assistant'; + content: string; +}; +type SendLLMMessageFnTypeInternal = (params: { + messages: LLMMessage[]; + onText: OnText; + onFinalMessage: (input: string) => void; + onError: (message: string) => void; + apiConfig: ApiConfig; +}) => { + abort: () => void; +}; -// OpenAI -const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { +type SendLLMMessageFnTypeExternal = (params: { + messages: LLMMessage[]; + onText: OnText; + onFinalMessage: (input: string) => void; + onError: (message: string) => void; + apiConfig: ApiConfig | null; +}) => { + abort: () => void; +}; - let didAbort = false - let fullText = '' +type AnthropicErrorResponse = { + type: string; + error: { + type: string; + message: string; + }; +}; - // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either - let abort: () => void = () => { +// Helper function to handle missing API keys +const handleMissingApiKey = (serviceName: string, onError: (message: string) => void) => { + onError(`${serviceName} API key not set`); + return { abort: () => {} }; +}; + +// Claude +const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ + messages, + onText, + onFinalMessage, + onError, + apiConfig, +}) => { + const { apikey, model, maxTokens } = apiConfig.anthropic; + + if (!apikey) { + return handleMissingApiKey('Anthropic', onError); + } + + let didAbort = false; + + const anthropic = new Anthropic({ + apiKey: apikey, + dangerouslyAllowBrowser: true, + }); + + const stream = anthropic.messages + .stream({ + model: model, + max_tokens: parseInt(maxTokens), + messages: messages, + stream: true, + }) + .on('error', (err) => { + if (err instanceof Anthropic.APIError) { + if (err.status === 401) { + onError('Unauthorized: Invalid Anthropic API key'); + } else { + onError((err.error as AnthropicErrorResponse).error.message); + } + } else { + console.error(err); + onError(err.message); + } + }) + .on('text', (newText, fullText) => { + if (didAbort) return; + onText(newText, fullText); + }) + .on('finalMessage', (claudeResponse) => { + if (didAbort) return; + const content = claudeResponse.content + .filter((c) => c.type === 'text') + .map((c) => c.text) + .join('\n'); + onFinalMessage(content); + }); + + const abort = () => { + stream.controller.abort(); didAbort = true; }; - const openai = new OpenAI({ apiKey: apiConfig.openai.apikey, dangerouslyAllowBrowser: true }); + return { abort }; +}; - openai.chat.completions.create({ - model: apiConfig.openai.model, - messages: messages, - stream: true, - }) - .then(async response => { +// OpenAI +const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ + messages, + onText, + onFinalMessage, + onError, + apiConfig, +}) => { + const { apikey, model } = apiConfig.openai; + + if (!apikey) { + return handleMissingApiKey('OpenAI', onError); + } + + let didAbort = false; + let fullText = ''; + + const openai = new OpenAI({ + apiKey: apikey, + dangerouslyAllowBrowser: true, + }); + + let abort = () => { + didAbort = true; + }; + + openai.chat.completions + .create({ + model: model, + messages: messages, + stream: true, + }) + .then(async (response) => { abort = () => { - // response.controller.abort() + response.controller.abort(); didAbort = true; - } - // when receive text + }; try { for await (const chunk of response) { if (didAbort) return; @@ -135,43 +175,65 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal fullText += newText; onText(newText, fullText); } - onFinalMessage(fullText); - } - // when error/fail - catch (error) { + if (!didAbort) { + onFinalMessage(fullText); + } + } catch (error) { + onError(`Error in OpenAI stream: ${error}`); console.error('Error in OpenAI stream:', error); - onFinalMessage(fullText); + if (!didAbort) { + onFinalMessage(fullText); + } } }) + .catch((responseError) => { + if (responseError.status === 401) { + onError('Unauthorized: Invalid OpenAI API key'); + } else if (responseError.status === 400 && responseError.param === 'stream') { + onError(`The OpenAI model '${model}' does not support streamed responses.`); + } else { + onError(responseError.message); + } + }); + return { abort }; }; - - // Ollama -export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { +const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ + messages, + onText, + onFinalMessage, + onError, + apiConfig, +}) => { + const { endpoint, model } = apiConfig.ollama; - let didAbort = false - let fullText = "" + if (!endpoint) { + onError('Ollama endpoint not set'); + return { abort: () => {} }; + } + + let didAbort = false; + let fullText = ''; + + const ollama = new Ollama({ host: endpoint }); - // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either let abort = () => { didAbort = true; }; - const ollama = new Ollama({ host: apiConfig.ollama.endpoint }) - - ollama.chat({ - model: apiConfig.ollama.model, - messages: messages, - stream: true, - }) - .then(async stream => { + ollama + .chat({ + model: model, + messages: messages, + stream: true, + }) + .then(async (stream) => { abort = () => { - // ollama.abort() - didAbort = true - } - // iterate through the stream + ollama.abort(); + didAbort = true; + }; try { for await (const chunk of stream) { if (didAbort) return; @@ -179,108 +241,168 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, fullText += newText; onText(newText, fullText); } - onFinalMessage(fullText); - } - // when error/fail - catch (error) { - console.error('Error:', error); - onFinalMessage(fullText); + if (!didAbort) { + onFinalMessage(fullText); + } + } catch (error) { + onError(`Error while streaming response: ${error}`); + console.error('Error while streaming response:', error); + if (!didAbort) { + onFinalMessage(fullText); + } } }) + .catch((responseError) => { + if (responseError.error) { + onError(responseError.error.charAt(0).toUpperCase() + responseError.error.slice(1)); + } else { + onError(responseError.message); + } + console.error(responseError); + }); + return { abort }; }; - - // Greptile -// https://docs.greptile.com/api-reference/query -// https://docs.greptile.com/quickstart#sample-response-streamed +const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ + messages, + onText, + onFinalMessage, + onError, + apiConfig, +}) => { + const { apikey, githubPAT, repoinfo } = apiConfig.greptile; -const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { + if (!apikey) { + return handleMissingApiKey('Greptile', onError); + } + if (!githubPAT) { + onError('GitHub token not set'); + return { abort: () => {} }; + } - let didAbort = false - let fullText = '' - - // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either - let abort: () => void = () => { didAbort = true } + let didAbort = false; + let fullText = ''; + const controller = new AbortController(); fetch('https://api.greptile.com/v2/query', { method: 'POST', headers: { - "Authorization": `Bearer ${apiConfig.greptile.apikey}`, - "X-Github-Token": `${apiConfig.greptile.githubPAT}`, - "Content-Type": `application/json`, + Authorization: `Bearer ${apikey}`, + 'X-Github-Token': `${githubPAT}`, + 'Content-Type': `application/json`, }, body: JSON.stringify({ messages, stream: true, - repositories: [apiConfig.greptile.repoinfo] + repositories: [repoinfo], }), + signal: controller.signal, }) - // this is {message}\n{message}\n{message}...\n - .then(async response => { - const text = await response.text() - console.log('got greptile', text) - return JSON.parse(`[${text.trim().split('\n').join(',')}]`) + .then((response) => { + if (response.status === 401) { + onError('Unauthorized: Invalid Greptile API key'); + return null; + } else if (response.status !== 200) { + onError(`Error: ${response.status} ${response.statusText}`); + return null; + } + return response.body; }) - // TODO make this actually stream, right now it just sends one message at the end - .then(async responseArr => { - if (didAbort) - return - - for (let response of responseArr) { - - const type: string = response['type'] - const message = response['message'] - - // when receive text - if (type === 'message') { - fullText += message - onText(message, fullText) - } - else if (type === 'sources') { - const { filepath, linestart, lineend } = message as { filepath: string, linestart: number | null, lineend: number | null } - fullText += filepath - onText(filepath, fullText) - } - // type: 'status' with an empty 'message' means last message - else if (type === 'status') { - if (!message) { - onFinalMessage(fullText) + .then(async (body) => { + if (!body || didAbort) return; + const reader = body.getReader(); + const decoder = new TextDecoder('utf-8'); + while (!didAbort) { + const { done, value } = await reader.read(); + if (done || didAbort) break; + const chunk = decoder.decode(value, { stream: true }); + const messages = chunk.trim().split('\n').filter(Boolean); + for (const msg of messages) { + try { + const parsed = JSON.parse(msg); + const { type, message } = parsed; + if (type === 'message' || type === 'sources') { + fullText += message; + onText(message, fullText); + } else if (type === 'status' && !message) { + if (!didAbort) { + onFinalMessage(fullText); + } + } + } catch (e) { + console.error('Error parsing Greptile response:', e); + onError(`Error parsing Greptile response: ${e}`); } } } - }) - .catch(e => { + .catch((e) => { + if (didAbort) return; console.error('Error in Greptile stream:', e); - onFinalMessage(fullText); - + onError(`Error in Greptile stream: ${e}`); + if (!didAbort) { + onFinalMessage(fullText); + } }); - return { abort } + const abort = () => { + controller.abort(); + didAbort = true; + }; -} + return { abort }; +}; - - -export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => { - if (!apiConfig) return { abort: () => { } } +export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ + messages, + onText, + onFinalMessage, + onError, + apiConfig, +}) => { + if (!apiConfig) { + onError('API configuration is missing'); + return { abort: () => {} }; + } switch (apiConfig.whichApi) { case 'anthropic': - return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); + return sendClaudeMsg({ + messages, + onText, + onFinalMessage, + onError, + apiConfig, + }); case 'openai': - return sendOpenAIMsg({ messages, onText, onFinalMessage, apiConfig }); + return sendOpenAIMsg({ + messages, + onText, + onFinalMessage, + onError, + apiConfig, + }); case 'greptile': - return sendGreptileMsg({ messages, onText, onFinalMessage, apiConfig }); + return sendGreptileMsg({ + messages, + onText, + onFinalMessage, + onError, + apiConfig, + }); case 'ollama': - return sendOllamaMsg({ messages, onText, onFinalMessage, apiConfig }); + return sendOllamaMsg({ + messages, + onText, + onFinalMessage, + onError, + apiConfig, + }); default: - console.error(`Error: whichApi was ${apiConfig.whichApi}, which is not recognized!`); - return { abort: () => { } } - //return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); // TODO + onError(`Error: whichApi was '${apiConfig.whichApi}', which is not recognized!`); + return { abort: () => {} }; } -} - +}; diff --git a/extensions/void/src/sidebar/Sidebar.tsx b/extensions/void/src/sidebar/Sidebar.tsx index e96006d0..4b2443fb 100644 --- a/extensions/void/src/sidebar/Sidebar.tsx +++ b/extensions/void/src/sidebar/Sidebar.tsx @@ -144,6 +144,11 @@ const Sidebar = () => { const [chatMessageHistory, setChatMessageHistory] = useState([]) const [messageStream, setMessageStream] = useState('') const [isLoading, setIsLoading] = useState(false) + const [requestFailed, setRequestFailed] = useState(false) + const [requestFailedReason, setRequestFailedReason] = useState('') + + + const abortFnRef = useRef<(() => void) | null>(null) @@ -191,6 +196,10 @@ const Sidebar = () => { e.preventDefault() if (isLoading) return + // Reset any error messages from previous submit + setRequestFailed(false) + setRequestFailedReason('') + setIsLoading(true) setInstructions(''); formRef.current?.reset(); // reset the form's text @@ -216,7 +225,7 @@ const Sidebar = () => { const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files } setChatMessageHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt]) - // send message to claude + // send message to LLM let { abort } = sendLLMMessage({ messages: [...chatMessageHistory.map(m => ({ role: m.role, content: m.content })), { role: 'user', content }], onText: (newText, fullText) => setMessageStream(fullText), @@ -227,6 +236,7 @@ const Sidebar = () => { setMessageStream('') setIsLoading(false) }, + onError: (message) => { onStop(); setRequestFailed(true); setRequestFailedReason(message)}, apiConfig: apiConfig }) abortFnRef.current = abort @@ -282,6 +292,12 @@ const Sidebar = () => {
  • )} + {/* error message */} + {requestFailed && ( +
    +
    {`${requestFailedReason}`}
    +
    + )}
    { e.preventDefault(); onSubmit(e) }}> - {/* input */} + {/* input */}