├── .certs └── .gitignore ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Cargo.toml ├── Makefile ├── README.md ├── _typos.toml ├── cliff.toml ├── deny.toml ├── input.css ├── package-lock.json ├── package.json ├── public ├── css │ └── .gitignore └── images │ └── ava-small.png ├── src ├── error.rs ├── extractors │ └── mod.rs ├── handlers │ ├── assistant.rs │ ├── chats.rs │ ├── common.rs │ └── mod.rs ├── lib.rs ├── main.rs └── tools │ └── mod.rs ├── tailwind.config.js ├── templates ├── base.html.j2 ├── blocks │ ├── image.html.j2 │ ├── markdown.html.j2 │ └── speech.html.j2 ├── events │ ├── chat_input.html.j2 │ ├── chat_input_skeleton.html.j2 │ ├── chat_reply.html.j2 │ ├── chat_reply_skeleton.html.j2 │ └── signal.html.j2 └── index.html.j2 └── test.http /.certs/.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build-rust: 13 | strategy: 14 | matrix: 15 | platform: [ubuntu-latest] 16 | runs-on: ${{ matrix.platform }} 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | - name: Install Rust 22 | run: rustup toolchain install stable --component llvm-tools-preview 23 | - name: Install cargo-llvm-cov 24 | uses: taiki-e/install-action@cargo-llvm-cov 25 | - name: install nextest 26 | uses: taiki-e/install-action@nextest 27 | - uses: Swatinem/rust-cache@v1 28 | - name: Check code format 29 | run: cargo fmt -- --check 30 | - name: Check the package for errors 31 | run: cargo check --all 32 | - name: Lint rust sources 33 | run: cargo clippy --all-targets --all-features --tests --benches -- -D warnings 34 | - name: Execute rust tests 35 | run: cargo nextest run --all-features 36 | env: 37 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | Cargo.lock 3 | /target/ 4 | 5 | # Node.js 6 | node_modules/ 7 | npm-debug.log 8 | yarn-error.log 9 | 10 | # macOS 11 | .DS_Store 12 | 13 | # python 14 | .venv 15 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: false 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.3.0 5 | hooks: 6 | - id: check-byte-order-marker 7 | - id: check-case-conflict 8 | - id: check-merge-conflict 9 | - id: check-symlinks 10 | - id: check-yaml 11 | - id: end-of-file-fixer 12 | - id: mixed-line-ending 13 | - id: trailing-whitespace 14 | - repo: https://github.com/psf/black 15 | rev: 22.10.0 16 | hooks: 17 | - id: black 18 | - repo: local 19 | hooks: 20 | - id: cargo-fmt 21 | name: cargo fmt 22 | description: Format files with rustfmt. 23 | entry: bash -c 'cargo fmt -- --check' 24 | language: rust 25 | files: \.rs$ 26 | args: [] 27 | - id: cargo-deny 28 | name: cargo deny check 29 | description: Check cargo dependencies 30 | entry: bash -c 'cargo deny check -d' 31 | language: rust 32 | files: \.rs$ 33 | args: [] 34 | - id: typos 35 | name: typos 36 | description: check typo 37 | entry: bash -c 'typos' 38 | language: rust 39 | files: \.*$ 40 | pass_filenames: false 41 | - id: cargo-check 42 | name: cargo check 43 | description: Check the package for errors. 44 | entry: bash -c 'cargo check --all' 45 | language: rust 46 | files: \.rs$ 47 | pass_filenames: false 48 | - id: cargo-clippy 49 | name: cargo clippy 50 | description: Lint rust sources 51 | entry: bash -c 'cargo clippy --all-targets --all-features --tests --benches -- -D warnings' 52 | language: rust 53 | files: \.rs$ 54 | pass_filenames: false 55 | - id: cargo-test 56 | name: cargo test 57 | description: unit test for the project 58 | entry: bash -c 'cargo nextest run --all-features' 59 | language: rust 60 | files: \.rs$ 61 | pass_filenames: false 62 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ava-bot" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.75" 10 | askama = { version = "0.12.1", features = ["with-axum"] } 11 | askama_axum = "0.3.0" 12 | axum = { version = "0.6.20", features = [ 13 | "http2", 14 | "headers", 15 | "multipart", 16 | "query", 17 | "tracing", 18 | ] } 19 | axum-extra = { version = "0.8.0", features = ["cookie"] } 20 | axum-server = { version = "0.5.1", features = ["tls-rustls"] } 21 | base64 = "0.21.5" 22 | chrono = { version = "0.4.31", features = ["serde"] } 23 | clap = { version = "4.4.8", features = ["derive"] } 24 | comrak = { version = "0.19.0", default-features = false, features = ["syntect"] } 25 | dashmap = "5.5.3" 26 | derive_more = "0.99.17" 27 | futures = "0.3.29" 28 | llm-sdk = "0.3.0" 29 | schemars = "0.8.16" 30 | serde = { version = "1.0.192", features = ["derive"] } 31 | serde_json = "1.0.108" 32 | strum = { version = "0.25.0", features = ["derive"] } 33 | tokio = { version = "1.34.0", features = ["rt", "rt-multi-thread", "macros"] } 34 | tokio-stream = { version = "0.1.14", features = ["sync"] } 35 | tower-http = { version = "0.4.4", features = [ 36 | "compression-full", 37 | "cors", 38 | "trace", 39 | "fs", 40 | ] } 41 | tracing = "0.1.40" 42 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } 43 | uuid = { version = "1.5.0", features = ["v4", "serde"] } 44 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: build-css 2 | @RUST_LOG=info cargo run 3 | 4 | run-release: build-css 5 | @RUST_LOG=info cargo run --release 6 | 7 | watch: 8 | @watchexec --restart --exts rs,js,css,j2 --ignore public -- make run 9 | 10 | build-css: 11 | @echo "Building CSS..." 12 | @npx tailwindcss build -i input.css -o public/css/main.css --minify 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ava Bot 2 | 3 | A conversational bot to assist you with your daily needs. 4 | -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [default.extend-words] 2 | 3 | [files] 4 | extend-exclude = ["CHANGELOG.md", "notebooks/*"] 5 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | [changelog] 2 | # changelog header 3 | header = """ 4 | # Changelog\n 5 | All notable changes to this project will be documented in this file.\n 6 | """ 7 | # template for the changelog body 8 | # https://tera.netlify.app/docs/#introduction 9 | body = """ 10 | {% if version %}\ 11 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 12 | {% else %}\ 13 | ## [unreleased] 14 | {% endif %}\ 15 | {% if previous %}\ 16 | {% if previous.commit_id %} 17 | [{{ previous.commit_id | truncate(length=7, end="") }}]({{ previous.commit_id }})...\ 18 | [{{ commit_id | truncate(length=7, end="") }}]({{ commit_id }}) 19 | {% endif %}\ 20 | {% endif %}\ 21 | {% for group, commits in commits | group_by(attribute="group") %} 22 | ### {{ group | upper_first }} 23 | {% for commit in commits %} 24 | - {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }}) - {{ commit.author.timestamp | date }} by {{ commit.author.name }})\ 25 | {% for footer in commit.footers -%} 26 | , {{ footer.token }}{{ footer.separator }}{{ footer.value }}\ 27 | {% endfor %}\ 28 | {% endfor %} 29 | {% endfor %}\n 30 | """ 31 | # remove the leading and trailing whitespace from the template 32 | trim = true 33 | # changelog footer 34 | footer = """ 35 | 36 | """ 37 | 38 | [git] 39 | # parse the commits based on https://www.conventionalcommits.org 40 | conventional_commits = true 41 | # filter out the commits that are not conventional 42 | filter_unconventional = true 43 | # process each line of a commit as an individual commit 44 | split_commits = false 45 | # regex for parsing and grouping commits 46 | commit_parsers = [ 47 | { message = "^feat", group = "Features"}, 48 | { message = "^fix", group = "Bug Fixes"}, 49 | { message = "^doc", group = "Documentation"}, 50 | { message = "^perf", group = "Performance"}, 51 | { message = "^refactor", group = "Refactor"}, 52 | { message = "^style", group = "Styling"}, 53 | { message = "^test", group = "Testing"}, 54 | { message = "^chore\\(release\\): prepare for", skip = true}, 55 | { message = "^chore", group = "Miscellaneous Tasks"}, 56 | { body = ".*security", group = "Security"}, 57 | ] 58 | # protect breaking changes from being skipped due to matching a skipping commit_parser 59 | protect_breaking_commits = false 60 | # filter out the commits that are not matched by commit parsers 61 | filter_commits = false 62 | # glob pattern for matching git tags 63 | tag_pattern = "v[0-9]*" 64 | # regex for skipping tags 65 | skip_tags = "v0.1.0-beta.1" 66 | # regex for ignoring tags 67 | ignore_tags = "" 68 | # sort the tags chronologically 69 | date_order = false 70 | # sort the commits inside sections by oldest/newest order 71 | sort_commits = "oldest" 72 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # This template contains all of the possible sections and their default values 2 | 3 | # Note that all fields that take a lint level have these possible values: 4 | # * deny - An error will be produced and the check will fail 5 | # * warn - A warning will be produced, but the check will not fail 6 | # * allow - No warning or error will be produced, though in some cases a note 7 | # will be 8 | 9 | # The values provided in this template are the default values that will be used 10 | # when any section or field is not specified in your own configuration 11 | 12 | # If 1 or more target triples (and optionally, target_features) are specified, 13 | # only the specified targets will be checked when running `cargo deny check`. 14 | # This means, if a particular package is only ever used as a target specific 15 | # dependency, such as, for example, the `nix` crate only being used via the 16 | # `target_family = "unix"` configuration, that only having windows targets in 17 | # this list would mean the nix crate, as well as any of its exclusive 18 | # dependencies not shared by any other crates, would be ignored, as the target 19 | # list here is effectively saying which targets you are building for. 20 | targets = [ 21 | # The triple can be any string, but only the target triples built in to 22 | # rustc (as of 1.40) can be checked against actual config expressions 23 | #{ triple = "x86_64-unknown-linux-musl" }, 24 | # You can also specify which target_features you promise are enabled for a 25 | # particular target. target_features are currently not validated against 26 | # the actual valid features supported by the target architecture. 27 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, 28 | ] 29 | 30 | # This section is considered when running `cargo deny check advisories` 31 | # More documentation for the advisories section can be found here: 32 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 33 | [advisories] 34 | # The path where the advisory database is cloned/fetched into 35 | db-path = "~/.cargo/advisory-db" 36 | # The url(s) of the advisory databases to use 37 | db-urls = ["https://github.com/rustsec/advisory-db"] 38 | # The lint level for security vulnerabilities 39 | vulnerability = "deny" 40 | # The lint level for unmaintained crates 41 | unmaintained = "warn" 42 | # The lint level for crates that have been yanked from their source registry 43 | yanked = "warn" 44 | # The lint level for crates with security notices. Note that as of 45 | # 2019-12-17 there are no security notice advisories in 46 | # https://github.com/rustsec/advisory-db 47 | notice = "warn" 48 | # A list of advisory IDs to ignore. Note that ignored advisories will still 49 | # output a note when they are encountered. 50 | ignore = [ 51 | #"RUSTSEC-0000-0000", 52 | ] 53 | # Threshold for security vulnerabilities, any vulnerability with a CVSS score 54 | # lower than the range specified will be ignored. Note that ignored advisories 55 | # will still output a note when they are encountered. 56 | # * None - CVSS Score 0.0 57 | # * Low - CVSS Score 0.1 - 3.9 58 | # * Medium - CVSS Score 4.0 - 6.9 59 | # * High - CVSS Score 7.0 - 8.9 60 | # * Critical - CVSS Score 9.0 - 10.0 61 | #severity-threshold = 62 | 63 | # This section is considered when running `cargo deny check licenses` 64 | # More documentation for the licenses section can be found here: 65 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 66 | [licenses] 67 | # The lint level for crates which do not have a detectable license 68 | unlicensed = "allow" 69 | # List of explicitly allowed licenses 70 | # See https://spdx.org/licenses/ for list of possible licenses 71 | # [possible values: any SPDX 3.7 short identifier (+ optional exception)]. 72 | allow = [ 73 | "MIT", 74 | "Apache-2.0", 75 | "Unicode-DFS-2016", 76 | "MPL-2.0", 77 | "BSD-2-Clause", 78 | "BSD-3-Clause", 79 | "ISC", 80 | ] 81 | # List of explicitly disallowed licenses 82 | # See https://spdx.org/licenses/ for list of possible licenses 83 | # [possible values: any SPDX 3.7 short identifier (+ optional exception)]. 84 | deny = [ 85 | #"Nokia", 86 | ] 87 | # Lint level for licenses considered copyleft 88 | copyleft = "warn" 89 | # Blanket approval or denial for OSI-approved or FSF Free/Libre licenses 90 | # * both - The license will be approved if it is both OSI-approved *AND* FSF 91 | # * either - The license will be approved if it is either OSI-approved *OR* FSF 92 | # * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF 93 | # * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved 94 | # * neither - This predicate is ignored and the default lint level is used 95 | allow-osi-fsf-free = "neither" 96 | # Lint level used when no other predicates are matched 97 | # 1. License isn't in the allow or deny lists 98 | # 2. License isn't copyleft 99 | # 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" 100 | default = "deny" 101 | # The confidence threshold for detecting a license from license text. 102 | # The higher the value, the more closely the license text must be to the 103 | # canonical license text of a valid SPDX license file. 104 | # [possible values: any between 0.0 and 1.0]. 105 | confidence-threshold = 0.8 106 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 107 | # aren't accepted for every possible crate as with the normal allow list 108 | exceptions = [ 109 | # Each entry is the crate and version constraint, and its specific allow 110 | # list 111 | #{ allow = ["Zlib"], name = "adler32", version = "*" }, 112 | ] 113 | 114 | # Some crates don't have (easily) machine readable licensing information, 115 | # adding a clarification entry for it allows you to manually specify the 116 | # licensing information 117 | #[[licenses.clarify]] 118 | # The name of the crate the clarification applies to 119 | #name = "ring" 120 | # The optional version constraint for the crate 121 | #version = "*" 122 | # The SPDX expression for the license requirements of the crate 123 | #expression = "MIT AND ISC AND OpenSSL" 124 | # One or more files in the crate's source used as the "source of truth" for 125 | # the license expression. If the contents match, the clarification will be used 126 | # when running the license check, otherwise the clarification will be ignored 127 | # and the crate will be checked normally, which may produce warnings or errors 128 | # depending on the rest of your configuration 129 | #license-files = [ 130 | # Each entry is a crate relative path, and the (opaque) hash of its contents 131 | #{ path = "LICENSE", hash = 0xbd0eed23 } 132 | #] 133 | 134 | [licenses.private] 135 | # If true, ignores workspace crates that aren't published, or are only 136 | # published to private registries 137 | ignore = false 138 | # One or more private registries that you might publish crates to, if a crate 139 | # is only published to private registries, and ignore is true, the crate will 140 | # not have its license(s) checked 141 | registries = [ 142 | #"https://sekretz.com/registry 143 | ] 144 | 145 | # This section is considered when running `cargo deny check bans`. 146 | # More documentation about the 'bans' section can be found here: 147 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 148 | [bans] 149 | # Lint level for when multiple versions of the same crate are detected 150 | multiple-versions = "warn" 151 | # Lint level for when a crate version requirement is `*` 152 | wildcards = "allow" 153 | # The graph highlighting used when creating dotgraphs for crates 154 | # with multiple versions 155 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 156 | # * simplest-path - The path to the version with the fewest edges is highlighted 157 | # * all - Both lowest-version and simplest-path are used 158 | highlight = "all" 159 | # List of crates that are allowed. Use with care! 160 | allow = [ 161 | #{ name = "ansi_term", version = "=0.11.0" }, 162 | ] 163 | # List of crates to deny 164 | deny = [ 165 | # Each entry the name of a crate and a version range. If version is 166 | # not specified, all versions will be matched. 167 | #{ name = "ansi_term", version = "=0.11.0" }, 168 | # 169 | # Wrapper crates can optionally be specified to allow the crate when it 170 | # is a direct dependency of the otherwise banned crate 171 | #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, 172 | ] 173 | # Certain crates/versions that will be skipped when doing duplicate detection. 174 | skip = [ 175 | #{ name = "ansi_term", version = "=0.11.0" }, 176 | ] 177 | # Similarly to `skip` allows you to skip certain crates during duplicate 178 | # detection. Unlike skip, it also includes the entire tree of transitive 179 | # dependencies starting at the specified crate, up to a certain depth, which is 180 | # by default infinite 181 | skip-tree = [ 182 | #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, 183 | ] 184 | 185 | # This section is considered when running `cargo deny check sources`. 186 | # More documentation about the 'sources' section can be found here: 187 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 188 | [sources] 189 | # Lint level for what to happen when a crate from a crate registry that is not 190 | # in the allow list is encountered 191 | unknown-registry = "warn" 192 | # Lint level for what to happen when a crate from a git repository that is not 193 | # in the allow list is encountered 194 | unknown-git = "warn" 195 | # List of URLs for allowed crate registries. Defaults to the crates.io index 196 | # if not specified. If it is specified but empty, no registries are allowed. 197 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 198 | # List of URLs for allowed Git repositories 199 | allow-git = [] 200 | 201 | [sources.allow-org] 202 | # 1 or more github.com organizations to allow git sources for 203 | github = [] 204 | # 1 or more gitlab.com organizations to allow git sources for 205 | gitlab = [] 206 | # 1 or more bitbucket.org organizations to allow git sources for 207 | bitbucket = [] 208 | -------------------------------------------------------------------------------- /input.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base'; 2 | @import 'tailwindcss/components'; 3 | @import 'tailwindcss/utilities'; 4 | 5 | @layer components { 6 | pre { 7 | @apply border rounded-sm; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ava-bot", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "ava-bot", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "tailwindcss-animated": "^1.0.1" 13 | }, 14 | "devDependencies": { 15 | "@tailwindcss/forms": "^0.5.7", 16 | "@tailwindcss/typography": "^0.5.10", 17 | "tailwindcss": "^3.3.5" 18 | } 19 | }, 20 | "node_modules/@alloc/quick-lru": { 21 | "version": "5.2.0", 22 | "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", 23 | "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", 24 | "engines": { 25 | "node": ">=10" 26 | }, 27 | "funding": { 28 | "url": "https://github.com/sponsors/sindresorhus" 29 | } 30 | }, 31 | "node_modules/@jridgewell/gen-mapping": { 32 | "version": "0.3.3", 33 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", 34 | "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", 35 | "dependencies": { 36 | "@jridgewell/set-array": "^1.0.1", 37 | "@jridgewell/sourcemap-codec": "^1.4.10", 38 | "@jridgewell/trace-mapping": "^0.3.9" 39 | }, 40 | "engines": { 41 | "node": ">=6.0.0" 42 | } 43 | }, 44 | "node_modules/@jridgewell/resolve-uri": { 45 | "version": "3.1.1", 46 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", 47 | "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", 48 | "engines": { 49 | "node": ">=6.0.0" 50 | } 51 | }, 52 | "node_modules/@jridgewell/set-array": { 53 | "version": "1.1.2", 54 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", 55 | "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", 56 | "engines": { 57 | "node": ">=6.0.0" 58 | } 59 | }, 60 | "node_modules/@jridgewell/sourcemap-codec": { 61 | "version": "1.4.15", 62 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", 63 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" 64 | }, 65 | "node_modules/@jridgewell/trace-mapping": { 66 | "version": "0.3.20", 67 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", 68 | "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", 69 | "dependencies": { 70 | "@jridgewell/resolve-uri": "^3.1.0", 71 | "@jridgewell/sourcemap-codec": "^1.4.14" 72 | } 73 | }, 74 | "node_modules/@nodelib/fs.scandir": { 75 | "version": "2.1.5", 76 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 77 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 78 | "dependencies": { 79 | "@nodelib/fs.stat": "2.0.5", 80 | "run-parallel": "^1.1.9" 81 | }, 82 | "engines": { 83 | "node": ">= 8" 84 | } 85 | }, 86 | "node_modules/@nodelib/fs.stat": { 87 | "version": "2.0.5", 88 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 89 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 90 | "engines": { 91 | "node": ">= 8" 92 | } 93 | }, 94 | "node_modules/@nodelib/fs.walk": { 95 | "version": "1.2.8", 96 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 97 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 98 | "dependencies": { 99 | "@nodelib/fs.scandir": "2.1.5", 100 | "fastq": "^1.6.0" 101 | }, 102 | "engines": { 103 | "node": ">= 8" 104 | } 105 | }, 106 | "node_modules/@tailwindcss/forms": { 107 | "version": "0.5.7", 108 | "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz", 109 | "integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==", 110 | "dev": true, 111 | "dependencies": { 112 | "mini-svg-data-uri": "^1.2.3" 113 | }, 114 | "peerDependencies": { 115 | "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" 116 | } 117 | }, 118 | "node_modules/@tailwindcss/typography": { 119 | "version": "0.5.10", 120 | "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz", 121 | "integrity": "sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==", 122 | "dev": true, 123 | "dependencies": { 124 | "lodash.castarray": "^4.4.0", 125 | "lodash.isplainobject": "^4.0.6", 126 | "lodash.merge": "^4.6.2", 127 | "postcss-selector-parser": "6.0.10" 128 | }, 129 | "peerDependencies": { 130 | "tailwindcss": ">=3.0.0 || insiders" 131 | } 132 | }, 133 | "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { 134 | "version": "6.0.10", 135 | "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", 136 | "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", 137 | "dev": true, 138 | "dependencies": { 139 | "cssesc": "^3.0.0", 140 | "util-deprecate": "^1.0.2" 141 | }, 142 | "engines": { 143 | "node": ">=4" 144 | } 145 | }, 146 | "node_modules/any-promise": { 147 | "version": "1.3.0", 148 | "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", 149 | "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" 150 | }, 151 | "node_modules/anymatch": { 152 | "version": "3.1.3", 153 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 154 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 155 | "dependencies": { 156 | "normalize-path": "^3.0.0", 157 | "picomatch": "^2.0.4" 158 | }, 159 | "engines": { 160 | "node": ">= 8" 161 | } 162 | }, 163 | "node_modules/arg": { 164 | "version": "5.0.2", 165 | "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", 166 | "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" 167 | }, 168 | "node_modules/balanced-match": { 169 | "version": "1.0.2", 170 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 171 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 172 | }, 173 | "node_modules/binary-extensions": { 174 | "version": "2.2.0", 175 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 176 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 177 | "engines": { 178 | "node": ">=8" 179 | } 180 | }, 181 | "node_modules/brace-expansion": { 182 | "version": "1.1.11", 183 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 184 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 185 | "dependencies": { 186 | "balanced-match": "^1.0.0", 187 | "concat-map": "0.0.1" 188 | } 189 | }, 190 | "node_modules/braces": { 191 | "version": "3.0.2", 192 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 193 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 194 | "dependencies": { 195 | "fill-range": "^7.0.1" 196 | }, 197 | "engines": { 198 | "node": ">=8" 199 | } 200 | }, 201 | "node_modules/camelcase-css": { 202 | "version": "2.0.1", 203 | "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", 204 | "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", 205 | "engines": { 206 | "node": ">= 6" 207 | } 208 | }, 209 | "node_modules/chokidar": { 210 | "version": "3.5.3", 211 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 212 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 213 | "funding": [ 214 | { 215 | "type": "individual", 216 | "url": "https://paulmillr.com/funding/" 217 | } 218 | ], 219 | "dependencies": { 220 | "anymatch": "~3.1.2", 221 | "braces": "~3.0.2", 222 | "glob-parent": "~5.1.2", 223 | "is-binary-path": "~2.1.0", 224 | "is-glob": "~4.0.1", 225 | "normalize-path": "~3.0.0", 226 | "readdirp": "~3.6.0" 227 | }, 228 | "engines": { 229 | "node": ">= 8.10.0" 230 | }, 231 | "optionalDependencies": { 232 | "fsevents": "~2.3.2" 233 | } 234 | }, 235 | "node_modules/chokidar/node_modules/glob-parent": { 236 | "version": "5.1.2", 237 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 238 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 239 | "dependencies": { 240 | "is-glob": "^4.0.1" 241 | }, 242 | "engines": { 243 | "node": ">= 6" 244 | } 245 | }, 246 | "node_modules/commander": { 247 | "version": "4.1.1", 248 | "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", 249 | "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", 250 | "engines": { 251 | "node": ">= 6" 252 | } 253 | }, 254 | "node_modules/concat-map": { 255 | "version": "0.0.1", 256 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 257 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 258 | }, 259 | "node_modules/cssesc": { 260 | "version": "3.0.0", 261 | "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", 262 | "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", 263 | "bin": { 264 | "cssesc": "bin/cssesc" 265 | }, 266 | "engines": { 267 | "node": ">=4" 268 | } 269 | }, 270 | "node_modules/didyoumean": { 271 | "version": "1.2.2", 272 | "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", 273 | "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" 274 | }, 275 | "node_modules/dlv": { 276 | "version": "1.1.3", 277 | "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", 278 | "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" 279 | }, 280 | "node_modules/fast-glob": { 281 | "version": "3.3.2", 282 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", 283 | "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", 284 | "dependencies": { 285 | "@nodelib/fs.stat": "^2.0.2", 286 | "@nodelib/fs.walk": "^1.2.3", 287 | "glob-parent": "^5.1.2", 288 | "merge2": "^1.3.0", 289 | "micromatch": "^4.0.4" 290 | }, 291 | "engines": { 292 | "node": ">=8.6.0" 293 | } 294 | }, 295 | "node_modules/fast-glob/node_modules/glob-parent": { 296 | "version": "5.1.2", 297 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 298 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 299 | "dependencies": { 300 | "is-glob": "^4.0.1" 301 | }, 302 | "engines": { 303 | "node": ">= 6" 304 | } 305 | }, 306 | "node_modules/fastq": { 307 | "version": "1.15.0", 308 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", 309 | "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", 310 | "dependencies": { 311 | "reusify": "^1.0.4" 312 | } 313 | }, 314 | "node_modules/fill-range": { 315 | "version": "7.0.1", 316 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 317 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 318 | "dependencies": { 319 | "to-regex-range": "^5.0.1" 320 | }, 321 | "engines": { 322 | "node": ">=8" 323 | } 324 | }, 325 | "node_modules/fs.realpath": { 326 | "version": "1.0.0", 327 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 328 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" 329 | }, 330 | "node_modules/fsevents": { 331 | "version": "2.3.3", 332 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 333 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 334 | "hasInstallScript": true, 335 | "optional": true, 336 | "os": [ 337 | "darwin" 338 | ], 339 | "engines": { 340 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 341 | } 342 | }, 343 | "node_modules/function-bind": { 344 | "version": "1.1.2", 345 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 346 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 347 | "funding": { 348 | "url": "https://github.com/sponsors/ljharb" 349 | } 350 | }, 351 | "node_modules/glob": { 352 | "version": "7.1.6", 353 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 354 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 355 | "dependencies": { 356 | "fs.realpath": "^1.0.0", 357 | "inflight": "^1.0.4", 358 | "inherits": "2", 359 | "minimatch": "^3.0.4", 360 | "once": "^1.3.0", 361 | "path-is-absolute": "^1.0.0" 362 | }, 363 | "engines": { 364 | "node": "*" 365 | }, 366 | "funding": { 367 | "url": "https://github.com/sponsors/isaacs" 368 | } 369 | }, 370 | "node_modules/glob-parent": { 371 | "version": "6.0.2", 372 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 373 | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 374 | "dependencies": { 375 | "is-glob": "^4.0.3" 376 | }, 377 | "engines": { 378 | "node": ">=10.13.0" 379 | } 380 | }, 381 | "node_modules/hasown": { 382 | "version": "2.0.0", 383 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", 384 | "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", 385 | "dependencies": { 386 | "function-bind": "^1.1.2" 387 | }, 388 | "engines": { 389 | "node": ">= 0.4" 390 | } 391 | }, 392 | "node_modules/inflight": { 393 | "version": "1.0.6", 394 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 395 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 396 | "dependencies": { 397 | "once": "^1.3.0", 398 | "wrappy": "1" 399 | } 400 | }, 401 | "node_modules/inherits": { 402 | "version": "2.0.4", 403 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 404 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 405 | }, 406 | "node_modules/is-binary-path": { 407 | "version": "2.1.0", 408 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 409 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 410 | "dependencies": { 411 | "binary-extensions": "^2.0.0" 412 | }, 413 | "engines": { 414 | "node": ">=8" 415 | } 416 | }, 417 | "node_modules/is-core-module": { 418 | "version": "2.13.1", 419 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", 420 | "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", 421 | "dependencies": { 422 | "hasown": "^2.0.0" 423 | }, 424 | "funding": { 425 | "url": "https://github.com/sponsors/ljharb" 426 | } 427 | }, 428 | "node_modules/is-extglob": { 429 | "version": "2.1.1", 430 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 431 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 432 | "engines": { 433 | "node": ">=0.10.0" 434 | } 435 | }, 436 | "node_modules/is-glob": { 437 | "version": "4.0.3", 438 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 439 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 440 | "dependencies": { 441 | "is-extglob": "^2.1.1" 442 | }, 443 | "engines": { 444 | "node": ">=0.10.0" 445 | } 446 | }, 447 | "node_modules/is-number": { 448 | "version": "7.0.0", 449 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 450 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 451 | "engines": { 452 | "node": ">=0.12.0" 453 | } 454 | }, 455 | "node_modules/jiti": { 456 | "version": "1.21.0", 457 | "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", 458 | "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", 459 | "bin": { 460 | "jiti": "bin/jiti.js" 461 | } 462 | }, 463 | "node_modules/lilconfig": { 464 | "version": "2.1.0", 465 | "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", 466 | "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", 467 | "engines": { 468 | "node": ">=10" 469 | } 470 | }, 471 | "node_modules/lines-and-columns": { 472 | "version": "1.2.4", 473 | "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", 474 | "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" 475 | }, 476 | "node_modules/lodash.castarray": { 477 | "version": "4.4.0", 478 | "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", 479 | "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", 480 | "dev": true 481 | }, 482 | "node_modules/lodash.isplainobject": { 483 | "version": "4.0.6", 484 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 485 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", 486 | "dev": true 487 | }, 488 | "node_modules/lodash.merge": { 489 | "version": "4.6.2", 490 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 491 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 492 | "dev": true 493 | }, 494 | "node_modules/merge2": { 495 | "version": "1.4.1", 496 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 497 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 498 | "engines": { 499 | "node": ">= 8" 500 | } 501 | }, 502 | "node_modules/micromatch": { 503 | "version": "4.0.5", 504 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", 505 | "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", 506 | "dependencies": { 507 | "braces": "^3.0.2", 508 | "picomatch": "^2.3.1" 509 | }, 510 | "engines": { 511 | "node": ">=8.6" 512 | } 513 | }, 514 | "node_modules/mini-svg-data-uri": { 515 | "version": "1.4.4", 516 | "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", 517 | "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", 518 | "dev": true, 519 | "bin": { 520 | "mini-svg-data-uri": "cli.js" 521 | } 522 | }, 523 | "node_modules/minimatch": { 524 | "version": "3.1.2", 525 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 526 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 527 | "dependencies": { 528 | "brace-expansion": "^1.1.7" 529 | }, 530 | "engines": { 531 | "node": "*" 532 | } 533 | }, 534 | "node_modules/mz": { 535 | "version": "2.7.0", 536 | "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", 537 | "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", 538 | "dependencies": { 539 | "any-promise": "^1.0.0", 540 | "object-assign": "^4.0.1", 541 | "thenify-all": "^1.0.0" 542 | } 543 | }, 544 | "node_modules/nanoid": { 545 | "version": "3.3.7", 546 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", 547 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", 548 | "funding": [ 549 | { 550 | "type": "github", 551 | "url": "https://github.com/sponsors/ai" 552 | } 553 | ], 554 | "bin": { 555 | "nanoid": "bin/nanoid.cjs" 556 | }, 557 | "engines": { 558 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 559 | } 560 | }, 561 | "node_modules/normalize-path": { 562 | "version": "3.0.0", 563 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 564 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 565 | "engines": { 566 | "node": ">=0.10.0" 567 | } 568 | }, 569 | "node_modules/object-assign": { 570 | "version": "4.1.1", 571 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 572 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 573 | "engines": { 574 | "node": ">=0.10.0" 575 | } 576 | }, 577 | "node_modules/object-hash": { 578 | "version": "3.0.0", 579 | "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", 580 | "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", 581 | "engines": { 582 | "node": ">= 6" 583 | } 584 | }, 585 | "node_modules/once": { 586 | "version": "1.4.0", 587 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 588 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 589 | "dependencies": { 590 | "wrappy": "1" 591 | } 592 | }, 593 | "node_modules/path-is-absolute": { 594 | "version": "1.0.1", 595 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 596 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 597 | "engines": { 598 | "node": ">=0.10.0" 599 | } 600 | }, 601 | "node_modules/path-parse": { 602 | "version": "1.0.7", 603 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 604 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" 605 | }, 606 | "node_modules/picocolors": { 607 | "version": "1.0.0", 608 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 609 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 610 | }, 611 | "node_modules/picomatch": { 612 | "version": "2.3.1", 613 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 614 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 615 | "engines": { 616 | "node": ">=8.6" 617 | }, 618 | "funding": { 619 | "url": "https://github.com/sponsors/jonschlinkert" 620 | } 621 | }, 622 | "node_modules/pify": { 623 | "version": "2.3.0", 624 | "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 625 | "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", 626 | "engines": { 627 | "node": ">=0.10.0" 628 | } 629 | }, 630 | "node_modules/pirates": { 631 | "version": "4.0.6", 632 | "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", 633 | "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", 634 | "engines": { 635 | "node": ">= 6" 636 | } 637 | }, 638 | "node_modules/postcss": { 639 | "version": "8.4.31", 640 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", 641 | "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", 642 | "funding": [ 643 | { 644 | "type": "opencollective", 645 | "url": "https://opencollective.com/postcss/" 646 | }, 647 | { 648 | "type": "tidelift", 649 | "url": "https://tidelift.com/funding/github/npm/postcss" 650 | }, 651 | { 652 | "type": "github", 653 | "url": "https://github.com/sponsors/ai" 654 | } 655 | ], 656 | "dependencies": { 657 | "nanoid": "^3.3.6", 658 | "picocolors": "^1.0.0", 659 | "source-map-js": "^1.0.2" 660 | }, 661 | "engines": { 662 | "node": "^10 || ^12 || >=14" 663 | } 664 | }, 665 | "node_modules/postcss-import": { 666 | "version": "15.1.0", 667 | "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", 668 | "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", 669 | "dependencies": { 670 | "postcss-value-parser": "^4.0.0", 671 | "read-cache": "^1.0.0", 672 | "resolve": "^1.1.7" 673 | }, 674 | "engines": { 675 | "node": ">=14.0.0" 676 | }, 677 | "peerDependencies": { 678 | "postcss": "^8.0.0" 679 | } 680 | }, 681 | "node_modules/postcss-js": { 682 | "version": "4.0.1", 683 | "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", 684 | "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", 685 | "dependencies": { 686 | "camelcase-css": "^2.0.1" 687 | }, 688 | "engines": { 689 | "node": "^12 || ^14 || >= 16" 690 | }, 691 | "funding": { 692 | "type": "opencollective", 693 | "url": "https://opencollective.com/postcss/" 694 | }, 695 | "peerDependencies": { 696 | "postcss": "^8.4.21" 697 | } 698 | }, 699 | "node_modules/postcss-load-config": { 700 | "version": "4.0.1", 701 | "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", 702 | "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", 703 | "dependencies": { 704 | "lilconfig": "^2.0.5", 705 | "yaml": "^2.1.1" 706 | }, 707 | "engines": { 708 | "node": ">= 14" 709 | }, 710 | "funding": { 711 | "type": "opencollective", 712 | "url": "https://opencollective.com/postcss/" 713 | }, 714 | "peerDependencies": { 715 | "postcss": ">=8.0.9", 716 | "ts-node": ">=9.0.0" 717 | }, 718 | "peerDependenciesMeta": { 719 | "postcss": { 720 | "optional": true 721 | }, 722 | "ts-node": { 723 | "optional": true 724 | } 725 | } 726 | }, 727 | "node_modules/postcss-nested": { 728 | "version": "6.0.1", 729 | "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", 730 | "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", 731 | "dependencies": { 732 | "postcss-selector-parser": "^6.0.11" 733 | }, 734 | "engines": { 735 | "node": ">=12.0" 736 | }, 737 | "funding": { 738 | "type": "opencollective", 739 | "url": "https://opencollective.com/postcss/" 740 | }, 741 | "peerDependencies": { 742 | "postcss": "^8.2.14" 743 | } 744 | }, 745 | "node_modules/postcss-selector-parser": { 746 | "version": "6.0.13", 747 | "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", 748 | "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", 749 | "dependencies": { 750 | "cssesc": "^3.0.0", 751 | "util-deprecate": "^1.0.2" 752 | }, 753 | "engines": { 754 | "node": ">=4" 755 | } 756 | }, 757 | "node_modules/postcss-value-parser": { 758 | "version": "4.2.0", 759 | "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", 760 | "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" 761 | }, 762 | "node_modules/queue-microtask": { 763 | "version": "1.2.3", 764 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 765 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 766 | "funding": [ 767 | { 768 | "type": "github", 769 | "url": "https://github.com/sponsors/feross" 770 | }, 771 | { 772 | "type": "patreon", 773 | "url": "https://www.patreon.com/feross" 774 | }, 775 | { 776 | "type": "consulting", 777 | "url": "https://feross.org/support" 778 | } 779 | ] 780 | }, 781 | "node_modules/read-cache": { 782 | "version": "1.0.0", 783 | "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", 784 | "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", 785 | "dependencies": { 786 | "pify": "^2.3.0" 787 | } 788 | }, 789 | "node_modules/readdirp": { 790 | "version": "3.6.0", 791 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 792 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 793 | "dependencies": { 794 | "picomatch": "^2.2.1" 795 | }, 796 | "engines": { 797 | "node": ">=8.10.0" 798 | } 799 | }, 800 | "node_modules/resolve": { 801 | "version": "1.22.8", 802 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", 803 | "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", 804 | "dependencies": { 805 | "is-core-module": "^2.13.0", 806 | "path-parse": "^1.0.7", 807 | "supports-preserve-symlinks-flag": "^1.0.0" 808 | }, 809 | "bin": { 810 | "resolve": "bin/resolve" 811 | }, 812 | "funding": { 813 | "url": "https://github.com/sponsors/ljharb" 814 | } 815 | }, 816 | "node_modules/reusify": { 817 | "version": "1.0.4", 818 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 819 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 820 | "engines": { 821 | "iojs": ">=1.0.0", 822 | "node": ">=0.10.0" 823 | } 824 | }, 825 | "node_modules/run-parallel": { 826 | "version": "1.2.0", 827 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 828 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 829 | "funding": [ 830 | { 831 | "type": "github", 832 | "url": "https://github.com/sponsors/feross" 833 | }, 834 | { 835 | "type": "patreon", 836 | "url": "https://www.patreon.com/feross" 837 | }, 838 | { 839 | "type": "consulting", 840 | "url": "https://feross.org/support" 841 | } 842 | ], 843 | "dependencies": { 844 | "queue-microtask": "^1.2.2" 845 | } 846 | }, 847 | "node_modules/source-map-js": { 848 | "version": "1.0.2", 849 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 850 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", 851 | "engines": { 852 | "node": ">=0.10.0" 853 | } 854 | }, 855 | "node_modules/sucrase": { 856 | "version": "3.34.0", 857 | "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", 858 | "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", 859 | "dependencies": { 860 | "@jridgewell/gen-mapping": "^0.3.2", 861 | "commander": "^4.0.0", 862 | "glob": "7.1.6", 863 | "lines-and-columns": "^1.1.6", 864 | "mz": "^2.7.0", 865 | "pirates": "^4.0.1", 866 | "ts-interface-checker": "^0.1.9" 867 | }, 868 | "bin": { 869 | "sucrase": "bin/sucrase", 870 | "sucrase-node": "bin/sucrase-node" 871 | }, 872 | "engines": { 873 | "node": ">=8" 874 | } 875 | }, 876 | "node_modules/supports-preserve-symlinks-flag": { 877 | "version": "1.0.0", 878 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 879 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 880 | "engines": { 881 | "node": ">= 0.4" 882 | }, 883 | "funding": { 884 | "url": "https://github.com/sponsors/ljharb" 885 | } 886 | }, 887 | "node_modules/tailwindcss": { 888 | "version": "3.3.5", 889 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", 890 | "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==", 891 | "dependencies": { 892 | "@alloc/quick-lru": "^5.2.0", 893 | "arg": "^5.0.2", 894 | "chokidar": "^3.5.3", 895 | "didyoumean": "^1.2.2", 896 | "dlv": "^1.1.3", 897 | "fast-glob": "^3.3.0", 898 | "glob-parent": "^6.0.2", 899 | "is-glob": "^4.0.3", 900 | "jiti": "^1.19.1", 901 | "lilconfig": "^2.1.0", 902 | "micromatch": "^4.0.5", 903 | "normalize-path": "^3.0.0", 904 | "object-hash": "^3.0.0", 905 | "picocolors": "^1.0.0", 906 | "postcss": "^8.4.23", 907 | "postcss-import": "^15.1.0", 908 | "postcss-js": "^4.0.1", 909 | "postcss-load-config": "^4.0.1", 910 | "postcss-nested": "^6.0.1", 911 | "postcss-selector-parser": "^6.0.11", 912 | "resolve": "^1.22.2", 913 | "sucrase": "^3.32.0" 914 | }, 915 | "bin": { 916 | "tailwind": "lib/cli.js", 917 | "tailwindcss": "lib/cli.js" 918 | }, 919 | "engines": { 920 | "node": ">=14.0.0" 921 | } 922 | }, 923 | "node_modules/tailwindcss-animated": { 924 | "version": "1.0.1", 925 | "resolved": "https://registry.npmjs.org/tailwindcss-animated/-/tailwindcss-animated-1.0.1.tgz", 926 | "integrity": "sha512-u5wusj89ZwP8I+s8WZlaAd7aZTWBN/XEG6QgMKpkIKmAf3xP1A6WYf7oYIKmGaB10UAQaSqWopi/i1ozzZEs8Q==", 927 | "peerDependencies": { 928 | "tailwindcss": ">=3.1.0" 929 | } 930 | }, 931 | "node_modules/thenify": { 932 | "version": "3.3.1", 933 | "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", 934 | "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", 935 | "dependencies": { 936 | "any-promise": "^1.0.0" 937 | } 938 | }, 939 | "node_modules/thenify-all": { 940 | "version": "1.6.0", 941 | "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", 942 | "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", 943 | "dependencies": { 944 | "thenify": ">= 3.1.0 < 4" 945 | }, 946 | "engines": { 947 | "node": ">=0.8" 948 | } 949 | }, 950 | "node_modules/to-regex-range": { 951 | "version": "5.0.1", 952 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 953 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 954 | "dependencies": { 955 | "is-number": "^7.0.0" 956 | }, 957 | "engines": { 958 | "node": ">=8.0" 959 | } 960 | }, 961 | "node_modules/ts-interface-checker": { 962 | "version": "0.1.13", 963 | "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", 964 | "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" 965 | }, 966 | "node_modules/util-deprecate": { 967 | "version": "1.0.2", 968 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 969 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 970 | }, 971 | "node_modules/wrappy": { 972 | "version": "1.0.2", 973 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 974 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 975 | }, 976 | "node_modules/yaml": { 977 | "version": "2.3.4", 978 | "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", 979 | "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", 980 | "engines": { 981 | "node": ">= 14" 982 | } 983 | } 984 | } 985 | } 986 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ava-bot", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@tailwindcss/forms": "^0.5.7", 14 | "@tailwindcss/typography": "^0.5.10", 15 | "tailwindcss": "^3.3.5" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /public/css/.gitignore: -------------------------------------------------------------------------------- 1 | main.css 2 | -------------------------------------------------------------------------------- /public/images/ava-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tyrchen/ava-bot/1239dd04a5594dce6df2522d4c3a623eb0ae1732/public/images/ava-small.png -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | http::StatusCode, 3 | response::{IntoResponse, Response}, 4 | }; 5 | 6 | // Make our own error that wraps `anyhow::Error`. 7 | pub struct AppError(anyhow::Error); 8 | 9 | // Tell axum how to convert `AppError` into a response. 10 | impl IntoResponse for AppError { 11 | fn into_response(self) -> Response { 12 | ( 13 | StatusCode::INTERNAL_SERVER_ERROR, 14 | format!("Something went wrong: {}", self.0), 15 | ) 16 | .into_response() 17 | } 18 | } 19 | 20 | // This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into 21 | // `Result<_, AppError>`. That way you don't need to do that manually. 22 | impl From for AppError 23 | where 24 | E: Into, 25 | { 26 | fn from(err: E) -> Self { 27 | Self(err.into()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/extractors/mod.rs: -------------------------------------------------------------------------------- 1 | use axum::async_trait; 2 | use axum::extract::FromRequestParts; 3 | use axum::http::request::Parts; 4 | use axum::http::StatusCode; 5 | use axum_extra::extract::CookieJar; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct AppContext { 9 | pub device_id: String, 10 | } 11 | 12 | #[async_trait] 13 | impl FromRequestParts for AppContext 14 | where 15 | S: Send + Sync, 16 | { 17 | type Rejection = (StatusCode, &'static str); 18 | 19 | async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { 20 | let jar = CookieJar::from_request_parts(parts, state).await.unwrap(); 21 | if let Some(device_id) = jar.get("device_id") { 22 | Ok(AppContext { 23 | device_id: device_id.value().to_string(), 24 | }) 25 | } else { 26 | Err((StatusCode::BAD_REQUEST, "cookie `device_id` is missing")) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/handlers/assistant.rs: -------------------------------------------------------------------------------- 1 | use super::{AssistantEvent, AssistantStep, SignalEvent, SpeechResult}; 2 | use crate::{ 3 | audio_path, audio_url, 4 | error::AppError, 5 | extractors::AppContext, 6 | handlers::{ChatInputEvent, ChatInputSkeletonEvent, ChatReplyEvent, ChatReplySkeletonEvent}, 7 | image_path, image_url, 8 | tools::{ 9 | tool_completion_request, AnswerArgs, AssistantTool, DrawImageArgs, DrawImageResult, 10 | WriteCodeArgs, WriteCodeResult, 11 | }, 12 | AppState, 13 | }; 14 | use anyhow::{anyhow, bail}; 15 | use axum::{ 16 | extract::{Multipart, State}, 17 | response::IntoResponse, 18 | Json, 19 | }; 20 | use base64::{engine::general_purpose::STANDARD, Engine as _}; 21 | use comrak::{markdown_to_html_with_plugins, plugins::syntect::SyntectAdapter}; 22 | use llm_sdk::{ 23 | ChatCompletionChoice, ChatCompletionMessage, ChatCompletionRequest, CreateImageRequestBuilder, 24 | ImageResponseFormat, LlmSdk, SpeechRequest, WhisperRequestBuilder, WhisperRequestType, 25 | }; 26 | use serde_json::json; 27 | use std::{str::FromStr, sync::Arc}; 28 | use tokio::{fs, sync::broadcast}; 29 | use tracing::info; 30 | use uuid::Uuid; 31 | 32 | pub async fn assistant_handler( 33 | context: AppContext, 34 | State(state): State>, 35 | data: Multipart, 36 | ) -> Result { 37 | let device_id = &context.device_id; 38 | let event_sender = state 39 | .events 40 | .get(device_id) 41 | .ok_or_else(|| anyhow!("device_id not found for signal sender"))? 42 | .clone(); 43 | 44 | info!("start assist for {}", device_id); 45 | 46 | match process(&event_sender, &state.llm, device_id, data).await { 47 | Ok(_) => Ok(Json(json!({"status": "done"}))), 48 | Err(e) => { 49 | event_sender.send(error(e.to_string()))?; 50 | Ok(Json(json!({"status": "error"}))) 51 | } 52 | } 53 | } 54 | 55 | async fn process( 56 | event_sender: &broadcast::Sender, 57 | llm: &LlmSdk, 58 | device_id: &str, 59 | mut data: Multipart, 60 | ) -> anyhow::Result<()> { 61 | let id = Uuid::new_v4().to_string(); 62 | event_sender.send(in_audio_upload()).unwrap(); 63 | 64 | let Some(field) = data.next_field().await? else { 65 | return Err(anyhow!("expected an audio field"))?; 66 | }; 67 | 68 | let data = match field.name() { 69 | Some("audio") => field.bytes().await?, 70 | _ => return Err(anyhow!("expected an audio field"))?, 71 | }; 72 | 73 | info!("audio data size: {}", data.len()); 74 | 75 | event_sender.send(in_transcription())?; 76 | event_sender.send(ChatInputSkeletonEvent::new(&id).into())?; 77 | 78 | let input = transcript(llm, data.to_vec()).await?; 79 | 80 | event_sender.send(ChatInputEvent::new(&id, &input).into())?; 81 | 82 | event_sender.send(in_thinking())?; 83 | event_sender.send(ChatReplySkeletonEvent::new(&id).into())?; 84 | 85 | let choice = chat_completion_with_tools(llm, &input).await?; 86 | 87 | match choice.finish_reason { 88 | llm_sdk::FinishReason::Stop => { 89 | let output = choice 90 | .message 91 | .content 92 | .ok_or_else(|| anyhow!("expect content but no content available"))?; 93 | 94 | event_sender.send(in_speech())?; 95 | let ret = SpeechResult::new_text_only(&output); 96 | event_sender.send(ChatReplyEvent::new(&id, ret).into())?; 97 | 98 | let ret = speech(llm, device_id, &output).await?; 99 | event_sender.send(complete())?; 100 | event_sender.send(ChatReplyEvent::new(&id, ret).into())?; 101 | } 102 | llm_sdk::FinishReason::ToolCalls => { 103 | let tool_call = &choice.message.tool_calls[0].function; 104 | match AssistantTool::from_str(&tool_call.name) { 105 | Ok(AssistantTool::DrawImage) => { 106 | let args: DrawImageArgs = serde_json::from_str(&tool_call.arguments)?; 107 | 108 | event_sender.send(in_draw_image())?; 109 | let ret = DrawImageResult::new("", &args.prompt); 110 | event_sender.send(ChatReplyEvent::new(&id, ret).into())?; 111 | 112 | let ret = draw_image(llm, device_id, args).await?; 113 | event_sender.send(complete())?; 114 | event_sender.send(ChatReplyEvent::new(&id, ret).into())?; 115 | } 116 | Ok(AssistantTool::WriteCode) => { 117 | event_sender.send(in_write_code())?; 118 | let ret = write_code(llm, serde_json::from_str(&tool_call.arguments)?).await?; 119 | event_sender.send(complete())?; 120 | event_sender.send(ChatReplyEvent::new(&id, ret).into())?; 121 | } 122 | 123 | Ok(AssistantTool::Answer) => { 124 | event_sender.send(in_chat_completion())?; 125 | let output = answer(llm, serde_json::from_str(&tool_call.arguments)?).await?; 126 | 127 | event_sender.send(complete())?; 128 | let ret = SpeechResult::new_text_only(&output); 129 | event_sender.send(ChatReplyEvent::new(&id, ret).into())?; 130 | 131 | event_sender.send(in_speech())?; 132 | let ret = speech(llm, device_id, &output).await?; 133 | event_sender.send(complete())?; 134 | event_sender.send(ChatReplyEvent::new(&id, ret).into())?; 135 | } 136 | _ => { 137 | bail!("no proper tool found at the moment") 138 | } 139 | } 140 | } 141 | _ => { 142 | bail!("stop reason not supported") 143 | } 144 | } 145 | 146 | Ok(()) 147 | } 148 | 149 | async fn transcript(llm: &LlmSdk, data: Vec) -> anyhow::Result { 150 | let req = WhisperRequestBuilder::default() 151 | .file(data) 152 | .prompt("If audio language is Chinese, please use Simplified Chinese") 153 | .request_type(WhisperRequestType::Transcription) 154 | .build() 155 | .unwrap(); 156 | let res = llm.whisper(req).await?; 157 | Ok(res.text) 158 | } 159 | 160 | async fn chat_completion_with_tools( 161 | llm: &LlmSdk, 162 | prompt: &str, 163 | ) -> anyhow::Result { 164 | let req = tool_completion_request(prompt, ""); 165 | let mut res = llm.chat_completion(req).await?; 166 | let choice = res 167 | .choices 168 | .pop() 169 | .ok_or_else(|| anyhow!("expect at least one choice"))?; 170 | Ok(choice) 171 | } 172 | 173 | async fn chat_completion( 174 | llm: &LlmSdk, 175 | messages: Vec, 176 | ) -> anyhow::Result { 177 | let req = ChatCompletionRequest::new(messages); 178 | let mut res = llm.chat_completion(req).await?; 179 | let content = res 180 | .choices 181 | .pop() 182 | .ok_or_else(|| anyhow!("expect at least one choice"))? 183 | .message 184 | .content 185 | .ok_or_else(|| anyhow!("expect content but no content available"))?; 186 | Ok(content) 187 | } 188 | 189 | async fn speech(llm: &LlmSdk, device_id: &str, text: &str) -> anyhow::Result { 190 | let req = SpeechRequest::new(text); 191 | let data = llm.speech(req).await?; 192 | let uuid = Uuid::new_v4().to_string(); 193 | let path = audio_path(device_id, &uuid); 194 | if let Some(parent) = path.parent() { 195 | if !parent.exists() { 196 | fs::create_dir_all(parent).await?; 197 | } 198 | } 199 | fs::write(&path, data).await?; 200 | Ok(SpeechResult::new(text, audio_url(device_id, &uuid))) 201 | } 202 | 203 | async fn draw_image( 204 | llm: &LlmSdk, 205 | device_id: &str, 206 | args: DrawImageArgs, 207 | ) -> anyhow::Result { 208 | let req = CreateImageRequestBuilder::default() 209 | .prompt(args.prompt) 210 | .response_format(ImageResponseFormat::B64Json) 211 | .build() 212 | .unwrap(); 213 | let mut ret = llm.create_image(req).await?; 214 | let img = ret 215 | .data 216 | .pop() 217 | .ok_or_else(|| anyhow!("expect at least one data"))?; 218 | let data = STANDARD.decode(img.b64_json.unwrap())?; 219 | let uuid = Uuid::new_v4().to_string(); 220 | let path = image_path(device_id, &uuid); 221 | if let Some(parent) = path.parent() { 222 | if !parent.exists() { 223 | fs::create_dir_all(parent).await?; 224 | } 225 | } 226 | fs::write(&path, data).await?; 227 | Ok(DrawImageResult::new( 228 | image_url(device_id, &uuid), 229 | img.revised_prompt, 230 | )) 231 | } 232 | 233 | async fn write_code(llm: &LlmSdk, args: WriteCodeArgs) -> anyhow::Result { 234 | let messages = vec![ 235 | ChatCompletionMessage::new_system("I'm an expert on coding, I'll write code for you in markdown format based on your prompt", "Ava"), 236 | ChatCompletionMessage::new_user(args.prompt, ""), 237 | ]; 238 | let md = chat_completion(llm, messages).await?; 239 | 240 | Ok(WriteCodeResult::new(md2html(&md))) 241 | } 242 | 243 | async fn answer(llm: &LlmSdk, args: AnswerArgs) -> anyhow::Result { 244 | let messages = vec![ 245 | ChatCompletionMessage::new_system("I can help answer anything you'd like to chat", "Ava"), 246 | ChatCompletionMessage::new_user(args.prompt, ""), 247 | ]; 248 | chat_completion(llm, messages).await 249 | } 250 | 251 | fn md2html(md: &str) -> String { 252 | let adapter = SyntectAdapter::new("Solarized (dark)"); 253 | let options = comrak::Options::default(); 254 | let mut plugins = comrak::Plugins::default(); 255 | 256 | plugins.render.codefence_syntax_highlighter = Some(&adapter); 257 | markdown_to_html_with_plugins(md, &options, &plugins) 258 | } 259 | 260 | fn in_audio_upload() -> AssistantEvent { 261 | SignalEvent::Processing(AssistantStep::UploadAudio).into() 262 | } 263 | 264 | fn in_transcription() -> AssistantEvent { 265 | SignalEvent::Processing(AssistantStep::Transcription).into() 266 | } 267 | 268 | fn in_thinking() -> AssistantEvent { 269 | SignalEvent::Processing(AssistantStep::Thinking).into() 270 | } 271 | 272 | fn in_chat_completion() -> AssistantEvent { 273 | SignalEvent::Processing(AssistantStep::ChatCompletion).into() 274 | } 275 | 276 | fn in_speech() -> AssistantEvent { 277 | SignalEvent::Processing(AssistantStep::Speech).into() 278 | } 279 | 280 | fn in_draw_image() -> AssistantEvent { 281 | SignalEvent::Processing(AssistantStep::DrawImage).into() 282 | } 283 | 284 | fn in_write_code() -> AssistantEvent { 285 | SignalEvent::Processing(AssistantStep::WriteCode).into() 286 | } 287 | 288 | fn complete() -> AssistantEvent { 289 | SignalEvent::Complete.into() 290 | } 291 | 292 | fn error(msg: impl Into) -> AssistantEvent { 293 | SignalEvent::Error(msg.into()).into() 294 | } 295 | 296 | #[cfg(test)] 297 | mod tests { 298 | use super::*; 299 | 300 | #[test] 301 | fn test_error_render() { 302 | let event: String = error("error").into(); 303 | assert_eq!( 304 | event, 305 | r#" 306 |

