From 0c1a9959f24dbbacdd0a98337b236c4ef4355df9 Mon Sep 17 00:00:00 2001 From: Kilu Date: Mon, 10 Jun 2024 16:27:45 +0800 Subject: [PATCH] chore: support openai chat on web --- frontend/appflowy_web_app/README.md | 343 ++++++------------ frontend/appflowy_web_app/package.json | 5 + frontend/appflowy_web_app/pnpm-lock.yaml | 140 ++++--- .../src/components/app/App.tsx | 2 + .../src/components/chat/Answer.tsx | 30 ++ .../src/components/chat/Chat.tsx | 114 ++++++ .../src/components/chat/Search.tsx | 81 +++++ .../src/components/chat/fetch.ts | 97 +++++ .../src/components/chat/index.ts | 3 + .../src/components/chat/markdown.scss | 62 ++++ .../src/components/chat/types.ts | 12 + .../appflowy_web_app/src/pages/AFChatPage.tsx | 8 + frontend/appflowy_web_app/src/vite-env.d.ts | 1 + frontend/appflowy_web_app/tailwind.config.cjs | 1 - 14 files changed, 617 insertions(+), 282 deletions(-) create mode 100644 frontend/appflowy_web_app/src/components/chat/Answer.tsx create mode 100644 frontend/appflowy_web_app/src/components/chat/Chat.tsx create mode 100644 frontend/appflowy_web_app/src/components/chat/Search.tsx create mode 100644 frontend/appflowy_web_app/src/components/chat/fetch.ts create mode 100644 frontend/appflowy_web_app/src/components/chat/index.ts create mode 100644 frontend/appflowy_web_app/src/components/chat/markdown.scss create mode 100644 frontend/appflowy_web_app/src/components/chat/types.ts create mode 100644 frontend/appflowy_web_app/src/pages/AFChatPage.tsx diff --git a/frontend/appflowy_web_app/README.md b/frontend/appflowy_web_app/README.md index c5c8ebf51f..ee45879356 100644 --- a/frontend/appflowy_web_app/README.md +++ b/frontend/appflowy_web_app/README.md @@ -1,284 +1,151 @@ +

AppFlowy Web

- -

AppFlowy Web Project

