diff --git a/docs/.pages b/docs/.pages index 169d8af..357f74b 100644 --- a/docs/.pages +++ b/docs/.pages @@ -1,6 +1,7 @@ nav: - installation.md - editor-integration.md + - images-integration.md - ai-backends.md - text-splitting.md - contributing.md diff --git a/docs/ai-backends.md b/docs/ai-backends.md index 2ccf947..0f5379a 100644 --- a/docs/ai-backends.md +++ b/docs/ai-backends.md @@ -2,11 +2,11 @@ Wagtail AI can be configured to use different backends to support different AI services. -Currently the only (and default) backend available in Wagtail AI is the ["LLM" backend](#the-llm-backend). +The default backend for text completion available in Wagtail AI is the ["LLM" backend](#the-llm-backend). To enable [image description](../images-integration/), you can use the ["OpenAI" backend](#the-openai-backend). ## The "LLM" backend -This backend uses the ["LLM" library](https://llm.datasette.io/en/stable/) which offers support for many AI services through plugins. +This backend uses the ["LLM" library](https://llm.datasette.io/en/stable/) which offers support for many AI services through plugins. At the moment it only supports [text completion](../editor-integration/). By default, it is configured to use OpenAI's `gpt-3.5-turbo` model. @@ -155,3 +155,40 @@ You can find the "LLM" library specific instructions at: https://llm.datasette.i } } ``` + +## The "OpenAI" backend + +Wagtail AI includes a backend for OpenAI that supports both [text completion](../editor-integration/) and [image description](../images-integration/). + +To use the OpenAI backend, you need an API key, which must be set in the `OPENAI_API_KEY` environment variable. Then, configure it in your Django project settings: + +```python +WAGTAIL_AI = { + "BACKENDS": { + "default": { + "CLASS": "wagtail_ai.ai.openai.OpenAIBackend", + "CONFIG": { + "MODEL_ID": "gpt-4", + }, + }, + }, +} +``` + +### Specifying another OpenAI model + +The OpenAI backend supports the use of custom models. For newer models that are not known to Wagtail AI, you must also specify a token limit: + +```python +WAGTAIL_AI = { + "BACKENDS": { + "vision": { + "CLASS": "wagtail_ai.ai.openai.OpenAIBackend", + "CONFIG": { + "MODEL_ID": "gpt-4-vision-preview", + "TOKEN_LIMIT": 300, + }, + }, + }, +} +``` diff --git a/docs/editor-integration.md b/docs/editor-integration.md index 6c8c5dc..a3b29df 100644 --- a/docs/editor-integration.md +++ b/docs/editor-integration.md @@ -15,3 +15,21 @@ When creating prompts you can provide a label and description to help describe t - 'Append after existing content' - keep your existing content intact and add the response from the AI to the end (useful for completions/suggestions). - 'Replace content' - replace the content in the editor with the response from the AI (useful for corrections, rewrites and translations.) + +### Configuring the AI backend + +By default, the `"default"` model will be used for text operations in the editor. To use a different model, set `TEXT_COMPLETION_BACKEND` to the name of another backend: + +```python +WAGTAIL_AI = { + "BACKENDS": { + "gpt4": { + "CLASS": "wagtail_ai.ai.openai.OpenAIBackend", + "CONFIG": { + "MODEL_ID": "gpt-4", + }, + }, + }, + "TEXT_COMPLETION_BACKEND": "gpt4", +} +``` diff --git a/docs/images-integration.md b/docs/images-integration.md new file mode 100644 index 0000000..f7369d0 --- /dev/null +++ b/docs/images-integration.md @@ -0,0 +1,73 @@ +# Images Integration + +Wagtail AI integrates with the image edit form to provide AI-generated descriptions to images. The integration requires a backend that supports image descriptions, such as [the OpenAI backend](../ai-backends/#the-openai-backend). + +## Configuration + +1. In the Django project settings, configure an AI backend, and a model, that support images. Set `IMAGE_DESCRIPTION_BACKEND` to the name of the backend: + ```python + WAGTAIL_AI = { + "BACKENDS": { + "vision": { + "CLASS": "wagtail_ai.ai.openai.OpenAIBackend", + "CONFIG": { + "MODEL_ID": "gpt-4-vision-preview", + "TOKEN_LIMIT": 300, + }, + }, + }, + "IMAGE_DESCRIPTION_BACKEND": "vision", + } + ``` +2. In the Django project settings, configure a [custom Wagtail image base form](https://docs.wagtail.org/en/stable/reference/settings.html#wagtailimages-image-form-base): + ```python + WAGTAILIMAGES_IMAGE_FORM_BASE = "wagtail_ai.forms.DescribeImageForm" + ``` + +Now, when you upload or edit an image, a magic wand icon should appear next to the _title_ field. Clicking on the icon will invoke the AI backend to generate an image description. + +## Separate backends for text completion and image description + +Multi-modal models are faily new, so you may want to configure two different backends for text completion and image description. The `default` model will be used for text completion: + +```python +WAGTAIL_AI = { + "BACKENDS": { + "default": { + "CLASS": "wagtail_ai.ai.llm.LLMBackend", + "CONFIG": { + "MODEL_ID": "gpt-3.5-turbo", + }, + }, + "vision": { + "CLASS": "wagtail_ai.ai.openai.OpenAIBackend", + "CONFIG": { + "MODEL_ID": "gpt-4-vision-preview", + "TOKEN_LIMIT": 300, + }, + }, + }, + "IMAGE_DESCRIPTION_BACKEND": "vision", +} +``` + +## Custom prompt + +Wagtail AI includes a simple prompt to ask the AI to generate an image description: + +> Describe this image. Make the description suitable for use as an alt-text. + +If you want to use a different prompt, override the `IMAGE_DESCRIPTION_PROMPT` value: + +```python +WAGTAIL_AI = { + "BACKENDS": { + # ... + }, + "IMAGE_DESCRIPTION_PROMPT": "Describe this image in the voice of Sir David Attenborough.", +} +``` + +## Custom form + +Wagtail AI includes an image form that enhances the `title` field with an AI button. If you are using a [custom image model](https://docs.wagtail.org/en/stable/advanced_topics/images/custom_image_model.html), you can provide your own form to target another field. Check out the implementation of `DescribeImageForm` in [`forms.py`](https://github.com/wagtail/wagtail-ai/blob/main/src/wagtail_ai/forms.py), adapt it to your needs, and set it as `WAGTAILIMAGES_IMAGE_FORM_BASE`. diff --git a/docs/index.md b/docs/index.md index b97d86a..7ca6885 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,6 +4,7 @@ Wagtail AI integrates Wagtail with OpenAI's APIs (think ChatGPT) to help you wri Right now, it can: -* Finish what you've started - write some text and tell Wagtail AI to finish it off for you -* Correct your spelling/grammar -* Let you add your own custom prompts +* Finish what you've started - write some text and tell Wagtail AI to finish it off for you. +* Correct your spelling/grammar. +* Generate image descriptions - useful for [image alt text](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/alt). +* Let you add your own custom prompts. diff --git a/package-lock.json b/package-lock.json index 44e17b1..6fe643d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "react-dom": "^16.13.1" }, "devDependencies": { - "@svgr/webpack": "^6.2.1", "@types/draft-js": "^0.11.10", "@types/react": "^16.8.19", "@types/react-dom": "^16.8.19", @@ -34,1716 +33,41 @@ "webpack-cli": "^4.9.1" } }, - "node_modules/@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.0.tgz", - "integrity": "sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.0.tgz", - "integrity": "sha512-PuxUbxcW6ZYe656yL3EAhpy7qXKq0DmYsrJLpbB8XrsCP9Nm+XCg9XFMb5vIDliPD7+U/+M+QJlH17XOcB7eXA==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.21.0", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-module-transforms": "^7.21.0", - "@babel/helpers": "^7.21.0", - "@babel/parser": "^7.21.0", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.0", - "@babel/types": "^7.21.0", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.21.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.1.tgz", - "integrity": "sha512-1lT45bAYlQhFn/BHivJs43AiW2rg3/UbLyShGfF3C0KmHvO5fSghWd5kBJy30kpRRucGzXStvnnCFniCR2kXAA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.21.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", - "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", - "dev": true, - "dependencies": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", - "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", - "lru-cache": "^5.1.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.0.tgz", - "integrity": "sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-member-expression-to-functions": "^7.21.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/helper-split-export-declaration": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.0.tgz", - "integrity": "sha512-N+LaFW/auRSWdx7SHD/HiARwXQju1vXTW4fKr4u5SgBUTm51OKEjKgj+cs00ggW3kEvNqwErnlwuq7Y3xBe4eg==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0-0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", - "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz", - "integrity": "sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.21.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz", - "integrity": "sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.2", - "@babel/types": "^7.21.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", - "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.20.7", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.7", - "@babel/types": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", - "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", - "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", - "dev": true, - "dependencies": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.5", - "@babel/types": "^7.20.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz", - "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==", - "dev": true, - "dependencies": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.0", - "@babel/types": "^7.21.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.2.tgz", - "integrity": "sha512-URpaIJQwEkEC2T9Kn+Ai6Xe/02iNaVCuT/PtoRz3GPVJVDpPd7mLo+VddTbhCRU9TXqW5mSrQfXZyi8kDKOVpQ==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", - "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-proposal-optional-chaining": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", - "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", - "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", - "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz", - "integrity": "sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", - "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", - "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", - "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz", - "integrity": "sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", - "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz", - "integrity": "sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz", - "integrity": "sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-split-export-declaration": "^7.18.6", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz", - "integrity": "sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/template": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.7.tgz", - "integrity": "sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", - "dev": true, - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz", - "integrity": "sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", - "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz", - "integrity": "sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.21.2", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-simple-access": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", - "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", - "dev": true, - "dependencies": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-identifier": "^7.19.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", - "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.20.5", - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", - "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz", - "integrity": "sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-constant-elements": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.20.2.tgz", - "integrity": "sha512-KS/G8YI8uwMGKErLFOHS/ekhqdHhpEloxs43NecQHVgo2QuQSyJhGIY1fL8UGl9wy5ItVwwoUL4YxVqsplGq2g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz", - "integrity": "sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.0.tgz", - "integrity": "sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-jsx": "^7.18.6", - "@babel/types": "^7.21.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz", - "integrity": "sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==", - "dev": true, - "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz", - "integrity": "sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz", - "integrity": "sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "regenerator-transform": "^0.15.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", - "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.21.0.tgz", - "integrity": "sha512-xo///XTPp3mDzTtrqXoBlK9eiAYW3wv9JXglcn/u1bi60RW11dEUxIgA8cbnDhutS1zacjMRmAwxE0gMklLnZg==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-typescript": "^7.20.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", - "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { + "node_modules/@babel/code-frame": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", - "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.20.1", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-async-generator-functions": "^7.20.1", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.18.6", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.20.2", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.18.6", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.20.0", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.18.6", - "@babel/plugin-transform-async-to-generator": "^7.18.6", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.20.2", - "@babel/plugin-transform-classes": "^7.20.2", - "@babel/plugin-transform-computed-properties": "^7.18.9", - "@babel/plugin-transform-destructuring": "^7.20.2", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.18.8", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.19.6", - "@babel/plugin-transform-modules-commonjs": "^7.19.6", - "@babel/plugin-transform-modules-systemjs": "^7.19.6", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.20.1", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.18.6", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.19.0", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.20.2", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "core-js-compat": "^3.25.1", - "semver": "^6.3.0" + "@babel/highlight": "^7.18.6" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/preset-react": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.18.6.tgz", - "integrity": "sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-transform-react-display-name": "^7.18.6", - "@babel/plugin-transform-react-jsx": "^7.18.6", - "@babel/plugin-transform-react-jsx-development": "^7.18.6", - "@babel/plugin-transform-react-pure-annotations": "^7.18.6" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/preset-typescript": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.21.0.tgz", - "integrity": "sha512-myc9mpoVA5m1rF8K8DgLEatOYFDpwC+RkMkjZ0Du6uI62YvDe8uxIEYVs/VCdSJ097nlALiU/yBC7//3nI+hNg==", + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-option": "^7.21.0", - "@babel/plugin-transform-typescript": "^7.21.0" + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "dev": true - }, "node_modules/@babel/runtime": { "version": "7.21.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", @@ -1755,55 +79,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.2.tgz", - "integrity": "sha512-ts5FFU/dSUPS13tv8XiEObDu9K+iagEKME9kAbaP7r0Y9KtZJZ+NGndDvWoRAYNpeWafbpFeki3q9QoMD6gxyw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.21.1", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.2", - "@babel/types": "^7.21.2", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.21.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.2.tgz", - "integrity": "sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@csstools/cascade-layer-name-parser": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.1.tgz", @@ -2371,19 +646,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", @@ -2467,280 +729,23 @@ "node_modules/@nodelib/fs.walk": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", - "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@svgr/babel-plugin-add-jsx-attribute": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz", - "integrity": "sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.5.0.tgz", - "integrity": "sha512-8zYdkym7qNyfXpWvu4yq46k41pyNM9SOstoWhKlm+IfdCE1DdnRKeMUPsWIEO/DEkaWxJ8T9esNdG3QwQ93jBA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-6.5.0.tgz", - "integrity": "sha512-NFdxMq3xA42Kb1UbzCVxplUc0iqSyM9X8kopImvFnB+uSDdzIHOdbs1op8ofAvVRtbg4oZiyRl3fTYeKcOe9Iw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.1.tgz", - "integrity": "sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-svg-dynamic-title": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.1.tgz", - "integrity": "sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-svg-em-dimensions": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.1.tgz", - "integrity": "sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-transform-react-native-svg": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.1.tgz", - "integrity": "sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-transform-svg-component": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.1.tgz", - "integrity": "sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-preset": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-6.5.1.tgz", - "integrity": "sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw==", - "dev": true, - "dependencies": { - "@svgr/babel-plugin-add-jsx-attribute": "^6.5.1", - "@svgr/babel-plugin-remove-jsx-attribute": "*", - "@svgr/babel-plugin-remove-jsx-empty-expression": "*", - "@svgr/babel-plugin-replace-jsx-attribute-value": "^6.5.1", - "@svgr/babel-plugin-svg-dynamic-title": "^6.5.1", - "@svgr/babel-plugin-svg-em-dimensions": "^6.5.1", - "@svgr/babel-plugin-transform-react-native-svg": "^6.5.1", - "@svgr/babel-plugin-transform-svg-component": "^6.5.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/core": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-6.5.1.tgz", - "integrity": "sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.19.6", - "@svgr/babel-preset": "^6.5.1", - "@svgr/plugin-jsx": "^6.5.1", - "camelcase": "^6.2.0", - "cosmiconfig": "^7.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/hast-util-to-babel-ast": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.5.1.tgz", - "integrity": "sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.0", - "entities": "^4.4.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/plugin-jsx": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-6.5.1.tgz", - "integrity": "sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.19.6", - "@svgr/babel-preset": "^6.5.1", - "@svgr/hast-util-to-babel-ast": "^6.5.1", - "svg-parser": "^2.0.4" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@svgr/core": "^6.0.0" - } - }, - "node_modules/@svgr/plugin-svgo": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-6.5.1.tgz", - "integrity": "sha512-omvZKf8ixP9z6GWgwbtmP9qQMPX4ODXi+wzbVZgomNFsUIlHA1sf4fThdwTWSsZGgvGAG6yE+b/F5gWUkcZ/iQ==", - "dev": true, - "dependencies": { - "cosmiconfig": "^7.0.1", - "deepmerge": "^4.2.2", - "svgo": "^2.8.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@svgr/core": "*" - } - }, - "node_modules/@svgr/webpack": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-6.5.1.tgz", - "integrity": "sha512-cQ/AsnBkXPkEK8cLbv4Dm7JGXq2XrumKnL1dRpJD9rIO2fTIlJI9a1uCciYG1F2aUsox/hJQyNGbt3soDxSRkA==", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "dependencies": { - "@babel/core": "^7.19.6", - "@babel/plugin-transform-react-constant-elements": "^7.18.12", - "@babel/preset-env": "^7.19.4", - "@babel/preset-react": "^7.18.6", - "@babel/preset-typescript": "^7.18.6", - "@svgr/core": "^6.5.1", - "@svgr/plugin-jsx": "^6.5.1", - "@svgr/plugin-svgo": "^6.5.1" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, "engines": { - "node": ">=10" - }, + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", + "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" + "type": "opencollective", + "url": "https://opencollective.com/popperjs" } }, "node_modules/@tippyjs/react": { @@ -2755,15 +760,6 @@ "react-dom": ">=16.8" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/draft-js": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@types/draft-js/-/draft-js-0.11.10.tgz", @@ -3224,45 +1220,6 @@ "postcss": "^8.1.0" } }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3", - "core-js-compat": "^3.25.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3278,12 +1235,6 @@ "node": "*" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3349,18 +1300,6 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001458", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz", @@ -3455,31 +1394,12 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, "node_modules/core-js": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", "integrity": "sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==", "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js." }, - "node_modules/core-js-compat": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.29.0.tgz", - "integrity": "sha512-ScMn3uZNAFhK2DGoEfErguoiAHhV2Ju+oJo/jK08p7B3f3UhocUrCCkTvnZaiS+edl5nlIoiBXKcwMc6elv4KQ==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -3625,47 +1545,6 @@ "postcss": "^8.4" } }, - "node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, "node_modules/cssdb": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.4.1.tgz", @@ -3688,18 +1567,6 @@ "node": ">=4" } }, - "node_modules/csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "dev": true, - "dependencies": { - "css-tree": "^1.1.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/csstype": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", @@ -3737,15 +1604,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/deepmerge": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz", - "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -3758,70 +1616,6 @@ "node": ">=6.0.0" } }, - "node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, "node_modules/downshift": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/downshift/-/downshift-7.2.2.tgz", @@ -3944,18 +1738,6 @@ "node": ">=10.13.0" } }, - "node_modules/entities": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/envinfo": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", @@ -4532,15 +2314,6 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4579,15 +2352,6 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -4911,18 +2675,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -5025,12 +2777,6 @@ "node": ">=8" } }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5048,21 +2794,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -5241,18 +2972,6 @@ "node": ">=0.10.0" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6237,38 +3956,11 @@ "node": ">= 0.10" } }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", - "dev": true, - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, - "node_modules/regenerator-transform": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", - "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, "node_modules/regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", @@ -6281,44 +3973,6 @@ "url": "https://github.com/sponsors/mysticatea" } }, - "node_modules/regexpu-core": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.1.tgz", - "integrity": "sha512-nCOzW2V/X15XpLsK2rlgdwrysrBq+AauCn+omItIz4R1pIcmeot5zvjdmOBRLzEH/CkC6IxMJVmxDe3QcMuNVQ==", - "dev": true, - "dependencies": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dev": true, - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -6475,15 +4129,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/serialize-javascript": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", @@ -6559,13 +4204,6 @@ "source-map": "^0.6.0" } }, - "node_modules/stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", - "dev": true - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -6630,33 +4268,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/svg-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", - "dev": true - }, - "node_modules/svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", - "dev": true, - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^4.1.3", - "css-tree": "^1.1.3", - "csso": "^4.2.0", - "picocolors": "^1.0.0", - "stable": "^0.1.8" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -6738,15 +4349,6 @@ "@popperjs/core": "^2.9.0" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6941,46 +4543,6 @@ "node": "*" } }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/union-class-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/union-class-names/-/union-class-names-1.0.0.tgz", @@ -7197,12 +4759,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", diff --git a/package.json b/package.json index 7bc3f95..7ae658e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "author": "", "license": "MIT", "devDependencies": { - "@svgr/webpack": "^6.2.1", "@types/draft-js": "^0.11.10", "@types/react": "^16.8.19", "@types/react-dom": "^16.8.19", diff --git a/src/wagtail_ai/ai/__init__.py b/src/wagtail_ai/ai/__init__.py index 754723c..8f31999 100644 --- a/src/wagtail_ai/ai/__init__.py +++ b/src/wagtail_ai/ai/__init__.py @@ -11,7 +11,7 @@ from ..text_splitters.length import NaiveTextSplitterCalculator from ..types import TextSplitterLengthCalculatorProtocol, TextSplitterProtocol from ..utils import deprecation -from .base import AIBackend, BaseAIBackendConfigSettings +from .base import AIBackend, BackendFeature, BaseAIBackendConfigSettings class TextSplittingSettingsDict(TypedDict): @@ -161,3 +161,22 @@ def get_ai_backend(alias: str) -> AIBackend: ) return ai_backend_cls(config=config) + + +class BackendNotFound(Exception): + pass + + +def get_backend(feature: BackendFeature = BackendFeature.TEXT_COMPLETION) -> AIBackend: + match feature: + case BackendFeature.TEXT_COMPLETION: + alias = settings.WAGTAIL_AI.get("TEXT_COMPLETION_BACKEND", "default") + case BackendFeature.IMAGE_DESCRIPTION: + alias = settings.WAGTAIL_AI.get("IMAGE_DESCRIPTION_BACKEND") + case _: + alias = None + + if alias is None: + raise BackendNotFound(f"No backend found for {feature.name}") + + return get_ai_backend(alias) diff --git a/src/wagtail_ai/ai/base.py b/src/wagtail_ai/ai/base.py index f1fd580..cb20887 100644 --- a/src/wagtail_ai/ai/base.py +++ b/src/wagtail_ai/ai/base.py @@ -1,5 +1,6 @@ -from abc import ABCMeta, abstractmethod +from abc import ABCMeta from dataclasses import dataclass +from enum import Enum from typing import ( Any, ClassVar, @@ -13,6 +14,7 @@ ) from django.core.exceptions import ImproperlyConfigured +from django.core.files import File from .. import tokens from ..types import ( @@ -22,6 +24,11 @@ ) +class BackendFeature(Enum): + TEXT_COMPLETION = "TEXT_COMPLETION" + IMAGE_DESCRIPTION = "IMAGE_DESCRIPTION" + + class BaseAIBackendConfigSettings(TypedDict): MODEL_ID: Required[str] TOKEN_LIMIT: NotRequired[int | None] @@ -99,14 +106,13 @@ def __init__( ) -> None: self.config = config - @abstractmethod def prompt_with_context( self, *, pre_prompt: str, context: str, post_prompt: str | None = None ) -> AIResponse: """ Given a prompt and a context, return a response. """ - ... + raise NotImplementedError("This backend does not support text completion") def get_text_splitter(self) -> TextSplitterProtocol: return self.config.text_splitter_class( @@ -116,3 +122,6 @@ def get_text_splitter(self) -> TextSplitterProtocol: def get_splitter_length_calculator(self) -> TextSplitterLengthCalculatorProtocol: return self.config.text_splitter_length_calculator_class() + + def describe_image(self, *, image_file: File, prompt: str) -> AIResponse: + raise NotImplementedError("This backend does not support image description") diff --git a/src/wagtail_ai/ai/echo.py b/src/wagtail_ai/ai/echo.py index 71cb6dc..ac1bee2 100644 --- a/src/wagtail_ai/ai/echo.py +++ b/src/wagtail_ai/ai/echo.py @@ -5,6 +5,7 @@ from typing import Any, NotRequired, Self from django.core.exceptions import ImproperlyConfigured +from django.core.files import File from .base import ( AIBackend, @@ -62,10 +63,18 @@ class EchoBackend(AIBackend[EchoBackendConfig]): def prompt_with_context( self, *, pre_prompt: str, context: str, post_prompt: str | None = None ) -> AIResponse: + return self.get_response( + ["This", "is", "an", "echo", "backend:", *context.split()] + ) + + def describe_image(self, *, image_file: File, prompt: str) -> AIResponse: + return self.get_response( + ["This", "is", "an", "echo", "backend:", image_file.name] + ) + + def get_response(self, words): def response_iterator() -> Generator[str, None, None]: - response = ["This", "is", "an", "echo", "backend:"] - response += context.split() - for word in response: + for word in words: if ( self.config.max_word_sleep_seconds is not None and self.config.max_word_sleep_seconds > 0 diff --git a/src/wagtail_ai/ai/openai.py b/src/wagtail_ai/ai/openai.py new file mode 100644 index 0000000..de49bea --- /dev/null +++ b/src/wagtail_ai/ai/openai.py @@ -0,0 +1,128 @@ +import base64 +import os +from collections.abc import Iterator +from dataclasses import dataclass +from typing import Any, NotRequired, Self + +import requests +from django.core.files import File + +from wagtail_ai.types import AIResponse + +from .base import AIBackend, BaseAIBackendConfig, BaseAIBackendConfigSettings + + +class OpenAIBackendConfigSettingsDict(BaseAIBackendConfigSettings): + TIMEOUT_SECONDS: NotRequired[int | None] + OPENAI_API_KEY: NotRequired[str | None] + + +@dataclass(kw_only=True) +class OpenAIBackendConfig(BaseAIBackendConfig[OpenAIBackendConfigSettingsDict]): + timeout_seconds: int + openai_api_key: str | None + + @classmethod + def from_settings( + cls, config: OpenAIBackendConfigSettingsDict, **kwargs: Any + ) -> Self: + timeout_seconds = config.get("TIMEOUT_SECONDS") + if timeout_seconds is None: + timeout_seconds = 15 + kwargs.setdefault("timeout_seconds", timeout_seconds) + + kwargs.setdefault("openai_api_key", config.get("OPENAI_API_KEY")) + + return super().from_settings(config, **kwargs) + + +class OpenAIResponse(AIResponse): + def __init__(self, response: requests.Response): + self.response = response + self._text = response.json()["choices"][0]["message"]["content"] + + def __iter__(self) -> Iterator[str]: + yield self._text + + def text(self) -> str: + return self._text + + def __str__(self): + return self.text() + + +class OpenAIBackend(AIBackend[OpenAIBackendConfig]): + config_cls = OpenAIBackendConfig + + def prompt_with_context( + self, *, pre_prompt: str, context: str, post_prompt: str | None = None + ) -> OpenAIResponse: + messages = [ + {"role": "system", "content": [{"type": "text", "text": pre_prompt}]}, + {"role": "user", "content": [{"type": "text", "text": context}]}, + ] + + if post_prompt is not None: + messages.append( + {"role": "system", "content": [{"type": "text", "text": post_prompt}]} + ) + + return self.chat_completions(messages) + + def describe_image(self, *, image_file: File, prompt: str) -> OpenAIResponse: + if not prompt: + raise ValueError("Prompt must not be empty.") + with image_file.open() as f: + base64_image = base64.b64encode(f.read()).decode("utf-8") + + return self.chat_completions( + messages=[ + { + "role": "user", + "content": [ + { + "type": "text", + "text": prompt, + }, + { + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{base64_image}" + }, + }, + ], + }, + ], + ) + + def chat_completions(self, messages: list[dict[str, Any]]) -> OpenAIResponse: + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.get_openai_api_key()}", + } + payload = { + "model": self.config.model_id, + "messages": messages, + "max_tokens": self.config.token_limit, + } + response = requests.post( + "https://api.openai.com/v1/chat/completions", + headers=headers, + json=payload, + timeout=self.config.timeout_seconds, + ) + + response.raise_for_status() + return OpenAIResponse(response) + + def get_openai_api_key(self) -> str: + if config_key := self.config.openai_api_key: + return config_key + + if env_key := os.environ.get("OPENAI_API_KEY"): + return env_key + + raise RuntimeError( + "Cannot find OpenAI API key. Please set the OPENAI_API_KEY environment" + " variable or the OPENAI_API_KEY backend config value in Django settings." + ) diff --git a/src/wagtail_ai/forms.py b/src/wagtail_ai/forms.py index ddcc34b..0c55fcc 100644 --- a/src/wagtail_ai/forms.py +++ b/src/wagtail_ai/forms.py @@ -1,6 +1,8 @@ from django import forms from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ +from wagtail.admin.staticfiles import versioned_static +from wagtail.images.forms import BaseImageForm class PromptTextField(forms.CharField): @@ -18,7 +20,17 @@ class PromptUUIDField(forms.UUIDField): } -class PromptForm(forms.Form): +class ApiForm(forms.Form): + def errors_for_json_response(self) -> str: + errors_for_response = [] + for _field, errors in self.errors.get_json_data().items(): + for error in errors: + errors_for_response.append(error["message"]) + + return " \n".join(errors_for_response) + + +class PromptForm(ApiForm): text = PromptTextField() prompt = PromptUUIDField() @@ -31,10 +43,21 @@ def clean_prompt(self): return prompt_uuid - def errors_for_json_response(self) -> str: - errors_for_response = [] - for _field, errors in self.errors.get_json_data().items(): - for error in errors: - errors_for_response.append(error["message"]) - return " \n".join(errors_for_response) +class DescribeImageApiForm(ApiForm): + image_id = forms.CharField() + maxlength = forms.IntegerField(required=False, min_value=0, max_value=4096) + + +class DescribeImageForm(BaseImageForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance and self.instance.pk: + widget = self.fields["title"].widget + widget.attrs["data-wagtailai-image-id"] = str(self.instance.pk) + widget.attrs["data-wagtailai-button-title"] = _("Describe image using AI") + widget.template_name = "wagtail_ai/widgets/image_title.html" + + class Media: + js = [versioned_static("wagtail_ai/image_description.js")] + css = {"all": [versioned_static("wagtail_ai/image_description.css")]} diff --git a/src/wagtail_ai/static_src/api.tsx b/src/wagtail_ai/static_src/api.tsx new file mode 100644 index 0000000..a475f67 --- /dev/null +++ b/src/wagtail_ai/static_src/api.tsx @@ -0,0 +1,38 @@ +import type { ApiUrlName, WagtailAiConfiguration } from './custom'; + +class APIRequestError extends Error {} + +export const getAIConfiguration = (): WagtailAiConfiguration => { + const configurationElement = + document.querySelector('#wagtail-ai-config'); + if (!configurationElement || !configurationElement.textContent) { + throw new Error('No wagtail-ai configuration found.'); + } + + try { + return JSON.parse(configurationElement.textContent); + } catch (err) { + throw new SyntaxError( + `Error parsing wagtail-ai configuration: ${err.message}`, + ); + } +}; + +export const fetchResponse = async ( + action: keyof typeof ApiUrlName, + body: FormData, + signal?: AbortSignal, +): Promise => { + try { + const urls = getAIConfiguration().urls; + const res = await fetch(urls[action], { method: 'POST', body, signal }); + const json = await res.json(); + if (res.ok) { + return json.message; + } else { + throw new APIRequestError(json.error); + } + } catch (err) { + throw new APIRequestError(err.message); + } +}; diff --git a/src/wagtail_ai/static_src/custom.d.ts b/src/wagtail_ai/static_src/custom.d.ts index a590ab1..f360855 100644 --- a/src/wagtail_ai/static_src/custom.d.ts +++ b/src/wagtail_ai/static_src/custom.d.ts @@ -1,9 +1,19 @@ /* eslint-disable no-unused-vars */ export {}; +export interface WagtailAiConfigurationUrls {} + +export enum ApiUrlName { + TEXT_COMPLETION = 'TEXT_COMPLETION', + DESCRIBE_IMAGE = 'DESCRIBE_IMAGE', +} + export interface WagtailAiConfiguration { aiPrompts: Prompt[]; - aiProcessUrl: string; + urls: { + [ApiUrlName.TEXT_COMPLETION]: string; + [ApiUrlName.DESCRIBE_IMAGE]: string; + }; } export type Prompt = { diff --git a/src/wagtail_ai/static_src/default.d.ts b/src/wagtail_ai/static_src/default.d.ts new file mode 100644 index 0000000..cdb2b1a --- /dev/null +++ b/src/wagtail_ai/static_src/default.d.ts @@ -0,0 +1,4 @@ +declare module '*.svg' { + const content: string; + export default content; +} diff --git a/src/wagtail_ai/static_src/AIControl.tsx b/src/wagtail_ai/static_src/draftail/AIControl.tsx similarity index 96% rename from src/wagtail_ai/static_src/AIControl.tsx rename to src/wagtail_ai/static_src/draftail/AIControl.tsx index feecd89..e30cd56 100644 --- a/src/wagtail_ai/static_src/AIControl.tsx +++ b/src/wagtail_ai/static_src/draftail/AIControl.tsx @@ -4,15 +4,11 @@ import { ToolbarButton } from 'draftail'; import { createPortal } from 'react-dom'; import { useOutsideAlerter } from './hooks'; import WandIcon from './WandIcon'; -import { - handleAppend, - handleReplace, - processAction, - getAIConfiguration, -} from './utils'; +import { handleAppend, handleReplace, processAction } from './utils'; +import { getAIConfiguration } from '../api'; import type { ControlComponentProps } from 'draftail'; -import type { Prompt } from './custom'; +import type { Prompt } from '../custom'; // Generated by asking ChatGPT 'Generate a list of loading messages which mean "Loading response from AI chat bot"'... const LOADING_MESSAGES = [ diff --git a/src/wagtail_ai/static_src/WandIcon.tsx b/src/wagtail_ai/static_src/draftail/WandIcon.tsx similarity index 100% rename from src/wagtail_ai/static_src/WandIcon.tsx rename to src/wagtail_ai/static_src/draftail/WandIcon.tsx diff --git a/src/wagtail_ai/static_src/hooks.tsx b/src/wagtail_ai/static_src/draftail/hooks.tsx similarity index 100% rename from src/wagtail_ai/static_src/hooks.tsx rename to src/wagtail_ai/static_src/draftail/hooks.tsx diff --git a/src/wagtail_ai/static_src/main.css b/src/wagtail_ai/static_src/draftail/main.css similarity index 100% rename from src/wagtail_ai/static_src/main.css rename to src/wagtail_ai/static_src/draftail/main.css diff --git a/src/wagtail_ai/static_src/main.tsx b/src/wagtail_ai/static_src/draftail/main.tsx similarity index 100% rename from src/wagtail_ai/static_src/main.tsx rename to src/wagtail_ai/static_src/draftail/main.tsx diff --git a/src/wagtail_ai/static_src/utils.tsx b/src/wagtail_ai/static_src/draftail/utils.tsx similarity index 66% rename from src/wagtail_ai/static_src/utils.tsx rename to src/wagtail_ai/static_src/draftail/utils.tsx index 3b5ae17..b7b8aa3 100644 --- a/src/wagtail_ai/static_src/utils.tsx +++ b/src/wagtail_ai/static_src/draftail/utils.tsx @@ -6,27 +6,10 @@ import { RichUtils, } from 'draft-js'; -import type { Prompt, WagtailAiConfiguration } from './custom'; +import type { Prompt } from '../custom'; +import { fetchResponse } from '../api'; -class APIRequestError extends Error {} - -export const getAIConfiguration = (): WagtailAiConfiguration => { - const configurationElement = - document.querySelector('#wagtail-ai-config'); - if (!configurationElement || !configurationElement.textContent) { - throw new Error('No wagtail-ai configuration found.'); - } - - try { - return JSON.parse(configurationElement.textContent); - } catch (err) { - throw new SyntaxError( - `Error parsing wagtail-ai configuration: ${err.message}`, - ); - } -}; - -const fetchAIResponse = async ( +export const fetchAIResponse = async ( text: string, prompt: Prompt, signal: AbortSignal, @@ -34,23 +17,7 @@ const fetchAIResponse = async ( const formData = new FormData(); formData.append('text', text); formData.append('prompt', prompt.uuid); - try { - const aiProcessUrl = getAIConfiguration().aiProcessUrl; - - const res = await fetch(aiProcessUrl, { - method: 'POST', - body: formData, - signal: signal, - }); - const json = await res.json(); - if (res.ok) { - return json.message; - } else { - throw new APIRequestError(json.error); - } - } catch (err) { - throw new APIRequestError(err.message); - } + return fetchResponse('TEXT_COMPLETION', formData, signal); }; const getAllSelection = (content: ContentState): SelectionState => { diff --git a/src/wagtail_ai/static_src/image_description/main.css b/src/wagtail_ai/static_src/image_description/main.css new file mode 100644 index 0000000..1f035ef --- /dev/null +++ b/src/wagtail_ai/static_src/image_description/main.css @@ -0,0 +1,101 @@ +.wagtailai-input-wrapper { + display: flex; + align-items: stretch; + gap: 0.3125rem; +} + +.wagtailai-button > svg { + fill: white; +} + +@keyframes wagtailai-button-twinkle { + 0% { + opacity: 1; + } + 50% { + opacity: 0.6; + } + 100% { + opacity: 1; + } +} +@keyframes wagtailai-button-dazzle { + 0% { + fill: hsl(0, 80%, 80%); + } /* Red */ + 8.33% { + fill: hsl(30, 80%, 70%); + } /* Red-Orange */ + 16.67% { + fill: hsl(60, 80%, 70%); + } /* Orange */ + 25% { + fill: hsl(90, 80%, 20%); + } /* Yellow */ + 33.33% { + fill: hsl(120, 80%, 70%); + } /* Yellow-Green */ + 41.67% { + fill: hsl(150, 80%, 70%); + } /* Green */ + 50% { + fill: hsl(180, 80%, 70%); + } /* Green-Cyan */ + 58.33% { + fill: hsl(210, 80%, 70%); + } /* Cyan */ + 66.67% { + fill: hsl(240, 80%, 70%); + } /* Cyan-Blue */ + 75% { + fill: hsl(270, 80%, 80%); + } /* Blue */ + 83.33% { + fill: hsl(300, 80%, 80%); + } /* Blue-Indigo */ + 91.67% { + fill: hsl(330, 80%, 70%); + } /* Indigo */ + 100% { + fill: hsl(360, 80%, 70%); + } /* Back to Red */ +} +@keyframes wagtailai-button-hover { + 0% { + transform: translateY(0px); + } + 50% { + transform: translateY(-11%); + } + 100% { + transform: translateY(0px); + } +} + +.wagtailai-button.loading svg { + opacity: 0.9; +} +.wagtailai-button.loading #wand { + transform-origin: center; + animation: wagtailai-button-dazzle 2.4s ease infinite; +} +.wagtailai-button.loading #star1 { + animation: wagtailai-button-hover 1.2s ease infinite, + wagtailai-button-twinkle 1.2s ease infinite, + wagtailai-button-dazzle 2.4s ease infinite; + transform-origin: center; + animation-delay: 0.2s, 0s; +} +.wagtailai-button.loading #star2 { + animation: wagtailai-button-hover 1.2s ease infinite, + wagtailai-button-twinkle 1.2s ease infinite, + wagtailai-button-dazzle 2.4s ease infinite; + transform-origin: center; + animation-delay: 0.1s, 0s; +} +.wagtailai-button.loading #star3 { + animation: wagtailai-button-hover 1.2s ease infinite, + wagtailai-button-twinkle 1.2s ease infinite, + wagtailai-button-dazzle 2.4s ease infinite; + transform-origin: center; +} diff --git a/src/wagtail_ai/static_src/image_description/main.tsx b/src/wagtail_ai/static_src/image_description/main.tsx new file mode 100644 index 0000000..0bf9827 --- /dev/null +++ b/src/wagtail_ai/static_src/image_description/main.tsx @@ -0,0 +1,62 @@ +import './main.css'; +import { fetchResponse } from '../api'; + +import wandIcon from '../wand_icon.svg'; + +document.addEventListener('wagtail-ai:image-form', (event) => { + const input = event.target as HTMLInputElement; + const imageId = input.dataset.wagtailaiImageId; + if (!imageId) { + throw new Error('The attribute data-wagtailai-image-id is missing.'); + } + if (!input.form) { + throw new Error('The input is not part of a form.'); + } + const csrfTokenInput = input.form.querySelector( + 'input[name=csrfmiddlewaretoken]', + ); + if (!csrfTokenInput) { + throw new Error('Could not find a CSRF token hidden input.'); + } + + const inputParent = input.parentNode!; + const flexWrapper = document.createElement('div'); + flexWrapper.classList.add('wagtailai-input-wrapper'); + inputParent.replaceChild(flexWrapper, input); + flexWrapper.appendChild(input); + + const button = document.createElement('button'); + button.type = 'button'; + button.title = input.dataset.wagtailaiButtonTitle || ''; + button.classList.add('button', 'wagtailai-button'); + button.innerHTML = wandIcon; + flexWrapper.appendChild(button); + + const maxLength = input.getAttribute('maxlength'); + + let errorMessage: HTMLParagraphElement | null = null; + + button.addEventListener('click', async () => { + errorMessage?.parentNode?.removeChild(errorMessage); + button.disabled = true; + button.classList.add('loading'); + + try { + const formData = new FormData(); + formData.append('image_id', imageId); + formData.append('csrfmiddlewaretoken', csrfTokenInput.value); + if (maxLength) { + formData.append('maxlength', maxLength); + } + input.value = await fetchResponse('DESCRIBE_IMAGE', formData); + } catch (error) { + errorMessage = document.createElement('p'); + errorMessage.classList.add('error-message'); + errorMessage.textContent = 'Could not generate image description.'; + inputParent.append(errorMessage); + } + + button.classList.remove('loading'); + button.disabled = false; + }); +}); diff --git a/src/wagtail_ai/static_src/wand_icon.svg b/src/wagtail_ai/static_src/wand_icon.svg new file mode 100644 index 0000000..8edfb33 --- /dev/null +++ b/src/wagtail_ai/static_src/wand_icon.svg @@ -0,0 +1,7 @@ + + diff --git a/src/wagtail_ai/templates/wagtail_ai/widgets/image_title.html b/src/wagtail_ai/templates/wagtail_ai/widgets/image_title.html new file mode 100644 index 0000000..944517a --- /dev/null +++ b/src/wagtail_ai/templates/wagtail_ai/widgets/image_title.html @@ -0,0 +1,6 @@ +{% include "django/forms/widgets/input.html" %} + diff --git a/src/wagtail_ai/text_splitters/dummy.py b/src/wagtail_ai/text_splitters/dummy.py index 60efeb7..0acb6f6 100644 --- a/src/wagtail_ai/text_splitters/dummy.py +++ b/src/wagtail_ai/text_splitters/dummy.py @@ -12,9 +12,9 @@ def __init__( ) -> None: pass - def split_text(self, text: str) -> str: + def split_text(self, text: str) -> list[str]: # Don't do any splitting. - return text + return [text] class DummyLengthCalculator(TextSplitterLengthCalculatorProtocol): diff --git a/src/wagtail_ai/views.py b/src/wagtail_ai/views.py index ae96b68..249950e 100644 --- a/src/wagtail_ai/views.py +++ b/src/wagtail_ai/views.py @@ -1,15 +1,21 @@ import logging import os +from typing import Type, cast from django import forms +from django.conf import settings from django.http import JsonResponse +from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ from django.views.decorators.csrf import csrf_exempt from wagtail.admin.ui.tables import UpdatedAtColumn from wagtail.admin.viewsets.model import ModelViewSet +from wagtail.images.models import AbstractImage +from wagtail.images.permissions import get_image_model from . import ai, types -from .forms import PromptForm +from .ai.base import BackendFeature +from .forms import DescribeImageApiForm, PromptForm from .models import Prompt logger = logging.getLogger(__name__) @@ -44,7 +50,7 @@ def _process_backend_request( def _replace_handler(*, prompt: Prompt, text: str) -> str: - ai_backend = ai.get_ai_backend(alias="default") # TODO update + ai_backend = ai.get_backend() splitter = ai_backend.get_text_splitter() texts = splitter.split_text(text) @@ -60,7 +66,7 @@ def _replace_handler(*, prompt: Prompt, text: str) -> str: def _append_handler(*, prompt: Prompt, text: str) -> str: - ai_backend = ai.get_ai_backend(alias="default") # TODO update + ai_backend = ai.get_backend() length_calculator = ai_backend.get_splitter_length_calculator() if length_calculator.get_splitter_length(text) > ai_backend.config.token_limit: raise AIHandlerException("Cannot run completion on text this long") @@ -74,19 +80,21 @@ def _append_handler(*, prompt: Prompt, text: str) -> str: return message +def ErrorJsonResponse(error_message, status=500): + return JsonResponse({"error": error_message}, status=status) + + @csrf_exempt -def process(request) -> JsonResponse: +def text_completion(request) -> JsonResponse: prompt_form = PromptForm(request.POST) if not prompt_form.is_valid(): - return JsonResponse( - {"error": prompt_form.errors_for_json_response()}, status=400 - ) + return ErrorJsonResponse(prompt_form.errors_for_json_response(), status=400) try: prompt = Prompt.objects.get(uuid=prompt_form.cleaned_data["prompt"]) except Prompt.DoesNotExist: - return JsonResponse({"error": _("Invalid prompt provided.")}, status=400) + return ErrorJsonResponse(_("Invalid prompt provided."), status=400) handlers = { Prompt.Method.REPLACE: _replace_handler, @@ -98,14 +106,72 @@ def process(request) -> JsonResponse: try: response = handler(prompt=prompt, text=prompt_form.cleaned_data["text"]) except AIHandlerException as e: - return JsonResponse({"error": str(e)}, status=400) + return ErrorJsonResponse(str(e), status=400) except Exception: logger.exception("An unexpected error occurred.") - return JsonResponse({"error": _("An unexpected error occurred.")}, status=500) + return ErrorJsonResponse(_("An unexpected error occurred.")) return JsonResponse({"message": response}) +def user_has_permission_for_image(user, image): + from wagtail.images.permissions import permission_policy + + return permission_policy.user_has_permission_for_instance(user, "choose", image) + + +def describe_image(request) -> JsonResponse: + form = DescribeImageApiForm(request.POST) + if not form.is_valid(): + return ErrorJsonResponse(form.errors_for_json_response(), status=400) + + model = cast(Type[AbstractImage], get_image_model()) + image = get_object_or_404(model, pk=form.cleaned_data["image_id"]) + + if not user_has_permission_for_image(request.user, image): + return ErrorJsonResponse("Access denied", status=403) + + try: + backend = ai.get_backend(BackendFeature.IMAGE_DESCRIPTION) + except ai.BackendNotFound: + return ErrorJsonResponse( + "No backend is configured for image description. Please set" + " `IMAGE_DESCRIPTION_BACKEND` in `settings.WAGTAIL_AI`.", + status=400, + ) + + wagtail_ai_settings = getattr(settings, "WAGTAIL_AI", {}) + rendition_filter = wagtail_ai_settings.get( + "IMAGE_DESCRIPTION_RENDITION_FILTER", "max-800x600" + ) + rendition = image.get_rendition(rendition_filter) + + maxlength = form.cleaned_data["maxlength"] + prompt = wagtail_ai_settings.get("IMAGE_DESCRIPTION_PROMPT") + + if prompt is None: + prompt = ( + "Describe this image. Make the description suitable for use as an alt-text." + ) + if maxlength is not None: + prompt += f" Make the description less than {maxlength} characters long." + + try: + ai_response = backend.describe_image(image_file=rendition.file, prompt=prompt) + description = ai_response.text() + except Exception: + logger.exception("There was an issue describing the image.") + return ErrorJsonResponse("There was an issue describing the image.") + + if not description: + return ErrorJsonResponse("There was an issue describing the image.") + + if maxlength is not None: + description = description[:maxlength] + + return JsonResponse({"message": description}) + + class PromptEditForm(forms.ModelForm): """ Custom form for the model admin to allow users to view and edit default prompts diff --git a/src/wagtail_ai/wagtail_hooks.py b/src/wagtail_ai/wagtail_hooks.py index bbfad6b..c9d2f42 100644 --- a/src/wagtail_ai/wagtail_hooks.py +++ b/src/wagtail_ai/wagtail_hooks.py @@ -9,7 +9,7 @@ from wagtail.admin.rich_text.editors.draftail.features import ControlFeature from .models import Prompt -from .views import process, prompt_viewset +from .views import describe_image, prompt_viewset, text_completion @hooks.register("register_admin_urls") # type: ignore @@ -21,9 +21,14 @@ def register_admin_urls(): name="javascript_catalog", ), path( - "process/", - process, - name="process", + "text_completion/", + text_completion, + name="text_completion", + ), + path( + "describe_image/", + describe_image, + name="describe_image", ), ] @@ -50,8 +55,8 @@ def register_ai_feature(features): { "type": feature_name, }, - js=["wagtail_ai/wagtail-ai.js"], - css={"all": ["wagtail_ai/main.css"]}, + js=["wagtail_ai/draftail.js"], + css={"all": ["wagtail_ai/draftail.css"]}, ), ) @@ -80,17 +85,17 @@ def get_prompts(): return [_serialize_prompt(prompt) for prompt in Prompt.objects.all()] -@hooks.register("insert_editor_js") # type: ignore +@hooks.register("insert_global_admin_js") # type: ignore def ai_editor_js(): - prompt_json = get_prompts() - process_url = reverse("wagtail_ai:process") - - wagtail_ai_config = json_script( - {"aiPrompts": prompt_json, "aiProcessUrl": process_url}, - "wagtail-ai-config", - ) + config = { + "aiPrompts": get_prompts(), + "urls": { + "TEXT_COMPLETION": reverse("wagtail_ai:text_completion"), + "DESCRIBE_IMAGE": reverse("wagtail_ai:describe_image"), + }, + } - return mark_safe(wagtail_ai_config) + return mark_safe(json_script(config, "wagtail-ai-config")) @hooks.register("register_admin_viewset") # type: ignore diff --git a/tests/backends/test_openai.py b/tests/backends/test_openai.py new file mode 100644 index 0000000..2709f27 --- /dev/null +++ b/tests/backends/test_openai.py @@ -0,0 +1,205 @@ +from typing import cast +from unittest.mock import ANY, Mock + +import pytest +from wagtail.images.models import Image +from wagtail_ai.ai import get_ai_backend, get_backend +from wagtail_ai.ai.base import BackendFeature +from wagtail_ai.ai.openai import OpenAIBackend +from wagtail_factories import ImageFactory + +pytestmark = pytest.mark.django_db + +MOCK_API_KEY = "openai-api-key-stub" +MOCK_OUTPUT = "mock ai output" + + +@pytest.fixture(autouse=True) +def stub_image_title_signal(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("OPENAI_API_KEY", MOCK_API_KEY) + + +@pytest.fixture +def mock_post(monkeypatch: pytest.MonkeyPatch): + mock = Mock() + monkeypatch.setattr("requests.post", mock) + return mock + + +def test_get_as_image_backend(settings): + settings.WAGTAIL_AI = { + "BACKENDS": { + "openai": { + "CLASS": "wagtail_ai.ai.openai.OpenAIBackend", + "CONFIG": { + "MODEL_ID": "mock model", + "TOKEN_LIMIT": 123123, + "TIMEOUT_SECONDS": 321, + }, + }, + }, + "IMAGE_DESCRIPTION_BACKEND": "openai", + } + + backend = get_backend(BackendFeature.IMAGE_DESCRIPTION) + assert isinstance(backend, OpenAIBackend) + assert backend.config.model_id == "mock model" + assert backend.config.token_limit == 123123 + assert backend.config.timeout_seconds == 321 + + +def test_describe_image(settings, mock_post): + settings.WAGTAIL_AI = { + "BACKENDS": { + "openai": { + "CLASS": "wagtail_ai.ai.openai.OpenAIBackend", + "CONFIG": { + "MODEL_ID": "gpt-4", + }, + }, + }, + } + + mock_post.return_value.json.return_value = { + "choices": [{"message": {"content": MOCK_OUTPUT}}], + } + image = cast(Image, ImageFactory()) + backend = get_ai_backend("openai") + prompt = "what do you see?" + + response = backend.describe_image(image_file=image.file, prompt=prompt) + assert response.text() == MOCK_OUTPUT + + headers = mock_post.call_args.kwargs["headers"] + assert headers["Authorization"] == f"Bearer {MOCK_API_KEY}" + + messages = mock_post.call_args.kwargs["json"]["messages"] + assert messages == [ + { + "role": "user", + "content": [ + {"type": "text", "text": prompt}, + {"type": "image_url", "image_url": {"url": ANY}}, + ], + } + ] + url = messages[0]["content"][1]["image_url"]["url"] + assert url.startswith("data:image/jpeg;base64,") + + +def test_text_completion(settings, mock_post): + settings.WAGTAIL_AI = { + "BACKENDS": { + "openai": { + "CLASS": "wagtail_ai.ai.openai.OpenAIBackend", + "CONFIG": { + "MODEL_ID": "gpt-4", + }, + }, + }, + } + + mock_post.return_value.json.return_value = { + "choices": [{"message": {"content": MOCK_OUTPUT}}], + } + backend = get_ai_backend("openai") + pre_prompt = "You are assisting the user in testing the code." + context = "Some mock content." + post_prompt = "What just happened?" + + response = backend.prompt_with_context( + pre_prompt=pre_prompt, context=context, post_prompt=post_prompt + ) + assert "".join(response) == MOCK_OUTPUT + assert response.text() == MOCK_OUTPUT + + headers = mock_post.call_args.kwargs["headers"] + assert headers["Authorization"] == f"Bearer {MOCK_API_KEY}" + + messages = mock_post.call_args.kwargs["json"]["messages"] + assert messages == [ + {"content": [{"type": "text", "text": pre_prompt}], "role": "system"}, + {"content": [{"type": "text", "text": context}], "role": "user"}, + {"content": [{"type": "text", "text": post_prompt}], "role": "system"}, + ] + + +def test_text_completion_without_post_prompt(settings, mock_post): + settings.WAGTAIL_AI = { + "BACKENDS": { + "openai": { + "CLASS": "wagtail_ai.ai.openai.OpenAIBackend", + "CONFIG": { + "MODEL_ID": "gpt-4", + }, + }, + }, + } + + mock_post.return_value.json.return_value = { + "choices": [{"message": {"content": MOCK_OUTPUT}}], + } + backend = get_ai_backend("openai") + pre_prompt = "You are assisting the user in testing the code." + context = "Some mock content." + + response = backend.prompt_with_context(pre_prompt=pre_prompt, context=context) + assert response.text() == MOCK_OUTPUT + + messages = mock_post.call_args.kwargs["json"]["messages"] + assert messages == [ + {"content": [{"type": "text", "text": pre_prompt}], "role": "system"}, + {"content": [{"type": "text", "text": context}], "role": "user"}, + ] + + +def test_default_token_limit(settings): + settings.WAGTAIL_AI = { + "BACKENDS": { + "openai": { + "CLASS": "wagtail_ai.ai.openai.OpenAIBackend", + "CONFIG": { + "MODEL_ID": "gpt-4", + }, + }, + }, + } + + backend = get_ai_backend("openai") + assert backend.config.token_limit == 8192 + + +def test_api_key_in_environ(settings, monkeypatch: pytest.MonkeyPatch): + test_key = "test-key-value" + settings.WAGTAIL_AI = { + "BACKENDS": { + "openai": { + "CLASS": "wagtail_ai.ai.openai.OpenAIBackend", + "CONFIG": { + "MODEL_ID": "gpt-4", + }, + }, + }, + } + monkeypatch.setenv("OPENAI_API_KEY", test_key) + + backend = cast(OpenAIBackend, get_ai_backend("openai")) + assert backend.get_openai_api_key() == test_key + + +def test_api_key_in_settings(settings): + test_key = "test-key-value" + settings.WAGTAIL_AI = { + "BACKENDS": { + "openai": { + "CLASS": "wagtail_ai.ai.openai.OpenAIBackend", + "CONFIG": { + "MODEL_ID": "gpt-4", + "OPENAI_API_KEY": test_key, + }, + }, + }, + } + + backend = cast(OpenAIBackend, get_ai_backend("openai")) + assert backend.get_openai_api_key() == test_key diff --git a/tests/conftest.py b/tests/conftest.py index 8fa40a7..8be819b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,4 @@ import pytest -from django.contrib.auth.models import User from wagtail_ai.models import Prompt TEST_PROMPT_LABEL = "Prompt Label" @@ -26,12 +25,6 @@ def setup_prompt_object(test_prompt_values): prompt.delete() -@pytest.fixture -def setup_users(): - superuser = User.objects.create_superuser( - "Superuser", "superuser@email.com", "password" - ) - - yield superuser - - superuser.delete() +@pytest.fixture(autouse=True) +def temporary_media(settings, tmp_path): + settings.MEDIA_ROOT = tmp_path / "media" diff --git a/tests/test_ai_backends.py b/tests/test_ai_backends.py index 8e15d2f..35793b1 100644 --- a/tests/test_ai_backends.py +++ b/tests/test_ai_backends.py @@ -5,7 +5,13 @@ custom_ai_backend_class, custom_ai_backend_settings, ) -from wagtail_ai.ai import InvalidAIBackendError, get_ai_backend +from wagtail_ai.ai import ( + BackendNotFound, + InvalidAIBackendError, + get_ai_backend, + get_backend, +) +from wagtail_ai.ai.base import BackendFeature from wagtail_ai.ai.echo import EchoBackend @@ -90,3 +96,36 @@ def test_prompt_with_context_iterator(): "like", "trains.", ] + + +def test_get_backend_with_feature(settings): + settings.WAGTAIL_AI = { + "BACKENDS": { + "default": { + "CLASS": "wagtail_ai.ai.echo.EchoBackend", + "CONFIG": {"MODEL_ID": "default", "TOKEN_LIMIT": 123123}, + }, + "images": { + "CLASS": "wagtail_ai.ai.echo.EchoBackend", + "CONFIG": {"MODEL_ID": "images", "TOKEN_LIMIT": 123123}, + }, + }, + "IMAGE_DESCRIPTION_BACKEND": "images", + } + assert get_backend().config.model_id == "default" + assert get_backend(BackendFeature.TEXT_COMPLETION).config.model_id == "default" + assert get_backend(BackendFeature.IMAGE_DESCRIPTION).config.model_id == "images" + + +def test_get_backend_not_found(settings): + settings.WAGTAIL_AI = { + "BACKENDS": { + "default": { + "CLASS": "wagtail_ai.ai.echo.EchoBackend", + "CONFIG": {"MODEL_ID": "default", "TOKEN_LIMIT": 123123}, + }, + }, + } + with pytest.raises(BackendNotFound) as exception: + get_backend(BackendFeature.IMAGE_DESCRIPTION) + assert exception.match(r"No backend found for IMAGE_DESCRIPTION") diff --git a/tests/testapp/settings.py b/tests/testapp/settings.py index ef63a56..844026e 100644 --- a/tests/testapp/settings.py +++ b/tests/testapp/settings.py @@ -165,9 +165,18 @@ "CONFIG": { "MODEL_ID": "gpt-3.5-turbo", }, - } - } + }, + "vision": { + "CLASS": "wagtail_ai.ai.openai.OpenAIBackend", + "CONFIG": { + "MODEL_ID": "gpt-4-vision-preview", + "TOKEN_LIMIT": 300, + }, + }, + }, + "IMAGE_DESCRIPTION_BACKEND": "vision", } + else: WAGTAIL_AI = { "BACKENDS": { @@ -180,8 +189,11 @@ }, }, }, + "IMAGE_DESCRIPTION_BACKEND": "default", } +WAGTAILIMAGES_IMAGE_FORM_BASE = "wagtail_ai.forms.DescribeImageForm" + LOGGING = { "version": 1, diff --git a/tests/testapp/urls.py b/tests/testapp/urls.py index 088227c..bd4bc60 100644 --- a/tests/testapp/urls.py +++ b/tests/testapp/urls.py @@ -1,4 +1,7 @@ +from django.conf import settings +from django.conf.urls.static import static from django.contrib import admin +from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.urls import include, path from wagtail import urls as wagtail_urls from wagtail.admin import urls as wagtailadmin_urls @@ -8,5 +11,7 @@ path("django-admin/", admin.site.urls), path("admin/", include(wagtailadmin_urls)), path("documents/", include(wagtaildocs_urls)), + *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), + *staticfiles_urlpatterns(), path("", include(wagtail_urls)), ] diff --git a/tests/views/test_describe_image.py b/tests/views/test_describe_image.py new file mode 100644 index 0000000..fe1512e --- /dev/null +++ b/tests/views/test_describe_image.py @@ -0,0 +1,178 @@ +from http import HTTPStatus +from typing import cast +from unittest.mock import ANY, Mock, call + +import pytest +from django.contrib.auth.models import Permission, User +from django.urls import reverse +from wagtail.images.models import Image +from wagtail_ai.ai import echo +from wagtail_factories import ImageFactory + +pytestmark = pytest.mark.django_db + + +def test_get_request(admin_client): + response = admin_client.get(reverse("wagtail_ai:describe_image")) + assert response.status_code == 400 + assert response.json() == {"error": "This field is required."} + + +def test_image_not_found(admin_client): + response = admin_client.post( + reverse("wagtail_ai:describe_image"), data={"image_id": 1} + ) + assert response.status_code == 404 + + +def test_access_denied(client): + user = User.objects.create_user(username="test") + user.user_permissions.add(Permission.objects.get(codename="access_admin")) + client.force_login(user) + image = cast(Image, ImageFactory()) + response = client.post( + reverse("wagtail_ai:describe_image"), data={"image_id": image.pk} + ) + assert response.status_code == 403 + assert response.json() == {"error": "Access denied"} + + +def test_backend_not_configured(settings, admin_client): + settings.WAGTAIL_AI = { + "BACKENDS": { + "echo": { + "CLASS": "wagtail_ai.ai.echo.EchoBackend", + "CONFIG": { + "MODEL_ID": "echo", + "TOKEN_LIMIT": 123123, + }, + }, + }, + } + + image = cast(Image, ImageFactory()) + response = admin_client.post( + reverse("wagtail_ai:describe_image"), data={"image_id": image.pk} + ) + assert response.status_code == 400 + assert response.json() == { + "error": ( + "No backend is configured for image description. Please set" + " `IMAGE_DESCRIPTION_BACKEND` in `settings.WAGTAIL_AI`." + ), + } + + +def test_success(admin_client, settings): + settings.WAGTAIL_AI = { + "BACKENDS": { + "echo": { + "CLASS": "wagtail_ai.ai.echo.EchoBackend", + "CONFIG": { + "MODEL_ID": "echo", + "TOKEN_LIMIT": 123123, + }, + }, + }, + "IMAGE_DESCRIPTION_BACKEND": "echo", + } + + image = cast(Image, ImageFactory()) + response = admin_client.post( + reverse("wagtail_ai:describe_image"), data={"image_id": image.pk} + ) + assert response.status_code == 200 + assert response.json() == { + "message": "This is an echo backend: images/example.max-800x600.jpg" + } + + +def test_custom_prompt(admin_client, settings, monkeypatch: pytest.MonkeyPatch): + describe_image = Mock(return_value=echo.EchoResponse(iter([]))) + monkeypatch.setattr(echo.EchoBackend, "describe_image", describe_image) + + CUSTOM_PROMPT = "Other prompt" + settings.WAGTAIL_AI = { + "BACKENDS": { + "echo": { + "CLASS": "wagtail_ai.ai.echo.EchoBackend", + "CONFIG": { + "MODEL_ID": "echo", + "TOKEN_LIMIT": 123123, + }, + }, + }, + "IMAGE_DESCRIPTION_BACKEND": "echo", + "IMAGE_DESCRIPTION_PROMPT": CUSTOM_PROMPT, + } + + image = cast(Image, ImageFactory()) + admin_client.post(reverse("wagtail_ai:describe_image"), data={"image_id": image.pk}) + assert describe_image.call_args == call(image_file=ANY, prompt=CUSTOM_PROMPT) + + +def test_custom_rendition_filter(admin_client, settings): + settings.WAGTAIL_AI = { + "BACKENDS": { + "echo": { + "CLASS": "wagtail_ai.ai.echo.EchoBackend", + "CONFIG": { + "MODEL_ID": "echo", + "TOKEN_LIMIT": 123123, + }, + }, + }, + "IMAGE_DESCRIPTION_BACKEND": "echo", + "IMAGE_DESCRIPTION_RENDITION_FILTER": "max-100x100", + } + + image = cast(Image, ImageFactory()) + response = admin_client.post( + reverse("wagtail_ai:describe_image"), data={"image_id": image.pk} + ) + assert response.status_code == 200 + assert response.json() == { + "message": "This is an echo backend: images/example.max-100x100.jpg" + } + + +@pytest.mark.parametrize( + "maxlength,expected_status,error_message", + [ + (100, HTTPStatus.OK, None), + ( + -1, + HTTPStatus.BAD_REQUEST, + "Ensure this value is greater than or equal to 0.", + ), + ( + 10000, + HTTPStatus.BAD_REQUEST, + "Ensure this value is less than or equal to 4096.", + ), + ], +) +def test_maxlength_validation( + admin_client, settings, maxlength, expected_status, error_message +): + settings.WAGTAIL_AI = { + "BACKENDS": { + "echo": { + "CLASS": "wagtail_ai.ai.echo.EchoBackend", + "CONFIG": { + "MODEL_ID": "echo", + "TOKEN_LIMIT": 123123, + }, + }, + }, + "IMAGE_DESCRIPTION_BACKEND": "echo", + } + + image = cast(Image, ImageFactory()) + response = admin_client.post( + reverse("wagtail_ai:describe_image"), + data={"image_id": image.pk, "maxlength": maxlength}, + ) + assert response.status_code == expected_status + if error_message is not None: + assert response.json() == {"error": error_message} diff --git a/tests/test_views.py b/tests/views/test_text_completion.py similarity index 57% rename from tests/test_views.py rename to tests/views/test_text_completion.py index 7df7e62..4df3e97 100644 --- a/tests/test_views.py +++ b/tests/views/test_text_completion.py @@ -4,6 +4,8 @@ from django.urls import reverse from wagtail_ai.views import PromptEditForm, prompt_viewset +pytestmark = pytest.mark.django_db + def test_prompt_model_admin_form_validation(test_prompt_values): form_data = { @@ -17,34 +19,24 @@ def test_prompt_model_admin_form_validation(test_prompt_values): assert "method" in form.errors -@pytest.mark.django_db -def test_prompt_model_admin_viewset_list_view(client, setup_users, setup_prompt_object): +def test_prompt_model_admin_viewset_list_view(admin_client, setup_prompt_object): url = reverse(f"{prompt_viewset.url_prefix}:index") - superuser = setup_users - client.force_login(superuser) - response = client.get(url) + response = admin_client.get(url) assert response.status_code == 200 assert setup_prompt_object.label in str(response.content) -@pytest.mark.django_db -def test_prompt_model_admin_viewset_edit_view(client, setup_users, setup_prompt_object): +def test_prompt_model_admin_viewset_edit_view(admin_client, setup_prompt_object): url = reverse(f"{prompt_viewset.url_prefix}:edit", args=[setup_prompt_object.id]) - superuser = setup_users - client.force_login(superuser) - response = client.get(url) + response = admin_client.get(url) assert response.status_code == 200 assert setup_prompt_object.label in str(response.content) -@pytest.mark.django_db -def test_process_view_get_request(client, setup_users): - url = reverse("wagtail_ai:process") - - superuser = setup_users - client.force_login(superuser) +def test_process_view_get_request(admin_client): + url = reverse("wagtail_ai:text_completion") - response = client.get(url) + response = admin_client.get(url) assert response.status_code == 400 assert response.json() == { "error": "No text provided - please enter some text before using AI features. " @@ -52,14 +44,10 @@ def test_process_view_get_request(client, setup_users): } -@pytest.mark.django_db -def test_process_view_post_without_text(client, setup_users): - url = reverse("wagtail_ai:process") - - superuser = setup_users - client.force_login(superuser) +def test_process_view_post_without_text(admin_client): + url = reverse("wagtail_ai:text_completion") - response = client.post(url, data={}) + response = admin_client.post(url, data={}) assert response.status_code == 400 assert response.json() == { "error": "No text provided - please enter some text before using AI features. " @@ -67,33 +55,25 @@ def test_process_view_post_without_text(client, setup_users): } -@pytest.mark.django_db @pytest.mark.parametrize( "prompt", [None, "NOT-A-UUID", str(uuid.uuid1()), str(uuid.uuid4())] ) -def test_process_view_with_bad_prompt_id(client, setup_users, prompt): - url = reverse("wagtail_ai:process") - - superuser = setup_users - client.force_login(superuser) +def test_process_view_with_bad_prompt_id(admin_client, prompt): + url = reverse("wagtail_ai:text_completion") data = {"text": "test"} if prompt is not None: data["prompt"] = prompt - response = client.post(url, data=data) + response = admin_client.post(url, data=data) assert response.status_code == 400 assert response.json() == {"error": "Invalid prompt provided."} -@pytest.mark.django_db -def test_process_view_with_correct_prompt(client, setup_users, setup_prompt_object): - url = reverse("wagtail_ai:process") - - superuser = setup_users - client.force_login(superuser) +def test_process_view_with_correct_prompt(admin_client, setup_prompt_object): + url = reverse("wagtail_ai:text_completion") - response = client.post( + response = admin_client.post( url, data={"text": "test", "prompt": str(setup_prompt_object.uuid)} ) assert response.status_code == 200 diff --git a/tsconfig.json b/tsconfig.json index b20289b..a1b2916 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,8 +8,8 @@ "strictNullChecks": true, "esModuleInterop": true }, - "files": [ - "src/wagtail_ai/static_src/main.tsx", - "src/wagtail_ai/static_src/custom.d.ts" + "include": [ + "src/wagtail_ai/static_src/**/*.ts", + "src/wagtail_ai/static_src/**/*.tsx" ] } diff --git a/webpack.config.js b/webpack.config.js index 16f10b8..6216b36 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,7 +3,10 @@ const postcssPresetEnv = require('postcss-preset-env'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { - entry: './src/wagtail_ai/static_src/main.tsx', + entry: { + draftail: './src/wagtail_ai/static_src/draftail/main.tsx', + image_description: './src/wagtail_ai/static_src/image_description/main.tsx', + }, plugins: [new MiniCssExtractPlugin()], module: { rules: [ @@ -29,7 +32,7 @@ module.exports = { }, { test: /\.svg$/, - use: ['@svgr/webpack'], + type: 'asset/source', }, { test: /\.(png|jpg|gif)$/, @@ -50,6 +53,6 @@ module.exports = { }, output: { path: path.resolve(__dirname, 'src/wagtail_ai/static/wagtail_ai'), - filename: 'wagtail-ai.js', + filename: '[name].js', }, };