Error: error

307 | "# 308 | ); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/handlers/chats.rs: -------------------------------------------------------------------------------- 1 | use crate::{extractors::AppContext, AppState}; 2 | use axum::{ 3 | extract::State, 4 | response::{sse::Event, IntoResponse, Sse}, 5 | }; 6 | use dashmap::DashMap; 7 | use std::{convert::Infallible, sync::Arc, time::Duration}; 8 | use tokio::sync::broadcast; 9 | use tokio_stream::{wrappers::BroadcastStream, StreamExt as _}; 10 | use tracing::info; 11 | 12 | use super::AssistantEvent; 13 | 14 | const MAX_EVENTS: usize = 128; 15 | 16 | pub async fn events_handler( 17 | context: AppContext, 18 | State(state): State>, 19 | ) -> impl IntoResponse { 20 | info!("user {} connected", context.device_id); 21 | sse_handler(context, &state.events).await 22 | } 23 | 24 | async fn sse_handler( 25 | context: AppContext, 26 | map: &DashMap>, 27 | ) -> impl IntoResponse { 28 | let device_id = &context.device_id; 29 | 30 | let rx = if let Some(tx) = map.get(device_id) { 31 | tx.subscribe() 32 | } else { 33 | let (tx, rx) = broadcast::channel(MAX_EVENTS); 34 | map.insert(device_id.to_string(), tx); 35 | rx 36 | }; 37 | 38 | // wrap receiver in a stream 39 | let stream = BroadcastStream::new(rx) 40 | .filter_map(|v| v.ok()) 41 | .map(|v| { 42 | let (event, id) = match &v { 43 | AssistantEvent::Signal(_) => ("signal", "".to_string()), 44 | AssistantEvent::InputSkeleton(_) => ("input_skeleton", "".to_string()), 45 | AssistantEvent::Input(v) => ("input", v.id.clone()), 46 | AssistantEvent::ReplySkeleton(_) => ("reply_skeleton", "".to_string()), 47 | AssistantEvent::Reply(v) => ("reply", v.id.clone()), 48 | }; 49 | let data: String = v.into(); 50 | Event::default().data(data).event(event).id(id) 51 | }) 52 | .map(Ok::<_, Infallible>); 53 | 54 | Sse::new(stream).keep_alive( 55 | axum::response::sse::KeepAlive::new() 56 | .interval(Duration::from_secs(1)) 57 | .text("keep-alive-text"), 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/handlers/common.rs: -------------------------------------------------------------------------------- 1 | use askama::Template; 2 | use axum::response::IntoResponse; 3 | use axum_extra::extract::{cookie::Cookie, CookieJar}; 4 | use uuid::Uuid; 5 | 6 | const COOKIE_NAME: &str = "device_id"; 7 | 8 | #[derive(Debug, Template)] 9 | #[template(path = "index.html.j2")] 10 | struct IndexTemplate {} 11 | 12 | pub async fn index_page(jar: CookieJar) -> impl IntoResponse { 13 | let jar = match jar.get(COOKIE_NAME) { 14 | Some(_) => jar, 15 | None => { 16 | let device_id = Uuid::new_v4().to_string(); 17 | let cookie = Cookie::build(COOKIE_NAME, device_id) 18 | .path("/") 19 | .secure(true) 20 | .permanent() 21 | .finish(); 22 | jar.add(cookie) 23 | } 24 | }; 25 | (jar, IndexTemplate {}) 26 | } 27 | -------------------------------------------------------------------------------- /src/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | mod assistant; 2 | mod chats; 3 | mod common; 4 | 5 | pub use assistant::*; 6 | pub use chats::*; 7 | pub use common::*; 8 | 9 | use crate::tools::{DrawImageResult, WriteCodeResult}; 10 | use askama::Template; 11 | use chrono::Local; 12 | use derive_more::From; 13 | use serde::{Deserialize, Serialize}; 14 | use strum::{Display, EnumString}; 15 | 16 | #[derive(Debug, Clone, From)] 17 | pub(crate) enum AssistantEvent { 18 | Signal(SignalEvent), 19 | InputSkeleton(ChatInputSkeletonEvent), 20 | Input(ChatInputEvent), 21 | ReplySkeleton(ChatReplySkeletonEvent), 22 | Reply(ChatReplyEvent), 23 | } 24 | 25 | #[derive(Debug, Clone, Serialize, Deserialize, Template)] 26 | #[template(path = "events/signal.html.j2")] 27 | #[serde(tag = "type", content = "data", rename_all = "snake_case")] 28 | pub(crate) enum SignalEvent { 29 | Processing(AssistantStep), 30 | Error(String), 31 | Complete, 32 | } 33 | 34 | #[derive(Debug, Clone, Serialize, Deserialize, Template)] 35 | #[template(path = "events/chat_input_skeleton.html.j2")] 36 | pub(crate) struct ChatInputSkeletonEvent { 37 | id: String, 38 | datetime: String, 39 | avatar: String, 40 | name: String, 41 | } 42 | 43 | #[derive(Debug, Clone, Serialize, Deserialize, Template)] 44 | #[template(path = "events/chat_input.html.j2")] 45 | pub(crate) struct ChatInputEvent { 46 | id: String, 47 | content: String, 48 | } 49 | 50 | #[derive(Debug, Clone, Serialize, Deserialize, Template)] 51 | #[template(path = "events/chat_reply_skeleton.html.j2")] 52 | pub(crate) struct ChatReplySkeletonEvent { 53 | id: String, 54 | avatar: String, // /public/images/ava-small.png 55 | name: String, // Ava 56 | } 57 | 58 | #[derive(Debug, Clone, Serialize, Deserialize, Template)] 59 | #[template(path = "events/chat_reply.html.j2")] 60 | pub(crate) struct ChatReplyEvent { 61 | id: String, 62 | data: ChatReplyData, 63 | } 64 | 65 | #[derive(Debug, Clone, Serialize, Deserialize, From)] 66 | #[serde(tag = "type", rename_all = "snake_case")] 67 | pub(crate) enum ChatReplyData { 68 | Speech(SpeechResult), 69 | Image(DrawImageResult), 70 | Markdown(WriteCodeResult), 71 | } 72 | 73 | #[derive(Debug, Clone, Serialize, Deserialize, Template)] 74 | #[template(path = "blocks/speech.html.j2")] 75 | pub(crate) struct SpeechResult { 76 | text: String, 77 | url: String, 78 | } 79 | 80 | #[derive(Debug, Clone, Serialize, Deserialize, EnumString, Display)] 81 | #[serde(rename_all = "snake_case")] 82 | #[strum(serialize_all = "snake_case")] 83 | pub(crate) enum AssistantStep { 84 | #[strum(serialize = "Uploading audio")] 85 | UploadAudio, 86 | #[strum(serialize = "Transcribing audio")] 87 | Transcription, 88 | #[strum(serialize = "Thinking hard")] 89 | Thinking, 90 | #[strum(serialize = "Organizing answer")] 91 | ChatCompletion, 92 | #[strum(serialize = "Drawing image")] 93 | DrawImage, 94 | #[strum(serialize = "Writing code")] 95 | WriteCode, 96 | #[strum(serialize = "Generating speech")] 97 | Speech, 98 | } 99 | 100 | impl ChatInputSkeletonEvent { 101 | pub fn new(id: impl Into) -> Self { 102 | Self { 103 | id: id.into(), 104 | datetime: Local::now().format("%Y-%m-%d %H:%M:%S").to_string(), 105 | avatar: "https://i.pravatar.cc/128".to_string(), 106 | name: "User".to_string(), 107 | } 108 | } 109 | } 110 | 111 | impl ChatInputEvent { 112 | pub fn new(id: impl Into, content: impl Into) -> Self { 113 | Self { 114 | id: id.into(), 115 | content: content.into(), 116 | } 117 | } 118 | } 119 | 120 | impl ChatReplySkeletonEvent { 121 | pub fn new(id: impl Into) -> Self { 122 | Self { 123 | id: id.into(), 124 | avatar: "/public/images/ava-small.png".to_string(), 125 | name: "Ava".to_string(), 126 | } 127 | } 128 | } 129 | 130 | impl ChatReplyEvent { 131 | pub fn new(id: impl Into, data: impl Into) -> Self { 132 | Self { 133 | id: id.into(), 134 | data: data.into(), 135 | } 136 | } 137 | } 138 | 139 | impl SpeechResult { 140 | fn new(text: impl Into, url: impl Into) -> Self { 141 | Self { 142 | text: text.into(), 143 | url: url.into(), 144 | } 145 | } 146 | 147 | fn new_text_only(text: impl Into) -> Self { 148 | Self::new(text, "".to_string()) 149 | } 150 | } 151 | 152 | impl From for String { 153 | fn from(event: AssistantEvent) -> Self { 154 | match event { 155 | AssistantEvent::Signal(v) => v.into(), 156 | AssistantEvent::InputSkeleton(v) => v.into(), 157 | AssistantEvent::Input(v) => v.into(), 158 | AssistantEvent::ReplySkeleton(v) => v.into(), 159 | AssistantEvent::Reply(v) => v.into(), 160 | } 161 | } 162 | } 163 | 164 | impl From for String { 165 | fn from(event: SignalEvent) -> Self { 166 | event.render().unwrap() 167 | } 168 | } 169 | 170 | impl From for String { 171 | fn from(event: ChatInputSkeletonEvent) -> Self { 172 | event.render().unwrap() 173 | } 174 | } 175 | 176 | impl From for String { 177 | fn from(event: ChatInputEvent) -> Self { 178 | event.render().unwrap() 179 | } 180 | } 181 | 182 | impl From for String { 183 | fn from(event: ChatReplySkeletonEvent) -> Self { 184 | event.render().unwrap() 185 | } 186 | } 187 | 188 | impl From for String { 189 | fn from(event: ChatReplyEvent) -> Self { 190 | event.render().unwrap() 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod extractors; 3 | pub mod handlers; 4 | mod tools; 5 | 6 | use std::{ 7 | env, 8 | path::{Path, PathBuf}, 9 | }; 10 | 11 | use clap::Parser; 12 | use dashmap::DashMap; 13 | use handlers::AssistantEvent; 14 | use llm_sdk::LlmSdk; 15 | use tokio::sync::broadcast; 16 | 17 | #[derive(Debug, Parser)] 18 | #[clap(name = "ava")] 19 | pub struct Args { 20 | #[clap(short, long, default_value = "8080")] 21 | pub port: u16, 22 | #[clap(short, long, default_value = "./.certs")] 23 | pub cert_path: String, 24 | } 25 | 26 | #[derive(Debug)] 27 | pub struct AppState { 28 | pub(crate) llm: LlmSdk, 29 | // each device_id has a channel to send messages to 30 | pub(crate) events: DashMap>, 31 | } 32 | 33 | impl Default for AppState { 34 | fn default() -> Self { 35 | Self { 36 | llm: LlmSdk::new( 37 | "https://api.openai.com/v1", 38 | env::var("OPENAI_API_KEY").unwrap(), 39 | 3, 40 | ), 41 | events: DashMap::new(), 42 | } 43 | } 44 | } 45 | 46 | pub fn audio_path(device_id: &str, name: &str) -> PathBuf { 47 | Path::new("/tmp/ava-bot/audio") 48 | .join(device_id) 49 | .join(format!("{}.mp3", name)) 50 | } 51 | 52 | pub fn audio_url(device_id: &str, name: &str) -> String { 53 | format!("/assets/audio/{}/{}.mp3", device_id, name) 54 | } 55 | 56 | pub fn image_path(device_id: &str, name: &str) -> PathBuf { 57 | Path::new("/tmp/ava-bot/image") 58 | .join(device_id) 59 | .join(format!("{}.png", name)) 60 | } 61 | 62 | pub fn image_url(device_id: &str, name: &str) -> String { 63 | format!("/assets/image/{}/{}.png", device_id, name) 64 | } 65 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use ava_bot::{ 3 | handlers::{assistant_handler, events_handler, index_page}, 4 | AppState, Args, 5 | }; 6 | use axum::{ 7 | routing::{get, post}, 8 | Router, 9 | }; 10 | use axum_server::tls_rustls::RustlsConfig; 11 | use clap::Parser; 12 | use std::sync::Arc; 13 | use tower_http::services::ServeDir; 14 | use tracing::info; 15 | 16 | #[tokio::main] 17 | async fn main() -> Result<()> { 18 | tracing_subscriber::fmt::init(); 19 | 20 | let args = Args::parse(); 21 | let state = Arc::new(AppState::default()); 22 | let app = Router::new() 23 | .route("/", get(index_page)) 24 | .route("/events", get(events_handler)) 25 | .route("/assistant", post(assistant_handler)) 26 | .nest_service("/public", ServeDir::new("./public")) 27 | .nest_service("/assets", ServeDir::new("/tmp/ava-bot")) 28 | .with_state(state); 29 | 30 | let addr = format!("0.0.0.0:{}", args.port); 31 | info!("Listening on {}", addr); 32 | 33 | let cert = std::fs::read(format!("{}/cert.pem", args.cert_path))?; 34 | let key = std::fs::read(format!("{}/key.pem", args.cert_path))?; 35 | let config = RustlsConfig::from_pem(cert, key).await?; 36 | axum_server::bind_rustls(addr.parse()?, config) 37 | .serve(app.into_make_service()) 38 | .await?; 39 | 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /src/tools/mod.rs: -------------------------------------------------------------------------------- 1 | use askama::Template; 2 | use llm_sdk::{ChatCompletionMessage, ChatCompletionRequest, Tool}; 3 | use schemars::JsonSchema; 4 | use serde::{Deserialize, Serialize}; 5 | use strum::{Display, EnumString}; 6 | 7 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumString, Display)] 8 | #[serde(rename_all = "snake_case")] 9 | #[strum(serialize_all = "snake_case")] 10 | pub(crate) enum AssistantTool { 11 | /// Draw a picture based on user's input 12 | DrawImage, 13 | /// Write code based on user's input 14 | WriteCode, 15 | /// Just reply based on user's input 16 | Answer, 17 | } 18 | 19 | #[derive(Debug, Clone, Deserialize, JsonSchema)] 20 | pub(crate) struct DrawImageArgs { 21 | /// The revised prompt for creating the image 22 | pub(crate) prompt: String, 23 | } 24 | 25 | #[derive(Debug, Clone, Serialize, Deserialize, Template)] 26 | #[template(path = "blocks/image.html.j2")] 27 | pub(crate) struct DrawImageResult { 28 | /// image url 29 | pub(crate) url: String, 30 | /// revised prompt 31 | pub(crate) prompt: String, 32 | } 33 | 34 | #[derive(Debug, Clone, Serialize, Deserialize, Template)] 35 | #[template(path = "blocks/markdown.html.j2")] 36 | pub(crate) struct WriteCodeResult { 37 | /// revised prompt 38 | pub(crate) content: String, 39 | } 40 | 41 | #[derive(Debug, Clone, Deserialize, JsonSchema)] 42 | pub(crate) struct WriteCodeArgs { 43 | /// The revised prompt for writing the code 44 | pub(crate) prompt: String, 45 | } 46 | 47 | #[derive(Debug, Clone, Deserialize, JsonSchema)] 48 | pub(crate) struct AnswerArgs { 49 | /// question or prompt from user 50 | pub(crate) prompt: String, 51 | } 52 | 53 | pub(crate) fn tool_completion_request( 54 | input: impl Into, 55 | name: &str, 56 | ) -> ChatCompletionRequest { 57 | let messages = vec![ 58 | ChatCompletionMessage::new_system("I can help to identify which tool to use, if no proper tool could be used, I'll directly reply the message with pure text", "Ava"), 59 | ChatCompletionMessage::new_user(input.into(), name) 60 | ]; 61 | ChatCompletionRequest::new_with_tools(messages, all_tools()) 62 | } 63 | 64 | // TODO: llm-sdk shall provide fuctionality to generate this code 65 | fn all_tools() -> Vec { 66 | vec![ 67 | Tool::new_function::("draw_image", "Draw an image based on the prompt."), 68 | Tool::new_function::("write_code", "Write code based on the prompt."), 69 | Tool::new_function::("answer", "Just reply based on the prompt."), 70 | ] 71 | } 72 | 73 | impl DrawImageResult { 74 | pub(crate) fn new(url: impl Into, prompt: impl Into) -> Self { 75 | Self { 76 | url: url.into(), 77 | prompt: prompt.into(), 78 | } 79 | } 80 | } 81 | 82 | impl WriteCodeResult { 83 | pub(crate) fn new(content: impl Into) -> Self { 84 | Self { 85 | content: content.into(), 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/**/*.rs", 5 | "./templates/**/*.html.j2", 6 | "./public/**/*.{html,js,css}", 7 | ], 8 | theme: { 9 | extend: { 10 | width: { 11 | 128: "32rem", 12 | 192: "48rem", 13 | 256: "64rem", 14 | }, 15 | height: { 16 | 128: "32rem", 17 | 192: "48rem", 18 | 256: "64rem", 19 | }, 20 | }, 21 | }, 22 | plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")], 23 | }; 24 | -------------------------------------------------------------------------------- /templates/base.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Ava Bot 8 | 9 | 12 | 13 | 14 | 15 | 16 | {% block content %}{% endblock %} {% block script %}{% endblock %} 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /templates/blocks/image.html.j2: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% if url.is_empty() %} 4 |
5 |
6 | 7 | {% else %} 8 | 9 | {% endif %} 10 |
11 |
12 |