- -
Welcome to the AppFlowy Web Project, a robust and versatile platform designed to bring the innovative features of -AppFlowy to the web. This project uniquely supports running as a desktop application via Tauri, and offers web -interfaces powered by WebAssembly (WASM). Dive into an exceptional development experience with high performance and -extensive capabilities.
- + + + + +
-## ๐Ÿ‘ Features +## ๐ŸŒŸ Introduction -- **Cross-Platform Compatibility**: Seamlessly run on desktop environments with Tauri, and on any web browser through - WASM. -- **High Performance**: Leverage the speed and efficiency of WebAssembly for your web interfaces. -- **Tauri Integration**: Build lightweight, secure, and efficient desktop applications. -- **Flexible Development**: Utilize a wide range of AppFlowy's functionalities in your web or desktop projects. +Welcome to the AppFlowy Web project! This project aims to bring the powerful features of AppFlowy to the web. Whether +you're a developer looking to contribute or a user eager to try out the latest features, this guide will help you get +started. -## ๐Ÿš€ Getting Started +AppFlowy Web is built with the following technologies: -### ๐Ÿ› ๏ธ Prerequisites +- **React**: A JavaScript library for building user interfaces. +- **TypeScript**: A typed superset of JavaScript that compiles to plain JavaScript. +- **Bun**: A fast all-in-one JavaScript runtime. +- **Nginx**: A high-performance web server. +- **Docker**: A platform to develop, ship, and run applications in containers. -Before you begin, ensure you have the following installed: +### Resource Sharing -- Node.js (v14 or later) -- Rust (latest stable version) -- Tauri prerequisites for your operating system -- PNPM (8.5.0) +To maintain consistency across different platforms, the Web project shares i18n translation files and Icons with the +Flutter project. This ensures a unified user experience and reduces duplication of effort in maintaining these +resources. -### ๐Ÿ—๏ธ Installation +- **i18n Translation Files**: The translation files are shared to provide a consistent localization experience across + both Web and Flutter applications. The path to the translation files is `frontend/resources/translations/`. -#### Clone the Repository + > The translation files are stored in JSON format and contain translations for different languages. The files are + named according to the language code (e.g., `en.json` for English, `es.json` for Spanish, etc.). - ```bash - git clone https://github.com/AppFlowy-IO/AppFlowy - ``` +- **Icons**: The icon set used in the Web project is the same as the one used in the Flutter project, ensuring visual + consistency. The icons are stored in the `frontend/resources/flowy_icons/` directory. -#### ๐ŸŒ Install the frontend dependencies: +Let's dive in and get the project up and running! ๐Ÿš€ - ```bash - cd frontend/appflowy_web_app - pnpm install - ``` +## ๐Ÿ›  Getting Started -#### ๐Ÿ–ฅ๏ธ Desktop Application (Tauri) (Optional) +### Prerequisites -> **Note**: if you want to run the web app in the browser, skip this step +Before you begin, make sure you have the following installed on your system: -- Follow the instructions [here](https://tauri.app/v1/guides/getting-started/prerequisites/) to install Tauri +- [Node.js](https://nodejs.org/) (v18.6.0) ๐ŸŒณ +- [pnpm](https://pnpm.io/) (package manager) ๐Ÿ“ฆ +- [Jest](https://jestjs.io/) (testing framework) ๐Ÿƒ +- [Cypress](https://www.cypress.io/) (end-to-end testing) ๐Ÿงช -##### Windows and Linux Prerequisites +### Clone the Repository -###### Windows only +First, clone the repository to your local machine: -- Install the Duckscript CLI and vcpkg +```bash +git clone https://github.com/AppFlowy-IO/AppFlowy.git +cd frontend/appflowy_web_app +``` - ```bash - cargo install --force duckscript_cli - vcpkg integrate install - ``` +### Install Dependencies -###### Linux only +Install the required dependencies using pnpm: -- Install the required dependencies +```bash +## ensure you have pnpm installed, if not run the following command +# npm install -g pnpm@8.5.0 - ```bash - sudo apt-get update - sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf - ``` +pnpm install +``` -- **Get error**: failed to run custom build command for librocksdb-sys v6.11.4 +### Start the Development Server - ```bash - sudo apt install clang - ``` +To start the development server, run the following command: -##### Install Tauri Dependencies +```bash +pnpm run dev +``` -- Install cargo-make +### ๐Ÿš€ Building for Production(Optional) - ```bash - cargo install --force cargo-make - ``` +if you want to run the production build, use the following commands +```bash +pnpm run build +pnpm run start +``` -- Install AppFlowy dev tools +This will start the application in development mode. Open http://localhost:3000 to view it in the browser. - ```bash - # install development tools - # make sure you are in the root directory of the project - cd frontend - cargo make appflowy-tauri-deps-tools - ``` +## ๐Ÿงช Running Tests -- Build the service/dependency +### Unit Tests - ```bash - # make sure you are in the root directory of the project - cd frontend/appflowy_web_app - mkdir dist - cd src-tauri - cargo build - ``` +We use **Jest** for running unit tests. To run the tests, use the following command: -### ๐Ÿš€ Running the Application +```bash +pnpm run test:unit +``` -#### ๐ŸŒ Web Application +This will execute all the unit tests in the project and provide a summary of the results. โœ… -- Run the web application +### Components Tests - ```bash - pnpm run dev - ``` -- Open your browser and navigate to `http://localhost:3000`, You can now interact with the AppFlowy web application +We use **Cypress** for end-to-end testing. To run the Cypress tests, use the following command: -#### ๐Ÿ–ฅ๏ธ Desktop Application (Tauri) +```bash +pnpm run cypress:open +``` -**Ensure close web application before running the desktop application** +This will open the Cypress Test Runner where you can run your end-to-end tests. ๐Ÿงช -- Run the desktop application - - ```bash - pnpm run tauri:dev - ``` -- The AppFlowy desktop application will open, and you can interact with it - -### ๐Ÿ› ๏ธ Development - -#### How to add or modify i18n keys - -- Modify the i18n files in `frontend/resources/translations/en.json` to add or modify i18n keys -- Run the following command to update the i18n keys in the application - - ```bash - pnpm run sync:i18n - ``` - -#### How to modify the theme - -Don't modify the theme file in `frontend/appflowy_web_app/src/styles/variables` directly) - -- Modify the theme file in `frontend/appflowy_web_app/style-dictionary/tokens/base.json( or dark.json or light.json)` to - add or modify theme keys -- Run the following command to update the theme in the application - - ```bash - pnpm run css:variables - ``` - -#### How to add or modify the environment variables - -- Modify the environment file in `frontend/appflowy_web_app/.env` to add or modify environment variables - -#### How to create symlink for the @appflowyinc/client-api-wasm in local development - -- Run the following command to create a symlink for the @appflowyinc/client-api-wasm - - ```bash - # ensure you are in the frontend/appflowy_web_app directory - - pnpm run link:client-api $source_path $target_path - - # Example - # pnpm run link:client-api ../../../AppFlowy-Cloud/libs/client-api-wasm/pkg ./node_modules/@appflowyinc/client-api-wasm - ``` - -### ๐Ÿ“ About the Project - -#### ๐Ÿ“ Directory Structure - -- `frontend/appflowy_web_app`: Contains the web application source code -- `frontend/appflowy_web_app/src`: Contains the app entry point and the source code -- `frontend/appflowy_web_app/src/components`: Contains the react components -- `frontend/appflowy_web_app/src/styles`: Contains the styles for the application -- `frontend/appflowy_web_app/src/utils`: Contains the utility functions -- `frontend/appflowy_web_app/src/i18n`: Contains the i18n files -- `frontend/appflowy_web_app/src/assets`: Contains the assets for the application -- `frontend/appflowy_web_app/src/store`: Contains the redux store -- `frontend/appflowy_web_app/src/@types`: Contains the typescript types -- `frontend/appflowy_web_app/src/applications/services`: Contains the services for the application. In vite.config.ts, - we have defined the alias for the services directory for different environments(Tauri/Web) - ```typescript - resolve: { - alias: [ - // ... - { - find: '$client-services', - replacement: !!process.env.TAURI_PLATFORM - ? `${__dirname}/src/application/services/tauri-services` - : `${__dirname}/src/application/services/js-services`, - }, - ] - } - ``` - -### ๐Ÿ“ฆ Deployment - -Use the AppFlowy CI/CD pipeline to deploy the application to the test and production environments. - -- Push the changes to the main branch -- Deploy Test Environment - - Automatically, the test environment will be deployed if merged to the main branch or build/test branch -- Deploy Production Environment - - Navigate to the Actions tab - - Click on the workflow and select the Run workflow - - Enter the options - - Click on the Run workflow button - -#### ๐Ÿ“ฆ Deployment (Self-Hosted EC2) - -##### Pre-requisites - -Please ensure you have learned about: - -- [Deploy Web application on AWS Cloud using EC2 Instance](https://www.youtube.com/watch?v=gWVIIU1ev0Y) -- [How to Install and Use Rsync Command](https://operavps.com/docs/install-rsync-command-in-linux/) -- [How to Use ssh-keygen to Generate a New SSH Key?](https://www.ssh.com/academy/ssh/keygen) -- [Linux post-installation steps for Docker Engine](https://docs.docker.com/engine/install/linux-postinstall/) -- [Configuring HTTPS servers](https://nginx.org/en/docs/http/configuring_https_servers.html) - -And then follow the steps below: - -1. Ensure you have the following installed on your server: - - Docker: [Install Docker](https://docs.docker.com/engine/install/) - - Rsync: [Install Rsync](https://operavps.com/docs/install-rsync-command-in-linux/) - -2. Create a new user for deploy, and generate an SSH key for the user - - ```bash - sudo adduser appflowy(or any name) - sudo su - appflowy - mkdir ~/.ssh - chmod 700 ~/.ssh - ssh-keygen -t rsa - chmod 600 ~/.ssh/authorized_keys - # add the user to the docker group, to run docker commands without sudo - sudo usermod -aG docker ${USER} - ``` - - visit the `~/.ssh/id_rsa` and `~/.ssh/id_rsa.pub` to get the private and public key respectively - - add the public key to the `~/.ssh/authorized_keys` file - - ensure the private key is kept safe - - exit and login back to the server with the new - user: `ssh -i your-existing-key.pem ec2-user@your-instance-public-dns` - -3. Clone the AppFlowy repository - -4. Set the following secrets in your - repository, have to - know [Using secrets in GitHub Actions](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) - -> Note: Test Environment: prefix the secret with `WEB_TEST_` and Production Environment: prefix the secret with `WEB_` - -> for example, `WEB_TEST_SSH_PRIVATE_KEY` and `WEB_SSH_PRIVATE_KEY` - -- `SSH_PRIVATE_KEY`: The private key generated in step 2: cat ~/.ssh/id_rsa -- `REMOTE_HOST`: The host of the server: `your-instance-public-dns` or `your-instance-ip` -- `REMOTE_USER`: The user created in step 2: `appflowy` -- `SSL_CERTIFICATE`: The SSL certificate for the - server - [Configuring HTTPS servers](https://nginx.org/en/docs/http/configuring_https_servers.html) -- `SSL_CERTIFICATE_KEY`: The SSL certificate key for the - server - [Configuring HTTPS servers](https://nginx.org/en/docs/http/configuring_https_servers.html) - -5. Run the deployment workflow to deploy the application(production or test environment) - -> Note: the test server will **automatically** deploy if merged to the main branch or build/test branch - -### ๐Ÿงช Testing - -> We use Cypress for end-to-end testing and component testing - [Cypress](https://www.cypress.io/) - -#### ๐Ÿงช End-to-End Testing - -> to be continued - -#### ๐Ÿงช Component Testing - -Run the following command to run the component tests +Alternatively, to run Cypress tests in the headless mode, use: ```bash pnpm run test:components ``` +Both commands will provide detailed test results and generate a code coverage report. + +## ๐Ÿ”„ Development Workflow + +### Linting + +To maintain code quality, we use **ESLint**. To run the linter and fix any linting errors, use the following command: + +```bash +pnpm run lint +``` + +## ๐Ÿš€ Production Deployment + +Our production deployment process is automated using GitHub Actions. The process involves: + +1. **Setting up an AWS EC2 instance**: We use an EC2 instance to host the application. +2. **Installing Docker and Docker Compose**: Docker is installed on the AWS instance. +3. **Configuring SSH Access**: SSH access is set up with a user and password. +4. **Preparing Project Configuration**: We configure `Dockerfile`, `nginx.conf`, and `server.cjs` in the web project. +5. **Using GitHub Actions**: We use the easingthemes/ssh-deploy@main action to deploy the project to the remote server. + +The deployment steps include building the Docker image and running the Docker container with the necessary port +mappings: + +```bash +docker build -t appflowy-web-app . +docker rm -f appflowy-web-app || true +docker run -d -p 80:80 -p 443:443 --name appflowy-web-app appflowy-web-app +``` + +The Web server runs on Bun. For more details about Bun, please refer to the [Bun documentation](https://bun.sh/). diff --git a/frontend/appflowy_web_app/package.json b/frontend/appflowy_web_app/package.json index f822a6b0f9..beb77ef937 100644 --- a/frontend/appflowy_web_app/package.json +++ b/frontend/appflowy_web_app/package.json @@ -36,6 +36,7 @@ "@mui/x-date-pickers-pro": "^6.18.2", "@reduxjs/toolkit": "2.0.0", "@slate-yjs/core": "^1.0.2", + "@tailwindcss/typography": "^0.5.13", "@tauri-apps/api": "^1.5.3", "@types/react-swipeable-views": "^0.13.4", "async-retry": "^1.3.3", @@ -43,9 +44,11 @@ "colorthief": "^2.4.0", "dayjs": "^1.11.9", "decimal.js": "^10.4.3", + "dompurify": "^3.1.5", "emoji-mart": "^5.5.2", "emoji-regex": "^10.2.1", "events": "^3.3.0", + "eventsource-parser": "^1.1.2", "google-protobuf": "^3.15.12", "i18next": "^22.4.10", "i18next-browser-languagedetector": "^7.0.1", @@ -55,6 +58,7 @@ "js-base64": "^3.7.5", "katex": "^0.16.7", "lodash-es": "^4.17.21", + "markdown-it": "^14.1.0", "nanoid": "^4.0.0", "numeral": "^2.0.6", "prismjs": "^1.29.0", @@ -107,6 +111,7 @@ "@svgr/plugin-svgo": "^8.0.1", "@tauri-apps/cli": "^1.5.11", "@testing-library/react": "^16.0.0", + "@types/dompurify": "^3.0.5", "@types/google-protobuf": "^3.15.12", "@types/is-hotkey": "^0.1.7", "@types/jest": "^29.5.3", diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml index 194beaa5dd..bb66a8bf1d 100644 --- a/frontend/appflowy_web_app/pnpm-lock.yaml +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: '@appflowyinc/client-api-wasm': specifier: 0.0.3 @@ -37,6 +41,9 @@ dependencies: '@slate-yjs/core': specifier: ^1.0.2 version: 1.0.2(slate@0.101.5)(yjs@13.6.15) + '@tailwindcss/typography': + specifier: ^0.5.13 + version: 0.5.13(tailwindcss@3.2.7) '@tauri-apps/api': specifier: ^1.5.3 version: 1.5.6 @@ -58,6 +65,9 @@ dependencies: decimal.js: specifier: ^10.4.3 version: 10.4.3 + dompurify: + specifier: ^3.1.5 + version: 3.1.5 emoji-mart: specifier: ^5.5.2 version: 5.6.0 @@ -67,6 +77,9 @@ dependencies: events: specifier: ^3.3.0 version: 3.3.0 + eventsource-parser: + specifier: ^1.1.2 + version: 1.1.2 google-protobuf: specifier: ^3.15.12 version: 3.21.2 @@ -94,6 +107,9 @@ dependencies: lodash-es: specifier: ^4.17.21 version: 4.17.21 + markdown-it: + specifier: ^14.1.0 + version: 14.1.0 nanoid: specifier: ^4.0.0 version: 4.0.2 @@ -246,6 +262,9 @@ devDependencies: '@testing-library/react': specifier: ^16.0.0 version: 16.0.0(@testing-library/dom@10.1.0)(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@types/dompurify': + specifier: ^3.0.5 + version: 3.0.5 '@types/google-protobuf': specifier: ^3.15.12 version: 3.15.12 @@ -3363,12 +3382,10 @@ packages: dependencies: '@nodelib/fs.stat': 2.0.5 run-parallel: 1.2.0 - dev: true /@nodelib/fs.stat@2.0.5: resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} engines: {node: '>= 8'} - dev: true /@nodelib/fs.walk@1.2.8: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} @@ -3376,7 +3393,6 @@ packages: dependencies: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 - dev: true /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} @@ -3821,6 +3837,18 @@ packages: - typescript dev: true + /@tailwindcss/typography@0.5.13(tailwindcss@3.2.7): + resolution: {integrity: sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + dependencies: + lodash.castarray: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + postcss-selector-parser: 6.0.10 + tailwindcss: 3.2.7(postcss@8.4.21) + dev: false + /@tauri-apps/api@1.5.6: resolution: {integrity: sha512-LH5ToovAHnDVe5Qa9f/+jW28I6DeMhos8bNDtBOmmnaDpPmJmYLyHdeDblAWWWYc7KKRDg9/66vMuKyq0WIeFA==} engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} @@ -4029,6 +4057,12 @@ packages: resolution: {integrity: sha512-p9eZ2X9B80iKiTW4ukVj8B4K6q9/+xFtQ5MGYA5HWToY9nL4EkhV9+6ftT2VHpVMEZb5Tv00Iel516bVdO+yRw==} dev: true + /@types/dompurify@3.0.5: + resolution: {integrity: sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==} + dependencies: + '@types/trusted-types': 2.0.7 + dev: true + /@types/eslint-scope@3.7.7: resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} dependencies: @@ -4273,6 +4307,10 @@ packages: resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} dev: true + /@types/trusted-types@2.0.7: + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + dev: true + /@types/use-sync-external-store@0.0.3: resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} dev: false @@ -4609,12 +4647,10 @@ packages: acorn: 7.4.1 acorn-walk: 7.2.0 xtend: 4.0.2 - dev: true /acorn-walk@7.2.0: resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} engines: {node: '>=0.4.0'} - dev: true /acorn-walk@8.3.2: resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} @@ -4625,7 +4661,6 @@ packages: resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} engines: {node: '>=0.4.0'} hasBin: true - dev: true /acorn@8.11.3: resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} @@ -4768,7 +4803,6 @@ packages: /arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - dev: true /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -4777,7 +4811,6 @@ packages: /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true /aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -5219,7 +5252,6 @@ packages: /camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - dev: true /camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} @@ -5600,7 +5632,6 @@ packages: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true - dev: true /csso@5.0.5: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} @@ -5836,7 +5867,6 @@ packages: /defined@1.0.1: resolution: {integrity: sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==} - dev: true /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} @@ -5858,11 +5888,9 @@ packages: acorn-node: 1.8.2 defined: 1.0.1 minimist: 1.2.8 - dev: true /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - dev: true /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} @@ -5887,7 +5915,6 @@ packages: /dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - dev: true /doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} @@ -5949,6 +5976,10 @@ packages: domelementtype: 2.3.0 dev: true + /dompurify@3.1.5: + resolution: {integrity: sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA==} + dev: false + /domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} dependencies: @@ -6036,7 +6067,6 @@ packages: /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - dev: true /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -6384,6 +6414,11 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + /eventsource-parser@1.1.2: + resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==} + engines: {node: '>=14.18'} + dev: false + /execa@4.1.0: resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} engines: {node: '>=10'} @@ -6483,7 +6518,6 @@ packages: glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.5 - dev: true /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -6496,7 +6530,6 @@ packages: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} dependencies: reusify: 1.0.4 - dev: true /fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} @@ -6790,7 +6823,6 @@ packages: engines: {node: '>=10.13.0'} dependencies: is-glob: 4.0.3 - dev: true /glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} @@ -8089,11 +8121,16 @@ packages: /lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} - dev: true /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + /linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + dependencies: + uc.micro: 2.1.0 + dev: false + /listr2@3.14.0(enquirer@2.4.1): resolution: {integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==} engines: {node: '>=10.0.0'} @@ -8151,6 +8188,10 @@ packages: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} dev: false + /lodash.castarray@4.4.0: + resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} + dev: false + /lodash.clonedeep@4.5.0: resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} @@ -8165,13 +8206,16 @@ packages: /lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: false + /lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} dev: true /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - dev: true /lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} @@ -8273,6 +8317,18 @@ packages: dependencies: tmpl: 1.0.5 + /markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + dev: false + /material-colors@1.2.6: resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==} dev: false @@ -8285,6 +8341,10 @@ packages: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} dev: true + /mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + dev: false + /memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} dev: false @@ -8299,7 +8359,6 @@ packages: /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - dev: true /micromatch@4.0.5: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} @@ -8343,7 +8402,6 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - dev: true /minipass@7.0.4: resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} @@ -8507,7 +8565,6 @@ packages: /object-hash@3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} - dev: true /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} @@ -8803,7 +8860,6 @@ packages: /pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} - dev: true /pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} @@ -8854,7 +8910,6 @@ packages: postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.8 - dev: true /postcss-js@4.0.1(postcss@8.4.21): resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} @@ -8864,7 +8919,6 @@ packages: dependencies: camelcase-css: 2.0.1 postcss: 8.4.21 - dev: true /postcss-load-config@3.1.4(postcss@8.4.21): resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} @@ -8881,7 +8935,6 @@ packages: lilconfig: 2.1.0 postcss: 8.4.21 yaml: 1.10.2 - dev: true /postcss-nested@6.0.0(postcss@8.4.21): resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==} @@ -8891,7 +8944,14 @@ packages: dependencies: postcss: 8.4.21 postcss-selector-parser: 6.0.16 - dev: true + + /postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: false /postcss-selector-parser@6.0.16: resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==} @@ -8899,11 +8959,9 @@ packages: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - dev: true /postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - dev: true /postcss@8.4.21: resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} @@ -8912,7 +8970,6 @@ packages: nanoid: 3.3.7 picocolors: 1.0.0 source-map-js: 1.2.0 - dev: true /postcss@8.4.38: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} @@ -9084,6 +9141,11 @@ packages: pump: 2.0.1 dev: true + /punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + dev: false + /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -9109,7 +9171,6 @@ packages: /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true /queue-tick@1.0.1: resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} @@ -9118,7 +9179,6 @@ packages: /quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} - dev: true /quill-delta@3.6.3: resolution: {integrity: sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==} @@ -9621,7 +9681,6 @@ packages: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} dependencies: pify: 2.3.0 - dev: true /readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -9827,7 +9886,6 @@ packages: /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true /rfdc@1.3.1: resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} @@ -9891,7 +9949,6 @@ packages: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: queue-microtask: 1.2.3 - dev: true /rxjs@7.8.0: resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==} @@ -10437,7 +10494,6 @@ packages: resolve: 1.22.8 transitivePeerDependencies: - ts-node - dev: true /tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} @@ -10809,6 +10865,10 @@ packages: hasBin: true dev: true + /uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + dev: false + /ufo@1.5.3: resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} dev: true @@ -10940,7 +11000,6 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: true /uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} @@ -11306,7 +11365,6 @@ packages: /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - dev: true /y-indexeddb@9.0.12(yjs@13.6.15): resolution: {integrity: sha512-9oCFRSPPzBK7/w5vOkJBaVCQZKHXB/v6SIT+WYhnJxlEC61juqG0hBrAf+y3gmSMLFLwICNH9nQ53uscuse6Hg==} @@ -11414,7 +11472,3 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: true - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/frontend/appflowy_web_app/src/components/app/App.tsx b/frontend/appflowy_web_app/src/components/app/App.tsx index d7e9037ad5..fde97fdcdb 100644 --- a/frontend/appflowy_web_app/src/components/app/App.tsx +++ b/frontend/appflowy_web_app/src/components/app/App.tsx @@ -1,3 +1,4 @@ +import AfChatPage from '@/pages/AFChatPage'; import FolderPage from '@/pages/FolderPage'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import ProtectedRoutes from '@/components/auth/ProtectedRoutes'; @@ -13,6 +14,7 @@ const AppMain = withAppWrapper(() => { } /> } /> + } /> ); }); diff --git a/frontend/appflowy_web_app/src/components/chat/Answer.tsx b/frontend/appflowy_web_app/src/components/chat/Answer.tsx new file mode 100644 index 0000000000..283881daaa --- /dev/null +++ b/frontend/appflowy_web_app/src/components/chat/Answer.tsx @@ -0,0 +1,30 @@ +import { FC, useMemo } from 'react'; +import DOMPurify from 'dompurify'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-expect-error +import markdownit from 'markdown-it'; + +interface AnswerProps { + query: string; + answer: string; + done: boolean; +} + +export const Answer: FC = ({ query, answer }) => { + const md = useMemo(() => markdownit(), []); + + return ( +
+
{`Q: ${query} `}
+ +
+
+
+
+ ); +}; diff --git a/frontend/appflowy_web_app/src/components/chat/Chat.tsx b/frontend/appflowy_web_app/src/components/chat/Chat.tsx new file mode 100644 index 0000000000..d424958c4a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/chat/Chat.tsx @@ -0,0 +1,114 @@ +import { AFScroller } from '@/components/_shared/scroller'; +import { SearchQuery } from '@/components/chat/types'; +import { Tooltip } from '@mui/material'; +import React, { useEffect, useRef, useState } from 'react'; +import { Answer } from './Answer'; +import { Search } from './Search'; +import { ReactComponent as ReloadICON } from '$icons/16x/reload.svg'; +import './markdown.scss'; + +export function Chat() { + const [searchQuery, setSearchQuery] = useState({ query: '' }); + const [answer, setAnswer] = useState(''); + const [apiKeyValue, setApiKeyValue] = useState(''); + const [apiKey, setApiKey] = useState(() => { + const key = localStorage.getItem('openai-api-key'); + + return key ? key : ''; + }); + const conversationRef = useRef< + { + query: string; + answer: string; + }[] + >([]); + + const [done, setDone] = useState(false); + + useEffect(() => { + if (!done) { + return; + } + + if (!answer) { + return; + } + + if (conversationRef.current.length === 0) { + conversationRef.current = [ + { + query: searchQuery.query, + answer, + }, + ]; + } else { + conversationRef.current.push({ + query: searchQuery.query, + answer, + }); + } + + setSearchQuery({ query: '' }); + setAnswer(''); + }, [answer, done, searchQuery.query]); + return ( +
+
+ + + +
+ + {conversationRef.current?.map((item, index) => ( + + ))} + {answer && } + +
+ {apiKey ? ( + { + setAnswer((prev) => prev + value); + }} + done={done} + onDone={setDone} + /> + ) : ( +
+ setApiKeyValue(e.target.value)} + className={'h-12 w-full rounded-md border border-gray-300 px-4'} + /> + +
+ )} +
+
+ ); +} + +export default Chat; diff --git a/frontend/appflowy_web_app/src/components/chat/Search.tsx b/frontend/appflowy_web_app/src/components/chat/Search.tsx new file mode 100644 index 0000000000..d498c052b9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/chat/Search.tsx @@ -0,0 +1,81 @@ +import { fetchData } from '@/components/chat/fetch'; +import CircularProgress from '@mui/material/CircularProgress'; +import { SearchQuery } from './types'; +import { FC, useRef, useState } from 'react'; +import { ReactComponent as IconArrowRight } from '$icons/16x/arrow_right.svg'; +import { ReactComponent as IconSearch } from '$icons/16x/search.svg'; + +interface SearchProps { + onSearch: (searchResult: SearchQuery) => void; + onAnswerUpdate: (answer: string) => void; + onDone: (done: boolean) => void; + done: boolean; + apiKey: string; +} + +export const Search: FC = ({ apiKey, onSearch, onAnswerUpdate, onDone }) => { + const inputRef = useRef(null); + const [query, setQuery] = useState(''); + const [loading, setLoading] = useState(false); + const handleSearch = async () => { + if (!query) { + alert('Please enter a query'); + return; + } + + onDone(false); + onSearch({ query }); + setLoading(true); + + await fetchData(apiKey, query, { + onAnswerUpdate, + onDone: (done) => { + console.log('done', done); + onDone(done); + setLoading(false); + setQuery(''); + }, + }); + }; + + return ( + <> +
+
+ + + setQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + void handleSearch(); + } + }} + /> + + +
+
+ + ); +}; diff --git a/frontend/appflowy_web_app/src/components/chat/fetch.ts b/frontend/appflowy_web_app/src/components/chat/fetch.ts new file mode 100644 index 0000000000..a9cd4ec326 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/chat/fetch.ts @@ -0,0 +1,97 @@ +import { OpenAIModel } from '@/components/chat/types'; + +const url = 'https://api.openai.com/v1/chat/completions'; + +// Create an AbortController to control and cancel the fetch request when the user hits the stop button +const controller = new AbortController(); + +export function stopFetch() { + controller.abort(); +} + +export async function fetchData( + apiKey: string, + searchContent: string, + { + onAnswerUpdate, + onDone, + }: { + onAnswerUpdate: (line: string) => void; + onDone: (done: boolean) => void; + } +) { + const prompt = `Generate a comprehensive summary in Markdown format on the topic of ${searchContent}. Include sections for definition, importance, current technologies, challenges, and future perspectives. Use headers, bullet points, and links where appropriate.`; + + // Make a POST request to the OpenAI API to get chat completions + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + messages: [ + { + role: 'user', + content: prompt, + }, + ], + temperature: 0.6, + model: OpenAIModel.DAVINCI_TURBO, + // Limiting the tokens during development + max_tokens: 150, + stream: true, + }), + // Use the AbortController's signal to allow aborting the request + // This is a `fetch()` API thing, not an OpenAI thing + signal: controller.signal, + }); + + if (!response.body) { + return; + } + + const data = response.body; + + if (!data) { + return; + } + + // Create a TextDecoder to decode the response body stream + const decoder = new TextDecoder(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + for await (const chunk of response.body as any) { + const decodedChunk = decoder.decode(chunk); + + const isDone = decodedChunk.includes('[DONE]'); + + if (isDone) { + onDone(true); + break; + } + + // Clean up the data + const lines = decodedChunk + .split('\n') + .map((line) => line.replace('data: ', '')) + .filter((line) => line.length > 0) + .filter((line) => line !== '[DONE]') + .map((line) => JSON.parse(line)); + + // Destructuring! + for (const line of lines) { + const { + choices: [ + { + delta: { content }, + }, + ], + } = line; + + if (content) { + onAnswerUpdate(content); + } + } + } +} diff --git a/frontend/appflowy_web_app/src/components/chat/index.ts b/frontend/appflowy_web_app/src/components/chat/index.ts new file mode 100644 index 0000000000..07ad80384f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/chat/index.ts @@ -0,0 +1,3 @@ +import { lazy } from 'react'; + +export const Chat = lazy(() => import('./Chat')); diff --git a/frontend/appflowy_web_app/src/components/chat/markdown.scss b/frontend/appflowy_web_app/src/components/chat/markdown.scss new file mode 100644 index 0000000000..225f94921c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/chat/markdown.scss @@ -0,0 +1,62 @@ + +.markdown-content { + & h1, & h2, & h3, & h4, & h5, & h6 { + @apply text-gray-800 font-semibold; + } + + & h1 { + @apply text-3xl my-6; + } + + & h2 { + @apply text-2xl my-4; + } + + & h3 { + @apply text-xl my-3; + } + + & p, & ul, & ol { + @apply text-base leading-relaxed text-gray-700; + } + + & a { + @apply text-blue-500 hover:text-blue-700; + } + + & strong { + @apply font-bold; + } + + & em { + @apply italic; + } + + & blockquote { + @apply border-l-4 border-gray-200 pl-4 italic text-gray-500; + } + + & ul, & ol { + @apply pl-5 list-decimal; + } + + & ul { + @apply list-disc; + } + + & code { + @apply bg-gray-100 text-gray-900 p-2 rounded; + } + + & pre { + @apply bg-gray-200 text-gray-800 p-3 overflow-x-auto; + } + + & img { + @apply my-4 mx-auto max-w-full h-auto; + } + + & hr { + @apply my-4 border-t border-gray-300; + } +} diff --git a/frontend/appflowy_web_app/src/components/chat/types.ts b/frontend/appflowy_web_app/src/components/chat/types.ts new file mode 100644 index 0000000000..4aac582d78 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/chat/types.ts @@ -0,0 +1,12 @@ +export enum OpenAIModel { + DAVINCI_TURBO = 'gpt-3.5-turbo', +} + +export type Source = { + url: string; + text: string; +}; + +export type SearchQuery = { + query: string; +}; diff --git a/frontend/appflowy_web_app/src/pages/AFChatPage.tsx b/frontend/appflowy_web_app/src/pages/AFChatPage.tsx new file mode 100644 index 0000000000..4b83079135 --- /dev/null +++ b/frontend/appflowy_web_app/src/pages/AFChatPage.tsx @@ -0,0 +1,8 @@ +import { Chat } from '@/components/chat'; +import React from 'react'; + +function AfChatPage() { + return ; +} + +export default AfChatPage; diff --git a/frontend/appflowy_web_app/src/vite-env.d.ts b/frontend/appflowy_web_app/src/vite-env.d.ts index 5748ee1aed..0f81773c89 100644 --- a/frontend/appflowy_web_app/src/vite-env.d.ts +++ b/frontend/appflowy_web_app/src/vite-env.d.ts @@ -9,4 +9,5 @@ interface Window { WebFont?: { load: (options: { google: { families: string[] } }) => void; }; + markdownit: () => any; } diff --git a/frontend/appflowy_web_app/tailwind.config.cjs b/frontend/appflowy_web_app/tailwind.config.cjs index 8589a0f4b6..99511e078c 100644 --- a/frontend/appflowy_web_app/tailwind.config.cjs +++ b/frontend/appflowy_web_app/tailwind.config.cjs @@ -16,5 +16,4 @@ module.exports = { boxShadow, }, }, - plugins: [], };