diff --git a/.github/scripts/install-postgres-ubuntu.sh b/.github/scripts/install-postgres-ubuntu.sh index 066551900c3..967a099a08d 100755 --- a/.github/scripts/install-postgres-ubuntu.sh +++ b/.github/scripts/install-postgres-ubuntu.sh @@ -17,7 +17,7 @@ echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" # remove existing postgresql and install desired version sudo apt update sudo apt remove -y postgresql\* -sudo apt install -y "postgresql-$pgversion" +sudo apt install -y "postgresql-$pgversion" "postgresql-$pgversion-pgvector" # add postgresql binaries to path echo "/usr/lib/postgresql/$pgversion/bin" >> $GITHUB_PATH diff --git a/.gitignore b/.gitignore index 8833206f879..18a79e622b8 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ dev deploy build packages/*/dist +packages/shared/src/baml_src/baml_client *.tsbuildinfo packages/*/{src,app}/**/*.d.ts database/docs diff --git a/database/model/ask_ai/conversations.md b/database/model/ask_ai/conversations.md new file mode 100644 index 00000000000..29f2d0aff7a --- /dev/null +++ b/database/model/ask_ai/conversations.md @@ -0,0 +1,27 @@ +{% docs ask_ai__table__conversations %} +Stores Ask AI chatbot conversation threads. Each conversation belongs to a single user and groups a sequence of messages. Conversations support soft-deletion via `deleted_at` so history can be recovered. Not synced between facility and central servers. +{% enddocs %} + +{% docs ask_ai__conversations__id %} +Primary key. UUID string assigned by the application. +{% enddocs %} + +{% docs ask_ai__conversations__user_id %} +Foreign key to `public.users.id`. The user who owns this conversation. +{% enddocs %} + +{% docs ask_ai__conversations__title %} +Optional human-readable title for the conversation. Nullable — not set until the conversation has been given a name. +{% enddocs %} + +{% docs ask_ai__conversations__created_at %} +Timestamp when the conversation was created. +{% enddocs %} + +{% docs ask_ai__conversations__updated_at %} +Timestamp when the conversation was last modified. +{% enddocs %} + +{% docs ask_ai__conversations__deleted_at %} +Soft-delete timestamp. Null when the conversation is active. Set when the conversation is deleted, allowing recovery if needed. +{% enddocs %} diff --git a/database/model/ask_ai/conversations.yml b/database/model/ask_ai/conversations.yml new file mode 100644 index 00000000000..6e3f573981f --- /dev/null +++ b/database/model/ask_ai/conversations.yml @@ -0,0 +1,43 @@ +version: 2 +sources: + - name: ask_ai__tamanu + schema: ask_ai + description: "{{ doc('ask_ai__generic__schema') }}" + tables: + - name: conversations + description: "{{ doc('ask_ai__table__conversations') }}" + config: + tags: [] + meta: + triggers: [] + columns: + - name: id + data_type: character varying(255) + description: "{{ doc('ask_ai__conversations__id') }}" + data_tests: + - unique + - not_null + - name: user_id + data_type: character varying(255) + description: "{{ doc('ask_ai__conversations__user_id') }}" + data_tests: + - not_null + - name: title + data_type: text + description: "{{ doc('ask_ai__conversations__title') }}" + config: + meta: + masking: text + - name: created_at + data_type: timestamp with time zone + description: "{{ doc('ask_ai__conversations__created_at') }}" + data_tests: + - not_null + - name: updated_at + data_type: timestamp with time zone + description: "{{ doc('ask_ai__conversations__updated_at') }}" + data_tests: + - not_null + - name: deleted_at + data_type: timestamp with time zone + description: "{{ doc('ask_ai__conversations__deleted_at') }}" diff --git a/database/model/ask_ai/generic.md b/database/model/ask_ai/generic.md new file mode 100644 index 00000000000..4d8271ba989 --- /dev/null +++ b/database/model/ask_ai/generic.md @@ -0,0 +1,3 @@ +{% docs ask_ai__generic__schema %} +Contains tables for the Ask AI chatbot feature, including conversation history and messages. +{% enddocs %} diff --git a/database/model/ask_ai/messages.md b/database/model/ask_ai/messages.md new file mode 100644 index 00000000000..b40d2df0e74 --- /dev/null +++ b/database/model/ask_ai/messages.md @@ -0,0 +1,27 @@ +{% docs ask_ai__table__messages %} +Stores individual messages within an Ask AI conversation. Each message is either from the user or the assistant, and together they form the conversation history sent to the LLM. Not soft-deleted — messages are permanently retained for the lifetime of the conversation. Not synced between facility and central servers. +{% enddocs %} + +{% docs ask_ai__messages__id %} +Primary key. UUID string assigned by the application. +{% enddocs %} + +{% docs ask_ai__messages__conversation_id %} +Foreign key to `ask_ai.conversations.id`. The conversation this message belongs to. +{% enddocs %} + +{% docs ask_ai__messages__role %} +The sender of the message. Either `user` (a message typed by the logged-in user) or `assistant` (a response generated by the LLM). +{% enddocs %} + +{% docs ask_ai__messages__content %} +The full text content of the message. +{% enddocs %} + +{% docs ask_ai__messages__created_at %} +Timestamp when the message was created. +{% enddocs %} + +{% docs ask_ai__messages__updated_at %} +Timestamp when the message was last modified. +{% enddocs %} diff --git a/database/model/ask_ai/messages.yml b/database/model/ask_ai/messages.yml new file mode 100644 index 00000000000..b10f0457573 --- /dev/null +++ b/database/model/ask_ai/messages.yml @@ -0,0 +1,50 @@ +version: 2 +sources: + - name: ask_ai__tamanu + schema: ask_ai + description: "{{ doc('ask_ai__generic__schema') }}" + tables: + - name: messages + description: "{{ doc('ask_ai__table__messages') }}" + config: + tags: [] + meta: + triggers: [] + columns: + - name: id + data_type: character varying(255) + description: "{{ doc('ask_ai__messages__id') }}" + data_tests: + - unique + - not_null + - name: conversation_id + data_type: character varying(255) + description: "{{ doc('ask_ai__messages__conversation_id') }}" + data_tests: + - not_null + - name: role + data_type: text + description: "{{ doc('ask_ai__messages__role') }}" + data_tests: + - not_null + config: + meta: + masking: text + - name: content + data_type: text + description: "{{ doc('ask_ai__messages__content') }}" + data_tests: + - not_null + config: + meta: + masking: text + - name: created_at + data_type: timestamp with time zone + description: "{{ doc('ask_ai__messages__created_at') }}" + data_tests: + - not_null + - name: updated_at + data_type: timestamp with time zone + description: "{{ doc('ask_ai__messages__updated_at') }}" + data_tests: + - not_null diff --git a/database/model/public/procedures.yml b/database/model/public/procedures.yml index 3cc43b1005c..01bc1446ff1 100644 --- a/database/model/public/procedures.yml +++ b/database/model/public/procedures.yml @@ -69,45 +69,51 @@ sources: description: "{{ doc('procedures__encounter_id') }}" data_tests: - relationships: - to: source('tamanu', 'encounters') - field: id + arguments: + to: source('tamanu', 'encounters') + field: id - name: location_id data_type: character varying(255) description: "{{ doc('procedures__location_id') }}" data_tests: - relationships: - to: source('tamanu', 'locations') - field: id + arguments: + to: source('tamanu', 'locations') + field: id - name: procedure_type_id data_type: character varying(255) description: "{{ doc('procedures__procedure_type_id') }}" data_tests: - dbt_utils.relationships_where: - to: source('tamanu', 'reference_data') - field: id - to_condition: type = 'procedureType' + arguments: + to: source('tamanu', 'reference_data') + field: id + to_condition: type = 'procedureType' - name: anaesthetic_id data_type: character varying(255) description: "{{ doc('procedures__anaesthetic_id') }}" data_tests: - dbt_utils.relationships_where: - to: source('tamanu', 'reference_data') - field: id - to_condition: type = 'drug' + arguments: + to: source('tamanu', 'reference_data') + field: id + to_condition: type = 'drug' - name: physician_id data_type: character varying(255) description: "{{ doc('procedures__physician_id') }}" data_tests: - relationships: - to: source('tamanu', 'users') - field: id + arguments: + to: source('tamanu', 'users') + field: id - name: anaesthetist_id data_type: character varying(255) description: "{{ doc('procedures__anaesthetist_id') }}" data_tests: - relationships: - to: source('tamanu', 'users') - field: id + arguments: + to: source('tamanu', 'users') + field: id - name: start_time data_type: character(19) description: "{{ doc('procedures__start_time') }}" diff --git a/package-lock.json b/package-lock.json index dcf2b5776ad..3d4e6f1615c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4335,6 +4335,160 @@ "integrity": "sha512-7xLM6wg76hnMdLi+mBZe1KcAwav2Q4U4XGSbS9fAHHmRvK0ntQTqm4ryYXrWjSncYIfz2u1mLMvMQQ1+EcgKLA==", "license": "MIT" }, + "node_modules/@boundaryml/baml": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml/-/baml-0.220.0.tgz", + "integrity": "sha512-37l5RltIdFRf3RT1qZiDYEcStZnrU+IzHfg0sPx4T3viWeVy+b6RbGgSw4eiAGL624jjmxl4aB90Uv0EOtr7eQ==", + "license": "MIT", + "dependencies": { + "@scarf/scarf": "^1.3.0" + }, + "bin": { + "baml": "cli.js", + "baml-cli": "cli.js" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@boundaryml/baml-darwin-arm64": "0.220.0", + "@boundaryml/baml-darwin-x64": "0.220.0", + "@boundaryml/baml-linux-arm64-gnu": "0.220.0", + "@boundaryml/baml-linux-arm64-musl": "0.220.0", + "@boundaryml/baml-linux-x64-gnu": "0.220.0", + "@boundaryml/baml-linux-x64-musl": "0.220.0", + "@boundaryml/baml-win32-arm64-msvc": "0.220.0", + "@boundaryml/baml-win32-x64-msvc": "0.220.0" + } + }, + "node_modules/@boundaryml/baml-darwin-arm64": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml-darwin-arm64/-/baml-darwin-arm64-0.220.0.tgz", + "integrity": "sha512-HhiSDwVKcqfA+RU7HeJJMdQSZsMw/sSml90ekSwu9KHvrgu8PXrtuLxlsDQbIHcwTXKHdOGUhHSnpT8wj76RzQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@boundaryml/baml-darwin-x64": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml-darwin-x64/-/baml-darwin-x64-0.220.0.tgz", + "integrity": "sha512-X4Ui/tquXZUB69iowIHconvlXgQnjX7fOJjYmUFfUGANpYU1V3YGd8tRUxtQqEj6N28c2TZKs0VR/6Er3R2GhA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@boundaryml/baml-linux-arm64-gnu": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml-linux-arm64-gnu/-/baml-linux-arm64-gnu-0.220.0.tgz", + "integrity": "sha512-aZZWj/XY0GWvVV5q8f+Kw4U7R4MGEUGTHWruevIsIawKJucvsPaz2EAoHFKEAU2F/9zD1F7+wI8bS2GP5tzd1w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@boundaryml/baml-linux-arm64-musl": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml-linux-arm64-musl/-/baml-linux-arm64-musl-0.220.0.tgz", + "integrity": "sha512-uodxLrVQgXt2IQB8xIcyFwO3Pnrc+mc065bxUqYsm9/+LNXcCzr863GjsDfU5aPrSlzpw2ei5UhfoA3oO0tZzw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@boundaryml/baml-linux-x64-gnu": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml-linux-x64-gnu/-/baml-linux-x64-gnu-0.220.0.tgz", + "integrity": "sha512-kCHHWD54TiuQi3wZG9+UCMcESquuYeQ8tAEm+94Q6daqkPPNdMal2rLw9Dk7OGG1pM8CK+bn2qtJMND8l5HDsA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@boundaryml/baml-linux-x64-musl": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml-linux-x64-musl/-/baml-linux-x64-musl-0.220.0.tgz", + "integrity": "sha512-BhBWFseDAbDhi/+acK0iaOCgNSkmvu63ZTvf2pIcfoiqxT8rPTH/mj7dFdmGMULnBKeBlS6SzCGFUzsOyqqz2w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@boundaryml/baml-win32-arm64-msvc": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml-win32-arm64-msvc/-/baml-win32-arm64-msvc-0.220.0.tgz", + "integrity": "sha512-5+WxZJu026YNfeKApk5j3sDbbQg35owmh9EOpD+KGYHXxpcDkX+hYjeL4KMHtIPlQTMbzEm7UOX60+k6n74OJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@boundaryml/baml-win32-x64-msvc": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@boundaryml/baml-win32-x64-msvc/-/baml-win32-x64-msvc-0.220.0.tgz", + "integrity": "sha512-QnBhcN4A5cskS+2FHrAlzPZnLSatRRmC6lVp0d4U+n0kNy/9A3iKHSbF5ceunzuO4an7YnPrP3zl9nLweLoazA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@bugsnag/browser": { "version": "7.25.0", "resolved": "https://registry.npmjs.org/@bugsnag/browser/-/browser-7.25.0.tgz", @@ -10967,6 +11121,13 @@ "url": "https://github.com/sponsors/sayem314" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -13072,6 +13233,15 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -13114,6 +13284,15 @@ "integrity": "sha512-qkcUlZmX6c4J8q45taBKTL3p+LbITgyx7qhlPYOdOHZB7B31K0mXbP5YA7i7SgDeEGuI9MnumiKPEMrxg8j3KQ==", "license": "MIT" }, + "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", @@ -13631,7 +13810,6 @@ "version": "2.0.11", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "dev": true, "license": "MIT" }, "node_modules/@types/use-sync-external-store": { @@ -14079,7 +14257,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, "license": "ISC" }, "node_modules/@upstash/redis": { @@ -16112,6 +16289,16 @@ "@babel/core": "^7.0.0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -17037,6 +17224,16 @@ "node": ">=12.19" } }, + "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/chai": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", @@ -17136,6 +17333,16 @@ "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": "1.1.4", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", @@ -17589,6 +17796,16 @@ "integrity": "sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==", "license": "Apache-2.0" }, + "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/command-exists": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", @@ -18522,6 +18739,29 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decode-named-character-reference/node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -18844,7 +19084,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -19176,6 +19415,19 @@ "typescript": "^5.4.4" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -20683,6 +20935,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", @@ -22515,6 +22777,52 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "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/helmet": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", @@ -22624,6 +22932,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/htmlparser2": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", @@ -23020,6 +23338,12 @@ "devOptional": true, "license": "ISC" }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/input-otp": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", @@ -26064,6 +26388,16 @@ "integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==", "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -26374,6 +26708,557 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-mdx-expression/node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/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", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-mdx-expression/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", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-mdx-jsx/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/mdast-util-mdx-jsx/node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/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", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-mdx-jsx/node_modules/parse-entities/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/mdast-util-mdx-jsx/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", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-mdxjs-esm/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-mdxjs-esm/node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm/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", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-mdxjs-esm/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", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "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-hast/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-to-markdown/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/mdast-util-to-markdown/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", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-to-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", @@ -27515,6 +28400,413 @@ "parse-entities": "^2.0.0" } }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -30503,6 +31795,16 @@ "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", "license": "MIT" }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/protobufjs": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", @@ -31154,6 +32456,42 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-markdown": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", + "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-markdown/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/react-native": { "version": "0.71.10", "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.71.10.tgz", @@ -31554,7 +32892,7 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/react-native-image-resizer/-/react-native-image-resizer-1.4.5.tgz", "integrity": "sha512-33EgL3C9pyvjKpullAB6fWyD5QhoYEpNNB9rxNvUsrpAnL2mHBW7PTrUCCZudJeB6Weg7nbweKrSw1nnto5aqg==", - "deprecated": "🚨 react-native-image-resizer has moved to @bam.tech/react-native-image-resizer", + "deprecated": "🚨 react-native-image-resizer has moved to @bam.tech/react-native-image-resizer", "license": "MIT", "peerDependencies": { "react-native": ">=v0.40.0" @@ -32599,6 +33937,148 @@ "jsesc": "bin/jsesc" } }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/remark-parse/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/remark-parse/node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse/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", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/remark-parse/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", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/remove-accents": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", @@ -34015,6 +35495,16 @@ "node": ">=0.10.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/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -34596,6 +36086,30 @@ "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/stringify-entities/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/stringify-object": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", @@ -34712,6 +36226,24 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/styled-components": { "version": "5.3.11", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", @@ -35881,6 +37413,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-repeated": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-2.0.0.tgz", @@ -35914,6 +37456,16 @@ "node": ">= 14.0.0" } }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -36917,6 +38469,43 @@ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", "license": "MIT" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unified/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -36955,6 +38544,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "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-position/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/unist-util-stringify-position": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", @@ -36969,6 +38596,47 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "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.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-visit/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -37160,6 +38828,59 @@ "extsprintf": "^1.2.0" } }, + "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.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "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/vfile-message/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/vfile-message/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", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", @@ -38713,6 +40434,16 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "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-client": { "name": "@tamanu/api-client", "version": "2.54.0", @@ -40142,41 +41873,6 @@ "yup": "*" } }, - "packages/mobile/node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "packages/mobile/node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, "packages/mobile/node_modules/dayjs": { "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", @@ -41706,6 +43402,7 @@ "version": "2.54.0", "license": "GPL-3.0-or-later AND BUSL-1.1", "dependencies": { + "@boundaryml/baml": "^0.220.0", "@casl/ability": "^6.8.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.28.0", @@ -41719,6 +43416,8 @@ "date-fns": "^2.25.0", "date-fns-tz": "^1.3.6", "decimal.js": "^10.4.3", + "express": "^4.16.2", + "express-async-handler": "^1.2.0", "hookable": "^5.5.3", "jsbarcode": "^3.12.3", "khmer-unicode-converter": "^0.0.6", @@ -41731,6 +43430,7 @@ "react": "^18.2.0", "semver-compare": "^1.0.0", "semver-diff": "^3.1.1", + "sequelize": "^6.21.3", "shortid": "^2.2.14", "winston": "^3.14.2", "zod": "^4.0.17" @@ -42021,6 +43721,7 @@ "react-dom": "^18.2.0", "react-draggable": "^4.4.6", "react-idle-timer": "=5.4.2", + "react-markdown": "^9.0.1", "react-redux": "8.0.2", "react-refresh": "^0.14.2", "react-router": "^7.13.1", diff --git a/packages/central-server/app/askAi.js b/packages/central-server/app/askAi.js new file mode 100644 index 00000000000..7f954893e1a --- /dev/null +++ b/packages/central-server/app/askAi.js @@ -0,0 +1,4 @@ +import { createAskAiRouter, askAiPublicRouter } from '@tamanu/shared/services/askAiRouter'; + +export { askAiPublicRouter }; +export const askAiRoutes = createAskAiRouter(req => req.settings.getFrontEndSettings()); diff --git a/packages/central-server/app/buildRoutes.js b/packages/central-server/app/buildRoutes.js index 733280f4a7a..d7df8097ef3 100644 --- a/packages/central-server/app/buildRoutes.js +++ b/packages/central-server/app/buildRoutes.js @@ -7,6 +7,7 @@ import { healthRoutes } from './health'; import { integrationRoutes } from './integrations'; import { adminRoutes } from './admin'; import { suggestionsRoutes } from './suggestions'; +import { askAiRoutes } from './askAi'; export const buildRoutes = ctx => { const routes = express.Router(); @@ -18,6 +19,7 @@ export const buildRoutes = ctx => { routes.use('/integration', integrationRoutes); routes.use('/admin', adminRoutes); routes.use('/suggestions', suggestionsRoutes); + routes.use('/ask-ai', askAiRoutes); return routes; }; diff --git a/packages/central-server/app/createApi.js b/packages/central-server/app/createApi.js index 499f7138c51..779dcf12daf 100644 --- a/packages/central-server/app/createApi.js +++ b/packages/central-server/app/createApi.js @@ -11,6 +11,7 @@ import { SERVER_TYPES } from '@tamanu/constants'; import { buildRoutes } from './buildRoutes'; import { authModule } from './auth'; +import { askAiPublicRouter } from './askAi'; import { publicRoutes } from './publicRoutes'; import { patientPortalApi } from './patientPortalApi'; @@ -48,6 +49,8 @@ function api(ctx) { } }), ); + // Pre-auth Ask AI routes (e.g. /status) — must come before authModule + apiRoutes.use('/ask-ai', askAiPublicRouter); apiRoutes.use(authModule); apiRoutes.use(attachAuditUserToDbSession); apiRoutes.use('/translation', translationRoutes); diff --git a/packages/central-server/config/default.json5 b/packages/central-server/config/default.json5 index 8063845aafc..55c74ee5281 100644 --- a/packages/central-server/config/default.json5 +++ b/packages/central-server/config/default.json5 @@ -518,5 +518,12 @@ "tokenDuration": "24h", "loginTokenDurationMinutes": 20, "registerTokenDurationMinutes": 43800 // 1 month - } + }, + askAi: { + enabled: false, // Set to true in local.json5 once API keys are configured + anthropicApiKey: '', // Anthropic API key for LLM calls + voyageApiKey: '', // Voyage AI API key for query embedding + ragNamespace: 'tamanu', // Namespace used when indexing with github-repo-rag + ragDatabaseUrl: '', // PostgreSQL connection URL for the RAG database, e.g. postgresql://user:pass@localhost:5432/tamanu-rag + }, } diff --git a/packages/database/src/migrations/1772000000000-createAskAiSchemas.ts b/packages/database/src/migrations/1772000000000-createAskAiSchemas.ts new file mode 100644 index 00000000000..fb113d8de37 --- /dev/null +++ b/packages/database/src/migrations/1772000000000-createAskAiSchemas.ts @@ -0,0 +1,21 @@ +import { type QueryInterface } from 'sequelize'; + +// Creates the rag and ask_ai schemas and enables the pgvector extension. +// +// The rag schema is populated by the github-repo-rag Python sidecar — Tamanu +// only needs the schema to exist so that queries against it don't fail on +// startup when the feature is disabled. +// +// The ask_ai schema holds conversation history for the Ask AI chatbot. + +export async function up(query: QueryInterface): Promise { + await query.sequelize.query('CREATE EXTENSION IF NOT EXISTS vector'); + await query.createSchema('rag', {}); + await query.createSchema('ask_ai', {}); +} + +export async function down(query: QueryInterface): Promise { + await query.dropSchema('ask_ai', {}); + await query.dropSchema('rag', {}); + // Intentionally not dropping the vector extension — it may be used by other schemas. +} diff --git a/packages/database/src/migrations/1772000000001-createAskAiTables.ts b/packages/database/src/migrations/1772000000001-createAskAiTables.ts new file mode 100644 index 00000000000..6290452fa42 --- /dev/null +++ b/packages/database/src/migrations/1772000000001-createAskAiTables.ts @@ -0,0 +1,87 @@ +import { DataTypes, type QueryInterface } from 'sequelize'; + +// Creates conversation history tables for the Ask AI chatbot in the ask_ai schema. +// These tables are not synced between facility and central servers. + +const CONVERSATIONS = { tableName: 'conversations', schema: 'ask_ai' }; +const MESSAGES = { tableName: 'messages', schema: 'ask_ai' }; + +export async function up(query: QueryInterface): Promise { + await query.createTable(CONVERSATIONS, { + id: { + type: DataTypes.STRING, + primaryKey: true, + allowNull: false, + }, + user_id: { + type: DataTypes.STRING, + allowNull: false, + references: { model: { tableName: 'users', schema: 'public' }, key: 'id' }, + }, + title: { + type: DataTypes.TEXT, + allowNull: true, + }, + created_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + deleted_at: { + type: DataTypes.DATE, + allowNull: true, + }, + }); + + await query.createTable(MESSAGES, { + id: { + type: DataTypes.STRING, + primaryKey: true, + allowNull: false, + }, + conversation_id: { + type: DataTypes.STRING, + allowNull: false, + references: { model: CONVERSATIONS, key: 'id' }, + }, + role: { + type: DataTypes.TEXT, + allowNull: false, + comment: 'user | assistant', + }, + content: { + type: DataTypes.TEXT, + allowNull: false, + }, + created_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + }); + + await query.addIndex(CONVERSATIONS, ['user_id'], { + name: 'idx_ask_ai_conversations_user_id', + }); + + await query.addIndex(MESSAGES, ['conversation_id'], { + name: 'idx_ask_ai_messages_conversation_id', + }); +} + +export async function down(query: QueryInterface): Promise { + await query.removeIndex(MESSAGES, 'idx_ask_ai_messages_conversation_id'); + await query.removeIndex(CONVERSATIONS, 'idx_ask_ai_conversations_user_id'); + await query.dropTable(MESSAGES); + await query.dropTable(CONVERSATIONS); +} diff --git a/packages/database/src/models/AskAiConversation.ts b/packages/database/src/models/AskAiConversation.ts new file mode 100644 index 00000000000..e0b6313b277 --- /dev/null +++ b/packages/database/src/models/AskAiConversation.ts @@ -0,0 +1,50 @@ +import { DataTypes } from 'sequelize'; + +import { SYNC_DIRECTIONS } from '@tamanu/constants'; + +import { Model } from './Model'; +import type { InitOptions, Models } from '../types/model'; + +export class AskAiConversation extends Model { + declare id: string; + declare userId: string; + declare title: string | null; + + static initModel({ primaryKey, ...options }: InitOptions) { + super.init( + { + id: primaryKey, + userId: { + type: DataTypes.STRING, + allowNull: false, + references: { + model: 'users', + key: 'id', + }, + }, + title: { + type: DataTypes.TEXT, + allowNull: true, + }, + }, + { + ...options, + tableName: 'conversations', + schema: 'ask_ai', + syncDirection: SYNC_DIRECTIONS.DO_NOT_SYNC, + }, + ); + } + + static initRelations(models: Models) { + this.belongsTo(models.User, { + foreignKey: 'userId', + as: 'user', + }); + + this.hasMany(models.AskAiMessage, { + foreignKey: 'conversationId', + as: 'messages', + }); + } +} diff --git a/packages/database/src/models/AskAiMessage.ts b/packages/database/src/models/AskAiMessage.ts new file mode 100644 index 00000000000..759da998d56 --- /dev/null +++ b/packages/database/src/models/AskAiMessage.ts @@ -0,0 +1,52 @@ +import { DataTypes } from 'sequelize'; + +import { SYNC_DIRECTIONS } from '@tamanu/constants'; + +import { Model } from './Model'; +import type { InitOptions, Models } from '../types/model'; + +export class AskAiMessage extends Model { + declare id: string; + declare conversationId: string; + declare role: 'user' | 'assistant'; + declare content: string; + + static initModel({ primaryKey, ...options }: InitOptions) { + super.init( + { + id: primaryKey, + conversationId: { + type: DataTypes.STRING, + allowNull: false, + references: { + model: { tableName: 'conversations', schema: 'ask_ai' }, + key: 'id', + }, + }, + role: { + type: DataTypes.TEXT, + allowNull: false, + comment: 'user | assistant', + }, + content: { + type: DataTypes.TEXT, + allowNull: false, + }, + }, + { + ...options, + tableName: 'messages', + schema: 'ask_ai', + syncDirection: SYNC_DIRECTIONS.DO_NOT_SYNC, + paranoid: false, + }, + ); + } + + static initRelations(models: Models) { + this.belongsTo(models.AskAiConversation, { + foreignKey: 'conversationId', + as: 'conversation', + }); + } +} diff --git a/packages/database/src/models/index.ts b/packages/database/src/models/index.ts index 9ea97c9b844..df20275ff2d 100644 --- a/packages/database/src/models/index.ts +++ b/packages/database/src/models/index.ts @@ -158,3 +158,5 @@ export * from './LocationAssignmentTemplate'; export * from './LocationAssignment'; export * from './DHIS2PushLog'; export * from './MSupplyPushLog'; +export * from './AskAiConversation'; +export * from './AskAiMessage'; diff --git a/packages/facility-server/app/middleware/auth.js b/packages/facility-server/app/middleware/auth.js index 8776db24306..fe6644da9db 100644 --- a/packages/facility-server/app/middleware/auth.js +++ b/packages/facility-server/app/middleware/auth.js @@ -91,7 +91,9 @@ export async function centralServerLogin({ }); // we've logged in as a valid central user - update local database to match - const { user, localisation, allowedFacilities, primaryTimeZone } = response; + const { user, localisation, allowedFacilities, primaryTimeZone: centralPrimaryTimeZone } = response; + // Fall back to local config if central server doesn't supply a timezone + const primaryTimeZone = centralPrimaryTimeZone ?? getPrimaryTimeZone(config); const { id, ...userDetails } = user; const userModel = await models.User.sequelize.transaction(async () => { diff --git a/packages/facility-server/app/routes/apiv1/askAi.js b/packages/facility-server/app/routes/apiv1/askAi.js new file mode 100644 index 00000000000..8c98102825d --- /dev/null +++ b/packages/facility-server/app/routes/apiv1/askAi.js @@ -0,0 +1,7 @@ +import { createAskAiRouter, askAiPublicRouter } from '@tamanu/shared/services/askAiRouter'; + +export { askAiPublicRouter }; + +export const askAi = createAskAiRouter(req => + req.settings[req.facilityId]?.getFrontEndSettings(), +); diff --git a/packages/facility-server/app/routes/apiv1/index.js b/packages/facility-server/app/routes/apiv1/index.js index f801287e7f2..16a1f7b0a29 100644 --- a/packages/facility-server/app/routes/apiv1/index.js +++ b/packages/facility-server/app/routes/apiv1/index.js @@ -15,6 +15,7 @@ import asyncHandler from 'express-async-handler'; import { keyBy, mapValues } from 'lodash'; import { allergy } from './allergy'; +import { askAi, askAiPublicRouter } from './askAi'; import { appointments } from './appointments'; import { asset } from './asset'; import { attachment } from './attachment'; @@ -110,6 +111,9 @@ apiv1.get( }), ); +// Pre-auth Ask AI routes (e.g. /status) — must come before authMiddleware +apiv1.use('/ask-ai', askAiPublicRouter); + apiv1.use(authMiddleware); apiv1.use(constructPermission); @@ -131,6 +135,7 @@ apiv1.use(patientDataRoutes); // see below for specifics apiv1.use(referenceDataRoutes); // see below for specifics apiv1.use(syncRoutes); // see below for specifics apiv1.use('/telegram', telegramRoutes); +apiv1.use('/ask-ai', askAi); // patient data endpoints patientDataRoutes.use('/allergy', allergy); diff --git a/packages/facility-server/config/default.json5 b/packages/facility-server/config/default.json5 index 04ada9e4f94..cf3f87af9a0 100644 --- a/packages/facility-server/config/default.json5 +++ b/packages/facility-server/config/default.json5 @@ -172,4 +172,11 @@ "medicationAdministrationRecord": { "upcomingRecordsShouldBeGeneratedTimeFrame": 72, // hours }, + askAi: { + enabled: false, // Set to true in local.json5 once API keys are configured + anthropicApiKey: '', // Anthropic API key for LLM calls + voyageApiKey: '', // Voyage AI API key for query embedding + ragNamespace: 'tamanu', // Namespace used when indexing with github-repo-rag + ragDatabaseUrl: '', // PostgreSQL connection URL for the RAG database, e.g. postgresql://user:pass@localhost:5432/tamanu-rag + }, } diff --git a/packages/shared/package.json b/packages/shared/package.json index 3a6e900b698..3dc3af5055b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -38,6 +38,14 @@ "require": "./dist/cjs/schemas/facility/requests/*.js", "import": "./dist/esm/schemas/facility/requests/*.js" }, + "./services/AskAiService": { + "require": "./dist/cjs/services/AskAiService.js", + "import": "./dist/esm/services/AskAiService.js" + }, + "./services/askAiRouter": { + "require": "./dist/cjs/services/askAiRouter.js", + "import": "./dist/esm/services/askAiRouter.js" + }, "./sync/*": { "require": "./dist/cjs/sync/*.js", "import": "./dist/esm/sync/*.js" @@ -118,7 +126,8 @@ "author": "Beyond Essential Systems Pty. Ltd.", "license": "GPL-3.0-or-later AND BUSL-1.1", "scripts": { - "build": "npm run clean && npm run build:esm && npm run build:cjs && npm run build:types", + "build": "npm run clean && npm run baml:generate && npm run build:esm && npm run build:cjs && npm run build:types", + "baml:generate": "baml-cli generate --from src/baml_src", "build:dev": "npm run build", "build:dev:watch": "npm run clean && concurrently \"npm run build:esm -- --watch\" \"npm run build:cjs -- --watch\"", "build:esm": "swc --out-dir dist/esm --copy-files --source-maps true src", @@ -136,6 +145,7 @@ "rimraf": "^6.0.1" }, "dependencies": { + "@boundaryml/baml": "^0.220.0", "@casl/ability": "^6.8.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.28.0", @@ -146,6 +156,8 @@ "calculate-luhn-mod-n": "^2.0.12", "chance": "^1.1.8", "config": "^3.3.9", + "express": "^4.16.2", + "express-async-handler": "^1.2.0", "date-fns": "^2.25.0", "date-fns-tz": "^1.3.6", "decimal.js": "^10.4.3", @@ -158,6 +170,7 @@ "multiparty": "^4.2.3", "node-schedule": "^2.0.0", "pg-notify": "^1.0.5", + "sequelize": "^6.21.3", "react": "^18.2.0", "semver-compare": "^1.0.0", "semver-diff": "^3.1.1", diff --git a/packages/shared/src/baml_src/ask-ai.baml b/packages/shared/src/baml_src/ask-ai.baml new file mode 100644 index 00000000000..6d5a6d67722 --- /dev/null +++ b/packages/shared/src/baml_src/ask-ai.baml @@ -0,0 +1,67 @@ +generator target { + output_type "typescript" + output_dir "." + version "0.220.0" +} + +client AnthropicClient { + provider anthropic + options { + model "claude-sonnet-4-6" + api_key env.ANTHROPIC_API_KEY + } +} + +class AskAiResponse { + answer string @description("The answer to the user's question. Empty when cannotAnswer is true or clarifyingQuestion is non-empty.") + cannotAnswer bool @description("True only if the provided documentation contained no useful information to answer the question.") + clarifyingQuestion string @description("A question to ask the user when their question is ambiguous or needs more context. Empty string if not needed.") +} + +function AskTamanu( + userQuestion: string, + conversationHistory: string, + appSettings: string, + ragContext: string, +) -> AskAiResponse { + client AnthropicClient + prompt #" + You are the Tamanu Assistant — a helpful, calm, and knowledgeable guide for clinical staff + and administrators using the Tamanu healthcare management system. + + Use plain language. Be concise. Clinical staff are busy; get to the point. + Be honest about the limits of your knowledge. + + Tamanu has two server types: the facility server (used at individual health facilities) + and the central server (used for system-wide administration and reporting). + If a feature is only available on one server type, say so clearly. + + {% if appSettings %} + This server's current application settings: + {{ appSettings }} + {% endif %} + + {% if ragContext %} + Relevant documentation found: + {{ ragContext }} + {% endif %} + + How to respond: + 1. If the question is unclear or ambiguous and you cannot give a useful answer even with the + documentation above, set clarifyingQuestion to ask what you need, and leave answer empty. + 2. Answer based on the documentation provided above. + 3. If the documentation does not contain enough information to answer, set cannotAnswer to true. + Do not invent features or workflows not found in the documentation. + + If your answer may not fully resolve the issue, or the user appears to need hands-on + support with their system, end your answer with: + "If you need further support, visit https://bes-support.zendesk.com/hc/en-us/" + + Conversation so far: + {{ conversationHistory }} + + User question: {{ userQuestion }} + + {{ ctx.output_format }} + "# +} diff --git a/packages/shared/src/services/AskAiService.ts b/packages/shared/src/services/AskAiService.ts new file mode 100644 index 00000000000..45bc430a79d --- /dev/null +++ b/packages/shared/src/services/AskAiService.ts @@ -0,0 +1,257 @@ +import { Sequelize } from 'sequelize'; + +const RAG_TOP_K = 10; +const CONVERSATION_HISTORY_LIMIT = 20; + +interface RagSource { + filePath: string; + excerpt: string; +} + +interface SearchRagResult { + chunks: string; + sources: RagSource[]; +} + +interface ChatParams { + conversationId: string; + userMessage: string; + ragDatabaseUrl: string; + models: Record; + voyageApiKey: string; + anthropicApiKey: string; + appSettings?: string; +} + +interface ChatResponse { + answer: string; + cannotAnswer: boolean; + clarifyingQuestion: string; + sources: RagSource[]; +} + +// Cache Sequelize connection pools keyed by RAG database URL. +// Capped at MAX_RAG_DB_CACHE_SIZE: if a new URL arrives when the cache is full +// the oldest entry is closed and evicted first, preventing pool leaks if the URL +// ever rotates or varies per request. +const MAX_RAG_DB_CACHE_SIZE = 10; +const ragDbCache = new Map(); + +function getRagDb(url: string): Sequelize { + if (!ragDbCache.has(url)) { + if (ragDbCache.size >= MAX_RAG_DB_CACHE_SIZE) { + const oldest = ragDbCache.entries().next().value as [string, Sequelize] | undefined; + if (oldest) { + const [oldestUrl, oldestDb] = oldest; + oldestDb.close().catch(() => {}); + ragDbCache.delete(oldestUrl); + } + } + ragDbCache.set( + url, + new Sequelize(url, { + logging: false, + pool: { + max: 2, + min: 0, + idle: 30_000, + acquire: 30_000, + evict: 60_000, + // Test the connection before handing it to a caller — discards dead + // connections that survive after a database restart + validate: (connection: any) => + Boolean(connection && !connection.connection?.stream?.destroyed), + }, + dialectOptions: { + // TCP keepalives so the OS detects dead connections without waiting + // for a query to time out + keepAlive: true, + }, + }), + ); + } + return ragDbCache.get(url)!; +} + +async function embedQuery(query: string, voyageApiKey: string): Promise { + const response = await fetch('https://api.voyageai.com/v1/embeddings', { + method: 'POST', + headers: { + Authorization: `Bearer ${voyageApiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + input: [query], + model: 'voyage-code-3', + output_dimension: 1024, + }), + }); + + if (!response.ok) { + throw new Error(`Voyage AI embedding failed: ${response.status} ${response.statusText}`); + } + + const data = (await response.json()) as { data: Array<{ embedding: number[] }> }; + return data.data[0].embedding; +} + +async function searchRag( + query: string, + ragDatabaseUrl: string, + voyageApiKey: string, +): Promise { + const embedding = await embedQuery(query, voyageApiKey); + // Pass the vector as a bind parameter ($embedding) so Postgres receives it + // out-of-band and can cache the query plan. Interpolating 1024 floats inline + // produces a ~10 KB literal repeated 4 times (~40 KB per request) that must + // be re-parsed on every call. + const embeddingVector = `[${embedding.join(',')}]`; + const db = getRagDb(ragDatabaseUrl); + + // Hybrid search: vector (cosine) + full-text, combined with Reciprocal Rank Fusion. + // + // Each source CTE ranks its own results before the join. The final step is a + // UNION ALL of both ranked sets followed by GROUP BY dedup — this avoids the + // cross-product blowup that a FULL OUTER JOIN on (file_path, text) would cause + // when the two sources return different chunks. + // + // Note: the FTS clauses use inline to_tsvector. The github-repo-rag sidecar + // creates a GIN index on to_tsvector('english', text) for each table + // ({table}_fts_idx), so these WHERE clauses benefit from index scans. + const sql = ` + WITH vector_ranked AS ( + SELECT file_path, text, + ROW_NUMBER() OVER (ORDER BY distance) AS rank + FROM ( + (SELECT file_path, text, embedding <=> $embedding::vector AS distance + FROM tamanu_code + WHERE embedding IS NOT NULL + ORDER BY distance + LIMIT ${RAG_TOP_K * 2}) + UNION ALL + (SELECT file_path, text, embedding <=> $embedding::vector AS distance + FROM tamanu_docs + WHERE embedding IS NOT NULL + ORDER BY distance + LIMIT ${RAG_TOP_K * 2}) + ) v + ), + fts_ranked AS ( + SELECT file_path, text, + ROW_NUMBER() OVER (ORDER BY score DESC) AS rank + FROM ( + (SELECT file_path, text, + ts_rank(to_tsvector('english', text), plainto_tsquery('english', $query)) AS score + FROM tamanu_code + WHERE to_tsvector('english', text) @@ plainto_tsquery('english', $query) + LIMIT ${RAG_TOP_K * 2}) + UNION ALL + (SELECT file_path, text, + ts_rank(to_tsvector('english', text), plainto_tsquery('english', $query)) AS score + FROM tamanu_docs + WHERE to_tsvector('english', text) @@ plainto_tsquery('english', $query) + LIMIT ${RAG_TOP_K * 2}) + ) f + ), + rrf AS ( + SELECT file_path, text, 1.0 / (60 + rank) AS rrf_score FROM vector_ranked + UNION ALL + SELECT file_path, text, 1.0 / (60 + rank) AS rrf_score FROM fts_ranked + ) + SELECT file_path, text + FROM rrf + GROUP BY file_path, text + ORDER BY SUM(rrf_score) DESC + LIMIT ${RAG_TOP_K}; + `; + + let rows: Array<{ file_path: string; text: string }>; + try { + [rows] = (await db.query(sql, { bind: { embedding: embeddingVector, query } })) as [ + Array<{ file_path: string; text: string }>, + unknown, + ]; + } catch (err: any) { + // RAG tables not yet indexed — return empty context so the LLM can still answer + if (err?.message?.includes('does not exist')) { + return { chunks: '', sources: [] }; + } + throw err; + } + + if (rows.length === 0) { + return { chunks: '', sources: [] }; + } + + const sources: RagSource[] = rows.map(row => ({ + filePath: row.file_path, + excerpt: row.text.slice(0, 200), + })); + + const chunks = rows.map(row => `[${row.file_path}]\n${row.text}`).join('\n\n---\n\n'); + + return { chunks, sources }; +} + +export async function chat({ + conversationId, + userMessage, + ragDatabaseUrl, + models, + voyageApiKey, + anthropicApiKey, + appSettings, +}: ChatParams): Promise { + // Load recent conversation history + const recentMessages = await models.AskAiMessage.findAll({ + where: { conversationId }, + order: [['createdAt', 'ASC']], + limit: CONVERSATION_HISTORY_LIMIT, + }); + + const conversationHistory = recentMessages + .map((m: any) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`) + .join('\n'); + + // Search RAG before calling the LLM so it has relevant documentation to answer from + const { chunks, sources: collectedSources } = await searchRag( + userMessage, + ragDatabaseUrl, + voyageApiKey, + ); + + const { b } = await import('../baml_src/baml_client/index'); + const response = await b.AskTamanu( + userMessage, + conversationHistory, + appSettings ?? '', + chunks, + { env: { ANTHROPIC_API_KEY: anthropicApiKey } }, + ); + + // Sources from the tamanu codebase namespace are file paths — not meaningful for end users + const result: ChatResponse = { + answer: response.answer, + cannotAnswer: response.cannotAnswer, + clarifyingQuestion: response.clarifyingQuestion, + sources: collectedSources ?? [], + }; + + // Persist messages — always save the user message; save assistant response + // regardless of whether it's an answer or a clarifying question + const assistantContent = response.clarifyingQuestion || response.answer; + await models.AskAiMessage.create({ + conversationId, + role: 'user', + content: userMessage, + }); + if (assistantContent) { + await models.AskAiMessage.create({ + conversationId, + role: 'assistant', + content: assistantContent, + }); + } + + return result; +} diff --git a/packages/shared/src/services/askAiRouter.js b/packages/shared/src/services/askAiRouter.js new file mode 100644 index 00000000000..a4cf052669a --- /dev/null +++ b/packages/shared/src/services/askAiRouter.js @@ -0,0 +1,158 @@ +import express from 'express'; +import asyncHandler from 'express-async-handler'; +import config from 'config'; +import { NotFoundError } from '@tamanu/errors'; +import { chat } from './AskAiService.js'; + +/** + * Creates the Ask AI Express router. + * + * @param {(req: import('express').Request) => Promise} getAppSettings + * Called in the POST messages handler to retrieve the front-end settings for + * the current server/facility. Facility server passes + * `req => req.settings[req.facilityId]?.getFrontEndSettings()`; + * central server passes `req => req.settings.getFrontEndSettings()`. + */ +// Pre-auth router — mounted before authMiddleware/authModule in both servers so +// the UI can discover whether the feature is on without a valid token. +export const askAiPublicRouter = express.Router(); +askAiPublicRouter.get( + '/status', + asyncHandler(async (req, res) => { + req.flagPermissionChecked(); + res.send({ enabled: Boolean(config.get('askAi').enabled) }); + }), +); + +export const createAskAiRouter = getAppSettings => { + const router = express.Router(); + + router.use( + asyncHandler(async (req, res, next) => { + if (!config.get('askAi').enabled) { + req.flagPermissionChecked(); + res.status(503).json({ error: 'Ask AI is not enabled on this server' }); + return; + } + next(); + }), + ); + + // POST /ask-ai/conversations + router.post( + '/conversations', + asyncHandler(async (req, res) => { + req.flagPermissionChecked(); + const { models } = req; + const { title } = req.body; + + const conversation = await models.AskAiConversation.create({ + userId: req.user.id, + title: title ?? 'New conversation', + }); + + res.send(conversation); + }), + ); + + // GET /ask-ai/conversations?limit=50&offset=0 + router.get( + '/conversations', + asyncHandler(async (req, res) => { + req.flagPermissionChecked(); + const { models } = req; + + const limit = Math.min(parseInt(req.query.limit ?? '50', 10), 100); + const offset = parseInt(req.query.offset ?? '0', 10); + + const { rows: conversations, count } = await models.AskAiConversation.findAndCountAll({ + where: { userId: req.user.id }, + order: [['createdAt', 'DESC']], + limit, + offset, + }); + + res.send({ data: conversations, count }); + }), + ); + + // GET /ask-ai/conversations/:id?limit=50&offset=0 + router.get( + '/conversations/:id', + asyncHandler(async (req, res) => { + req.flagPermissionChecked(); + const { models } = req; + + const conversation = await models.AskAiConversation.findOne({ + where: { id: req.params.id, userId: req.user.id }, + }); + + if (!conversation) throw new NotFoundError(); + + const limit = Math.min(parseInt(req.query.limit ?? '50', 10), 100); + const offset = parseInt(req.query.offset ?? '0', 10); + + const { rows: messages, count } = await models.AskAiMessage.findAndCountAll({ + where: { conversationId: conversation.id }, + order: [['createdAt', 'ASC']], + limit, + offset, + }); + + res.send({ ...conversation.toJSON(), messages, messageCount: count }); + }), + ); + + // POST /ask-ai/conversations/:id/messages + router.post( + '/conversations/:id/messages', + asyncHandler(async (req, res) => { + req.flagPermissionChecked(); + const { models } = req; + const askAiConfig = config.get('askAi'); + + const conversation = await models.AskAiConversation.findOne({ + where: { id: req.params.id, userId: req.user.id }, + }); + + if (!conversation) throw new NotFoundError(); + + const { content } = req.body; + + const appSettings = JSON.stringify((await getAppSettings(req)) ?? {}, null, 2); + + const response = await chat({ + conversationId: conversation.id, + userMessage: content, + ragDatabaseUrl: askAiConfig.ragDatabaseUrl, + models, + voyageApiKey: askAiConfig.voyageApiKey, + anthropicApiKey: askAiConfig.anthropicApiKey, + appSettings, + }); + + res.send(response); + }), + ); + + // DELETE /ask-ai/conversations/:id + router.delete( + '/conversations/:id', + asyncHandler(async (req, res) => { + req.flagPermissionChecked(); + const { models } = req; + + const conversation = await models.AskAiConversation.findOne({ + where: { id: req.params.id, userId: req.user.id }, + }); + + if (!conversation) throw new NotFoundError(); + + await conversation.destroy(); + + res.send({}); + }), + ); + + return router; +}; diff --git a/packages/web/app/App.jsx b/packages/web/app/App.jsx index d1e2221ff72..f0f0765e087 100644 --- a/packages/web/app/App.jsx +++ b/packages/web/app/App.jsx @@ -1,7 +1,10 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; import Bowser from 'bowser'; +import { IconButton } from '@material-ui/core'; +import ChatBubbleOutline from '@mui/icons-material/ChatBubbleOutline'; +import { AskAiPanel } from './components/AskAi'; import 'typeface-roboto'; import { Colors } from './constants'; import { checkIsLoggedIn, checkIsFacilitySelected, getServerType } from './store/auth'; @@ -18,6 +21,7 @@ import { SingleTabStatusPage, } from './components/StatusPage'; import { useCheckServerAliveQuery } from './api/queries/useCheckServerAliveQuery'; +import { useAskAiStatusQuery } from './api/queries/useAskAiStatusQuery'; import { useSingleTab } from './utils/singleTab'; import { SERVER_TYPES } from '@tamanu/constants'; @@ -33,10 +37,30 @@ const AppContentsContainer = styled.div` border-top: 1px solid ${Colors.softOutline}; `; +const AskAiFab = styled(IconButton)` + position: fixed; + bottom: 24px; + right: 24px; + width: 48px; + height: 48px; + background: ${Colors.primaryDark}; + color: white; + z-index: 1200; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3); + + &:hover { + background: #4e5f71; + } +`; + export function App({ sidebar, children }) { + const [askAiOpen, setAskAiOpen] = useState(false); const { data: isServerAlive, isLoading } = useCheckServerAliveQuery(); const isUserLoggedIn = useSelector(checkIsLoggedIn); const isFacilitySelected = useSelector(checkIsFacilitySelected); + const isFacilityReady = isUserLoggedIn && isFacilitySelected; + const { data: askAiStatus } = useAskAiStatusQuery({ enabled: isFacilityReady }); + const isAskAiEnabled = Boolean(askAiStatus?.enabled); const location = useLocation(); const serverType = useSelector(getServerType); const isPrimaryTab = useSingleTab(); @@ -76,6 +100,14 @@ export function App({ sidebar, children }) { + {isAskAiEnabled && ( + <> + setAskAiOpen(o => !o)} title="Chat"> + + + setAskAiOpen(false)} /> + + )} ); } diff --git a/packages/web/app/api/queries/useAskAiStatusQuery.js b/packages/web/app/api/queries/useAskAiStatusQuery.js new file mode 100644 index 00000000000..4c2f54f1449 --- /dev/null +++ b/packages/web/app/api/queries/useAskAiStatusQuery.js @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query'; +import { useApi } from '../useApi'; + +export const useAskAiStatusQuery = ({ enabled }) => { + const api = useApi(); + + return useQuery(['askAiStatus'], () => api.get('ask-ai/status'), { + enabled, + // Status is stable for the lifetime of the session — no need to refetch + staleTime: Infinity, + }); +}; diff --git a/packages/web/app/components/AskAi/AskAiPanel.jsx b/packages/web/app/components/AskAi/AskAiPanel.jsx new file mode 100644 index 00000000000..b6ca85983ea --- /dev/null +++ b/packages/web/app/components/AskAi/AskAiPanel.jsx @@ -0,0 +1,423 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; +import { CircularProgress, IconButton, TextField, Typography } from '@material-ui/core'; +import Close from '@mui/icons-material/Close'; +import Remove from '@mui/icons-material/Remove'; +import Send from '@mui/icons-material/Send'; +import Draggable from 'react-draggable'; +import { Resizable } from 're-resizable'; +import Markdown from 'react-markdown'; +import { Colors } from '../../constants'; +import { useApi } from '../../api'; +import { TranslatedText } from '../Translation/TranslatedText'; +import { useTranslation } from '../../contexts/Translation'; + +const OuterContainer = styled.div` + position: fixed; + bottom: 80px; + right: 24px; + z-index: 1300; + display: flex; + flex-direction: column; + filter: drop-shadow(0 4px 20px rgba(0, 0, 0, 0.15)); + border: 1px solid ${Colors.softOutline}; + border-radius: 8px; +`; + +const Header = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + background: ${Colors.white}; + border-radius: ${props => (props.$minimised ? '8px' : '8px 8px 0 0')}; + border-bottom: ${props => (props.$minimised ? 'none' : `1px solid ${Colors.softOutline}`)}; + flex-shrink: 0; + cursor: grab; + user-select: none; + + &:active { + cursor: grabbing; + } +`; + +const HeaderTitle = styled(Typography)` + color: ${Colors.darkestText}; + font-weight: 600; + font-size: 14px; +`; + +const HeaderButtons = styled.div` + display: flex; + gap: 2px; +`; + +const HeaderButton = styled(IconButton)` + padding: 4px; + color: ${Colors.midText}; + &:hover { + background-color: ${Colors.background}; + } +`; + +const PanelBody = styled.div` + display: flex; + flex-direction: column; + background: ${Colors.white}; + border-radius: 0 0 8px 8px; + overflow: hidden; + height: 100%; +`; + +const MessageList = styled.div` + flex: 1; + overflow-y: auto; + padding: 14px; + display: flex; + flex-direction: column; + gap: 10px; +`; + +const MessageBubble = styled.div` + max-width: 88%; + padding: 8px 12px; + border-radius: ${props => (props.$role === 'user' ? '12px 12px 2px 12px' : '12px 12px 12px 2px')}; + background: ${props => (props.$role === 'user' ? Colors.primary10 : Colors.background)}; + align-self: ${props => (props.$role === 'user' ? 'flex-end' : 'flex-start')}; + color: ${Colors.darkestText}; + font-size: 13px; + line-height: 1.5; + word-break: break-word; + border: 1px solid ${props => (props.$role === 'user' ? Colors.softOutline : Colors.outline)}; + + p { margin: 0 0 6px 0; } + p:last-child { margin-bottom: 0; } + ul, ol { margin: 4px 0; padding-left: 18px; } + li { margin: 2px 0; } + code { background: rgba(0,0,0,0.07); border-radius: 3px; padding: 1px 4px; font-size: 12px; } + pre { background: rgba(0,0,0,0.06); border-radius: 4px; padding: 8px; overflow-x: auto; } + pre code { background: none; padding: 0; } + strong { font-weight: 600; } +`; + +const CannotAnswerBubble = styled(MessageBubble)` + background: #fff8e6; + border: 1px solid #f0d070; + color: #7a5c00; + font-style: italic; +`; + +const SourcesBox = styled.div` + margin-top: 4px; + font-size: 11px; + color: ${Colors.midText}; + align-self: flex-start; +`; + +const DisclaimerNote = styled.div` + font-size: 11px; + color: ${Colors.softText}; + font-style: italic; + align-self: flex-start; + margin-top: 2px; +`; + +const SourceItem = styled.div` + font-family: monospace; + font-size: 11px; +`; + +const LoadingRow = styled.div` + display: flex; + align-items: center; + gap: 8px; + color: ${Colors.midText}; + font-size: 13px; + align-self: flex-start; + padding: 4px 0; +`; + +const InputArea = styled.div` + display: flex; + align-items: flex-end; + gap: 8px; + padding: 10px 12px; + border-top: 1px solid ${Colors.outline}; + flex-shrink: 0; + background: ${Colors.white}; +`; + +const StyledTextField = styled(TextField)` + flex: 1; + & .MuiInputBase-root { + color: ${Colors.darkestText}; + background: ${Colors.background}; + border-radius: 6px; + font-size: 13px; + padding: 8px 12px; + } + & .MuiInputBase-root:hover { + background: ${Colors.softOutline}; + } + & .MuiOutlinedInput-notchedOutline { + border-color: ${Colors.outline}; + } + & .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline { + border-color: ${Colors.softText}; + } + & .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline { + border-color: ${Colors.primary}; + } + & .MuiInputBase-input::placeholder { + color: ${Colors.softText}; + opacity: 1; + } +`; + +const SendButton = styled(IconButton)` + padding: 8px; + color: ${Colors.white}; + background: ${Colors.primary}; + border-radius: 6px; + &:hover { + background: ${Colors.primaryDark}; + } + &:disabled { + color: ${Colors.softText}; + background: ${Colors.softOutline}; + } +`; + +export const AskAiPanel = ({ open, onClose }) => { + const api = useApi(); + const { getTranslation } = useTranslation(); + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [conversationId, setConversationId] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [minimised, setMinimised] = useState(false); + const [size, setSize] = useState({ width: 380, height: 500 }); + const scrollRef = useRef(null); + const nodeRef = useRef(null); + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [messages, isLoading]); + + const handleClose = useCallback(() => { + setMessages([]); + setInput(''); + setConversationId(null); + setIsLoading(false); + setMinimised(false); + onClose(); + }, [onClose]); + + const handleSend = useCallback(async () => { + const trimmed = input.trim(); + if (!trimmed || isLoading) return; + + setMinimised(false); + setInput(''); + setMessages(prev => [...prev, { role: 'user', content: trimmed }]); + setIsLoading(true); + + try { + let activeConversationId = conversationId; + if (!activeConversationId) { + const conv = await api.post('ask-ai/conversations', { title: trimmed.slice(0, 60) }); + activeConversationId = conv.id; + setConversationId(activeConversationId); + } + + const response = await api.post(`ask-ai/conversations/${activeConversationId}/messages`, { + content: trimmed, + }); + + const isClarifying = Boolean(response.clarifyingQuestion); + setMessages(prev => [ + ...prev, + { + role: 'assistant', + content: isClarifying ? response.clarifyingQuestion : response.answer, + sources: response.sources ?? [], + cannotAnswer: !isClarifying && response.cannotAnswer, + }, + ]); + } catch { + setMessages(prev => [ + ...prev, + { + role: 'error', + content: getTranslation('askAi.chat.error', 'Something went wrong. Please try again.'), + }, + ]); + } finally { + setIsLoading(false); + } + }, [api, conversationId, input, isLoading]); + + const handleKeyDown = useCallback( + e => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }, + [handleSend], + ); + + if (!open) return null; + + return ( + + +
+ + + + + setMinimised(m => !m)} + size="small" + title={minimised ? 'Restore' : 'Minimise'} + > + + + + + + +
+ + {!minimised && ( + { + setSize(prev => ({ + width: prev.width + delta.width, + height: prev.height + delta.height, + })); + }} + minWidth={280} + minHeight={200} + maxWidth={700} + maxHeight={800} + enable={{ + top: true, + left: true, + topLeft: true, + topRight: true, + bottomLeft: true, + bottom: false, + right: false, + bottomRight: false, + }} + > + + + {messages.length === 0 && ( +
+ +
+ )} + + {messages.map((msg, index) => { + if (msg.cannotAnswer) { + return ( + + + {' '} + + bes-support.zendesk.com + + . + + + ); + } + + return ( + + + {msg.content} + + {msg.role === 'assistant' && msg.sources?.length > 0 && ( + + + {msg.sources.map((source, i) => ( + {source.filePath} + ))} + + )} + {msg.role === 'assistant' && ( + + + + )} + + ); + })} + + {isLoading && ( + + + + + )} +
+ + + setInput(e.target.value)} + onKeyDown={handleKeyDown} + disabled={isLoading} + inputProps={{ 'data-testid': 'askai-input' }} + /> + + {isLoading ? ( + + ) : ( + + )} + + +
+
+ )} +
+
+ ); +}; diff --git a/packages/web/app/components/AskAi/index.js b/packages/web/app/components/AskAi/index.js new file mode 100644 index 00000000000..87610c9e4d5 --- /dev/null +++ b/packages/web/app/components/AskAi/index.js @@ -0,0 +1 @@ +export { AskAiPanel } from './AskAiPanel'; diff --git a/packages/web/app/components/Sidebar/Sidebar.jsx b/packages/web/app/components/Sidebar/Sidebar.jsx index 04fd83f0372..47da21c881c 100644 --- a/packages/web/app/components/Sidebar/Sidebar.jsx +++ b/packages/web/app/components/Sidebar/Sidebar.jsx @@ -138,6 +138,7 @@ const StyledMetadataBox = styled(Box)` margin-bottom: 5px; `; + const getInitials = string => string .match(/\b(\w)/g) diff --git a/packages/web/package.json b/packages/web/package.json index 88b21420458..18f51c08752 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -87,6 +87,7 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", + "react-markdown": "^9.0.1", "react-draggable": "^4.4.6", "react-idle-timer": "=5.4.2", "react-redux": "8.0.2",