{{ prompt }}

13 |
14 |
15 | -------------------------------------------------------------------------------- /templates/blocks/markdown.html.j2: -------------------------------------------------------------------------------- 1 |
2 | {{ content|safe }} 3 |
4 | -------------------------------------------------------------------------------- /templates/blocks/speech.html.j2: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% if url.is_empty() %} 4 |
5 |
6 | {% else %} 7 |
8 | 11 |
12 | {% endif %} 13 |
14 |
15 |

{{ text }}

16 |
17 |
18 | -------------------------------------------------------------------------------- /templates/events/chat_input.html.j2: -------------------------------------------------------------------------------- 1 | {{ content }} 2 | -------------------------------------------------------------------------------- /templates/events/chat_input_skeleton.html.j2: -------------------------------------------------------------------------------- 1 |
  • 2 | 4 | {{ name }} 5 | 6 |
    8 | 9 |
    10 |
    11 |
    12 |
    13 | Loading... 14 |
    15 | 16 |
    17 |
    18 |
  • 19 | -------------------------------------------------------------------------------- /templates/events/chat_reply.html.j2: -------------------------------------------------------------------------------- 1 | {% match data %} 2 | {% when ChatReplyData::Speech with (v) %} 3 | {{ v|safe }} 4 | {% when ChatReplyData::Markdown with (v) %} 5 | {{ v|safe }} 6 | {% when ChatReplyData::Image with (v) %} 7 | {{ v|safe }} 8 | {% endmatch %} 9 | -------------------------------------------------------------------------------- /templates/events/chat_reply_skeleton.html.j2: -------------------------------------------------------------------------------- 1 |
  • 2 | 4 | {{ name }} 5 | 6 |
    8 |
    9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 | Loading... 17 |
    18 |
    19 |
    20 |
  • 21 | -------------------------------------------------------------------------------- /templates/events/signal.html.j2: -------------------------------------------------------------------------------- 1 | {% match self %} 2 | {% when SignalEvent::Processing with (v) %} 3 |

    {{ v }}...

    4 | {% when SignalEvent::Error with (v) %} 5 |

    Error: {{ v }}

    6 | {% when SignalEvent::Complete %} 7 |

    Completed!

    8 | {% else %} 9 |

    Unknown event

    10 | {% endmatch %} 11 | -------------------------------------------------------------------------------- /templates/index.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "base.html.j2" %} {% block content %} 2 |
    3 |

    Ava Bot

    4 |
      5 |
    6 | 7 |
    8 | 12 |
    13 |
    14 |
    15 |
    16 | 17 | 18 | {% endblock %} 19 | {% block script %} 20 | 148 | {% endblock %} 149 | -------------------------------------------------------------------------------- /test.http: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | ## events API 4 | 5 | GET https://127.0.0.1:8080/events 6 | Cookie: hello=world; device_id=1234 7 | Accept: text/event-stream 8 | 9 | 10 | ## Notion API test 11 | 12 | @token = {{$processEnv NOTION_API_KEY}} 13 | @db = cb88ac0aa1ee4e5aa29b98151870bd52 14 | 15 | ### Get a db 16 | 17 | GET https://api.notion.com/v1/databases/{{db}} 18 | Notion-Version: 2022-06-28 19 | Authorization: Bearer {{token}} 20 | 21 | ### Create a page in database 22 | 23 | POST https://api.notion.com/v1/pages 24 | Notion-Version: 2022-06-28 25 | Authorization: Bearer {{token}} 26 | Content-Type: application/json 27 | 28 | { 29 | "parent": { "database_id": "{{db}}" }, 30 | "properties": { 31 | "Title": { 32 | "title": [ 33 | { 34 | "text": { 35 | "content": "Go shopping" 36 | } 37 | } 38 | ] 39 | }, 40 | "Finished": { 41 | "checkbox": true 42 | }, 43 | "Priority": { 44 | "select": { 45 | "name": "Medium" 46 | } 47 | } 48 | } 49 | } 50 | --------------------------------------------------------------------------------