diff --git a/CHANGELOG.md b/CHANGELOG.md index 24c1bb22ff..fa95cf84d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/ - Added `styleOptions.bubbleAttachmentMaxWidth`/`bubbleAttachmentMinWidth` and `styleOptions.bubbleMessageMaxWidth`/`bubbleMessageMinWidth`, in PR [#5321](https://github.com/microsoft/BotFramework-WebChat/pull/5321), by [@compulim](https://github.com/compulim) - (Experimental) Added more CSS variables support, in PR [#5321](https://github.com/microsoft/BotFramework-WebChat/pull/5321), by [@compulim](https://github.com/compulim) - Added MathML/TeX block support in Markdown via [`micromark-extension-math`](https://npmjs.com/package/micromark-extension-math) and [`katex`](https://katex.org/), in PR [#5332](https://github.com/microsoft/BotFramework-WebChat/pull/5332), by [@compulim](https://github.com/compulim) +- Added code viewer dialog with syntax highlighting, in PR [#5335](https://github.com/microsoft/BotFramework-WebChat/pull/5335), by [@OEvgeny](https://github.com/OEvgeny) ### Changed diff --git a/__tests__/html/copyButton.hideAndShow.html b/__tests__/html/copyButton.hideAndShow.html index c39052b75a..f66d3623f5 100644 --- a/__tests__/html/copyButton.hideAndShow.html +++ b/__tests__/html/copyButton.hideAndShow.html @@ -53,6 +53,12 @@ await pageConditions.numActivitiesShown(1); + await pageConditions.became( + 'copy button is available', + () => !!document.querySelector(`[data-testid="${WebChat.testIds.copyButton}"]`), + 1000 + ); + // WHEN: Focus on the "Copy" button via keyboard. await host.click(document.querySelector(`[data-testid="${WebChat.testIds.copyButton}"]`)); diff --git a/__tests__/html2/activity/viewCodeButton.html b/__tests__/html2/activity/viewCodeButton.html new file mode 100644 index 0000000000..a680798c5e --- /dev/null +++ b/__tests__/html2/activity/viewCodeButton.html @@ -0,0 +1,140 @@ + + + + + + + + + + + + +
+ + + diff --git a/__tests__/html2/activity/viewCodeButton.html.snap-1.png b/__tests__/html2/activity/viewCodeButton.html.snap-1.png new file mode 100644 index 0000000000..8550f94e79 Binary files /dev/null and b/__tests__/html2/activity/viewCodeButton.html.snap-1.png differ diff --git a/__tests__/html2/activity/viewCodeButton.html.snap-2.png b/__tests__/html2/activity/viewCodeButton.html.snap-2.png new file mode 100644 index 0000000000..49db456c86 Binary files /dev/null and b/__tests__/html2/activity/viewCodeButton.html.snap-2.png differ diff --git a/__tests__/html2/activity/viewCodeButton.html.snap-3.png b/__tests__/html2/activity/viewCodeButton.html.snap-3.png new file mode 100644 index 0000000000..f1d71eae6e Binary files /dev/null and b/__tests__/html2/activity/viewCodeButton.html.snap-3.png differ diff --git a/package-lock.json b/package-lock.json index 627c89de4c..65a1266e81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4378,6 +4378,57 @@ "dev": true, "license": "MIT" }, + "node_modules/@shikijs/core": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.22.1.tgz", + "integrity": "sha512-bqAhT/Ri5ixV4oYsvJNH8UJjpjbINWlWyXY6tBTsP4OmD6XnFv43nRJ+lTdxd2rmG5pgam/x+zGR6kLRXrpEKA==", + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "1.22.1", + "@shikijs/engine-oniguruma": "1.22.1", + "@shikijs/types": "1.22.1", + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.3" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.22.1.tgz", + "integrity": "sha512-540pyoy0LWe4jj2BVbgELwOFu1uFvRI7lg4hdsExrSXA9x7gqfzZ/Nnh4RfX86aDAgJ647gx4TCmRwACbnQSvw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.22.1", + "@shikijs/vscode-textmate": "^9.3.0", + "oniguruma-to-js": "0.4.3" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.1.tgz", + "integrity": "sha512-L+1Vmd+a2kk8HtogUFymQS6BjUfJnzcWoUp1BUgxoDiklbKSMvrsMuLZGevTOP1m0rEjgnC5MsDmsr8lX1lC+Q==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.22.1", + "@shikijs/vscode-textmate": "^9.3.0" + } + }, + "node_modules/@shikijs/types": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.22.1.tgz", + "integrity": "sha512-+45f8mu/Hxqs6Kyhfm98Nld5n7Q7lwhjU8UtdQwrOPs7BnM4VAb929O3IQ2ce+4D7SlNFlZGd8CnKRSnwbQreQ==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz", + "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==", + "license": "MIT" + }, "node_modules/@sinclair/typebox": { "version": "0.24.51", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", @@ -4709,6 +4760,15 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", @@ -5309,7 +5369,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true, "license": "ISC" }, "node_modules/@webassemblyjs/ast": { @@ -7070,6 +7129,16 @@ "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "license": "Apache-2.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -7196,6 +7265,26 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -7632,6 +7721,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", @@ -11503,6 +11602,42 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz", + "integrity": "sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==", + "license": "MIT", + "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": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/heimdalljs": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz", @@ -11571,6 +11706,16 @@ "dev": true, "license": "MIT" }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -15988,6 +16133,27 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-to-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", @@ -17662,6 +17828,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oniguruma-to-js": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/oniguruma-to-js/-/oniguruma-to-js-0.4.3.tgz", + "integrity": "sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==", + "license": "MIT", + "dependencies": { + "regex": "^4.3.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -18303,6 +18481,16 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -19168,6 +19356,12 @@ "@babel/runtime": "^7.8.4" } }, + "node_modules/regex": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/regex/-/regex-4.3.3.tgz", + "integrity": "sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==", + "license": "MIT" + }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -20260,6 +20454,20 @@ "dev": true, "license": "MIT" }, + "node_modules/shiki": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.22.1.tgz", + "integrity": "sha512-PbJ6XxrWLMwB2rm3qdjIHNm3zq4SfFnOx0B3rEoi4AN8AUngsdyZ1tRe5slMPtn6jQkbUURLNZPpLR7Do3k78g==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.22.1", + "@shikijs/engine-javascript": "1.22.1", + "@shikijs/engine-oniguruma": "1.22.1", + "@shikijs/types": "1.22.1", + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4" + } + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -20433,6 +20641,16 @@ "source-map": "^0.6.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/spawn-command": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", @@ -20780,6 +20998,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -21502,6 +21734,16 @@ "tree-kill": "cli.js" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", @@ -22753,6 +22995,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", @@ -22766,6 +23034,35 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -23003,6 +23300,34 @@ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "license": "MIT" }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -23907,6 +24232,16 @@ "license": "MIT", "peer": true }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "packages/api": { "name": "botframework-webchat-api", "version": "0.0.0-0", @@ -24189,6 +24524,7 @@ "react-say": "2.1.0", "react-scroll-to-bottom": "4.2.1-main.53844f5", "redux": "5.0.1", + "shiki": "^1.22.1", "simple-update-in": "2.2.0", "use-propagate": "^0.2.0-main.fb24772", "use-ref-from": "0.1.0", diff --git a/packages/api/src/localization/en-US.json b/packages/api/src/localization/en-US.json index a88c141f9e..8b5fd1ffd5 100644 --- a/packages/api/src/localization/en-US.json +++ b/packages/api/src/localization/en-US.json @@ -10,7 +10,10 @@ "_ACTIVITY_YOU_SAID_ALT.comment": "This is for screen reader only.", "ACTIVITY_BOT_ATTACHED_ALT": "Bot attached:", "_ACTIVITY_BOT_ATTACHED_ALT.comment": "This is for screen reader and is narrated before each attachments sent by the bot.", + "ACTIVITY_CODE_ALT": "Code sample: $1", + "_ACTIVITY_CODE_ALT.comment": "This is for screen reader code sample dialog label, $1 is the dialog title.", "ACTIVITY_CONTENT_CAUTION": "AI-generated content may be incorrect", + "ACTIVITY_CODE_CAUTION": "AI-generated code. Review and use carefully", "ACTIVITY_ERROR_BOX_TITLE": "Error message", "ACTIVITY_INTERACTIVE_FOOTNOTE_ALT": "Click to interact.", "_ACTIVITY_INTERACTIVE_FOOTNOTE_ALT.comment": "This is for screen reader. When the user is navigating to this message which is either contains interactive elements, contains links, or failed to send (with a retry button), it will give this hint.", @@ -168,6 +171,7 @@ "TYPING_INDICATOR_SINGLE_TEXT": "$1 is typing.", "_TYPING_INDICATOR_MULTIPLE_TEXT.comment": "This shows when two or more users are typing simultaneously.", "TYPING_INDICATOR_MULTIPLE_TEXT": "$1 and others are typing.", + "VIEW_CODE_BUTTON_TEXT": "Code", "VOTE_DISLIKE_ALT": "Dislike", "_VOTE_DISLIKE_ALT.comment": "This is for screen reader. The label of a button with a thumb up icon and is placed next to the timestamp. The button is for giving feedback that the end-user don't think the response is useful.", "VOTE_LIKE_ALT": "Like", diff --git a/packages/component/package.json b/packages/component/package.json index a7282db9a5..3d000c8c5e 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -158,6 +158,7 @@ "react-say": "2.1.0", "react-scroll-to-bottom": "4.2.1-main.53844f5", "redux": "5.0.1", + "shiki": "^1.22.1", "simple-update-in": "2.2.0", "use-propagate": "^0.2.0-main.fb24772", "use-ref-from": "0.1.0", diff --git a/packages/component/src/Attachment/Text/private/ActivityViewCodeButton.tsx b/packages/component/src/Attachment/Text/private/ActivityViewCodeButton.tsx new file mode 100644 index 0000000000..6d6efd09c7 --- /dev/null +++ b/packages/component/src/Attachment/Text/private/ActivityViewCodeButton.tsx @@ -0,0 +1,60 @@ +import { hooks } from 'botframework-webchat-api'; +import classNames from 'classnames'; +import React, { memo, useCallback } from 'react'; + +import useStyleSet from '../../../hooks/useStyleSet'; +import useShowModal from '../../../providers/ModalDialog/useShowModal'; +import LocalizedString from '../../../Utils/LocalizedString'; +import ActivityButton from './ActivityButton'; +import CodeContent from './CodeContent'; + +const CODE_ICON_URL = `data:image/svg+xml;utf8,${encodeURIComponent('')}`; + +const { useLocalizer } = hooks; + +type Props = Readonly<{ + className?: string | undefined; + code: string; + language?: string | undefined; + isAIGenerated: boolean; + title: string; +}>; + +const ViewCodeButton = ({ className, code, language, title = '', isAIGenerated = false }: Props) => { + const [{ activityButton, viewCodeDialog }] = useStyleSet(); + const showModal = useShowModal(); + const localize = useLocalizer(); + + const showCodeModal = useCallback(() => { + showModal( + () => ( + + {isAIGenerated && ( +
+ +
+ )} +
+ ), + { + className: classNames('webchat__view-code-dialog', viewCodeDialog), + 'aria-label': localize('ACTIVITY_CODE_ALT', title ?? '') + } + ); + }, [code, isAIGenerated, language, localize, showModal, title, viewCodeDialog]); + + return ( + + ); +}; + +export default memo(ViewCodeButton); diff --git a/packages/component/src/Attachment/Text/private/CodeContent.tsx b/packages/component/src/Attachment/Text/private/CodeContent.tsx new file mode 100644 index 0000000000..7eaeccb64c --- /dev/null +++ b/packages/component/src/Attachment/Text/private/CodeContent.tsx @@ -0,0 +1,65 @@ +import React, { Fragment, memo, ReactNode, useEffect, useState } from 'react'; +import classNames from 'classnames'; +import createHighlighter from './shiki'; + +type Props = Readonly<{ + children?: ReactNode | undefined; + className?: string | undefined; + code: string; + language?: string | undefined; + title: string; +}>; + +const highlighterPromise = createHighlighter(); + +const CodeContent = memo(({ children, className, code, language, title }: Props) => { + const [highlightedCode, setHighlightedCode] = useState(''); + + useEffect(() => { + let mounted = true; + (async function highlight() { + const highlighter = await highlighterPromise; + if (!mounted) { + return; + } + try { + const html = highlighter.codeToHtml(code, { + lang: language, + theme: 'github-light-default' + }); + + setHighlightedCode(html); + } catch (error) { + console.error('botframework-webchat: Failed to highlight code:', error); + + const pre = document.createElement('pre'); + pre.textContent = code; + + setHighlightedCode(pre.outerHTML); + } + })(); + + return () => { + mounted = false; + }; + }, [code, language]); + + return ( + +
+

{title}

+ {/* */} +
+
+ {children} + + ); +}); + +CodeContent.displayName = 'CodeContent'; + +export default memo(CodeContent); diff --git a/packages/component/src/Attachment/Text/private/MarkdownTextContent.tsx b/packages/component/src/Attachment/Text/private/MarkdownTextContent.tsx index c3b702cd44..e08c98b16f 100644 --- a/packages/component/src/Attachment/Text/private/MarkdownTextContent.tsx +++ b/packages/component/src/Attachment/Text/private/MarkdownTextContent.tsx @@ -19,8 +19,11 @@ import useStyleSet from '../../../hooks/useStyleSet'; import useShowModal from '../../../providers/ModalDialog/useShowModal'; import { type PropsOf } from '../../../types/PropsOf'; import ActivityCopyButton from './ActivityCopyButton'; +import ActivityViewCodeButton from './ActivityViewCodeButton'; import CitationModalContext from './CitationModalContent'; import MessageSensitivityLabel, { type MessageSensitivityLabelProps } from './MessageSensitivityLabel'; +import isAIGeneratedActivity from './isAIGeneratedActivity'; +import isBasedOnSoftwareSourceCode from './isBasedOnSoftwareSourceCode'; import isHTMLButtonElement from './isHTMLButtonElement'; const { useLocalizer } = hooks; @@ -232,13 +235,24 @@ const MarkdownTextContent = memo(({ activity, children, markdown }: Props) => { ))} )} - {activity.type === 'message' && activity.text && messageThing?.keywords?.includes('AllowCopy') ? ( - - ) : null} +
+ {activity.type === 'message' && isBasedOnSoftwareSourceCode(messageThing) ? ( + + ) : null} + {activity.type === 'message' && activity.text && messageThing?.keywords?.includes('AllowCopy') ? ( + + ) : null} +
); }); diff --git a/packages/component/src/Attachment/Text/private/isBasedOnSoftwareSourceCode.ts b/packages/component/src/Attachment/Text/private/isBasedOnSoftwareSourceCode.ts new file mode 100644 index 0000000000..37f192caa8 --- /dev/null +++ b/packages/component/src/Attachment/Text/private/isBasedOnSoftwareSourceCode.ts @@ -0,0 +1,18 @@ +import { type OrgSchemaCreativeWork } from 'botframework-webchat-core'; + +/** + * Type guard to check if the isBasedOn field is of type SoftwareSourceCode + * @param messageEntity The message entity to check + * @returns True if isBasedOn is of type SoftwareSourceCode, false otherwise + */ +export default function isBasedOnSoftwareSourceCode( + messageEntity?: OrgSchemaCreativeWork +): messageEntity is OrgSchemaCreativeWork & { isBasedOn: SoftwareSourceCode } { + return messageEntity?.isBasedOn?.['@type'] === 'SoftwareSourceCode'; +} + +interface SoftwareSourceCode { + '@type': 'SoftwareSourceCode'; + programmingLanguage: string; + text: string; +} diff --git a/packages/component/src/Attachment/Text/private/shiki.ts b/packages/component/src/Attachment/Text/private/shiki.ts new file mode 100644 index 0000000000..5a083989a5 --- /dev/null +++ b/packages/component/src/Attachment/Text/private/shiki.ts @@ -0,0 +1,25 @@ +// `shiki/core` entry does not include any themes or languages or the wasm binary. +import { createHighlighterCore } from 'shiki/core'; +import { createJavaScriptRegexEngine } from 'shiki/engine-javascript.mjs'; + +// directly import the theme and language modules, only the ones you imported will be bundled. +import themeGitHubDark from 'shiki/themes/github-dark-default.mjs'; +import themeGitHubLight from 'shiki/themes/github-light-default.mjs'; + +import languageJavaScript from 'shiki/langs/js.mjs'; +import languagePython from 'shiki/langs/py.mjs'; +import languageTypeScript from 'shiki/langs/ts.mjs'; + +function createHighlighter() { + return createHighlighterCore({ + themes: [ + // instead of strings, you need to pass the imported module + themeGitHubDark, + themeGitHubLight + ], + langs: [languageJavaScript, languagePython, languageTypeScript], + engine: createJavaScriptRegexEngine() + }); +} + +export default createHighlighter; diff --git a/packages/component/src/Styles/StyleSet/TextContent.ts b/packages/component/src/Styles/StyleSet/TextContent.ts index 6985804c85..3322ecb340 100644 --- a/packages/component/src/Styles/StyleSet/TextContent.ts +++ b/packages/component/src/Styles/StyleSet/TextContent.ts @@ -28,8 +28,14 @@ export default function createTextContentStyle() { height: '.75em' }, - '& .webchat__text-content__activity-copy-button': { - alignSelf: 'flex-start' + '& .webchat__text-content__activity-actions': { + alignSelf: 'flex-start', + display: 'flex', + gap: `calc(${CSSTokens.PaddingRegular} / 2)` + }, + + '& .webchat__text-content__activity-actions:empty': { + display: 'none' } } }; diff --git a/packages/component/src/Styles/StyleSet/ViewCodeDialog.ts b/packages/component/src/Styles/StyleSet/ViewCodeDialog.ts new file mode 100644 index 0000000000..4846c3ca28 --- /dev/null +++ b/packages/component/src/Styles/StyleSet/ViewCodeDialog.ts @@ -0,0 +1,56 @@ +import CSSTokens from '../CSSTokens'; + +export default function createViewCodeDialogStyle() { + return { + '&.webchat__view-code-dialog': { + boxSizing: 'border-box', + display: 'grid', + height: '100%', + margin: 0, + maxHeight: '100vh', + overflow: 'hidden', + padding: '1rem', + placeItems: 'center', + + '& .webchat__modal-dialog__box': { + display: 'flex', + flexDirection: 'column', + maxHeight: '100%', + maxWidth: '100%', + position: 'relative' + }, + + '& .webchat__modal-dialog__close-button-layout': { + position: 'absolute', + right: 0 + }, + + '& .webchat__modal-dialog__body': { + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + gap: CSSTokens.PaddingRegular + }, + + '& .webchat__view-code-dialog__header': { + display: 'flex', + paddingInlineEnd: '30px' + }, + + '& .webchat__view-code-dialog__title': { + margin: '0 auto 0 0', + textTransform: 'capitalize' + }, + + '& .webchat__view-code-dialog__body': { + lineHeight: '20px', + overflow: 'auto' + }, + + '& .webchat__view-code-dialog__footer': { + color: CSSTokens.ColorSubtle, + lineHeight: '20px' + } + } + }; +} diff --git a/packages/component/src/Styles/createStyleSet.ts b/packages/component/src/Styles/createStyleSet.ts index b6d812c888..09bd7dab8a 100644 --- a/packages/component/src/Styles/createStyleSet.ts +++ b/packages/component/src/Styles/createStyleSet.ts @@ -45,6 +45,7 @@ import createTypingIndicatorStyle from './StyleSet/TypingIndicator'; import createUploadButtonStyle from './StyleSet/UploadButton'; import createVideoAttachmentStyle from './StyleSet/VideoAttachment'; import createVideoContentStyle from './StyleSet/VideoContent'; +import createViewCodeDialogStyle from './StyleSet/ViewCodeDialog'; import createVimeoContentStyle from './StyleSet/VimeoContent'; import createWarningNotificationStyle from './StyleSet/WarningNotification'; import createYouTubeContentStyle from './StyleSet/YouTubeContent'; @@ -108,6 +109,7 @@ export default function createStyleSet(styleOptions: StyleOptions) { sendStatus: createSendStatusStyle(), slottedActivityStatus: createSlottedActivityStatusStyle(), textContent: createTextContentStyle(), - thumbButton: createThumbButtonStyle() + thumbButton: createThumbButtonStyle(), + viewCodeDialog: createViewCodeDialogStyle() } as const); } diff --git a/packages/component/src/testIds.ts b/packages/component/src/testIds.ts index 243a383523..86d87fb503 100644 --- a/packages/component/src/testIds.ts +++ b/packages/component/src/testIds.ts @@ -1,6 +1,7 @@ const testIds = { copyButton: 'copy button', - sendBoxTextBox: 'send box text area' + sendBoxTextBox: 'send box text area', + viewCodeButton: 'view code button' }; export default testIds; diff --git a/packages/core/src/types/external/OrgSchema/CreativeWork.ts b/packages/core/src/types/external/OrgSchema/CreativeWork.ts index 13e957016f..f5f1cc0d74 100644 --- a/packages/core/src/types/external/OrgSchema/CreativeWork.ts +++ b/packages/core/src/types/external/OrgSchema/CreativeWork.ts @@ -35,6 +35,11 @@ export type CreativeWork = Thing & { */ citation?: readonly CreativeWork[] | undefined; + /** + * The schema.org [isBasedOn](https://schema.org/isBasedOn) property provides a resource from which this work is derived or from which it is a modification or adaptation. + */ + isBasedOn?: CreativeWork | undefined; + /** * Keywords or tags used to describe some item. Multiple textual entries in a keywords list are typically delimited by commas, or by repeating the property. * @@ -88,6 +93,7 @@ export const creativeWork = (entries?: TEntries abstract: orgSchemaProperty(string()), author: orgSchemaProperty(union([person(), string()])), citation: orgSchemaProperties(lazy(() => creativeWork())), + isBasedOn: orgSchemaProperty(lazy(() => creativeWork())), keywords: orgSchemaProperties(union([lazy(() => definedTerm()), string()])), pattern: orgSchemaProperty(lazy(() => definedTerm())), text: orgSchemaProperty(string()),