Merge pull request #4656 from SillyTavern/staging Staging
Signed| @@ -41,6 +41,16 @@ labels: | ||
| 41 | 41 | 🔬 This PR needs testing! |
| 42 | 42 | Any contributor can test and leave reviews, so feel free to help us out! |
| 43 | 43 | |
| 44 | + - name: ❗ Against Release Branch | |
| 45 | + labeled: | |
| 46 | + pr: | |
| 47 | + body: > | |
| 48 | + ❗ This PR is against the `release` branch. | |
| 49 | + | |
| 50 | + Please make sure this was intended, and you did not want to target the `staging` branch. Only hotfixes, readme changes and similar should be made against `release`. | |
| 51 | + | |
| 52 | + You can change the target branch **without recreating the PR** by clicking "Edit" at the top of the page. | |
| 53 | + | |
| 44 | 54 | - name: 🟥 ⬤⬤⬤⬤⬤ |
| 45 | 55 | labeled: |
| 46 | 56 | pr: |
| @@ -2,4 +2,4 @@ | ||
| 2 | 2 | |
| 3 | 3 | ## Checklist: |
| 4 | 4 | |
| 5 | 5 | - [ ] I have read the [ContributingContribution guidelines](https://github.com/SillyTavern/SillyTavern/blob/release/CONTRIBUTING.md). |
| @@ -15,8 +15,19 @@ jobs: | ||
| 15 | 15 | label-on-content: |
| 16 | 16 | name: 🏷️ Label Issues by Content |
| 17 | 17 | runs-on: ubuntu-latest |
| 18 | + if: always() | |
| 18 | 19 | |
| 19 | 20 | steps: |
| 21 | + - name: Mint App Token | |
| 22 | + id: app | |
| 23 | + # Create a GitHub App token | |
| 24 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 25 | + uses: actions/create-github-app-token@v2 | |
| 26 | + with: | |
| 27 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 28 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 29 | + owner: ${{ github.repository_owner }} | |
| 30 | + | |
| 20 | 31 | - name: Checkout Repository |
| 21 | 32 | # Checkout |
| 22 | 33 | # https://github.com/marketplace/actions/checkout |
| @@ -32,13 +43,25 @@ jobs: | ||
| 32 | 43 | with: |
| 33 | 44 | configuration-path: .github/issues-auto-labels.yml |
| 34 | 45 | enable-versioned-regex: 0 |
| 35 | 46 | repo-token: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 36 | 47 | |
| 37 | 48 | label-on-labels: |
| 38 | 49 | name: 🏷️ Label Issues by Labels |
| 50 | + needs: [label-on-content] | |
| 39 | 51 | runs-on: ubuntu-latest |
| 52 | + if: always() | |
| 40 | 53 | |
| 41 | 54 | steps: |
| 55 | + - name: Mint App Token | |
| 56 | + id: app | |
| 57 | + # Create a GitHub App token | |
| 58 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 59 | + uses: actions/create-github-app-token@v2 | |
| 60 | + with: | |
| 61 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 62 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 63 | + owner: ${{ github.repository_owner }} | |
| 64 | + | |
| 42 | 65 | - name: ✅ Add "👍 Approved" for relevant labels |
| 43 | 66 | if: contains(fromJSON('["👩💻 Good First Issue", "🙏 Help Wanted", "🪲 Confirmed", "⚠️ High Priority", "❕ Medium Priority", "💤 Low Priority"]'), github.event.label.name) |
| 44 | 67 | # 🤖 Issues Helper |
| @@ -46,7 +69,7 @@ jobs: | ||
| 46 | 69 | uses: actions-cool/issues-helper@v3.6.0 |
| 47 | 70 | with: |
| 48 | 71 | actions: 'add-labels' |
| 49 | 72 | token: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 50 | 73 | labels: '👍 Approved' |
| 51 | 74 | |
| 52 | 75 | - name: ❌ Remove progress labels when issue is marked done or stale |
| @@ -56,7 +79,7 @@ jobs: | ||
| 56 | 79 | uses: actions-cool/issues-helper@v3.6.0 |
| 57 | 80 | with: |
| 58 | 81 | actions: 'remove-labels' |
| 59 | 82 | token: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 60 | 83 | labels: '🧑💻 In Progress,🤔 Unsure,🤔 Under Consideration' |
| 61 | 84 | |
| 62 | 85 | - name: ❌ Remove temporary labels when confirmed labels are added |
| @@ -66,7 +89,7 @@ jobs: | ||
| 66 | 89 | uses: actions-cool/issues-helper@v3.6.0 |
| 67 | 90 | with: |
| 68 | 91 | actions: 'remove-labels' |
| 69 | 92 | token: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 70 | 93 | labels: '🤔 Unsure,🤔 Under Consideration' |
| 71 | 94 | |
| 72 | 95 | - name: ❌ Remove no bug labels when "🪲 Confirmed" is added |
| @@ -76,32 +99,54 @@ jobs: | ||
| 76 | 99 | uses: actions-cool/issues-helper@v3.6.0 |
| 77 | 100 | with: |
| 78 | 101 | actions: 'remove-labels' |
| 79 | 102 | token: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 80 | 103 | labels: '✖️ Not Reproducible,✖️ Not A Bug' |
| 81 | 104 | |
| 82 | 105 | remove-stale-label: |
| 83 | 106 | name: 🗑️ Remove Stale Label on Comment |
| 107 | + needs: [label-on-content, label-on-labels] | |
| 84 | 108 | runs-on: ubuntu-latest |
| 85 | 109 | # Only run this on new comments, to automatically remove the stale label |
| 86 | 110 | if: always() && (github.event_name == 'issue_comment' && github.actorevent.sender.type != 'github-actions[bot]Bot') |
| 87 | 111 | |
| 88 | 112 | steps: |
| 113 | + - name: Mint App Token | |
| 114 | + id: app | |
| 115 | + # Create a GitHub App token | |
| 116 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 117 | + uses: actions/create-github-app-token@v2 | |
| 118 | + with: | |
| 119 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 120 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 121 | + owner: ${{ github.repository_owner }} | |
| 122 | + | |
| 89 | 123 | - name: Remove Stale Label |
| 90 | 124 | # 🤖 Issues Helper |
| 91 | 125 | # https://github.com/marketplace/actions/issues-helper |
| 92 | 126 | uses: actions-cool/issues-helper@v3.6.0 |
| 93 | 127 | with: |
| 94 | 128 | actions: 'remove-labels' |
| 95 | 129 | token: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 96 | 130 | issue-number: ${{ github.event.issue.number }} |
| 97 | 131 | labels: '⚰️ Stale,🕸️ Inactive,🚏 Awaiting User Response,🛑 No Response' |
| 98 | 132 | |
| 99 | 133 | write-auto-comments: |
| 100 | 134 | name: 💬 Post Issue Comments Based on Labels |
| 101 | 135 | needs: [label-on-content, label-on-labels, remove-stale-label] |
| 102 | 136 | runs-on: ubuntu-latest |
| 137 | + if: always() | |
| 103 | 138 | |
| 104 | 139 | steps: |
| 140 | + - name: Mint App Token | |
| 141 | + id: app | |
| 142 | + # Create a GitHub App token | |
| 143 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 144 | + uses: actions/create-github-app-token@v2 | |
| 145 | + with: | |
| 146 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 147 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 148 | + owner: ${{ github.repository_owner }} | |
| 149 | + | |
| 105 | 150 | - name: Checkout Repository |
| 106 | 151 | # Checkout |
| 107 | 152 | # https://github.com/marketplace/actions/checkout |
| @@ -113,4 +158,4 @@ jobs: | ||
| 113 | 158 | uses: peaceiris/actions-label-commenter@v1.10.0 |
| 114 | 159 | with: |
| 115 | 160 | config_file: .github/issues-auto-comments.yml |
| 116 | 161 | github_token: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| @@ -15,8 +15,19 @@ jobs: | ||
| 15 | 15 | update-linked-issues: |
| 16 | 16 | name: 🔗 Mark Linked Issues Done on Push |
| 17 | 17 | runs-on: ubuntu-latest |
| 18 | + if: always() | |
| 18 | 19 | |
| 19 | 20 | steps: |
| 21 | + - name: Mint App Token | |
| 22 | + id: app | |
| 23 | + # Create a GitHub App token | |
| 24 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 25 | + uses: actions/create-github-app-token@v2 | |
| 26 | + with: | |
| 27 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 28 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 29 | + owner: ${{ github.repository_owner }} | |
| 30 | + | |
| 20 | 31 | - name: Checkout Repository |
| 21 | 32 | # Checkout |
| 22 | 33 | # https://github.com/marketplace/actions/checkout |
| @@ -31,7 +42,7 @@ jobs: | ||
| 31 | 42 | - name: Label Linked Issues |
| 32 | 43 | id: label_linked_issues |
| 33 | 44 | env: |
| 34 | 45 | GH_TOKEN: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 35 | 46 | run: | |
| 36 | 47 | for ISSUE in $(echo $issues | jq -r '.[]'); do |
| 37 | 48 | if [ "${{ github.ref }}" == "refs/heads/staging" ]; then |
| @@ -15,14 +15,25 @@ jobs: | ||
| 15 | 15 | mark-inactivity: |
| 16 | 16 | name: ⏳ Mark Issues/PRs without Activity |
| 17 | 17 | runs-on: ubuntu-latest |
| 18 | + if: always() | |
| 18 | 19 | |
| 19 | 20 | steps: |
| 21 | + - name: Mint App Token | |
| 22 | + id: app | |
| 23 | + # Create a GitHub App token | |
| 24 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 25 | + uses: actions/create-github-app-token@v2 | |
| 26 | + with: | |
| 27 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 28 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 29 | + owner: ${{ github.repository_owner }} | |
| 30 | + | |
| 20 | 31 | - name: Mark Issues/PRs without Activity |
| 21 | 32 | # Close Stale Issues and PRs |
| 22 | 33 | # https://github.com/marketplace/actions/close-stale-issues |
| 23 | 34 | uses: actions/stale@v9.1.0 |
| 24 | 35 | with: |
| 25 | 36 | repo-token: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 26 | 37 | days-before-stale: 183 |
| 27 | 38 | days-before-close: 7 |
| 28 | 39 | operations-per-run: 30 |
| @@ -47,16 +58,27 @@ jobs: | ||
| 47 | 58 | |
| 48 | 59 | await-user-response: |
| 49 | 60 | name: ⚠️ Mark Issues/PRs Awaiting User Response |
| 61 | + needs: [mark-inactivity] | |
| 50 | 62 | runs-on: ubuntu-latest |
| 51 | - needs: mark-inactivity | |
| 63 | + if: always() | |
| 52 | 64 | |
| 53 | 65 | steps: |
| 66 | + - name: Mint App Token | |
| 67 | + id: app | |
| 68 | + # Create a GitHub App token | |
| 69 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 70 | + uses: actions/create-github-app-token@v2 | |
| 71 | + with: | |
| 72 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 73 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 74 | + owner: ${{ github.repository_owner }} | |
| 75 | + | |
| 54 | 76 | - name: Mark Issues/PRs Awaiting User Response |
| 55 | 77 | # Close Stale Issues and PRs |
| 56 | 78 | # https://github.com/marketplace/actions/close-stale-issues |
| 57 | 79 | uses: actions/stale@v9.1.0 |
| 58 | 80 | with: |
| 59 | 81 | repo-token: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 60 | 82 | days-before-stale: 7 |
| 61 | 83 | days-before-close: 7 |
| 62 | 84 | operations-per-run: 30 |
| @@ -74,16 +96,27 @@ jobs: | ||
| 74 | 96 | |
| 75 | 97 | alternative-exists: |
| 76 | 98 | name: 🔄 Mark Issues with Alternative Exists |
| 99 | + needs: [mark-inactivity, await-user-response] | |
| 77 | 100 | runs-on: ubuntu-latest |
| 78 | - needs: await-user-response | |
| 101 | + if: always() | |
| 79 | 102 | |
| 80 | 103 | steps: |
| 104 | + - name: Mint App Token | |
| 105 | + id: app | |
| 106 | + # Create a GitHub App token | |
| 107 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 108 | + uses: actions/create-github-app-token@v2 | |
| 109 | + with: | |
| 110 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 111 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 112 | + owner: ${{ github.repository_owner }} | |
| 113 | + | |
| 81 | 114 | - name: Mark Issues with Alternative Exists |
| 82 | 115 | # Close Stale Issues and PRs |
| 83 | 116 | # https://github.com/marketplace/actions/close-stale-issues |
| 84 | 117 | uses: actions/stale@v9.1.0 |
| 85 | 118 | with: |
| 86 | 119 | repo-token: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 87 | 120 | days-before-stale: 7 |
| 88 | 121 | days-before-close: 7 |
| 89 | 122 | operations-per-run: 30 |
| @@ -15,14 +15,25 @@ jobs: | ||
| 15 | 15 | remove-labels: |
| 16 | 16 | name: 🗑️ Remove Pending Labels on Close |
| 17 | 17 | runs-on: ubuntu-latest |
| 18 | + if: always() | |
| 18 | 19 | |
| 19 | 20 | steps: |
| 21 | + - name: Mint App Token | |
| 22 | + id: app | |
| 23 | + # Create a GitHub App token | |
| 24 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 25 | + uses: actions/create-github-app-token@v2 | |
| 26 | + with: | |
| 27 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 28 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 29 | + owner: ${{ github.repository_owner }} | |
| 30 | + | |
| 20 | 31 | - name: Remove Pending Labels on Close |
| 21 | 32 | # 🤖 Issues Helper |
| 22 | 33 | # https://github.com/marketplace/actions/issues-helper |
| 23 | 34 | uses: actions-cool/issues-helper@v3.6.0 |
| 24 | 35 | with: |
| 25 | 36 | actions: remove-labels |
| 26 | 37 | token: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 27 | 38 | issue-number: ${{ github.event.issue.number || github.event.pull_request.number }} |
| 28 | 39 | labels: '🚏 Awaiting User Response,🧑💻 In Progress,📌 Keep Open,🚫 Merge Conflicts,🔬 Needs Testing,🔨 Needs Work,⚰️ Stale,⛔ Waiting For External/Upstream' |
| @@ -15,15 +15,25 @@ jobs: | ||
| 15 | 15 | label-maintainer: |
| 16 | 16 | name: 🏷️ Label if Author is a Repo Maintainer |
| 17 | 17 | runs-on: ubuntu-latest |
| 18 | 18 | if: always() && contains(fromJson('["Cohee1207", "RossAscends", "Wolfsblvt"]'), github.actor) |
| 19 | 19 | |
| 20 | 20 | steps: |
| 21 | + - name: Mint App Token | |
| 22 | + id: app | |
| 23 | + # Create a GitHub App token | |
| 24 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 25 | + uses: actions/create-github-app-token@v2 | |
| 26 | + with: | |
| 27 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 28 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 29 | + owner: ${{ github.repository_owner }} | |
| 30 | + | |
| 21 | 31 | - name: Label if Author is a Repo Maintainer |
| 22 | 32 | # 🤖 Issues Helper |
| 23 | 33 | # https://github.com/marketplace/actions/issues-helper |
| 24 | 34 | uses: actions-cool/issues-helper@v3.6.0 |
| 25 | 35 | with: |
| 26 | 36 | actions: 'add-labels' |
| 27 | 37 | token: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 28 | 38 | issue-number: ${{ github.event.issue.number || github.event.pull_request.number }} |
| 29 | 39 | labels: '👷 Maintainer' |
| @@ -16,7 +16,7 @@ jobs: | ||
| 16 | 16 | name: ✅ Check ESLint on PR |
| 17 | 17 | runs-on: ubuntu-latest |
| 18 | 18 | # Only needs to run when code is changed |
| 19 | 19 | if: always() && (github.event.action == 'opened' || github.event.action == 'synchronize') |
| 20 | 20 | |
| 21 | 21 | # Override permissions, linter likely needs write access to issues |
| 22 | 22 | permissions: |
| @@ -48,7 +48,7 @@ jobs: | ||
| 48 | 48 | # https://github.com/marketplace/actions/action-eslint |
| 49 | 49 | uses: sibiraj-s/action-eslint@v3.0.1 |
| 50 | 50 | with: |
| 51 | 51 | token: ${{ secrets.GITHUB_TOKEN }} # ESLint can run with the original permissions |
| 52 | 52 | eslint-args: '--ignore-path=.gitignore --quiet' |
| 53 | 53 | extensions: 'js' |
| 54 | 54 | annotations: true |
| @@ -71,12 +71,22 @@ jobs: | ||
| 71 | 71 | pull-requests: write |
| 72 | 72 | |
| 73 | 73 | steps: |
| 74 | + - name: Mint App Token | |
| 75 | + id: app | |
| 76 | + # Create a GitHub App token | |
| 77 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 78 | + uses: actions/create-github-app-token@v2 | |
| 79 | + with: | |
| 80 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 81 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 82 | + owner: ${{ github.repository_owner }} | |
| 83 | + | |
| 74 | 84 | - name: Label PR Size |
| 75 | 85 | # Pull Request Size Labeler |
| 76 | 86 | # https://github.com/marketplace/actions/pull-request-size-labeler |
| 77 | 87 | uses: codelytv/pr-size-labeler@v1.10.2 |
| 78 | 88 | with: |
| 79 | 89 | GITHUB_TOKEN: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 80 | 90 | xs_label: '🟩 ⬤○○○○' |
| 81 | 91 | xs_max_size: '20' |
| 82 | 92 | s_label: '🟩 ⬤⬤○○○' |
| @@ -95,9 +105,19 @@ jobs: | ||
| 95 | 105 | name: 🏷️ Label PR by Branches |
| 96 | 106 | runs-on: ubuntu-latest |
| 97 | 107 | # Only label once when PR is created or when base branch is changed, to allow manual label removal |
| 98 | 108 | if: always() && (github.event.action == 'opened' || (github.event.action == 'synchronize' && github.event.changes.base)) |
| 99 | 109 | |
| 100 | 110 | steps: |
| 111 | + - name: Mint App Token | |
| 112 | + id: app | |
| 113 | + # Create a GitHub App token | |
| 114 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 115 | + uses: actions/create-github-app-token@v2 | |
| 116 | + with: | |
| 117 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 118 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 119 | + owner: ${{ github.repository_owner }} | |
| 120 | + | |
| 101 | 121 | - name: Checkout Repository |
| 102 | 122 | # Checkout |
| 103 | 123 | # https://github.com/marketplace/actions/checkout |
| @@ -109,15 +129,26 @@ jobs: | ||
| 109 | 129 | uses: actions/labeler@v5.0.0 |
| 110 | 130 | with: |
| 111 | 131 | configuration-path: .github/pr-auto-labels-by-branch.yml |
| 112 | 132 | repo-token: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 113 | 133 | |
| 114 | 134 | label-by-files: |
| 115 | 135 | name: 🏷️ Label PR by Files |
| 136 | + needs: [label-by-branches] | |
| 116 | 137 | runs-on: ubuntu-latest |
| 117 | 138 | # Only needs to run when code is changed |
| 118 | 139 | if: always() && (github.event.action == 'opened' || github.event.action == 'synchronize') |
| 119 | 140 | |
| 120 | 141 | steps: |
| 142 | + - name: Mint App Token | |
| 143 | + id: app | |
| 144 | + # Create a GitHub App token | |
| 145 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 146 | + uses: actions/create-github-app-token@v2 | |
| 147 | + with: | |
| 148 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 149 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 150 | + owner: ${{ github.repository_owner }} | |
| 151 | + | |
| 121 | 152 | - name: Checkout Repository |
| 122 | 153 | # Checkout |
| 123 | 154 | # https://github.com/marketplace/actions/checkout |
| @@ -127,15 +158,18 @@ jobs: | ||
| 127 | 158 | # Pull Request Labeler |
| 128 | 159 | # https://github.com/marketplace/actions/labeler |
| 129 | 160 | uses: actions/labeler@v5.0.0 |
| 161 | + env: | |
| 162 | + GITHUB_TOKEN: ${{ steps.app.outputs.token }} # labeler action needs some handholding | |
| 130 | 163 | with: |
| 131 | 164 | configuration-path: .github/pr-auto-labels-by-files.yml |
| 132 | 165 | repo-token: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 133 | 166 | |
| 134 | 167 | remove-stale-label: |
| 135 | 168 | name: 🗑️ Remove Stale Label on Comment |
| 169 | + needs: [label-by-branches, label-by-files] | |
| 136 | 170 | runs-on: ubuntu-latest |
| 137 | 171 | # Only runs on comments not done by the github actions bot |
| 138 | 172 | if: always() && (github.event_name == 'pull_request_review_comment' && github.actorevent.sender.type != 'github-actions[bot]Bot') |
| 139 | 173 | |
| 140 | 174 | # Override permissions, issue labeler needs issues write access |
| 141 | 175 | permissions: |
| @@ -144,19 +178,33 @@ jobs: | ||
| 144 | 178 | pull-requests: write |
| 145 | 179 | |
| 146 | 180 | steps: |
| 181 | + - name: Mint App Token | |
| 182 | + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} | |
| 183 | + id: app | |
| 184 | + # Only run if the PR is from the same repository | |
| 185 | + # This action runs on comments, which will not receive the env vars for this | |
| 186 | + # Create a GitHub App token | |
| 187 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 188 | + uses: actions/create-github-app-token@v2 | |
| 189 | + with: | |
| 190 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 191 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 192 | + owner: ${{ github.repository_owner }} | |
| 193 | + | |
| 147 | 194 | - name: Remove Stale Label |
| 195 | + if: always() | |
| 148 | 196 | # 🤖 Issues Helper |
| 149 | 197 | # https://github.com/marketplace/actions/issues-helper |
| 150 | 198 | uses: actions-cool/issues-helper@v3.6.0 |
| 151 | 199 | with: |
| 152 | 200 | actions: 'remove-labels' |
| 153 | - token: ${{ secrets.GITHUB_TOKEN }} | |
| 201 | + token: ${{ steps.app.outputs.token || github.token }} # Use fallback to GITHUB_TOKEN if app token is not available | |
| 154 | 202 | issue-number: ${{ github.event.pull_request.number }} |
| 155 | 203 | labels: '⚰️ Stale' |
| 156 | 204 | |
| 157 | 205 | check-merge-blocking-labels: |
| 158 | 206 | name: 🚫 Check Merge Blocking Labels |
| 159 | 207 | needs: [label-by-branches, label-by-files, remove-stale-label] |
| 160 | 208 | runs-on: ubuntu-latest |
| 161 | 209 | # Run, even if the previous jobs were skipped/failed |
| 162 | 210 | if: always() |
| @@ -206,12 +254,22 @@ jobs: | ||
| 206 | 254 | |
| 207 | 255 | write-auto-comments: |
| 208 | 256 | name: 💬 Post PR Comments Based on Labels |
| 209 | 257 | needs: [label-by-branches, label-by-files, remove-stale-label, check-merge-blocking-labels] |
| 210 | 258 | runs-on: ubuntu-latest |
| 211 | 259 | # Run, even if the previous jobs were skipped/failed |
| 212 | - if: always() | |
| 260 | + if: always() && (github.event_name == 'pull_request_target') | |
| 213 | 261 | |
| 214 | 262 | steps: |
| 263 | + - name: Mint App Token | |
| 264 | + id: app | |
| 265 | + # Create a GitHub App token | |
| 266 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 267 | + uses: actions/create-github-app-token@v2 | |
| 268 | + with: | |
| 269 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 270 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 271 | + owner: ${{ github.repository_owner }} | |
| 272 | + | |
| 215 | 273 | - name: Checkout Repository |
| 216 | 274 | # Checkout |
| 217 | 275 | # https://github.com/marketplace/actions/checkout |
| @@ -223,13 +281,17 @@ jobs: | ||
| 223 | 281 | uses: peaceiris/actions-label-commenter@v1.10.0 |
| 224 | 282 | with: |
| 225 | 283 | config_file: .github/pr-auto-comments.yml |
| 226 | 284 | github_token: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 227 | 285 | |
| 228 | 286 | # This runs on merged PRs to staging, reading the PR body and directly linked issues. Check `issues-updates-on-merge.yml`:`update-linked-issues` for commit-based updates. |
| 229 | 287 | update-linked-issues: |
| 230 | 288 | name: 🔗 Mark Linked Issues Done on Staging Merge |
| 231 | 289 | runs-on: ubuntu-latest |
| 232 | - if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'staging' | |
| 290 | + if: > | |
| 291 | + always() && | |
| 292 | + github.event_name == 'pull_request_target' && | |
| 293 | + github.event.pull_request.merged == true && | |
| 294 | + github.event.pull_request.base.ref == 'staging' | |
| 233 | 295 | |
| 234 | 296 | # Override permissions, We need to be able to write to issues |
| 235 | 297 | permissions: |
| @@ -238,6 +300,16 @@ jobs: | ||
| 238 | 300 | pull-requests: write |
| 239 | 301 | |
| 240 | 302 | steps: |
| 303 | + - name: Mint App Token | |
| 304 | + id: app | |
| 305 | + # Create a GitHub App token | |
| 306 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 307 | + uses: actions/create-github-app-token@v2 | |
| 308 | + with: | |
| 309 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 310 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 311 | + owner: ${{ github.repository_owner }} | |
| 312 | + | |
| 241 | 313 | - name: Extract Linked Issues From PR Description |
| 242 | 314 | id: extract_issues |
| 243 | 315 | run: | |
| @@ -250,7 +322,7 @@ jobs: | ||
| 250 | 322 | PR_NUMBER=${{ github.event.pull_request.number }} |
| 251 | 323 | REPO=${{ github.repository }} |
| 252 | 324 | API_URL="https://api.github.com/repos/$REPO/pulls/$PR_NUMBER/issues" |
| 253 | 325 | ISSUES=$(curl -s -H "Authorization: token ${{ secretssteps.GITHUB_TOKENapp.outputs.token }}" "$API_URL" | jq -r '.[].number' | jq -R -s -c 'split("\n")[:-1]') |
| 254 | 326 | echo "linked_issues=$ISSUES" >> $GITHUB_ENV |
| 255 | 327 | |
| 256 | 328 | - name: Merge Issue Lists |
| @@ -262,7 +334,7 @@ jobs: | ||
| 262 | 334 | - name: Label Linked Issues |
| 263 | 335 | id: label_linked_issues |
| 264 | 336 | env: |
| 265 | 337 | GH_TOKEN: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 266 | 338 | run: | |
| 267 | 339 | for ISSUE in $(echo $final_issues | jq -r '.[]'); do |
| 268 | 340 | gh issue edit $ISSUE -R ${{ github.repository }} --add-label "✅ Done (staging)" --remove-label "🧑💻 In Progress" |
| @@ -15,14 +15,25 @@ jobs: | ||
| 15 | 15 | check-merge-conflicts: |
| 16 | 16 | name: ⚔️ Check Merge Conflicts |
| 17 | 17 | runs-on: ubuntu-latest |
| 18 | + if: always() | |
| 18 | 19 | |
| 19 | 20 | steps: |
| 21 | + - name: Mint App Token | |
| 22 | + id: app | |
| 23 | + # Create a GitHub App token | |
| 24 | + # https://github.com/marketplace/actions/create-github-app-token | |
| 25 | + uses: actions/create-github-app-token@v2 | |
| 26 | + with: | |
| 27 | + app-id: ${{ vars.ST_BOT_APP_ID }} | |
| 28 | + private-key: ${{ secrets.ST_BOT_PRIVATE_KEY }} | |
| 29 | + owner: ${{ github.repository_owner }} | |
| 30 | + | |
| 20 | 31 | - name: Check Merge Conflicts |
| 21 | 32 | # Label Conflicting Pull Requests |
| 22 | 33 | # https://github.com/marketplace/actions/label-conflicting-pull-requests |
| 23 | 34 | uses: eps1lon/actions-label-merge-conflict@v3.0.3 |
| 24 | 35 | with: |
| 25 | 36 | dirtyLabel: '🚫 Merge Conflicts' |
| 26 | 37 | repoToken: ${{ secretssteps.GITHUB_TOKENapp.outputs.token }} |
| 27 | 38 | commentOnDirty: > |
| 28 | 39 | ⚠️ This PR has conflicts that need to be resolved before it can be merged. |
| @@ -55,3 +55,4 @@ public/scripts/extensions/third-party | ||
| 55 | 55 | .env |
| 56 | 56 | /StartDev.bat |
| 57 | 57 | yarn.lock |
| 58 | +*.code-workspace | |
| 58 | \ No newline at end of file | |
| @@ -9,7 +9,7 @@ | ||
| 9 | 9 | ## Getting the code ready |
| 10 | 10 | |
| 11 | 11 | 1. Register a GitHub account. |
| 12 | 12 | 2. Fork this repository under your account. |
| 13 | 13 | 3. Clone the fork onto your machine. |
| 14 | 14 | 4. Open the cloned repository in the code editor. |
| 15 | 15 | 5. Create a git branch (recommended). |
| @@ -29,11 +29,22 @@ | ||
| 29 | 29 | - Updating GitHub Actions. |
| 30 | 30 | - Hotfixing a critical bug. |
| 31 | 31 | 4. Project maintainers will test and can change your code before merging. |
| 32 | -5. Write at least somewhat meaningful PR descriptions. There's no "right" way to do it, but the following may help with outlining a general structure: | |
| 32 | +5. To make sure that your contribution remains testable and reviewable, try not to exceed a soft limit of **200 lines of code** (both additions and deletions) per pull request. If you have more to contribute, split it into multiple pull requests. We can also consider creating a separate feature branch for more substantial changes, but please discuss it with the maintainers first. | |
| 33 | +6. Write at least somewhat meaningful PR descriptions and commit messages. There's no "right" way to do it, but the following may help with outlining a general structure: | |
| 33 | 34 | - What is the reason for a change? |
| 34 | 35 | - What did you do to achieve this? |
| 35 | 36 | - How would a reviewer test the change? |
| 36 | -6. Mind the license. Your contributions will be licensed under the GNU Affero General Public License. If you don't know what that implies, consult your lawyer. | |
| 37 | +7. English is the primary language of communication in this project. Please use only English when writing commit messages, PR descriptions, comments and other text. This does not apply to contributions to localization files. | |
| 38 | +8. Mind the license. Your contributions will be licensed under the GNU Affero General Public License. If you don't know what that implies, consult your lawyer. | |
| 39 | + | |
| 40 | +## Use of AI coding assistance tools ("Vibe Coding") | |
| 41 | + | |
| 42 | +We do not prohibit nor encourage the use of AI tools for coding assistance to help you write code, documentation, etc. This includes specialized IDEs, plugins and add-ons, chat interfaces, etc. However, please keep in mind the following: | |
| 43 | + | |
| 44 | +- No matter who (or what) wrote the code, you are responsible for it. Make sure to carefully review and test everything before committing, and be ready to discuss and fix any issues that may arise during the review. | |
| 45 | +- Maintainers can reject reviewing and accepting PRs of very low quality, i.e. if the time to fix the issues exceeds the time to write the code from scratch. | |
| 46 | +- Avoid common mistakes attributed to AI tools, such as: adding/removing unrelated comments, excessive logging, unawareness of the project context and conventions, etc. | |
| 47 | +- You are allowed, but not required, to trigger AI tools that are added to the project by maintainers (Gemini, Copilot, Codex). Keep in mind that any feedback (comments, suggestions) that these tools generate is not a call to action; make sure to properly assess it before applying. | |
| 37 | 48 | |
| 38 | 49 | ## Further reading |
| 39 | 50 | |
| @@ -82,18 +82,30 @@ requestProxy: | ||
| 82 | 82 | enableUserAccounts: false |
| 83 | 83 | # Enable discreet login mode: hides user list on the login screen |
| 84 | 84 | enableDiscreetLogin: false |
| 85 | -# Enable's authlia based auto login. Only enable this if you | |
| 86 | -# have setup and installed Authelia as a middle-ware on your | |
| 87 | -# reverse proxy | |
| 88 | -# https://www.authelia.com/ | |
| 89 | -# This will use auto login to an account with the same username | |
| 90 | -# as that used for authlia. (Ensure the username in authlia | |
| 91 | -# is an exact match in lowercase with that in sillytavern) | |
| 92 | -autheliaAuth: false | |
| 93 | 85 | # If `basicAuthMode` and this are enabled then |
| 94 | 86 | # the username and passwords for basic auth are the same as those |
| 95 | 87 | # for the individual accounts |
| 96 | 88 | perUserBasicAuth: false |
| 89 | + | |
| 90 | +# -- SSO LOGIN CONFIGURATION -- | |
| 91 | +sso: | |
| 92 | + # Enable's authlia based auto login. Only enable this if you | |
| 93 | + # have setup and installed Authelia as a middle-ware on your | |
| 94 | + # reverse proxy | |
| 95 | + # https://www.authelia.com/ | |
| 96 | + # This will use auto login to an account with the same username | |
| 97 | + # as that used for authlia. (Ensure the username in authlia | |
| 98 | + # is an exact match in lowercase with that in sillytavern) | |
| 99 | + autheliaAuth: false | |
| 100 | + # Enable's authentik based auto login. Only enable this if you | |
| 101 | + # have setup and installed Authentik as a middle-ware on your | |
| 102 | + # reverse proxy. | |
| 103 | + # https://goauthentik.io/ | |
| 104 | + # This will use auto login to an account with the same username | |
| 105 | + # as that used for authentik. (Ensure the username in authentik | |
| 106 | + # is an exact match in lowercase with that in sillytavern). | |
| 107 | + authentikAuth: false | |
| 108 | + | |
| 97 | 109 | # Host whitelist configuration. Recommended if you're using a listen mode |
| 98 | 110 | hostWhitelist: |
| 99 | 111 | # Enable or disable host whitelisting |
| @@ -1,20 +1,23 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "chat_completion_source": "openai", |
| 3 | 3 | "openai_model": "gpt-4-turbo", |
| 4 | 4 | "claude_model": "claude-3-5-sonnet-202406204-5", |
| 5 | 5 | "openrouter_model": "OR_Website", |
| 6 | 6 | "openrouter_use_fallback": false, |
| 7 | 7 | "openrouter_group_models": false, |
| 8 | 8 | "openrouter_sort_models": "alphabetically", |
| 9 | 9 | "ai21_model": "jamba-large", |
| 10 | 10 | "mistralai_model": "mistral-large-latest", |
| 11 | + "electronhub_model": "gpt-4o-mini", | |
| 12 | + "electronhub_sort_models": "alphabetically", | |
| 13 | + "electronhub_group_models": false, | |
| 11 | 14 | "custom_model": "", |
| 12 | 15 | "custom_url": "", |
| 13 | 16 | "custom_include_body": "", |
| 14 | 17 | "custom_exclude_body": "", |
| 15 | 18 | "custom_include_headers": "", |
| 16 | 19 | "google_model": "gemini-2.5-pro", |
| 17 | 20 | "vertexai_model": "gemini-2.0-flash5-001pro", |
| 18 | 21 | "temperature": 1, |
| 19 | 22 | "frequency_penalty": 0, |
| 20 | 23 | "presence_penalty": 0, |
| @@ -620,7 +620,7 @@ | ||
| 620 | 620 | }, |
| 621 | 621 | "wi_format": "{0}", |
| 622 | 622 | "openai_model": "gpt-4-turbo", |
| 623 | 623 | "claude_model": "claude-3-5-sonnet-202406204-5", |
| 624 | 624 | "ai21_model": "jamba-large", |
| 625 | 625 | "openrouter_model": "OR_Website", |
| 626 | 626 | "reverse_proxy": "", |
| @@ -1,12 +1,12 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "name": "sillytavern", |
| 3 | 3 | "version": "1.13.45", |
| 4 | 4 | "lockfileVersion": 3, |
| 5 | 5 | "requires": true, |
| 6 | 6 | "packages": { |
| 7 | 7 | "": { |
| 8 | 8 | "name": "sillytavern", |
| 9 | 9 | "version": "1.13.45", |
| 10 | 10 | "hasInstallScript": true, |
| 11 | 11 | "license": "AGPL-3.0", |
| 12 | 12 | "dependencies": { |
| @@ -113,7 +113,7 @@ | ||
| 113 | 113 | "type": "git", |
| 114 | 114 | "url": "https://github.com/SillyTavern/SillyTavern.git" |
| 115 | 115 | }, |
| 116 | 116 | "version": "1.13.45", |
| 117 | 117 | "scripts": { |
| 118 | 118 | "start": "node server.js", |
| 119 | 119 | "debug": "node --inspect server.js", |
| @@ -1,6 +1,5 @@ | ||
| 1 | 1 | /* Main Page Backgrounds */ |
| 2 | 2 | #bg1, { |
| 3 | -#bg_custom { | |
| 4 | 3 | background-repeat: no-repeat; |
| 5 | 4 | background-attachment: fixed; |
| 6 | 5 | background-size: cover; |
| @@ -8,72 +7,46 @@ | ||
| 8 | 7 | width: 100%; |
| 9 | 8 | height: 100%; |
| 10 | 9 | transition: background-image var(--animation-duration-3x) ease-in-out; |
| 10 | + z-index: -1; | |
| 11 | 11 | } |
| 12 | 12 | |
| 13 | 13 | /* Fitting options */ |
| 14 | 14 | #background_fitting { |
| 15 | 15 | max-width: 6em8em; |
| 16 | 16 | } |
| 17 | 17 | |
| 18 | 18 | /* Fill/Cover - scales to fill width while maintaining aspect ratio */ |
| 19 | 19 | #bg1.cover, { |
| 20 | -#bg_custom.cover { | |
| 21 | 20 | background-size: cover; |
| 22 | 21 | background-position: center; |
| 23 | 22 | } |
| 24 | 23 | |
| 25 | 24 | /* Fit/Contain - shows entire image maintaining aspect ratio */ |
| 26 | 25 | #bg1.contain, { |
| 27 | -#bg_custom.contain { | |
| 28 | 26 | background-size: contain; |
| 29 | 27 | background-position: center; |
| 30 | 28 | background-repeat: no-repeat; |
| 31 | 29 | } |
| 32 | 30 | |
| 33 | 31 | /* Stretch - stretches to fill entire space */ |
| 34 | 32 | #bg1.stretch, { |
| 35 | -#bg_custom.stretch { | |
| 36 | 33 | background-size: 100% 100%; |
| 37 | 34 | } |
| 38 | 35 | |
| 39 | 36 | /* Center - centers without scaling */ |
| 40 | 37 | #bg1.center, { |
| 41 | -#bg_custom.center { | |
| 42 | 38 | background-size: auto; |
| 43 | 39 | background-position: center; |
| 44 | 40 | background-repeat: no-repeat; |
| 45 | 41 | } |
| 46 | 42 | |
| 47 | -body.reduced-motion #bg1, | |
| 48 | -body.reduced-motion #bg_custom { | |
| 49 | - transition: none; | |
| 50 | -} | |
| 51 | - | |
| 52 | -#bg1 { | |
| 53 | - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='); | |
| 54 | - z-index: -3; | |
| 55 | -} | |
| 56 | - | |
| 57 | -#bg_custom { | |
| 58 | - background-image: none; | |
| 59 | - z-index: -2; | |
| 60 | -} | |
| 61 | - | |
| 62 | -.bg_example.flex-container.locked:not(:focus-visible) { | |
| 63 | - outline-color: var(--golden); | |
| 64 | -} | |
| 65 | - | |
| 66 | 43 | /* This is the main flex container for the entire drawer */ |
| 67 | 44 | #Backgrounds.drawer-content.openDrawer.bg-drawer-layout { |
| 68 | 45 | display: flex; |
| 46 | +} | |
| 47 | + | |
| 48 | +.bg-drawer-layout { | |
| 69 | 49 | flex-direction: column; |
| 70 | - height: calc(100vh - var(--topBarBlockSize)); | |
| 71 | - max-height: calc(100vh - var(--topBarBlockSize)); | |
| 72 | - height: calc(100dvh - var(--topBarBlockSize)); | |
| 73 | - max-height: calc(100dvh - var(--topBarBlockSize)); | |
| 74 | - overflow: hidden; | |
| 75 | - width: var(--sheldWidth); | |
| 76 | - max-width: var(--sheldWidth); | |
| 77 | 50 | padding: 0; |
| 78 | 51 | } |
| 79 | 52 | |
| @@ -82,11 +55,31 @@ body.reduced-motion #bg_custom { | ||
| 82 | 55 | padding: 5px; |
| 83 | 56 | background-color: var(--SmartThemeBlurTintColor); |
| 84 | 57 | border-bottom: 1px solid var(--SmartThemeBorderColor); |
| 58 | + width: 100%; | |
| 85 | 59 | } |
| 86 | 60 | |
| 87 | -#bg-header-fixed>.flex-container { | |
| 61 | +.bg-header-row-1, | |
| 62 | +.bg-header-row-2 { | |
| 63 | + display: flex; | |
| 64 | + gap: 5px; | |
| 65 | + width: 100%; | |
| 66 | +} | |
| 67 | + | |
| 68 | +/* Control buttons in header */ | |
| 69 | +.heading-container-with-controls { | |
| 70 | + position: relative; | |
| 71 | +} | |
| 72 | + | |
| 73 | +.heading-container-with-controls .heading-text { | |
| 74 | + margin: 10px 0; | |
| 75 | +} | |
| 76 | + | |
| 77 | +.heading-container-with-controls .heading-controls { | |
| 78 | + position: absolute; | |
| 79 | + right: 5px; | |
| 80 | + top: 50%; | |
| 81 | + transform: translateY(-50%); | |
| 88 | 82 | display: flex; |
| 89 | - align-items: center; | |
| 90 | 83 | gap: 5px; |
| 91 | 84 | } |
| 92 | 85 | |
| @@ -94,49 +87,92 @@ body.reduced-motion #bg_custom { | ||
| 94 | 87 | flex-grow: 1; |
| 95 | 88 | overflow-y: auto; |
| 96 | 89 | overflow-x: hidden; |
| 97 | 90 | padding: 0 5px 5px15px; |
| 91 | + position: relative; | |
| 92 | +} | |
| 93 | + | |
| 94 | +#bg_menu_content, | |
| 95 | +#bg_custom_content { | |
| 96 | + display: grid; | |
| 97 | + gap: 5px; | |
| 98 | + width: 100%; | |
| 99 | + grid-template-columns: repeat(var(--bg-thumb-columns, 5), 1fr); | |
| 98 | 100 | } |
| 99 | 101 | |
| 100 | 102 | #bg-filter { |
| 101 | 103 | font-size: calc(var(--mainFontSize) * 0.95); |
| 102 | 104 | } |
| 103 | 105 | |
| 104 | 106 | /* Thumbnail Menu & ButtonsThumbnails */ |
| 107 | +.bg_example:hover .BGSampleTitle { | |
| 108 | + opacity: 1; | |
| 109 | +} | |
| 110 | + | |
| 105 | 111 | .bg_example .mobile-only-menu-toggle { |
| 106 | 112 | display: none; |
| 107 | 113 | } |
| 108 | 114 | |
| 109 | 115 | .bg_example.flex-container { |
| 110 | - width: 30%; | |
| 111 | - max-width: 200px; | |
| 112 | - margin: 5px; | |
| 113 | - aspect-ratio: 16/9; | |
| 114 | 116 | cursor: pointer; |
| 115 | 117 | box-shadow: 0 0 7px var(--black50a); |
| 116 | - | |
| 117 | 118 | position: relative; |
| 118 | 119 | overflow: hidden; |
| 119 | 120 | border-radius: 8px; |
| 120 | 121 | border: 0px solid transparent; |
| 121 | 122 | outline: 2px solid var(--SmartThemeBorderColor); |
| 122 | 123 | outline-offset: -1px; |
| 124 | + | |
| 125 | + height: auto; | |
| 126 | + aspect-ratio: 16 / 9; | |
| 123 | 127 | } |
| 124 | 128 | |
| 125 | 129 | .bg_example.flex-container:focus-visible { |
| 126 | 130 | outline-offset: inherit; |
| 131 | + outline-color: var(--interactable-outline-color); | |
| 132 | +} | |
| 133 | + | |
| 134 | +.bg_example.locked-background { | |
| 135 | + outline: 2px solid var(--golden); | |
| 136 | + outline-offset: 0; | |
| 127 | 137 | } |
| 128 | 138 | |
| 129 | -.bg_example_img { | |
| 139 | +.bg_example.locked-background::after { | |
| 140 | + content: '\f023'; | |
| 141 | + font-family: 'Font Awesome 6 Free'; | |
| 142 | + font-weight: 900; | |
| 143 | + | |
| 130 | 144 | position: absolute; |
| 131 | 145 | topbottom: -2px5px; |
| 132 | 146 | leftright: -2px5px; |
| 133 | 147 | rightz-index: -2px4; |
| 134 | 148 | bottomcolor: var(-2px-golden); |
| 149 | + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.8)); | |
| 150 | + font-size: calc(var(--mainFontSize) * 0.8); | |
| 151 | + pointer-events: none; | |
| 152 | +} | |
| 135 | 153 | |
| 136 | - background-image: inherit; | |
| 154 | +.bg_example:not(.locked-background) .jg-unlock, | |
| 155 | +.bg_example.locked-background .jg-lock { | |
| 156 | + display: none; | |
| 157 | +} | |
| 137 | 158 | |
| 138 | - background-size: cover; | |
| 159 | +.bg_example.selected-background { | |
| 139 | - background-position: center; | |
| 160 | + outline: 2px solid white; | |
| 161 | + outline-offset: 0; | |
| 162 | +} | |
| 163 | + | |
| 164 | +.bg_example.selected-background::before { | |
| 165 | + content: '\f00c'; | |
| 166 | + font-family: 'Font Awesome 6 Free'; | |
| 167 | + font-weight: 900; | |
| 168 | + position: absolute; | |
| 169 | + top: 5px; | |
| 170 | + left: 5px; | |
| 171 | + z-index: 4; | |
| 172 | + color: var(--white100); | |
| 173 | + filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.8)); | |
| 174 | + font-size: calc(var(--mainFontSize) * 0.9); | |
| 175 | + pointer-events: none; | |
| 140 | 176 | } |
| 141 | 177 | |
| 142 | 178 | .bg_example .jg-menu { |
| @@ -145,9 +181,8 @@ body.reduced-motion #bg_custom { | ||
| 145 | 181 | top: 2px; |
| 146 | 182 | right: 2px; |
| 147 | 183 | background-color: rgba(0, 0, 0, 0.5); |
| 148 | 184 | border-radius: 8px5px; |
| 149 | 185 | gappadding: 3px 3px; |
| 150 | - padding: 3px 5px; | |
| 151 | 186 | z-index: 3; |
| 152 | 187 | backdrop-filter: blur(4px); |
| 153 | 188 | border: 1px solid var(--SmartThemeBorderColor); |
| @@ -170,14 +205,14 @@ body.reduced-motion #bg_custom { | ||
| 170 | 205 | |
| 171 | 206 | .bg_example .jg-button { |
| 172 | 207 | display: flex; |
| 173 | 208 | width: 30px24px; |
| 174 | 209 | height: 30px24px; |
| 175 | 210 | align-items: center; |
| 176 | 211 | justify-content: center; |
| 177 | 212 | color: white; |
| 178 | 213 | padding: 5px; |
| 179 | 214 | font-size: 1.1em; |
| 180 | 215 | border-radius: 6px5px; |
| 181 | 216 | transition: background-color var(--animation-duration) ease; |
| 182 | 217 | } |
| 183 | 218 | |
| @@ -185,16 +220,54 @@ body.reduced-motion #bg_custom { | ||
| 185 | 220 | background-color: rgba(255, 255, 255, 0.2); |
| 186 | 221 | } |
| 187 | 222 | |
| 188 | -.bg_example .jg-unlock { | |
| 223 | +/* Scroll-to-Top Button */ | |
| 189 | - display: none; | |
| 224 | +#bg-scroll-top { | |
| 225 | + position: absolute; | |
| 226 | + bottom: 20px; | |
| 227 | + right: 20px; | |
| 228 | + width: 42px; | |
| 229 | + height: 42px; | |
| 230 | + background: var(--SmartThemeBlurTintColor); | |
| 231 | + color: var(--SmartThemeBodyColor); | |
| 232 | + border: 1px solid var(--SmartThemeBorderColor); | |
| 233 | + border-radius: 50%; | |
| 234 | + cursor: pointer; | |
| 235 | + z-index: 10; | |
| 236 | + font-size: 18px; | |
| 237 | + display: inline-flex; | |
| 238 | + align-items: center; | |
| 239 | + justify-content: center; | |
| 240 | + opacity: 0; | |
| 241 | + transition: opacity var(--animation-duration) ease; | |
| 242 | + pointer-events: none; | |
| 190 | 243 | } |
| 191 | 244 | |
| 192 | -.bg_example.locked .jg-lock { | |
| 245 | +#bg-scroll-top.visible { | |
| 193 | 246 | displayopacity: none1; |
| 247 | + pointer-events: auto; | |
| 194 | 248 | } |
| 195 | 249 | |
| 196 | -.bg_example.locked .jg-unlock { | |
| 250 | +#bg-scroll-top:hover { | |
| 197 | 251 | displayfilter: flexbrightness(150%); |
| 252 | + outline: 1px solid var(--interactable-outline-color); | |
| 253 | +} | |
| 254 | + | |
| 255 | +#bg-scroll-top .fa-solid { | |
| 256 | + margin: 0; | |
| 257 | + padding: 0; | |
| 258 | + line-height: 1; | |
| 259 | +} | |
| 260 | + | |
| 261 | +.thumbnail-clipper { | |
| 262 | + position: absolute; | |
| 263 | + top: -2px; | |
| 264 | + left: -2px; | |
| 265 | + right: -2px; | |
| 266 | + bottom: -2px; | |
| 267 | + overflow: hidden; | |
| 268 | + border-radius: inherit; | |
| 269 | + background-size: cover; | |
| 270 | + background-position: center; | |
| 198 | 271 | } |
| 199 | 272 | |
| 200 | 273 | .bg_example:not([custom="true"]) .jg-copy, |
| @@ -10,6 +10,18 @@ | ||
| 10 | 10 | flex-basis: 100%; |
| 11 | 11 | } |
| 12 | 12 | |
| 13 | + #rm_button_panel_pin_div, | |
| 14 | + #lm_button_panel_pin_div { | |
| 15 | + display: none; | |
| 16 | + } | |
| 17 | + | |
| 18 | + #rm_button_characters { | |
| 19 | + font-size: var(--topBarIconSize); | |
| 20 | + } | |
| 21 | + | |
| 22 | + #CharListButtonAndHotSwaps { | |
| 23 | + align-items: center; | |
| 24 | + } | |
| 13 | 25 | |
| 14 | 26 | #send_form.compact #leftSendForm, |
| 15 | 27 | #send_form.compact #rightSendForm { |
| @@ -21,6 +33,15 @@ | ||
| 21 | 33 | display: none; |
| 22 | 34 | } |
| 23 | 35 | |
| 36 | + #bg_menu_content, | |
| 37 | + #bg_custom_content { | |
| 38 | + grid-template-columns: repeat(var(--bg-thumb-columns, 3), 1fr); | |
| 39 | + } | |
| 40 | + | |
| 41 | + .bg_list { | |
| 42 | + width: unset; | |
| 43 | + } | |
| 44 | + | |
| 24 | 45 | .bg_button { |
| 25 | 46 | font-size: 15px; |
| 26 | 47 | } |
| @@ -40,6 +61,11 @@ | ||
| 40 | 61 | z-index: 4; |
| 41 | 62 | } |
| 42 | 63 | |
| 64 | + .bg_example.mobile-menu-open .mobile-only-menu-toggle { | |
| 65 | + opacity: 0; | |
| 66 | + pointer-events: none; | |
| 67 | + } | |
| 68 | + | |
| 43 | 69 | .bg_example .mobile-only-menu-toggle { |
| 44 | 70 | display: flex; |
| 45 | 71 | align-items: center; |
| @@ -57,48 +83,13 @@ | ||
| 57 | 83 | backdrop-filter: blur(2px); |
| 58 | 84 | } |
| 59 | 85 | |
| 60 | - #bg-header-controls { | |
| 86 | + .bg_example .jg-button { | |
| 61 | 87 | flex-wrapwidth: wrap30px; |
| 62 | 88 | row-gapheight: 10px30px; |
| 63 | - } | |
| 64 | - | |
| 65 | - #bg-header-fixed>.flex-container { | |
| 66 | - flex-wrap: wrap; | |
| 67 | - row-gap: 0px; | |
| 68 | - } | |
| 69 | - | |
| 70 | - #Backgrounds:not(.selection-mode-active) #bg-header-fixed>.flex-container::after { | |
| 71 | - content: ''; | |
| 72 | - order: 1; | |
| 73 | - flex-basis: 100%; | |
| 74 | - height: 0; | |
| 75 | - } | |
| 76 | - | |
| 77 | - /* --- Row 1 Item --- */ | |
| 78 | - #bg-header-fixed #bg-header-title { | |
| 79 | - order: 1; | |
| 80 | - flex-grow: 1; | |
| 81 | - } | |
| 82 | - | |
| 83 | - #bg-header-fixed #background_fitting, | |
| 84 | - #bg-header-fixed #auto_background { | |
| 85 | - order: 1; | |
| 86 | - } | |
| 87 | - | |
| 88 | - /* --- Row 2 Item --- */ | |
| 89 | - #bg-header-fixed #bg-filter { | |
| 90 | - order: 2; | |
| 91 | - flex-grow: 1; | |
| 92 | - min-width: 0; | |
| 93 | 89 | } |
| 94 | 90 | |
| 95 | - /* --- Row 3 Item --- */ | |
| 91 | + #background_fitting { | |
| 96 | - #bg-header-fixed #add_background_button_top { | |
| 92 | + max-width: 6em; | |
| 97 | - order: 3; | |
| 98 | - width: 100%; | |
| 99 | - text-align: center; | |
| 100 | - padding-top: 0.5em; | |
| 101 | - padding-bottom: 0.5em; | |
| 102 | 93 | } |
| 103 | 94 | |
| 104 | 95 | #Backgrounds.drawer-content.openDrawer.bg-drawer-layout { |
| @@ -429,10 +420,6 @@ | ||
| 429 | 420 | .horde_multiple_hint { |
| 430 | 421 | display: none; |
| 431 | 422 | } |
| 432 | - | |
| 433 | - .bg_list { | |
| 434 | - width: unset; | |
| 435 | - } | |
| 436 | 423 | } |
| 437 | 424 | |
| 438 | 425 | /*landscape mode phones and ipads*/ |
| @@ -301,6 +301,10 @@ select.keyselect+span.select2-container .select2-selection--multiple { | ||
| 301 | 301 | display: none; |
| 302 | 302 | } |
| 303 | 303 | |
| 304 | +.world_entry:not(:has(select[name="position"] option[value="7"]:checked)) .world_entry_form_control:has(input[name="outletName"]) { | |
| 305 | + display: none; | |
| 306 | +} | |
| 307 | + | |
| 304 | 308 | .world_entry label[for="__invisible"] { |
| 305 | 309 | visibility: hidden; |
| 306 | 310 | pointer-events: none; |
Binary file
Binary file
| @@ -49,7 +49,6 @@ | ||
| 49 | 49 | |
| 50 | 50 | <body class="no-blur"> |
| 51 | 51 | <div id="preloader"></div> |
| 52 | - <div id="bg_custom"></div> | |
| 53 | 52 | <div id="bg1"></div> |
| 54 | 53 | <div id="character_context_menu" class="hidden"> |
| 55 | 54 | <ul> |
| @@ -254,7 +253,7 @@ | ||
| 254 | 253 | </div> |
| 255 | 254 | <div id="common-gen-settings-block" class="width100p"> |
| 256 | 255 | <div id="pro-settings-block" class="flex-container gap10h5v justifyCenter"> |
| 257 | 256 | <div id="amount_gen_block" class="range-block-range-and-counter alignitemscenter flex-container marginBot5 flexFlowColumn flexBasis48p flexGrow flexShrink gap0"> |
| 258 | 257 | <small data-i18n="response legth(tokens)">Response (tokens)</small> |
| 259 | 258 | <input class="neo-range-slider" type="range" id="amount_gen" name="volume" min="16" max="2048" step="1"> |
| 260 | 259 | <div data-randomization-disabled="true" class="wide100p"> |
| @@ -285,7 +284,7 @@ | ||
| 285 | 284 | </label> |
| 286 | 285 | </div> |
| 287 | 286 | </div> |
| 288 | 287 | <div id="max_context_block" class="range-block-range-and-counter alignitemscenter flex-container marginBot5 flexFlowColumn flexBasis48p flexGrow flexShrink gap0"> |
| 289 | 288 | <small data-i18n="context size(tokens)">Context (tokens)</small> |
| 290 | 289 | <input class="neo-range-slider" type="range" id="max_context" name="volume" min="512" max="8192" step="64"> |
| 291 | 290 | <div data-randomization-disabled="true" class="wide100p"> |
| @@ -649,7 +648,7 @@ | ||
| 649 | 648 | Max Response Length (tokens) |
| 650 | 649 | </div> |
| 651 | 650 | <div class="wide100p"> |
| 652 | 651 | <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536" step="1"> |
| 653 | 652 | </div> |
| 654 | 653 | </div> |
| 655 | 654 | <div class="range-block" data-source="openai,custom,xai,aimlapi,moonshot,azure_openai"> |
| @@ -676,6 +675,9 @@ | ||
| 676 | 675 | <div data-source="openrouter"> |
| 677 | 676 | <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="openrouter_max_prompt_cost" data-i18n="Unknown">Unknown</span> |
| 678 | 677 | </div> |
| 678 | + <div data-source="electronhub"> | |
| 679 | + <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="electronhub_max_prompt_cost" data-i18n="Unknown">Unknown</span> | |
| 680 | + </div> | |
| 679 | 681 | <hr> |
| 680 | 682 | <div class="range-block"> |
| 681 | 683 | <label for="stream_toggle" title="Enable OpenAI completion streaming" data-i18n="[title]Enable OpenAI completion streaming" class="checkbox_label widthFreeExpand"> |
| @@ -1463,12 +1465,10 @@ | ||
| 1463 | 1465 | </div> |
| 1464 | 1466 | </div> |
| 1465 | 1467 | <div class="range-block marginTop5" title="Tokens across which sequence matching is not continued. Specified as a comma-separated list of quoted strings." data-i18n="[title]DRY_Sequence_Breakers_desc"> |
| 1466 | 1468 | <div class="range-block-title textAlignCenter flex-container justifyCenter alignitemscenter"> |
| 1467 | 1469 | <small data-i18n="Sequence Breakers">Sequence Breakers</small> |
| 1468 | 1470 | </div> |
| 1469 | - <div class="wide100p"> | |
| 1471 | + <textarea id="dry_sequence_breakers_textgenerationwebui" class="text_pole textarea_compact" name="sequence_breakers" rows="3" data-i18n="[placeholder]JSON-serialized array of strings." placeholder="JSON-serialized array of strings."></textarea> | |
| 1470 | - <textarea id="dry_sequence_breakers_textgenerationwebui" class="text_pole textarea_compact" name="sequence_breakers" rows="3" data-i18n="[placeholder]JSON-serialized array of strings." placeholder="JSON-serialized array of strings."></textarea> | |
| 1471 | - </div> | |
| 1472 | 1472 | </div> |
| 1473 | 1473 | </div> |
| 1474 | 1474 | <div data-tg-type="ooba, mancer, koboldcpp, tabby, llamacpp, aphrodite" id="dynatemp_block_ooba" class="wide100p"> |
| @@ -1502,7 +1502,7 @@ | ||
| 1502 | 1502 | <div data-tg-type="ooba,infermaticai,koboldcpp,llamacpp,tabby" id="mirostat_block_ooba" class="wide100p"> |
| 1503 | 1503 | <h4 class="wide100p textAlignCenter"> |
| 1504 | 1504 | <label data-i18n="Mirostat (mode=1 is only for llama.cpp)">Mirostat</label> |
| 1505 | 1505 | <div class=" fa-solid fa-circle-info opacity50p " data-i18n="[title]Mirostat_desc" title="Mirostat is a thermostat for output perplexity. Mirostat matches the output perplexity to that of the input, thus avoiding the repetition trap (where, as the autoregressive inference produces text, the perplexity of the output tends toward zero) and the confusion trap (where the perplexity diverges). For details, see the paper Mirostat: A Neural Text Decoding Algorithm that Directly Controls Perplexity by Basu et al. (2020). Mode chooses the Mirostat version. 0=disable, 1=Mirostat 1.0 (llama.cpp only), 2=Mirostat 2.0. Tau = Variability parameter for Mirostat outputs. Eta = Learning rate of Mirostat."></div> |
| 1506 | 1506 | </h4> |
| 1507 | 1507 | <div class="flex-container flexFlowRow gap10px flexShrink"> |
| 1508 | 1508 | <div class="alignitemscenter flex-container marginBot5 flexFlowColumn flexGrow flexShrink gap0"> |
| @@ -1511,18 +1511,12 @@ | ||
| 1511 | 1511 | <input class="neo-range-input" type="number" min="0" max="2" step="1" data-for="mirostat_mode_textgenerationwebui" id="mirostat_mode_counter_textgenerationwebui"> |
| 1512 | 1512 | </div> |
| 1513 | 1513 | <div class="alignitemscenter flex-container marginBot5 flexFlowColumn flexGrow flexShrink gap0"> |
| 1514 | - <label> | |
| 1514 | + <small data-i18n="Mirostat Tau">Tau</small> | |
| 1515 | - <small data-i18n="Mirostat Tau">Tau</small> | |
| 1516 | - <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Variability parameter for Mirostat outputs" title="Variability parameter for Mirostat outputs."></div> | |
| 1517 | - </label> | |
| 1518 | 1515 | <input class="neo-range-slider" type="range" id="mirostat_tau_textgenerationwebui" name="volume" min="0" max="20" step="0.01" /> |
| 1519 | 1516 | <input class="neo-range-input" type="number" min="0" max="20" step="0.01" data-for="mirostat_tau_textgenerationwebui" id="mirostat_tau_counter_textgenerationwebui"> |
| 1520 | 1517 | </div> |
| 1521 | 1518 | <div class="alignitemscenter flex-container marginBot5 flexFlowColumn flexGrow flexShrink gap0"> |
| 1522 | - <label> | |
| 1519 | + <small data-i18n="Mirostat Eta">Eta</small> | |
| 1523 | - <small data-i18n="Mirostat Eta">Eta</small> | |
| 1524 | - <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Learning rate of Mirostat" title="Learning rate of Mirostat."></div> | |
| 1525 | - </label> | |
| 1526 | 1520 | <input class="neo-range-slider" type="range" id="mirostat_eta_textgenerationwebui" name="volume" min="0" max="1" step="0.01" /> |
| 1527 | 1521 | <input class="neo-range-input" type="number" min="0" max="1" step="0.01" data-for="mirostat_eta_textgenerationwebui" id="mirostat_eta_counter_textgenerationwebui"> |
| 1528 | 1522 | </div> |
| @@ -1583,17 +1577,13 @@ | ||
| 1583 | 1577 | </label> |
| 1584 | 1578 | <label data-tg-type="ooba, tabby" class="checkbox_label flexGrow flexShrink" for="add_bos_token_textgenerationwebui"> |
| 1585 | 1579 | <input type="checkbox" id="add_bos_token_textgenerationwebui" /> |
| 1586 | - <label> | |
| 1580 | + <small data-i18n="Add BOS Token">Add BOS Token</small> | |
| 1587 | - <small data-i18n="Add BOS Token">Add BOS Token</small> | |
| 1581 | + <div class="fa-solid fa-circle-info opacity50p " data-i18n="[title]Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative" title="Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative."></div> | |
| 1588 | - <div class="fa-solid fa-circle-info opacity50p " data-i18n="[title]Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative" title="Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative."></div> | |
| 1589 | - </label> | |
| 1590 | 1582 | </label> |
| 1591 | 1583 | <label data-tg-type="ooba, llamacpp, tabby, koboldcpp, dreamgen" class="checkbox_label flexGrow flexShrink" for="ban_eos_token_textgenerationwebui"> |
| 1592 | 1584 | <input type="checkbox" id="ban_eos_token_textgenerationwebui" /> |
| 1593 | - <label> | |
| 1585 | + <small data-i18n="Ban EOS Token">Ban EOS Token</small> | |
| 1594 | - <small data-i18n="Ban EOS Token">Ban EOS Token</small> | |
| 1586 | + <div class="fa-solid fa-circle-info opacity50p " data-i18n="[title]Ban the eos_token. This forces the model to never end the generation prematurely" title="Ban the eos_token. This forces the model to never end the generation prematurely."></div> | |
| 1595 | - <div class="fa-solid fa-circle-info opacity50p " data-i18n="[title]Ban the eos_token. This forces the model to never end the generation prematurely" title="Ban the eos_token. This forces the model to never end the generation prematurely."></div> | |
| 1596 | - </label> | |
| 1597 | 1587 | </label> |
| 1598 | 1588 | <label data-tg-type="vllm, aphrodite, infermaticai" class="checkbox_label" for="ignore_eos_token_textgenerationwebui"> |
| 1599 | 1589 | <input type="checkbox" id="ignore_eos_token_textgenerationwebui" /> |
| @@ -1610,19 +1600,14 @@ | ||
| 1610 | 1600 | </label> |
| 1611 | 1601 | <label data-tg-type="ooba, aphrodite, tabby" class="checkbox_label flexGrow flexShrink" for="temperature_last_textgenerationwebui"> |
| 1612 | 1602 | <input type="checkbox" id="temperature_last_textgenerationwebui" /> |
| 1613 | - <label> | |
| 1603 | + <small data-i18n="Temperature Last">Temperature Last</small> | |
| 1614 | - <small data-i18n="Temperature Last">Temperature Last</small> | |
| 1604 | + <div class="fa-solid fa-circle-info opacity50p " data-i18n="[title]Temperature_Last_desc" title="Use the temperature sampler last. This is almost always the sensible thing to do. When enabled: sample the set of plausible tokens first, then apply temperature to adjust their relative probabilities (technically, logits). When disabled: apply temperature to adjust the relative probabilities of ALL tokens first, then sample plausible tokens from that. Disabling Temperature Last boosts the probabilities in the tail of the distribution, which tends to amplify the chances of getting an incoherent response."></div> | |
| 1615 | - <div class="fa-solid fa-circle-info opacity50p " data-i18n="[title]Temperature_Last_desc" title="Use the temperature sampler last. This is almost always the sensible thing to do. When enabled: sample the set of plausible tokens first, then apply temperature to adjust their relative probabilities (technically, logits). When disabled: apply temperature to adjust the relative probabilities of ALL tokens first, then sample plausible tokens from that. Disabling Temperature Last boosts the probabilities in the tail of the distribution, which tends to amplify the chances of getting an incoherent response."></div> | |
| 1616 | - </label> | |
| 1617 | 1605 | </label> |
| 1618 | 1606 | <label data-tg-type="tabby" class="checkbox_label flexGrow flexShrink" for="speculative_ngram_textgenerationwebui"> |
| 1619 | 1607 | <input type="checkbox" id="speculative_ngram_textgenerationwebui" /> |
| 1620 | - <label> | |
| 1608 | + <small data-i18n="Speculative Ngram">Speculative Ngram</small> | |
| 1621 | - <small data-i18n="Speculative Ngram">Speculative Ngram</small> | |
| 1609 | + <div class="fa-solid fa-circle-info opacity50p " data-i18n="[title]Use a different speculative decoding method without a draft model" title="Use a different speculative decoding method without a draft model. Using a draft model is preferred. Speculative ngram is not as effective."></div> | |
| 1622 | - <div class="fa-solid fa-circle-info opacity50p " data-i18n="[title]Use a different speculative decoding method without a draft model" title="Use a different speculative decoding method without a draft model. Using a draft model is preferred. Speculative ngram is not as effective."></div> | |
| 1623 | - </label> | |
| 1624 | 1610 | </label> |
| 1625 | - | |
| 1626 | 1611 | <label data-tg-type="vllm, aphrodite, infermaticai" class="checkbox_label" for="spaces_between_special_tokens_textgenerationwebui"> |
| 1627 | 1612 | <input type="checkbox" id="spaces_between_special_tokens_textgenerationwebui" /> |
| 1628 | 1613 | <small data-i18n="Spaces Between Special Tokens">Spaces Between Special Tokens</small> |
| @@ -1663,9 +1648,11 @@ | ||
| 1663 | 1648 | </div> |
| 1664 | 1649 | </div> |
| 1665 | 1650 | </div> |
| 1666 | 1651 | <div class="range-block wide100p" id="logit_bias_block_ooba"> |
| 1667 | 1652 | <div id="logit_bias_textgenerationwebui" class="range-block-title title_restorable"> |
| 1668 | - <strong data-i18n="Logit Bias">Logit Bias</strong> | |
| 1653 | + <h4> | |
| 1654 | + <span data-i18n="Logit Bias">Logit Bias</span> | |
| 1655 | + </h4> | |
| 1669 | 1656 | <div id="textgen_logit_bias_new_entry" class="menu_button menu_button_icon"> |
| 1670 | 1657 | <i class="fa-xs fa-solid fa-plus"></i> |
| 1671 | 1658 | <small data-i18n="Add">Add</small> |
| @@ -1678,7 +1665,7 @@ | ||
| 1678 | 1665 | <div class="logit_bias_list"></div> |
| 1679 | 1666 | </div> |
| 1680 | 1667 | </div> |
| 1681 | 1668 | <div id="cfg_block_ooba" data-tg-type="ooba, tabby" class="flex-container flexFlowColumn wide100p"> |
| 1682 | 1669 | <hr class="width100p"> |
| 1683 | 1670 | <h4 class="textAlignCenter"> |
| 1684 | 1671 | <span data-i18n="CFG">CFG</span> |
| @@ -1690,29 +1677,23 @@ | ||
| 1690 | 1677 | <input class="neo-range-input" type="number" min="0.1" max="4" step="0.05" data-for="guidance_scale_textgenerationwebui" id="guidance_scale_counter_textgenerationwebui"> |
| 1691 | 1678 | </div> |
| 1692 | 1679 | <div class="range-block"> |
| 1693 | 1680 | <div class="range-blockalignitemscenter justifyCenter flex-titlecontainer justifyLeftflexShrink"> |
| 1694 | 1681 | <spansmall data-i18n="Negative Prompt">Negative Prompt</spansmall> |
| 1695 | - <small> | |
| 1682 | + <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Used if CFG Scale is unset globally, per chat or character" title="Used if CFG Scale is unset globally, per chat or character"></div> | |
| 1696 | - <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Used if CFG Scale is unset globally, per chat or character" title="Used if CFG Scale is unset globally, per chat or character"></div> | |
| 1697 | - </small> | |
| 1698 | - </div> | |
| 1699 | - <div class="wide100p"> | |
| 1700 | - <textarea id="negative_prompt_textgenerationwebui" class="text_pole textarea_compact" name="negative_prompt" rows="3" data-i18n="[placeholder]Add text here that would make the AI generate things you don't want in your outputs." placeholder="Add text here that would make the AI generate things you don't want in your outputs."></textarea> | |
| 1701 | 1683 | </div> |
| 1684 | + <textarea id="negative_prompt_textgenerationwebui" class="wide100p text_pole textarea_compact" name="negative_prompt" rows="3" data-i18n="[placeholder]Add text here that would make the AI generate things you don't want in your outputs." placeholder="Add text here that would make the AI generate things you don't want in your outputs."></textarea> | |
| 1702 | 1685 | </div> |
| 1703 | 1686 | </div> |
| 1704 | 1687 | <div id="grammar_block_ooba" data-tg-type="ooba,aphrodite,tabby" class="wide100p"> |
| 1705 | 1688 | <hr class="wide100p"> |
| 1706 | 1689 | <h4 class="wide100p textAlignCenter"> |
| 1707 | - <label> | |
| 1690 | + <div class="flex-container justifyCenter alignitemscenter"> | |
| 1708 | 1691 | <span data-i18n="Grammar String">Grammar String</span> |
| 1709 | 1692 | <div class="margin5 fa-solid fa-circle-info opacity50p " data-i18n="[title]GBNF or EBNF, depends on the backend in use. If you're using this you should know which." title="GBNF or EBNF, depends on the backend in use. If you're using this you should know which."></div> |
| 1710 | 1693 | <a href="https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md" target="_blank"> |
| 1711 | - <small> | |
| 1694 | + <div class="fa-solid fa-up-right-from-square note-link-span"></div> | |
| 1712 | - <div class="fa-solid fa-up-right-from-square note-link-span"></div> | |
| 1713 | - </small> | |
| 1714 | 1695 | </a> |
| 1715 | 1696 | </labeldiv> |
| 1716 | 1697 | </h4> |
| 1717 | 1698 | <textarea id="grammar_string_textgenerationwebui" rows="4" class="text_pole textarea_compact monospace" data-i18n="[placeholder]Type in the desired custom grammar" placeholder="Type in the desired custom grammar"></textarea> |
| 1718 | 1699 | </div> |
| @@ -2077,7 +2058,7 @@ | ||
| 2077 | 2058 | </span> |
| 2078 | 2059 | </div> |
| 2079 | 2060 | </div> |
| 2080 | 2061 | <div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub,azure_openai,nanogpt"> |
| 2081 | 2062 | <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand"> |
| 2082 | 2063 | <input id="openai_show_thoughts" type="checkbox" /> |
| 2083 | 2064 | <span data-i18n="Request model reasoning">Request model reasoning</span> |
| @@ -2144,7 +2125,7 @@ | ||
| 2144 | 2125 | </div> |
| 2145 | 2126 | </div> |
| 2146 | 2127 | </div> |
| 2147 | 2128 | <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom,electronhub,azure_openai"> |
| 2148 | 2129 | <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias"> |
| 2149 | 2130 | Logit Bias |
| 2150 | 2131 | </div> |
| @@ -3035,6 +3016,10 @@ | ||
| 3035 | 3016 | <h4 data-i18n="Claude Model">Claude Model</h4> |
| 3036 | 3017 | <select id="model_claude_select"> |
| 3037 | 3018 | <optgroup label="Versions"> |
| 3019 | + <option value="claude-sonnet-4-5">claude-sonnet-4-5</option> | |
| 3020 | + <option value="claude-sonnet-4-5-20250929">claude-sonnet-4-5-20250929</option> | |
| 3021 | + <option value="claude-haiku-4-5">claude-haiku-4-5</option> | |
| 3022 | + <option value="claude-haiku-4-5-20251001">claude-haiku-4-5-20251001</option> | |
| 3038 | 3023 | <option value="claude-opus-4-1">claude-opus-4-1</option> |
| 3039 | 3024 | <option value="claude-opus-4-1-20250805">claude-opus-4-1-20250805</option> |
| 3040 | 3025 | <option value="claude-opus-4-0">claude-opus-4-0</option> |
| @@ -3082,7 +3067,7 @@ | ||
| 3082 | 3067 | <div class="marginTopBot5"> |
| 3083 | 3068 | <div class="inline-drawer wide100p"> |
| 3084 | 3069 | <div class="inline-drawer-toggle inline-drawer-header"> |
| 3085 | 3070 | <b data-i18n="OpenRouter Model OrderSorting">OpenRouter Model Sorting</b> |
| 3086 | 3071 | <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> |
| 3087 | 3072 | </div> |
| 3088 | 3073 | <div class="inline-drawer-content m-b-1"> |
| @@ -3174,46 +3159,32 @@ | ||
| 3174 | 3159 | <option value="gemini-2.5-pro-preview-06-05">gemini-2.5-pro-preview-06-05</option> |
| 3175 | 3160 | <option value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option> |
| 3176 | 3161 | <option value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option> |
| 3177 | - <option value="gemini-2.5-pro-exp-03-25">gemini-2.5-pro-exp-03-25</option> | |
| 3178 | 3162 | <option value="gemini-2.5-flash">gemini-2.5-flash</option> |
| 3163 | + <option value="gemini-2.5-flash-preview-09-2025">gemini-2.5-flash-preview-09-2025</option> | |
| 3179 | 3164 | <option value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option> |
| 3180 | - <option value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option> | |
| 3181 | 3165 | <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option> |
| 3166 | + <option value="gemini-2.5-flash-lite-preview-09-2025">gemini-2.5-flash-lite-preview-09-2025</option> | |
| 3182 | 3167 | <option value="gemini-2.5-flash-lite-preview-06-17">gemini-2.5-flash-lite-preview-06-17</option> |
| 3168 | + <option value="gemini-2.5-flash-image">gemini-2.5-flash-image</option> | |
| 3183 | 3169 | <option value="gemini-2.5-flash-image-preview">gemini-2.5-flash-image-preview</option> |
| 3184 | 3170 | </optgroup> |
| 3185 | 3171 | <optgroup label="Gemini 2.0"> |
| 3186 | 3172 | <option value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05 → 2.5-pro-exp-03-25</option> |
| 3187 | 3173 | <option value="gemini-2.0-pro-exp">gemini-2.0-pro-exp → 2.5-pro-exp-03-25</option> |
| 3188 | 3174 | <option value="gemini-exp-1206">gemini-exp-1206 → 2.5-pro-exp-03-25</option> |
| 3189 | 3175 | <option value="gemini-2.0-flash-001">gemini-2.0-flash-001</option> |
| 3190 | 3176 | <option value="gemini-2.0-flash-exp-image-generation">gemini-2.0-flash-exp-image-generation</option> |
| 3191 | 3177 | <option value="gemini-2.0-flash-preview-image-generation">gemini-2.0-flash-preview-image-generation</option> |
| 3192 | 3178 | <option value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option> |
| 3193 | 3179 | <option value="gemini-2.0-flash">gemini-2.0-flash</option> |
| 3194 | 3180 | <option value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21 → 2.5-flash-preview-0405-1720</option> |
| 3195 | 3181 | <option value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219 → 2.5-flash-preview-0405-1720</option> |
| 3196 | 3182 | <option value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp → 2.5-flash-preview-0405-1720</option> |
| 3197 | 3183 | <option value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option> |
| 3198 | 3184 | <option value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option> |
| 3199 | 3185 | <option value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option> |
| 3200 | 3186 | <option value="gemini-2.0-flash-lite">gemini-2.0-flash-lite</option> |
| 3201 | 3187 | </optgroup> |
| 3202 | - <optgroup label="Gemini 1.5"> | |
| 3203 | - <option value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option> | |
| 3204 | - <option value="gemini-1.5-pro-002">gemini-1.5-pro-002</option> | |
| 3205 | - <option value="gemini-1.5-pro-001">gemini-1.5-pro-001</option> | |
| 3206 | - <option value="gemini-1.5-pro">gemini-1.5-pro</option> | |
| 3207 | - <option value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option> | |
| 3208 | - <option value="gemini-1.5-flash-002">gemini-1.5-flash-002</option> | |
| 3209 | - <option value="gemini-1.5-flash-001">gemini-1.5-flash-001</option> | |
| 3210 | - <option value="gemini-1.5-flash">gemini-1.5-flash</option> | |
| 3211 | - <option value="gemini-1.5-flash-8b-latest">gemini-1.5-flash-8b-latest</option> | |
| 3212 | - <option value="gemini-1.5-flash-8b-001">gemini-1.5-flash-8b-001</option> | |
| 3213 | - <option value="gemini-1.5-flash-8b-exp-0924">gemini-1.5-flash-8b-exp-0924</option> | |
| 3214 | - <option value="gemini-1.5-flash-8b-exp-0827">gemini-1.5-flash-8b-exp-0827</option> | |
| 3215 | - <option value="gemini-1.5-flash-8b">gemini-1.5-flash-8b</option> | |
| 3216 | - </optgroup> | |
| 3217 | 3188 | <optgroup label="Gemma"> |
| 3218 | 3189 | <option value="gemma-3n-e4b-it">gemma-3n-e4b-it</option> |
| 3219 | 3190 | <option value="gemma-3n-e2b-it">gemma-3n-e2b-it</option> |
| @@ -3225,6 +3196,9 @@ | ||
| 3225 | 3196 | <optgroup label="LearnLM"> |
| 3226 | 3197 | <option value="learnlm-2.0-flash-experimental">learnlm-2.0-flash-experimental</option> |
| 3227 | 3198 | </optgroup> |
| 3199 | + <optgroup label="Robotics-ER"> | |
| 3200 | + <option value="gemini-robotics-er-1.5-preview">gemini-robotics-er-1.5-preview</option> | |
| 3201 | + </optgroup> | |
| 3228 | 3202 | <optgroup id="google_other_models" label="Other"></optgroup> |
| 3229 | 3203 | </select> |
| 3230 | 3204 | </div> |
| @@ -3350,15 +3324,9 @@ | ||
| 3350 | 3324 | <!-- data-mode="full" is for models that require a service account --> |
| 3351 | 3325 | <optgroup label="Gemini 2.5"> |
| 3352 | 3326 | <option value="gemini-2.5-pro">gemini-2.5-pro</option> |
| 3353 | - <option value="gemini-2.5-pro-preview-06-05">gemini-2.5-pro-preview-06-05</option> | |
| 3354 | - <option value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option> | |
| 3355 | - <option value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option> | |
| 3356 | - <option value="gemini-2.5-pro-exp-03-25" data-mode="full">gemini-2.5-pro-exp-03-25</option> | |
| 3357 | 3327 | <option value="gemini-2.5-flash">gemini-2.5-flash</option> |
| 3358 | - <option value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option> | |
| 3359 | - <option value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option> | |
| 3360 | 3328 | <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option> |
| 3361 | 3329 | <option value="gemini-2.5-flash-lite-preview-06-17image">gemini-2.5-flash-lite-preview-06-17image</option> |
| 3362 | 3330 | <option value="gemini-2.5-flash-image-preview">gemini-2.5-flash-image-preview</option> |
| 3363 | 3331 | </optgroup> |
| 3364 | 3332 | <optgroup label="Gemini 2.0"> |
| @@ -3383,67 +3351,7 @@ | ||
| 3383 | 3351 | <div> |
| 3384 | 3352 | <h4 data-i18n="MistralAI Model">MistralAI Model</h4> |
| 3385 | 3353 | <select id="model_mistralai_select"> |
| 3386 | - <optgroup label="Latest"> | |
| 3354 | + <option data-i18n="-- Connect to the API --" value="">-- Connect to the API --</option> | |
| 3387 | - <option value="open-mistral-nemo">open-mistral-nemo</option> | |
| 3388 | - <option value="open-mistral-7b">open-mistral-7b</option> | |
| 3389 | - <option value="open-mixtral-8x7b">open-mixtral-8x7b</option> | |
| 3390 | - <option value="open-mixtral-8x22b">open-mixtral-8x22b</option> | |
| 3391 | - <option value="open-codestral-mamba">open-codestral-mamba</option> | |
| 3392 | - <option value="ministral-3b-latest">ministral-3b-latest</option> | |
| 3393 | - <option value="ministral-8b-latest">ministral-8b-latest</option> | |
| 3394 | - <option value="mistral-tiny-latest">mistral-tiny-latest</option> | |
| 3395 | - <option value="mistral-small-latest">mistral-small-latest</option> | |
| 3396 | - <option value="mistral-medium-latest">mistral-medium-latest</option> | |
| 3397 | - <option value="mistral-large-latest">mistral-large-latest</option> | |
| 3398 | - <option value="mistral-saba-latest">mistral-saba-latest</option> | |
| 3399 | - <option value="codestral-latest">codestral-latest</option> | |
| 3400 | - <option value="codestral-mamba-latest">codestral-mamba-latest</option> | |
| 3401 | - <option value="pixtral-12b-latest">pixtral-12b-latest</option> | |
| 3402 | - <option value="pixtral-large-latest">pixtral-large-latest</option> | |
| 3403 | - <option value="devstral-small-latest">devstral-small-latest</option> | |
| 3404 | - <option value="devstral-medium-latest">devstral-medium-latest</option> | |
| 3405 | - <option value="magistral-small-latest">magistral-small-latest</option> | |
| 3406 | - <option value="magistral-medium-latest">magistral-medium-latest</option> | |
| 3407 | - </optgroup> | |
| 3408 | - <optgroup label="Sub-versions"> | |
| 3409 | - <option value="open-mistral-nemo-2407">open-mistral-nemo-2407</option> | |
| 3410 | - <option value="open-mixtral-8x22b-2404">open-mixtral-8x22b-2404</option> | |
| 3411 | - <option value="ministral-3b-2410">ministral-3b-2410</option> | |
| 3412 | - <option value="ministral-8b-2410">ministral-8b-2410</option> | |
| 3413 | - <option value="mistral-tiny-2312">mistral-tiny-2312</option> | |
| 3414 | - <option value="mistral-tiny-2407">mistral-tiny-2407</option> | |
| 3415 | - <option value="mistral-small-2312">mistral-small-2312</option> | |
| 3416 | - <option value="mistral-small-2402">mistral-small-2402</option> | |
| 3417 | - <option value="mistral-small-2409">mistral-small-2409</option> | |
| 3418 | - <option value="mistral-small-2501">mistral-small-2501</option> | |
| 3419 | - <option value="mistral-small-2503">mistral-small-2503</option> | |
| 3420 | - <option value="mistral-small-2506">mistral-small-2506</option> | |
| 3421 | - <option value="mistral-medium-2312">mistral-medium-2312</option> | |
| 3422 | - <option value="mistral-medium-2505">mistral-medium-2505</option> | |
| 3423 | - <option value="mistral-medium-2508">mistral-medium-2508</option> | |
| 3424 | - <option value="mistral-large-2402">mistral-large-2402</option> | |
| 3425 | - <option value="mistral-large-2407">mistral-large-2407</option> | |
| 3426 | - <option value="mistral-large-2411">mistral-large-2411</option> | |
| 3427 | - <option value="mistral-large-pixtral-2411">mistral-large-pixtral-2411</option> | |
| 3428 | - <option value="mistral-saba-2502">mistral-saba-2502</option> | |
| 3429 | - <option value="codestral-2405">codestral-2405</option> | |
| 3430 | - <option value="codestral-2405-blue">codestral-2405-blue</option> | |
| 3431 | - <option value="codestral-mamba-2407">codestral-mamba-2407</option> | |
| 3432 | - <option value="codestral-2411-rc5">codestral-2411-rc5</option> | |
| 3433 | - <option value="codestral-2412">codestral-2412</option> | |
| 3434 | - <option value="codestral-2501">codestral-2501</option> | |
| 3435 | - <option value="codestral-2508">codestral-2508</option> | |
| 3436 | - <option value="pixtral-12b-2409">pixtral-12b-2409</option> | |
| 3437 | - <option value="pixtral-large-2411">pixtral-large-2411</option> | |
| 3438 | - <option value="devstral-small-2505">devstral-small-2505</option> | |
| 3439 | - <option value="devstral-small-2507">devstral-small-2507</option> | |
| 3440 | - <option value="devstral-medium-2507">devstral-medium-2507</option> | |
| 3441 | - <option value="magistral-small-2506">magistral-small-2506</option> | |
| 3442 | - <option value="magistral-medium-2506">magistral-medium-2506</option> | |
| 3443 | - <option value="magistral-small-2507">magistral-small-2507</option> | |
| 3444 | - <option value="magistral-medium-2507">magistral-medium-2507</option> | |
| 3445 | - </optgroup> | |
| 3446 | - <optgroup id="mistralai_other_models" label="Other"></optgroup> | |
| 3447 | 3355 | </select> |
| 3448 | 3356 | </div> |
| 3449 | 3357 | </form> |
| @@ -3473,6 +3381,9 @@ | ||
| 3473 | 3381 | </div> |
| 3474 | 3382 | <div id="electronhub_form" data-source="electronhub"> |
| 3475 | 3383 | <h4 data-i18n="Electron Hub API Key">Electron Hub API Key</h4> |
| 3384 | + <div> | |
| 3385 | + <a href="https://playground.electronhub.ai/console" target="_blank" data-i18n="View Remaining Credits">View Remaining Credits</a> | |
| 3386 | + </div> | |
| 3476 | 3387 | <div class="flex-container"> |
| 3477 | 3388 | <input id="api_key_electronhub" name="api_key_electronhub" class="text_pole flex1" value="" type="text" autocomplete="off"> |
| 3478 | 3389 | <div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_electronhub"></div> |
| @@ -3480,10 +3391,43 @@ | ||
| 3480 | 3391 | <div data-for="api_key_electronhub" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'."> |
| 3481 | 3392 | For privacy reasons, your API key will be hidden after you click 'Connect'. |
| 3482 | 3393 | </div> |
| 3483 | - <h4 data-i18n="Electron Hub Model">Electron Hub Model</h4> | |
| 3394 | + <div> | |
| 3484 | - <select id="model_electronhub_select"> | |
| 3395 | + <h4 data-i18n="Electron Hub Model">Electron Hub Model</h4> | |
| 3485 | - <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> | |
| 3396 | + <select id="model_electronhub_select"> | |
| 3486 | - </select> | |
| 3397 | + <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> | |
| 3398 | + </select> | |
| 3399 | + </div> | |
| 3400 | + <div class="marginTopBot5"> | |
| 3401 | + <div class="inline-drawer wide100p"> | |
| 3402 | + <div class="inline-drawer-toggle inline-drawer-header"> | |
| 3403 | + <b data-i18n="Electron Hub Model Sorting">Electron Hub Model Sorting</b> | |
| 3404 | + <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> | |
| 3405 | + </div> | |
| 3406 | + <div class="inline-drawer-content m-b-1"> | |
| 3407 | + <div class="marginTopBot5"> | |
| 3408 | + <label for="electronhub_sort_models" class="checkbox_label"> | |
| 3409 | + <select id="electronhub_sort_models"> | |
| 3410 | + <option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option> | |
| 3411 | + <option data-i18n="Input Price" value="pricing.input">Input Price (cheapest)</option> | |
| 3412 | + <option data-i18n="Output Price" value="pricing.output">Output Price (cheapest)</option> | |
| 3413 | + <option data-i18n="Context Size" value="context_length">Context Size</option> | |
| 3414 | + </select> | |
| 3415 | + </label> | |
| 3416 | + </div> | |
| 3417 | + <div class="marginTopBot5"> | |
| 3418 | + <label for="electronhub_group_models" class="checkbox_label"> | |
| 3419 | + <input id="electronhub_group_models" type="checkbox" /> | |
| 3420 | + <span data-i18n="Group by vendors">Group by vendors</span> | |
| 3421 | + </label> | |
| 3422 | + <div class="toggle-description justifyLeft wide100p"> | |
| 3423 | + <span data-i18n="Group by vendors Description"> | |
| 3424 | + Put OpenAI models in one group, Anthropic models in other group, etc. Can be combined with sorting. | |
| 3425 | + </span> | |
| 3426 | + </div> | |
| 3427 | + </div> | |
| 3428 | + </div> | |
| 3429 | + </div> | |
| 3430 | + </div> | |
| 3487 | 3431 | </div> |
| 3488 | 3432 | <div id="nanogpt_form" data-source="nanogpt"> |
| 3489 | 3433 | <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4> |
| @@ -3666,18 +3610,12 @@ | ||
| 3666 | 3610 | <div data-for="api_key_xai" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'."> |
| 3667 | 3611 | For privacy reasons, your API key will be hidden after you click 'Connect'. |
| 3668 | 3612 | </div> |
| 3669 | - <h4 data-i18n="xAI Model">xAI Model</h4> | |
| 3613 | + <div> | |
| 3670 | - <select id="model_xai_select"> | |
| 3614 | + <h4 data-i18n="xAI Model">xAI Model</h4> | |
| 3671 | - <option value="grok-4-0709">grok-4-0709</option> | |
| 3615 | + <select id="model_xai_select"> | |
| 3672 | - <option value="grok-3-beta">grok-3-beta</option> | |
| 3616 | + <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> | |
| 3673 | - <option value="grok-3-fast-beta">grok-3-fast-beta</option> | |
| 3617 | + </select> | |
| 3674 | - <option value="grok-3-mini-beta">grok-3-mini-beta</option> | |
| 3618 | + </div> | |
| 3675 | - <option value="grok-3-mini-fast-beta">grok-3-mini-fast-beta</option> | |
| 3676 | - <option value="grok-2-vision-1212">grok-2-vision-1212</option> | |
| 3677 | - <option value="grok-2-1212">grok-2-1212</option> | |
| 3678 | - <option value="grok-vision-beta">grok-vision-beta</option> | |
| 3679 | - <option value="grok-beta">grok-beta</option> | |
| 3680 | - </select> | |
| 3681 | 3619 | </div> |
| 3682 | 3620 | <div id="aimlapi_form" data-source="aimlapi"> |
| 3683 | 3621 | <h4> |
| @@ -4279,7 +4217,7 @@ | ||
| 4279 | 4217 | Token Padding |
| 4280 | 4218 | </small> |
| 4281 | 4219 | </div> |
| 4282 | 4220 | <input id="token_padding" class="text_pole textarea_compact" type="number" min="-2048" max="2048" step="1" /> |
| 4283 | 4221 | </div> |
| 4284 | 4222 | </div> |
| 4285 | 4223 | <div> |
| @@ -4959,7 +4897,6 @@ | ||
| 4959 | 4897 | <small data-i18n="Smooth Streaming"> |
| 4960 | 4898 | Smooth Streaming |
| 4961 | 4899 | </small> |
| 4962 | - <i class="fa-solid fa-flask" data-i18n="[title]Experimental feature. May not work for all backends." title="Experimental feature. May not work for all backends."></i> | |
| 4963 | 4900 | </div> |
| 4964 | 4901 | <div id="smooth_streaming_speed_control" class="flexBasis100p wide100p"> |
| 4965 | 4902 | <input type="range" id="smooth_streaming_speed" name="smooth_streaming_speed" min="0" max="100" step="10" value="50"> |
| @@ -4970,6 +4907,11 @@ | ||
| 4970 | 4907 | </div> |
| 4971 | 4908 | </div> |
| 4972 | 4909 | </label> |
| 4910 | + <label class="checkbox_label" for="stream_fade_in" title="Fade in streamed text when it appears, instead of it just popping in." data-i18n="[title]Fade in streamed text when it appears, instead of it just popping in"> | |
| 4911 | + <input id="stream_fade_in" type="checkbox" /> | |
| 4912 | + <small data-i18n="Stream Fade-In">Stream Fade-In</small> | |
| 4913 | + <i class="fa-solid fa-flask" data-i18n="[title]Experimental feature. May not work for all backends." title="Experimental feature. May not work for all backends."></i> | |
| 4914 | + </label> | |
| 4973 | 4915 | |
| 4974 | 4916 | <label for="play_message_sound" class="checkbox_label" title="Play a sound when a message generation finishes." data-i18n="[title]Play a sound when a message generation finishes"> |
| 4975 | 4917 | <input id="play_message_sound" type="checkbox" /> |
| @@ -5339,15 +5281,18 @@ | ||
| 5339 | 5281 | </div> |
| 5340 | 5282 | </div> |
| 5341 | 5283 | </div> |
| 5342 | 5284 | <div id="logo_blockbackgrounds-button" class="drawer"> |
| 5343 | 5285 | <div id="site_logobackgrounds-drawer-toggle" class="drawer-toggle drawer-header" title="Change Background Image" data-i18n="[title]Change Background Image"> |
| 5344 | 5286 | <div class="drawer-icon fa-solid fa-panorama fa-fw closedIcon"></div> |
| 5345 | 5287 | </div> |
| 5346 | 5288 | <div id="Backgrounds" class="drawer-content closedDrawer bg-drawer-layout"> |
| 5347 | 5289 | <div id="bg-header-fixed"> |
| 5348 | 5290 | <div class="flexbg-container alignItemsBaseline wide100pheader-row-1"> |
| 5349 | - <h3 id="bg-header-title" class="margin0" data-i18n="Backgrounds">Backgrounds</h3> | |
| 5291 | + <label for="add_bg_button" id="add_background_button_top" class="menu_button menu_button_icon" data-i18n="[title]Add a new background" title="Add a new background"> | |
| 5350 | - <input id="bg-filter" class="text_pole flex1" type="search" data-i18n="[placeholder]Search" placeholder="Search" /> | |
| 5292 | + <i class="fa-solid fa-plus"></i> | |
| 5293 | + <span data-i18n="Add Background">Add Background</span> | |
| 5294 | + </label> | |
| 5295 | + <span class="expander"></span> | |
| 5351 | 5296 | <select id="background_fitting" class="text_pole" data-i18n="[title]Background Fitting" title="Background Fitting"> |
| 5352 | 5297 | <option value="classic" data-i18n="Classic">Classic</option> |
| 5353 | 5298 | <option value="cover" data-i18n="Cover">Cover</option> |
| @@ -5359,16 +5304,25 @@ | ||
| 5359 | 5304 | <i class="fa-solid fa-wand-magic"></i> |
| 5360 | 5305 | <span data-i18n="Auto-select">Auto-select</span> |
| 5361 | 5306 | </div> |
| 5362 | - <label for="add_bg_button" id="add_background_button_top" class="menu_button menu_button_icon interactable" title="Add a new background"> | |
| 5307 | + </div> | |
| 5363 | 5308 | <idiv class="fabg-solid faheader-plusrow-2"></i> |
| 5364 | - <span data-i18n="Add Background">Add Background</span> | |
| 5309 | + <input id="bg-filter" class="text_pole" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." /> | |
| 5365 | - </label> | |
| 5366 | 5310 | </div> |
| 5367 | 5311 | </div> |
| 5368 | 5312 | <div id="bg-scrollable-content"> |
| 5369 | - <h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter"> | |
| 5313 | + <div class="heading-container-with-controls"> | |
| 5370 | - System Backgrounds | |
| 5314 | + <h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter heading-text"> | |
| 5371 | - </h3> | |
| 5315 | + System Backgrounds | |
| 5316 | + </h3> | |
| 5317 | + <div class="heading-controls"> | |
| 5318 | + <button id="bg_thumb_zoom_out" class="menu_button menu_button_icon" title="Make thumbnails smaller" data-i18n="[title]Make thumbnails smaller"> | |
| 5319 | + <i class="fa-solid fa-minus"></i> | |
| 5320 | + </button> | |
| 5321 | + <button id="bg_thumb_zoom_in" class="menu_button menu_button_icon" title="Make thumbnails larger" data-i18n="[title]Make thumbnails larger"> | |
| 5322 | + <i class="fa-solid fa-plus"></i> | |
| 5323 | + </button> | |
| 5324 | + </div> | |
| 5325 | + </div> | |
| 5372 | 5326 | <div id="bg_menu_content" class="bg_list"> |
| 5373 | 5327 | </div> |
| 5374 | 5328 | <h3 data-i18n="Chat Backgrounds" class="wide100p textAlignCenter"> |
| @@ -5381,8 +5335,11 @@ | ||
| 5381 | 5335 | </div> |
| 5382 | 5336 | </div> |
| 5383 | 5337 | <form id="form_bg_upload" style="display: none;"> |
| 5384 | 5338 | <input type="file" id="add_bg_button" name="avatar" accept="image/jpeg,image/png,image/gif,image/bmp,image/svg+xml,video/*"> |
| 5385 | 5339 | </form> |
| 5340 | + <button id="bg-scroll-top" type="button" class="menu_button menu_button_icon" title="Scroll backgrounds to top" data-i18n="[title]Scroll backgrounds to top"> | |
| 5341 | + <i class="fa-solid fa-chevron-up" aria-hidden="true"></i> | |
| 5342 | + </button> | |
| 5386 | 5343 | </div> |
| 5387 | 5344 | </div> |
| 5388 | 5345 | <div id="extensions-settings-button" class="drawer"> |
| @@ -5540,7 +5497,10 @@ | ||
| 5540 | 5497 | </div> |
| 5541 | 5498 | </div> |
| 5542 | 5499 | |
| 5543 | 5500 | <h4 data-i18nclass="Personaflex-container DescriptionalignItemsBaseline">Persona Description</h4> |
| 5501 | + <span data-i18n="Persona Description">Persona Description</span> | |
| 5502 | + <i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="persona_description" title="Expand the editor" data-i18n="[title]Expand the editor"></i> | |
| 5503 | + </h4> | |
| 5544 | 5504 | <textarea id="persona_description" name="persona_description" data-i18n="[placeholder]Example: [{{user}} is a 28-year-old Romanian cat girl.]" placeholder="Example: [{{user}} is a 28-year-old Romanian cat girl.]" class="text_pole textarea_compact" value="" autocomplete="off" rows="8"></textarea> |
| 5545 | 5505 | |
| 5546 | 5506 | <div class="flex-container justifySpaceBetween"> |
| @@ -6291,7 +6251,7 @@ | ||
| 6291 | 6251 | <div name="selectChatPopupHeader" class="flex-container alignitemscenter justifySpaceBetween flexGap10"> |
| 6292 | 6252 | <div id="select_chat_import"> <!-- import chat popup header --> |
| 6293 | 6253 | <form id="form_import_chat" action="javascript:void(null);" method="post" enctype="multipart/form-data" style="display: none;"> |
| 6294 | 6254 | <input type="file" id="chat_import_file" accept=".json, .jsonl" multiple name="avatar"> |
| 6295 | 6255 | <input id="chat_import_file_type" name="file_type" class="text_pole" value="" autocomplete="off" style="display: none;"> |
| 6296 | 6256 | <input id="chat_import_avatar_url" name="avatar_url" class="text_pole" value="" autocomplete="off" style="display: none;"> |
| 6297 | 6257 | <input id="chat_import_character_name" name="character_name" class="text_pole" value="" autocomplete="off" style="display: none;"> |
| @@ -6322,10 +6282,9 @@ | ||
| 6322 | 6282 | <i class="fa-solid fa-ellipsis-vertical"></i> |
| 6323 | 6283 | </div> |
| 6324 | 6284 | <div class="jg-menu"> |
| 6325 | - <div data-action="copy" class="jg-button jg-copy fa-solid fa-file-arrow-up" data-i18n="[title]Copy to system backgrounds" title="Copy to system backgrounds"></div> | |
| 6326 | - <!-- temporarily moved lock icon here (will be moved to header) --> | |
| 6327 | 6285 | <div data-action="lock" class="jg-button jg-lock fa-solid fa-lock fa-fw pointer" data-i18n="[title]Lock" title="Lock"></div> |
| 6328 | 6286 | <div data-action="unlock" class="jg-button jg-unlock fa-solid fa-lock-open fa-fw pointer" data-i18n="[title]Unlock" title="Unlock"></div> |
| 6287 | + <div data-action="copy" class="jg-button jg-copy fa-solid fa-file-arrow-up" data-i18n="[title]Copy to system backgrounds" title="Copy to system backgrounds"></div> | |
| 6329 | 6288 | <div data-action="edit" class="jg-button jg-edit fa-solid fa-pen-to-square fa-fw pointer" data-i18n="[title]Rename Background" title="Rename Background"></div> |
| 6330 | 6289 | <div data-action="delete" class="jg-button jg-delete fa-solid fa-trash-can fa-fw pointer" data-i18n="[title]Delete Background" title="Delete Background"></div> |
| 6331 | 6290 | </div> |
| @@ -6448,6 +6407,13 @@ | ||
| 6448 | 6407 | </div> |
| 6449 | 6408 | </div> |
| 6450 | 6409 | <div name="perEntryOverridesBlock" class="flex-container wide100p alignitemscenter"> |
| 6410 | + <div class="world_entry_form_control flex1" title="Set the outlet name for this WI entry. WI entries with position 'outlet' will not be added to the prompt automatically. Instead, they will be collected and can be used as a macro in the prompt. Add {{outlet::YourName}} to the prompt in any place you want to add all WI entries this specific outlet." data-i18n="[title]wi_outlet_name"> | |
| 6411 | + <small class="textAlignCenter"> | |
| 6412 | + <span data-i18n="Outlet Name">Outlet Name</span> | |
| 6413 | + <div class="fa-solid fa-circle-info opacity50p"></div> | |
| 6414 | + </small> | |
| 6415 | + <input class="text_pole margin0" name="outletName" type="text" placeholder="Outlet Name" data-i18n="[placeholder]Outlet Name"> | |
| 6416 | + </div> | |
| 6451 | 6417 | <div class="world_entry_form_control flex1"> |
| 6452 | 6418 | <small class="textAlignCenter" data-i18n="Scan Depth">Scan Depth</small> |
| 6453 | 6419 | <input class="text_pole margin0" name="scanDepth" type="number" placeholder="Use global setting" data-i18n="[placeholder]Use global setting" max="1000"> |
| @@ -6785,6 +6751,9 @@ | ||
| 6785 | 6751 | <option value="4" data-role="2" data-i18n="at Depth AI"> |
| 6786 | 6752 | @D 🤖 |
| 6787 | 6753 | </option> |
| 6754 | + <option value="7" data-role="" data-i18n="Outlet"> | |
| 6755 | + ➡️ Outlet | |
| 6756 | + </option> | |
| 6788 | 6757 | </select> |
| 6789 | 6758 | </div> |
| 6790 | 6759 | <div class="world_entry_form_control wi-enter-footer-text flex-container flexNoGap"> |
| @@ -139,9 +139,9 @@ | ||
| 139 | 139 | return el; |
| 140 | 140 | }, |
| 141 | 141 | |
| 142 | 142 | getPageLinkTag: function(indextext) { |
| 143 | 143 | var pageLink = attributes.pageLink; |
| 144 | 144 | return pageLink ? `<a href="${pageLink}">${indextext}</a>` : `<a>${indextext}</a>`; |
| 145 | 145 | }, |
| 146 | 146 | |
| 147 | 147 | // Generate HTML for page numbers |
| @@ -233,6 +233,8 @@ | ||
| 233 | 233 | |
| 234 | 234 | var prevText = attributes.prevText; |
| 235 | 235 | var nextText = attributes.nextText; |
| 236 | + var firstText = attributes.firstText; | |
| 237 | + var lastText = attributes.lastText; | |
| 236 | 238 | var goButtonText = attributes.goButtonText; |
| 237 | 239 | |
| 238 | 240 | var classPrefix = attributes.classPrefix; |
| @@ -240,6 +242,8 @@ | ||
| 240 | 242 | var ulClassName = attributes.ulClassName || ''; |
| 241 | 243 | var prevClassName = attributes.prevClassName || ''; |
| 242 | 244 | var nextClassName = attributes.nextClassName || ''; |
| 245 | + var firstClassName = attributes.firstClassName || ''; | |
| 246 | + var lastClassName = attributes.lastClassName || ''; | |
| 243 | 247 | |
| 244 | 248 | var html = ''; |
| 245 | 249 | var sizeSelect = `<select class="J-paginationjs-size-select">`; |
| @@ -295,9 +299,11 @@ | ||
| 295 | 299 | if (showPrevious) { |
| 296 | 300 | if (currentPage <= 1) { |
| 297 | 301 | if (!autoHidePrevious) { |
| 302 | + html += `<li class="${classPrefix}-first ${disableClassName} ${firstClassName}"><a>${firstText}</a></li>`; | |
| 298 | 303 | html += `<li class="${classPrefix}-prev ${disableClassName} ${prevClassName}"><a>${prevText}</a></li>`; |
| 299 | 304 | } |
| 300 | 305 | } else { |
| 306 | + html += `<li class="${classPrefix}-first J-paginationjs-first ${firstClassName}" data-num="1" title="First page">${getPageLinkTag(firstText)}</li>`; | |
| 301 | 307 | html += `<li class="${classPrefix}-prev J-paginationjs-previous ${prevClassName}" data-num="${currentPage - 1}" title="Previous page">${getPageLinkTag(prevText)}</li>`; |
| 302 | 308 | } |
| 303 | 309 | } |
| @@ -312,9 +318,11 @@ | ||
| 312 | 318 | if (currentPage >= totalPage) { |
| 313 | 319 | if (!autoHideNext) { |
| 314 | 320 | html += `<li class="${classPrefix}-next ${disableClassName} ${nextClassName}"><a>${nextText}</a></li>`; |
| 321 | + html += `<li class="${classPrefix}-last ${disableClassName} ${lastClassName}"><a>${lastText}</a></li>`; | |
| 315 | 322 | } |
| 316 | 323 | } else { |
| 317 | 324 | html += `<li class="${classPrefix}-next J-paginationjs-next ${nextClassName}" data-num="${currentPage + 1}" title="Next page">${getPageLinkTag(nextText)}</li>`; |
| 325 | + html += `<li class="${classPrefix}-last J-paginationjs-last ${lastClassName}" data-num="${totalPage}" title="Last page">${getPageLinkTag(lastText)}</li>`; | |
| 318 | 326 | } |
| 319 | 327 | } |
| 320 | 328 | html += `</ul></div>`; |
| @@ -542,6 +550,14 @@ | ||
| 542 | 550 | this.go(this.model.pageNumber + 1, callback); |
| 543 | 551 | }, |
| 544 | 552 | |
| 553 | + first: function(callback) { | |
| 554 | + this.go(1, callback); | |
| 555 | + }, | |
| 556 | + | |
| 557 | + last: function(callback) { | |
| 558 | + this.go(this.model.totalPage, callback); | |
| 559 | + }, | |
| 560 | + | |
| 545 | 561 | disable: function() { |
| 546 | 562 | var self = this; |
| 547 | 563 | var source = self.isAsync ? 'async' : 'sync'; |
| @@ -774,6 +790,38 @@ | ||
| 774 | 790 | if (!attributes.pageLink) return false; |
| 775 | 791 | }); |
| 776 | 792 | |
| 793 | + // First button click listener | |
| 794 | + el.on('click', '.J-paginationjs-first', function(event) { | |
| 795 | + var current = $(event.currentTarget); | |
| 796 | + var pageNumber = current.attr('data-num').trim(); | |
| 797 | + | |
| 798 | + if (!pageNumber || current.hasClass(attributes.disableClassName)) return; | |
| 799 | + | |
| 800 | + if (self.callHook('beforeFirstOnClick', event, pageNumber) === false) return false; | |
| 801 | + | |
| 802 | + self.go(pageNumber); | |
| 803 | + | |
| 804 | + self.callHook('afterFirstOnClick', event, pageNumber); | |
| 805 | + | |
| 806 | + if (!attributes.pageLink) return false; | |
| 807 | + }); | |
| 808 | + | |
| 809 | + // Last button click listener | |
| 810 | + el.on('click', '.J-paginationjs-last', function(event) { | |
| 811 | + var current = $(event.currentTarget); | |
| 812 | + var pageNumber = current.attr('data-num').trim(); | |
| 813 | + | |
| 814 | + if (!pageNumber || current.hasClass(attributes.disableClassName)) return; | |
| 815 | + | |
| 816 | + if (self.callHook('beforeLastOnClick', event, pageNumber) === false) return false; | |
| 817 | + | |
| 818 | + self.go(pageNumber); | |
| 819 | + | |
| 820 | + self.callHook('afterLastOnClick', event, pageNumber); | |
| 821 | + | |
| 822 | + if (!attributes.pageLink) return false; | |
| 823 | + }); | |
| 824 | + | |
| 777 | 825 | // Go button click listener |
| 778 | 826 | el.on('click', '.J-paginationjs-go-button', function(event) { |
| 779 | 827 | var pageNumber = $('.J-paginationjs-go-pagenumber', el).val(); |
| @@ -833,6 +881,16 @@ | ||
| 833 | 881 | self.next(done); |
| 834 | 882 | }); |
| 835 | 883 | |
| 884 | + // First page | |
| 885 | + container.on(eventPrefix + 'first', function(event, done) { | |
| 886 | + self.first(done); | |
| 887 | + }); | |
| 888 | + | |
| 889 | + // Last page | |
| 890 | + container.on(eventPrefix + 'last', function(event, done) { | |
| 891 | + self.last(done); | |
| 892 | + }); | |
| 893 | + | |
| 836 | 894 | // Disable |
| 837 | 895 | container.on(eventPrefix + 'disable', function() { |
| 838 | 896 | self.disable(); |
| @@ -892,6 +950,8 @@ | ||
| 892 | 950 | switch (options) { |
| 893 | 951 | case 'previous': |
| 894 | 952 | case 'next': |
| 953 | + case 'first': | |
| 954 | + case 'last': | |
| 895 | 955 | case 'go': |
| 896 | 956 | case 'disable': |
| 897 | 957 | case 'enable': |
| @@ -991,6 +1051,12 @@ | ||
| 991 | 1051 | // 'Next' text |
| 992 | 1052 | nextText: '›', |
| 993 | 1053 | |
| 1054 | + // 'First' text | |
| 1055 | + firstText: '«', | |
| 1056 | + | |
| 1057 | + // 'Last' text | |
| 1058 | + lastText: '»', | |
| 1059 | + | |
| 994 | 1060 | // Ellipsis text |
| 995 | 1061 | ellipsisText: '...', |
| 996 | 1062 | |
| @@ -1149,7 +1215,7 @@ | ||
| 1149 | 1215 | |
| 1150 | 1216 | // uninstall plugin |
| 1151 | 1217 | function uninstallPlugin(target) { |
| 1152 | 1218 | var events = ['go', 'previous', 'next', 'first', 'last', 'disable', 'enable', 'refresh', 'show', 'hide', 'destroy']; |
| 1153 | 1219 | |
| 1154 | 1220 | // off all events |
| 1155 | 1221 | $.each(events, function(index, value) { |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "وحدة تحكم المطور في Anthropic", |
| 370 | 370 | "Claude Model": "نموذج Claude", |
| 371 | 371 | "Window AI Model": "نموذج Window AI", |
| 372 | 372 | "OpenRouter Model OrderSorting": "فرز نموذج OpenRouter", |
| 373 | 373 | "Alphabetically": "أبجديا", |
| 374 | 374 | "Price": "السعر (الأرخص)", |
| 375 | 375 | "Context Size": "حجم السياق", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "Anthropics Entwicklerkonsole", |
| 370 | 370 | "Claude Model": "Claude-Modell", |
| 371 | 371 | "Window AI Model": "Fenster AI-Modell", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Sortierung des OpenRouter-Modells", |
| 373 | 373 | "Alphabetically": "Alphabetisch", |
| 374 | 374 | "Price": "Preis (am günstigsten)", |
| 375 | 375 | "Context Size": "Kontextgröße", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "la consola de desarrolladores de Anthropic", |
| 370 | 370 | "Claude Model": "Modelo de Claude", |
| 371 | 371 | "Window AI Model": "Modelo de Window AI", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Clasificación de modelos de OpenRouter", |
| 373 | 373 | "Alphabetically": "Alfabéticamente", |
| 374 | 374 | "Price": "Precio (más barato)", |
| 375 | 375 | "Context Size": "Tamaño del contexto", |
| @@ -348,7 +348,7 @@ | ||
| 348 | 348 | "Anthropic's developer console": "Console de développement d'Anthropic", |
| 349 | 349 | "Claude Model": "Modèle Claude", |
| 350 | 350 | "Window AI Model": "Modèle Window AI", |
| 351 | 351 | "OpenRouter Model OrderSorting": "Tri des modèles OpenRouter", |
| 352 | 352 | "Alphabetically": "Alphabétiquement", |
| 353 | 353 | "Price": "Prix (le moins cher)", |
| 354 | 354 | "Context Size": "Taille du contexte", |
| @@ -1566,7 +1566,7 @@ | ||
| 1566 | 1566 | "These files are available for all characters in the current chat.": "Ces fichiers sont disponibles pour tous les personnages du chat actuel.", |
| 1567 | 1567 | "Image Captioning": "Légende des images", |
| 1568 | 1568 | "Local": "Local", |
| 1569 | - "Multimodal (OpenAI / Anthropic / llama / Google)": "Multimodal (OpenAI / Anthropic / llama / Google)", | |
| 1569 | + "Multimodal": "Multimodal", | |
| 1570 | 1570 | "Extras": "Extras", |
| 1571 | 1571 | "Horde": "Horde", |
| 1572 | 1572 | "API": "API", |
| @@ -367,7 +367,7 @@ | ||
| 367 | 367 | "Anthropic's developer console": "Uppbyggingaraðilar Forritara stjórnborð", |
| 368 | 368 | "Claude Model": "Claude módel", |
| 369 | 369 | "Window AI Model": "Vindauga AI módel", |
| 370 | 370 | "OpenRouter Model OrderSorting": "OpenRouter líkanaflokkun", |
| 371 | 371 | "Alphabetically": "Stafrófsröð", |
| 372 | 372 | "Price": "Verð (ódýrast)", |
| 373 | 373 | "Context Size": "Samhengisstærð", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "Console dello sviluppatore di Anthropic", |
| 370 | 370 | "Claude Model": "Modello Claude", |
| 371 | 371 | "Window AI Model": "Modello AI di Window", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Ordinamento dei modelli OpenRouter", |
| 373 | 373 | "Alphabetically": "In ordine alfabetico", |
| 374 | 374 | "Price": "Prezzo (il più economico)", |
| 375 | 375 | "Context Size": "Dimensione del contesto", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "Anthropicの開発者コンソール", |
| 370 | 370 | "Claude Model": "Claudeモデル", |
| 371 | 371 | "Window AI Model": "Window AIモデル", |
| 372 | 372 | "OpenRouter Model OrderSorting": "OpenRouter モデルのソート", |
| 373 | 373 | "Alphabetically": "アルファベット順", |
| 374 | 374 | "Price": "価格(最安値)", |
| 375 | 375 | "Context Size": "コンテキストサイズ", |
| @@ -374,7 +374,7 @@ | ||
| 374 | 374 | "Slack and Poe cookies will not work here, do not bother trying.": "Slack과 Poe 쿠키는 여기서 작동하지 않습니다. 시도하지 마세요.", |
| 375 | 375 | "Claude Model": "Claude 모델", |
| 376 | 376 | "Window AI Model": "Window AI 모델", |
| 377 | 377 | "OpenRouter Model OrderSorting": "OpenRouter 모델 정렬", |
| 378 | 378 | "Alphabetically": "알파벳순", |
| 379 | 379 | "Price": "가격(가장 저렴)", |
| 380 | 380 | "Context Size": "컨텍스트 크기", |
| @@ -367,7 +367,7 @@ | ||
| 367 | 367 | "Anthropic's developer console": "Anthropic's ontwikkelaarsconsole", |
| 368 | 368 | "Claude Model": "Claude-model", |
| 369 | 369 | "Window AI Model": "Window AI-model", |
| 370 | 370 | "OpenRouter Model OrderSorting": "Sorteren van OpenRouter-modellen", |
| 371 | 371 | "Alphabetically": "Alfabetisch", |
| 372 | 372 | "Price": "Prijs (goedkoopste)", |
| 373 | 373 | "Context Size": "Contextgrootte", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "console de desenvolvedor da Anthropic", |
| 370 | 370 | "Claude Model": "Modelo Claude", |
| 371 | 371 | "Window AI Model": "Modelo Window AI", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Classificação de modelo OpenRouter", |
| 373 | 373 | "Alphabetically": "Alfabeticamente", |
| 374 | 374 | "Price": "Preço (mais barato)", |
| 375 | 375 | "Context Size": "Tamanho do contexto", |
| @@ -24,7 +24,7 @@ | ||
| 24 | 24 | "Mirostat_Tau_desc": "Целевая перплексия.", |
| 25 | 25 | "Mirostat_Eta_desc": "Скорость обучения Mirostat.", |
| 26 | 26 | "Temperature Last": "Температура последней", |
| 27 | 27 | "LLaMA / Mistral / Yi models only": "Только для моделей LLaMA / Mistral / Yi. Перед этим обязательно выберите подходящий токенизатор.\nПоследовательности, которых не должно быть на выходе.\nОдна на строку. Текст или [идентификаторы токенов].\nМногие токены имеют пробел впереди. Используйте счетчиксчётчик токенов, если не уверены, что правильно их определяете.", |
| 28 | 28 | "Example: some text [42, 69, 1337]": "Пример:\nкакой-то текст\n[42, 69, 1337]", |
| 29 | 29 | "Classifier Free Guidance. More helpful tip coming soon": "Classifier Free Guidance. Чуть позже опишем более подробно", |
| 30 | 30 | "Scale": "Scale", |
| @@ -91,7 +91,7 @@ | ||
| 91 | 91 | "Impersonation prompt": "Промпт для перевоплощения", |
| 92 | 92 | "Prompt that is used for Impersonation function": "Промпт, применяемый при генерации действий от лица пользователя", |
| 93 | 93 | "Logit Bias": "Смещение логитов", |
| 94 | 94 | "Helps to ban or reenforce the usage of certain words": "Запрещает или поощряет использование определенных слов.", |
| 95 | 95 | "View / Edit bias preset": "Просмотр / Редактирование пресета смещения", |
| 96 | 96 | "Add bias entry": "Добавить правило смещения", |
| 97 | 97 | "Connect": "Подключиться", |
| @@ -201,8 +201,8 @@ | ||
| 201 | 201 | "Bubbles": "Пузыри", |
| 202 | 202 | "No Blur Effect": "Отключить размытие", |
| 203 | 203 | "No Text Shadows": "Отключить тень текста", |
| 204 | 204 | "Waifu Mode": "Режим вайфувизуальной новеллы", |
| 205 | 205 | "Message Timer": "ТаймерДлительность генерации сообщений", |
| 206 | 206 | "Model Icon": "Значки моделей", |
| 207 | 207 | "Advanced Character Search": "Расширенный поиск по персонажам", |
| 208 | 208 | "Allow {{char}}: in bot messages": "Показывать {{char}}: в ответах", |
| @@ -211,12 +211,12 @@ | ||
| 211 | 211 | "Lorebook Import Dialog": "Показывать окно импорта лорбука", |
| 212 | 212 | "MUI Preset": "Пресет MUI:", |
| 213 | 213 | "If set in the advanced character definitions, this field will be displayed in the characters list.": "Если это поле задано в расширенных параметрах персонажа, оно будет отображаться в списке персонажей.", |
| 214 | 214 | "Relaxed API URLS": "Смягчённые\"Ленивые\" адреса API", |
| 215 | 215 | "Custom CSS": "Пользовательский CSS", |
| 216 | 216 | "Mancer Model": "Модель Mancer", |
| 217 | 217 | "API Type": "Тип API", |
| 218 | 218 | "Aphrodite API key": "Ключ от API Aphrodite", |
| 219 | 219 | "Relax message trim in Groups": "МягкаяСмягчить обрезкатребования сообщенийк сообщениям ИИ в группах", |
| 220 | 220 | "Characters Hotswap": "HotSwap (смена персонажей на лету)", |
| 221 | 221 | "Request token probabilities": "Запрашивать вероятность токена", |
| 222 | 222 | "Movable UI Panels": "Подвижные панели UI", |
| @@ -229,10 +229,10 @@ | ||
| 229 | 229 | "Text Shadow Width": "Размер теней текста", |
| 230 | 230 | "Swipes": "Свайпы", |
| 231 | 231 | "Miscellaneous": "Разное", |
| 232 | 232 | "Background Sound Only": "Только фоновыйфоновые звукзвуки", |
| 233 | 233 | "Auto-load Last Chat": "Автозагрузка последнего чата", |
| 234 | 234 | "Auto-save Message Edits": "Автоматически сохранять отредактированные сообщения", |
| 235 | 235 | "Auto-fix Markdown": "Автоисправление разметки Markdown", |
| 236 | 236 | "Auto-scroll Chat": "Автоматическая прокрутка чата", |
| 237 | 237 | "Send on Enter": "Отправка на Enter", |
| 238 | 238 | "Debug Menu": "Меню отладки", |
| @@ -329,7 +329,7 @@ | ||
| 329 | 329 | "Global Lore First": "Сначала глобальный лор", |
| 330 | 330 | "Recursive Scan": "Рекурсивное сканирование", |
| 331 | 331 | "Case Sensitive": "Учитывать регистр", |
| 332 | 332 | "Alert On Overflow": "Оповещение о переполнениипревышении бюджета", |
| 333 | 333 | "Use Probability": "Использовать вероятность", |
| 334 | 334 | "Exclude from recursion": "Исключить из рекурсии", |
| 335 | 335 | "Entry Title/Memo": "Название или заметка о записи", |
| @@ -354,7 +354,7 @@ | ||
| 354 | 354 | "Chat Background": "Фон чата", |
| 355 | 355 | "UI Background": "Фон UI", |
| 356 | 356 | "Mad Lab Mode": "Режим безумца", |
| 357 | 357 | "Show Message Token Count": "СчетчикПоказывать кол-во токенов сообщенияв сообщении", |
| 358 | 358 | "Compact Input Area (Mobile)": "Компактная зона ввода", |
| 359 | 359 | "Zen Sliders": "Дзен слайдеры", |
| 360 | 360 | "UI Border": "Границы UI", |
| @@ -363,7 +363,7 @@ | ||
| 363 | 363 | "Tags as Folders": "Теги как папки", |
| 364 | 364 | "Streaming FPS": "FPS для стриминга", |
| 365 | 365 | "Gestures": "Жесты", |
| 366 | 366 | "Message IDs": "IDНомера сообщений", |
| 367 | 367 | "Prefer Character Card Prompt": "Приоритет промпту из карточки персонажа", |
| 368 | 368 | "Prefer Character Card Jailbreak": "Приоритет джейлбрейку из карточки персонажа", |
| 369 | 369 | "Press Send to continue": "Кнопка отправки продолжает сообщение", |
| @@ -372,7 +372,7 @@ | ||
| 372 | 372 | "Never resize avatars": "Не менять размер аватарок", |
| 373 | 373 | "Show avatar filenames": "Показывать названия файлов аватарок", |
| 374 | 374 | "Import Card Tags": "Импортировать теги карточки", |
| 375 | 375 | "Confirm message deletion": "ПодтверждениеСпрашивать удаленияпри удалении сообщений", |
| 376 | 376 | "Spoiler Free Mode": "Режим без спойлеров", |
| 377 | 377 | "Auto-swipe": "Автоматические свайпы", |
| 378 | 378 | "Minimum generated message length": "Минимальная длина сгенерированных сообщений", |
| @@ -392,11 +392,11 @@ | ||
| 392 | 392 | "Always show the full list of the Message Actions context items for chat messages, instead of hiding them behind '...'": "Всегда показывать полный список действий с сообщением, а не прятать их за '...'.", |
| 393 | 393 | "Alternative UI for numeric sampling parameters with fewer steps": "Уменьшить кол-во шагов для параметров, регулируемых слайдерами.", |
| 394 | 394 | "Entirely unrestrict all numeric sampling parameters": "Снять ограничения со всех числовых сэмплеров.", |
| 395 | 395 | "Time the AI's message generation, and show the duration in the chat log": "ВремяЗасекать генерациивремя, сообщенийза ИИкоторое было сгенерировано сообщение, и егоотображать показэту винформацию журналев чатачате.", |
| 396 | 396 | "Show a timestamp for each message in the chat log": "Показывать временную метку для каждого сообщения в журнале чата.", |
| 397 | 397 | "Show an icon for the API that generated the message": "ПоказатьПоказывать значок API, сгенерировавшего сообщение.", |
| 398 | 398 | "Show sequential message numbers in the chat log": "Показывать порядковые номера сообщений в журнале чатачате.", |
| 399 | 399 | "Show the number of tokens in each message in the chat log": "ПоказатьПоказывать количество токенов в каждом сообщении в журнале чатачате.", |
| 400 | 400 | "Single-row message input area. Mobile only, no effect on PC": "Однорядная область ввода сообщений. Только для мобильных устройств, на ПК не работает.", |
| 401 | 401 | "In the Character Management panel, show quick selection buttons for favorited characters": "На панели управления персонажами будут отображены кнопки быстрого выбора для избранных персонажей.", |
| 402 | 402 | "Show tagged character folders in the character list": "Отобразить теговые папки с персонажами в списке персонажей.", |
| @@ -404,7 +404,7 @@ | ||
| 404 | 404 | "Only play a sound when ST's browser tab is unfocused": "Воспроизводить звук только тогда, когда вкладка браузера ST не выбрана.", |
| 405 | 405 | "Reduce the formatting requirements on API URLs": "Снижение требований к форматированию URL-адресов API.", |
| 406 | 406 | "Ask to import the World Info/Lorebook for every new character with embedded lorebook. If unchecked, a brief message will be shown instead": "Спрашивать разрешение на импорт лорбука для всех персонажей со встроенным лорбуком. При выключенной опции вместо этого будет показываться короткое сообщение", |
| 407 | 407 | "Restore unsaved user input on page refresh": "Восстановление несохраненного пользовательского запросасообщения при обновлении страницы.", |
| 408 | 408 | "Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "Позволяет перемещать некоторые элементы интерфейса путем их перетаскивания. Только для ПК, на телефонах не работает.", |
| 409 | 409 | "MovingUI preset. Predefined/saved draggable positions": "Пресет для MovingUI. Предопределенные/сохраненные позиции для перетаскивания.", |
| 410 | 410 | "Save movingUI changes to a new file": "Сохранение изменений MovingUI в новый файл.", |
| @@ -419,7 +419,7 @@ | ||
| 419 | 419 | "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "Показывать кнопки со стрелками на последнем сообщении в чате, чтобы генерировать альтернативные ответы ИИ. Как для ПК, так и для мобильных устройств.", |
| 420 | 420 | "Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "Позволяет использовать жесты смахивания на последнем сообщении в чате, чтобы вызвать альтернативную генерацию. Только для мобильных устройств, на ПК не работает.", |
| 421 | 421 | "Save edits to messages without confirmation as you type": "Сохранять правки в сообщениях без подтверждения по мере ввода текста.", |
| 422 | 422 | "Skip encoding and characters in message text, allowing a subset of HTML markup as well as Markdown": "Не кодировать символы < и > в тексте сообщения, что позволяет использовать подмножествочасть возможностей HTML-разметки, а также разметку Markdown.", |
| 423 | 423 | "Allow AI messages in groups to contain lines spoken by other group members": "Разрешить ИИ в группах генерировать строчки за других участников группы в своих сообщениях.", |
| 424 | 424 | "Requests logprobs from the API for the Token Probabilities feature": "Запросить логпробы из API для функции Token Probabilities.", |
| 425 | 425 | "Automatically reject and re-generate AI message based on configurable criteria": "Автоматическое отклонение и повторная генерация сообщений AI на основе настраиваемых критериев.", |
| @@ -549,7 +549,7 @@ | ||
| 549 | 549 | "Character Management": "Управление персонажами", |
| 550 | 550 | "Locked = Character Management panel will stay open": "Закреплено = Панель управление персонажами останется открытой", |
| 551 | 551 | "Select/Create Characters": "Выбрать/Создать персонажа", |
| 552 | 552 | "Token counts may be inaccurate and provided just for reference.": "СчетчикСчётчик токенов может быть неточным, используйте как ориентир", |
| 553 | 553 | "Click to select a new avatar for this character": "Нажмите чтобы выбрать новый аватар для этого персонажа", |
| 554 | 554 | "Example: [{{user}} is a 28-year-old Romanian cat girl.]": "Пример:\n [{{user}} is a 28-year-old Romanian cat girl.]", |
| 555 | 555 | "Toggle grid view": "Сменить вид сетки", |
| @@ -718,7 +718,7 @@ | ||
| 718 | 718 | "Google Model": "Модель Google", |
| 719 | 719 | "Cohere API Key": "Ключ от API Cohere", |
| 720 | 720 | "Cohere Model": "Модель Cohere", |
| 721 | 721 | "OpenRouter Model OrderSorting": "Сортировка моделей OpenRouter", |
| 722 | 722 | "Alphabetically": "По алфавиту", |
| 723 | 723 | "Price": "По цене (наиболее низкая)", |
| 724 | 724 | "Context Size": "По размеру контекста", |
| @@ -1024,10 +1024,10 @@ | ||
| 1024 | 1024 | "image_inlining_hint_2": ". Также это можно сделать через меню", |
| 1025 | 1025 | "image_inlining_hint_3": ".", |
| 1026 | 1026 | "Contest Winners": "Победители конкурса", |
| 1027 | 1027 | "Rename backgroundBackground": "Переименовать фон", |
| 1028 | 1028 | "Lock": "Закрепить", |
| 1029 | 1029 | "Unlock": "Открепить", |
| 1030 | 1030 | "Delete backgroundBackground": "Удалить фон", |
| 1031 | 1031 | "Export all": "Экспортировать всё", |
| 1032 | 1032 | "Export all your prompts to a file": "Экспортировать все промпты в виде файла", |
| 1033 | 1033 | "Don't add character names.": "Не добавлять имя персонажа", |
| @@ -1621,7 +1621,7 @@ | ||
| 1621 | 1621 | "Auxiliary": "Вспомогательный", |
| 1622 | 1622 | "Post-History Instructions": "Инструкции после истории", |
| 1623 | 1623 | "Current persona updated": "Текущая персона изменена", |
| 1624 | 1624 | "Your messages will now be sent as ${0}": "Ваши сообщенияТеперь будутвы отправлятьсябудете отподписываться лицакак ${0}", |
| 1625 | 1625 | "Copied!": "Скопировано!", |
| 1626 | 1626 | "Are you sure you want to delete this message?": "Вы точно хотите удалить это сообщение?", |
| 1627 | 1627 | "Delete Message": "Удалить сообщение", |
| @@ -1977,7 +1977,7 @@ | ||
| 1977 | 1977 | "Importing Tags": "Импорт тегов", |
| 1978 | 1978 | "Couldn't import tags:": "Не удалось импортировать теги:", |
| 1979 | 1979 | "Allow fallback models": "Разрешить резервные модели", |
| 1980 | 1980 | "Allow fallback providers": "Разрешить fallback-резервных провайдеров", |
| 1981 | 1981 | "To use instruct formatting, switch to OpenRouter under Text Completion API.": "Переключитесь на OpenRouter в Text Completion API, чтобы использовать форматирование Instruct-режима.", |
| 1982 | 1982 | "Select providers. No selection = all providers.": "Выберите провайдера. Нет выбранного = выбраны все.", |
| 1983 | 1983 | "Model Providers": "Провайдеры моделей", |
| @@ -2406,7 +2406,7 @@ | ||
| 2406 | 2406 | "${0} is a system assistant. Choose another character.": "Персонаж ${0} выбран в качестве системного помощника. Выберите другого.", |
| 2407 | 2407 | "Set ${0} as your assistant.": "Персонаж ${0} установлен в качестве помощника.", |
| 2408 | 2408 | "${0} is no longer your assistant.": "Персонаж ${0} снят с роли помощника.", |
| 2409 | 2409 | "Manage API keys": "УправлениеМенеджер ключамиключей", |
| 2410 | 2410 | "Key:": "Ключ:", |
| 2411 | 2411 | "No secrets saved.": "Ключей нет.", |
| 2412 | 2412 | "Add Secret": "Добавить ключ", |
| @@ -2433,7 +2433,7 @@ | ||
| 2433 | 2433 | "prompt_post_processing_semi_tools": "Semi-strict (чередовать роли, функции включены)", |
| 2434 | 2434 | "prompt_post_processing_strict_tools": "Strict (чередовать роли, сначала пользователь; функции включены)", |
| 2435 | 2435 | "prompt_post_processing_single": "Только одно сообщение пользователя (функции отключены)", |
| 2436 | 2436 | "prompt_post_processing_merge_tools": "Объединять идущие подряд сообщениесообщения с одной ролью (функции включены)", |
| 2437 | 2437 | "completions note prefix": "Постфикс ", |
| 2438 | 2438 | "suffix will be added automatically.": "будет добавлен автоматически.", |
| 2439 | 2439 | "Tabby Model": "Модель Tabby", |
| @@ -2548,7 +2548,7 @@ | ||
| 2548 | 2548 | "Default (top of context)": "По умолчанию (наверху контекста)", |
| 2549 | 2549 | "Bind Model to Templates": "Связать модель с шаблоном", |
| 2550 | 2550 | "Persona Auto Selected": "Персона автоматически изменена", |
| 2551 | 2551 | "Auto-selected persona based on ${0} connection.<br />Your messages will now be sent as ${1}.": "Персона выбрана автоматически на основе следующего критерия: ${0}<br />Ваши сообщенияТеперь будутвы отправлятьсябудете отподписываться лицакак ${1}", |
| 2552 | 2552 | "Filter to specific generation types.": "Применять промпт только при определённых типах генерации.", |
| 2553 | 2553 | "Quiet": "Тихая", |
| 2554 | 2554 | "Swipe": "Свайп", |
| @@ -2601,7 +2601,7 @@ | ||
| 2601 | 2601 | "sd_snap_txt": "Корректировать автоматически выбранное разрешение", |
| 2602 | 2602 | "sd_snap": "Подгонять картинки с фиксированным соотношением сторон (фоны, портреты) к ближайшему известному разрешению (рекомендуется для SDXL)", |
| 2603 | 2603 | "Source": "API", |
| 2604 | 2604 | "Hint: Save an API key in AI Horde API settings to use it here.": "ПодсказкаСовет: сохраните ключ от API в настройках API на сайте AI Horde, чтобы использоватьавтоматически подтянуть его автоматическисюда.", |
| 2605 | 2605 | "Sanitize prompts (recommended)": "Прогонять промпты через санитайзер (рекомендуется)", |
| 2606 | 2606 | "Sampling method": "Метод сэмплинга", |
| 2607 | 2607 | "Resolution": "Целевое разрешение", |
| @@ -2652,5 +2652,97 @@ | ||
| 2652 | 2652 | "sd_auto_auth_warning_1": " запускайте Stable Diffusion с флагом", |
| 2653 | 2653 | "sd_auto_auth_warning_2": "! Адрес SD должен быть доступен с сервера SillyTavern.", |
| 2654 | 2654 | "Upscale by": "Множитель апскейлинга", |
| 2655 | 2655 | "Hires steps (2nd pass)": "Кол-во шагов Hires (на втором проходе)", |
| 2656 | + "Disallow embedded media from other domains in chat messages": "Запретить медиафайлы со сторонних доменов в сообщениях чата.", | |
| 2657 | + "Add Background": "Добавить фон", | |
| 2658 | + "Add a new background": "Добавить новый фон", | |
| 2659 | + "Backgrounds": "Фоны", | |
| 2660 | + "ext_regex_debugger_desc": "Дополнительные инструменты отладки", | |
| 2661 | + "ext_regex_debugger": "Отладка", | |
| 2662 | + "ext_regex_presets": "Пресеты", | |
| 2663 | + "ext_regex_presets_desc": "Переключайтесь между разными группами активных рег. выражений.", | |
| 2664 | + "ext_regex_preset_create": "Создать новый пресет", | |
| 2665 | + "ext_regex_preset_update": "Обновить текущий пресет", | |
| 2666 | + "ext_regex_preset_apply": "Перезагрузить текущий пресет", | |
| 2667 | + "ext_regex_preset_delete": "Удалить текущий пресет", | |
| 2668 | + "[No presets saved]": "[Пресет не выбран]", | |
| 2669 | + "Enter a name for the new regex preset:": "Введите имя нового пресета:", | |
| 2670 | + "ext_regex_debugger_active_rules": "Активные скрипты", | |
| 2671 | + "No regex rules found.": "Скриптов не найдено.", | |
| 2672 | + "ext_regex_debugger_testing_area": "Зона тестирования", | |
| 2673 | + "ext_regex_debugger_run_test": "Протестировать", | |
| 2674 | + "ext_regex_debugger_display_replace": "Заменить", | |
| 2675 | + "ext_regex_debugger_display_highlight": "Посдветить", | |
| 2676 | + "ext_regex_debugger_render_text": "Отображать как текст", | |
| 2677 | + "ext_regex_debugger_render_message": "Отображать как сообщение", | |
| 2678 | + "ext_regex_debugger_save_order": "Сохранить порядок", | |
| 2679 | + "ext_regex_debugger_save_order_help": "Сохранить текущий порядок скриптов", | |
| 2680 | + "Regex script order saved!": "Порядок сохранён!", | |
| 2681 | + "ext_regex_debugger_raw_input": "Исходный текст", | |
| 2682 | + "ext_regex_debugger_step_by_step": "Пошаговая трансформация", | |
| 2683 | + "ext_regex_debugger_final_output": "Итоговый результат", | |
| 2684 | + "Expand view": "Развернуть", | |
| 2685 | + "Global Rules": "Глобальные скрипты", | |
| 2686 | + "Scoped Rules": "Локальные скрипты", | |
| 2687 | + "Edit Rule": "Редактировать скрипт", | |
| 2688 | + "Global": "Глобальный", | |
| 2689 | + "Scoped": "Локальный", | |
| 2690 | + "Captured:": "Зафиксировано:", | |
| 2691 | + "Added:": "Добавлено:", | |
| 2692 | + "Removed:": "Удалено:", | |
| 2693 | + "Total Captured:": "Всего зафиксировано:", | |
| 2694 | + "Total Added:": "Всего добавлено:", | |
| 2695 | + "Total Removed:": "Всего удалено:", | |
| 2696 | + "ext_regex_debugger_run_test_help": "Прогнать текст через данный набор скриптов", | |
| 2697 | + "After:": "После:", | |
| 2698 | + "Are you sure you want to delete this regex preset?": "Точно хотите удалить этот пресет?", | |
| 2699 | + "Regex preset updated": "Пресет обновлён", | |
| 2700 | + "Regex preset saved": "Пресет сохранён", | |
| 2701 | + "Regex preset deleted": "Пресет удалён", | |
| 2702 | + "Enhance": "Улучшить", | |
| 2703 | + "Enables prompt enhancing (passes prompts through an LLM to add detail).": "Включить улучшение промпта (дополнительно прогоняет промпт через LLM, чтобы добавить больше деталей)", | |
| 2704 | + "Use ADetailer (Face)": "Использовать ADetailer (для лиц)", | |
| 2705 | + "You can find your API key in the Stability AI dashboard.": "Ключ от API можно найти на дашборде на сайте Stability AI", | |
| 2706 | + "sd_stability_style_preset": "Стиль", | |
| 2707 | + "Avoid spending Anlas": "Стараться не тратить Anlas", | |
| 2708 | + "Automatically adjust generation parameters to ensure free image generations.": "Автоматически подгонять параметры таким образом, чтобы генерация получалась бесплатной.", | |
| 2709 | + "View my Anlas": "Узнать баланс Anlas", | |
| 2710 | + "Hint: Save an API key in the NovelAI API settings to use it here.": "Совет: сохраните API-ключ в настройках API NovelAI, чтобы автоматически подтянуть его сюда.", | |
| 2711 | + "Hint: Save an API key in the Hugging Face (Text Completion) API settings to use it here.": "Совет: сохраните API-ключ в настройках API HuggingFace (раздел Text Completion), чтобы автоматически подтянуть его сюда.", | |
| 2712 | + "Model ID": "Идентификатор модели", | |
| 2713 | + "<Enter Model ID above>": "<Введите идентификатор в поле выше>", | |
| 2714 | + "Hint: Save an API key in the NanoGPT (Chat Completion) API settings to use it here.": "Совет: сохраните API-ключ в настройках API NanoGPT (раздел Chat Completion), чтобы автоматически подтянуть его сюда.", | |
| 2715 | + "Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.": "Совет: сохраните API-ключ в настройках API Electron Hub (раздел Chat Completion), чтобы автоматически подтянуть его сюда.", | |
| 2716 | + "sd_drawthings_auth_txt": " запускайте DrawThings только со включенным в UI флажком HTTP API! Сервер должен быть доступен с хоста, на котором запущена SillyTavern.", | |
| 2717 | + "Send me a picture of:": "Пришли мне...", | |
| 2718 | + "sd_Yourself": "Свою фотографию", | |
| 2719 | + "sd_Your_Face": "Своё лицо", | |
| 2720 | + "sd_Me": "Мою фотографию", | |
| 2721 | + "sd_The_Whole_Story": "Изображение всей истории", | |
| 2722 | + "sd_The_Last_Message": "Изображение посл. сообщения", | |
| 2723 | + "sd_Raw_Last_Message": "Изображение посл. сообщения до обработки", | |
| 2724 | + "sd_Background": "Фотографию фона", | |
| 2725 | + "ext_regex_title": "Регулярные выражения", | |
| 2726 | + "ext_sum_title": "Саммари (пересказ)", | |
| 2727 | + "Image Generation": "Генерация изображений", | |
| 2728 | + "ext_translate_title": "Перевод чата", | |
| 2729 | + "Select TTS Provider": "Выберите TTS-движок", | |
| 2730 | + "tts_enabled": "Включить", | |
| 2731 | + "Narrate user messages": "Озвучивать сообщения пользователя", | |
| 2732 | + "Auto Generation": "Автоматическая генерация", | |
| 2733 | + "Narrate by paragraphs (when streaming)": "Озвучивать по параграфу за раз (при вкл. стриминге)", | |
| 2734 | + "Narrate by paragraphs (when not streaming)": "Озвучивать по параграфу за раз (при выкл. стриминге)", | |
| 2735 | + "Only narrate quotes": "Озвучивать только текст \"в кавычках\"", | |
| 2736 | + "Ignore text, even quotes, inside asterisk": "Не озвучивать *текст, даже \"в кавычках\", если он внутри звёздочек*", | |
| 2737 | + "Narrate only the translated text": "Озвучивать только переведённый текст", | |
| 2738 | + "Skip codeblocks": "Пропускать блоки кода", | |
| 2739 | + "Skip tagged blocks": "Пропускать блоки с <тегами>", | |
| 2740 | + "Pass Asterisks to TTS Engine": "Передавать звёздочки TTS-движку", | |
| 2741 | + "Different voices for quotes and text inside asterisks": "Разные голоса для \"кавычек\", *звёздочек* и остального текста.", | |
| 2742 | + "Requires auto generation to be enabled.": "Работает только при включенной функции \"Автоматическая генерация\"", | |
| 2743 | + "tts_refresh": "Обновить", | |
| 2744 | + "Uses the voices provided by your operating system": "Используем стандартные голоса вашей ОС", | |
| 2745 | + "Your browser or operating system doesn't support speech synthesis": "Ваш браузер либо ОС не поддерживают синтез речи", | |
| 2746 | + "Works best when: Pass Asterisks to TTS Engine is enabled, and both Only narrate quotes and Ignore *text, even 'quotes', inside asterisks* are disabled.": "Работает лучше всего, когда: одновременно включена опция \"Передавать звёздочки TTS-движку\", а также отключены обе опции: \"Не озвучивать *текст, даже \"в кавычках\", если он внутри звёздочек*\", и \"Озвучивать только текст \"в кавычках\"\"", | |
| 2747 | + "Available voices": "Доступные голоса" | |
| 2656 | 2748 | } |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "консолі розробника Anthropic", |
| 370 | 370 | "Claude Model": "Модель Claude", |
| 371 | 371 | "Window AI Model": "Модель Window AI", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Сортування моделі OpenRouter", |
| 373 | 373 | "Alphabetically": "За алфавітом", |
| 374 | 374 | "Price": "Ціна (найдешевша)", |
| 375 | 375 | "Context Size": "Розмір контексту", |
| @@ -369,7 +369,7 @@ | ||
| 369 | 369 | "Anthropic's developer console": "developer console của Anthropic", |
| 370 | 370 | "Claude Model": "Model Claude", |
| 371 | 371 | "Window AI Model": "Model Window AI", |
| 372 | 372 | "OpenRouter Model OrderSorting": "Sắp xếp model OpenRouter", |
| 373 | 373 | "Alphabetically": "Theo thứ tự bảng chữ cái", |
| 374 | 374 | "Price": "Giá (rẻ nhất)", |
| 375 | 375 | "Context Size": "Kích thước bối cảnh", |
| @@ -452,7 +452,7 @@ | ||
| 452 | 452 | "Claude Model": "Claude 模型", |
| 453 | 453 | "Allow fallback routes Description": "如果所选模型无法响应您的请求,则自动选择备用模型。", |
| 454 | 454 | "Allow fallback models": "允许后备模型", |
| 455 | 455 | "OpenRouter Model OrderSorting": "OpenRouter 模型顺序", |
| 456 | 456 | "Alphabetically": "按字母顺序", |
| 457 | 457 | "Price": "价格(最便宜)", |
| 458 | 458 | "Context Size": "上下文长度", |
| @@ -1479,7 +1479,7 @@ | ||
| 1479 | 1479 | "Image Captioning": "图像描述", |
| 1480 | 1480 | "Source": "来源", |
| 1481 | 1481 | "Local": "本地", |
| 1482 | - "Multimodal (OpenAI / Anthropic / llama / Google)": "多模态(OpenAI / Anthropic / llama / Google)", | |
| 1482 | + "Multimodal": "多模态", | |
| 1483 | 1483 | "Extras": "更多", |
| 1484 | 1484 | "Horde": "Horde", |
| 1485 | 1485 | "API": "API", |
| @@ -1646,8 +1646,9 @@ | ||
| 1646 | 1646 | "macro for manual injection)": "宏用于手动注入)", |
| 1647 | 1647 | "Color": "颜色", |
| 1648 | 1648 | "Only apply color as accent": "仅应用颜色作为强调", |
| 1649 | 1649 | "ext_regex_new_global_script_desc": "新增「全局」正规表达式正则表达式", |
| 1650 | 1650 | "ext_regex_new_scoped_script_desc": "新增「局部」正规表达式正则表达式", |
| 1651 | + "ext_regex_new_preset_script_desc": "新增「预设」正则表达式", | |
| 1651 | 1652 | "ext_regex_debugger_active_rules": "激活的规则", |
| 1652 | 1653 | "ext_regex_debugger_save_order": "保存此顺序", |
| 1653 | 1654 | "ext_regex_debugger_testing_area": "测试区域", |
| @@ -1668,6 +1669,7 @@ | ||
| 1668 | 1669 | "ext_regex_preset_delete": "删除当前预设", |
| 1669 | 1670 | "ext_regex_new_global_script": "新建全局正则", |
| 1670 | 1671 | "ext_regex_new_scoped_script": "新建局部正则", |
| 1672 | + "ext_regex_new_preset_script": "新建预设正则", | |
| 1671 | 1673 | "ext_regex_import_script": "导入正则", |
| 1672 | 1674 | "ext_regex_bulk_edit": "批量编辑", |
| 1673 | 1675 | "ext_regex_debugger_desc": "高级正则调试工具", |
| @@ -1677,9 +1679,13 @@ | ||
| 1677 | 1679 | "ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中", |
| 1678 | 1680 | "No scripts found": "没有找到脚本", |
| 1679 | 1681 | "ext_regex_scoped_scripts": "局部正则脚本", |
| 1682 | + "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中", | |
| 1683 | + "ext_regex_preset_scripts": "预设正则脚本", | |
| 1684 | + "ext_regex_preset_scripts_desc": "只影响当前预设,保存在预设中", | |
| 1680 | 1685 | "ext_regex_disallow_scoped": "不允许使用局部正则", |
| 1681 | 1686 | "ext_regex_allow_scoped": "允许使用局部正则", |
| 1682 | 1687 | "ext_regex_scoped_scripts_descext_regex_disallow_preset": "只影响当前角色,保存在角色卡片中不允许使用预设正则", |
| 1688 | + "ext_regex_allow_preset": "允许使用预设正则", | |
| 1683 | 1689 | "Regex Editor": "正则表达式编辑器", |
| 1684 | 1690 | "Test Mode": "测试模式", |
| 1685 | 1691 | "ext_regex_desc": "“正则”是一个使用“正则表达式”来查找/替换字符串的工具。如果您想了解更多信息,请点击标题旁边的“?”。", |
| @@ -1728,10 +1734,16 @@ | ||
| 1728 | 1734 | "ext_regex_disable_script": "禁用脚本", |
| 1729 | 1735 | "ext_regex_enable_script": "启用脚本", |
| 1730 | 1736 | "ext_regex_edit_script": "编辑脚本", |
| 1731 | 1737 | "ext_regex_move_to_global": "移至全局脚本移至全局", |
| 1732 | 1738 | "ext_regex_move_to_scoped": "移至作用域脚本移至局部", |
| 1739 | + "ext_regex_move_to_preset": "移至预设", | |
| 1733 | 1740 | "ext_regex_export_script": "导出脚本", |
| 1734 | 1741 | "ext_regex_delete_script": "删除脚本", |
| 1742 | + "This preset has embedded regex script(s).": "此预设包含内置正则脚本。", | |
| 1743 | + "Preset '${0}' contains enabled regex scripts": "预设 '${0}' 包含被启用的正则脚本", | |
| 1744 | + "Reload the chat for regex to take effect": "重新加载聊天以使正则生效", | |
| 1745 | + "Click here to reload immediately": "点击此处立即重新加载", | |
| 1746 | + "If you want to do it later, select \"Regex\" from the extensions menu.": "您可稍后从扩展菜单中的(Regex)启用。", | |
| 1735 | 1747 | "Trigger Stable Diffusion": "触发Stable Diffusion", |
| 1736 | 1748 | "Abort current image generation task": "中止当前图像生成", |
| 1737 | 1749 | "Stop Image Generation": "停止图像生成", |
| @@ -368,7 +368,7 @@ | ||
| 368 | 368 | "Anthropic's developer console": "Anthropic 的開發者控制台", |
| 369 | 369 | "Claude Model": "Claude 模型", |
| 370 | 370 | "Window AI Model": "Window AI 模型", |
| 371 | 371 | "OpenRouter Model OrderSorting": "模型順序", |
| 372 | 372 | "Alphabetically": "按字母順序", |
| 373 | 373 | "Price": "價格(最便宜的)", |
| 374 | 374 | "Context Size": "上下文長度", |
| @@ -1674,7 +1674,7 @@ | ||
| 1674 | 1674 | "Message Template": "訊息範本", |
| 1675 | 1675 | "Model ID": "模型 ID", |
| 1676 | 1676 | "mui_reset": "重設", |
| 1677 | - "Multimodal (OpenAI / Anthropic / llama / Google)": "多模態(OpenAI/Anthropic/llama/Google)", | |
| 1677 | + "Multimodal": "多模態", | |
| 1678 | 1678 | "must be set in Tabby's config.yml to switch models.": "須在 Tabby's config.yml 中設定以切換模型。", |
| 1679 | 1679 | "Names as Stop Strings": "將名稱用作停止字串", |
| 1680 | 1680 | "Never": "從不", |
| @@ -25,6 +25,16 @@ | ||
| 25 | 25 | "src": "img/apple-icon-144x144.png", |
| 26 | 26 | "sizes": "144x144", |
| 27 | 27 | "type": "image/png" |
| 28 | + }, | |
| 29 | + { | |
| 30 | + "src": "img/apple-icon-192x192.png", | |
| 31 | + "sizes": "192x192", | |
| 32 | + "type": "image/png" | |
| 33 | + }, | |
| 34 | + { | |
| 35 | + "src": "img/apple-icon-512x512.png", | |
| 36 | + "sizes": "512x512", | |
| 37 | + "type": "image/png" | |
| 28 | 38 | } |
| 29 | 39 | ] |
| 30 | 40 | } |
| @@ -43,10 +43,11 @@ import { | ||
| 43 | 43 | importEmbeddedWorldInfo, |
| 44 | 44 | checkEmbeddedWorld, |
| 45 | 45 | setWorldInfoButtonClass, |
| 46 | - importWorldInfo, | |
| 47 | 46 | wi_anchor_position, |
| 48 | 47 | world_info_include_names, |
| 49 | 48 | initWorldInfo, |
| 49 | + charUpdatePrimaryWorld, | |
| 50 | + charSetAuxWorlds, | |
| 50 | 51 | } from './scripts/world-info.js'; |
| 51 | 52 | |
| 52 | 53 | import { |
| @@ -175,6 +176,9 @@ import { | ||
| 175 | 176 | localizePagination, |
| 176 | 177 | renderPaginationDropdown, |
| 177 | 178 | paginationDropdownChangeHandler, |
| 179 | + importFromExternalUrl, | |
| 180 | + shiftUpByOne, | |
| 181 | + shiftDownByOne, | |
| 178 | 182 | } from './scripts/utils.js'; |
| 179 | 183 | import { debounce_timeout, GENERATION_TYPE_TRIGGERS, IGNORE_SYMBOL, inject_ids } from './scripts/constants.js'; |
| 180 | 184 | |
| @@ -267,6 +271,8 @@ import { clearItemizedPrompts, deleteItemizedPrompts, findItemizedPromptSet, ini | ||
| 267 | 271 | import { getSystemMessageByType, initSystemMessages, SAFETY_CHAT, sendSystemMessage, system_message_types, system_messages } from './scripts/system-messages.js'; |
| 268 | 272 | import { event_types, eventSource } from './scripts/events.js'; |
| 269 | 273 | import { initAccessibility } from './scripts/a11y.js'; |
| 274 | +import { applyStreamFadeIn } from './scripts/util/stream-fadein.js'; | |
| 275 | +import { initDomHandlers } from './scripts/dom-handlers.js'; | |
| 270 | 276 | |
| 271 | 277 | // API OBJECT FOR EXTERNAL WIRING |
| 272 | 278 | globalThis.SillyTavern = { |
| @@ -390,7 +396,7 @@ let isExportPopupOpen = false; | ||
| 390 | 396 | |
| 391 | 397 | // Saved here for performance reasons |
| 392 | 398 | const messageTemplate = $('#message_template .mes'); |
| 393 | 399 | export const chatElement = $('#chat'); |
| 394 | 400 | |
| 395 | 401 | let dialogueResolve = null; |
| 396 | 402 | let dialogueCloseStop = false; |
| @@ -403,6 +409,7 @@ let fav_ch_checked = false; | ||
| 403 | 409 | let scrollLock = false; |
| 404 | 410 | export let abortStatusCheck = new AbortController(); |
| 405 | 411 | export let charDragDropHandler = null; |
| 412 | +export let chatDragDropHandler = null; | |
| 406 | 413 | |
| 407 | 414 | /** @type {debounce_timeout} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */ |
| 408 | 415 | export const DEFAULT_SAVE_EDIT_TIMEOUT = debounce_timeout.relaxed; |
| @@ -637,6 +644,7 @@ async function firstLoadInit() { | ||
| 637 | 644 | |
| 638 | 645 | showLoader(); |
| 639 | 646 | registerPromptManagerMigration(); |
| 647 | + initDomHandlers(); | |
| 640 | 648 | initStandaloneMode(); |
| 641 | 649 | initLibraryShims(); |
| 642 | 650 | addShowdownPatch(showdown); |
| @@ -1344,7 +1352,7 @@ export async function replaceCurrentChat() { | ||
| 1344 | 1352 | } |
| 1345 | 1353 | |
| 1346 | 1354 | export async function showMoreMessages(messagesToLoad = null) { |
| 1347 | 1355 | const firstDisplayedMesId = $('#chat')chatElement.children('.mes').first().attr('mesid'); |
| 1348 | 1356 | let messageId = Number(firstDisplayedMesId); |
| 1349 | 1357 | let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER; |
| 1350 | 1358 | |
| @@ -1355,7 +1363,7 @@ export async function showMoreMessages(messagesToLoad = null) { | ||
| 1355 | 1363 | } |
| 1356 | 1364 | |
| 1357 | 1365 | console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); |
| 1358 | 1366 | const prevHeight = $('#chat')chatElement.prop('scrollHeight'); |
| 1359 | 1367 | const isButtonInView = isElementInViewport($('#show_more_messages')[0]); |
| 1360 | 1368 | |
| 1361 | 1369 | while (messageId > 0 && count > 0) { |
| @@ -1370,8 +1378,8 @@ export async function showMoreMessages(messagesToLoad = null) { | ||
| 1370 | 1378 | } |
| 1371 | 1379 | |
| 1372 | 1380 | if (isButtonInView) { |
| 1373 | 1381 | const newHeight = $('#chat')chatElement.prop('scrollHeight'); |
| 1374 | 1382 | $('#chat')chatElement.scrollTop(newHeight - prevHeight); |
| 1375 | 1383 | } |
| 1376 | 1384 | |
| 1377 | 1385 | applyStylePins(); |
| @@ -1384,7 +1392,7 @@ export async function printMessages() { | ||
| 1384 | 1392 | |
| 1385 | 1393 | if (chat.length > count) { |
| 1386 | 1394 | startIndex = chat.length - count; |
| 1387 | 1395 | $('#chat')chatElement.append('<div id="show_more_messages">Show more messages</div>'); |
| 1388 | 1396 | } |
| 1389 | 1397 | |
| 1390 | 1398 | for (let i = startIndex; i < chat.length; i++) { |
| @@ -1407,8 +1415,8 @@ export async function printMessages() { | ||
| 1407 | 1415 | } |
| 1408 | 1416 | } |
| 1409 | 1417 | |
| 1410 | 1418 | $chatElement.find('#chat .mes').removeClass('last_mes'); |
| 1411 | 1419 | $chatElement.find('#chat .mes').last().addClass('last_mes'); |
| 1412 | 1420 | hideSwipeButtons(); |
| 1413 | 1421 | showSwipeButtons(); |
| 1414 | 1422 | scrollChatToBottom(); |
| @@ -1441,7 +1449,7 @@ export async function clearChat() { | ||
| 1441 | 1449 | if (is_delete_mode) { |
| 1442 | 1450 | $('#dialogue_del_mes_cancel').trigger('click'); |
| 1443 | 1451 | } |
| 1444 | 1452 | $('#chat')chatElement.children().remove(); |
| 1445 | 1453 | if ($('.zoomed_avatar[forChar]').length) { |
| 1446 | 1454 | console.debug('saw avatars to remove'); |
| 1447 | 1455 | $('.zoomed_avatar[forChar]').remove(); |
| @@ -1453,7 +1461,7 @@ export async function clearChat() { | ||
| 1453 | 1461 | |
| 1454 | 1462 | export async function deleteLastMessage() { |
| 1455 | 1463 | chat.length = chat.length - 1; |
| 1456 | 1464 | $('#chat')chatElement.children('.mes').last().remove(); |
| 1457 | 1465 | await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); |
| 1458 | 1466 | } |
| 1459 | 1467 | |
| @@ -1796,7 +1804,7 @@ function getMessageFromTemplate({ | ||
| 1796 | 1804 | * @param {boolean} [options.rerenderMessage=true] Whether to re-render the message content (inside <c>.mes_text</c>) |
| 1797 | 1805 | */ |
| 1798 | 1806 | export function updateMessageBlock(messageId, message, { rerenderMessage = true } = {}) { |
| 1799 | 1807 | const messageElement = $chatElement.find(`#chat [mesid="${messageId}"]`); |
| 1800 | 1808 | if (rerenderMessage) { |
| 1801 | 1809 | const text = message?.extra?.display_text ?? message.mes; |
| 1802 | 1810 | messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false)); |
| @@ -1818,7 +1826,7 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { | ||
| 1818 | 1826 | // Add image to message |
| 1819 | 1827 | if (mes.extra?.image) { |
| 1820 | 1828 | const container = messageElement.find('.mes_img_container'); |
| 1821 | 1829 | const chatHeight = $('#chat')chatElement.prop('scrollHeight'); |
| 1822 | 1830 | const image = messageElement.find('.mes_img'); |
| 1823 | 1831 | const text = messageElement.find('.mes_text'); |
| 1824 | 1832 | const isInline = !!mes.extra?.inline_image; |
| @@ -1826,10 +1834,10 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { | ||
| 1826 | 1834 | if (!adjustScroll) { |
| 1827 | 1835 | return; |
| 1828 | 1836 | } |
| 1829 | 1837 | const scrollPosition = $('#chat')chatElement.scrollTop(); |
| 1830 | 1838 | const newChatHeight = $('#chat')chatElement.prop('scrollHeight'); |
| 1831 | 1839 | const diff = newChatHeight - chatHeight; |
| 1832 | 1840 | $('#chat')chatElement.scrollTop(scrollPosition + diff); |
| 1833 | 1841 | }; |
| 1834 | 1842 | image.off('load').on('load', function () { |
| 1835 | 1843 | image.removeAttr('alt'); |
| @@ -1876,16 +1884,16 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { | ||
| 1876 | 1884 | const container = $('#message_video_template .mes_video_container').clone(); |
| 1877 | 1885 | messageElement.find('.mes_video_container').remove(); |
| 1878 | 1886 | messageElement.find('.mes_block').append(container); |
| 1879 | 1887 | const chatHeight = $('#chat')chatElement.prop('scrollHeight'); |
| 1880 | 1888 | const video = container.find('.mes_video'); |
| 1881 | 1889 | video.off('loadedmetadata').on('loadedmetadata', function () { |
| 1882 | 1890 | if (!adjustScroll) { |
| 1883 | 1891 | return; |
| 1884 | 1892 | } |
| 1885 | 1893 | const scrollPosition = $('#chat')chatElement.scrollTop(); |
| 1886 | 1894 | const newChatHeight = $('#chat')chatElement.prop('scrollHeight'); |
| 1887 | 1895 | const diff = newChatHeight - chatHeight; |
| 1888 | 1896 | $('#chat')chatElement.scrollTop(scrollPosition + diff); |
| 1889 | 1897 | }); |
| 1890 | 1898 | |
| 1891 | 1899 | video.attr('src', mes.extra?.video); |
| @@ -1927,7 +1935,7 @@ export function addCopyToCodeBlocks(messageElement) { | ||
| 1927 | 1935 | e.stopPropagation(); |
| 1928 | 1936 | }); |
| 1929 | 1937 | copyButton.addEventListener('pointerup', async function () { |
| 1930 | 1938 | const text = codeBlocks.get(i).innerTexttextContent; |
| 1931 | 1939 | await copyText(text); |
| 1932 | 1940 | toastr.info(t`Copied!`, '', { timeOut: 2000 }); |
| 1933 | 1941 | }); |
| @@ -2040,7 +2048,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2040 | 2048 | // Callers push the new message to chat before calling addOneMessage |
| 2041 | 2049 | const newMessageId = typeof forceId == 'number' ? forceId : chat.length - 1; |
| 2042 | 2050 | |
| 2043 | 2051 | const newMessage = $chatElement.find(`#chat [mesid="${newMessageId}"]`); |
| 2044 | 2052 | const isSmallSys = mes?.extra?.isSmallSys; |
| 2045 | 2053 | |
| 2046 | 2054 | if (isSmallSys === true) { |
| @@ -2102,8 +2110,8 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2102 | 2110 | } |
| 2103 | 2111 | |
| 2104 | 2112 | if (showSwipes) { |
| 2105 | 2113 | $chatElement.find('#chat .mes').last().addClass('last_mes'); |
| 2106 | 2114 | $chatElement.find('#chat .mes').eq(-2).removeClass('last_mes'); |
| 2107 | 2115 | hideSwipeButtons(); |
| 2108 | 2116 | showSwipeButtons(); |
| 2109 | 2117 | } |
| @@ -2252,6 +2260,33 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re | ||
| 2252 | 2260 | } |
| 2253 | 2261 | }; |
| 2254 | 2262 | |
| 2263 | + const getNotCharValue = () => { | |
| 2264 | + const currentUser = _name1 ?? name1; | |
| 2265 | + const currentSpeaker = _name2 ?? name2; | |
| 2266 | + | |
| 2267 | + // Single character chat | |
| 2268 | + if (!selected_group) { | |
| 2269 | + return currentUser; | |
| 2270 | + } | |
| 2271 | + | |
| 2272 | + // Group chat | |
| 2273 | + const members = groups.find(x => x.id === selected_group)?.members; | |
| 2274 | + | |
| 2275 | + if (!Array.isArray(members)) { | |
| 2276 | + return currentUser; | |
| 2277 | + } | |
| 2278 | + | |
| 2279 | + const memberNames = members | |
| 2280 | + .map(m => characters.find(c => c.avatar === m)?.name) | |
| 2281 | + .filter(Boolean); // Filter out any null/undefined names | |
| 2282 | + | |
| 2283 | + // Filter out the current speaker and add the user | |
| 2284 | + const otherMembers = memberNames.filter(name => name !== currentSpeaker); | |
| 2285 | + otherMembers.push(currentUser); | |
| 2286 | + | |
| 2287 | + return otherMembers.join(', '); | |
| 2288 | + }; | |
| 2289 | + | |
| 2255 | 2290 | if (_replaceCharacterCard) { |
| 2256 | 2291 | const fields = getCharacterCardFields(); |
| 2257 | 2292 | environment.charPrompt = fields.system || ''; |
| @@ -2281,6 +2316,7 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re | ||
| 2281 | 2316 | environment.char = _name2 ?? name2; |
| 2282 | 2317 | environment.group = environment.charIfNotGroup = getGroupValue(true); |
| 2283 | 2318 | environment.groupNotMuted = getGroupValue(false); |
| 2319 | + environment.notChar = getNotCharValue(); | |
| 2284 | 2320 | environment.model = getGeneratingModel(); |
| 2285 | 2321 | |
| 2286 | 2322 | if (additionalMacro && typeof additionalMacro === 'object') { |
| @@ -2905,7 +2941,11 @@ class StreamingProcessor { | ||
| 2905 | 2941 | false, |
| 2906 | 2942 | ); |
| 2907 | 2943 | if (this.messageTextDom instanceof HTMLElement) { |
| 2908 | - this.messageTextDom.innerHTML = formattedText; | |
| 2944 | + if (power_user.stream_fade_in) { | |
| 2945 | + applyStreamFadeIn(this.messageTextDom, formattedText); | |
| 2946 | + } else { | |
| 2947 | + this.messageTextDom.innerHTML = formattedText; | |
| 2948 | + } | |
| 2909 | 2949 | } |
| 2910 | 2950 | |
| 2911 | 2951 | const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount, this.reasoningHandler.getDuration(), this.timeToFirstToken); |
| @@ -2925,7 +2965,7 @@ class StreamingProcessor { | ||
| 2925 | 2965 | async onFinishStreaming(messageId, text) { |
| 2926 | 2966 | this.markUIGenStopped(); |
| 2927 | 2967 | await this.onProgressStreaming(messageId, text, true); |
| 2928 | 2968 | addCopyToCodeBlocks($chatElement.find(`#chat .mes[mesid="${messageId}"]`)); |
| 2929 | 2969 | |
| 2930 | 2970 | await this.reasoningHandler.finish(messageId); |
| 2931 | 2971 | |
| @@ -3164,6 +3204,11 @@ export async function generateRaw({ prompt = '', api = null, instructOverride = | ||
| 3164 | 3204 | // construct final prompt from the input. Can either be a string or an array of chat-style messages. |
| 3165 | 3205 | prompt = createRawPrompt(prompt, api, instructOverride, quietToLoud, systemPrompt, prefill); |
| 3166 | 3206 | |
| 3207 | + // Allow extensions to stop generation before it happens | |
| 3208 | + const eventAbortController = new AbortController(); | |
| 3209 | + const abortHook = () => eventAbortController.abort(new Error('Cancelled by extension')); | |
| 3210 | + eventSource.on(event_types.GENERATION_STOPPED, abortHook); | |
| 3211 | + | |
| 3167 | 3212 | try { |
| 3168 | 3213 | if (responseLengthCustomized) { |
| 3169 | 3214 | TempResponseLength.save(api, responseLength); |
| @@ -3171,6 +3216,23 @@ export async function generateRaw({ prompt = '', api = null, instructOverride = | ||
| 3171 | 3216 | /** @type {object|any[]} */ |
| 3172 | 3217 | let generateData = {}; |
| 3173 | 3218 | |
| 3219 | + // Allow extensions to modify the prompt before generation | |
| 3220 | + // 1. for text completion | |
| 3221 | + if (typeof prompt === 'string') { | |
| 3222 | + const eventData = { prompt: prompt, dryRun: false }; | |
| 3223 | + await eventSource.emit(event_types.GENERATE_AFTER_COMBINE_PROMPTS, eventData); | |
| 3224 | + prompt = eventData.prompt; | |
| 3225 | + } | |
| 3226 | + // 2. for chat completion | |
| 3227 | + if (Array.isArray(prompt)) { | |
| 3228 | + const eventData = { chat: prompt, dryRun: false }; | |
| 3229 | + await eventSource.emit(event_types.CHAT_COMPLETION_PROMPT_READY, eventData); | |
| 3230 | + prompt = eventData.chat; | |
| 3231 | + } | |
| 3232 | + | |
| 3233 | + // Check if the generation was aborted during the event | |
| 3234 | + eventAbortController.signal.throwIfAborted(); | |
| 3235 | + | |
| 3174 | 3236 | switch (api) { |
| 3175 | 3237 | case 'kobold': |
| 3176 | 3238 | case 'koboldhorde': |
| @@ -3250,6 +3312,7 @@ export async function generateRaw({ prompt = '', api = null, instructOverride = | ||
| 3250 | 3312 | |
| 3251 | 3313 | return message; |
| 3252 | 3314 | } finally { |
| 3315 | + eventSource.removeListener(event_types.GENERATION_STOPPED, abortHook); | |
| 3253 | 3316 | if (responseLengthCustomized && TempResponseLength.isCustomized()) { |
| 3254 | 3317 | TempResponseLength.restore(api); |
| 3255 | 3318 | TempResponseLength.removeEventHook(api, eventHook); |
| @@ -3353,7 +3416,7 @@ class TempResponseLength { | ||
| 3353 | 3416 | */ |
| 3354 | 3417 | function removeLastMessage() { |
| 3355 | 3418 | return new Promise((resolve) => { |
| 3356 | 3419 | const lastMes = $('#chat')chatElement.children('.mes').last(); |
| 3357 | 3420 | if (lastMes.length === 0) { |
| 3358 | 3421 | return resolve(); |
| 3359 | 3422 | } |
| @@ -3575,18 +3638,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 3575 | 3638 | creatorNotes, |
| 3576 | 3639 | } = getCharacterCardFields(); |
| 3577 | 3640 | |
| 3578 | - if (main_api !== 'openai') { | |
| 3579 | - if (power_user.sysprompt.enabled) { | |
| 3580 | - system = power_user.prefer_character_prompt && system | |
| 3581 | - ? substituteParams(system, name1, name2, (power_user.sysprompt.content ?? '')) | |
| 3582 | - : baseChatReplace(power_user.sysprompt.content, name1, name2); | |
| 3583 | - system = isInstruct ? substituteParams(system, name1, name2, power_user.sysprompt.content) : system; | |
| 3584 | - } else { | |
| 3585 | - // Nullify if it's not enabled | |
| 3586 | - system = ''; | |
| 3587 | - } | |
| 3588 | - } | |
| 3589 | - | |
| 3590 | 3641 | // Depth prompt (character-specific A/N) |
| 3591 | 3642 | removeDepthPrompts(); |
| 3592 | 3643 | const groupDepthPrompts = getGroupDepthPrompts(selected_group, Number(this_chid)); |
| @@ -3717,12 +3768,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 3717 | 3768 | |
| 3718 | 3769 | let mesExamplesArray = parseMesExamples(mesExamples, isInstruct); |
| 3719 | 3770 | |
| 3720 | - ////////////////////////////////// | |
| 3721 | - // Extension added strings | |
| 3722 | 3771 | // Set non-WI AN |
| 3723 | 3772 | setFloatingPrompt(); |
| 3724 | - // Add persona description to prompt | |
| 3725 | - addPersonaDescriptionExtensionPrompt(); | |
| 3726 | 3773 | |
| 3727 | 3774 | // Add WI to prompt (and also inject WI to AN value via hijack) |
| 3728 | 3775 | // Make quiet prompt available for WIAN |
| @@ -3738,7 +3785,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 3738 | 3785 | creatorNotes: creatorNotes, |
| 3739 | 3786 | trigger: GENERATION_TYPE_TRIGGERS.includes(type) ? type : 'normal', |
| 3740 | 3787 | }; |
| 3741 | 3788 | const { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoExamples, worldInfoDepth, outletEntries } = await getWorldInfoPrompt(chatForWI, this_max_context, dryRun, globalScanData); |
| 3742 | 3789 | setExtensionPrompt(inject_ids.QUIET_PROMPT, '', extension_prompt_types.IN_PROMPT, 0, true); |
| 3743 | 3790 | |
| 3744 | 3791 | // Add message example WI |
| @@ -3770,17 +3817,38 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 3770 | 3817 | if (skipWIAN !== true) { |
| 3771 | 3818 | console.log('skipWIAN not active, adding WIAN'); |
| 3772 | 3819 | // Add all depth WI entries to prompt |
| 3773 | 3820 | flushWIDepthInjectionsflushWIInjections(); |
| 3774 | 3821 | if (Array.isArray(worldInfoDepth)) { |
| 3775 | 3822 | worldInfoDepth.forEach((e) => { |
| 3776 | 3823 | const joinedEntries = e.entries.join('\n'); |
| 3777 | 3824 | setExtensionPrompt(inject_ids.CUSTOM_WI_DEPTH_ROLE(e.depth, e.role), joinedEntries, extension_prompt_types.IN_CHAT, e.depth, false, e.role); |
| 3778 | 3825 | }); |
| 3779 | 3826 | } |
| 3827 | + if (outletEntries && typeof outletEntries === 'object' && Object.keys(outletEntries).length > 0) { | |
| 3828 | + Object.entries(outletEntries).forEach(([key, value]) => { | |
| 3829 | + setExtensionPrompt(inject_ids.CUSTOM_WI_OUTLET(key), value.join('\n'), extension_prompt_types.NONE, 0); | |
| 3830 | + }); | |
| 3831 | + } | |
| 3780 | 3832 | } else { |
| 3781 | 3833 | console.log('skipping WIAN'); |
| 3782 | 3834 | } |
| 3783 | 3835 | |
| 3836 | + // Add persona description to prompt | |
| 3837 | + addPersonaDescriptionExtensionPrompt(); | |
| 3838 | + | |
| 3839 | + // Prepare the system prompt for Text Completion APIs | |
| 3840 | + if (main_api !== 'openai') { | |
| 3841 | + if (power_user.sysprompt.enabled) { | |
| 3842 | + system = power_user.prefer_character_prompt && system | |
| 3843 | + ? substituteParams(system, name1, name2, (power_user.sysprompt.content ?? '')) | |
| 3844 | + : baseChatReplace(power_user.sysprompt.content, name1, name2); | |
| 3845 | + system = isInstruct ? substituteParams(system, name1, name2, power_user.sysprompt.content) : system; | |
| 3846 | + } else { | |
| 3847 | + // Nullify if it's not enabled | |
| 3848 | + system = ''; | |
| 3849 | + } | |
| 3850 | + } | |
| 3851 | + | |
| 3784 | 3852 | // Collect before / after story string injections |
| 3785 | 3853 | const beforeScenarioAnchor = await getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT); |
| 3786 | 3854 | const afterScenarioAnchor = await getExtensionPrompt(extension_prompt_types.IN_PROMPT); |
| @@ -3845,14 +3913,14 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 3845 | 3913 | // This operation will result in the injectedIndices indexes being off by one |
| 3846 | 3914 | coreChat.push({ mes: jailbreak, is_user: true }); |
| 3847 | 3915 | // Add +1 to the elements to correct for the new PHI/Jailbreak message. |
| 3848 | - injectedIndices.forEach((e, idx) => injectedIndices[idx] = e + 1); | |
| 3916 | + injectedIndices.forEach(shiftUpByOne); | |
| 3849 | 3917 | } |
| 3850 | 3918 | } |
| 3851 | 3919 | } |
| 3852 | 3920 | |
| 3853 | 3921 | let chat2 = []; |
| 3854 | 3922 | let continue_mag = ''; |
| 3855 | 3923 | constlet userMessageIndices = []; |
| 3856 | 3924 | const lastUserMessageIndex = coreChat.findLastIndex(x => x.is_user); |
| 3857 | 3925 | |
| 3858 | 3926 | for (let i = coreChat.length - 1, j = 0; i >= 0; i--, j++) { |
| @@ -3951,16 +4019,24 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 3951 | 4019 | // Only add the chat in context if past the greeting message |
| 3952 | 4020 | if (isContinue && (chat2.length > 1 || main_api === 'openai')) { |
| 3953 | 4021 | cyclePrompt = chat2.shift(); |
| 4022 | + // Adjust indices to account for the shift | |
| 4023 | + injectedIndices = injectedIndices.map(shiftDownByOne).filter(x => x >= 0); | |
| 4024 | + userMessageIndices = userMessageIndices.map(shiftDownByOne).filter(x => x >= 0); | |
| 3954 | 4025 | } |
| 3955 | 4026 | |
| 3956 | 4027 | // Collect enough messages to fill the context |
| 3957 | 4028 | let arrMes = new Array(chat2.length); |
| 3958 | 4029 | let tokenCount = await getMessagesTokenCount(); |
| 3959 | 4030 | let lastAddedIndex = -10; |
| 3960 | 4031 | |
| 3961 | 4032 | // Pre-allocate all injections first. |
| 3962 | 4033 | // If it doesn't fit - user shot himself in the foot |
| 3963 | 4034 | for (const index of injectedIndices) { |
| 4035 | + // not needed for OAI prompting | |
| 4036 | + if (main_api == 'openai') { | |
| 4037 | + break; | |
| 4038 | + } | |
| 4039 | + | |
| 3964 | 4040 | const item = chat2[index]; |
| 3965 | 4041 | |
| 3966 | 4042 | if (typeof item !== 'string') { |
| @@ -4394,7 +4470,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 4394 | 4470 | } |
| 4395 | 4471 | } |
| 4396 | 4472 | |
| 4397 | 4473 | await eventSource.emit(event_types.GENERATE_AFTER_DATA, generate_data, dryRun); |
| 4398 | 4474 | |
| 4399 | 4475 | if (dryRun) { |
| 4400 | 4476 | return Promise.resolve(); |
| @@ -4703,7 +4779,7 @@ export function stopGeneration() { | ||
| 4703 | 4779 | * @returns {Promise<number[]>} Array of indices where the extension prompts were injected |
| 4704 | 4780 | */ |
| 4705 | 4781 | async function doChatInject(messages, isContinue) { |
| 4706 | 4782 | const injectedIndicesinjectedMessages = []; |
| 4707 | 4783 | let totalInsertedMessages = 0; |
| 4708 | 4784 | messages.reverse(); |
| 4709 | 4785 | |
| @@ -4743,18 +4819,21 @@ async function doChatInject(messages, isContinue) { | ||
| 4743 | 4819 | const injectIdx = Math.min(depth + totalInsertedMessages, messages.length); |
| 4744 | 4820 | messages.splice(injectIdx, 0, ...roleMessages); |
| 4745 | 4821 | totalInsertedMessages += roleMessages.length; |
| 4746 | - injectedIndices.push(...Array.from({ length: roleMessages.length }, (_, i) => injectIdx + i)); | |
| 4822 | + injectedMessages.push(...roleMessages); | |
| 4747 | 4823 | } |
| 4748 | 4824 | } |
| 4749 | 4825 | |
| 4826 | + const injectedIndices = injectedMessages.map(msg => messages.indexOf(msg)); | |
| 4750 | 4827 | messages.reverse(); |
| 4751 | 4828 | return injectedIndices; |
| 4752 | 4829 | } |
| 4753 | 4830 | |
| 4754 | 4831 | function flushWIDepthInjectionsflushWIInjections() { |
| 4755 | - //prevent custom depth WI entries (which have unique random key names) from duplicating | |
| 4832 | + const depthPrefix = inject_ids.CUSTOM_WI_DEPTH; | |
| 4833 | + const outletPrefix = inject_ids.CUSTOM_WI_OUTLET(''); | |
| 4834 | + | |
| 4756 | 4835 | for (const key of Object.keys(extension_prompts)) { |
| 4757 | 4836 | if (key.startsWith(inject_idsdepthPrefix) || key.CUSTOM_WI_DEPTHstartsWith(outletPrefix)) { |
| 4758 | 4837 | delete extension_prompts[key]; |
| 4759 | 4838 | } |
| 4760 | 4839 | } |
| @@ -4775,7 +4854,7 @@ function unblockGeneration(type) { | ||
| 4775 | 4854 | showSwipeButtons(); |
| 4776 | 4855 | setGenerationProgress(0); |
| 4777 | 4856 | flushEphemeralStoppingStrings(); |
| 4778 | 4857 | flushWIDepthInjectionsflushWIInjections(); |
| 4779 | 4858 | } |
| 4780 | 4859 | |
| 4781 | 4860 | export function getNextMessageId(type) { |
| @@ -4977,6 +5056,8 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul | ||
| 4977 | 5056 | await populateFileAttachment(message); |
| 4978 | 5057 | statMesProcess(message, 'user', characters, this_chid, ''); |
| 4979 | 5058 | |
| 5059 | + chat_metadata['tainted'] = true; | |
| 5060 | + | |
| 4980 | 5061 | if (typeof insertAt === 'number' && insertAt >= 0 && insertAt <= chat.length) { |
| 4981 | 5062 | chat.splice(insertAt, 0, message); |
| 4982 | 5063 | await saveChatConditional(); |
| @@ -5110,18 +5191,18 @@ export async function duplicateCharacter() { | ||
| 5110 | 5191 | } |
| 5111 | 5192 | |
| 5112 | 5193 | function setInContextMessages(msgInContextCount, type) { |
| 5113 | 5194 | $chatElement.find('#chat .mes').removeClass('lastInContext'); |
| 5114 | 5195 | |
| 5115 | 5196 | if (type === 'swipe' || type === 'regenerate' || type === 'continue') { |
| 5116 | 5197 | msgInContextCount++; |
| 5117 | 5198 | } |
| 5118 | 5199 | |
| 5119 | 5200 | const lastMessageBlock = $chatElement.find('#chat .mes:not([is_system="true"])').eq(-msgInContextCount); |
| 5120 | 5201 | lastMessageBlock.addClass('lastInContext'); |
| 5121 | 5202 | |
| 5122 | 5203 | if (lastMessageBlock.length === 0) { |
| 5123 | 5204 | const firstMessageId = getFirstDisplayedMessageId(); |
| 5124 | 5205 | $chatElement.find(`#chat .mes[mesid="${firstMessageId}"`).addClass('lastInContext'); |
| 5125 | 5206 | } |
| 5126 | 5207 | |
| 5127 | 5208 | // Update last id to chat. No metadata save on purpose, gets hopefully saved via another call |
| @@ -5679,9 +5760,9 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title | ||
| 5679 | 5760 | chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); |
| 5680 | 5761 | } |
| 5681 | 5762 | const chat_id = (chat.length - 1); |
| 5682 | 5763 | !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type); |
| 5683 | 5764 | addOneMessage(chat[chat_id], { type: 'swipe' }); |
| 5684 | 5765 | !fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type); |
| 5685 | 5766 | } else { |
| 5686 | 5767 | chat[chat.length - 1]['mes'] = getMessage; |
| 5687 | 5768 | } |
| @@ -5703,9 +5784,9 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title | ||
| 5703 | 5784 | chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); |
| 5704 | 5785 | } |
| 5705 | 5786 | const chat_id = (chat.length - 1); |
| 5706 | 5787 | !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type); |
| 5707 | 5788 | addOneMessage(chat[chat_id], { type: 'swipe' }); |
| 5708 | 5789 | !fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type); |
| 5709 | 5790 | } else if (type === 'appendFinal') { |
| 5710 | 5791 | oldMessage = chat[chat.length - 1]['mes']; |
| 5711 | 5792 | console.debug('Trying to appendFinal.'); |
| @@ -5724,9 +5805,9 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title | ||
| 5724 | 5805 | chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); |
| 5725 | 5806 | } |
| 5726 | 5807 | const chat_id = (chat.length - 1); |
| 5727 | 5808 | !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type); |
| 5728 | 5809 | addOneMessage(chat[chat_id], { type: 'swipe' }); |
| 5729 | 5810 | !fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type); |
| 5730 | 5811 | |
| 5731 | 5812 | } else { |
| 5732 | 5813 | console.debug('entering chat update routine for non-swipe post'); |
| @@ -6617,7 +6698,7 @@ async function getChatResult() { | ||
| 6617 | 6698 | } |
| 6618 | 6699 | |
| 6619 | 6700 | function getFirstMessage() { |
| 6620 | 6701 | const firstMes = characters[this_chid]?.first_mes || ''; |
| 6621 | 6702 | const alternateGreetings = characters[this_chid]?.data?.alternate_greetings; |
| 6622 | 6703 | |
| 6623 | 6704 | const message = { |
| @@ -6639,7 +6720,12 @@ function getFirstMessage() { | ||
| 6639 | 6720 | |
| 6640 | 6721 | message['swipe_id'] = 0; |
| 6641 | 6722 | message['swipes'] = swipes; |
| 6642 | 6723 | message['swipe_info'] = [];swipes.map(_ => ({ |
| 6724 | + send_date: message.send_date, | |
| 6725 | + gen_started: void 0, | |
| 6726 | + gen_finished: void 0, | |
| 6727 | + extra: {}, | |
| 6728 | + })); | |
| 6643 | 6729 | } |
| 6644 | 6730 | |
| 6645 | 6731 | return message; |
| @@ -6752,7 +6838,7 @@ export function changeMainAPI() { | ||
| 6752 | 6838 | $('#ai_module_block_novel').css('display', 'none'); |
| 6753 | 6839 | } |
| 6754 | 6840 | |
| 6755 | 6841 | $('#prompt_cost_block').toggle(selectedVal === 'textgenerationwebui' && textgen_settings.type === textgen_types.OPENROUTER); |
| 6756 | 6842 | |
| 6757 | 6843 | // Hide common settings for OpenAI |
| 6758 | 6844 | console.debug('value?', selectedVal); |
| @@ -7274,8 +7360,9 @@ export function getCurrentChatDetails() { | ||
| 7274 | 7360 | * The function first fetches the chats, processes them, and then displays them in |
| 7275 | 7361 | * the HTML. It also has a built-in search functionality that allows filtering the |
| 7276 | 7362 | * displayed chats based on a search query. |
| 7363 | + * @param {string[]} hightlightNames - An array of chat names to highlight | |
| 7277 | 7364 | */ |
| 7278 | 7365 | export async function displayPastChats(hightlightNames = []) { |
| 7279 | 7366 | $('#select_chat_div').empty(); |
| 7280 | 7367 | $('#select_chat_search').val('').off('input'); |
| 7281 | 7368 | |
| @@ -7284,10 +7371,10 @@ export async function displayPastChats() { | ||
| 7284 | 7371 | const displayName = chatDetails.characterName; |
| 7285 | 7372 | const avatarImg = chatDetails.avatarImgURL; |
| 7286 | 7373 | |
| 7287 | 7374 | await displayChats('', currentChat, displayName, avatarImg, selected_group, hightlightNames); |
| 7288 | 7375 | |
| 7289 | 7376 | const debouncedDisplay = debounce((searchQuery) => { |
| 7290 | 7377 | displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group, []); |
| 7291 | 7378 | }); |
| 7292 | 7379 | |
| 7293 | 7380 | // Define the search input listener |
| @@ -7303,7 +7390,7 @@ export async function displayPastChats() { | ||
| 7303 | 7390 | }, 200); |
| 7304 | 7391 | } |
| 7305 | 7392 | |
| 7306 | 7393 | async function displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group, highlightNames) { |
| 7307 | 7394 | try { |
| 7308 | 7395 | const trimExtension = (fileName) => String(fileName).replace('.jsonl', ''); |
| 7309 | 7396 | |
| @@ -7343,6 +7430,12 @@ async function displayChats(searchQuery, currentChat, displayName, avatarImg, se | ||
| 7343 | 7430 | } |
| 7344 | 7431 | |
| 7345 | 7432 | $('#select_chat_div').append(template); |
| 7433 | + | |
| 7434 | + if (Array.isArray(highlightNames) && highlightNames.includes(chat.file_name)) { | |
| 7435 | + const templateOffset = template.offset().top - template.parent().offset().top; | |
| 7436 | + $('#select_chat_div').scrollTop(templateOffset); | |
| 7437 | + flashHighlight(template, debounce_timeout.extended); | |
| 7438 | + } | |
| 7346 | 7439 | } |
| 7347 | 7440 | } catch (error) { |
| 7348 | 7441 | console.error('Error loading chats:', error); |
| @@ -7476,7 +7569,7 @@ export function select_rm_info(type, charId, previousCharId = null) { | ||
| 7476 | 7569 | |
| 7477 | 7570 | /** |
| 7478 | 7571 | * Selects the right menu for displaying the character editor. |
| 7479 | 7572 | * @param {number|string} chid Character array index |
| 7480 | 7573 | * @param {object} [param1] Options for the switch |
| 7481 | 7574 | * @param {boolean} [param1.switchMenu=true] Whether to switch the menu |
| 7482 | 7575 | */ |
| @@ -7554,6 +7647,11 @@ export function select_selected_character(chid, { switchMenu = true } = {}) { | ||
| 7554 | 7647 | $('#character_media_allowed_icon').toggle(externalMediaState); |
| 7555 | 7648 | $('#character_media_forbidden_icon').toggle(!externalMediaState); |
| 7556 | 7649 | |
| 7650 | + // Update some stuff about the char management dropdown | |
| 7651 | + $('#character_source').attr('disabled', !getCharacterSource(chid) ? '' : null); | |
| 7652 | + | |
| 7653 | + eventSource.emit(event_types.CHARACTER_EDITOR_OPENED, chid); | |
| 7654 | + | |
| 7557 | 7655 | saveSettingsDebounced(); |
| 7558 | 7656 | } |
| 7559 | 7657 | |
| @@ -7836,7 +7934,7 @@ export function showSwipeButtons() { | ||
| 7836 | 7934 | }; |
| 7837 | 7935 | } |
| 7838 | 7936 | |
| 7839 | 7937 | const currentMessage = $('#chat')chatElement.children().filter(`[mesid="${chat.length - 1}"]`); |
| 7840 | 7938 | const swipeId = chat[chat.length - 1].swipe_id; |
| 7841 | 7939 | const swipeCounterText = formatSwipeCounter((swipeId + 1), chat[chat.length - 1].swipes.length); |
| 7842 | 7940 | const swipeRight = currentMessage.find('.swipe_right'); |
| @@ -7872,42 +7970,45 @@ export function hideSwipeButtons() { | ||
| 7872 | 7970 | /** |
| 7873 | 7971 | * Deletes a swipe from the chat. |
| 7874 | 7972 | * |
| 7875 | 7973 | * @param {number?} [swipeId = null] - The ID of the swipe to delete. If not provided, the current swipe will be deleted. |
| 7974 | + * @param {number?} [messageId = chat.length - 1] - The ID of the message to delete from. If not provided, the last message will be targeted. | |
| 7876 | 7975 | * @returns {Promise<number>|undefined} - The ID of the new swipe after deletion. |
| 7877 | 7976 | */ |
| 7878 | 7977 | export async function deleteSwipe(swipeId = null, messageId = chat.length - 1) { |
| 7879 | 7978 | if (swipeId && (isNaN(swipeId) || swipeId < 0)) { |
| 7880 | 7979 | toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`); |
| 7881 | 7980 | return; |
| 7882 | 7981 | } |
| 7883 | 7982 | |
| 7884 | 7983 | const lastMessagemessage = chat[chat.length - 1messageId]; |
| 7885 | 7984 | if (!lastMessagemessage || !Array.isArray(lastMessagemessage.swipes) || !lastMessagemessage.swipes.length) { |
| 7886 | 7985 | toastr.warning(t`No messages to delete swipes from.`); |
| 7887 | 7986 | return; |
| 7888 | 7987 | } |
| 7889 | 7988 | |
| 7890 | 7989 | if (lastMessagemessage.swipes.length <= 1) { |
| 7891 | 7990 | toastr.warning(t`Can't delete the last swipe.`); |
| 7892 | 7991 | return; |
| 7893 | 7992 | } |
| 7894 | 7993 | |
| 7895 | 7994 | swipeId = swipeId ?? lastMessagemessage.swipe_id; |
| 7896 | 7995 | |
| 7897 | 7996 | if (swipeId < 0 || swipeId >= lastMessagemessage.swipes.length) { |
| 7898 | 7997 | toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`); |
| 7899 | 7998 | return; |
| 7900 | 7999 | } |
| 7901 | 8000 | |
| 7902 | 8001 | lastMessagemessage.swipes.splice(swipeId, 1); |
| 7903 | 8002 | |
| 7904 | 8003 | if (Array.isArray(lastMessagemessage.swipe_info) && lastMessagemessage.swipe_info.length) { |
| 7905 | 8004 | lastMessagemessage.swipe_info.splice(swipeId, 1); |
| 7906 | 8005 | } |
| 7907 | 8006 | |
| 7908 | 8007 | // Select the next swipe, or the one before if it was the last one |
| 7909 | 8008 | const newSwipeId = Math.min(swipeId, lastMessagemessage.swipes.length - 1); |
| 7910 | 8009 | syncSwipeToMes(nullmessageId, newSwipeId); |
| 8010 | + | |
| 8011 | + await eventSource.emit(event_types.MESSAGE_SWIPE_DELETED, { messageId, swipeId, newSwipeId }); | |
| 7911 | 8012 | |
| 7912 | 8013 | await saveChatConditional(); |
| 7913 | 8014 | await reloadCurrentChat(); |
| @@ -7957,9 +8058,11 @@ export async function saveChatConditional() { | ||
| 7957 | 8058 | /** |
| 7958 | 8059 | * Saves the chat to the server. |
| 7959 | 8060 | * @param {FormData} formData Form data to send to the server. |
| 7960 | 8061 | * @param {EventTargetobject} eventTarget Event target[options={}] toOptions triggerfor the event on.import |
| 8062 | + * @param {boolean} [options.refresh] Whether to refresh the group chat list after import | |
| 8063 | + * @returns {Promise<string[]>} List of imported file names. | |
| 7961 | 8064 | */ |
| 7962 | 8065 | async function importCharacterChat(formData, eventTarget{ refresh = true } = {}) { |
| 7963 | 8066 | const fetchResult = await fetch('/api/chats/import', { |
| 7964 | 8067 | method: 'POST', |
| 7965 | 8068 | body: formData, |
| @@ -7969,26 +8072,25 @@ async function importCharacterChat(formData, eventTarget) { | ||
| 7969 | 8072 | |
| 7970 | 8073 | if (fetchResult.ok) { |
| 7971 | 8074 | const data = await fetchResult.json(); |
| 7972 | 8075 | if (data.res && refresh) { |
| 7973 | 8076 | await displayPastChats(); |
| 7974 | 8077 | } |
| 8078 | + return data?.fileNames || []; | |
| 7975 | 8079 | } |
| 7976 | 8080 | |
| 7977 | - if (eventTarget instanceof HTMLInputElement) { | |
| 8081 | + return []; | |
| 7978 | - eventTarget.value = ''; | |
| 7979 | - } | |
| 7980 | 8082 | } |
| 7981 | 8083 | |
| 7982 | 8084 | function updateViewMessageIds(startFromZero = false) { |
| 7983 | 8085 | const minId = startFromZero ? 0 : getFirstDisplayedMessageId(); |
| 7984 | 8086 | |
| 7985 | 8087 | $('#chat')chatElement.find('.mes').each(function (index, element) { |
| 7986 | 8088 | $(element).attr('mesid', minId + index); |
| 7987 | 8089 | $(element).find('.mesIDDisplay').text(`#${minId + index}`); |
| 7988 | 8090 | }); |
| 7989 | 8091 | |
| 7990 | 8092 | $chatElement.find('#chat .mes').removeClass('last_mes'); |
| 7991 | 8093 | $chatElement.find('#chat .mes').last().addClass('last_mes'); |
| 7992 | 8094 | |
| 7993 | 8095 | updateEditArrowClasses(); |
| 7994 | 8096 | } |
| @@ -8000,14 +8102,14 @@ export function getFirstDisplayedMessageId() { | ||
| 8000 | 8102 | } |
| 8001 | 8103 | |
| 8002 | 8104 | function updateEditArrowClasses() { |
| 8003 | 8105 | $chatElement.find('#chat .mes .mes_edit_up').removeClass('disabled'); |
| 8004 | 8106 | $chatElement.find('#chat .mes .mes_edit_down').removeClass('disabled'); |
| 8005 | 8107 | |
| 8006 | 8108 | if (this_edit_mes_id !== undefined) { |
| 8007 | 8109 | const down = $chatElement.find(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_down`); |
| 8008 | 8110 | const up = $chatElement.find(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_up`); |
| 8009 | 8111 | const lastId = Number($chatElement.find('#chat .mes').last().attr('mesid')); |
| 8010 | 8112 | const firstId = Number($chatElement.find('#chat .mes').first().attr('mesid')); |
| 8011 | 8113 | |
| 8012 | 8114 | if (lastId == Number(this_edit_mes_id)) { |
| 8013 | 8115 | down.addClass('disabled'); |
| @@ -8026,7 +8128,7 @@ function updateEditArrowClasses() { | ||
| 8026 | 8128 | export function closeMessageEditor(what = 'all') { |
| 8027 | 8129 | if (what === 'message' || what === 'all') { |
| 8028 | 8130 | if (this_edit_mes_id) { |
| 8029 | 8131 | $chatElement.find(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).trigger('click'); |
| 8030 | 8132 | } |
| 8031 | 8133 | } |
| 8032 | 8134 | if (what === 'reasoning' || what === 'all') { |
| @@ -8081,61 +8183,16 @@ async function openCharacterWorldPopup() { | ||
| 8081 | 8183 | const selectedValue = $(this).val(); |
| 8082 | 8184 | const worldIndex = selectedValue !== '' ? Number(selectedValue) : NaN; |
| 8083 | 8185 | const name = !isNaN(worldIndex) ? world_names[worldIndex] : ''; |
| 8084 | - const previousValue = $('#character_world').val(); | |
| 8186 | + await charUpdatePrimaryWorld(name); | |
| 8085 | - $('#character_world').val(name); | |
| 8086 | - | |
| 8087 | - console.debug('Character world selected:', name); | |
| 8088 | - | |
| 8089 | - if (menu_type == 'create') { | |
| 8090 | - create_save.world = name; | |
| 8091 | - } else { | |
| 8092 | - if (previousValue && !name) { | |
| 8093 | - try { | |
| 8094 | - // Dirty hack to remove embedded lorebook from character JSON data. | |
| 8095 | - const data = JSON.parse(String($('#character_json_data').val())); | |
| 8096 | - | |
| 8097 | - if (data?.data?.character_book) { | |
| 8098 | - data.data.character_book = undefined; | |
| 8099 | - } | |
| 8100 | - | |
| 8101 | - $('#character_json_data').val(JSON.stringify(data)); | |
| 8102 | - toastr.info(t`Embedded lorebook will be removed from this character.`); | |
| 8103 | - } catch { | |
| 8104 | - console.error('Failed to parse character JSON data.'); | |
| 8105 | - } | |
| 8106 | - } | |
| 8107 | - | |
| 8108 | - await createOrEditCharacter(); | |
| 8109 | - } | |
| 8110 | - | |
| 8111 | - setWorldInfoButtonClass(undefined, !!name); | |
| 8112 | 8187 | } |
| 8113 | 8188 | |
| 8114 | 8189 | function handleExtrasWorldSelect(evt) { |
| 8115 | - const selectedValues = $(this).val(); | |
| 8190 | + const el = evt?.currentTarget ?? this; | |
| 8116 | 8191 | const selectedWorldsselectedValues = Array$(el).isArrayval(selectedValues) ? selectedValues : []; |
| 8117 | 8192 | letconst charLoreselected = world_infoArray.charLoreisArray(selectedValues) ?? selectedValues : []; |
| 8118 | - const tempExtraBooks = selectedWorlds.map((index) => world_names[index]).filter(Boolean); | |
| 8193 | + const fileName = getCharaFilename(null, {}); | |
| 8119 | 8194 | const existingCharIndexnextList = charLoreselected.findIndex(map(e)i => eworld_names[i]).name === fileNamefilter(Boolean); |
| 8120 | - | |
| 8195 | + charSetAuxWorlds(fileName, nextList); | |
| 8121 | - if (menu_type == 'create') { | |
| 8122 | - create_save.extra_books = tempExtraBooks; | |
| 8123 | - return; | |
| 8124 | - } | |
| 8125 | - | |
| 8126 | - if (existingCharIndex === -1) { | |
| 8127 | - // Add record only if at least 1 lorebook is selected. | |
| 8128 | - if (tempExtraBooks.length > 0) { | |
| 8129 | - charLore.push({ name: fileName, extraBooks: tempExtraBooks }); | |
| 8130 | - } | |
| 8131 | - } else if (tempExtraBooks.length === 0) { | |
| 8132 | - charLore.splice(existingCharIndex, 1); | |
| 8133 | - } else { | |
| 8134 | - charLore[existingCharIndex].extraBooks = tempExtraBooks; | |
| 8135 | - } | |
| 8136 | - | |
| 8137 | - Object.assign(world_info, { charLore: charLore }); | |
| 8138 | - saveSettingsDebounced(); | |
| 8139 | 8196 | } |
| 8140 | 8197 | |
| 8141 | 8198 | // --- Populate Dropdowns --- |
| @@ -8258,7 +8315,7 @@ function addAlternateGreeting(template, greeting, index, getArray, popup) { | ||
| 8258 | 8315 | * Creates or edits a character based on the form data. |
| 8259 | 8316 | * @param {Event} [e] Event that triggered the function call. |
| 8260 | 8317 | */ |
| 8261 | 8318 | export async function createOrEditCharacter(e) { |
| 8262 | 8319 | $('#rm_info_avatar').html(''); |
| 8263 | 8320 | const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0))); |
| 8264 | 8321 | formData.set('fav', String(fav_ch_checked)); |
| @@ -8701,10 +8758,10 @@ export function swipe_right(_event = null, { source, repeated } = {}) { | ||
| 8701 | 8758 | easing: animation_easing, |
| 8702 | 8759 | queue: false, |
| 8703 | 8760 | complete: async function () { |
| 8704 | 8761 | const is_animation_scroll = ($('#chat')chatElement.scrollTop() >= ($('#chat')chatElement.prop('scrollHeight') - $('#chat')chatElement.outerHeight()) - 10); |
| 8705 | 8762 | //console.log(parseInt(chat[chat.length-1]['swipe_id'])); |
| 8706 | 8763 | //console.log(chat[chat.length-1]['swipes'].length); |
| 8707 | 8764 | const swipeMessage = $('#chat')chatElement.find('[mesid="' + (chat.length - 1) + '"]'); |
| 8708 | 8765 | if (run_generate && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) { |
| 8709 | 8766 | //shows "..." while generating |
| 8710 | 8767 | swipeMessage.find('.mes_text').html('...'); |
| @@ -8737,12 +8794,12 @@ export function swipe_right(_event = null, { source, repeated } = {}) { | ||
| 8737 | 8794 | queue: false, |
| 8738 | 8795 | progress: function () { |
| 8739 | 8796 | // Scroll the chat down as the message expands |
| 8740 | 8797 | if (is_animation_scroll) $('#chat')chatElement.scrollTop($('#chat')chatElement[0].scrollHeight); |
| 8741 | 8798 | }, |
| 8742 | 8799 | complete: function () { |
| 8743 | 8800 | this_mes_div.css('height', 'auto'); |
| 8744 | 8801 | // Scroll the chat down to the bottom once the animation is complete |
| 8745 | 8802 | if (is_animation_scroll) $('#chat')chatElement.scrollTop($('#chat')chatElement[0].scrollHeight); |
| 8746 | 8803 | }, |
| 8747 | 8804 | }); |
| 8748 | 8805 | this_mes_div.children('.mes_block').transition({ |
| @@ -8888,6 +8945,8 @@ async function importCharacter(file, { preserveFileName = '', importTags = false | ||
| 8888 | 8945 | return; |
| 8889 | 8946 | } |
| 8890 | 8947 | |
| 8948 | + const exists = preserveFileName ? characters.find(character => character.avatar === preserveFileName) : undefined; | |
| 8949 | + | |
| 8891 | 8950 | const format = ext[1].toLowerCase(); |
| 8892 | 8951 | $('#character_import_file_type').val(format); |
| 8893 | 8952 | const formData = new FormData(); |
| @@ -8914,10 +8973,20 @@ async function importCharacter(file, { preserveFileName = '', importTags = false | ||
| 8914 | 8973 | } |
| 8915 | 8974 | |
| 8916 | 8975 | if (data.file_name !== undefined) { |
| 8976 | + let avatarFileName = `${data.file_name}.png`; | |
| 8977 | + | |
| 8978 | + // Refresh existing thumbnail | |
| 8979 | + if (exists && this_chid !== undefined) { | |
| 8980 | + await fetch(getThumbnailUrl('avatar', avatarFileName), { cache: 'reload' }); | |
| 8981 | + } | |
| 8982 | + | |
| 8917 | 8983 | $('#character_search_bar').val('').trigger('input'); |
| 8918 | 8984 | |
| 8919 | - toastr.success(t`Character Created: ${String(data.file_name).replace('.png', '')}`); | |
| 8985 | + if (exists) { | |
| 8920 | - let avatarFileName = `${data.file_name}.png`; | |
| 8986 | + toastr.success(t`Character Replaced: ${String(data.file_name).replace('.png', '')}`); | |
| 8987 | + } else { | |
| 8988 | + toastr.success(t`Character Created: ${String(data.file_name).replace('.png', '')}`); | |
| 8989 | + } | |
| 8921 | 8990 | if (importTags) { |
| 8922 | 8991 | await importCharactersTags([avatarFileName]); |
| 8923 | 8992 | selectImportedChar(data.file_name); |
| @@ -8972,7 +9041,7 @@ export async function doNewChat({ deleteCurrentChat = false } = {}) { | ||
| 8972 | 9041 | |
| 8973 | 9042 | if (selected_group) { |
| 8974 | 9043 | await createNewGroupChat(selected_group); |
| 8975 | - if (deleteCurrentChat) await deleteGroupChat(selected_group, chat_file_for_del); | |
| 9044 | + if (deleteCurrentChat) await deleteGroupChat(selected_group, chat_file_for_del, { jumpToNewChat: false }); // don't jump, new chat was already created and jumped to above | |
| 8976 | 9045 | } |
| 8977 | 9046 | else { |
| 8978 | 9047 | //RossAscends: added character name to new chat filenames and replaced Date.now() with humanizedDateTime; |
| @@ -9073,6 +9142,34 @@ export async function renameChat(oldFileName, newName) { | ||
| 9073 | 9142 | } |
| 9074 | 9143 | |
| 9075 | 9144 | /** |
| 9145 | + * Closes the current chat, clearing all associated data and resetting the UI. | |
| 9146 | + * If a message generation is in progress, it prompts the user to stop it first. | |
| 9147 | + * @returns {Promise<boolean>} True if the chat was successfully closed, false otherwise. | |
| 9148 | + */ | |
| 9149 | +export async function closeCurrentChat() { | |
| 9150 | + if (is_send_press == false) { | |
| 9151 | + await waitUntilCondition(() => !isChatSaving, debounce_timeout.extended, 10); | |
| 9152 | + await clearChat(); | |
| 9153 | + chat.length = 0; | |
| 9154 | + resetSelectedGroup(); | |
| 9155 | + setCharacterId(undefined); | |
| 9156 | + setCharacterName(''); | |
| 9157 | + setActiveCharacter(null); | |
| 9158 | + setActiveGroup(null); | |
| 9159 | + this_edit_mes_id = undefined; | |
| 9160 | + chat_metadata = {}; | |
| 9161 | + selected_button = 'characters'; | |
| 9162 | + $('#rm_button_selected_ch').children('h2').text(''); | |
| 9163 | + select_rm_characters(); | |
| 9164 | + await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); | |
| 9165 | + return true; | |
| 9166 | + } else { | |
| 9167 | + toastr.info(t`Please stop the message generation first.`); | |
| 9168 | + return false; | |
| 9169 | + } | |
| 9170 | +} | |
| 9171 | + | |
| 9172 | +/** | |
| 9076 | 9173 | * Forces the update of the chat name for a remote character. |
| 9077 | 9174 | * @param {string|number} characterId Character ID to update chat name for |
| 9078 | 9175 | * @param {string} newName New name for the chat |
| @@ -9137,6 +9234,22 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {}) | ||
| 9137 | 9234 | characterKey = [characterKey]; |
| 9138 | 9235 | } |
| 9139 | 9236 | |
| 9237 | + const inTempChat = this_chid === undefined && name2 === neutralCharacterName; | |
| 9238 | + if (inTempChat) { | |
| 9239 | + const confirmClose = await Popup.show.confirm( | |
| 9240 | + t`You are currently in a temporary chat.`, | |
| 9241 | + t`Deleting this character will close the chat and you will lose any unsaved messages. Do you want to proceed?`, | |
| 9242 | + ); | |
| 9243 | + if (!confirmClose) { | |
| 9244 | + return; | |
| 9245 | + } | |
| 9246 | + } | |
| 9247 | + | |
| 9248 | + const closeChatResult = await closeCurrentChat(); | |
| 9249 | + if (!closeChatResult) { | |
| 9250 | + return; | |
| 9251 | + } | |
| 9252 | + | |
| 9140 | 9253 | for (const key of characterKey) { |
| 9141 | 9254 | const character = characters.find(x => x.avatar == key); |
| 9142 | 9255 | if (!character) { |
| @@ -9743,6 +9856,12 @@ jQuery(async function () { | ||
| 9743 | 9856 | }); |
| 9744 | 9857 | }); |
| 9745 | 9858 | |
| 9859 | + $('#creator_notes_textarea').on('input', function () { | |
| 9860 | + const notes = String($('#creator_notes_textarea').val()); | |
| 9861 | + const avatar = menu_type === 'create' ? '' : characters[this_chid]?.avatar; | |
| 9862 | + $('#creator_notes_spoiler').html(formatCreatorNotes(notes, avatar)); | |
| 9863 | + }); | |
| 9864 | + | |
| 9746 | 9865 | $('#favorite_button').on('click', function () { |
| 9747 | 9866 | updateFavButtonState(!fav_ch_checked); |
| 9748 | 9867 | if (menu_type != 'create') { |
| @@ -9940,24 +10059,7 @@ jQuery(async function () { | ||
| 9940 | 10059 | } |
| 9941 | 10060 | |
| 9942 | 10061 | else if (id == 'option_close_chat') { |
| 9943 | - if (is_send_press == false) { | |
| 10062 | + await closeCurrentChat(); | |
| 9944 | - await waitUntilCondition(() => !isChatSaving, debounce_timeout.extended, 10); | |
| 9945 | - await clearChat(); | |
| 9946 | - chat.length = 0; | |
| 9947 | - resetSelectedGroup(); | |
| 9948 | - setCharacterId(undefined); | |
| 9949 | - setCharacterName(''); | |
| 9950 | - setActiveCharacter(null); | |
| 9951 | - setActiveGroup(null); | |
| 9952 | - this_edit_mes_id = undefined; | |
| 9953 | - chat_metadata = {}; | |
| 9954 | - selected_button = 'characters'; | |
| 9955 | - $('#rm_button_selected_ch').children('h2').text(''); | |
| 9956 | - select_rm_characters(); | |
| 9957 | - await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); | |
| 9958 | - } else { | |
| 9959 | - toastr.info(t`Please stop the message generation first.`); | |
| 9960 | - } | |
| 9961 | 10063 | } |
| 9962 | 10064 | |
| 9963 | 10065 | else if (id === 'option_settings') { |
| @@ -10021,15 +10123,15 @@ jQuery(async function () { | ||
| 10021 | 10123 | }); |
| 10022 | 10124 | |
| 10023 | 10125 | if (this_del_mes >= 0) { |
| 10024 | 10126 | $chatElement.find(`.mes[mesid="${this_del_mes}"]`).nextAll('div').remove(); |
| 10025 | 10127 | $chatElement.find(`.mes[mesid="${this_del_mes}"]`).remove(); |
| 10026 | 10128 | chat.length = this_del_mes; |
| 10027 | 10129 | chat_metadata['tainted'] = true; |
| 10028 | 10130 | await saveChatConditional(); |
| 10029 | 10131 | chatElement.scrollTop(chatElement[0].scrollHeight); |
| 10030 | 10132 | await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); |
| 10031 | 10133 | $chatElement.find('#chat .mes').removeClass('last_mes'); |
| 10032 | 10134 | $chatElement.find('#chat .mes').last().addClass('last_mes'); |
| 10033 | 10135 | } else { |
| 10034 | 10136 | console.log('this_del_mes is not >= 0, not deleting'); |
| 10035 | 10137 | } |
| @@ -10134,9 +10236,9 @@ jQuery(async function () { | ||
| 10134 | 10236 | return; |
| 10135 | 10237 | }*/ |
| 10136 | 10238 | |
| 10137 | 10239 | let chatScrollPosition = $('#chat')chatElement.scrollTop(); |
| 10138 | 10240 | if (this_edit_mes_id !== undefined) { |
| 10139 | 10241 | let mes_edited = $chatElement.find(`#chat [mesid="${this_edit_mes_id}"]`).find('.mes_edit_done'); |
| 10140 | 10242 | if (Number(edit_mes_id) == chat.length - 1) { //if the generating swipe (...) |
| 10141 | 10243 | let run_edit = true; |
| 10142 | 10244 | if (chat[edit_mes_id]['swipe_id'] !== undefined) { |
| @@ -10195,7 +10297,7 @@ jQuery(async function () { | ||
| 10195 | 10297 | String(edit_textarea.val()).length, |
| 10196 | 10298 | ); |
| 10197 | 10299 | if (Number(this_edit_mes_id) === chat.length - 1) { |
| 10198 | 10300 | $('#chat')chatElement.scrollTop(chatScrollPosition); |
| 10199 | 10301 | } |
| 10200 | 10302 | |
| 10201 | 10303 | updateEditArrowClasses(); |
| @@ -10313,7 +10415,7 @@ jQuery(async function () { | ||
| 10313 | 10415 | |
| 10314 | 10416 | hideSwipeButtons(); |
| 10315 | 10417 | const targetId = Number(this_edit_mes_id) - 1; |
| 10316 | 10418 | const target = $chatElement.find(`#chat .mes[mesid="${targetId}"]`); |
| 10317 | 10419 | const root = $(this).closest('.mes'); |
| 10318 | 10420 | |
| 10319 | 10421 | if (root.length === 0 || target.length === 0) { |
| @@ -10342,7 +10444,7 @@ jQuery(async function () { | ||
| 10342 | 10444 | |
| 10343 | 10445 | hideSwipeButtons(); |
| 10344 | 10446 | const targetId = Number(this_edit_mes_id) + 1; |
| 10345 | 10447 | const target = $chatElement.find(`#chat .mes[mesid="${targetId}"]`); |
| 10346 | 10448 | const root = $(this).closest('.mes'); |
| 10347 | 10449 | |
| 10348 | 10450 | if (root.length === 0 || target.length === 0) { |
| @@ -10414,7 +10516,7 @@ jQuery(async function () { | ||
| 10414 | 10516 | if (deleteOnlySwipe) { |
| 10415 | 10517 | const message = chat[this_edit_mes_id]; |
| 10416 | 10518 | const swipe_id = message.swipe_id; |
| 10417 | 10519 | await deleteSwipe(swipe_id, Number(this_edit_mes_id)); |
| 10418 | 10520 | return; |
| 10419 | 10521 | } |
| 10420 | 10522 | |
| @@ -10519,41 +10621,45 @@ jQuery(async function () { | ||
| 10519 | 10621 | }); |
| 10520 | 10622 | |
| 10521 | 10623 | $('#chat_import_file').on('change', async function (e) { |
| 10522 | 10624 | const targetElement = /** @type {HTMLInputElement} */ (e.target); |
| 10523 | - if (!(targetElement instanceof HTMLInputElement)) { | |
| 10625 | + const formElement = document.getElementById('form_import_chat'); | |
| 10626 | + if (!(targetElement instanceof HTMLInputElement) || !(formElement instanceof HTMLFormElement)) { | |
| 10524 | 10627 | return; |
| 10525 | 10628 | } |
| 10526 | - const file = targetElement.files[0]; | |
| 10527 | 10629 | |
| 10528 | - if (!file) { | |
| 10630 | + const importedFileNames = []; | |
| 10529 | - return; | |
| 10530 | - } | |
| 10531 | 10631 | |
| 10532 | - const ext = file.name.match(/\.(\w+)$/); | |
| 10632 | + for (const file of targetElement.files) { | |
| 10533 | - if ( | |
| 10633 | + const ext = file.name.match(/\.(\w+)$/); | |
| 10534 | - !ext || | |
| 10634 | + const format = ext?.[1]?.toLowerCase(); | |
| 10535 | - (ext[1].toLowerCase() != 'json' && ext[1].toLowerCase() != 'jsonl') | |
| 10536 | - ) { | |
| 10537 | - return; | |
| 10538 | - } | |
| 10539 | 10635 | |
| 10540 | - if (selected_group && file.name.endsWith('.json')) { | |
| 10636 | + if (!['json', 'jsonl'].includes(format)) { | |
| 10541 | 10637 | toastr.warning('t`Only SillyTavern\'sJSON ownand formatJSONL isfiles are supported for group chat imports. Sorry!'`); |
| 10542 | - return; | |
| 10638 | + continue; | |
| 10543 | 10639 | } |
| 10544 | 10640 | |
| 10545 | - const format = ext[1].toLowerCase(); | |
| 10641 | + if (selected_group && format === 'json') { | |
| 10546 | - $('#chat_import_file_type').val(format); | |
| 10642 | + toastr.warning(t`Only SillyTavern's own format is supported for group chat imports. Sorry!`); | |
| 10643 | + continue; | |
| 10644 | + } | |
| 10547 | 10645 | |
| 10548 | - const formData = new FormData(/** @type {HTMLFormElement} */($('#form_import_chat').get(0))); | |
| 10646 | + const formData = new FormData(formElement); | |
| 10549 | 10647 | formData.appendset('user_namefile_type', name1format); |
| 10550 | - $('#select_chat_div').html(''); | |
| 10648 | + formData.set('avatar', file); | |
| 10649 | + formData.set('user_name', name1); | |
| 10551 | 10650 | |
| 10552 | - if (selected_group) { | |
| 10651 | + const importFn = selected_group ? importGroupChat : importCharacterChat; | |
| 10553 | - await importGroupChat(formData, e.originalEvent.target); | |
| 10652 | + const result = await importFn(formData, { refresh: false }); | |
| 10554 | - } else { | |
| 10653 | + importedFileNames.push(...result); | |
| 10555 | - await importCharacterChat(formData, e.originalEvent.target); | |
| 10654 | + } | |
| 10655 | + | |
| 10656 | + if (importedFileNames.length > 0) { | |
| 10657 | + toastr.success(t`Successfully imported ${importedFileNames.length} chat(s).`); | |
| 10556 | 10658 | } |
| 10659 | + | |
| 10660 | + await displayPastChats(importedFileNames); | |
| 10661 | + | |
| 10662 | + targetElement.value = ''; | |
| 10557 | 10663 | }); |
| 10558 | 10664 | |
| 10559 | 10665 | $('#rm_button_group_chats').on('click', function () { |
| @@ -10775,7 +10881,7 @@ jQuery(async function () { | ||
| 10775 | 10881 | return; |
| 10776 | 10882 | } |
| 10777 | 10883 | if (isEditVisible && power_user.auto_save_msg_edits === true) { |
| 10778 | 10884 | $chatElement.find(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).trigger('click'); |
| 10779 | 10885 | closeMessageEditor('reasoning'); |
| 10780 | 10886 | $('#send_textarea').trigger('focus'); |
| 10781 | 10887 | return; |
| @@ -10819,27 +10925,66 @@ jQuery(async function () { | ||
| 10819 | 10925 | } |
| 10820 | 10926 | } break; |
| 10821 | 10927 | case 'replace_update': { |
| 10822 | - const confirm = await Popup.show.confirm(t`Replace Character`, '<p>' + t`Choose a new character card to replace this character with.` + '</p>' + t`All chats, assets and group memberships will be preserved, but local changes to the character data will be lost.` + '<br />' + t`Proceed?`); | |
| 10928 | + let onlineUrl = getCharacterSource(this_chid); | |
| 10823 | - if (confirm) { | |
| 10929 | + | |
| 10824 | - async function uploadReplacementCard(e) { | |
| 10930 | + const POPUP_RESULT_URL = POPUP_RESULT.CUSTOM1, POPUP_RESULT_FILE = POPUP_RESULT.CUSTOM2; | |
| 10825 | - const file = e.target.files[0]; | |
| 10931 | + const result = await Popup.show.confirm(t`Replace Character`, | |
| 10932 | + `<p>${t`Choose a new character card to replace this character with.`}</p>` + | |
| 10933 | + `<p>${t`You can also replace this character with the one from the online source.`}${onlineUrl ? `<br />This character was downloaded from: <var>${onlineUrl}</var>` : ''}</p>` + | |
| 10934 | + `<p>${t`All chats, assets and group memberships will be preserved, but local changes to the character data will be lost.`}<br />${t`Proceed?`}</p>`, | |
| 10935 | + { | |
| 10936 | + okButton: false, | |
| 10937 | + customButtons: [{ | |
| 10938 | + text: t`Replace with URL`, | |
| 10939 | + result: POPUP_RESULT_URL, | |
| 10940 | + classes: ['popup-button-ok'], | |
| 10941 | + }, { | |
| 10942 | + text: t`Replace with File`, | |
| 10943 | + result: POPUP_RESULT_FILE, | |
| 10944 | + classes: ['popup-button-ok'], | |
| 10945 | + }], | |
| 10946 | + defaultResult: onlineUrl ? POPUP_RESULT_URL : POPUP_RESULT_FILE, | |
| 10947 | + }); | |
| 10826 | 10948 | |
| 10827 | - if (!file) { | |
| 10949 | + // Remember the chat currently selected, so we can reload it after the replacement | |
| 10828 | - return; | |
| 10950 | + const currentChatFile = characters[this_chid]['chat']; | |
| 10829 | - } | |
| 10951 | + async function postReplace() { | |
| 10952 | + await openCharacterChat(currentChatFile); | |
| 10953 | + } | |
| 10830 | 10954 | |
| 10831 | - try { | |
| 10955 | + switch (result) { | |
| 10832 | - const chatFile = characters[this_chid]['chat']; | |
| 10956 | + case POPUP_RESULT_FILE: { | |
| 10833 | - const data = new Map(); | |
| 10957 | + async function uploadReplacementCard(e) { | |
| 10834 | - data.set(file, characters[this_chid].avatar); | |
| 10958 | + const file = e.target.files[0]; | |
| 10835 | - await processDroppedFiles([file], data); | |
| 10959 | + if (!file) { | |
| 10836 | - await openCharacterChat(chatFile); | |
| 10960 | + return; | |
| 10837 | - await fetch(getThumbnailUrl('avatar', characters[this_chid].avatar), { cache: 'reload' }); | |
| 10961 | + } | |
| 10838 | - } catch { | |
| 10962 | + | |
| 10839 | - toastr.error('Failed to replace the character card.', 'Something went wrong'); | |
| 10963 | + try { | |
| 10964 | + const data = new Map(); | |
| 10965 | + data.set(file, characters[this_chid].avatar); | |
| 10966 | + await processDroppedFiles([file], data); | |
| 10967 | + await postReplace(); | |
| 10968 | + } catch { | |
| 10969 | + toastr.error('Failed to replace the character card.', 'Something went wrong'); | |
| 10970 | + } | |
| 10840 | 10971 | } |
| 10972 | + $('#character_replace_file').off('change').on('change', uploadReplacementCard).trigger('click'); | |
| 10973 | + break; | |
| 10974 | + } | |
| 10975 | + case POPUP_RESULT_URL: { | |
| 10976 | + const inputUrl = await Popup.show.input(t`Replace Character from URL`, | |
| 10977 | + `<p>${t`Enter the URL of the character card to replace this character with.`}</p>` + | |
| 10978 | + (onlineUrl ? `<p>${t`This character was downloaded from: <var>${onlineUrl}</var>`}</p>` : ''), | |
| 10979 | + onlineUrl); | |
| 10980 | + if (!inputUrl) { | |
| 10981 | + break; | |
| 10982 | + } | |
| 10983 | + onlineUrl = inputUrl; | |
| 10984 | + await importFromExternalUrl(onlineUrl, { preserveFileName: characters[this_chid].avatar }); | |
| 10985 | + await postReplace(); | |
| 10986 | + break; | |
| 10841 | 10987 | } |
| 10842 | - $('#character_replace_file').off('change').on('change', uploadReplacementCard).trigger('click'); | |
| 10843 | 10988 | } |
| 10844 | 10989 | } break; |
| 10845 | 10990 | case 'import_tags': { |
| @@ -10881,7 +11026,6 @@ jQuery(async function () { | ||
| 10881 | 11026 | if (!(e.target instanceof HTMLElement)) { |
| 10882 | 11027 | return; |
| 10883 | 11028 | } |
| 10884 | - e.target.focus(); | |
| 10885 | 11029 | e.target.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true })); |
| 10886 | 11030 | }); |
| 10887 | 11031 | |
| @@ -10950,47 +11094,7 @@ jQuery(async function () { | ||
| 10950 | 11094 | const inputs = String(input).split('\n').map(x => x.trim()).filter(x => x.length > 0); |
| 10951 | 11095 | |
| 10952 | 11096 | for (const url of inputs) { |
| 10953 | - let request; | |
| 11097 | + await importFromExternalUrl(url); | |
| 10954 | - | |
| 10955 | - if (isValidUrl(url)) { | |
| 10956 | - console.debug('Custom content import started for URL: ', url); | |
| 10957 | - request = await fetch('/api/content/importURL', { | |
| 10958 | - method: 'POST', | |
| 10959 | - headers: getRequestHeaders(), | |
| 10960 | - body: JSON.stringify({ url }), | |
| 10961 | - }); | |
| 10962 | - } else { | |
| 10963 | - console.debug('Custom content import started for Char UUID: ', url); | |
| 10964 | - request = await fetch('/api/content/importUUID', { | |
| 10965 | - method: 'POST', | |
| 10966 | - headers: getRequestHeaders(), | |
| 10967 | - body: JSON.stringify({ url }), | |
| 10968 | - }); | |
| 10969 | - } | |
| 10970 | - | |
| 10971 | - if (!request.ok) { | |
| 10972 | - toastr.info(request.statusText, 'Custom content import failed'); | |
| 10973 | - console.error('Custom content import failed', request.status, request.statusText); | |
| 10974 | - return; | |
| 10975 | - } | |
| 10976 | - | |
| 10977 | - const data = await request.blob(); | |
| 10978 | - const customContentType = request.headers.get('X-Custom-Content-Type'); | |
| 10979 | - const fileName = request.headers.get('Content-Disposition').split('filename=')[1].replace(/"/g, ''); | |
| 10980 | - const file = new File([data], fileName, { type: data.type }); | |
| 10981 | - | |
| 10982 | - switch (customContentType) { | |
| 10983 | - case 'character': | |
| 10984 | - await processDroppedFiles([file]); | |
| 10985 | - break; | |
| 10986 | - case 'lorebook': | |
| 10987 | - await importWorldInfo(file); | |
| 10988 | - break; | |
| 10989 | - default: | |
| 10990 | - toastr.warning('Unknown content type'); | |
| 10991 | - console.error('Unknown content type', customContentType); | |
| 10992 | - break; | |
| 10993 | - } | |
| 10994 | 11098 | } |
| 10995 | 11099 | }); |
| 10996 | 11100 | |
| @@ -11001,6 +11105,14 @@ jQuery(async function () { | ||
| 11001 | 11105 | await processDroppedFiles(files); |
| 11002 | 11106 | }, { noAnimation: true }); |
| 11003 | 11107 | |
| 11108 | + chatDragDropHandler = new DragAndDropHandler('#select_chat_popup', async (_, event) => { | |
| 11109 | + const importFile = document.getElementById('chat_import_file'); | |
| 11110 | + if (importFile instanceof HTMLInputElement) { | |
| 11111 | + importFile.files = event.originalEvent.dataTransfer.files; | |
| 11112 | + $(importFile).trigger('change'); | |
| 11113 | + } | |
| 11114 | + }); | |
| 11115 | + | |
| 11004 | 11116 | $('#charListGridToggle').on('click', async () => { |
| 11005 | 11117 | doCharListDisplaySwitch(); |
| 11006 | 11118 | }); |
| @@ -766,45 +766,48 @@ export function initRossMods() { | ||
| 766 | 766 | } |
| 767 | 767 | }); |
| 768 | 768 | |
| 769 | - // read the state of right Nav Lock and apply to rightnav classlist | |
| 769 | + if (!isMobile()) { //only read/set pin states on non-mobile devices | |
| 770 | - $(RPanelPin).prop('checked', accountStorage.getItem('NavLockOn') == 'true'); | |
| 770 | + // read the state of right Nav Lock and apply to rightnav classlist | |
| 771 | 771 | if $(RPanelPin).prop('checked', accountStorage.getItem('NavLockOn') == 'true') {; |
| 772 | - //console.log('setting pin class via local var'); | |
| 772 | + if (accountStorage.getItem('NavLockOn') == 'true') { | |
| 773 | - $(RightNavPanel).addClass('pinnedOpen'); | |
| 773 | + //console.log('setting pin class via local var'); | |
| 774 | 774 | $(RightNavDrawerIconRightNavPanel).addClass('drawerPinnedOpenpinnedOpen'); |
| 775 | - } | |
| 775 | + $(RightNavDrawerIcon).addClass('drawerPinnedOpen'); | |
| 776 | - if ($(RPanelPin).prop('checked')) { | |
| 776 | + } | |
| 777 | - console.debug('setting pin class via checkbox state'); | |
| 777 | + if ($(RPanelPin).prop('checked')) { | |
| 778 | - $(RightNavPanel).addClass('pinnedOpen'); | |
| 778 | + console.debug('setting pin class via checkbox state'); | |
| 779 | 779 | $(RightNavDrawerIconRightNavPanel).addClass('drawerPinnedOpenpinnedOpen'); |
| 780 | - } | |
| 780 | + $(RightNavDrawerIcon).addClass('drawerPinnedOpen'); | |
| 781 | - // read the state of left Nav Lock and apply to leftnav classlist | |
| 781 | + } | |
| 782 | - $(LPanelPin).prop('checked', accountStorage.getItem('LNavLockOn') === 'true'); | |
| 782 | + // read the state of left Nav Lock and apply to leftnav classlist | |
| 783 | 783 | if $(LPanelPin).prop('checked', accountStorage.getItem('LNavLockOn') === 'true') {; |
| 784 | - //console.log('setting pin class via local var'); | |
| 784 | + if (accountStorage.getItem('LNavLockOn') == 'true') { | |
| 785 | - $(LeftNavPanel).addClass('pinnedOpen'); | |
| 785 | + //console.log('setting pin class via local var'); | |
| 786 | 786 | $(LeftNavDrawerIconLeftNavPanel).addClass('drawerPinnedOpenpinnedOpen'); |
| 787 | - } | |
| 787 | + $(LeftNavDrawerIcon).addClass('drawerPinnedOpen'); | |
| 788 | - if ($(LPanelPin).prop('checked')) { | |
| 788 | + } | |
| 789 | - console.debug('setting pin class via checkbox state'); | |
| 789 | + if ($(LPanelPin).prop('checked')) { | |
| 790 | - $(LeftNavPanel).addClass('pinnedOpen'); | |
| 790 | + console.debug('setting pin class via checkbox state'); | |
| 791 | 791 | $(LeftNavDrawerIconLeftNavPanel).addClass('drawerPinnedOpenpinnedOpen'); |
| 792 | - } | |
| 792 | + $(LeftNavDrawerIcon).addClass('drawerPinnedOpen'); | |
| 793 | + } | |
| 793 | 794 | |
| 794 | 795 | // read the state of left Nav Lock and apply to leftnav classlist |
| 795 | 796 | $(WIPanelPin).prop('checked', accountStorage.getItem('WINavLockOn') === 'true'); |
| 796 | 797 | if (accountStorage.getItem('WINavLockOn') == 'true') { |
| 797 | 798 | //console.log('setting pin class via local var'); |
| 798 | 799 | $(WorldInfo).addClass('pinnedOpen'); |
| 799 | 800 | $(WIDrawerIcon).addClass('drawerPinnedOpen'); |
| 800 | 801 | } |
| 801 | 802 | |
| 802 | 803 | if ($(WIPanelPin).prop('checked')) { |
| 803 | 804 | console.debug('setting pin class via checkbox state'); |
| 804 | 805 | $(WorldInfo).addClass('pinnedOpen'); |
| 805 | 806 | $(WIDrawerIcon).addClass('drawerPinnedOpen'); |
| 807 | + } | |
| 806 | 808 | } |
| 807 | 809 | |
| 810 | + | |
| 808 | 811 | //save state of Right nav being open or closed |
| 809 | 812 | $('#rightNavDrawerIcon').on('click', function () { |
| 810 | 813 | if (!$('#rightNavDrawerIcon').hasClass('openIcon')) { |
| @@ -3,7 +3,8 @@ import { chat_metadata, eventSource, event_types, generateQuietPrompt, getCurren | ||
| 3 | 3 | import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; |
| 4 | 4 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 5 | 5 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 6 | 6 | import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop } from './utils.js'; |
| 7 | +import { debounce_timeout } from './constants.js'; | |
| 7 | 8 | import { t } from './i18n.js'; |
| 8 | 9 | import { Popup } from './popup.js'; |
| 9 | 10 | |
| @@ -15,6 +16,11 @@ const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkY | ||
| 15 | 16 | const PNG_PIXEL_BLOB = new Blob([Uint8Array.from(atob(PNG_PIXEL), c => c.charCodeAt(0))], { type: 'image/png' }); |
| 16 | 17 | const PLACEHOLDER_IMAGE = `url('data:image/png;base64,${PNG_PIXEL}')`; |
| 17 | 18 | |
| 19 | +const THUMBNAIL_COLUMNS_MIN = 2; | |
| 20 | +const THUMBNAIL_COLUMNS_MAX = 8; | |
| 21 | +const THUMBNAIL_COLUMNS_DEFAULT_DESKTOP = 5; | |
| 22 | +const THUMBNAIL_COLUMNS_DEFAULT_MOBILE = 3; | |
| 23 | + | |
| 18 | 24 | /** |
| 19 | 25 | * Storage for frontend-generated background thumbnails. |
| 20 | 26 | * This is used to store thumbnails for backgrounds that cannot be generated on the server. |
| @@ -45,6 +51,53 @@ export let background_settings = { | ||
| 45 | 51 | animation: false, |
| 46 | 52 | }; |
| 47 | 53 | |
| 54 | +/** | |
| 55 | + * Creates a single thumbnail DOM element. The CSS now handles all sizing. | |
| 56 | + * @param {object} imageData - Data for the image (filename, isCustom). | |
| 57 | + * @returns {HTMLElement} The created thumbnail element. | |
| 58 | + */ | |
| 59 | +function createThumbnailElement(imageData) { | |
| 60 | + const bg = imageData.filename; | |
| 61 | + const isCustom = imageData.isCustom; | |
| 62 | + | |
| 63 | + const thumbnail = $('#background_template .bg_example').clone(); | |
| 64 | + | |
| 65 | + const clipper = document.createElement('div'); | |
| 66 | + clipper.className = 'thumbnail-clipper lazy-load-background'; | |
| 67 | + clipper.style.backgroundImage = PLACEHOLDER_IMAGE; | |
| 68 | + | |
| 69 | + const titleElement = thumbnail.find('.BGSampleTitle'); | |
| 70 | + clipper.appendChild(titleElement.get(0)); | |
| 71 | + thumbnail.append(clipper); | |
| 72 | + | |
| 73 | + const url = generateUrlParameter(bg, isCustom); | |
| 74 | + const title = isCustom ? bg.split('/').pop() : bg; | |
| 75 | + const friendlyTitle = title.slice(0, title.lastIndexOf('.')); | |
| 76 | + | |
| 77 | + thumbnail.attr('title', title); | |
| 78 | + thumbnail.attr('bgfile', bg); | |
| 79 | + thumbnail.attr('custom', String(isCustom)); | |
| 80 | + thumbnail.data('url', url); | |
| 81 | + titleElement.text(friendlyTitle); | |
| 82 | + | |
| 83 | + return thumbnail.get(0); | |
| 84 | +} | |
| 85 | + | |
| 86 | +/** | |
| 87 | + * Applies the thumbnail column count to the CSS and updates button states. | |
| 88 | + * @param {number} count - The number of columns to display. | |
| 89 | + */ | |
| 90 | +function applyThumbnailColumns(count) { | |
| 91 | + const newCount = Math.max(THUMBNAIL_COLUMNS_MIN, Math.min(count, THUMBNAIL_COLUMNS_MAX)); | |
| 92 | + background_settings.thumbnailColumns = newCount; | |
| 93 | + document.documentElement.style.setProperty('--bg-thumb-columns', newCount.toString()); | |
| 94 | + | |
| 95 | + $('#bg_thumb_zoom_in').prop('disabled', newCount <= THUMBNAIL_COLUMNS_MIN); | |
| 96 | + $('#bg_thumb_zoom_out').prop('disabled', newCount >= THUMBNAIL_COLUMNS_MAX); | |
| 97 | + | |
| 98 | + saveSettingsDebounced(); | |
| 99 | +} | |
| 100 | + | |
| 48 | 101 | export function loadBackgroundSettings(settings) { |
| 49 | 102 | let backgroundSettings = settings.background; |
| 50 | 103 | if (!backgroundSettings || !backgroundSettings.name || !backgroundSettings.url) { |
| @@ -56,10 +109,21 @@ export function loadBackgroundSettings(settings) { | ||
| 56 | 109 | if (!Object.hasOwn(backgroundSettings, 'animation')) { |
| 57 | 110 | backgroundSettings.animation = false; |
| 58 | 111 | } |
| 112 | + | |
| 113 | + // If a value is already saved, use it. Otherwise, determine default based on screen size. | |
| 114 | + let columns = backgroundSettings.thumbnailColumns; | |
| 115 | + if (!columns) { | |
| 116 | + const isNarrowScreen = window.matchMedia('(max-width: 480px)').matches; | |
| 117 | + columns = isNarrowScreen ? THUMBNAIL_COLUMNS_DEFAULT_MOBILE : THUMBNAIL_COLUMNS_DEFAULT_DESKTOP; | |
| 118 | + } | |
| 119 | + background_settings.thumbnailColumns = columns; | |
| 120 | + applyThumbnailColumns(background_settings.thumbnailColumns); | |
| 121 | + | |
| 59 | 122 | setBackground(backgroundSettings.name, backgroundSettings.url); |
| 60 | 123 | setFittingClass(backgroundSettings.fitting); |
| 61 | 124 | $('#background_fitting').val(backgroundSettings.fitting); |
| 62 | 125 | $('#background_thumbnails_animation').prop('checked', background_settings.animation); |
| 126 | + highlightSelectedBackground(); | |
| 63 | 127 | } |
| 64 | 128 | |
| 65 | 129 | /** |
| @@ -68,46 +132,26 @@ export function loadBackgroundSettings(settings) { | ||
| 68 | 132 | */ |
| 69 | 133 | async function forceSetBackground(backgroundInfo) { |
| 70 | 134 | saveBackgroundMetadata(backgroundInfo.url); |
| 71 | - setCustomBackground(); | |
| 135 | + $('#bg1').css('background-image', backgroundInfo.url); | |
| 72 | 136 | |
| 73 | 137 | const list = chat_metadata[LIST_METADATA_KEY] || []; |
| 74 | 138 | const bg = backgroundInfo.path; |
| 75 | 139 | list.push(bg); |
| 76 | 140 | chat_metadata[LIST_METADATA_KEY] = list; |
| 77 | 141 | saveMetadataDebounced(); |
| 78 | 142 | await getChatBackgroundsListrenderChatBackgrounds(); |
| 79 | 143 | highlightNewBackground(bg); |
| 80 | 144 | highlightLockedBackground(); |
| 81 | 145 | } |
| 82 | 146 | |
| 83 | 147 | async function onChatChanged() { |
| 84 | - if (hasCustomBackground()) { | |
| 148 | + const lockedUrl = chat_metadata[BG_METADATA_KEY]; | |
| 85 | - setCustomBackground(); | |
| 86 | - } | |
| 87 | - else { | |
| 88 | - unsetCustomBackground(); | |
| 89 | - } | |
| 90 | - | |
| 91 | - await getChatBackgroundsList(); | |
| 92 | - highlightLockedBackground(); | |
| 93 | -} | |
| 94 | 149 | |
| 95 | -async function getChatBackgroundsList() { | |
| 150 | + $('#bg1').css('background-image', lockedUrl || background_settings.url); | |
| 96 | - const list = chat_metadata[LIST_METADATA_KEY]; | |
| 97 | - const listEmpty = !Array.isArray(list) || list.length === 0; | |
| 98 | 151 | |
| 99 | - $('#bg_custom_content').empty(); | |
| 152 | + renderChatBackgrounds(); | |
| 100 | - $('#bg_chat_hint').toggle(listEmpty); | |
| 153 | + highlightLockedBackground(); | |
| 101 | - | |
| 154 | + highlightSelectedBackground(); | |
| 102 | - if (listEmpty) { | |
| 103 | - return; | |
| 104 | - } | |
| 105 | - | |
| 106 | - for (const bg of list) { | |
| 107 | - const template = await getBackgroundFromTemplate(bg, true); | |
| 108 | - $('#bg_custom_content').append(template); | |
| 109 | - } | |
| 110 | - activateLazyLoader(); | |
| 111 | 155 | } |
| 112 | 156 | |
| 113 | 157 | function getBackgroundPath(fileUrl) { |
| @@ -115,59 +159,53 @@ function getBackgroundPath(fileUrl) { | ||
| 115 | 159 | } |
| 116 | 160 | |
| 117 | 161 | function highlightLockedBackground() { |
| 118 | 162 | $('.bg_example.locked-background').removeClass('locked-background'); |
| 119 | 163 | |
| 120 | 164 | const lockedBackgroundlockedBackgroundUrl = chat_metadata[BG_METADATA_KEY]; |
| 121 | 165 | |
| 122 | 166 | if (!lockedBackgroundlockedBackgroundUrl) { |
| 123 | - return; | |
| 167 | + $('.bg_example').filter(function () { | |
| 168 | + return $(this).data('url') === lockedBackgroundUrl; | |
| 169 | + }).addClass('locked-background'); | |
| 124 | 170 | } |
| 125 | - | |
| 126 | - $('.bg_example').each(function () { | |
| 127 | - const url = $(this).data('url'); | |
| 128 | - if (url === lockedBackground) { | |
| 129 | - $(this).addClass('locked'); | |
| 130 | - } | |
| 131 | - }); | |
| 132 | 171 | } |
| 133 | 172 | |
| 134 | 173 | /** |
| 135 | 174 | * Locks the background for the current chat |
| 136 | 175 | * @param {Event|null} e Click event |
| 137 | - * @returns {string} Empty string | |
| 138 | 176 | */ |
| 139 | 177 | function onLockBackgroundClick(eevent = null) { |
| 140 | - e?.stopPropagation(); | |
| 178 | + if (!getCurrentChatId()) { | |
| 141 | - | |
| 179 | + toastr.warning(t`Select a chat to lock the background for it`); | |
| 142 | - const chatName = getCurrentChatId(); | |
| 180 | + return; | |
| 143 | - | |
| 144 | - if (!chatName) { | |
| 145 | - toastr.warning('Select a chat to lock the background for it'); | |
| 146 | - return ''; | |
| 147 | 181 | } |
| 148 | 182 | |
| 149 | - const relativeBgImage = getUrlParameter(this) ?? background_settings.url; | |
| 183 | + // Take the global background's URL and save it to the chat's metadata. | |
| 184 | + const urlToLock = event ? $(event.target).closest('.bg_example').data('url') : background_settings.url; | |
| 185 | + saveBackgroundMetadata(urlToLock); | |
| 186 | + $('#bg1').css('background-image', urlToLock); | |
| 150 | 187 | |
| 151 | - saveBackgroundMetadata(relativeBgImage); | |
| 188 | + // Update UI states to reflect the new lock. | |
| 152 | - setCustomBackground(); | |
| 153 | 189 | highlightLockedBackground(); |
| 154 | - return ''; | |
| 155 | 190 | } |
| 156 | 191 | |
| 157 | 192 | /** |
| 158 | 193 | * LocksUnlocks the background for the current chat |
| 159 | 194 | * @param {Event|null} e Click event_event |
| 160 | - * @returns {string} Empty string | |
| 161 | 195 | */ |
| 162 | 196 | function onUnlockBackgroundClick(e_event = null) { |
| 163 | - e?.stopPropagation(); | |
| 197 | + // Delete the lock from the chat's metadata. | |
| 164 | 198 | removeBackgroundMetadata(); |
| 165 | - unsetCustomBackground(); | |
| 199 | + | |
| 200 | + // Revert the view to the current global background. | |
| 201 | + $('#bg1').css('background-image', background_settings.url); | |
| 202 | + | |
| 203 | + // Update UI states to reflect the removal of the lock. | |
| 166 | 204 | highlightLockedBackground(); |
| 167 | - return ''; | |
| 205 | + highlightSelectedBackground(); | |
| 168 | 206 | } |
| 169 | 207 | |
| 170 | 208 | function hasCustomBackgroundisChatBackgroundLocked() { |
| 171 | 209 | return chat_metadata[BG_METADATA_KEY]; |
| 172 | 210 | } |
| 173 | 211 | |
| @@ -181,54 +219,22 @@ function removeBackgroundMetadata() { | ||
| 181 | 219 | saveMetadataDebounced(); |
| 182 | 220 | } |
| 183 | 221 | |
| 184 | -function setCustomBackground() { | |
| 185 | - const file = chat_metadata[BG_METADATA_KEY]; | |
| 186 | - | |
| 187 | - // bg already set | |
| 188 | - if (document.getElementById('bg_custom').style.backgroundImage == file) { | |
| 189 | - return; | |
| 190 | - } | |
| 191 | - | |
| 192 | - $('#bg_custom').css('background-image', file); | |
| 193 | -} | |
| 194 | - | |
| 195 | -function unsetCustomBackground() { | |
| 196 | - $('#bg_custom').css('background-image', 'none'); | |
| 197 | -} | |
| 198 | - | |
| 199 | 222 | function onSelectBackgroundClick() { |
| 200 | 223 | const isCustombgFile = $(this).attr('custombgfile') === 'true'; |
| 201 | 224 | const relativeBgImagebackgroundCssUrl = getUrlParameter(this); |
| 202 | - | |
| 203 | - // if clicked on upload button | |
| 204 | - if (!relativeBgImage) { | |
| 205 | - return; | |
| 206 | - } | |
| 207 | 225 | |
| 208 | - // Automatically lock the background if it's custom or other background is locked | |
| 226 | + if (isChatBackgroundLocked()) { | |
| 209 | - if (hasCustomBackground() || isCustom) { | |
| 227 | + // If a background is locked, update the locked background directly | |
| 210 | 228 | saveBackgroundMetadata(relativeBgImagebackgroundCssUrl); |
| 211 | - setCustomBackground(); | |
| 229 | + $('#bg1').css('background-image', backgroundCssUrl); | |
| 212 | 230 | highlightLockedBackground(); |
| 231 | + } else { | |
| 232 | + // Otherwise, update the global background setting | |
| 233 | + setBackground(bgFile, backgroundCssUrl); | |
| 213 | 234 | } |
| 214 | - highlightLockedBackground(); | |
| 215 | - | |
| 216 | - const customBg = window.getComputedStyle(document.getElementById('bg_custom')).backgroundImage; | |
| 217 | - | |
| 218 | - // Custom background is set. Do not override the layer below | |
| 219 | - if (customBg !== 'none') { | |
| 220 | - return; | |
| 221 | - } | |
| 222 | - | |
| 223 | - const bgFile = $(this).attr('bgfile'); | |
| 224 | - const backgroundUrl = getBackgroundPath(bgFile); | |
| 225 | 235 | |
| 226 | 236 | // Fetching toUpdate browserUI memoryhighlights to reducereflect flickerthe changes. |
| 227 | - fetch(backgroundUrl).then(() => { | |
| 237 | + highlightSelectedBackground(); | |
| 228 | - setBackground(bgFile, relativeBgImage); | |
| 229 | - }).catch(() => { | |
| 230 | - console.log('Background could not be set: ' + backgroundUrl); | |
| 231 | - }); | |
| 232 | 238 | } |
| 233 | 239 | |
| 234 | 240 | async function onCopyToSystemBackgroundClick(e) { |
| @@ -257,7 +263,7 @@ async function onCopyToSystemBackgroundClick(e) { | ||
| 257 | 263 | const index = list.indexOf(bgNames.oldBg); |
| 258 | 264 | list.splice(index, 1); |
| 259 | 265 | saveMetadataDebounced(); |
| 260 | 266 | await getChatBackgroundsListrenderChatBackgrounds(); |
| 261 | 267 | } |
| 262 | 268 | |
| 263 | 269 | /** |
| @@ -382,29 +388,32 @@ async function onDeleteBackgroundClick(e) { | ||
| 382 | 388 | list.splice(index, 1); |
| 383 | 389 | } |
| 384 | 390 | |
| 385 | - const siblingSelector = '.bg_example:not(#form_bg_download)'; | |
| 391 | + if (bg === background_settings.name) { | |
| 386 | 392 | const nextBgsiblingSelector = bgToDelete'.next(siblingSelector)bg_example'; |
| 387 | 393 | const prevBgnextBg = bgToDelete.prevnext(siblingSelector); |
| 388 | 394 | const anyBgprevBg = $bgToDelete.prev(siblingSelector); |
| 389 | 395 | |
| 390 | 396 | if (nextBg.length > 0) { |
| 391 | 397 | nextBg.trigger('click'); |
| 392 | 398 | } else if (prevBg.length > 0) { |
| 393 | 399 | prevBg.trigger('click'); |
| 394 | 400 | } else { |
| 395 | - $(anyBg[Math.floor(Math.random() * anyBg.length)]).trigger('click'); | |
| 401 | + const anyOtherBg = $('.bg_example').not(bgToDelete).first(); | |
| 402 | + if (anyOtherBg.length > 0) { | |
| 403 | + anyOtherBg.trigger('click'); | |
| 404 | + } | |
| 405 | + } | |
| 396 | 406 | } |
| 397 | 407 | |
| 398 | 408 | bgToDelete.remove(); |
| 399 | 409 | |
| 400 | 410 | if (url === chat_metadata[BG_METADATA_KEY]) { |
| 401 | 411 | removeBackgroundMetadata(); |
| 402 | - unsetCustomBackground(); | |
| 403 | 412 | highlightLockedBackground(); |
| 404 | 413 | } |
| 405 | 414 | |
| 406 | 415 | if (isCustom) { |
| 407 | 416 | await getChatBackgroundsListrenderChatBackgrounds(); |
| 408 | 417 | saveMetadataDebounced(); |
| 409 | 418 | } |
| 410 | 419 | } |
| @@ -445,6 +454,47 @@ async function autoBackgroundCommand() { | ||
| 445 | 454 | return ''; |
| 446 | 455 | } |
| 447 | 456 | |
| 457 | +/** | |
| 458 | + * Renders the system backgrounds gallery. | |
| 459 | + * @param {string[]} [backgrounds] - Optional filtered list of backgrounds. | |
| 460 | + */ | |
| 461 | +function renderSystemBackgrounds(backgrounds) { | |
| 462 | + const sourceList = backgrounds || []; | |
| 463 | + const container = $('#bg_menu_content'); | |
| 464 | + container.empty(); | |
| 465 | + | |
| 466 | + if (sourceList.length === 0) return; | |
| 467 | + | |
| 468 | + sourceList.forEach(bg => { | |
| 469 | + const imageData = { filename: bg, isCustom: false }; | |
| 470 | + const thumbnail = createThumbnailElement(imageData); | |
| 471 | + container.append(thumbnail); | |
| 472 | + }); | |
| 473 | + | |
| 474 | + activateLazyLoader(); | |
| 475 | +} | |
| 476 | + | |
| 477 | +/** | |
| 478 | + * Renders the chat-specific (custom) backgrounds gallery. | |
| 479 | + * @param {string[]} [backgrounds] - Optional filtered list of backgrounds. | |
| 480 | + */ | |
| 481 | +function renderChatBackgrounds(backgrounds) { | |
| 482 | + const sourceList = backgrounds ?? (chat_metadata[LIST_METADATA_KEY] || []); | |
| 483 | + const container = $('#bg_custom_content'); | |
| 484 | + container.empty(); | |
| 485 | + $('#bg_chat_hint').toggle(!sourceList.length); | |
| 486 | + | |
| 487 | + if (sourceList.length === 0) return; | |
| 488 | + | |
| 489 | + sourceList.forEach(bg => { | |
| 490 | + const imageData = { filename: bg, isCustom: true }; | |
| 491 | + const thumbnail = createThumbnailElement(imageData); | |
| 492 | + container.append(thumbnail); | |
| 493 | + }); | |
| 494 | + | |
| 495 | + activateLazyLoader(); | |
| 496 | +} | |
| 497 | + | |
| 448 | 498 | export async function getBackgrounds() { |
| 449 | 499 | const response = await fetch('/api/backgrounds/all', { |
| 450 | 500 | method: 'POST', |
| @@ -454,12 +504,9 @@ export async function getBackgrounds() { | ||
| 454 | 504 | if (response.ok) { |
| 455 | 505 | const { images, config } = await response.json(); |
| 456 | 506 | Object.assign(THUMBNAIL_CONFIG, config); |
| 457 | - $('#bg_menu_content').children('div').remove(); | |
| 507 | + | |
| 458 | - for (const bg of images) { | |
| 508 | + renderSystemBackgrounds(images); | |
| 459 | - const template = await getBackgroundFromTemplate(bg, false); | |
| 509 | + highlightSelectedBackground(); | |
| 460 | - $('#bg_menu_content').append(template); | |
| 461 | - } | |
| 462 | - activateLazyLoader(); | |
| 463 | 510 | } |
| 464 | 511 | } |
| 465 | 512 | |
| @@ -481,14 +528,19 @@ function activateLazyLoader() { | ||
| 481 | 528 | lazyLoadObserver = new IntersectionObserver((entries, observer) => { |
| 482 | 529 | entries.forEach(entry => { |
| 483 | 530 | if (entry.target instanceof HTMLElement && entry.isIntersecting) { |
| 484 | 531 | const targetclipper = entry.target; |
| 485 | 532 | const bgparentThumbnail = targetclipper.getAttributeclosest('bgfile.bg_example'); |
| 486 | - const isCustom = target.getAttribute('custom') === 'true'; | |
| 533 | + | |
| 487 | - resolveImageUrl(bg, isCustom) | |
| 534 | + if (parentThumbnail) { | |
| 488 | - .then(url => { target.style.backgroundImage = url; }) | |
| 535 | + const bg = parentThumbnail.getAttribute('bgfile'); | |
| 489 | - .catch(() => { target.style.backgroundImage = PLACEHOLDER_IMAGE; }); | |
| 536 | + const isCustom = parentThumbnail.getAttribute('custom') === 'true'; | |
| 490 | - target.classList.remove('lazy-load-background'); | |
| 537 | + resolveImageUrl(bg, isCustom) | |
| 491 | - observer.unobserve(target); | |
| 538 | + .then(url => { clipper.style.backgroundImage = url; }) | |
| 539 | + .catch(() => { clipper.style.backgroundImage = PLACEHOLDER_IMAGE; }); | |
| 540 | + } | |
| 541 | + | |
| 542 | + clipper.classList.remove('lazy-load-background'); | |
| 543 | + observer.unobserve(clipper); | |
| 492 | 544 | } |
| 493 | 545 | }); |
| 494 | 546 | }, options); |
| @@ -529,30 +581,11 @@ async function resolveImageUrl(bg, isCustom) { | ||
| 529 | 581 | return `url("${thumbnailUrl}")`; |
| 530 | 582 | } |
| 531 | 583 | |
| 532 | -/** | |
| 533 | - * Instantiates a background template | |
| 534 | - * @param {string} bg Path to background | |
| 535 | - * @param {boolean} isCustom Whether the background is custom | |
| 536 | - * @returns {Promise<JQuery<HTMLElement>>} Background template | |
| 537 | - */ | |
| 538 | -async function getBackgroundFromTemplate(bg, isCustom) { | |
| 539 | - const template = $('#background_template .bg_example').clone(); | |
| 540 | - const url = generateUrlParameter(bg, isCustom); | |
| 541 | - const title = isCustom ? bg.split('/').pop() : bg; | |
| 542 | - const friendlyTitle = title.slice(0, title.lastIndexOf('.')); | |
| 543 | - | |
| 544 | - template.attr('title', title); | |
| 545 | - template.attr('bgfile', bg); | |
| 546 | - template.attr('custom', String(isCustom)); | |
| 547 | - template.data('url', url); | |
| 548 | - template.addClass('lazy-load-background'); | |
| 549 | - template.css('background-image', PLACEHOLDER_IMAGE); | |
| 550 | - template.find('.BGSampleTitle').text(friendlyTitle); | |
| 551 | - return template; | |
| 552 | -} | |
| 553 | - | |
| 554 | 584 | async function setBackground(bg, url) { |
| 555 | - $('#bg1').css('background-image', url); | |
| 585 | + // Only change the visual background if one is not locked for the current chat. | |
| 586 | + if (!isChatBackgroundLocked()) { | |
| 587 | + $('#bg1').css('background-image', url); | |
| 588 | + } | |
| 556 | 589 | background_settings.name = bg; |
| 557 | 590 | background_settings.url = url; |
| 558 | 591 | saveSettingsDebounced(); |
| @@ -680,25 +713,39 @@ function highlightNewBackground(bg) { | ||
| 680 | 713 | * @param {string} fitting Fitting type |
| 681 | 714 | */ |
| 682 | 715 | function setFittingClass(fitting) { |
| 683 | 716 | const backgrounds = $('#bg1, #bg_custom'); |
| 684 | 717 | for (const option of ['cover', 'contain', 'stretch', 'center']) { |
| 685 | 718 | backgrounds.toggleClass(option, option === fitting); |
| 686 | 719 | } |
| 687 | 720 | background_settings.fitting = fitting; |
| 688 | 721 | } |
| 689 | 722 | |
| 723 | +function highlightSelectedBackground() { | |
| 724 | + $('.bg_example.selected-background').removeClass('selected-background'); | |
| 725 | + | |
| 726 | + // The "selected" highlight should always reflect the global background setting. | |
| 727 | + const activeUrl = background_settings.url; | |
| 728 | + | |
| 729 | + if (activeUrl) { | |
| 730 | + // Find the thumbnail whose data-url attribute matches the active URL | |
| 731 | + $('.bg_example').filter(function () { | |
| 732 | + return $(this).data('url') === activeUrl; | |
| 733 | + }).addClass('selected-background'); | |
| 734 | + } | |
| 735 | +} | |
| 736 | + | |
| 690 | 737 | function onBackgroundFilterInput() { |
| 691 | 738 | const filterValue = String($(this'#bg-filter').val()).toLowerCase(); |
| 692 | 739 | $('#bg_menu_content > div.bg_example, #bg_custom_content > .bg_example').each(function () { |
| 693 | 740 | const $bgContentbg = $(this); |
| 694 | - if ($bgContent.attr('title').toLowerCase().includes(filterValue)) { | |
| 741 | + const title = $bg.attr('title') || ''; | |
| 695 | - $bgContent.show(); | |
| 742 | + const hasMatch = title.toLowerCase().includes(filterValue); | |
| 696 | - } else { | |
| 743 | + $bg.toggle(hasMatch); | |
| 697 | - $bgContent.hide(); | |
| 698 | - } | |
| 699 | 744 | }); |
| 700 | 745 | } |
| 701 | 746 | |
| 747 | +const debouncedOnBackgroundFilterInput = debounce(onBackgroundFilterInput, debounce_timeout.standard); | |
| 748 | + | |
| 702 | 749 | export function initBackgrounds() { |
| 703 | 750 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); |
| 704 | 751 | eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground); |
| @@ -715,6 +762,11 @@ export function initBackgrounds() { | ||
| 715 | 762 | $context.addClass('mobile-menu-open'); |
| 716 | 763 | } |
| 717 | 764 | }) |
| 765 | + .off('blur', '.bg_example.mobile-menu-open').on('blur', '.bg_example.mobile-menu-open', function () { | |
| 766 | + if (!$(this).is(':focus-within')) { | |
| 767 | + $(this).removeClass('mobile-menu-open'); | |
| 768 | + } | |
| 769 | + }) | |
| 718 | 770 | .off('click', '.jg-button').on('click', '.jg-button', function (e) { |
| 719 | 771 | e.stopPropagation(); |
| 720 | 772 | const action = $(this).data('action'); |
| @@ -738,18 +790,30 @@ export function initBackgrounds() { | ||
| 738 | 790 | } |
| 739 | 791 | }); |
| 740 | 792 | |
| 793 | + $('#bg_thumb_zoom_in').on('click', () => { | |
| 794 | + applyThumbnailColumns(background_settings.thumbnailColumns - 1); | |
| 795 | + }); | |
| 796 | + $('#bg_thumb_zoom_out').on('click', () => { | |
| 797 | + applyThumbnailColumns(background_settings.thumbnailColumns + 1); | |
| 798 | + }); | |
| 741 | 799 | $('#auto_background').on('click', autoBackgroundCommand); |
| 742 | 800 | $('#add_bg_button').on('change', onBackgroundUploadSelected); |
| 743 | 801 | $('#bg-filter').on('input', onBackgroundFilterInput() => debouncedOnBackgroundFilterInput()); |
| 744 | 802 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 745 | 803 | name: 'lockbg', |
| 746 | 804 | callback: () => onLockBackgroundClick(new CustomEvent('click')),{ |
| 805 | + onLockBackgroundClick(); | |
| 806 | + return ''; | |
| 807 | + }, | |
| 747 | 808 | aliases: ['bglock'], |
| 748 | 809 | helpString: 'Locks a background for the currently selected chat', |
| 749 | 810 | })); |
| 750 | 811 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 751 | 812 | name: 'unlockbg', |
| 752 | 813 | callback: () => onUnlockBackgroundClick(new CustomEvent('click')),{ |
| 814 | + onUnlockBackgroundClick(); | |
| 815 | + return ''; | |
| 816 | + }, | |
| 753 | 817 | aliases: ['bgunlock'], |
| 754 | 818 | helpString: 'Unlocks a background for the currently selected chat', |
| 755 | 819 | })); |
| @@ -774,4 +838,10 @@ export function initBackgrounds() { | ||
| 774 | 838 | await getBackgrounds(); |
| 775 | 839 | await onChatChanged(); |
| 776 | 840 | }); |
| 841 | + | |
| 842 | + setupScrollToTop({ | |
| 843 | + scrollContainerId: 'bg-scrollable-content', | |
| 844 | + buttonId: 'bg-scroll-top', | |
| 845 | + drawerId: 'Backgrounds', | |
| 846 | + }); | |
| 777 | 847 | } |
| @@ -750,10 +750,9 @@ function getStyleContentsFromMarkdown(text) { | ||
| 750 | 750 | return ''; |
| 751 | 751 | } |
| 752 | 752 | |
| 753 | - const div = document.createElement('div'); | |
| 754 | 753 | const html = converter.makeHtml(substituteParams(text)); |
| 755 | - div.innerHTML = html; | |
| 754 | + const parsedDocument = new DOMParser().parseFromString(html, 'text/html'); | |
| 756 | 755 | const styleElements = Array.from(divparsedDocument.querySelectorAll('style')); |
| 757 | 756 | return styleElements |
| 758 | 757 | .filter(s => s.textContent.trim().length > 0) |
| 759 | 758 | .map(s => s.textContent.trim()) |
| @@ -51,6 +51,7 @@ export const inject_ids = { | ||
| 51 | 51 | DEPTH_PROMPT_INDEX: (index) => `DEPTH_PROMPT_${index}`, |
| 52 | 52 | CUSTOM_WI_DEPTH: 'customDepthWI', |
| 53 | 53 | CUSTOM_WI_DEPTH_ROLE: (depth, role) => `customDepthWI_${depth}_${role}`, |
| 54 | + CUSTOM_WI_OUTLET: (key) => `customWIOutlet_${key}`, | |
| 54 | 55 | }; |
| 55 | 56 | |
| 56 | 57 | export const COMETAPI_IGNORE_PATTERNS = [ |
| @@ -49,6 +49,7 @@ import EventSourceStream from './sse-stream.js'; | ||
| 49 | 49 | * @property {string} [custom_url] - Optional custom URL |
| 50 | 50 | * @property {string} [reverse_proxy] - Optional reverse proxy URL |
| 51 | 51 | * @property {string} [proxy_password] - Optional proxy password |
| 52 | + * @property {string} [custom_prompt_post_processing] - Optional custom prompt post-processing | |
| 52 | 53 | */ |
| 53 | 54 | |
| 54 | 55 | /** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */ |
| @@ -414,7 +415,7 @@ export class ChatCompletionService { | ||
| 414 | 415 | * @param {ChatCompletionPayload} custom |
| 415 | 416 | * @returns {ChatCompletionPayload} |
| 416 | 417 | */ |
| 417 | 418 | static createRequestData({ stream = false, messages, model, chat_completion_source, max_tokens, temperature, custom_url, reverse_proxy, proxy_password, custom_prompt_post_processing, ...props }) { |
| 418 | 419 | const payload = { |
| 419 | 420 | stream, |
| 420 | 421 | messages, |
| @@ -425,6 +426,7 @@ export class ChatCompletionService { | ||
| 425 | 426 | custom_url, |
| 426 | 427 | reverse_proxy, |
| 427 | 428 | proxy_password, |
| 429 | + custom_prompt_post_processing, | |
| 428 | 430 | use_makersuite_sysprompt: true, |
| 429 | 431 | claude_use_sysprompt: true, |
| 430 | 432 | ...props, |
| @@ -0,0 +1,66 @@ | ||
| 1 | +import { throttle } from './utils.js'; | |
| 2 | + | |
| 3 | +export function initDomHandlers() { | |
| 4 | + handleInputWheel(); | |
| 5 | +} | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * Trap mouse wheel inside of focused number inputs to prevent scrolling their containers. | |
| 9 | + * Instead of firing wheel events, manually update both slider and input values. | |
| 10 | + * This also makes wheel work inside Firefox. | |
| 11 | + */ | |
| 12 | +function handleInputWheel() { | |
| 13 | + const minInterval = 25; // ms | |
| 14 | + | |
| 15 | + /** | |
| 16 | + * Update input and slider values based on wheel delta | |
| 17 | + * @param {HTMLInputElement} input The number input element | |
| 18 | + * @param {HTMLInputElement|null} slider The associated range input element, if any | |
| 19 | + * @param {number} deltaY The wheel deltaY value | |
| 20 | + */ | |
| 21 | + function updateValue(input, slider, deltaY) { | |
| 22 | + const currentValue = parseFloat(input.value); | |
| 23 | + const step = parseFloat(input.step); | |
| 24 | + const min = parseFloat(input.min); | |
| 25 | + const max = parseFloat(input.max); | |
| 26 | + | |
| 27 | + // Sanity checks before trying to calculate new value | |
| 28 | + if (isNaN(currentValue) || isNaN(step) || step <= 0 || deltaY === 0) return; | |
| 29 | + | |
| 30 | + // Calculate new value based on wheel movement delta (negative = up, positive = down) | |
| 31 | + let newValue = currentValue + (deltaY > 0 ? -step : step); | |
| 32 | + // Ensure it's a multiple of step | |
| 33 | + newValue = Math.round(newValue / step) * step; | |
| 34 | + // Ensure it's within the min and max range (NaN-aware) | |
| 35 | + newValue = !isNaN(min) ? Math.max(newValue, min) : newValue; | |
| 36 | + newValue = !isNaN(max) ? Math.min(newValue, max) : newValue; | |
| 37 | + // Simple fix for floating point precision issues | |
| 38 | + newValue = Math.round(newValue * 1e10) / 1e10; | |
| 39 | + | |
| 40 | + // Update both input and slider values | |
| 41 | + input.value = newValue.toString(); | |
| 42 | + if (slider) slider.value = newValue.toString(); | |
| 43 | + // Trigger input event (just ONE) to update any listeners | |
| 44 | + const inputEvent = new Event('input', { bubbles: true }); | |
| 45 | + input.dispatchEvent(inputEvent); | |
| 46 | + } | |
| 47 | + | |
| 48 | + const updateValueThrottled = throttle(updateValue, minInterval); | |
| 49 | + | |
| 50 | + document.addEventListener('wheel', (e) => { | |
| 51 | + // Try to carefully narrow down if we even need to fire this handler | |
| 52 | + const input = document.activeElement instanceof HTMLInputElement ? document.activeElement : null; | |
| 53 | + if (input && input.type === 'number' && input.hasAttribute('step')) { | |
| 54 | + const parent = input.closest('.range-block-range-and-counter') ?? input.closest('div') ?? input.parentElement; | |
| 55 | + const slider = /** @type {HTMLInputElement} */ (parent?.querySelector('input[type="range"]')); | |
| 56 | + | |
| 57 | + // Stop propagation for either target | |
| 58 | + if (e.target === input || (slider && e.target === slider)) { | |
| 59 | + e.stopPropagation(); | |
| 60 | + e.preventDefault(); | |
| 61 | + | |
| 62 | + updateValueThrottled(input, slider, e.deltaY); | |
| 63 | + } | |
| 64 | + } | |
| 65 | + }, { passive: false }); | |
| 66 | +} | |
| @@ -12,6 +12,7 @@ export const event_types = { | ||
| 12 | 12 | MESSAGE_FILE_EMBEDDED: 'message_file_embedded', |
| 13 | 13 | MESSAGE_REASONING_EDITED: 'message_reasoning_edited', |
| 14 | 14 | MESSAGE_REASONING_DELETED: 'message_reasoning_deleted', |
| 15 | + MESSAGE_SWIPE_DELETED: 'message_swipe_deleted', | |
| 15 | 16 | MORE_MESSAGES_LOADED: 'more_messages_loaded', |
| 16 | 17 | IMPERSONATE_READY: 'impersonate_ready', |
| 17 | 18 | CHAT_CHANGED: 'chat_id_changed', |
| @@ -36,6 +37,7 @@ export const event_types = { | ||
| 36 | 37 | OAI_PRESET_IMPORT_READY: 'oai_preset_import_ready', |
| 37 | 38 | WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated', |
| 38 | 39 | WORLDINFO_UPDATED: 'worldinfo_updated', |
| 40 | + CHARACTER_EDITOR_OPENED: 'character_editor_opened', | |
| 39 | 41 | CHARACTER_EDITED: 'character_edited', |
| 40 | 42 | CHARACTER_PAGE_LOADED: 'character_page_loaded', |
| 41 | 43 | CHARACTER_GROUP_OVERLAY_STATE_CHANGE_BEFORE: 'character_group_overlay_state_change_before', |
| @@ -85,6 +87,8 @@ export const event_types = { | ||
| 85 | 87 | SECRET_EDITED: 'secret_edited', |
| 86 | 88 | PRESET_CHANGED: 'preset_changed', |
| 87 | 89 | PRESET_DELETED: 'preset_deleted', |
| 90 | + PRESET_RENAMED: 'preset_renamed', | |
| 91 | + PRESET_RENAMED_BEFORE: 'preset_renamed_before', | |
| 88 | 92 | MAIN_API_CHANGED: 'main_api_changed', |
| 89 | 93 | WORLDINFO_ENTRIES_LOADED: 'worldinfo_entries_loaded', |
| 90 | 94 | }; |
| @@ -190,7 +190,10 @@ export const extension_settings = { | ||
| 190 | 190 | regex: [], |
| 191 | 191 | /** @type {import('./extensions/regex/index.js').RegexPreset[]} */ |
| 192 | 192 | regex_presets: [], |
| 193 | + /** @type {string[]} */ | |
| 193 | 194 | character_allowed_regex: [], |
| 195 | + /** @type {Record<string, string[]>} */ | |
| 196 | + preset_allowed_regex: {}, | |
| 194 | 197 | tts: {}, |
| 195 | 198 | sd: { |
| 196 | 199 | prompts: {}, |
| @@ -10,7 +10,7 @@ import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js'; | ||
| 10 | 10 | import { executeSlashCommandsWithOptions } from '../../slash-commands.js'; |
| 11 | 11 | import { accountStorage } from '../../util/AccountStorage.js'; |
| 12 | 12 | import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js'; |
| 13 | 13 | import { t, translate } from '../../i18n.js'; |
| 14 | 14 | export { MODULE_NAME }; |
| 15 | 15 | |
| 16 | 16 | const MODULE_NAME = 'assets'; |
| @@ -60,6 +60,36 @@ const KNOWN_TYPES = { | ||
| 60 | 60 | 'blip': t`Blip sounds`, |
| 61 | 61 | }; |
| 62 | 62 | |
| 63 | +const EMPTY_AUTHOR = { | |
| 64 | + name: '', | |
| 65 | + url: '', | |
| 66 | +}; | |
| 67 | + | |
| 68 | +/** | |
| 69 | + * Extracts the repository author from a given URL. | |
| 70 | + * @param {string} url - The URL of the repository. | |
| 71 | + * @returns {{name: string, url: string}} Object containing the author's name and URL, or empty strings if not found. | |
| 72 | + */ | |
| 73 | +function getAuthorFromUrl(url) { | |
| 74 | + const result = structuredClone(EMPTY_AUTHOR); | |
| 75 | + | |
| 76 | + try { | |
| 77 | + const parsedUrl = new URL(url); | |
| 78 | + const pathSegments = parsedUrl.pathname.split('/').filter(s => s.length > 0); | |
| 79 | + | |
| 80 | + // TODO: Handle non-GitHub URLs if needed | |
| 81 | + if (parsedUrl.host === 'github.com' && pathSegments.length >= 2) { | |
| 82 | + result.name = pathSegments[0]; | |
| 83 | + result.url = `${parsedUrl.protocol}//${parsedUrl.hostname}/${result.name}`; | |
| 84 | + } | |
| 85 | + } | |
| 86 | + catch (error) { | |
| 87 | + console.debug(DEBUG_PREFIX, 'Error parsing URL:', error); | |
| 88 | + } | |
| 89 | + | |
| 90 | + return result; | |
| 91 | +} | |
| 92 | + | |
| 63 | 93 | async function downloadAssetsList(url) { |
| 64 | 94 | updateCurrentAssets().then(async function () { |
| 65 | 95 | fetch(url, { cache: 'no-cache' }) |
| @@ -88,7 +118,8 @@ async function downloadAssetsList(url) { | ||
| 88 | 118 | $('#assets_type_select').append($('<option />', { value: '', text: t`All` })); |
| 89 | 119 | |
| 90 | 120 | for (const type of assetTypes) { |
| 91 | 121 | const optiontext = $('<option />', { value: type, text: ttranslate([KNOWN_TYPES[type] || type]) }); |
| 122 | + const option = $('<option />', { value: type, text: text }); | |
| 92 | 123 | $('#assets_type_select').append(option); |
| 93 | 124 | } |
| 94 | 125 | |
| @@ -184,10 +215,11 @@ async function downloadAssetsList(url) { | ||
| 184 | 215 | const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`; |
| 185 | 216 | const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple'; |
| 186 | 217 | const toolTag = assetType === 'extension' && asset['tool']; |
| 218 | + const author = url && assetType === 'extension' ? getAuthorFromUrl(url) : EMPTY_AUTHOR; | |
| 187 | 219 | |
| 188 | 220 | const assetBlock = $('<i></i>') |
| 189 | 221 | .append(element) |
| 190 | 222 | .append(`<div class="flex-container flexFlowColumn flexNoGap wide100p overflowHidden"> |
| 191 | 223 | <span class="asset-name flex-container alignitemscenter"> |
| 192 | 224 | <b>${displayName}</b> |
| 193 | 225 | <a class="asset_preview" href="${url}" target="_blank" title="${title}"> |
| @@ -195,6 +227,8 @@ async function downloadAssetsList(url) { | ||
| 195 | 227 | </a>` + |
| 196 | 228 | (toolTag ? '<span class="tag" title="' + t`Adds a function tool` + '"><i class="fa-solid fa-sm fa-wrench"></i> ' + |
| 197 | 229 | t`Tool` + '</span>' : '') + |
| 230 | + '<span class="expander"></span>' + | |
| 231 | + (author.name ? `<a href="${author.url}" target="_blank" class="asset-author-info"><i class="fa-solid fa-at fa-xs"></i><span>${author.name}</span></a>` : '') + | |
| 198 | 232 | `</span> |
| 199 | 233 | <small class="asset-description"> |
| 200 | 234 | ${description} |
| @@ -35,14 +35,15 @@ | ||
| 35 | 35 | color: inherit; |
| 36 | 36 | } |
| 37 | 37 | |
| 38 | 38 | .assets-list-div > i { |
| 39 | 39 | display: flex; |
| 40 | 40 | flex-direction: row; |
| 41 | 41 | align-items: center; |
| 42 | 42 | justify-content: left; |
| 43 | 43 | padding: 10px 5px; |
| 44 | 44 | font-style: normal; |
| 45 | 45 | gap: 5px; |
| 46 | + border-bottom: 1px solid var(--SmartThemeBorderColor); | |
| 46 | 47 | } |
| 47 | 48 | |
| 48 | 49 | .assets-list-div i span:first-of-type { |
| @@ -173,3 +174,27 @@ | ||
| 173 | 174 | opacity: 0.9; |
| 174 | 175 | margin-left: 2px; |
| 175 | 176 | } |
| 177 | + | |
| 178 | +.asset-name .asset-author-info { | |
| 179 | + display: flex; | |
| 180 | + align-items: baseline; | |
| 181 | + gap: 2px; | |
| 182 | + opacity: 0.7; | |
| 183 | + font-size: 0.85em; | |
| 184 | + overflow: hidden; | |
| 185 | +} | |
| 186 | + | |
| 187 | +.asset-name .asset-author-info>span { | |
| 188 | + white-space: nowrap; | |
| 189 | + overflow: hidden; | |
| 190 | + text-overflow: ellipsis; | |
| 191 | +} | |
| 192 | + | |
| 193 | +.asset-name .asset-author-info:hover { | |
| 194 | + opacity: 1; | |
| 195 | + transition: opacity var(--animation-duration) ease-in-out; | |
| 196 | +} | |
| 197 | + | |
| 198 | +.asset-name>b { | |
| 199 | + font-weight: 600; | |
| 200 | +} | |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | import { ensureImageFormatSupported, getBase64Async, getFileExtension, isTrueBoolean, saveBase64AsFile } from '../../utils.js'; |
| 2 | 2 | import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 3 | 3 | import { appendMediaToMessage, chat_metadata, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js'; |
| 4 | 4 | import { getMessageTimeStamp } from '../../RossAscends-mods.js'; |
| 5 | 5 | import { SECRET_KEYS, secret_state } from '../../secrets.js'; |
| 6 | 6 | import { getMultimodalCaption } from '../shared.js'; |
| @@ -174,6 +174,7 @@ async function sendCaptionedMessage(caption, image) { | ||
| 174 | 174 | inline_image: !!extension_settings.caption.show_in_chat, |
| 175 | 175 | }, |
| 176 | 176 | }; |
| 177 | + chat_metadata['tainted'] = true; | |
| 177 | 178 | context.chat.push(message); |
| 178 | 179 | const messageId = context.chat.length - 1; |
| 179 | 180 | await eventSource.emit(event_types.MESSAGE_SENT, messageId); |
| @@ -549,6 +550,8 @@ jQuery(async function () { | ||
| 549 | 550 | await processEndpoint('pollinations', '/api/backends/chat-completions/multimodal-models/pollinations'); |
| 550 | 551 | await processEndpoint('nanogpt', '/api/backends/chat-completions/multimodal-models/nanogpt'); |
| 551 | 552 | await processEndpoint('electronhub', '/api/backends/chat-completions/multimodal-models/electronhub'); |
| 553 | + await processEndpoint('mistral', '/api/backends/chat-completions/multimodal-models/mistral'); | |
| 554 | + await processEndpoint('xai', '/api/backends/chat-completions/multimodal-models/xai'); | |
| 552 | 555 | } |
| 553 | 556 | |
| 554 | 557 | await addSettings(); |
| @@ -626,6 +629,11 @@ jQuery(async function () { | ||
| 626 | 629 | extension_settings.caption.ollama_custom_model = String($('#caption_ollama_custom_model').val()).trim(); |
| 627 | 630 | saveSettingsDebounced(); |
| 628 | 631 | }); |
| 632 | + $('#caption_refresh_models').on('click', async () => { | |
| 633 | + extension_settings.caption.multimodal_model = ''; | |
| 634 | + await switchMultimodalBlocks(); | |
| 635 | + saveSettingsDebounced(); | |
| 636 | + }); | |
| 629 | 637 | |
| 630 | 638 | const onMessageEvent = async (index) => { |
| 631 | 639 | if (!extension_settings.caption.auto_mode) { |
| @@ -9,7 +9,7 @@ | ||
| 9 | 9 | <label for="caption_source" data-i18n="Source">Source</label> |
| 10 | 10 | <select id="caption_source" class="text_pole"> |
| 11 | 11 | <option value="local" data-i18n="Local">Local</option> |
| 12 | 12 | <option value="multimodal" data-i18n="Multimodal (OpenAI / Anthropic / llama / Google)">Multimodal (OpenAI / Anthropic / llama / Google)</option> |
| 13 | 13 | <option value="extras" data-i18n="Extras">Extras (deprecated)</option> |
| 14 | 14 | <option value="horde" data-i18n="Horde">Horde</option> |
| 15 | 15 | </select> |
| @@ -18,7 +18,7 @@ | ||
| 18 | 18 | <label for="caption_multimodal_api" data-i18n="API">API</label> |
| 19 | 19 | <select id="caption_multimodal_api" class="flex1 text_pole"> |
| 20 | 20 | <option value="aimlapi">AI/ML API</option> |
| 21 | 21 | <option value="anthropic">AnthropicClaude</option> |
| 22 | 22 | <option value="cohere">Cohere</option> |
| 23 | 23 | <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option> |
| 24 | 24 | <option value="electronhub">Electron Hub</option> |
| @@ -40,23 +40,17 @@ | ||
| 40 | 40 | </select> |
| 41 | 41 | </div> |
| 42 | 42 | <div class="flex1 flex-container flexFlowColumn flexNoGap"> |
| 43 | 43 | <label for="caption_multimodal_model" data-i18nclass="Modelflex-container justifySpaceBetween">Model</label> |
| 44 | + <span data-i18n="Model">Model</span> | |
| 45 | + <div id="caption_refresh_models" class="right_menu_button margin0 padding0" title="Refresh model list" data-i18n="[title]Refresh model list"> | |
| 46 | + <i class="fa-solid fa-sync"></i> | |
| 47 | + </div> | |
| 48 | + </label> | |
| 44 | 49 | <select id="caption_multimodal_model" class="flex1 text_pole"> |
| 45 | 50 | <!-- AI/ML API, OpenRouter, Pollinations, NanoGPT, Mistral, xAI are added externally by JavaScript --> |
| 46 | 51 | <option data-type="cohere" value="c4ai-aya-vision-8b">c4ai-aya-vision-8b</option> |
| 47 | 52 | <option data-type="cohere" value="c4ai-aya-vision-32b">c4ai-aya-vision-32b</option> |
| 48 | 53 | <option data-type="cohere" value="command-a-vision-07-2025">command-a-vision-07-2025</option> |
| 49 | - <option data-type="mistral" value="pixtral-12b-latest">pixtral-12b-latest</option> | |
| 50 | - <option data-type="mistral" value="pixtral-12b-2409">pixtral-12b-2409</option> | |
| 51 | - <option data-type="mistral" value="pixtral-large-latest">pixtral-large-latest</option> | |
| 52 | - <option data-type="mistral" value="pixtral-large-2411">pixtral-large-2411</option> | |
| 53 | - <option data-type="mistral" value="mistral-large-pixtral-2411">mistral-large-pixtral-2411</option> | |
| 54 | - <option data-type="mistral" value="mistral-small-2503">mistral-small-2503</option> | |
| 55 | - <option data-type="mistral" value="mistral-small-2506">mistral-small-2506</option> | |
| 56 | - <option data-type="mistral" value="mistral-small-latest">mistral-small-latest</option> | |
| 57 | - <option data-type="mistral" value="mistral-medium-latest">mistral-medium-latest</option> | |
| 58 | - <option data-type="mistral" value="mistral-medium-2505">mistral-medium-2505</option> | |
| 59 | - <option data-type="mistral" value="mistral-medium-2508">mistral-medium-2508</option> | |
| 60 | 54 | <option data-type="moonshot" value="moonshot-v1-8k-vision-preview">moonshot-v1-8k-vision-preview</option> |
| 61 | 55 | <option data-type="moonshot" value="moonshot-v1-32k-vision-preview">moonshot-v1-32k-vision-preview</option> |
| 62 | 56 | <option data-type="moonshot" value="moonshot-v1-128k-vision-preview">moonshot-v1-128k-vision-preview</option> |
| @@ -87,6 +81,10 @@ | ||
| 87 | 81 | <option data-type="openai" value="o4-mini-2025-04-16">o4-mini-2025-04-16</option> |
| 88 | 82 | <option data-type="openai" value="gpt-4.5-preview">gpt-4.5-preview</option> |
| 89 | 83 | <option data-type="openai" value="gpt-4.5-preview-2025-02-27">gpt-4.5-preview-2025-02-27</option> |
| 84 | + <option data-type="anthropic" value="claude-sonnet-4-5">claude-sonnet-4-5</option> | |
| 85 | + <option data-type="anthropic" value="claude-sonnet-4-5-20250929">claude-sonnet-4-5-20250929</option> | |
| 86 | + <option data-type="anthropic" value="claude-haiku-4-5">claude-haiku-4-5</option> | |
| 87 | + <option data-type="anthropic" value="claude-haiku-4-5-20251001">claude-haiku-4-5-20251001</option> | |
| 90 | 88 | <option data-type="anthropic" value="claude-opus-4-1">claude-opus-4-1</option> |
| 91 | 89 | <option data-type="anthropic" value="claude-opus-4-1-20250805">claude-opus-4-1-20250805</option> |
| 92 | 90 | <option data-type="anthropic" value="claude-opus-4-0">claude-opus-4-0</option> |
| @@ -106,49 +104,33 @@ | ||
| 106 | 104 | <option data-type="google" value="gemini-2.5-pro-preview-06-05">gemini-2.5-pro-preview-06-05</option> |
| 107 | 105 | <option data-type="google" value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option> |
| 108 | 106 | <option data-type="google" value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option> |
| 109 | - <option data-type="google" value="gemini-2.5-pro-exp-03-25">gemini-2.5-pro-exp-03-25</option> | |
| 110 | 107 | <option data-type="google" value="gemini-2.5-flash">gemini-2.5-flash</option> |
| 108 | + <option data-type="google" value="gemini-2.5-flash-preview-09-2025">gemini-2.5-flash-preview-09-2025</option> | |
| 111 | 109 | <option data-type="google" value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option> |
| 112 | - <option data-type="google" value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option> | |
| 113 | 110 | <option data-type="google" value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option> |
| 111 | + <option data-type="google" value="gemini-2.5-flash-lite-preview-09-2025">gemini-2.5-flash-lite-preview-09-2025</option> | |
| 114 | 112 | <option data-type="google" value="gemini-2.5-flash-lite-preview-06-17">gemini-2.5-flash-lite-preview-06-17</option> |
| 113 | + <option data-type="google" value="gemini-2.5-flash-image">gemini-2.5-flash-image</option> | |
| 115 | 114 | <option data-type="google" value="gemini-2.5-flash-image-preview">gemini-2.5-flash-image-preview</option> |
| 116 | 115 | <option data-type="google" value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05 → 2.5-pro-exp-03-25</option> |
| 117 | 116 | <option data-type="google" value="gemini-2.0-pro-exp">gemini-2.0-pro-exp → 2.5-pro-exp-03-25</option> |
| 118 | 117 | <option data-type="google" value="gemini-exp-1206">gemini-exp-1206 → 2.5-pro-exp-03-25</option> |
| 119 | 118 | <option data-type="google" value="gemini-2.0-flash-001">gemini-2.0-flash-001</option> |
| 120 | 119 | <option data-type="google" value="gemini-2.0-flash-exp-image-generation">gemini-2.0-flash-exp-image-generation</option> |
| 121 | 120 | <option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option> |
| 122 | 121 | <option data-type="google" value="gemini-2.0-flash">gemini-2.0-flash</option> |
| 123 | 122 | <option data-type="google" value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21 → 2.5-flash-preview-045-1720</option> |
| 124 | 123 | <option data-type="google" value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219 → 2.5-flash-preview-0405-1720</option> |
| 125 | 124 | <option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp → 2.5-flash-preview-0405-1720</option> |
| 126 | 125 | <option data-type="google" value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option> |
| 127 | 126 | <option data-type="google" value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option> |
| 128 | 127 | <option data-type="google" value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option> |
| 129 | - <option data-type="google" value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option> | |
| 130 | - <option data-type="google" value="gemini-1.5-pro-002">gemini-1.5-pro-002</option> | |
| 131 | - <option data-type="google" value="gemini-1.5-pro-001">gemini-1.5-pro-001</option> | |
| 132 | - <option data-type="google" value="gemini-1.5-pro">gemini-1.5-pro</option> | |
| 133 | - <option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option> | |
| 134 | - <option data-type="google" value="gemini-1.5-flash-002">gemini-1.5-flash-002</option> | |
| 135 | - <option data-type="google" value="gemini-1.5-flash-001">gemini-1.5-flash-001</option> | |
| 136 | - <option data-type="google" value="gemini-1.5-flash">gemini-1.5-flash</option> | |
| 137 | - <option data-type="google" value="gemini-1.5-flash-8b-latest">gemini-1.5-flash-8b-latest</option> | |
| 138 | - <option data-type="google" value="gemini-1.5-flash-8b-001">gemini-1.5-flash-8b-001</option> | |
| 139 | - <option data-type="google" value="gemini-1.5-flash-8b-exp-0924">gemini-1.5-flash-8b-exp-0924</option> | |
| 140 | - <option data-type="google" value="gemini-1.5-flash-8b-exp-0827">gemini-1.5-flash-8b-exp-0827</option> | |
| 141 | 128 | <option data-type="google" value="learnlm-2.0-flash-experimental">learnlm-2.0-flash-experimental</option> |
| 142 | 129 | <option data-type="google" value="learnlmgemini-robotics-er-1.5-pro-experimentalpreview">learnlmgemini-robotics-er-1.5-pro-experimentalpreview</option> |
| 143 | 130 | <option data-type="vertexai" value="gemini-2.5-pro">gemini-2.5-pro</option> |
| 144 | - <option data-type="vertexai" value="gemini-2.5-pro-preview-06-05">gemini-2.5-pro-preview-06-05</option> | |
| 145 | - <option data-type="vertexai" value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option> | |
| 146 | - <option data-type="vertexai" value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option> | |
| 147 | 131 | <option data-type="vertexai" value="gemini-2.5-flash">gemini-2.5-flash</option> |
| 148 | - <option data-type="vertexai" value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option> | |
| 149 | - <option data-type="vertexai" value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option> | |
| 150 | 132 | <option data-type="vertexai" value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option> |
| 151 | 133 | <option data-type="vertexai" value="gemini-2.5-flash-lite-preview-06-17image">gemini-2.5-flash-lite-preview-06-17image</option> |
| 152 | 134 | <option data-type="vertexai" value="gemini-2.5-flash-image-preview">gemini-2.5-flash-image-preview</option> |
| 153 | 135 | <option data-type="vertexai" value="gemini-2.0-flash-001">gemini-2.0-flash-001</option> |
| 154 | 136 | <option data-type="vertexai" value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option> |
| @@ -174,9 +156,6 @@ | ||
| 174 | 156 | <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option> |
| 175 | 157 | <option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option> |
| 176 | 158 | <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option> |
| 177 | - <option data-type="xai" value="grok-4-0709">grok-4-0709</option> | |
| 178 | - <option data-type="xai" value="grok-2-vision-1212">grok-2-vision-1212</option> | |
| 179 | - <option data-type="xai" value="grok-vision-beta">grok-vision-beta</option> | |
| 180 | 159 | </select> |
| 181 | 160 | </div> |
| 182 | 161 | <div data-type="ollama"> |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | import { DOMPurify, Fuse } from '../../../lib.js'; |
| 2 | 2 | |
| 3 | 3 | import { event_types, eventSource, main_api, online_status, saveSettingsDebounced } from '../../../script.js'; |
| 4 | 4 | import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 5 | 5 | import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js'; |
| 6 | 6 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| @@ -11,7 +11,7 @@ import { SlashCommandDebugController } from '../../slash-commands/SlashCommandDe | ||
| 11 | 11 | import { enumTypes, SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 12 | 12 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| 13 | 13 | import { SlashCommandScope } from '../../slash-commands/SlashCommandScope.js'; |
| 14 | 14 | import { collapseSpaces, getUniqueName, isFalseBoolean, uuidv4, waitUntilCondition } from '../../utils.js'; |
| 15 | 15 | import { t } from '../../i18n.js'; |
| 16 | 16 | import { getSecretLabelById } from '../../secrets.js'; |
| 17 | 17 | |
| @@ -167,6 +167,12 @@ const profilesProvider = () => [ | ||
| 167 | 167 | * @property {string} [stop-strings] Custom Stopping Strings |
| 168 | 168 | * @property {string} [start-reply-with] Start Reply With |
| 169 | 169 | * @property {string} [reasoning-template] Reasoning Template |
| 170 | + * @property {string} [prompt-post-processing] Prompt Post-Processing | |
| 171 | + * @property {string} [sysprompt] System Prompt Name | |
| 172 | + * @property {string} [sysprompt-state] Use System Prompt | |
| 173 | + * @property {string} [api-url] Server URL | |
| 174 | + * @property {string} [secret-id] Secret ID | |
| 175 | + * @property {string} [regex-preset] Regex Preset ID | |
| 170 | 176 | * @property {string[]} [exclude] Commands to exclude |
| 171 | 177 | */ |
| 172 | 178 | |
| @@ -684,6 +690,13 @@ async function renderDetailsContent(detailsContent) { | ||
| 684 | 690 | defaultValue: 'true', |
| 685 | 691 | enumList: commonEnumProviders.boolean('trueFalse')(), |
| 686 | 692 | }), |
| 693 | + SlashCommandNamedArgument.fromProps({ | |
| 694 | + name: 'timeout', | |
| 695 | + description: 'Maximum time to wait for the API connection to be established, in milliseconds. Set to 0 to disable. Only applies when await=true.', | |
| 696 | + isRequired: false, | |
| 697 | + typeList: [ARGUMENT_TYPE.NUMBER], | |
| 698 | + defaultValue: '2000', | |
| 699 | + }), | |
| 687 | 700 | ], |
| 688 | 701 | callback: async (args, value) => { |
| 689 | 702 | if (!value || typeof value !== 'string') { |
| @@ -715,6 +728,13 @@ async function renderDetailsContent(detailsContent) { | ||
| 715 | 728 | |
| 716 | 729 | if (shouldAwait) { |
| 717 | 730 | await awaitPromise; |
| 731 | + | |
| 732 | + // We should also await the connection to be established | |
| 733 | + const parsedTimeout = parseInt(args?.timeout?.toString()); | |
| 734 | + const timeout = !isNaN(parsedTimeout) ? Math.max(0, parsedTimeout) : 2000; | |
| 735 | + if (timeout > 0) { | |
| 736 | + await waitUntilCondition(() => online_status !== 'no_connection', timeout, 100, { rejectOnTimeout: false }); | |
| 737 | + } | |
| 718 | 738 | } |
| 719 | 739 | |
| 720 | 740 | return profile.name; |
| @@ -993,7 +993,7 @@ function doPopout(e) { | ||
| 993 | 993 | originalElement.empty(); |
| 994 | 994 | originalElement.html('<div class="flex-container alignitemscenter justifyCenter wide100p"><small>Currently popped out</small></div>'); |
| 995 | 995 | newElement.append(controlBarHtml).append(originalHTMLClone); |
| 996 | 996 | $('body#movingDivs').append(newElement); |
| 997 | 997 | newElement.transition({ opacity: 1, duration: animation_duration, easing: animation_easing }); |
| 998 | 998 | $('#summaryExtensionDrawerContents').addClass('scrollableInnerFull'); |
| 999 | 999 | setMemoryContext(prevSummaryBoxContents, false); //paste prev summary box contents into popout box |
| @@ -11,6 +11,7 @@ | ||
| 11 | 11 | <button |
| 12 | 12 | id="regex_debugger_save_order" |
| 13 | 13 | class="menu_button menu_button_icon interactable" |
| 14 | + data-i18n="[title]ext_regex_debugger_save_order_help" | |
| 14 | 15 | title="Save current rule order" |
| 15 | 16 | tabindex="0" |
| 16 | 17 | > |
| @@ -53,6 +54,7 @@ | ||
| 53 | 54 | <button |
| 54 | 55 | id="regex_debugger_run_test" |
| 55 | 56 | class="menu_button menu_button_icon interactable" |
| 57 | + data-i18n="[title]ext_regex_debugger_run_test_help" | |
| 56 | 58 | title="Run the test pipeline" |
| 57 | 59 | tabindex="0" |
| 58 | 60 | > |
| @@ -115,6 +117,7 @@ | ||
| 115 | 117 | <div |
| 116 | 118 | id="regex_debugger_expand_steps" |
| 117 | 119 | class="menu_button menu_button_icon" |
| 120 | + data-i18n="[title]Expand view" | |
| 118 | 121 | title="Expand view" |
| 119 | 122 | > |
| 120 | 123 | <i class="fa-solid fa-expand"></i> |
| @@ -132,6 +135,7 @@ | ||
| 132 | 135 | <div |
| 133 | 136 | id="regex_debugger_expand_final" |
| 134 | 137 | class="menu_button menu_button_icon" |
| 138 | + data-i18n="[title]Expand view" | |
| 135 | 139 | title="Expand view" |
| 136 | 140 | > |
| 137 | 141 | <i class="fa-solid fa-expand"></i> |
| @@ -158,7 +162,7 @@ | ||
| 158 | 162 | <code class="rule-regex"></code> |
| 159 | 163 | <small class="rule-scope"></small> |
| 160 | 164 | </div> |
| 161 | 165 | <div class="menu_button menu_button_icon edit_rule" data-i18n="[title]Edit Rule" title="Edit Rule"> |
| 162 | 166 | <i class="fa-solid fa-pencil"></i> |
| 163 | 167 | </div> |
| 164 | 168 | </li> |
| @@ -16,6 +16,10 @@ | ||
| 16 | 16 | <i class="fa-solid fa-address-card"></i> |
| 17 | 17 | <small data-i18n="ext_regex_new_scoped_script">+ Scoped</small> |
| 18 | 18 | </div> |
| 19 | + <div id="open_preset_editor" class="menu_button menu_button_icon" data-i18n="[title]ext_regex_new_preset_script_desc" title="New preset regex script"> | |
| 20 | + <i class="fa-solid fa-sliders"></i> | |
| 21 | + <small data-i18n="ext_regex_new_preset_script">+ Preset</small> | |
| 22 | + </div> | |
| 19 | 23 | <div id="import_regex" class="menu_button menu_button_icon"> |
| 20 | 24 | <i class="fa-solid fa-file-import"></i> |
| 21 | 25 | <small data-i18n="ext_regex_import_script">Import</small> |
| @@ -31,7 +35,8 @@ | ||
| 31 | 35 | <small data-i18n="ext_regex_debugger">Debugger</small> |
| 32 | 36 | </div> |
| 33 | 37 | </div> |
| 34 | 38 | <divhr class="regex_bulk_operations flex-container justifyCenterregex_bulk_operations_hr" /> |
| 39 | + <div class="regex_bulk_operations flex-container"> | |
| 35 | 40 | <div id="bulk_select_all_toggle" class="menu_button menu_button_icon" title="Toggle Select All"> |
| 36 | 41 | <i class="fa-solid fa-check-double"></i> |
| 37 | 42 | </div> |
| @@ -43,6 +48,18 @@ | ||
| 43 | 48 | <i class="fa-solid fa-toggle-off"></i> |
| 44 | 49 | <small data-i18n="Disable">Disable</small> |
| 45 | 50 | </div> |
| 51 | + <div id="bulk_regex_move_to_global" class="menu_button menu_button_icon" hidden> | |
| 52 | + <i class="fa-solid fa-globe"></i> | |
| 53 | + <small data-i18n="ext_regex_move_to_global">Move to global scripts</small> | |
| 54 | + </div> | |
| 55 | + <div id="bulk_regex_move_to_scoped" class="menu_button menu_button_icon" hidden> | |
| 56 | + <i class="fa-solid fa-address-card"></i> | |
| 57 | + <small data-i18n="ext_regex_move_to_scoped">Move to scoped scripts</small> | |
| 58 | + </div> | |
| 59 | + <div id="bulk_regex_move_to_preset" class="menu_button menu_button_icon" hidden> | |
| 60 | + <i class="fa-solid fa-sliders"></i> | |
| 61 | + <small data-i18n="ext_regex_move_to_preset">Move to preset scripts</small> | |
| 62 | + </div> | |
| 46 | 63 | <div id="bulk_export_regex" class="menu_button menu_button_icon"> |
| 47 | 64 | <i class="fa-solid fa-file-export"></i> |
| 48 | 65 | <small data-i18n="Export">Export</small> |
| @@ -69,7 +86,7 @@ | ||
| 69 | 86 | </div> |
| 70 | 87 | </div> |
| 71 | 88 | <hr /> |
| 72 | 89 | <div id="global_scripts_block" class="padding5"> |
| 73 | 90 | <div> |
| 74 | 91 | <strong data-i18n="ext_regex_global_scripts">Global Scripts</strong> |
| 75 | 92 | </div> |
| @@ -79,7 +96,7 @@ | ||
| 79 | 96 | <div id="saved_regex_scripts" no-scripts-text="No scripts found" data-i18n="[no-scripts-text]No scripts found" class="flex-container regex-script-container flexFlowColumn"></div> |
| 80 | 97 | </div> |
| 81 | 98 | <hr /> |
| 82 | 99 | <div id="scoped_scripts_block" class="padding5"> |
| 83 | 100 | <div class="flex-container alignItemsBaseline"> |
| 84 | 101 | <strong class="flex1" data-i18n="ext_regex_scoped_scripts">Scoped Scripts</strong> |
| 85 | 102 | <label id="toggle_scoped_regex" class="checkbox flex-container" for="regex_scoped_toggle"> |
| @@ -93,6 +110,21 @@ | ||
| 93 | 110 | </small> |
| 94 | 111 | <div id="saved_scoped_scripts" no-scripts-text="No scripts found" data-i18n="[no-scripts-text]No scripts found" class="flex-container regex-script-container flexFlowColumn"></div> |
| 95 | 112 | </div> |
| 113 | + <hr /> | |
| 114 | + <div id="preset_scripts_block"> | |
| 115 | + <div class="flex-container alignItemsBaseline"> | |
| 116 | + <strong class="flex1" data-i18n="ext_regex_preset_scripts">Preset Scripts</strong> | |
| 117 | + <label id="toggle_preset_regex" class="checkbox flex-container" for="regex_preset_toggle"> | |
| 118 | + <input type="checkbox" id="regex_preset_toggle" class="enable_scoped" /> | |
| 119 | + <span class="regex-toggle-on fa-solid fa-toggle-on fa-lg" data-i18n="[title]ext_regex_disallow_preset" title="Disallow using preset regex"></span> | |
| 120 | + <span class="regex-toggle-off fa-solid fa-toggle-off fa-lg" data-i18n="[title]ext_regex_allow_preset" title="Allow using preset regex"></span> | |
| 121 | + </label> | |
| 122 | + </div> | |
| 123 | + <small data-i18n="ext_regex_preset_scripts_desc"> | |
| 124 | + Only available for this preset. Saved to the preset data. | |
| 125 | + </small> | |
| 126 | + <div id="saved_preset_scripts" class="flex-container regex-script-container flexFlowColumn"></div> | |
| 127 | + </div> | |
| 96 | 128 | </div> |
| 97 | 129 | </div> |
| 98 | 130 | </div> |
| @@ -1,16 +1,228 @@ | ||
| 1 | 1 | import { characters, saveSettingsDebounced, substituteParams, substituteParamsExtended, this_chid } from '../../../script.js'; |
| 2 | 2 | import { extension_settings, writeExtensionField } from '../../extensions.js'; |
| 3 | +import { getPresetManager } from '../../preset-manager.js'; | |
| 3 | 4 | import { regexFromString } from '../../utils.js'; |
| 4 | -export { | |
| 5 | +import { lodash } from '../../../lib.js'; | |
| 5 | - regex_placement, | |
| 6 | + | |
| 6 | - getRegexedString, | |
| 7 | +/** | |
| 7 | - runRegexScript, | |
| 8 | + * @enum {number} Regex scripts types | |
| 9 | + * @readonly | |
| 10 | + */ | |
| 11 | +export const SCRIPT_TYPES = { | |
| 12 | + GLOBAL: 0, | |
| 13 | + SCOPED: 1, | |
| 14 | + PRESET: 2, | |
| 8 | 15 | }; |
| 9 | 16 | |
| 10 | 17 | /** |
| 18 | + * Special type for unknown/invalid script types. | |
| 19 | + */ | |
| 20 | +export const SCRIPT_TYPE_UNKNOWN = -1; | |
| 21 | + | |
| 22 | +/** | |
| 23 | + * @typedef {import('../../char-data.js').RegexScriptData} RegexScript | |
| 24 | + */ | |
| 25 | + | |
| 26 | +/** | |
| 27 | + * @typedef {object} GetRegexScriptsOptions | |
| 28 | + * @property {boolean} allowedOnly Only return allowed scripts | |
| 29 | + */ | |
| 30 | + | |
| 31 | +/** | |
| 32 | + * @type {Readonly<GetRegexScriptsOptions>} | |
| 33 | + */ | |
| 34 | +const DEFAULT_GET_REGEX_SCRIPTS_OPTIONS = Object.freeze({ allowedOnly: false }); | |
| 35 | + | |
| 36 | +/** | |
| 37 | + * Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data | |
| 38 | + * | |
| 39 | + * @param {GetRegexScriptsOptions} options Options for retrieving the regex scripts | |
| 40 | + * @returns {RegexScript[]} An array of regex scripts, where each script is an object containing the necessary information. | |
| 41 | + */ | |
| 42 | +export function getRegexScripts(options = DEFAULT_GET_REGEX_SCRIPTS_OPTIONS) { | |
| 43 | + return [...Object.values(SCRIPT_TYPES).flatMap(type => getScriptsByType(type, options))]; | |
| 44 | +} | |
| 45 | + | |
| 46 | +/** | |
| 47 | + * Retrieves the regex scripts for a specific type. | |
| 48 | + * @param {SCRIPT_TYPES} scriptType The type of regex scripts to retrieve. | |
| 49 | + * @param {GetRegexScriptsOptions} options Options for retrieving the regex scripts | |
| 50 | + * @returns {RegexScript[]} An array of regex scripts for the specified type. | |
| 51 | + */ | |
| 52 | +export function getScriptsByType(scriptType, { allowedOnly } = DEFAULT_GET_REGEX_SCRIPTS_OPTIONS) { | |
| 53 | + switch (scriptType) { | |
| 54 | + case SCRIPT_TYPE_UNKNOWN: | |
| 55 | + return []; | |
| 56 | + case SCRIPT_TYPES.GLOBAL: | |
| 57 | + return extension_settings.regex ?? []; | |
| 58 | + case SCRIPT_TYPES.SCOPED: { | |
| 59 | + if (allowedOnly && !extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar)) { | |
| 60 | + return []; | |
| 61 | + } | |
| 62 | + const scopedScripts = characters[this_chid]?.data?.extensions?.regex_scripts; | |
| 63 | + return Array.isArray(scopedScripts) ? scopedScripts : []; | |
| 64 | + } | |
| 65 | + case SCRIPT_TYPES.PRESET: { | |
| 66 | + if (allowedOnly && !extension_settings?.preset_allowed_regex?.[getCurrentPresetAPI()]?.includes(getCurrentPresetName())) { | |
| 67 | + return []; | |
| 68 | + } | |
| 69 | + const presetManager = getPresetManager(); | |
| 70 | + const presetScripts = presetManager?.readPresetExtensionField({ path: 'regex_scripts' }); | |
| 71 | + return Array.isArray(presetScripts) ? presetScripts : []; | |
| 72 | + } | |
| 73 | + default: | |
| 74 | + console.warn(`getScriptsByType: Invalid script type ${scriptType}`); | |
| 75 | + return []; | |
| 76 | + } | |
| 77 | +} | |
| 78 | + | |
| 79 | +/** | |
| 80 | + * Saves an array of regex scripts for a specific type. | |
| 81 | + * @param {RegexScript[]} scripts An array of regex scripts to save. | |
| 82 | + * @param {SCRIPT_TYPES} scriptType The type of regex scripts to save. | |
| 83 | + * @returns {Promise<void>} | |
| 84 | + */ | |
| 85 | +export async function saveScriptsByType(scripts, scriptType) { | |
| 86 | + switch (scriptType) { | |
| 87 | + case SCRIPT_TYPES.GLOBAL: | |
| 88 | + extension_settings.regex = scripts; | |
| 89 | + saveSettingsDebounced(); | |
| 90 | + break; | |
| 91 | + case SCRIPT_TYPES.SCOPED: | |
| 92 | + await writeExtensionField(this_chid, 'regex_scripts', scripts); | |
| 93 | + break; | |
| 94 | + case SCRIPT_TYPES.PRESET: { | |
| 95 | + const presetManager = getPresetManager(); | |
| 96 | + await presetManager.writePresetExtensionField({ path: 'regex_scripts', value: scripts }); | |
| 97 | + break; | |
| 98 | + } | |
| 99 | + default: | |
| 100 | + console.warn(`saveScriptsByType: Invalid script type ${scriptType}`); | |
| 101 | + break; | |
| 102 | + } | |
| 103 | +} | |
| 104 | + | |
| 105 | +/** | |
| 106 | + * Check if character's regexes are allowed to be used; if character is undefined, returns false | |
| 107 | + * @param {import('../../char-data.js').v1CharData|undefined} character | |
| 108 | + * @returns {boolean} | |
| 109 | + */ | |
| 110 | +export function isScopedScriptsAllowed(character) { | |
| 111 | + return !!extension_settings?.character_allowed_regex?.includes(character?.avatar); | |
| 112 | +} | |
| 113 | + | |
| 114 | +/** | |
| 115 | + * Allow character's regexes to be used; if character is undefined, do nothing | |
| 116 | + * @param {import('../../char-data.js').v1CharData|undefined} character | |
| 117 | + * @returns {void} | |
| 118 | + */ | |
| 119 | +export function allowScopedScripts(character) { | |
| 120 | + const avatar = character?.avatar; | |
| 121 | + if (!avatar) { | |
| 122 | + return; | |
| 123 | + } | |
| 124 | + if (!Array.isArray(extension_settings?.character_allowed_regex)) { | |
| 125 | + extension_settings.character_allowed_regex = []; | |
| 126 | + } | |
| 127 | + if (!extension_settings.character_allowed_regex.includes(avatar)) { | |
| 128 | + extension_settings.character_allowed_regex.push(avatar); | |
| 129 | + saveSettingsDebounced(); | |
| 130 | + } | |
| 131 | +} | |
| 132 | + | |
| 133 | +/** | |
| 134 | + * Disallow character's regexes to be used; if character is undefined, do nothing | |
| 135 | + * @param {import('../../char-data.js').v1CharData|undefined} character | |
| 136 | + * @returns {void} | |
| 137 | + */ | |
| 138 | +export function disallowScopedScripts(character) { | |
| 139 | + const avatar = character?.avatar; | |
| 140 | + if (!avatar) { | |
| 141 | + return; | |
| 142 | + } | |
| 143 | + if (!Array.isArray(extension_settings?.character_allowed_regex)) { | |
| 144 | + return; | |
| 145 | + } | |
| 146 | + const index = extension_settings.character_allowed_regex.indexOf(avatar); | |
| 147 | + if (index !== -1) { | |
| 148 | + extension_settings.character_allowed_regex.splice(index, 1); | |
| 149 | + saveSettingsDebounced(); | |
| 150 | + } | |
| 151 | +} | |
| 152 | + | |
| 153 | +/** | |
| 154 | + * Check if preset's regexes are allowed to be used | |
| 155 | + * @param {string} apiId API ID | |
| 156 | + * @param {string} presetName Preset name | |
| 157 | + * @returns {boolean} True if allowed, false if not | |
| 158 | + */ | |
| 159 | +export function isPresetScriptsAllowed(apiId, presetName) { | |
| 160 | + if (!apiId || !presetName) { | |
| 161 | + return false; | |
| 162 | + } | |
| 163 | + return !!extension_settings?.preset_allowed_regex?.[apiId]?.includes(presetName); | |
| 164 | +} | |
| 165 | + | |
| 166 | +/** | |
| 167 | + * Allow preset's regexes to be used | |
| 168 | + * @param {string} apiId API ID | |
| 169 | + * @param {string} presetName Preset name | |
| 170 | + * @returns {void} | |
| 171 | + */ | |
| 172 | +export function allowPresetScripts(apiId, presetName) { | |
| 173 | + if (!apiId || !presetName) { | |
| 174 | + return; | |
| 175 | + } | |
| 176 | + if (!Array.isArray(extension_settings?.preset_allowed_regex?.[apiId])) { | |
| 177 | + lodash.set(extension_settings, ['preset_allowed_regex', apiId], []); | |
| 178 | + } | |
| 179 | + if (!extension_settings.preset_allowed_regex[apiId].includes(presetName)) { | |
| 180 | + extension_settings.preset_allowed_regex[apiId].push(presetName); | |
| 181 | + saveSettingsDebounced(); | |
| 182 | + } | |
| 183 | +} | |
| 184 | + | |
| 185 | +/** | |
| 186 | + * Disallow preset's regexes to be used | |
| 187 | + * @param {string} apiId API ID | |
| 188 | + * @param {string} presetName Preset name | |
| 189 | + * @returns {void} | |
| 190 | + */ | |
| 191 | +export function disallowPresetScripts(apiId, presetName) { | |
| 192 | + if (!apiId || !presetName) { | |
| 193 | + return; | |
| 194 | + } | |
| 195 | + if (!Array.isArray(extension_settings?.preset_allowed_regex?.[apiId])) { | |
| 196 | + return; | |
| 197 | + } | |
| 198 | + const index = extension_settings.preset_allowed_regex[apiId].indexOf(presetName); | |
| 199 | + if (index !== -1) { | |
| 200 | + extension_settings.preset_allowed_regex[apiId].splice(index, 1); | |
| 201 | + saveSettingsDebounced(); | |
| 202 | + } | |
| 203 | +} | |
| 204 | + | |
| 205 | +/** | |
| 206 | + * Gets the current API ID from the preset manager. | |
| 207 | + * @returns {string|null} Current API ID, or null if no preset manager | |
| 208 | + */ | |
| 209 | +export function getCurrentPresetAPI() { | |
| 210 | + return getPresetManager()?.apiId ?? null; | |
| 211 | +} | |
| 212 | + | |
| 213 | +/** | |
| 214 | + * Gets the name of the currently selected preset. | |
| 215 | + * @returns {string|null} The name of the currently selected preset, or null if no preset manager | |
| 216 | + */ | |
| 217 | +export function getCurrentPresetName() { | |
| 218 | + return getPresetManager()?.getSelectedPresetName() ?? null; | |
| 219 | +} | |
| 220 | + | |
| 221 | +/** | |
| 11 | 222 | * @enum {number} Where the regex script should be applied |
| 223 | + * @readonly | |
| 12 | 224 | */ |
| 13 | 225 | export const regex_placement = { |
| 14 | 226 | /** |
| 15 | 227 | * @deprecated MD Display is deprecated. Do not use. |
| 16 | 228 | */ |
| @@ -23,6 +235,10 @@ const regex_placement = { | ||
| 23 | 235 | REASONING: 6, |
| 24 | 236 | }; |
| 25 | 237 | |
| 238 | +/** | |
| 239 | + * @enum {number} How to substitute parameters in the find regex | |
| 240 | + * @readonly | |
| 241 | + */ | |
| 26 | 242 | export const substitute_find_regex = { |
| 27 | 243 | NONE: 0, |
| 28 | 244 | RAW: 1, |
| @@ -51,22 +267,6 @@ function sanitizeRegexMacro(x) { | ||
| 51 | 267 | }) : x; |
| 52 | 268 | } |
| 53 | 269 | |
| 54 | -function getScopedRegex() { | |
| 55 | - const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar); | |
| 56 | - | |
| 57 | - if (!isAllowed) { | |
| 58 | - return []; | |
| 59 | - } | |
| 60 | - | |
| 61 | - const scripts = characters[this_chid]?.data?.extensions?.regex_scripts; | |
| 62 | - | |
| 63 | - if (!Array.isArray(scripts)) { | |
| 64 | - return []; | |
| 65 | - } | |
| 66 | - | |
| 67 | - return scripts; | |
| 68 | -} | |
| 69 | - | |
| 70 | 270 | /** |
| 71 | 271 | * Parent function to fetch a regexed version of a raw string |
| 72 | 272 | * @param {string} rawString The raw string to be regexed |
| @@ -75,7 +275,7 @@ function getScopedRegex() { | ||
| 75 | 275 | * @returns {string} The regexed string |
| 76 | 276 | * @typedef {{characterOverride?: string, isMarkdown?: boolean, isPrompt?: boolean, isEdit?: boolean, depth?: number }} RegexParams The parameters to use for the regex script |
| 77 | 277 | */ |
| 78 | 278 | export function getRegexedString(rawString, placement, { characterOverride, isMarkdown, isPrompt, isEdit, depth } = {}) { |
| 79 | 279 | // WTF have you passed me? |
| 80 | 280 | if (typeof rawString !== 'string') { |
| 81 | 281 | console.warn('getRegexedString: rawString is not a string. Returning empty string.'); |
| @@ -87,7 +287,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown, | ||
| 87 | 287 | return finalString; |
| 88 | 288 | } |
| 89 | 289 | |
| 90 | - const allRegex = [...(extension_settings.regex ?? []), ...(getScopedRegex() ?? [])]; | |
| 290 | + const allRegex = getRegexScripts({ allowedOnly: true }); | |
| 91 | 291 | allRegex.forEach((script) => { |
| 92 | 292 | if ( |
| 93 | 293 | // Script applies to Markdown and input is Markdown |
| @@ -126,13 +326,13 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown, | ||
| 126 | 326 | |
| 127 | 327 | /** |
| 128 | 328 | * Runs the provided regex script on the given string |
| 129 | 329 | * @param {import('./index.js').RegexScript} regexScript The regex script to run |
| 130 | 330 | * @param {string} rawString The string to run the regex script on |
| 131 | 331 | * @param {RegexScriptParams} params The parameters to use for the regex script |
| 132 | 332 | * @returns {string} The new string |
| 133 | 333 | * @typedef {{characterOverride?: string}} RegexScriptParams The parameters to use for the regex script |
| 134 | 334 | */ |
| 135 | 335 | export function runRegexScript(regexScript, rawString, { characterOverride } = {}) { |
| 136 | 336 | let newString = rawString; |
| 137 | 337 | if (!regexScript || !!(regexScript.disabled) || !regexScript?.findRegex || !rawString) { |
| 138 | 338 | return newString; |
| @@ -15,5 +15,11 @@ | ||
| 15 | 15 | Scoped Scripts |
| 16 | 16 | </span> |
| 17 | 17 | </label> |
| 18 | + <label for="regex_import_target_preset"> | |
| 19 | + <input type="radio" name="regex_import_target" id="regex_import_target_preset" value="preset" /> | |
| 20 | + <span data-i18n="ext_regex_preset_scripts"> | |
| 21 | + Preset Scripts | |
| 22 | + </span> | |
| 23 | + </label> | |
| 18 | 24 | </div> |
| 19 | 25 | </div> |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | import { characters, eventSource, event_types, getCurrentChatId, messageFormatting, reloadCurrentChat, saveSettingsDebounced, this_chid } from '../../../script.js'; |
| 2 | 2 | import { extension_settings, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js'; |
| 3 | 3 | import { selected_group } from '../../group-chats.js'; |
| 4 | 4 | import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; |
| 5 | 5 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| @@ -7,10 +7,14 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from ' | ||
| 7 | 7 | import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 8 | 8 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 9 | 9 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| 10 | 10 | import { download, equalsIgnoreCaseAndAccents, escapeHtml, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4, escapeHtml } from '../../utils.js'; |
| 11 | 11 | import { allowPresetScripts, allowScopedScripts, disallowPresetScripts, disallowScopedScripts, getCurrentPresetAPI, getCurrentPresetName, getRegexScripts, getScriptsByType, isPresetScriptsAllowed, isScopedScriptsAllowed, regex_placement, runRegexScript, saveScriptsByType, SCRIPT_TYPE_UNKNOWN, SCRIPT_TYPES, substitute_find_regex } from './engine.js'; |
| 12 | 12 | import { t } from '../../i18n.js'; |
| 13 | 13 | import { accountStorage } from '../../util/AccountStorage.js'; |
| 14 | +import { getPresetManager } from '../../preset-manager.js'; | |
| 15 | + | |
| 16 | +// Re-exports for legacy extensions | |
| 17 | +export { getRegexScripts }; | |
| 14 | 18 | |
| 15 | 19 | const sanitizeFileName = name => name.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '_').toLowerCase(); |
| 16 | 20 | |
| @@ -30,12 +34,14 @@ const sanitizeFileName = name => name.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, ' | ||
| 30 | 34 | * @property {boolean} isSelected - Whether the preset is currently selected |
| 31 | 35 | * @property {RegexPresetItem[]} global - The list of global preset items |
| 32 | 36 | * @property {RegexPresetItem[]} scoped - The list of scoped preset items |
| 37 | + * @property {RegexPresetItem[]} preset - The list of preset preset items | |
| 33 | 38 | */ |
| 34 | 39 | |
| 35 | 40 | /** |
| 36 | 41 | * @typedef {object} RegexPresetState |
| 37 | 42 | * @property {string[]} global - List of enabled global regex script IDs |
| 38 | 43 | * @property {string[]} scoped - List of enabled scoped regex script IDs |
| 44 | + * @property {string[]} preset - List of enabled preset regex script IDs | |
| 39 | 45 | */ |
| 40 | 46 | |
| 41 | 47 | class RegexPresetManager { |
| @@ -65,12 +71,14 @@ class RegexPresetManager { | ||
| 65 | 71 | * @returns {RegexPresetState} The current state object |
| 66 | 72 | */ |
| 67 | 73 | captureCurrentState() { |
| 68 | 74 | const globalScripts = this.regexListToPresetItems(extension_settingsgetScriptsByType(SCRIPT_TYPES.regexGLOBAL) || []); |
| 69 | 75 | const scopedScripts = this.regexListToPresetItems(characters[this_chid]?.data?.extensions?getScriptsByType(SCRIPT_TYPES.regex_scriptsSCOPED) || []); |
| 76 | + const presetScripts = this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.PRESET)); | |
| 70 | 77 | |
| 71 | 78 | return { |
| 72 | 79 | global: globalScripts.map(item => item.id).sort(), |
| 73 | 80 | scoped: scopedScripts.map(item => item.id).sort(), |
| 81 | + preset: presetScripts.map(item => item.id).sort(), | |
| 74 | 82 | }; |
| 75 | 83 | } |
| 76 | 84 | |
| @@ -87,13 +95,16 @@ class RegexPresetManager { | ||
| 87 | 95 | const global2 = state2.global || []; |
| 88 | 96 | const scoped1 = state1.scoped || []; |
| 89 | 97 | const scoped2 = state2.scoped || []; |
| 98 | + const preset1 = state1.preset || []; | |
| 99 | + const preset2 = state2.preset || []; | |
| 90 | 100 | |
| 91 | 101 | if (global1.length !== global2.length || scoped1.length !== scoped2.length) { |
| 92 | 102 | return true; |
| 93 | 103 | } |
| 94 | 104 | |
| 95 | 105 | return !global1.every(id => global2.includes(id)) || |
| 96 | 106 | !scoped1.every(id => scoped2.includes(id)); || |
| 107 | + !preset1.every(id => preset2.includes(id)); | |
| 97 | 108 | } |
| 98 | 109 | |
| 99 | 110 | /** |
| @@ -362,17 +373,18 @@ class RegexPresetManager { | ||
| 362 | 373 | return; |
| 363 | 374 | } |
| 364 | 375 | |
| 365 | 376 | // Apply to both globalpreset andto scopedall lists |
| 366 | - await this.applyPresetList({ | |
| 377 | + for (const scriptType of Object.values(SCRIPT_TYPES)) { | |
| 367 | - presetList: preset.global, | |
| 378 | + await this.applyPresetList({ | |
| 368 | - targetList: extension_settings.regex, | |
| 379 | + presetList: { | |
| 369 | - saveFunction: () => saveSettingsDebounced(), | |
| 380 | + [SCRIPT_TYPES.GLOBAL]: preset.global, | |
| 370 | - }); | |
| 381 | + [SCRIPT_TYPES.SCOPED]: preset.scoped, | |
| 371 | - await this.applyPresetList({ | |
| 382 | + [SCRIPT_TYPES.PRESET]: preset.preset, | |
| 372 | - presetList: preset.scoped, | |
| 383 | + }[scriptType], | |
| 373 | - targetList: characters[this_chid]?.data?.extensions?.regex_scripts, | |
| 384 | + targetList: getScriptsByType(scriptType), | |
| 374 | 385 | saveFunction: (scripts) => writeExtensionFieldsaveScriptsByType(this_chid, 'regex_scripts'scripts, scriptsscriptType), |
| 375 | 386 | }); |
| 387 | + } | |
| 376 | 388 | |
| 377 | 389 | // Render the changes to the UI |
| 378 | 390 | await loadRegexScripts(); |
| @@ -418,8 +430,9 @@ class RegexPresetManager { | ||
| 418 | 430 | id: id, |
| 419 | 431 | name: name, |
| 420 | 432 | isSelected: false, |
| 421 | 433 | global: this.regexListToPresetItems(extension_settingsgetScriptsByType(SCRIPT_TYPES.regexGLOBAL)), |
| 422 | 434 | scoped: this.regexListToPresetItems(characters[this_chid]?.data?.extensions?getScriptsByType(SCRIPT_TYPES.regex_scriptsSCOPED)), |
| 435 | + preset: this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.PRESET)), | |
| 423 | 436 | }; |
| 424 | 437 | |
| 425 | 438 | if (isUpdate) { |
| @@ -465,15 +478,6 @@ class RegexPresetManager { | ||
| 465 | 478 | const presetManager = new RegexPresetManager(); |
| 466 | 479 | |
| 467 | 480 | /** |
| 468 | - * Retrieves the list of regex scripts by combining the scripts from the extension settings and the character data | |
| 469 | - * | |
| 470 | - * @return {RegexScript[]} An array of regex scripts, where each script is an object containing the necessary information. | |
| 471 | - */ | |
| 472 | -export function getRegexScripts() { | |
| 473 | - return [...(extension_settings.regex ?? []), ...(characters[this_chid]?.data?.extensions?.regex_scripts ?? [])]; | |
| 474 | -} | |
| 475 | - | |
| 476 | -/** | |
| 477 | 481 | * Toggle the icon for the "select all" checkbox in the regex settings. |
| 478 | 482 | * - Use `fa-check-double` when the checkbox is unchecked (indicating all scripts are not selected). |
| 479 | 483 | * - Use `fa-minus` when the checkbox is checked (indicating all scripts are selected). |
| @@ -486,15 +490,28 @@ function setToggleAllIcon(allAreChecked) { | ||
| 486 | 490 | } |
| 487 | 491 | |
| 488 | 492 | /** |
| 493 | + * Sets the visibility of the bulk move buttons based on selected scripts. | |
| 494 | + */ | |
| 495 | +function setMoveButtonsVisibility() { | |
| 496 | + const hasGlobalScripts = $('#saved_regex_scripts .regex-script-label:has(.regex_bulk_checkbox:checked)').length > 0; | |
| 497 | + const hasScopedScripts = $('#saved_scoped_scripts .regex-script-label:has(.regex_bulk_checkbox:checked)').length > 0; | |
| 498 | + const hasPresetScripts = $('#saved_preset_scripts .regex-script-label:has(.regex_bulk_checkbox:checked)').length > 0; | |
| 499 | + $('#bulk_regex_move_to_global').toggle(hasScopedScripts || hasPresetScripts); | |
| 500 | + $('#bulk_regex_move_to_scoped').toggle(hasGlobalScripts || hasPresetScripts); | |
| 501 | + $('#bulk_regex_move_to_preset').toggle(hasGlobalScripts || hasScopedScripts); | |
| 502 | +} | |
| 503 | + | |
| 504 | +/** | |
| 489 | 505 | * Saves a regex script to the extension settings or character data. |
| 490 | 506 | * @param {import('../../char-data.js').RegexScriptData} regexScript |
| 491 | 507 | * @param {number} existingScriptIndex Index of the existing script |
| 492 | 508 | * @param {booleanSCRIPT_TYPES} isScopedscriptType IsType of the script scoped to a character? |
| 509 | + * @param {boolean} [saveSettings=true] Whether to save the settings immediately | |
| 493 | 510 | * @returns {Promise<void>} |
| 494 | 511 | */ |
| 495 | 512 | async function saveRegexScript(regexScript, existingScriptIndex, isScopedscriptType, saveSettings = true) { |
| 496 | 513 | // If not editing |
| 497 | - const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? []; | |
| 514 | + const array = getScriptsByType(scriptType); | |
| 498 | 515 | |
| 499 | 516 | // Assign a UUID if it doesn't exist |
| 500 | 517 | if (!regexScript.id) { |
| @@ -523,22 +540,25 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) { | ||
| 523 | 540 | array.push(regexScript); |
| 524 | 541 | } |
| 525 | 542 | |
| 526 | 543 | if (isScopedscriptType === SCRIPT_TYPES.SCOPED) { |
| 527 | 544 | await writeExtensionFieldsaveScriptsByType(this_chid, 'regex_scripts'array, arraySCRIPT_TYPES.SCOPED); |
| 545 | + allowScopedScripts(characters?.[this_chid]); | |
| 546 | + } | |
| 528 | 547 | |
| 529 | - // Add the character to the allowed list | |
| 548 | + if (scriptType === SCRIPT_TYPES.PRESET) { | |
| 530 | - if (!extension_settings.character_allowed_regex.includes(characters[this_chid].avatar)) { | |
| 549 | + await saveScriptsByType(array, SCRIPT_TYPES.PRESET); | |
| 531 | - extension_settings.character_allowed_regex.push(characters[this_chid].avatar); | |
| 550 | + allowPresetScripts(getCurrentPresetAPI(), getCurrentPresetName()); | |
| 532 | - } | |
| 533 | 551 | } |
| 534 | 552 | |
| 535 | - saveSettingsDebounced(); | |
| 553 | + if (saveSettings) { | |
| 536 | 554 | await loadRegexScripts saveSettingsDebounced(); |
| 555 | + await loadRegexScripts(); | |
| 537 | 556 | |
| 538 | 557 | // Reload the current chat to undo previous markdown |
| 539 | 558 | const currentChatId = getCurrentChatId(); |
| 540 | - if (currentChatId !== undefined && currentChatId !== null) { | |
| 559 | + if (currentChatId) { | |
| 541 | 560 | await reloadCurrentChat(); |
| 561 | + } | |
| 542 | 562 | } |
| 543 | 563 | |
| 544 | 564 | const debuggerPopup = $('#regex_debugger_popup'); |
| @@ -547,25 +567,67 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) { | ||
| 547 | 567 | } |
| 548 | 568 | } |
| 549 | 569 | |
| 550 | -async function deleteRegexScript({ id, isScoped }) { | |
| 570 | +/** | |
| 551 | - const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? []; | |
| 571 | + * Delete a regex script by ID | |
| 572 | + * @param {string} id ID of the script to delete | |
| 573 | + * @param {SCRIPT_TYPES} scriptType Type of the script | |
| 574 | + * @param {boolean} saveSettings Whether to save the settings immediately | |
| 575 | + * @returns {Promise<void>} | |
| 576 | + */ | |
| 577 | +async function deleteRegexScript(id, scriptType, saveSettings = true) { | |
| 578 | + const array = getScriptsByType(scriptType); | |
| 552 | 579 | |
| 553 | 580 | const existingScriptIndex = array.findIndex((script) => script.id === id); |
| 554 | 581 | if (existingScriptIndex !== -1) { |
| 555 | 582 | array.splice(existingScriptIndex, 1); |
| 556 | 583 | |
| 557 | 584 | ifswitch (isScopedscriptType) { |
| 558 | - await writeExtensionField(this_chid, 'regex_scripts', array); | |
| 585 | + case SCRIPT_TYPES.GLOBAL: | |
| 586 | + // will be handled by saveSettingsDebounced | |
| 587 | + break; | |
| 588 | + case SCRIPT_TYPES.SCOPED: | |
| 589 | + await saveScriptsByType(array, SCRIPT_TYPES.SCOPED); | |
| 590 | + break; | |
| 591 | + case SCRIPT_TYPES.PRESET: | |
| 592 | + await saveScriptsByType(array, SCRIPT_TYPES.PRESET); | |
| 593 | + break; | |
| 594 | + default: | |
| 595 | + break; | |
| 596 | + } | |
| 597 | + if (saveSettings) { | |
| 598 | + saveSettingsDebounced(); | |
| 599 | + await loadRegexScripts(); | |
| 559 | 600 | } |
| 601 | + } | |
| 602 | +} | |
| 560 | 603 | |
| 561 | - saveSettingsDebounced(); | |
| 604 | +/** | |
| 562 | - await loadRegexScripts(); | |
| 605 | + * Move a regex script from one type to another | |
| 606 | + * @param {import('../../char-data.js').RegexScriptData} script The script to move | |
| 607 | + * @param {SCRIPT_TYPES} toType Target type | |
| 608 | + * @param {SCRIPT_TYPES|null} fromType Source type, if null it will be determined automatically | |
| 609 | + * @param {boolean} saveSettings Whether to save the settings immediately | |
| 610 | + * @returns {Promise<void>} | |
| 611 | + */ | |
| 612 | +async function moveRegexScript(script, toType, fromType = null, saveSettings = true) { | |
| 613 | + if (!Object.values(SCRIPT_TYPES).includes(toType)) { | |
| 614 | + console.warn(`moveRegexScript: Invalid target script type ${toType}`); | |
| 615 | + return; | |
| 616 | + } | |
| 617 | + if (!Object.values(SCRIPT_TYPES).includes(fromType)) { | |
| 618 | + fromType = getScriptType(script); | |
| 563 | 619 | } |
| 620 | + if (fromType === toType || fromType === SCRIPT_TYPE_UNKNOWN || toType === SCRIPT_TYPE_UNKNOWN) { | |
| 621 | + return; | |
| 622 | + } | |
| 623 | + await deleteRegexScript(script.id, fromType, false); | |
| 624 | + await saveRegexScript(script, -1, toType, saveSettings); | |
| 564 | 625 | } |
| 565 | 626 | |
| 566 | 627 | async function loadRegexScripts() { |
| 567 | 628 | $('#saved_regex_scripts').empty(); |
| 568 | 629 | $('#saved_scoped_scripts').empty(); |
| 630 | + $('#saved_preset_scripts').empty(); | |
| 569 | 631 | setToggleAllIcon(false); |
| 570 | 632 | |
| 571 | 633 | const scriptTemplate = $(await renderExtensionTemplateAsync('regex', 'scriptTemplate')); |
| @@ -574,20 +636,20 @@ async function loadRegexScripts() { | ||
| 574 | 636 | * Renders a script to the UI. |
| 575 | 637 | * @param {string} container Container to render the script to |
| 576 | 638 | * @param {import('../../char-data.js').RegexScriptData} script Script data |
| 577 | 639 | * @param {booleanSCRIPT_TYPES} isScoped Script isscriptType scopedType toof athe characterscript |
| 578 | 640 | * @param {number} index Index of the script in the array |
| 579 | 641 | */ |
| 580 | 642 | function renderScript(container, script, isScopedscriptType, index) { |
| 581 | 643 | // Have to clone here |
| 582 | 644 | const scriptHtml = scriptTemplate.clone(); |
| 583 | 645 | const save = () => saveRegexScript(script, index, isScopedscriptType); |
| 584 | 646 | |
| 585 | 647 | if (!script.id) { |
| 586 | 648 | script.id = uuidv4(); |
| 587 | 649 | } |
| 588 | 650 | |
| 589 | 651 | scriptHtml.attr('id', script.id); |
| 590 | 652 | scriptHtml.find('.regex_script_name').text(script.scriptName).attr('title', script.scriptName); |
| 591 | 653 | scriptHtml.find('.disable_regex').prop('checked', script.disabled ?? false) |
| 592 | 654 | .on('input', async function () { |
| 593 | 655 | script.disabled = !!$(this).prop('checked'); |
| @@ -600,7 +662,7 @@ async function loadRegexScripts() { | ||
| 600 | 662 | scriptHtml.find('.disable_regex').prop('checked', false).trigger('input'); |
| 601 | 663 | }); |
| 602 | 664 | scriptHtml.find('.edit_existing_regex').on('click', async function () { |
| 603 | 665 | await onRegexEditorOpenClick(scriptHtml.attr('id'), isScopedscriptType); |
| 604 | 666 | }); |
| 605 | 667 | scriptHtml.find('.move_to_global').on('click', async function () { |
| 606 | 668 | const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to global?`, POPUP_TYPE.CONFIRM); |
| @@ -608,29 +670,32 @@ async function loadRegexScripts() { | ||
| 608 | 670 | if (!confirm) { |
| 609 | 671 | return; |
| 610 | 672 | } |
| 611 | - | |
| 673 | + await moveRegexScript(script, SCRIPT_TYPES.GLOBAL, scriptType); | |
| 612 | - await deleteRegexScript({ id: script.id, isScoped: true }); | |
| 613 | - await saveRegexScript(script, -1, false); | |
| 614 | 674 | }); |
| 615 | 675 | scriptHtml.find('.move_to_scoped').on('click', async function () { |
| 616 | 676 | if (this_chid === undefined) { |
| 617 | 677 | toastr.error(t`No character selected.`); |
| 618 | 678 | return; |
| 619 | 679 | } |
| 620 | - | |
| 621 | 680 | if (selected_group) { |
| 622 | 681 | toastr.error(t`Cannot edit scoped scripts in group chats.`); |
| 623 | 682 | return; |
| 624 | 683 | } |
| 625 | - | |
| 626 | 684 | const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to scoped?`, POPUP_TYPE.CONFIRM); |
| 627 | - | |
| 628 | 685 | if (!confirm) { |
| 629 | 686 | return; |
| 630 | 687 | } |
| 631 | - | |
| 688 | + await moveRegexScript(script, SCRIPT_TYPES.SCOPED, scriptType); | |
| 632 | - await deleteRegexScript({ id: script.id, isScoped: false }); | |
| 689 | + }); | |
| 633 | - await saveRegexScript(script, -1, true); | |
| 690 | + scriptHtml.find('.move_to_preset').on('click', async function () { | |
| 691 | + const confirm = await callGenericPopup( | |
| 692 | + t`Are you sure you want to move this regex script to preset?`, | |
| 693 | + POPUP_TYPE.CONFIRM, | |
| 694 | + ); | |
| 695 | + if (!confirm) { | |
| 696 | + return; | |
| 697 | + } | |
| 698 | + await moveRegexScript(script, SCRIPT_TYPES.PRESET, scriptType); | |
| 634 | 699 | }); |
| 635 | 700 | scriptHtml.find('.export_regex').on('click', async function () { |
| 636 | 701 | const fileName = `regex-${sanitizeFileName(script.scriptName)}.json`; |
| @@ -639,39 +704,65 @@ async function loadRegexScripts() { | ||
| 639 | 704 | }); |
| 640 | 705 | scriptHtml.find('.delete_regex').on('click', async function () { |
| 641 | 706 | const confirm = await callGenericPopup(t`Are you sure you want to delete this regex script?`, POPUP_TYPE.CONFIRM); |
| 642 | - | |
| 643 | 707 | if (!confirm) { |
| 644 | 708 | return; |
| 645 | 709 | } |
| 646 | - | |
| 710 | + await deleteRegexScript(script.id, scriptType); | |
| 647 | - await deleteRegexScript({ id: script.id, isScoped }); | |
| 648 | 711 | await reloadCurrentChat(); |
| 649 | 712 | }); |
| 650 | 713 | scriptHtml.find('.regex_bulk_checkbox').on('change', function () { |
| 714 | + setMoveButtonsVisibility(); | |
| 651 | 715 | const checkboxes = $('#regex_container .regex_bulk_checkbox'); |
| 652 | 716 | const allAreChecked = checkboxes.length === checkboxes.filter(':checked').length; |
| 653 | 717 | setToggleAllIcon(allAreChecked); |
| 654 | 718 | }); |
| 719 | + scriptHtml.find('input[name="regex_expand"]').on('change', function () { | |
| 720 | + if (!(this instanceof HTMLInputElement)) { | |
| 721 | + return; | |
| 722 | + } | |
| 723 | + | |
| 724 | + if (!this.checked) { | |
| 725 | + return; | |
| 726 | + } | |
| 727 | + | |
| 728 | + const closeMenuHandler = (e) => { | |
| 729 | + if (e.target instanceof HTMLElement) { | |
| 730 | + if (e.target.closest('.regex-script-label')) { | |
| 731 | + return; | |
| 732 | + } | |
| 733 | + this.checked = false; | |
| 734 | + document.removeEventListener('click', closeMenuHandler); | |
| 735 | + } | |
| 736 | + }; | |
| 737 | + | |
| 738 | + // Use setTimeout to avoid closing immediately from the same click | |
| 739 | + setTimeout(() => { | |
| 740 | + document.addEventListener('click', closeMenuHandler, { passive: true, once: false }); | |
| 741 | + }, 0); | |
| 742 | + }); | |
| 655 | 743 | |
| 656 | 744 | $(container).append(scriptHtml); |
| 657 | 745 | } |
| 658 | 746 | |
| 659 | 747 | extension_settings?getScriptsByType(SCRIPT_TYPES.regex?GLOBAL).forEach((script, index) => renderScript('#saved_regex_scripts', script, falseSCRIPT_TYPES.GLOBAL, index)); |
| 660 | 748 | characters[this_chid]?.data?.extensions?getScriptsByType(SCRIPT_TYPES.regex_scripts?SCOPED).forEach((script, index) => renderScript('#saved_scoped_scripts', script, trueSCRIPT_TYPES.SCOPED, index)); |
| 749 | + getScriptsByType(SCRIPT_TYPES.PRESET).forEach((script, index) => renderScript('#saved_preset_scripts', script, SCRIPT_TYPES.PRESET, index)); | |
| 661 | 750 | |
| 662 | - const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar); | |
| 751 | + $('#regex_scoped_toggle').prop('checked', isScopedScriptsAllowed(characters?.[this_chid])); | |
| 663 | 752 | $('#regex_scoped_toggleregex_preset_toggle').prop('checked', isAllowedisPresetScriptsAllowed(getCurrentPresetAPI(), getCurrentPresetName())); |
| 753 | + | |
| 754 | + setMoveButtonsVisibility(); | |
| 664 | 755 | } |
| 665 | 756 | |
| 666 | 757 | /** |
| 667 | 758 | * Opens the regex editor. |
| 668 | 759 | * @param {string|boolean} existingId Existing ID |
| 669 | 760 | * @param {booleanSCRIPT_TYPES} isScopedscriptType IsType of the script scoped to a character? |
| 670 | 761 | * @returns {Promise<void>} |
| 671 | 762 | */ |
| 672 | 763 | async function onRegexEditorOpenClick(existingId, isScopedscriptType) { |
| 673 | 764 | const editorHtml = $(await renderExtensionTemplateAsync('regex', 'editor')); |
| 674 | - const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? []; | |
| 765 | + const array = getScriptsByType(scriptType); | |
| 675 | 766 | |
| 676 | 767 | // If an ID exists, fill in all the values |
| 677 | 768 | let existingScriptIndex = -1; |
| @@ -776,7 +867,7 @@ async function onRegexEditorOpenClick(existingId, isScoped) { | ||
| 776 | 867 | maxDepth: parseInt(String(editorHtml.find('input[name="max_depth"]').val())), |
| 777 | 868 | }; |
| 778 | 869 | |
| 779 | 870 | saveRegexScript(newRegexScript, existingScriptIndex, isScopedscriptType); |
| 780 | 871 | } |
| 781 | 872 | } |
| 782 | 873 | |
| @@ -969,28 +1060,35 @@ function populateDebuggerRuleList(container) { | ||
| 969 | 1060 | |
| 970 | 1061 | const allScripts = getRegexScripts(); |
| 971 | 1062 | if (!allScripts || allScripts.length === 0) { |
| 972 | 1063 | rulesContainer.append('<div class="regex-debugger-no-rules">' + t`No regex rules found.` + '</div>'); |
| 973 | 1064 | return; |
| 974 | 1065 | } |
| 975 | 1066 | |
| 976 | 1067 | const globalScriptIds = new Set(getScriptsByType(extension_settingsSCRIPT_TYPES.regex ?? []GLOBAL).map(s => s.id)); |
| 1068 | + const scopedScriptIds = new Set(getScriptsByType(SCRIPT_TYPES.SCOPED).map(s => s.id)); | |
| 1069 | + const presetScriptIds = new Set(getScriptsByType(SCRIPT_TYPES.PRESET).map(s => s.id)); | |
| 977 | 1070 | const globalScripts = []; |
| 978 | 1071 | const scopedScripts = []; |
| 1072 | + const presetScripts = []; | |
| 979 | 1073 | |
| 980 | 1074 | allScripts.forEach(script => { |
| 981 | 1075 | const scriptCopy = structuredClone(script); // Use structuredClone for deep copy |
| 982 | 1076 | if (globalScriptIds.has(script.id)) { |
| 983 | 1077 | // @ts-ignore |
| 984 | 1078 | scriptCopy.isScopedtype = falseSCRIPT_TYPES.GLOBAL; |
| 985 | 1079 | globalScripts.push(scriptCopy); |
| 986 | - } else { | |
| 1080 | + } else if (scopedScriptIds.has(script.id)) { | |
| 987 | 1081 | // @ts-ignore |
| 988 | 1082 | scriptCopy.isScopedtype = trueSCRIPT_TYPES.SCOPED; |
| 989 | 1083 | scopedScripts.push(scriptCopy); |
| 1084 | + } else if (presetScriptIds.has(script.id)) { | |
| 1085 | + // @ts-ignore | |
| 1086 | + scriptCopy.type = SCRIPT_TYPES.PRESET; | |
| 1087 | + presetScripts.push(scriptCopy); | |
| 990 | 1088 | } |
| 991 | 1089 | }); |
| 992 | 1090 | |
| 993 | 1091 | container.data('allScripts', [...globalScripts, ...scopedScripts, ...presetScripts]); |
| 994 | 1092 | |
| 995 | 1093 | const renderRule = (script) => { |
| 996 | 1094 | if (!script.id) script.id = uuidv4(); |
| @@ -1002,10 +1100,18 @@ function populateDebuggerRuleList(container) { | ||
| 1002 | 1100 | ruleElement.find('.rule-name').text(script.scriptName); |
| 1003 | 1101 | ruleElement.find('.rule-regex').text(script.findRegex); |
| 1004 | 1102 | // @ts-ignore |
| 1005 | - ruleElement.find('.rule-scope').text(script.isScoped ? 'Scoped' : 'Global'); | |
| 1103 | + ruleElement | |
| 1104 | + .find('.rule-scope') | |
| 1105 | + .text( | |
| 1106 | + { | |
| 1107 | + [SCRIPT_TYPES.SCOPED]: t`Scoped`, | |
| 1108 | + [SCRIPT_TYPES.GLOBAL]: t`Global`, | |
| 1109 | + [SCRIPT_TYPES.PRESET]: t`Preset`, | |
| 1110 | + }[script.type], | |
| 1111 | + ); | |
| 1006 | 1112 | ruleElement.find('.rule-enabled').prop('checked', !script.disabled); |
| 1007 | 1113 | // @ts-ignore |
| 1008 | 1114 | ruleElement.find('.edit_rule').on('click', () => onRegexEditorOpenClick(script.id, script.isScopedtype)); |
| 1009 | 1115 | |
| 1010 | 1116 | ruleElement.on('click', function (event) { |
| 1011 | 1117 | if ($(event.target).is('input, .menu_button, .menu_button i')) { |
| @@ -1035,18 +1141,25 @@ function populateDebuggerRuleList(container) { | ||
| 1035 | 1141 | }; |
| 1036 | 1142 | |
| 1037 | 1143 | if (globalScripts.length > 0) { |
| 1038 | 1144 | rulesContainer.append('<div class="list-header regex-debugger-list-header">' + t`Global Rules` + '</div>'); |
| 1039 | 1145 | const globalList = $('<ul id="regex_debugger_rules_global" class="sortable-list"></ul>'); |
| 1040 | 1146 | globalScripts.forEach(script => globalList.append(renderRule(script))); |
| 1041 | 1147 | rulesContainer.append(globalList); |
| 1042 | 1148 | } |
| 1043 | 1149 | |
| 1044 | 1150 | if (scopedScripts.length > 0) { |
| 1045 | 1151 | rulesContainer.append('<div class="list-header regex-debugger-list-header">' + t`Scoped Rules` + '</div>'); |
| 1046 | 1152 | const scopedList = $('<ul id="regex_debugger_rules_scoped" class="sortable-list"></ul>'); |
| 1047 | 1153 | scopedScripts.forEach(script => scopedList.append(renderRule(script))); |
| 1048 | 1154 | rulesContainer.append(scopedList); |
| 1049 | 1155 | } |
| 1156 | + | |
| 1157 | + if (presetScripts.length > 0) { | |
| 1158 | + rulesContainer.append('<div class="list-header regex-debugger-list-header">' + t`Preset Rules` + '</div>'); | |
| 1159 | + const presetList = $('<ul id="regex_debugger_rules_preset" class="sortable-list"></ul>'); | |
| 1160 | + presetScripts.forEach(script => presetList.append(renderRule(script))); | |
| 1161 | + rulesContainer.append(presetList); | |
| 1162 | + } | |
| 1050 | 1163 | } |
| 1051 | 1164 | |
| 1052 | 1165 | /** |
| @@ -1065,12 +1178,15 @@ async function onRegexDebuggerOpenClick() { | ||
| 1065 | 1178 | debuggerHtml.find('#regex_debugger_rules_global').sortable({ delay: getSortableDelay() }).disableSelection(); |
| 1066 | 1179 | // @ts-ignore |
| 1067 | 1180 | debuggerHtml.find('#regex_debugger_rules_scoped').sortable({ delay: getSortableDelay() }).disableSelection(); |
| 1181 | + // @ts-ignore | |
| 1182 | + debuggerHtml.find('#regex_debugger_rules_preset').sortable({ delay: getSortableDelay() }).disableSelection(); | |
| 1068 | 1183 | |
| 1069 | 1184 | debuggerHtml.find('#regex_debugger_run_test').on('click', function () { |
| 1070 | 1185 | const allScripts = debuggerHtml.data('allScripts'); |
| 1071 | 1186 | const orderedRuleIds = [ |
| 1072 | 1187 | ...$('#regex_debugger_rules_global').find('li.regex-debugger-rule').map((i, el) => $(el).data('id')).get(), |
| 1073 | 1188 | ...$('#regex_debugger_rules_scoped').find('li.regex-debugger-rule').map((i, el) => $(el).data('id')).get(), |
| 1189 | + ...$('#regex_debugger_rules_preset').find('li.regex-debugger-rule').map((i, el) => $(el).data('id')).get(), | |
| 1074 | 1190 | ]; |
| 1075 | 1191 | |
| 1076 | 1192 | const rawInput = String($('#regex_debugger_raw_input').val()); |
| @@ -1106,9 +1222,9 @@ async function onRegexDebuggerOpenClick() { | ||
| 1106 | 1222 | // Set the ID on the TOP-LEVEL element that is being appended. |
| 1107 | 1223 | stepElement.find('>:first-child').attr('id', `step-result-${script.id}`); |
| 1108 | 1224 | const stepHeader = stepElement.find('.step-header'); |
| 1109 | 1225 | stepHeader.find('strong').text(t`After:` + ` ${script.scriptName}`); |
| 1110 | 1226 | |
| 1111 | 1227 | const metricsHtml = `'<span class="step-metrics">' + t`Captured:` + ` ${result.charsCaptured}, ` + t`Added:` + ` +${result.charsAdded}, ` + t`Removed:` + ` -${result.charsRemoved}</span>`; |
| 1112 | 1228 | stepHeader.append(metricsHtml); |
| 1113 | 1229 | |
| 1114 | 1230 | if (displayMode === 'highlight') { |
| @@ -1128,7 +1244,7 @@ async function onRegexDebuggerOpenClick() { | ||
| 1128 | 1244 | |
| 1129 | 1245 | const summaryHtml = ` |
| 1130 | 1246 | <div id="regex_debugger_final_summary" class="regex-debugger-summary"> |
| 1131 | 1247 | <strong>` + t`Total Captured:` + `</strong> ${totalCharsCaptured} | <strong>` + t`Total Added:` + `</strong> +${totalCharsAdded} | <strong>` + t`Total Removed:` + `</strong> -${totalCharsRemoved} |
| 1132 | 1248 | </div> |
| 1133 | 1249 | `; |
| 1134 | 1250 | finalOutput.before(summaryHtml); |
| @@ -1148,11 +1264,13 @@ async function onRegexDebuggerOpenClick() { | ||
| 1148 | 1264 | const allKnownScripts = getRegexScripts(); |
| 1149 | 1265 | const newGlobalScripts = $('#regex_debugger_rules_global').children('li').map((_, el) => allKnownScripts.find(s => s.id === $(el).data('id'))).get().filter(Boolean); |
| 1150 | 1266 | const newScopedScripts = $('#regex_debugger_rules_scoped').children('li').map((_, el) => allKnownScripts.find(s => s.id === $(el).data('id'))).get().filter(Boolean); |
| 1267 | + const newPresetScripts = $('#regex_debugger_rules_preset').children('li').map((_, el) => allKnownScripts.find(s => s.id === $(el).data('id'))).get().filter(Boolean); | |
| 1151 | 1268 | |
| 1152 | 1269 | extension_settings.regex = newGlobalScripts; |
| 1153 | 1270 | if (this_chid !== undefined) { |
| 1154 | 1271 | await writeExtensionFieldsaveScriptsByType(this_chid, 'regex_scripts'newScopedScripts, newScopedScriptsSCRIPT_TYPES.SCOPED); |
| 1155 | 1272 | } |
| 1273 | + await saveScriptsByType(newPresetScripts, SCRIPT_TYPES.PRESET); | |
| 1156 | 1274 | |
| 1157 | 1275 | saveSettingsDebounced(); |
| 1158 | 1276 | await loadRegexScripts(); |
| @@ -1164,6 +1282,8 @@ async function onRegexDebuggerOpenClick() { | ||
| 1164 | 1282 | currentPopupContent.find('#regex_debugger_rules_global').sortable({ delay: getSortableDelay() }).disableSelection(); |
| 1165 | 1283 | // @ts-ignore |
| 1166 | 1284 | currentPopupContent.find('#regex_debugger_rules_scoped').sortable({ delay: getSortableDelay() }).disableSelection(); |
| 1285 | + // @ts-ignore | |
| 1286 | + currentPopupContent.find('#regex_debugger_rules_preset').sortable({ delay: getSortableDelay() }).disableSelection(); | |
| 1167 | 1287 | }); |
| 1168 | 1288 | |
| 1169 | 1289 | debuggerHtml.find('#regex_debugger_expand_steps').on('click', function () { |
| @@ -1204,13 +1324,13 @@ async function onRegexDebuggerOpenClick() { | ||
| 1204 | 1324 | }); |
| 1205 | 1325 | |
| 1206 | 1326 | popupContainer.append(navPanel).append(contentPanel); |
| 1207 | 1327 | callGenericPopup(popupContainer, POPUP_TYPE.TEXT, 't`Step-by-step Transformation'`, { wide: true, allowVerticalScrolling: false }); |
| 1208 | 1328 | }); |
| 1209 | 1329 | |
| 1210 | 1330 | debuggerHtml.find('#regex_debugger_expand_final').on('click', function () { |
| 1211 | 1331 | const content = $('#regex_debugger_final_output').html(); |
| 1212 | 1332 | const popupContent = $('<div class="regex-popup-content"></div>').html(content); |
| 1213 | 1333 | callGenericPopup(popupContent, POPUP_TYPE.TEXT, 't`Final Output'`, { wide: true, large: true, allowVerticalScrolling: true }); |
| 1214 | 1334 | }); |
| 1215 | 1335 | |
| 1216 | 1336 | await callGenericPopup(debuggerHtml.children(), POPUP_TYPE.TEXT, '', { wide: true, allowVerticalScrolling: true }); |
| @@ -1289,11 +1409,6 @@ function migrateSettings() { | ||
| 1289 | 1409 | } |
| 1290 | 1410 | }); |
| 1291 | 1411 | |
| 1292 | - if (!extension_settings.character_allowed_regex) { | |
| 1293 | - extension_settings.character_allowed_regex = []; | |
| 1294 | - performSave = true; | |
| 1295 | - } | |
| 1296 | - | |
| 1297 | 1412 | if (performSave) { |
| 1298 | 1413 | saveSettingsDebounced(); |
| 1299 | 1414 | } |
| @@ -1364,10 +1479,10 @@ async function toggleRegexCallback(args, scriptName) { | ||
| 1364 | 1479 | break; |
| 1365 | 1480 | } |
| 1366 | 1481 | |
| 1367 | - const isScoped = characters[this_chid]?.data?.extensions?.regex_scripts?.some(s => s.id === script.id); | |
| 1482 | + const scriptType = getScriptType(script); | |
| 1368 | 1483 | const index = isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts?.indexOfgetScriptsByType(scriptscriptType) : scripts.indexOf(script); |
| 1369 | 1484 | |
| 1370 | 1485 | await saveRegexScript(script, index, isScopedscriptType); |
| 1371 | 1486 | if (script.disabled) { |
| 1372 | 1487 | !quiet && toastr.success(t`Regex script '${scriptName}' has been disabled.`); |
| 1373 | 1488 | } else { |
| @@ -1379,10 +1494,10 @@ async function toggleRegexCallback(args, scriptName) { | ||
| 1379 | 1494 | |
| 1380 | 1495 | /** |
| 1381 | 1496 | * Performs the import of the regex object. |
| 1382 | 1497 | * @param {ObjectRegexScript} regexScript Input object |
| 1383 | 1498 | * @param {booleanSCRIPT_TYPES} isScopedscriptType IsThe thetype scriptof scopedscript to aimport character?as |
| 1384 | 1499 | */ |
| 1385 | 1500 | async function onRegexImportObjectChange(regexScript, isScopedscriptType) { |
| 1386 | 1501 | try { |
| 1387 | 1502 | if (!regexScript.scriptName) { |
| 1388 | 1503 | throw new Error('No script name provided.'); |
| @@ -1391,11 +1506,21 @@ async function onRegexImportObjectChange(regexScript, isScoped) { | ||
| 1391 | 1506 | // Assign a new UUID |
| 1392 | 1507 | regexScript.id = uuidv4(); |
| 1393 | 1508 | |
| 1394 | - const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? []; | |
| 1509 | + const array = getScriptsByType(scriptType); | |
| 1395 | 1510 | array.push(regexScript); |
| 1396 | 1511 | |
| 1397 | 1512 | ifswitch (isScopedscriptType) { |
| 1398 | - await writeExtensionField(this_chid, 'regex_scripts', array); | |
| 1513 | + case SCRIPT_TYPES.GLOBAL: | |
| 1514 | + // will be handled by saveSettingsDebounced | |
| 1515 | + break; | |
| 1516 | + case SCRIPT_TYPES.SCOPED: | |
| 1517 | + await saveScriptsByType(array, SCRIPT_TYPES.SCOPED); | |
| 1518 | + break; | |
| 1519 | + case SCRIPT_TYPES.PRESET: | |
| 1520 | + await saveScriptsByType(array, SCRIPT_TYPES.PRESET); | |
| 1521 | + break; | |
| 1522 | + default: | |
| 1523 | + break; | |
| 1399 | 1524 | } |
| 1400 | 1525 | |
| 1401 | 1526 | saveSettingsDebounced(); |
| @@ -1411,9 +1536,9 @@ async function onRegexImportObjectChange(regexScript, isScoped) { | ||
| 1411 | 1536 | /** |
| 1412 | 1537 | * Performs the import of the regex file. |
| 1413 | 1538 | * @param {File} file Input file |
| 1414 | 1539 | * @param {booleanSCRIPT_TYPES} isScopedscriptType IsThe thetype scriptof scopedscript to aimport character?as |
| 1415 | 1540 | */ |
| 1416 | 1541 | async function onRegexImportFileChange(file, isScopedscriptType) { |
| 1417 | 1542 | if (!file) { |
| 1418 | 1543 | toastr.error('No file provided.'); |
| 1419 | 1544 | return; |
| @@ -1423,10 +1548,10 @@ async function onRegexImportFileChange(file, isScoped) { | ||
| 1423 | 1548 | const regexScripts = JSON.parse(await getFileText(file)); |
| 1424 | 1549 | if (Array.isArray(regexScripts)) { |
| 1425 | 1550 | for (const regexScript of regexScripts) { |
| 1426 | 1551 | await onRegexImportObjectChange(regexScript, isScopedscriptType); |
| 1427 | 1552 | } |
| 1428 | 1553 | } else { |
| 1429 | 1554 | await onRegexImportObjectChange(regexScripts, isScopedscriptType); |
| 1430 | 1555 | } |
| 1431 | 1556 | } catch (error) { |
| 1432 | 1557 | console.log(error); |
| @@ -1435,45 +1560,150 @@ async function onRegexImportFileChange(file, isScoped) { | ||
| 1435 | 1560 | } |
| 1436 | 1561 | } |
| 1437 | 1562 | |
| 1563 | +/** | |
| 1564 | + * Determines the type of a given script. | |
| 1565 | + * @param {RegexScript} script The script to check | |
| 1566 | + * @returns {SCRIPT_TYPES} The script type. | |
| 1567 | + */ | |
| 1568 | +function getScriptType(script) { | |
| 1569 | + for (const scriptType of Object.values(SCRIPT_TYPES)) { | |
| 1570 | + const scripts = getScriptsByType(scriptType); | |
| 1571 | + if (scripts.some(s => s.id === script.id)) { | |
| 1572 | + return scriptType; | |
| 1573 | + } | |
| 1574 | + } | |
| 1575 | + return SCRIPT_TYPE_UNKNOWN; | |
| 1576 | +} | |
| 1577 | + | |
| 1578 | +function getSelectedScripts() { | |
| 1579 | + const scripts = getRegexScripts(); | |
| 1580 | + const selector = '#regex_container .regex-script-label:has(.regex_bulk_checkbox:checked)'; | |
| 1581 | + const selectedIds = Array.from(document.querySelectorAll(selector)) | |
| 1582 | + .map(e => e.getAttribute('id')) | |
| 1583 | + .filter(id => id); | |
| 1584 | + return scripts.filter(script => selectedIds.includes(script.id)); | |
| 1585 | +} | |
| 1586 | + | |
| 1438 | 1587 | function purgeEmbeddedRegexScripts({ character }) { |
| 1439 | 1588 | const avatar = character?.avatar; |
| 1589 | + if (!avatar) { | |
| 1590 | + return; | |
| 1591 | + } | |
| 1592 | + const checkKey = `AlertRegex_${avatar}`; | |
| 1593 | + if (accountStorage.getItem(checkKey)) { | |
| 1594 | + accountStorage.removeItem(checkKey); | |
| 1595 | + } | |
| 1596 | + disallowScopedScripts(characters?.[this_chid]); | |
| 1597 | +} | |
| 1440 | 1598 | |
| 1441 | - if (avatar && extension_settings.character_allowed_regex?.includes(avatar)) { | |
| 1599 | +function purgePresetEmbeddedRegexScripts({ apiId, name }) { | |
| 1442 | - const index = extension_settings.character_allowed_regex.indexOf(avatar); | |
| 1600 | + const checkKey = `AlertRegex_${apiId}_${name}`; | |
| 1443 | - if (index !== -1) { | |
| 1601 | + if (accountStorage.getItem(checkKey)) { | |
| 1444 | - extension_settings.character_allowed_regex.splice(index, 1); | |
| 1602 | + accountStorage.removeItem(checkKey); | |
| 1445 | - saveSettingsDebounced(); | |
| 1446 | - } | |
| 1447 | 1603 | } |
| 1604 | + disallowPresetScripts(apiId, name); | |
| 1448 | 1605 | } |
| 1449 | 1606 | |
| 1450 | 1607 | async function checkEmbeddedRegexScriptscheckCharEmbeddedRegexScripts() { |
| 1451 | 1608 | const chid = this_chid; |
| 1452 | 1609 | |
| 1453 | 1610 | if (chid !== undefined && !selected_group) { |
| 1454 | 1611 | const avatarcharacter = characters[chid]?.avatar; |
| 1455 | 1612 | const scripts = characters[chid]?.data?.extensions?getScriptsByType(SCRIPT_TYPES.regex_scriptsSCOPED); |
| 1456 | 1613 | |
| 1457 | 1614 | if (Array.isArray(scripts) && scripts.length > 0) { |
| 1458 | 1615 | if (avatar && !extension_settings.character_allowed_regex.includesisScopedScriptsAllowed(avatarcharacter)) { |
| 1459 | 1616 | const checkKey = `AlertRegex_${characters[chid]character.avatar}`; |
| 1460 | - | |
| 1461 | 1617 | if (!accountStorage.getItem(checkKey)) { |
| 1462 | 1618 | accountStorage.setItem(checkKey, 'true'); |
| 1463 | 1619 | const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {}); |
| 1464 | 1620 | const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Yes' }); |
| 1465 | 1621 | |
| 1466 | 1622 | if (result) { |
| 1467 | 1623 | extension_settings.character_allowed_regex.pushallowScopedScripts(avatarcharacter); |
| 1468 | 1624 | await reloadCurrentChat(); |
| 1469 | - saveSettingsDebounced(); | |
| 1470 | 1625 | } |
| 1471 | 1626 | } |
| 1472 | 1627 | } |
| 1473 | 1628 | } |
| 1474 | 1629 | } |
| 1475 | 1630 | |
| 1476 | 1631 | await loadRegexScripts(); |
| 1632 | +} | |
| 1633 | + | |
| 1634 | +/** | |
| 1635 | + * Notify whether to reload current chat when preset is changed | |
| 1636 | + * @param {string} presetName The name of the preset | |
| 1637 | + */ | |
| 1638 | +function notifyReloadCurrentChat(presetName) { | |
| 1639 | + toastr.info( | |
| 1640 | + t`Reload the chat for regex to take effect` + '<br><u>' + t`Click here to reload immediately` + '</u>', | |
| 1641 | + t`Preset '${presetName}' contains enabled regex scripts`, | |
| 1642 | + { | |
| 1643 | + timeOut: 5000, | |
| 1644 | + escapeHtml: false, | |
| 1645 | + onclick: reloadCurrentChat, | |
| 1646 | + }); | |
| 1647 | +} | |
| 1648 | + | |
| 1649 | +async function checkPresetEmbeddedRegexScripts() { | |
| 1650 | + const apiId = getCurrentPresetAPI(); | |
| 1651 | + const name = getCurrentPresetName(); | |
| 1652 | + const scripts = getScriptsByType(SCRIPT_TYPES.PRESET); | |
| 1653 | + | |
| 1654 | + if (Array.isArray(scripts) && scripts.length > 0) { | |
| 1655 | + if (!isPresetScriptsAllowed(apiId, name)) { | |
| 1656 | + const checkKey = `AlertRegex_${apiId}_${name}`; | |
| 1657 | + | |
| 1658 | + if (!accountStorage.getItem(checkKey)) { | |
| 1659 | + accountStorage.setItem(checkKey, 'true'); | |
| 1660 | + const template = await renderExtensionTemplateAsync('regex', 'presetEmbeddedScripts', {}); | |
| 1661 | + const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, ''); | |
| 1662 | + | |
| 1663 | + if (result) { | |
| 1664 | + allowPresetScripts(apiId, name); | |
| 1665 | + if (getCurrentChatId()) { | |
| 1666 | + await reloadCurrentChat(); | |
| 1667 | + } | |
| 1668 | + } | |
| 1669 | + } | |
| 1670 | + } else if (getCurrentChatId() && scripts.filter(script => !script.disabled).length > 0) { | |
| 1671 | + notifyReloadCurrentChat(name); | |
| 1672 | + } | |
| 1673 | + } | |
| 1674 | + | |
| 1675 | + await loadRegexScripts(); | |
| 1676 | +} | |
| 1677 | + | |
| 1678 | +async function onMainApiChanged({ apiId }) { | |
| 1679 | + const presetManager = getPresetManager(apiId); | |
| 1680 | + if (!presetManager) { | |
| 1681 | + return; | |
| 1682 | + } | |
| 1683 | + const presetName = presetManager.getSelectedPresetName(); | |
| 1684 | + const presetScripts = presetManager.readPresetExtensionField({ path: 'regex_scripts' }) ?? []; | |
| 1685 | + if (getCurrentChatId() && | |
| 1686 | + isPresetScriptsAllowed(apiId, presetName) && | |
| 1687 | + Array.isArray(presetScripts) && | |
| 1688 | + presetScripts.filter(script => !script.disabled).length > 0) { | |
| 1689 | + notifyReloadCurrentChat(presetName); | |
| 1690 | + } | |
| 1691 | + | |
| 1692 | + await loadRegexScripts(); | |
| 1693 | +} | |
| 1694 | + | |
| 1695 | +function onPresetRenamed({ apiId, oldName, newName }) { | |
| 1696 | + const oldCheckKey = `AlertRegex_${apiId}_${oldName}`; | |
| 1697 | + const checkKey = `AlertRegex_${apiId}_${newName}`; | |
| 1698 | + const value = accountStorage.getItem(oldCheckKey); | |
| 1699 | + if (value) { | |
| 1700 | + accountStorage.setItem(checkKey, value); | |
| 1701 | + accountStorage.removeItem(oldCheckKey); | |
| 1702 | + } | |
| 1703 | + if (isPresetScriptsAllowed(apiId, oldName)) { | |
| 1704 | + disallowPresetScripts(apiId, oldName); | |
| 1705 | + allowPresetScripts(apiId, newName); | |
| 1706 | + } | |
| 1477 | 1707 | } |
| 1478 | 1708 | |
| 1479 | 1709 | // Workaround for loading in sequence with other extensions |
| @@ -1497,7 +1727,7 @@ jQuery(async () => { | ||
| 1497 | 1727 | const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown')); |
| 1498 | 1728 | $('#regex_container').append(settingsHtml); |
| 1499 | 1729 | $('#open_regex_editor').on('click', function () { |
| 1500 | 1730 | onRegexEditorOpenClick(false, falseSCRIPT_TYPES.GLOBAL); |
| 1501 | 1731 | }); |
| 1502 | 1732 | $('#open_regex_debugger').on('click', onRegexDebuggerOpenClick); |
| 1503 | 1733 | $('#open_scoped_editor').on('click', function () { |
| @@ -1511,19 +1741,23 @@ jQuery(async () => { | ||
| 1511 | 1741 | return; |
| 1512 | 1742 | } |
| 1513 | 1743 | |
| 1514 | 1744 | onRegexEditorOpenClick(false, trueSCRIPT_TYPES.SCOPED); |
| 1745 | + }); | |
| 1746 | + $('#open_preset_editor').on('click', function () { | |
| 1747 | + onRegexEditorOpenClick(false, SCRIPT_TYPES.PRESET); | |
| 1515 | 1748 | }); |
| 1516 | 1749 | $('#import_regex_file').on('change', async function () { |
| 1517 | 1750 | let target = 'global'SCRIPT_TYPES.GLOBAL; |
| 1518 | 1751 | const template = $(await renderExtensionTemplateAsync('regex', 'importTarget')); |
| 1519 | 1752 | template.find('#regex_import_target_global').on('input', () => (target = 'global'SCRIPT_TYPES.GLOBAL)); |
| 1520 | 1753 | template.find('#regex_import_target_scoped').on('input', () => (target = 'scoped'SCRIPT_TYPES.SCOPED)); |
| 1754 | + template.find('#regex_import_target_preset').on('input', () => (target = SCRIPT_TYPES.PRESET)); | |
| 1521 | 1755 | |
| 1522 | 1756 | await callGenericPopup(template, POPUP_TYPE.TEXT); |
| 1523 | 1757 | |
| 1524 | 1758 | const inputElement = this instanceof HTMLInputElement && this; |
| 1525 | 1759 | for (const file of inputElement.files) { |
| 1526 | 1760 | await onRegexImportFileChange(file, target === 'scoped'); |
| 1527 | 1761 | } |
| 1528 | 1762 | inputElement.value = ''; |
| 1529 | 1763 | }); |
| @@ -1531,13 +1765,6 @@ jQuery(async () => { | ||
| 1531 | 1765 | $('#import_regex_file').trigger('click'); |
| 1532 | 1766 | }); |
| 1533 | 1767 | |
| 1534 | - function getSelectedScripts() { | |
| 1535 | - const scripts = getRegexScripts(); | |
| 1536 | - const selector = '#regex_container .regex-script-label:has(.regex_bulk_checkbox:checked)'; | |
| 1537 | - const selectedIds = Array.from(document.querySelectorAll(selector)).map(e => e.getAttribute('id')).filter(id => id); | |
| 1538 | - return scripts.filter(script => selectedIds.includes(script.id)); | |
| 1539 | - } | |
| 1540 | - | |
| 1541 | 1768 | $('#bulk_select_all_toggle').on('click', async function () { |
| 1542 | 1769 | const checkboxes = $('#regex_container .regex_bulk_checkbox'); |
| 1543 | 1770 | if (checkboxes.length === 0) { |
| @@ -1549,32 +1776,98 @@ jQuery(async () => { | ||
| 1549 | 1776 | |
| 1550 | 1777 | checkboxes.prop('checked', newState); |
| 1551 | 1778 | setToggleAllIcon(newState); |
| 1779 | + setMoveButtonsVisibility(); | |
| 1552 | 1780 | }); |
| 1553 | 1781 | |
| 1554 | 1782 | $('#bulk_enable_regex').on('click', async function () { |
| 1555 | - const scripts = getSelectedScripts().filter(script => script.disabled); | |
| 1783 | + await bulkToggleRegexScripts(true); | |
| 1784 | + }); | |
| 1785 | + | |
| 1786 | + $('#bulk_disable_regex').on('click', async function () { | |
| 1787 | + await bulkToggleRegexScripts(false); | |
| 1788 | + }); | |
| 1789 | + | |
| 1790 | + /** | |
| 1791 | + * Bulk enable or disable regex scripts | |
| 1792 | + * @param {boolean} newState New state to set (true = enable, false = disable) | |
| 1793 | + * @returns {Promise<void>} | |
| 1794 | + */ | |
| 1795 | + async function bulkToggleRegexScripts(newState) { | |
| 1796 | + const scripts = getSelectedScripts().filter(script => script.disabled === newState); | |
| 1556 | 1797 | if (scripts.length === 0) { |
| 1557 | - toastr.warning(t`No regex scripts selected for enabling.`); | |
| 1798 | + toastr.warning(newState | |
| 1799 | + ? t`No regex scripts selected for enabling.` | |
| 1800 | + : t`No regex scripts selected for disabling.`, | |
| 1801 | + ); | |
| 1558 | 1802 | return; |
| 1559 | 1803 | } |
| 1804 | + const scriptTypesToSave = new Set(); | |
| 1560 | 1805 | for (const script of scripts) { |
| 1561 | - script.disabled = false; | |
| 1806 | + const scriptType = getScriptType(script); | |
| 1807 | + scriptTypesToSave.add(scriptType); | |
| 1808 | + script.disabled = !newState; | |
| 1809 | + } | |
| 1810 | + for (const scriptType of scriptTypesToSave) { | |
| 1811 | + const scriptsOfType = getScriptsByType(scriptType); | |
| 1812 | + await saveScriptsByType(scriptsOfType, scriptType); | |
| 1562 | 1813 | } |
| 1814 | + | |
| 1563 | 1815 | saveSettingsDebounced(); |
| 1564 | 1816 | await loadRegexScripts(); |
| 1565 | - }); | |
| 1566 | 1817 | |
| 1567 | - $('#bulk_disable_regex').on('click', async function () { | |
| 1818 | + // Reload the current chat to undo previous markdown | |
| 1568 | 1819 | const scriptscurrentChatId = getSelectedScripts().filtergetCurrentChatId(script => !script.disabled); |
| 1820 | + if (currentChatId) { | |
| 1821 | + await reloadCurrentChat(); | |
| 1822 | + } | |
| 1823 | + } | |
| 1824 | + | |
| 1825 | + /** | |
| 1826 | + * Bulk move regex scripts to the specified type | |
| 1827 | + * @param {SCRIPT_TYPES} toType destination type | |
| 1828 | + */ | |
| 1829 | + async function bulkMoveRegexScript(toType) { | |
| 1830 | + const scripts = getSelectedScripts(); | |
| 1569 | 1831 | if (scripts.length === 0) { |
| 1570 | 1832 | toastr.warning(t`No regex scripts selected for disablingmoving.`); |
| 1571 | 1833 | return; |
| 1572 | 1834 | } |
| 1573 | 1835 | for (const script of scripts) { |
| 1574 | - script.disabled = true; | |
| 1836 | + await moveRegexScript(script, toType, getScriptType(script), false); | |
| 1575 | 1837 | } |
| 1838 | + | |
| 1576 | 1839 | saveSettingsDebounced(); |
| 1577 | 1840 | await loadRegexScripts(); |
| 1841 | + | |
| 1842 | + // Reload the current chat to undo previous markdown | |
| 1843 | + const currentChatId = getCurrentChatId(); | |
| 1844 | + if (currentChatId) { | |
| 1845 | + await reloadCurrentChat(); | |
| 1846 | + } | |
| 1847 | + } | |
| 1848 | + | |
| 1849 | + $('#bulk_regex_move_to_global').on('click', async () => { | |
| 1850 | + const confirm = await callGenericPopup(t`Are you sure you want to move the selected regex scripts to global?`, POPUP_TYPE.CONFIRM); | |
| 1851 | + if (!confirm) { | |
| 1852 | + return; | |
| 1853 | + } | |
| 1854 | + await bulkMoveRegexScript(SCRIPT_TYPES.GLOBAL); | |
| 1855 | + }); | |
| 1856 | + | |
| 1857 | + $('#bulk_regex_move_to_scoped').on('click', async () => { | |
| 1858 | + const confirm = await callGenericPopup(t`Are you sure you want to move the selected regex scripts to scoped?`, POPUP_TYPE.CONFIRM); | |
| 1859 | + if (!confirm) { | |
| 1860 | + return; | |
| 1861 | + } | |
| 1862 | + await bulkMoveRegexScript(SCRIPT_TYPES.SCOPED); | |
| 1863 | + }); | |
| 1864 | + | |
| 1865 | + $('#bulk_regex_move_to_preset').on('click', async function () { | |
| 1866 | + const confirm = await callGenericPopup(t`Are you sure you want to move the selected regex scripts to preset?`, POPUP_TYPE.CONFIRM); | |
| 1867 | + if (!confirm) { | |
| 1868 | + return; | |
| 1869 | + } | |
| 1870 | + await bulkMoveRegexScript(SCRIPT_TYPES.PRESET); | |
| 1578 | 1871 | }); |
| 1579 | 1872 | |
| 1580 | 1873 | $('#bulk_delete_regex').on('click', async function () { |
| @@ -1583,16 +1876,16 @@ jQuery(async () => { | ||
| 1583 | 1876 | toastr.warning(t`No regex scripts selected for deletion.`); |
| 1584 | 1877 | return; |
| 1585 | 1878 | } |
| 1586 | 1879 | const confirm = await callGenericPopup('t`Are you sure you want to delete the selected regex scripts?'`, POPUP_TYPE.CONFIRM); |
| 1587 | 1880 | if (!confirm) { |
| 1588 | 1881 | return; |
| 1589 | 1882 | } |
| 1590 | 1883 | for (const script of scripts) { |
| 1591 | - const isScoped = characters[this_chid]?.data?.extensions?.regex_scripts?.some(s => s.id === script.id); | |
| 1884 | + await deleteRegexScript(script.id, getScriptType(script), false); | |
| 1592 | - await deleteRegexScript({ id: script.id, isScoped: isScoped }); | |
| 1593 | 1885 | } |
| 1594 | - await reloadCurrentChat(); | |
| 1595 | 1886 | saveSettingsDebounced(); |
| 1887 | + await loadRegexScripts(); | |
| 1888 | + await reloadCurrentChat(); | |
| 1596 | 1889 | }); |
| 1597 | 1890 | |
| 1598 | 1891 | $('#bulk_export_regex').on('click', async function () { |
| @@ -1610,13 +1903,18 @@ jQuery(async () => { | ||
| 1610 | 1903 | let sortableDatas = [ |
| 1611 | 1904 | { |
| 1612 | 1905 | selector: '#saved_regex_scripts', |
| 1613 | 1906 | setter: xscripts => extension_settings.regex =saveScriptsByType(scripts, xSCRIPT_TYPES.GLOBAL), |
| 1614 | 1907 | getter: () => extension_settingsgetScriptsByType(SCRIPT_TYPES.regex ?? []GLOBAL), |
| 1615 | 1908 | }, |
| 1616 | 1909 | { |
| 1617 | 1910 | selector: '#saved_scoped_scripts', |
| 1618 | 1911 | setter: xscripts => writeExtensionFieldsaveScriptsByType(this_chid, 'regex_scripts'scripts, xSCRIPT_TYPES.SCOPED), |
| 1619 | 1912 | getter: () => characters[this_chid]?.data?.extensions?getScriptsByType(SCRIPT_TYPES.regex_scripts ?? []SCOPED), |
| 1913 | + }, | |
| 1914 | + { | |
| 1915 | + selector: '#saved_preset_scripts', | |
| 1916 | + setter: scripts => saveScriptsByType(scripts, SCRIPT_TYPES.PRESET), | |
| 1917 | + getter: () => getScriptsByType(SCRIPT_TYPES.PRESET), | |
| 1620 | 1918 | }, |
| 1621 | 1919 | ]; |
| 1622 | 1920 | for (const { selector, setter, getter } of sortableDatas) { |
| @@ -1638,6 +1936,7 @@ jQuery(async () => { | ||
| 1638 | 1936 | saveSettingsDebounced(); |
| 1639 | 1937 | |
| 1640 | 1938 | console.debug(`Regex scripts in ${selector} reordered`); |
| 1939 | + await reloadCurrentChat(); | |
| 1641 | 1940 | await loadRegexScripts(); |
| 1642 | 1941 | }, |
| 1643 | 1942 | }); |
| @@ -1655,17 +1954,26 @@ jQuery(async () => { | ||
| 1655 | 1954 | } |
| 1656 | 1955 | |
| 1657 | 1956 | const isEnable = !!$(this).prop('checked'); |
| 1658 | 1957 | const avatarcharacter = characters[this_chid].avatar; |
| 1659 | 1958 | |
| 1660 | 1959 | if (isEnable) { |
| 1661 | - if (!extension_settings.character_allowed_regex.includes(avatar)) { | |
| 1960 | + allowScopedScripts(character); | |
| 1662 | - extension_settings.character_allowed_regex.push(avatar); | |
| 1663 | - } | |
| 1664 | 1961 | } else { |
| 1665 | - const index = extension_settings.character_allowed_regex.indexOf(avatar); | |
| 1962 | + disallowScopedScripts(character); | |
| 1666 | - if (index !== -1) { | |
| 1963 | + } | |
| 1667 | - extension_settings.character_allowed_regex.splice(index, 1); | |
| 1964 | + | |
| 1668 | - } | |
| 1965 | + saveSettingsDebounced(); | |
| 1966 | + reloadCurrentChat(); | |
| 1967 | + }); | |
| 1968 | + | |
| 1969 | + $('#regex_preset_toggle').on('input', function () { | |
| 1970 | + const isEnable = !!$(this).prop('checked'); | |
| 1971 | + const name = getCurrentPresetName(); | |
| 1972 | + | |
| 1973 | + if (isEnable) { | |
| 1974 | + allowPresetScripts(getCurrentPresetAPI(), name); | |
| 1975 | + } else { | |
| 1976 | + disallowPresetScripts(getCurrentPresetAPI(), name); | |
| 1669 | 1977 | } |
| 1670 | 1978 | |
| 1671 | 1979 | saveSettingsDebounced(); |
| @@ -1676,12 +1984,58 @@ jQuery(async () => { | ||
| 1676 | 1984 | // @ts-ignore |
| 1677 | 1985 | $('#saved_regex_scripts').sortable('enable'); |
| 1678 | 1986 | |
| 1987 | + /** | |
| 1988 | + * @typedef {object} ScriptDecorators | |
| 1989 | + * @property {string} typename | |
| 1990 | + * @property {import('../../slash-commands/SlashCommandEnumValue.js').EnumType} color | |
| 1991 | + * @property {string} icon | |
| 1992 | + */ | |
| 1993 | + | |
| 1994 | + /** | |
| 1995 | + * @param {SCRIPT_TYPES} type The script type | |
| 1996 | + * @returns {ScriptDecorators} The decorators for the script type | |
| 1997 | + */ | |
| 1998 | + function getScriptDecorators(type) { | |
| 1999 | + switch (type) { | |
| 2000 | + case SCRIPT_TYPES.GLOBAL: | |
| 2001 | + return { | |
| 2002 | + typename: 'global', | |
| 2003 | + color: enumTypes.enum, | |
| 2004 | + icon: 'G', | |
| 2005 | + }; | |
| 2006 | + case SCRIPT_TYPES.SCOPED: | |
| 2007 | + return { | |
| 2008 | + typename: 'scoped', | |
| 2009 | + color: enumTypes.name, | |
| 2010 | + icon: 'S', | |
| 2011 | + }; | |
| 2012 | + case SCRIPT_TYPES.PRESET: | |
| 2013 | + return { | |
| 2014 | + typename: 'preset', | |
| 2015 | + color: enumTypes.name, | |
| 2016 | + icon: 'P', | |
| 2017 | + }; | |
| 2018 | + default: | |
| 2019 | + return { | |
| 2020 | + typename: 'Unknown', | |
| 2021 | + color: enumTypes.variable, | |
| 2022 | + icon: 'Unknown', | |
| 2023 | + }; | |
| 2024 | + } | |
| 2025 | + } | |
| 2026 | + | |
| 1679 | 2027 | const localEnumProviders = { |
| 1680 | 2028 | regexScripts: () => getRegexScripts().map(script => { |
| 1681 | - const isGlobal = extension_settings.regex?.some(x => x.scriptName === script.scriptName); | |
| 2029 | + getRegexScripts().map(script => { | |
| 1682 | - return new SlashCommandEnumValue(script.scriptName, `${enumIcons.getStateIcon(!script.disabled)} [${isGlobal ? 'global' : 'scoped'}] ${script.findRegex}`, | |
| 2030 | + const type = getScriptType(script); | |
| 1683 | - isGlobal ? enumTypes.enum : enumTypes.name, isGlobal ? 'G' : 'S'); | |
| 2031 | + const { typename, color, icon } = getScriptDecorators(type); | |
| 1684 | - }), | |
| 2032 | + return new SlashCommandEnumValue( | |
| 2033 | + script.scriptName, | |
| 2034 | + `${enumIcons.getStateIcon(!script.disabled)} [${typename}] ${script.findRegex}`, | |
| 2035 | + color, | |
| 2036 | + icon, | |
| 2037 | + ); | |
| 2038 | + }), | |
| 1685 | 2039 | }; |
| 1686 | 2040 | |
| 1687 | 2041 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| @@ -1750,8 +2104,12 @@ jQuery(async () => { | ||
| 1750 | 2104 | `, |
| 1751 | 2105 | })); |
| 1752 | 2106 | |
| 1753 | 2107 | eventSource.on(event_types.CHAT_CHANGEDMAIN_API_CHANGED, checkEmbeddedRegexScriptsonMainApiChanged); |
| 2108 | + eventSource.on(event_types.CHAT_CHANGED, checkCharEmbeddedRegexScripts); | |
| 1754 | 2109 | eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts); |
| 2110 | + eventSource.on(event_types.PRESET_RENAMED_BEFORE, onPresetRenamed); | |
| 2111 | + eventSource.on(event_types.PRESET_CHANGED, checkPresetEmbeddedRegexScripts); | |
| 2112 | + eventSource.on(event_types.PRESET_DELETED, purgePresetEmbeddedRegexScripts); | |
| 1755 | 2113 | |
| 1756 | 2114 | presetManager.setupEventListeners(); |
| 1757 | 2115 | presetManager.registerSlashCommands(); |
| @@ -0,0 +1,5 @@ | ||
| 1 | +<div> | |
| 2 | + <h3 data-i18n="This preset has embedded regex script(s).">This preset has embedded regex script(s).</h3> | |
| 3 | + <h3 data-i18n="Would you like to allow using them?">Would you like to allow using them?</h3> | |
| 4 | + <div class="m-b-1" data-i18n="If you want to do it later, select 'Regex' from the extensions menu.">If you want to do it later, select "Regex" from the extensions menu.</div> | |
| 5 | +</div> | |
| @@ -1,25 +1,34 @@ | ||
| 1 | 1 | <div class="regex-script-label flex-container flexnowrap"> |
| 2 | 2 | <input type="checkbox" class="regex_bulk_checkbox" /> |
| 3 | 3 | <span class="drag-handle menu-handle">☰</span> |
| 4 | 4 | <div class="regex_script_name flexGrowflex1 overflow-hidden"></div> |
| 5 | 5 | <div class="flex-container flexnowrap"> |
| 6 | 6 | <label class="checkbox flex-container margin-r5" for="regex_disable"> |
| 7 | 7 | <input type="checkbox" name="regex_disable" class="disable_regex" /> |
| 8 | 8 | <span class="regex-toggle-on fa-solid fa-toggle-on" data-i18n="[title]ext_regex_disable_script" title="Disable script"></span> |
| 9 | 9 | <span class="regex-toggle-off fa-solid fa-toggle-off" data-i18n="[title]ext_regex_enable_script" title="Enable script"></span> |
| 10 | 10 | </label> |
| 11 | + <label class="menu_button regex_script_expand" title="Show more options" data-i18n="[title]Show more options"> | |
| 12 | + <input type="checkbox" name="regex_expand" /> | |
| 13 | + <span class="fa-solid fa-ellipsis"></span> | |
| 14 | + </label> | |
| 15 | + <div class="flex-container regex_script_buttons"> | |
| 16 | + <div class="move_to_global menu_button" data-i18n="[title]ext_regex_move_to_global" title="Move to global scripts"> | |
| 17 | + <i class="fa-solid fa-globe"></i> | |
| 18 | + </div> | |
| 19 | + <div class="move_to_scoped menu_button" data-i18n="[title]ext_regex_move_to_scoped" title="Move to scoped scripts"> | |
| 20 | + <i class="fa-solid fa-address-card"></i> | |
| 21 | + </div> | |
| 22 | + <div class="move_to_preset menu_button" data-i18n="[title]ext_regex_move_to_preset" title="Move to preset scripts"> | |
| 23 | + <i class="fa-solid fa-sliders"></i> | |
| 24 | + </div> | |
| 25 | + <div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script"> | |
| 26 | + <i class="fa-solid fa-file-export"></i> | |
| 27 | + </div> | |
| 28 | + </div> | |
| 11 | 29 | <div class="edit_existing_regex menu_button" data-i18n="[title]ext_regex_edit_script" title="Edit script"> |
| 12 | 30 | <i class="fa-solid fa-pencil"></i> |
| 13 | 31 | </div> |
| 14 | - <div class="move_to_global menu_button" data-i18n="[title]ext_regex_move_to_global" title="Move to global scripts"> | |
| 15 | - <i class="fa-solid fa-arrow-up"></i> | |
| 16 | - </div> | |
| 17 | - <div class="move_to_scoped menu_button" data-i18n="[title]ext_regex_move_to_scoped" title="Move to scoped scripts"> | |
| 18 | - <i class="fa-solid fa-arrow-down"></i> | |
| 19 | - </div> | |
| 20 | - <div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script"> | |
| 21 | - <i class="fa-solid fa-file-export"></i> | |
| 22 | - </div> | |
| 23 | 32 | <div class="delete_regex menu_button" data-i18n="[title]ext_regex_delete_script" title="Delete script"> |
| 24 | 33 | <i class="fa-solid fa-trash"></i> |
| 25 | 34 | </div> |
| @@ -24,39 +24,33 @@ | ||
| 24 | 24 | text-align: center; |
| 25 | 25 | } |
| 26 | 26 | |
| 27 | 27 | #scoped_scripts_block {, |
| 28 | +#preset_scripts_block { | |
| 28 | 29 | opacity: 1; |
| 29 | 30 | transition: opacity var(--animation-duration-2x) ease-in-out; |
| 30 | 31 | } |
| 31 | 32 | |
| 32 | 33 | #scoped_scripts_block .move_to_scoped {, |
| 34 | +#global_scripts_block .move_to_global, | |
| 35 | +#preset_scripts_block .move_to_preset { | |
| 33 | 36 | display: none; |
| 34 | 37 | } |
| 35 | 38 | |
| 36 | -#global_scripts_block .move_to_global { | |
| 39 | +#scoped_scripts_block:not(:has(#regex_scoped_toggle:checked)), | |
| 37 | - display: none; | |
| 40 | +#preset_scripts_block:not(:has(#regex_preset_toggle:checked)) { | |
| 38 | -} | |
| 39 | - | |
| 40 | -#scoped_scripts_block:not(:has(#regex_scoped_toggle:checked)) { | |
| 41 | 41 | opacity: 0.5; |
| 42 | 42 | } |
| 43 | 43 | |
| 44 | 44 | .enable_scoped:checked~.regex-toggle-on {, |
| 45 | +.enable_scoped:not(:checked)~.regex-toggle-off { | |
| 45 | 46 | display: block; |
| 46 | 47 | } |
| 47 | 48 | |
| 48 | 49 | .enable_scoped:checked~.regex-toggle-off {, |
| 49 | - display: none; | |
| 50 | -} | |
| 51 | - | |
| 52 | 50 | .enable_scoped:not(:checked)~.regex-toggle-on { |
| 53 | 51 | display: none; |
| 54 | 52 | } |
| 55 | 53 | |
| 56 | -.enable_scoped:not(:checked)~.regex-toggle-off { | |
| 57 | - display: block; | |
| 58 | -} | |
| 59 | - | |
| 60 | 54 | .regex-script-label { |
| 61 | 55 | align-items: baseline; |
| 62 | 56 | border: 1px solid var(--SmartThemeBorderColor); |
| @@ -92,19 +86,13 @@ input.enable_scoped { | ||
| 92 | 86 | cursor: pointer; |
| 93 | 87 | } |
| 94 | 88 | |
| 95 | 89 | .disable_regex:checked~.regex-toggle-off {on, |
| 96 | - display: block; | |
| 97 | -} | |
| 98 | - | |
| 99 | -.disable_regex:checked~.regex-toggle-on { | |
| 100 | - display: none; | |
| 101 | -} | |
| 102 | - | |
| 103 | 90 | .disable_regex:not(:checked)~.regex-toggle-off { |
| 104 | 91 | display: none; |
| 105 | 92 | } |
| 106 | 93 | |
| 107 | 94 | .disable_regex:not(:checked)~.regex-toggle-on {, |
| 95 | +.disable_regex:checked~.regex-toggle-off { | |
| 108 | 96 | display: block; |
| 109 | 97 | } |
| 110 | 98 | |
| @@ -139,7 +127,8 @@ input.enable_scoped { | ||
| 139 | 127 | } |
| 140 | 128 | |
| 141 | 129 | .regex_settings .regex_bulk_operations, |
| 142 | 130 | .regex_settings .regex_bulk_checkbox {, |
| 131 | +.regex_settings .regex_bulk_operations_hr { | |
| 143 | 132 | display: none; |
| 144 | 133 | } |
| 145 | 134 | |
| @@ -147,6 +136,26 @@ input.enable_scoped { | ||
| 147 | 136 | display: flex; |
| 148 | 137 | } |
| 149 | 138 | |
| 139 | +.regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_operations_hr { | |
| 140 | + display: block; | |
| 141 | +} | |
| 142 | + | |
| 150 | 143 | .regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_checkbox { |
| 151 | 144 | display: inline-grid; |
| 152 | 145 | } |
| 146 | + | |
| 147 | +@supports not selector(:has(*)) { | |
| 148 | + .regex-script-label label.regex_script_expand { | |
| 149 | + display: none; | |
| 150 | + } | |
| 151 | + | |
| 152 | + .regex-script-label .regex_script_buttons { | |
| 153 | + display: flex; | |
| 154 | + } | |
| 155 | +} | |
| 156 | + | |
| 157 | +.regex-script-label label.regex_script_expand input[name="regex_expand"], | |
| 158 | +.regex-script-label:has(input[name="regex_expand"]:checked) label.regex_script_expand, | |
| 159 | +.regex-script-label:not(:has(input[name="regex_expand"]:checked)) .regex_script_buttons { | |
| 160 | + display: none; | |
| 161 | +} | |
| @@ -410,6 +410,7 @@ export class ConnectionManagerRequestService { | ||
| 410 | 410 | custom_url: profile['api-url'], |
| 411 | 411 | reverse_proxy: proxyPreset?.url, |
| 412 | 412 | proxy_password: proxyPreset?.password, |
| 413 | + custom_prompt_post_processing: profile['prompt-post-processing'], | |
| 413 | 414 | ...overridePayload, |
| 414 | 415 | }, { |
| 415 | 416 | presetName: includePreset ? profile.preset : undefined, |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | <div id="sd_dropdown"> |
| 2 | 2 | <ul class="list-group"> |
| 3 | 3 | <span data-i18n="Send me a picture of:">Send me a picture of:</span> |
| 4 | 4 | <li class="list-group-item" id="sd_you" data-value="you" data-i18n="sd_Yourself">Yourself</li> |
| 5 | 5 | <li class="list-group-item" id="sd_face" data-value="face" data-i18n="sd_Your_Face">Your Face</li> |
| 6 | 6 | <li class="list-group-item" id="sd_me" data-value="me" data-i18n="sd_Me">Me</li> |
| @@ -1705,7 +1705,7 @@ async function loadModels() { | ||
| 1705 | 1705 | models = await loadStabilityModels(); |
| 1706 | 1706 | break; |
| 1707 | 1707 | case sources.huggingface: |
| 1708 | 1708 | models = [{ value: '', text: 't`<Enter Model ID above>'` }]; |
| 1709 | 1709 | break; |
| 1710 | 1710 | case sources.electronhub: |
| 1711 | 1711 | models = await loadElectronHubModels(); |
| @@ -90,15 +90,15 @@ | ||
| 90 | 90 | <i><b data-i18n="Important:">Important:</b></i><i data-i18n="sd_drawthings_auth_txt"> run DrawThings app with HTTP API switch enabled in the UI! The server must be accessible from the SillyTavern host machine.</i> |
| 91 | 91 | </div> |
| 92 | 92 | <div data-sd-source="huggingface"> |
| 93 | 93 | <i data-i18n="Hint: Save an API key in the Hugging Face (Text Completion) API settings to use it here.">Hint: Save an API key in the Hugging Face (Text Completion) API settings to use it here.</i> |
| 94 | 94 | <label for="sd_huggingface_model_id" data-i18n="Model ID">Model ID</label> |
| 95 | 95 | <input id="sd_huggingface_model_id" type="text" class="text_pole" data-i18n="[placeholder]e.g. black-forest-labs/FLUX.1-dev" placeholder="e.g. black-forest-labs/FLUX.1-dev" value="" /> |
| 96 | 96 | </div> |
| 97 | 97 | <div data-sd-source="electronhub"> |
| 98 | 98 | <i data-i18n="Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.">Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.</i> |
| 99 | 99 | </div> |
| 100 | 100 | <div data-sd-source="nanogpt"> |
| 101 | 101 | <i data-i18n="Hint: Save an API key in the NanoGPT (Chat Completion) API settings to use it here.">Hint: Save an API key in the NanoGPT (Chat Completion) API settings to use it here.</i> |
| 102 | 102 | </div> |
| 103 | 103 | <div data-sd-source="vlad"> |
| 104 | 104 | <label for="sd_vlad_url">SD.Next API URL</label> |
| @@ -143,7 +143,7 @@ | ||
| 143 | 143 | View my Anlas |
| 144 | 144 | </div> |
| 145 | 145 | </div> |
| 146 | 146 | <i data-i18n="Hint: Save an API key in the NovelAI API settings to use it here.">Hint: Save an API key in the NovelAI API settings to use it here.</i> |
| 147 | 147 | </div> |
| 148 | 148 | <div data-sd-source="aimlapi"> |
| 149 | 149 | <div class="flex-container flexnowrap alignItemsBaseline marginBot5"> |
| @@ -204,7 +204,7 @@ | ||
| 204 | 204 | <a href="https://pollinations.ai">Pollinations.ai</a> |
| 205 | 205 | </p> |
| 206 | 206 | <div class="flex-container"> |
| 207 | 207 | <label class="flex1 checkbox_label" for="sd_pollinations_enhance" data-i18n="[title]Enables prompt enhancing (passes prompts through an LLM to add detail)." title="Enables prompt enhancing (passes prompts through an LLM to add detail)."> |
| 208 | 208 | <input id="sd_pollinations_enhance" type="checkbox" /> |
| 209 | 209 | <span data-i18n="Enhance"> |
| 210 | 210 | Enhance |
| @@ -297,7 +297,7 @@ | ||
| 297 | 297 | </div> |
| 298 | 298 | </div> |
| 299 | 299 | <div class="flex-container"> |
| 300 | 300 | <label class="flex1 checkbox_label" for="sd_google_enhance" data-i18n="[title]Enables prompt enhancing (passes prompts through an LLM to add detail)." title="Enables prompt enhancing (passes prompts through an LLM to add detail)."> |
| 301 | 301 | <input id="sd_google_enhance" type="checkbox" /> |
| 302 | 302 | <span data-i18n="Enhance"> |
| 303 | 303 | Enhance |
| @@ -1,3 +1,8 @@ | ||
| 1 | +.minimax_tts_settings>.tts_block { | |
| 2 | + gap: 5px; | |
| 3 | + margin: 5px 0; | |
| 4 | +} | |
| 5 | + | |
| 1 | 6 | .minimax-custom-item { |
| 2 | 7 | display: flex; |
| 3 | 8 | justify-content: space-between; |
| @@ -0,0 +1,455 @@ | ||
| 1 | +import { event_types, eventSource, getRequestHeaders } from '../../../script.js'; | |
| 2 | +import { SECRET_KEYS, secret_state } from '../../secrets.js'; | |
| 3 | +import { getPreviewString, saveTtsProviderSettings, initVoiceMap } from './index.js'; | |
| 4 | + | |
| 5 | +export { ElectronHubTtsProvider }; | |
| 6 | + | |
| 7 | +class ElectronHubTtsProvider { | |
| 8 | + settings; | |
| 9 | + voices = []; | |
| 10 | + models = []; | |
| 11 | + separator = ' . '; | |
| 12 | + audioElement = document.createElement('audio'); | |
| 13 | + | |
| 14 | + defaultSettings = { | |
| 15 | + voiceMap: {}, | |
| 16 | + model: 'tts-1', | |
| 17 | + speed: 1, | |
| 18 | + temperature: 1, | |
| 19 | + top_p: 1, | |
| 20 | + // GPT-4o Mini TTS | |
| 21 | + instructions: '', | |
| 22 | + // Dia | |
| 23 | + speaker_transcript: '', | |
| 24 | + cfg_filter_top_k: 25, | |
| 25 | + cfg_scale: 3, | |
| 26 | + // Microsoft TTS | |
| 27 | + speech_rate: 0, | |
| 28 | + pitch_adjustment: 0, | |
| 29 | + emotional_style: '', | |
| 30 | + }; | |
| 31 | + | |
| 32 | + get settingsHtml() { | |
| 33 | + let html = ` | |
| 34 | + <div>Electron Hub unified TTS API.</div> | |
| 35 | + <div class="flex-container alignItemsCenter"> | |
| 36 | + <div class="flex1"></div> | |
| 37 | + <div id="electronhub_tts_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_electronhub"> | |
| 38 | + <i class="fa-solid fa-key"></i> | |
| 39 | + <span>API Key</span> | |
| 40 | + </div> | |
| 41 | + </div> | |
| 42 | + <div class="flex-container flexGap10 wrap"> | |
| 43 | + <div class="flex1"> | |
| 44 | + <label for="electronhub_tts_model">Model</label> | |
| 45 | + <select id="electronhub_tts_model" class="text_pole"></select> | |
| 46 | + </div> | |
| 47 | + <div> | |
| 48 | + <label for="electronhub_tts_speed">Speed <span id="electronhub_tts_speed_output"></span></label> | |
| 49 | + <input type="range" id="electronhub_tts_speed" value="1" min="0.25" max="4" step="0.05"> | |
| 50 | + </div> | |
| 51 | + <div> | |
| 52 | + <label for="electronhub_tts_temperature">Temperature</label> | |
| 53 | + <input id="electronhub_tts_temperature" class="text_pole" type="number" min="0" max="2" step="0.1" value="1" /> | |
| 54 | + </div> | |
| 55 | + <div id="electronhub_block_top_p" style="display:none;"> | |
| 56 | + <label for="electronhub_tts_top_p">Top-p</label> | |
| 57 | + <input id="electronhub_tts_top_p" class="text_pole" type="number" min="0" max="1" step="0.01" value="1" /> | |
| 58 | + </div> | |
| 59 | + </div> | |
| 60 | + | |
| 61 | + <div id="electronhub_block_instructions" style="display:none;"> | |
| 62 | + <label for="electronhub_tts_instructions">Instructions (GPT-4o Mini TTS):</label> | |
| 63 | + <textarea id="electronhub_tts_instructions" class="textarea_compact autoSetHeight" placeholder="e.g., 'Speak cheerfully and energetically'"></textarea> | |
| 64 | + </div> | |
| 65 | + | |
| 66 | + <div id="electronhub_block_dia" style="display:none;"> | |
| 67 | + <label for="electronhub_tts_speaker_transcript">Speaker transcript (Dia):</label> | |
| 68 | + <textarea id="electronhub_tts_speaker_transcript" class="textarea_compact autoSetHeight" maxlength="1000"></textarea> | |
| 69 | + <label for="electronhub_tts_cfg_scale">CFG scale (1-5):</label> | |
| 70 | + <input id="electronhub_tts_cfg_scale" type="number" min="1" max="5" step="1" /> | |
| 71 | + <label for="electronhub_tts_cfg_topk">CFG filter top_k (15-50):</label> | |
| 72 | + <input id="electronhub_tts_cfg_topk" type="number" min="15" max="50" step="1" /> | |
| 73 | + </div> | |
| 74 | + | |
| 75 | + <div id="electronhub_block_msft" style="display:none;"> | |
| 76 | + <div class="flex-container flexGap10 wrap"> | |
| 77 | + <div> | |
| 78 | + <label for="electronhub_tts_speech_rate">Speech rate (-100..100)</label> | |
| 79 | + <input id="electronhub_tts_speech_rate" class="text_pole" type="number" min="-100" max="100" step="1" style="width:120px;" /> | |
| 80 | + </div> | |
| 81 | + <div> | |
| 82 | + <label for="electronhub_tts_pitch_adjustment">Pitch adjustment (-100..100)</label> | |
| 83 | + <input id="electronhub_tts_pitch_adjustment" class="text_pole" type="number" min="-100" max="100" step="1" style="width:120px;" /> | |
| 84 | + </div> | |
| 85 | + </div> | |
| 86 | + <div class="flex-container flexGap10"> | |
| 87 | + <div class="flex1"> | |
| 88 | + <label for="electronhub_tts_emotional_style">Emotional style</label> | |
| 89 | + <input id="electronhub_tts_emotional_style" class="text_pole" type="text" placeholder="cheerful, sad, angry, gentle..." /> | |
| 90 | + </div> | |
| 91 | + </div> | |
| 92 | + </div> | |
| 93 | + | |
| 94 | + <div id="electronhub_dynamic_params" class="flex-container flexGap10 wrap" style="display:none;"></div>`; | |
| 95 | + return html; | |
| 96 | + } | |
| 97 | + | |
| 98 | + constructor() { | |
| 99 | + this.handler = async function (/** @type {string} */ key) { | |
| 100 | + if (key !== SECRET_KEYS.ELECTRONHUB) return; | |
| 101 | + $('#electronhub_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.ELECTRONHUB]); | |
| 102 | + await this.onRefreshClick(); | |
| 103 | + }.bind(this); | |
| 104 | + } | |
| 105 | + | |
| 106 | + dispose() { | |
| 107 | + [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => { | |
| 108 | + eventSource.removeListener(event, this.handler); | |
| 109 | + }); | |
| 110 | + } | |
| 111 | + | |
| 112 | + async loadSettings(settings) { | |
| 113 | + if (Object.keys(settings).length == 0) { | |
| 114 | + console.info('Using default Electron Hub TTS settings'); | |
| 115 | + } | |
| 116 | + | |
| 117 | + this.settings = { ...this.defaultSettings, ...settings }; | |
| 118 | + | |
| 119 | + await this.loadModels(); | |
| 120 | + this.populateModelSelect(); | |
| 121 | + | |
| 122 | + $('#electronhub_tts_model').val(this.settings.model); | |
| 123 | + $('#electronhub_tts_model').on('change', () => { this.onSettingsChange(); }); | |
| 124 | + | |
| 125 | + $('#electronhub_tts_speed').val(this.settings.speed); | |
| 126 | + $('#electronhub_tts_speed_output').text(this.settings.speed); | |
| 127 | + $('#electronhub_tts_speed').on('input', () => { this.onSettingsChange(); }); | |
| 128 | + | |
| 129 | + $('#electronhub_tts_temperature').val(this.settings.temperature); | |
| 130 | + $('#electronhub_tts_temperature').on('input', () => { this.onSettingsChange(); }); | |
| 131 | + | |
| 132 | + $('#electronhub_tts_top_p').val(this.settings.top_p); | |
| 133 | + $('#electronhub_tts_top_p').on('input', () => { this.onSettingsChange(); }); | |
| 134 | + | |
| 135 | + $('#electronhub_tts_instructions').val(this.settings.instructions); | |
| 136 | + $('#electronhub_tts_instructions').on('input', () => { this.onSettingsChange(); }); | |
| 137 | + | |
| 138 | + $('#electronhub_tts_speaker_transcript').val(this.settings.speaker_transcript); | |
| 139 | + $('#electronhub_tts_speaker_transcript').on('input', () => { this.onSettingsChange(); }); | |
| 140 | + $('#electronhub_tts_cfg_scale').val(this.settings.cfg_scale); | |
| 141 | + $('#electronhub_tts_cfg_scale').on('input', () => { this.onSettingsChange(); }); | |
| 142 | + $('#electronhub_tts_cfg_topk').val(this.settings.cfg_filter_top_k); | |
| 143 | + $('#electronhub_tts_cfg_topk').on('input', () => { this.onSettingsChange(); }); | |
| 144 | + | |
| 145 | + $('#electronhub_tts_speech_rate').val(this.settings.speech_rate); | |
| 146 | + $('#electronhub_tts_speech_rate').on('input', () => { this.onSettingsChange(); }); | |
| 147 | + $('#electronhub_tts_pitch_adjustment').val(this.settings.pitch_adjustment); | |
| 148 | + $('#electronhub_tts_pitch_adjustment').on('input', () => { this.onSettingsChange(); }); | |
| 149 | + $('#electronhub_tts_emotional_style').val(this.settings.emotional_style); | |
| 150 | + $('#electronhub_tts_emotional_style').on('input', () => { this.onSettingsChange(); }); | |
| 151 | + | |
| 152 | + $('#electronhub_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.ELECTRONHUB]); | |
| 153 | + [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => { | |
| 154 | + eventSource.on(event, this.handler); | |
| 155 | + }); | |
| 156 | + | |
| 157 | + await this.checkReady(); | |
| 158 | + this.updateConditionalBlocks(); | |
| 159 | + this.renderDynamicParams(); | |
| 160 | + console.debug('Electron Hub TTS: Settings loaded'); | |
| 161 | + } | |
| 162 | + | |
| 163 | + async onSettingsChange() { | |
| 164 | + const previousModel = this.settings.model; | |
| 165 | + this.settings.model = String($('#electronhub_tts_model').find(':selected').val() || this.settings.model); | |
| 166 | + this.settings.speed = Number($('#electronhub_tts_speed').val()); | |
| 167 | + $('#electronhub_tts_speed_output').text(this.settings.speed); | |
| 168 | + this.settings.temperature = Number($('#electronhub_tts_temperature').val()); | |
| 169 | + this.settings.top_p = Number($('#electronhub_tts_top_p').val()); | |
| 170 | + this.settings.instructions = String($('#electronhub_tts_instructions').val() || ''); | |
| 171 | + this.settings.speaker_transcript = String($('#electronhub_tts_speaker_transcript').val() || ''); | |
| 172 | + this.settings.cfg_scale = Number($('#electronhub_tts_cfg_scale').val()); | |
| 173 | + this.settings.cfg_filter_top_k = Number($('#electronhub_tts_cfg_topk').val()); | |
| 174 | + this.settings.speech_rate = Number($('#electronhub_tts_speech_rate').val()); | |
| 175 | + this.settings.pitch_adjustment = Number($('#electronhub_tts_pitch_adjustment').val()); | |
| 176 | + this.settings.emotional_style = String($('#electronhub_tts_emotional_style').val() || ''); | |
| 177 | + this.updateConditionalBlocks(); | |
| 178 | + this.renderDynamicParams(); | |
| 179 | + saveTtsProviderSettings(); | |
| 180 | + if (previousModel !== this.settings.model) { | |
| 181 | + this.voices = await this.fetchTtsVoiceObjects(); | |
| 182 | + await initVoiceMap(); | |
| 183 | + } | |
| 184 | + } | |
| 185 | + | |
| 186 | + async loadModels() { | |
| 187 | + try { | |
| 188 | + const response = await fetch('/api/openai/electronhub/models', { | |
| 189 | + method: 'POST', | |
| 190 | + headers: getRequestHeaders(), | |
| 191 | + }); | |
| 192 | + if (!response.ok) { | |
| 193 | + throw new Error(`HTTP ${response.status}: ${await response.text()}`); | |
| 194 | + } | |
| 195 | + /** @type {Array<any>} */ | |
| 196 | + const data = await response.json(); | |
| 197 | + const allModels = Array.isArray(data) ? data : []; | |
| 198 | + const ttsModels = allModels.filter(m => { | |
| 199 | + const eps = Array.isArray(m?.endpoints) ? m.endpoints : []; | |
| 200 | + return eps.some(ep => { | |
| 201 | + if (typeof ep !== 'string') return false; | |
| 202 | + return ep === '/v1/audio/speech' || ep.endsWith('/audio/speech') || ep === 'audio/speech'; | |
| 203 | + }); | |
| 204 | + }); | |
| 205 | + | |
| 206 | + this.models = ttsModels; | |
| 207 | + | |
| 208 | + if (this.models.length > 0 && !this.models.find(m => m.id === this.settings.model)) { | |
| 209 | + this.settings.model = this.models[0].id; | |
| 210 | + saveTtsProviderSettings(); | |
| 211 | + } | |
| 212 | + } catch (err) { | |
| 213 | + console.warn('Electron Hub models fetch failed', err); | |
| 214 | + this.models = []; | |
| 215 | + } | |
| 216 | + } | |
| 217 | + | |
| 218 | + populateModelSelect() { | |
| 219 | + const select = $('#electronhub_tts_model'); | |
| 220 | + select.empty(); | |
| 221 | + const groups = this.groupByVendor(this.models); | |
| 222 | + for (const [vendor, models] of groups.entries()) { | |
| 223 | + const optgroup = document.createElement('optgroup'); | |
| 224 | + optgroup.label = vendor; | |
| 225 | + for (const m of models) { | |
| 226 | + const opt = document.createElement('option'); | |
| 227 | + opt.value = m.id; | |
| 228 | + opt.text = m.name || m.id; | |
| 229 | + optgroup.appendChild(opt); | |
| 230 | + } | |
| 231 | + select.append(optgroup); | |
| 232 | + } | |
| 233 | + | |
| 234 | + if (this.models.find(x => x.id === this.settings.model)) { | |
| 235 | + select.val(this.settings.model); | |
| 236 | + } | |
| 237 | + } | |
| 238 | + | |
| 239 | + /** | |
| 240 | + * Group models by vendor prefix from name before ':' | |
| 241 | + * @param {Array<any>} array | |
| 242 | + * @returns {Map<string, any[]>} | |
| 243 | + */ | |
| 244 | + groupByVendor(array) { | |
| 245 | + return array.reduce((acc, curr) => { | |
| 246 | + const name = String(curr?.name || curr?.id || 'Other'); | |
| 247 | + const vendor = name.split(':')[0].trim() || 'Other'; | |
| 248 | + if (!acc.has(vendor)) acc.set(vendor, []); | |
| 249 | + acc.get(vendor).push(curr); | |
| 250 | + return acc; | |
| 251 | + }, new Map()); | |
| 252 | + } | |
| 253 | + | |
| 254 | + updateConditionalBlocks() { | |
| 255 | + const modelId = this.settings.model; | |
| 256 | + const model = this.models.find(m => m.id === modelId); | |
| 257 | + const params = model?.parameters || {}; | |
| 258 | + const vendorName = String(model?.name || '').split(':')[0].trim().toLowerCase(); | |
| 259 | + | |
| 260 | + const hasInstructions = 'instructions' in params || modelId === 'gpt-4o-mini-tts'; | |
| 261 | + const hasDia = 'speaker_transcript' in params || 'cfg_scale' in params || 'cfg_filter_top_k' in params || modelId.includes('dia'); | |
| 262 | + | |
| 263 | + const hasMsft = 'speech_rate' in params || 'pitch_adjustment' in params || 'emotional_style' in params || vendorName === 'microsoft' || modelId === 'microsoft-tts'; | |
| 264 | + const hasTopP = 'top_p' in params; | |
| 265 | + | |
| 266 | + $('#electronhub_block_instructions').toggle(!!hasInstructions); | |
| 267 | + $('#electronhub_block_dia').toggle(!!hasDia); | |
| 268 | + $('#electronhub_block_msft').toggle(!!hasMsft); | |
| 269 | + $('#electronhub_block_top_p').toggle(!!hasTopP); | |
| 270 | + } | |
| 271 | + | |
| 272 | + /** | |
| 273 | + * Build UI for additional model parameters dynamically | |
| 274 | + */ | |
| 275 | + renderDynamicParams() { | |
| 276 | + const container = $('#electronhub_dynamic_params'); | |
| 277 | + container.empty(); | |
| 278 | + const model = this.models.find(m => m.id === this.settings.model); | |
| 279 | + const params = model?.parameters || {}; | |
| 280 | + const modelHasVoices = Array.isArray(model?.voices) && model.voices.length > 0; | |
| 281 | + const exclude = new Set(['input', 'response_format', 'model', 'speed', 'temperature', 'top_p', 'instructions', 'speaker_transcript', 'cfg_scale', 'cfg_filter_top_k', 'speech_rate', 'pitch_adjustment', 'emotional_style']); | |
| 282 | + if (modelHasVoices) exclude.add('voice'); | |
| 283 | + | |
| 284 | + const entries = Object.entries(params).filter(([k]) => !exclude.has(k)); | |
| 285 | + container.toggle(entries.length > 0); | |
| 286 | + if (entries.length === 0) return; | |
| 287 | + | |
| 288 | + for (const [key, spec] of entries) { | |
| 289 | + const nice = key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); | |
| 290 | + const type = String(spec?.type || 'string'); | |
| 291 | + const id = `electronhub_dyn_${key.replace(/[^a-zA-Z0-9_-]/g, '_')}`; | |
| 292 | + | |
| 293 | + if (Array.isArray(spec?.enum) && spec.enum.length) { | |
| 294 | + const select = $(`<div><label for="${id}">${nice}</label><select id="${id}" class="text_pole"></select></div>`); | |
| 295 | + container.append(select); | |
| 296 | + const el = select.find('select'); | |
| 297 | + for (const opt of spec.enum) el.append(new Option(String(opt), String(opt))); | |
| 298 | + const val = this.settings[key] ?? spec.default ?? spec.enum[0]; | |
| 299 | + el.val(String(val)); | |
| 300 | + el.on('change', () => { this.settings[key] = String(el.val() || ''); saveTtsProviderSettings(); }); | |
| 301 | + continue; | |
| 302 | + } | |
| 303 | + | |
| 304 | + if (type === 'boolean') { | |
| 305 | + const block = $(`<label class="checkbox_label" for="${id}"><input type="checkbox" id="${id}"> <small>${nice}</small></label>`); | |
| 306 | + container.append(block); | |
| 307 | + const el = block.find('input'); | |
| 308 | + el.prop('checked', !!(this.settings[key] ?? spec.default ?? false)); | |
| 309 | + el.on('change', () => { this.settings[key] = !!el.is(':checked'); saveTtsProviderSettings(); }); | |
| 310 | + continue; | |
| 311 | + } | |
| 312 | + | |
| 313 | + if (type === 'number' || type === 'integer') { | |
| 314 | + const min = spec.minimum ?? undefined; | |
| 315 | + const max = spec.maximum ?? undefined; | |
| 316 | + const step = type === 'integer' ? 1 : (spec.step ?? 0.01); | |
| 317 | + const block = $(`<div><label for="${id}">${nice}${(min != null || max != null) ? ` (${min ?? ''}..${max ?? ''})` : ''}:</label><input id="${id}" type="number" class="text_pole" ${min != null ? `min="${min}"` : ''} ${max != null ? `max="${max}"` : ''} step="${step}"></div>`); | |
| 318 | + container.append(block); | |
| 319 | + const el = block.find('input'); | |
| 320 | + const val = this.settings[key] ?? spec.default ?? ''; | |
| 321 | + if (val !== '') el.val(val); | |
| 322 | + el.on('input', () => { | |
| 323 | + const raw = el.val(); | |
| 324 | + this.settings[key] = (raw === '') ? '' : Number(raw); | |
| 325 | + saveTtsProviderSettings(); | |
| 326 | + }); | |
| 327 | + continue; | |
| 328 | + } | |
| 329 | + | |
| 330 | + const isLong = /instructions|transcript|style|prompt|description/i.test(key); | |
| 331 | + if (isLong) { | |
| 332 | + const block = $(`<div><label for="${id}">${nice}</label><textarea id="${id}" class="textarea_compact autoSetHeight"></textarea></div>`); | |
| 333 | + container.append(block); | |
| 334 | + const el = block.find('textarea'); | |
| 335 | + el.val(String(this.settings[key] ?? spec.default ?? '')); | |
| 336 | + el.on('input', () => { this.settings[key] = String(el.val() || ''); saveTtsProviderSettings(); }); | |
| 337 | + } else { | |
| 338 | + const block = $(`<div><label for="${id}">${nice}</label><input id="${id}" type="text" class="text_pole" /></div>`); | |
| 339 | + container.append(block); | |
| 340 | + const el = block.find('input'); | |
| 341 | + el.val(String(this.settings[key] ?? spec.default ?? '')); | |
| 342 | + el.on('input', () => { this.settings[key] = String(el.val() || ''); saveTtsProviderSettings(); }); | |
| 343 | + } | |
| 344 | + } | |
| 345 | + } | |
| 346 | + | |
| 347 | + async checkReady() { | |
| 348 | + this.voices = await this.fetchTtsVoiceObjects(); | |
| 349 | + } | |
| 350 | + | |
| 351 | + async onRefreshClick() { | |
| 352 | + await this.loadModels(); | |
| 353 | + this.populateModelSelect(); | |
| 354 | + this.voices = await this.fetchTtsVoiceObjects(); | |
| 355 | + this.updateConditionalBlocks(); | |
| 356 | + this.renderDynamicParams(); | |
| 357 | + saveTtsProviderSettings(); | |
| 358 | + } | |
| 359 | + | |
| 360 | + async getVoice(voiceName) { | |
| 361 | + if (this.voices.length == 0) { | |
| 362 | + this.voices = await this.fetchTtsVoiceObjects(); | |
| 363 | + } | |
| 364 | + const match = this.voices.filter(v => v.name == voiceName)[0]; | |
| 365 | + if (!match) { | |
| 366 | + throw `TTS Voice name ${voiceName} not found`; | |
| 367 | + } | |
| 368 | + return match; | |
| 369 | + } | |
| 370 | + | |
| 371 | + async generateTts(text, voiceId) { | |
| 372 | + const response = await this.fetchTtsGeneration(text, voiceId); | |
| 373 | + return response; | |
| 374 | + } | |
| 375 | + | |
| 376 | + async fetchTtsVoiceObjects() { | |
| 377 | + const modelId = this.settings.model; | |
| 378 | + const model = this.models.find(m => m.id === modelId); | |
| 379 | + if (model && Array.isArray(model.voices) && model.voices.length) { | |
| 380 | + return model.voices.map(name => ({ name, voice_id: name, lang: 'en-US' })); | |
| 381 | + } | |
| 382 | + // Fallback to common OpenAI voices | |
| 383 | + const fallback = ['alloy', 'ash', 'ballad', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer', 'verse']; | |
| 384 | + return fallback.map(name => ({ name, voice_id: name, lang: 'en-US' })); | |
| 385 | + } | |
| 386 | + | |
| 387 | + async previewTtsVoice(voiceId) { | |
| 388 | + this.audioElement.pause(); | |
| 389 | + this.audioElement.currentTime = 0; | |
| 390 | + const text = getPreviewString('en-US'); | |
| 391 | + const response = await this.fetchTtsGeneration(text, voiceId); | |
| 392 | + if (!response.ok) { | |
| 393 | + throw new Error(`HTTP ${response.status}`); | |
| 394 | + } | |
| 395 | + const audio = await response.blob(); | |
| 396 | + const url = URL.createObjectURL(audio); | |
| 397 | + this.audioElement.src = url; | |
| 398 | + this.audioElement.play(); | |
| 399 | + this.audioElement.onended = () => URL.revokeObjectURL(url); | |
| 400 | + } | |
| 401 | + | |
| 402 | + async fetchTtsGeneration(inputText, voiceId) { | |
| 403 | + console.info(`Generating Electron Hub TTS for voice_id ${voiceId}`); | |
| 404 | + const body = { | |
| 405 | + input: inputText, | |
| 406 | + voice: voiceId, | |
| 407 | + speed: this.settings.speed, | |
| 408 | + temperature: this.settings.temperature, | |
| 409 | + model: this.settings.model, | |
| 410 | + }; | |
| 411 | + | |
| 412 | + const model = (this.settings.model || '').toLowerCase(); | |
| 413 | + if (model === 'gpt-4o-mini-tts') { | |
| 414 | + if (this.settings.instructions?.trim()) body.instructions = this.settings.instructions.trim(); | |
| 415 | + } | |
| 416 | + if (model.includes('dia')) { | |
| 417 | + if (this.settings.speaker_transcript?.trim()) body.speaker_transcript = this.settings.speaker_transcript.trim(); | |
| 418 | + if (Number.isFinite(this.settings.cfg_scale)) body.cfg_scale = Number(this.settings.cfg_scale); | |
| 419 | + if (Number.isFinite(this.settings.cfg_filter_top_k)) body.cfg_filter_top_k = Number(this.settings.cfg_filter_top_k); | |
| 420 | + } | |
| 421 | + if (model.includes('microsoft-tts')) { | |
| 422 | + if (Number.isFinite(this.settings.speech_rate)) body.speech_rate = Number(this.settings.speech_rate); | |
| 423 | + if (Number.isFinite(this.settings.pitch_adjustment)) body.pitch_adjustment = Number(this.settings.pitch_adjustment); | |
| 424 | + if ((this.settings.emotional_style || '').trim()) body.emotional_style = String(this.settings.emotional_style).trim(); | |
| 425 | + } | |
| 426 | + if (Number.isFinite(this.settings.top_p)) { | |
| 427 | + body.top_p = Number(this.settings.top_p); | |
| 428 | + } | |
| 429 | + | |
| 430 | + // add dynamic params based on schema | |
| 431 | + const modelObj = this.models.find(m => m.id === this.settings.model); | |
| 432 | + const params = modelObj?.parameters || {}; | |
| 433 | + const modelHasVoices = Array.isArray(modelObj?.voices) && modelObj.voices.length > 0; | |
| 434 | + const exclude = new Set(['input', 'response_format', 'model', 'speed', 'temperature', 'top_p', 'instructions', 'speaker_transcript', 'cfg_scale', 'cfg_filter_top_k', 'speech_rate', 'pitch_adjustment', 'emotional_style']); | |
| 435 | + if (modelHasVoices) exclude.add('voice'); | |
| 436 | + for (const key of Object.keys(params)) { | |
| 437 | + if (exclude.has(key)) continue; | |
| 438 | + const val = this.settings[key]; | |
| 439 | + if (val === undefined || val === '') continue; | |
| 440 | + body[key] = val; | |
| 441 | + } | |
| 442 | + | |
| 443 | + const response = await fetch('/api/openai/electronhub/generate-voice', { | |
| 444 | + method: 'POST', | |
| 445 | + headers: getRequestHeaders(), | |
| 446 | + body: JSON.stringify(body), | |
| 447 | + }); | |
| 448 | + | |
| 449 | + if (!response.ok) { | |
| 450 | + throw new Error(`HTTP ${response.status}: ${await response.text()}`); | |
| 451 | + } | |
| 452 | + | |
| 453 | + return response; | |
| 454 | + } | |
| 455 | +} | |
| @@ -92,10 +92,9 @@ class GptSovitsV2Provider { | ||
| 92 | 92 | } |
| 93 | 93 | |
| 94 | 94 | // Set initial values from the settings |
| 95 | 95 | $('#tts_endpoint').val(this.settings.provider_endpoint).on('change', this.onSettingsChange.bind(this)); |
| 96 | 96 | $('#text_lang').val(this.settings.text_lang).on('change', this.onSettingsChange.bind(this)); |
| 97 | 97 | $('#prompt_lang').val(this.settings.prompt_lang).on('change', this.onSettingsChange.bind(this)); |
| 98 | - | |
| 99 | 98 | |
| 100 | 99 | await this.checkReady(); |
| 101 | 100 | |
| @@ -108,7 +107,7 @@ class GptSovitsV2Provider { | ||
| 108 | 107 | } |
| 109 | 108 | |
| 110 | 109 | async onRefreshClick() { |
| 111 | - return; | |
| 110 | + return await this.checkReady(); | |
| 112 | 111 | } |
| 113 | 112 | |
| 114 | 113 | //#################// |
| @@ -33,6 +33,7 @@ import { KokoroTtsProvider } from './kokoro.js'; | ||
| 33 | 33 | import { TtsWebuiProvider } from './tts-webui.js'; |
| 34 | 34 | import { PollinationsTtsProvider } from './pollinations.js'; |
| 35 | 35 | import { MiniMaxTtsProvider } from './minimax.js'; |
| 36 | +import { ElectronHubTtsProvider } from './electronhub.js'; | |
| 36 | 37 | |
| 37 | 38 | const UPDATE_INTERVAL = 1000; |
| 38 | 39 | const wrapper = new ModuleWorkerWrapper(moduleWorker); |
| @@ -123,6 +124,7 @@ const ttsProviders = { | ||
| 123 | 124 | 'CosyVoice (Unofficial)': CosyVoiceProvider, |
| 124 | 125 | Edge: EdgeTtsProvider, |
| 125 | 126 | ElevenLabs: ElevenLabsTtsProvider, |
| 127 | + 'Electron Hub': ElectronHubTtsProvider, | |
| 126 | 128 | 'Google Translate': GoogleTranslateTtsProvider, |
| 127 | 129 | 'Google Gemini TTS': GoogleNativeTtsProvider, |
| 128 | 130 | GSVI: GSVITtsProvider, |
| @@ -19,9 +19,9 @@ class MiniMaxTtsProvider { | ||
| 19 | 19 | apiHost: 'https://api.minimax.io', |
| 20 | 20 | model: 'speech-02-hd', |
| 21 | 21 | voiceMap: {}, |
| 22 | - speed: 1.0, | |
| 22 | + speed: { default: 1.0, min: 0.5, max: 2.0, step: 0.1 }, | |
| 23 | - volume: 1.0, | |
| 23 | + volume: { default: 1.0, min: 0.0, max: 10.0, step: 0.1 }, | |
| 24 | - pitch: 1.0, | |
| 24 | + pitch: { default: 0, min: -12, max: 12, step: 1 }, | |
| 25 | 25 | audioSampleRate: 32000, |
| 26 | 26 | bitrate: 128000, |
| 27 | 27 | format: 'mp3', |
| @@ -84,15 +84,15 @@ class MiniMaxTtsProvider { | ||
| 84 | 84 | |
| 85 | 85 | <div class="tts_block"> |
| 86 | 86 | <label for="minimax_tts_speed">Speed: <span id="minimax_tts_speed_output"></span></label> |
| 87 | 87 | <input id="minimax_tts_speed" type="range" value="${this.defaultSettings.speed.default}" min="0${this.5defaultSettings.speed.min}" max="2${this.0defaultSettings.speed.max}" step="0${this.1defaultSettings.speed.step}" /> |
| 88 | 88 | </div> |
| 89 | 89 | <div class="tts_block"> |
| 90 | 90 | <label for="minimax_tts_volume">Volume: <span id="minimax_tts_volume_output"></span></label> |
| 91 | 91 | <input id="minimax_tts_volume" type="range" value="${this.defaultSettings.volume.default}" min="0${this.1defaultSettings.volume.min}" max="2${this.0defaultSettings.volume.max}" step="0${this.1defaultSettings.volume.step}" /> |
| 92 | 92 | </div> |
| 93 | 93 | <div class="tts_block"> |
| 94 | 94 | <label for="minimax_tts_pitch">Pitch: <span id="minimax_tts_pitch_output"></span></label> |
| 95 | 95 | <input id="minimax_tts_pitch" type="range" value="${this.defaultSettings.pitch.default}" min="0${this.5defaultSettings.pitch.min}" max="2${this.0defaultSettings.pitch.max}" step="0${this.1defaultSettings.pitch.step}" /> |
| 96 | 96 | </div> |
| 97 | 97 | <div class="tts_block"> |
| 98 | 98 | <label for="minimax_tts_format">Audio Format</label> |
| @@ -190,14 +190,14 @@ class MiniMaxTtsProvider { | ||
| 190 | 190 | this.settings.apiHost = $('#minimax_tts_api_host').val(); |
| 191 | 191 | this.settings.speed = parseFloat($('#minimax_tts_speed').val().toString()); |
| 192 | 192 | this.settings.volume = parseFloat($('#minimax_tts_volume').val().toString()); |
| 193 | 193 | this.settings.pitch = parseFloatparseInt($('#minimax_tts_pitch').val().toString()); |
| 194 | 194 | this.settings.model = $('#minimax_tts_model').find(':selected').val(); |
| 195 | 195 | this.settings.format = $('#minimax_tts_format').find(':selected').val(); |
| 196 | 196 | this.settings.customVoiceId = $('#minimax_tts_custom_voice_id').val(); |
| 197 | 197 | |
| 198 | 198 | $('#minimax_tts_speed_output').text(this.settings.speed.toFixed(1)); |
| 199 | 199 | $('#minimax_tts_volume_output').text(this.settings.volume.toFixed(1)); |
| 200 | 200 | $('#minimax_tts_pitch_output').text(this.settings.pitch.toFixed(1)); |
| 201 | 201 | |
| 202 | 202 | saveTtsProviderSettings(); |
| 203 | 203 | } |
| @@ -458,6 +458,16 @@ class MiniMaxTtsProvider { | ||
| 458 | 458 | // Only accept keys defined in defaultSettings |
| 459 | 459 | this.settings = { ...this.defaultSettings }; |
| 460 | 460 | |
| 461 | + // Flatten the settings fields with default/min/max definitions so the actual values are used | |
| 462 | + this.settings = Object.fromEntries( | |
| 463 | + Object.entries(this.defaultSettings).map(([key, value]) => { | |
| 464 | + if (value && typeof value === 'object' && 'default' in value) { | |
| 465 | + return [key, value.default]; | |
| 466 | + } | |
| 467 | + return [key, value]; | |
| 468 | + }), | |
| 469 | + ); | |
| 470 | + | |
| 461 | 471 | for (const key in settings) { |
| 462 | 472 | if (key in this.settings) { |
| 463 | 473 | this.settings[key] = settings[key]; |
| @@ -470,6 +480,21 @@ class MiniMaxTtsProvider { | ||
| 470 | 480 | if (!this.settings.customModels) this.settings.customModels = []; |
| 471 | 481 | if (!this.settings.customVoices) this.settings.customVoices = []; |
| 472 | 482 | |
| 483 | + // # Migrate settings | |
| 484 | + // Pitch value changed from float to int. If it's a float, let's try to extrapolate it to the new range | |
| 485 | + if (!Number.isInteger(this.settings.pitch)) { | |
| 486 | + const oldPitch = parseFloat(this.settings.pitch); | |
| 487 | + if (!isNaN(oldPitch)) { | |
| 488 | + // map old [0.5..1.0] to [-12..0], and [1.0..2.0] to [0..12] (old default was 1.0, new default is 0) | |
| 489 | + const newPitch = (oldPitch < 1.0) ? (oldPitch - 1.0) * 24 : (oldPitch - 1.0) * 12; | |
| 490 | + this.settings.pitch = Math.max(-12, Math.min(12, Math.round(newPitch))); | |
| 491 | + console.info(`MiniMax TTS: Migrated pitch from ${oldPitch} to ${this.settings.pitch}`); | |
| 492 | + } else { | |
| 493 | + this.settings.pitch = 0; | |
| 494 | + console.info(`MiniMax TTS: Migration reset pitch to default ${this.settings.pitch}`); | |
| 495 | + } | |
| 496 | + } | |
| 497 | + | |
| 473 | 498 | $('#minimax_tts_api_host').val(this.settings.apiHost || 'https://api.minimax.io'); |
| 474 | 499 | $('#minimax_tts_model').val(this.settings.model); |
| 475 | 500 | $('#minimax_tts_speed').val(this.settings.speed); |
| @@ -546,7 +571,7 @@ class MiniMaxTtsProvider { | ||
| 546 | 571 | |
| 547 | 572 | $('#minimax_tts_speed_output').text(this.settings.speed.toFixed(1)); |
| 548 | 573 | $('#minimax_tts_volume_output').text(this.settings.volume.toFixed(1)); |
| 549 | 574 | $('#minimax_tts_pitch_output').text(this.settings.pitch.toFixed(1)); |
| 550 | 575 | |
| 551 | 576 | // Initialize custom configuration display |
| 552 | 577 | this.updateCustomModelsDisplay(); |
| @@ -756,17 +781,20 @@ class MiniMaxTtsProvider { | ||
| 756 | 781 | throw error; |
| 757 | 782 | } |
| 758 | 783 | |
| 784 | + /** @param {number} number @param {number} lower @param {number} upper @returns {number} */ | |
| 785 | + const clamp = (number, lower, upper) => Math.min(Math.max(number, lower), upper); | |
| 786 | + | |
| 759 | 787 | const requestBody = { |
| 760 | 788 | text: inputText, |
| 761 | 789 | voiceId: voiceId, |
| 762 | 790 | apiHost: this.settings.apiHost, |
| 763 | 791 | model: this.settings.model || 'speech-02-hd'this.defaultSettings.model, |
| 764 | 792 | speed: clamp(Number(this.settings.speed) || 1this.0defaultSettings.speed.default, this.defaultSettings.speed.min, this.defaultSettings.speed.max), |
| 765 | 793 | volume: clamp(Number(this.settings.volume) || 1this.0defaultSettings.volume.default, this.defaultSettings.volume.min, this.defaultSettings.volume.max), |
| 766 | 794 | pitch: clamp(Math.round(Number(this.settings.pitch)) || 1this.0defaultSettings.pitch.default, this.defaultSettings.pitch.min, this.defaultSettings.pitch.max), |
| 767 | 795 | audioSampleRate: Number(this.settings.audioSampleRate) || 32000this.defaultSettings.audioSampleRate, |
| 768 | 796 | bitrate: Number(this.settings.bitrate) || 128000this.defaultSettings.bitrate, |
| 769 | 797 | format: this.settings.format || 'mp3'this.defaultSettings.format, |
| 770 | 798 | language: language, |
| 771 | 799 | }; |
| 772 | 800 | |
| @@ -12,7 +12,7 @@ | ||
| 12 | 12 | <div class="tts_block"> |
| 13 | 13 | <select id="tts_provider" class="flex1"> |
| 14 | 14 | </select> |
| 15 | 15 | <input id="tts_refresh" data-i18n="[value]tts_refresh" class="menu_button" type="submit" value="Reload" /> |
| 16 | 16 | </div> |
| 17 | 17 | <div> |
| 18 | 18 | <label class="checkbox_label" for="tts_enabled"> |
| @@ -88,7 +88,7 @@ | ||
| 88 | 88 | <form id="tts_provider_settings"> |
| 89 | 89 | </form> |
| 90 | 90 | <div class="tts_buttons"> |
| 91 | 91 | <input id="tts_voices" class="menu_button" data-i18n="[value]Available voices" type="submit" value="Available voices" /> |
| 92 | 92 | </div> |
| 93 | 93 | </div> |
| 94 | 94 | </div> |
| @@ -2,6 +2,7 @@ import { isMobile } from '../../RossAscends-mods.js'; | ||
| 2 | 2 | import { getPreviewString } from './index.js'; |
| 3 | 3 | import { saveTtsProviderSettings } from './index.js'; |
| 4 | 4 | export { SystemTtsProvider }; |
| 5 | +import { t } from '../../i18n.js'; | |
| 5 | 6 | |
| 6 | 7 | /** |
| 7 | 8 | * Chunkify |
| @@ -96,13 +97,13 @@ class SystemTtsProvider { | ||
| 96 | 97 | |
| 97 | 98 | get settingsHtml() { |
| 98 | 99 | if (!('speechSynthesis' in window)) { |
| 99 | 100 | return 't`Your browser or operating system doesn\'t support speech synthesis'`; |
| 100 | 101 | } |
| 101 | 102 | |
| 102 | 103 | return `'<p>' + t`Uses the voices provided by your operating system` + `</p> |
| 103 | 104 | <label for="system_tts_rate">` + t`Rate:` + ` <span id="system_tts_rate_output"></span></label> |
| 104 | 105 | <input id="system_tts_rate" type="range" value="${this.defaultSettings.rate}" min="0.1" max="2" step="0.01" /> |
| 105 | 106 | <label for="system_tts_pitch">` + t`Pitch:` + ` <span id="system_tts_pitch_output"></span></label> |
| 106 | 107 | <input id="system_tts_pitch" type="range" value="${this.defaultSettings.pitch}" min="0" max="2" step="0.01" />`; |
| 107 | 108 | } |
| 108 | 109 | |
| @@ -66,7 +66,7 @@ const settings = { | ||
| 66 | 66 | ollama_keep: false, |
| 67 | 67 | vllm_model: '', |
| 68 | 68 | webllm_model: '', |
| 69 | 69 | google_model: 'text-embedding-004005', |
| 70 | 70 | summarize: false, |
| 71 | 71 | summarize_sent: false, |
| 72 | 72 | summary_source: 'main', |
| @@ -140,6 +140,7 @@ | ||
| 140 | 140 | <option value="gemini-embedding-001">gemini-embedding-001</option> |
| 141 | 141 | <option value="gemini-embedding-exp-03-07">gemini-embedding-exp-03-07</option> |
| 142 | 142 | <option value="text-embedding-004">text-embedding-004</option> |
| 143 | + <option value="text-embedding-005">text-embedding-005</option> | |
| 143 | 144 | <option value="embedding-001">embedding-001</option> |
| 144 | 145 | </select> |
| 145 | 146 | </div> |
| @@ -76,6 +76,7 @@ import { | ||
| 76 | 76 | depth_prompt_role_default, |
| 77 | 77 | shouldAutoContinue, |
| 78 | 78 | unshallowCharacter, |
| 79 | + chatElement, | |
| 79 | 80 | } from '../script.js'; |
| 80 | 81 | import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js'; |
| 81 | 82 | import { FILTER_TYPES, FilterHelper } from './filters.js'; |
| @@ -236,37 +237,37 @@ export async function getGroupChat(groupId, reload = false) { | ||
| 236 | 237 | const chat_id = group.chat_id; |
| 237 | 238 | const data = await loadGroupChat(chat_id); |
| 238 | 239 | const metadata = group.chat_metadata ?? {}; |
| 239 | 240 | letconst freshChat = false!metadata.tainted; |
| 240 | 241 | |
| 241 | 242 | await loadItemizedPrompts(getCurrentChatId()); |
| 242 | 243 | |
| 243 | 244 | if (group && Array.isArray(datagroup.members) && data.lengthfreshChat) { |
| 244 | - data[0].is_group = true; | |
| 245 | + chat.splice(0, chat.length); | |
| 245 | - chat.splice(0, chat.length, ...data); | |
| 246 | + chatElement.find('.mes').remove(); | |
| 246 | - await printMessages(); | |
| 247 | + for (let member of group.members) { | |
| 247 | - } else { | |
| 248 | + const character = characters.find(x => x.avatar === member || x.name === member); | |
| 248 | - freshChat = !metadata.tainted; | |
| 249 | + if (!character) { | |
| 249 | - if (group && Array.isArray(group.members) && freshChat) { | |
| 250 | + continue; | |
| 250 | - for (let member of group.members) { | |
| 251 | + } | |
| 251 | - const character = characters.find(x => x.avatar === member || x.name === member); | |
| 252 | - if (!character) { | |
| 253 | - continue; | |
| 254 | - } | |
| 255 | - | |
| 256 | - const mes = await getFirstCharacterMessage(character); | |
| 257 | 252 | |
| 258 | - // No first message | |
| 253 | + const mes = await getFirstCharacterMessage(character); | |
| 259 | - if (!(mes?.mes)) { | |
| 260 | - continue; | |
| 261 | - } | |
| 262 | 254 | |
| 263 | - chat.push(mes); | |
| 255 | + // No first message | |
| 264 | - await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1), 'first_message'); | |
| 256 | + if (!(mes?.mes)) { | |
| 265 | - addOneMessage(mes); | |
| 257 | + continue; | |
| 266 | - await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1), 'first_message'); | |
| 267 | 258 | } |
| 268 | - await saveGroupChat(groupId, false); | |
| 259 | + | |
| 260 | + chat.push(mes); | |
| 261 | + await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1), 'first_message'); | |
| 262 | + addOneMessage(mes); | |
| 263 | + await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1), 'first_message'); | |
| 269 | 264 | } |
| 265 | + await saveGroupChat(groupId, false); | |
| 266 | + } else if (Array.isArray(data) && data.length) { | |
| 267 | + data[0].is_group = true; | |
| 268 | + chat.splice(0, chat.length, ...data); | |
| 269 | + chatElement.find('.mes').remove(); | |
| 270 | + await printMessages(); | |
| 270 | 271 | } |
| 271 | 272 | |
| 272 | 273 | updateChatMetadata(metadata, true); |
| @@ -2018,7 +2019,14 @@ export async function deleteGroupChatByName(groupId, chatName) { | ||
| 2018 | 2019 | await eventSource.emit(event_types.GROUP_CHAT_DELETED, chatName); |
| 2019 | 2020 | } |
| 2020 | 2021 | |
| 2021 | -export async function deleteGroupChat(groupId, chatId) { | |
| 2022 | +/** | |
| 2023 | + * Deletes a group chat by name. | |
| 2024 | + * @param {string} groupId The ID of the group containing the chat to delete. | |
| 2025 | + * @param {string} chatId The id/name of the chat to delete. | |
| 2026 | + * @param {object} [options={}] Options for the deletion. | |
| 2027 | + * @param {boolean} [options.jumpToNewChat=true] Whether to jump to a new chat after deletion (existing one, or create a new one if none exists) | |
| 2028 | + */ | |
| 2029 | +export async function deleteGroupChat(groupId, chatId, { jumpToNewChat = true } = {}) { | |
| 2022 | 2030 | const group = groups.find(x => x.id === groupId); |
| 2023 | 2031 | |
| 2024 | 2032 | if (!group || !group.chats.includes(chatId)) { |
| @@ -2026,10 +2034,13 @@ export async function deleteGroupChat(groupId, chatId) { | ||
| 2026 | 2034 | } |
| 2027 | 2035 | |
| 2028 | 2036 | group.chats.splice(group.chats.indexOf(chatId), 1); |
| 2029 | - group.chat_metadata = {}; | |
| 2030 | - group.chat_id = ''; | |
| 2031 | 2037 | delete group.past_metadata[chatId]; |
| 2032 | - updateChatMetadata(group.chat_metadata, true); | |
| 2038 | + | |
| 2039 | + if (group.chat_id === chatId) { | |
| 2040 | + group.chat_id = ''; | |
| 2041 | + group.chat_metadata = {}; | |
| 2042 | + updateChatMetadata(group.chat_metadata, true); | |
| 2043 | + } | |
| 2033 | 2044 | |
| 2034 | 2045 | const response = await fetch('/api/chats/group/delete', { |
| 2035 | 2046 | method: 'POST', |
| @@ -2038,10 +2049,12 @@ export async function deleteGroupChat(groupId, chatId) { | ||
| 2038 | 2049 | }); |
| 2039 | 2050 | |
| 2040 | 2051 | if (response.ok) { |
| 2041 | 2052 | if (group.chats.lengthjumpToNewChat) { |
| 2042 | 2053 | awaitif openGroupChat(groupId, group.chats[group.chats.length - 1]); { |
| 2043 | - } else { | |
| 2054 | + await openGroupChat(groupId, group.chats[group.chats.length - 1]); | |
| 2044 | - await createNewGroupChat(groupId); | |
| 2055 | + } else { | |
| 2056 | + await createNewGroupChat(groupId); | |
| 2057 | + } | |
| 2045 | 2058 | } |
| 2046 | 2059 | |
| 2047 | 2060 | await eventSource.emit(event_types.GROUP_CHAT_DELETED, chatId); |
| @@ -2051,9 +2064,11 @@ export async function deleteGroupChat(groupId, chatId) { | ||
| 2051 | 2064 | /** |
| 2052 | 2065 | * Imports a group chat from a file and adds it to the group. |
| 2053 | 2066 | * @param {FormData} formData Form data to send to the server |
| 2054 | 2067 | * @param {EventTargetobject} eventTarget Element[options={}] thatOptions triggeredfor the import |
| 2068 | + * @param {boolean} [options.refresh] Whether to refresh the group chat list after import | |
| 2069 | + * @returns {Promise<string[]>} List of imported file names | |
| 2055 | 2070 | */ |
| 2056 | 2071 | export async function importGroupChat(formData, eventTarget{ refresh = true } = {}) { |
| 2057 | 2072 | const fetchResult = await fetch('/api/chats/group/import', { |
| 2058 | 2073 | method: 'POST', |
| 2059 | 2074 | headers: getRequestHeaders({ omitContentType: true }), |
| @@ -2070,14 +2085,16 @@ export async function importGroupChat(formData, eventTarget) { | ||
| 2070 | 2085 | if (group) { |
| 2071 | 2086 | group.chats.push(chatId); |
| 2072 | 2087 | await editGroup(selected_group, true, true); |
| 2073 | 2088 | awaitif displayPastChats(refresh); { |
| 2089 | + await displayPastChats(); | |
| 2090 | + } | |
| 2074 | 2091 | } |
| 2075 | 2092 | } |
| 2076 | - } | |
| 2077 | 2093 | |
| 2078 | - if (eventTarget instanceof HTMLInputElement) { | |
| 2094 | + return data?.fileNames || []; | |
| 2079 | - eventTarget.value = ''; | |
| 2080 | 2095 | } |
| 2096 | + | |
| 2097 | + return []; | |
| 2081 | 2098 | } |
| 2082 | 2099 | |
| 2083 | 2100 | export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) { |
| @@ -2147,7 +2164,7 @@ function doCurMemberListPopout() { | ||
| 2147 | 2164 | // Remove pagination from popout |
| 2148 | 2165 | newElement.find('.group_pagination').empty(); |
| 2149 | 2166 | |
| 2150 | 2167 | $('body#movingDivs').append(newElement); |
| 2151 | 2168 | loadMovingUIState(); |
| 2152 | 2169 | $('#groupMemberListPopout').fadeIn(animation_duration); |
| 2153 | 2170 | dragElement(newElement); |
| @@ -1,10 +1,11 @@ | ||
| 1 | 1 | import { Handlebars, moment, seedrandom, droll } from '../lib.js'; |
| 2 | 2 | import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams, eventSource, event_types, extension_prompts } from '../script.js'; |
| 3 | 3 | import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } from './utils.js'; |
| 4 | 4 | import { textgenerationwebui_banned_in_macros } from './textgen-settings.js'; |
| 5 | 5 | import { getInstructMacros } from './instruct-mode.js'; |
| 6 | 6 | import { getVariableMacros } from './variables.js'; |
| 7 | 7 | import { isMobile } from './RossAscends-mods.js'; |
| 8 | +import { inject_ids } from './constants.js'; | |
| 8 | 9 | |
| 9 | 10 | /** |
| 10 | 11 | * @typedef Macro |
| @@ -56,6 +57,24 @@ export class MacrosParser { | ||
| 56 | 57 | }; |
| 57 | 58 | |
| 58 | 59 | /** |
| 60 | + * Access a macro by its name. | |
| 61 | + * @param {string} key Macro name (key) | |
| 62 | + * @returns {string|MacroFunction|undefined} The macro value | |
| 63 | + */ | |
| 64 | + static get(key) { | |
| 65 | + return MacrosParser.#macros.get(key); | |
| 66 | + } | |
| 67 | + | |
| 68 | + /** | |
| 69 | + * Checks if a macro is registered. | |
| 70 | + * @param {string} key Macro name (key) | |
| 71 | + * @returns {boolean} True if the macro is registered, false otherwise | |
| 72 | + */ | |
| 73 | + static has(key) { | |
| 74 | + return MacrosParser.#macros.has(key); | |
| 75 | + } | |
| 76 | + | |
| 77 | + /** | |
| 59 | 78 | * Registers a global macro that can be used anywhere where substitution is allowed. |
| 60 | 79 | * @param {string} key Macro name (key) |
| 61 | 80 | * @param {string|MacroFunction} value A string or a function that returns a string |
| @@ -459,6 +478,16 @@ function getTimeDiffMacro() { | ||
| 459 | 478 | } |
| 460 | 479 | |
| 461 | 480 | /** |
| 481 | + * Returns the outlet prompt for a given outlet key. | |
| 482 | + * @param {string} key - The outlet key | |
| 483 | + * @returns {string} The outlet prompt | |
| 484 | + */ | |
| 485 | +function getOutletPrompt(key) { | |
| 486 | + const value = extension_prompts[inject_ids.CUSTOM_WI_OUTLET(key)]?.value; | |
| 487 | + return value || ''; | |
| 488 | +} | |
| 489 | + | |
| 490 | +/** | |
| 462 | 491 | * Substitutes {{macro}} parameters in a string. |
| 463 | 492 | * @param {string} content - The string to substitute parameters in. |
| 464 | 493 | * @param {EnvObject} env - Map of macro names to the values they'll be substituted with. If the param |
| @@ -518,6 +547,7 @@ export function evaluateMacros(content, env, postProcessFn) { | ||
| 518 | 547 | { regex: /{{datetimeformat +([^}]*)}}/gi, replace: (_, format) => moment().format(format) }, |
| 519 | 548 | { regex: /{{idle_duration}}/gi, replace: () => getTimeSinceLastMessage() }, |
| 520 | 549 | { regex: /{{time_UTC([-+]\d+)}}/gi, replace: (_, offset) => moment().utc().utcOffset(parseInt(offset, 10)).format('LT') }, |
| 550 | + { regex: /{{outlet::(.+?)}}/gi, replace: (_, key) => getOutletPrompt(key.trim()) || '' }, | |
| 521 | 551 | getTimeDiffMacro(), |
| 522 | 552 | getBannedWordsMacro(), |
| 523 | 553 | getRandomReplaceMacro(), |
| @@ -276,6 +276,8 @@ export const settingsToUpdate = { | ||
| 276 | 276 | perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true], |
| 277 | 277 | groq_model: ['#model_groq_select', 'groq_model', false, true], |
| 278 | 278 | electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true], |
| 279 | + electronhub_sort_models: ['#electronhub_sort_models', 'electronhub_sort_models', false, true], | |
| 280 | + electronhub_group_models: ['#electronhub_group_models', 'electronhub_group_models', false, true], | |
| 279 | 281 | nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true], |
| 280 | 282 | deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true], |
| 281 | 283 | aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true], |
| @@ -370,15 +372,17 @@ const default_settings = { | ||
| 370 | 372 | scenario_format: default_scenario_format, |
| 371 | 373 | personality_format: default_personality_format, |
| 372 | 374 | openai_model: 'gpt-4-turbo', |
| 373 | 375 | claude_model: 'claude-3-5-sonnet-202406204-5', |
| 374 | 376 | google_model: 'gemini-12.5-pro', |
| 375 | 377 | vertexai_model: 'gemini-2.0-flash5-001pro', |
| 376 | 378 | ai21_model: 'jamba-large', |
| 377 | 379 | mistralai_model: 'mistral-large-latest', |
| 378 | 380 | cohere_model: 'command-r-plus', |
| 379 | 381 | perplexity_model: 'sonar-pro', |
| 380 | 382 | groq_model: 'llama-3.3-70b-versatile', |
| 381 | 383 | electronhub_model: 'gpt-4o-mini', |
| 384 | + electronhub_sort_models: 'alphabetically', | |
| 385 | + electronhub_group_models: false, | |
| 382 | 386 | nanogpt_model: 'gpt-4o-mini', |
| 383 | 387 | deepseek_model: 'deepseek-chat', |
| 384 | 388 | aimlapi_model: 'gpt-4o-mini-2024-07-18', |
| @@ -464,15 +468,17 @@ const oai_settings = { | ||
| 464 | 468 | scenario_format: default_scenario_format, |
| 465 | 469 | personality_format: default_personality_format, |
| 466 | 470 | openai_model: 'gpt-4-turbo', |
| 467 | 471 | claude_model: 'claude-3-5-sonnet-202406204-5', |
| 468 | 472 | google_model: 'gemini-12.5-pro', |
| 469 | 473 | vertexai_model: 'gemini-2.0-flash5-001pro', |
| 470 | 474 | ai21_model: 'jamba-large', |
| 471 | 475 | mistralai_model: 'mistral-large-latest', |
| 472 | 476 | cohere_model: 'command-r-plus', |
| 473 | 477 | perplexity_model: 'sonar-pro', |
| 474 | 478 | groq_model: 'llama-3.1-70b-versatile', |
| 475 | 479 | electronhub_model: 'gpt-4o-mini', |
| 480 | + electronhub_sort_models: 'alphabetically', | |
| 481 | + electronhub_group_models: false, | |
| 476 | 482 | nanogpt_model: 'gpt-4o-mini', |
| 477 | 483 | deepseek_model: 'deepseek-chat', |
| 478 | 484 | aimlapi_model: 'gpt-4-turbo', |
| @@ -1710,6 +1716,59 @@ function calculateOpenRouterCost() { | ||
| 1710 | 1716 | $('#openrouter_max_prompt_cost').text(cost); |
| 1711 | 1717 | } |
| 1712 | 1718 | |
| 1719 | +function getElectronHubModelTemplate(option) { | |
| 1720 | + const model = model_list.find(x => x.id === option?.element?.value); | |
| 1721 | + | |
| 1722 | + if (!option.id || !model) { | |
| 1723 | + return option.text; | |
| 1724 | + } | |
| 1725 | + | |
| 1726 | + const inputPrice = model.pricing?.input; | |
| 1727 | + const outputPrice = model.pricing?.output; | |
| 1728 | + const price = inputPrice && outputPrice ? `$${inputPrice}/$${outputPrice} in/out Mtoken` : 'Unknown'; | |
| 1729 | + | |
| 1730 | + const visionIcon = model.metadata?.vision ? '<i class="fa-solid fa-eye fa-sm" title="This model supports vision"></i>' : ''; | |
| 1731 | + const reasoningIcon = model.metadata?.reasoning ? '<i class="fa-solid fa-brain fa-sm" title="This model supports reasoning"></i>' : ''; | |
| 1732 | + const toolCallsIcon = model.metadata?.function_call ? '<i class="fa-solid fa-wrench fa-sm" title="This model supports function tools"></i>' : ''; | |
| 1733 | + const premiumIcon = model?.premium_model ? '<i class="fa-solid fa-crown fa-sm" title="This model requires a subscription"></i>' : ''; | |
| 1734 | + | |
| 1735 | + const iconsContainer = document.createElement('span'); | |
| 1736 | + iconsContainer.insertAdjacentHTML('beforeend', visionIcon); | |
| 1737 | + iconsContainer.insertAdjacentHTML('beforeend', reasoningIcon); | |
| 1738 | + iconsContainer.insertAdjacentHTML('beforeend', toolCallsIcon); | |
| 1739 | + iconsContainer.insertAdjacentHTML('beforeend', premiumIcon); | |
| 1740 | + | |
| 1741 | + const capabilities = (iconsContainer.children.length) ? ` | ${iconsContainer.innerHTML}` : ''; | |
| 1742 | + | |
| 1743 | + return $((` | |
| 1744 | + <div class="flex-container alignItemsBaseline" title="${DOMPurify.sanitize(model.id)}"> | |
| 1745 | + <strong>${DOMPurify.sanitize(model.name)}</strong> | ${model.tokens} ctx | <small>${price}</small>${capabilities} | |
| 1746 | + </div> | |
| 1747 | + `)); | |
| 1748 | +} | |
| 1749 | + | |
| 1750 | +function calculateElectronHubCost() { | |
| 1751 | + if (oai_settings.chat_completion_source !== chat_completion_sources.ELECTRONHUB) { | |
| 1752 | + return; | |
| 1753 | + } | |
| 1754 | + | |
| 1755 | + let cost = 'Unknown'; | |
| 1756 | + const model = model_list.find(x => x.id === oai_settings.electronhub_model); | |
| 1757 | + | |
| 1758 | + if (model?.pricing) { | |
| 1759 | + const outputCost = Number(model.pricing.output / 1000000); | |
| 1760 | + const inputCost = Number(model.pricing.input / 1000000); | |
| 1761 | + const outputTokens = oai_settings.openai_max_tokens; | |
| 1762 | + const inputTokens = (oai_settings.openai_max_context - outputTokens); | |
| 1763 | + const totalCost = (outputCost * outputTokens) + (inputCost * inputTokens); | |
| 1764 | + if (!isNaN(totalCost)) { | |
| 1765 | + cost = '$' + totalCost.toFixed(4); | |
| 1766 | + } | |
| 1767 | + } | |
| 1768 | + | |
| 1769 | + $('#electronhub_max_prompt_cost').text(cost); | |
| 1770 | +} | |
| 1771 | + | |
| 1713 | 1772 | function saveModelList(data) { |
| 1714 | 1773 | model_list = data.map((model) => ({ ...model })); |
| 1715 | 1774 | model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id)); |
| @@ -1775,44 +1834,29 @@ function saveModelList(data) { | ||
| 1775 | 1834 | } |
| 1776 | 1835 | |
| 1777 | 1836 | if (oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) { |
| 1778 | - /** @type {HTMLSelectElement} */ | |
| 1837 | + $('#model_mistralai_select').empty(); | |
| 1779 | - const mistralModelSelect = document.querySelector('#model_mistralai_select'); | |
| 1780 | - if (mistralModelSelect) { | |
| 1781 | - const options = Array.from(mistralModelSelect.options); | |
| 1782 | - options.forEach((option) => { | |
| 1783 | - const existingModel = model_list.find(model => model.id === option.value); | |
| 1784 | - if (!existingModel) { | |
| 1785 | - option.remove(); | |
| 1786 | - } | |
| 1787 | - }); | |
| 1788 | - | |
| 1789 | - const otherOptionsGroup = mistralModelSelect.querySelector('#mistralai_other_models'); | |
| 1790 | - for (const model of model_list.filter(model => model?.capabilities?.completion_chat)) { | |
| 1791 | - if (!options.some(option => option.value === model.id) && otherOptionsGroup) { | |
| 1792 | - otherOptionsGroup.append(new Option(model.id, model.id)); | |
| 1793 | - } | |
| 1794 | - } | |
| 1795 | 1838 | |
| 1796 | 1839 | for (const selectedModelmodel =of model_list.findfilter(model => model?.id === oai_settingscapabilities?.mistralai_modelcompletion_chat);) { |
| 1797 | - if (!selectedModel) { | |
| 1840 | + $('#model_mistralai_select').append(new Option(model.id, model.id)); | |
| 1798 | - oai_settings.mistralai_model = model_list.find(model => model?.capabilities?.completion_chat)?.id; | |
| 1841 | + } | |
| 1799 | - } | |
| 1800 | 1842 | |
| 1801 | - $('#model_mistralai_select').val(oai_settings.mistralai_model).trigger('change'); | |
| 1843 | + const selectedModel = model_list.find(model => model.id === oai_settings.mistralai_model); | |
| 1844 | + if (!selectedModel) { | |
| 1845 | + oai_settings.mistralai_model = model_list.find(model => model?.capabilities?.completion_chat)?.id; | |
| 1802 | 1846 | } |
| 1847 | + | |
| 1848 | + $('#model_mistralai_select').val(oai_settings.mistralai_model).trigger('change'); | |
| 1803 | 1849 | } |
| 1804 | 1850 | |
| 1805 | 1851 | if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) { |
| 1852 | + model_list = model_list.filter(model => model?.endpoints?.includes('/v1/chat/completions')); | |
| 1853 | + | |
| 1854 | + model_list = electronHubSortBy(model_list, oai_settings.electronhub_sort_models); | |
| 1855 | + | |
| 1806 | 1856 | $('#model_electronhub_select').empty(); |
| 1807 | - model_list.forEach((model) => { | |
| 1857 | + | |
| 1808 | - if (model?.endpoints?.includes('/v1/chat/completions')) { | |
| 1858 | + const groupedList = oai_settings.electronhub_group_models ? electronHubGroupByVendor(model_list) : model_list; | |
| 1809 | - $('#model_electronhub_select').append( | |
| 1859 | + appendElectronHubOptions(groupedList, oai_settings.electronhub_group_models); | |
| 1810 | - $('<option>', { | |
| 1811 | - value: model.id, | |
| 1812 | - text: model.name, | |
| 1813 | - })); | |
| 1814 | - } | |
| 1815 | - }); | |
| 1816 | 1860 | |
| 1817 | 1861 | const selectedModel = model_list.find(model => model.id === oai_settings.electronhub_model); |
| 1818 | 1862 | if (model_list.length > 0 && (!selectedModel || !oai_settings.electronhub_model)) { |
| @@ -1877,7 +1921,7 @@ function saveModelList(data) { | ||
| 1877 | 1921 | } |
| 1878 | 1922 | |
| 1879 | 1923 | if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) { |
| 1880 | 1924 | // Clear only the "Other" optgroup for dynamic models |
| 1881 | 1925 | $('#google_other_models').empty(); |
| 1882 | 1926 | |
| 1883 | 1927 | // Get static model options that are already in the HTML |
| @@ -1888,7 +1932,7 @@ function saveModelList(data) { | ||
| 1888 | 1932 | |
| 1889 | 1933 | // Add dynamic models to the "Other" group |
| 1890 | 1934 | model_list.forEach((model) => { |
| 1891 | 1935 | // Only add if not already in static list |
| 1892 | 1936 | if (!staticModels.includes(model.id)) { |
| 1893 | 1937 | $('#google_other_models').append( |
| 1894 | 1938 | $('<option>', { |
| @@ -1984,6 +2028,24 @@ function saveModelList(data) { | ||
| 1984 | 2028 | .append(new Option(modelId || 'None', modelId || '', true, true)) |
| 1985 | 2029 | .trigger('change'); |
| 1986 | 2030 | } |
| 2031 | + | |
| 2032 | + if (oai_settings.chat_completion_source == chat_completion_sources.XAI) { | |
| 2033 | + $('#model_xai_select').empty(); | |
| 2034 | + model_list.forEach((model) => { | |
| 2035 | + $('#model_xai_select').append( | |
| 2036 | + $('<option>', { | |
| 2037 | + value: model.id, | |
| 2038 | + text: model.id, | |
| 2039 | + })); | |
| 2040 | + }); | |
| 2041 | + | |
| 2042 | + const selectedModel = model_list.find(model => model.id === oai_settings.xai_model); | |
| 2043 | + if (model_list.length > 0 && (!selectedModel || !oai_settings.xai_model)) { | |
| 2044 | + oai_settings.xai_model = model_list[0].id; | |
| 2045 | + } | |
| 2046 | + | |
| 2047 | + $('#model_xai_select').val(oai_settings.xai_model).trigger('change'); | |
| 2048 | + } | |
| 1987 | 2049 | } |
| 1988 | 2050 | |
| 1989 | 2051 | function appendOpenRouterOptions(model_list, groupModels = false, sort = false) { |
| @@ -2041,6 +2103,61 @@ function openRouterGroupByVendor(array) { | ||
| 2041 | 2103 | }, new Map()); |
| 2042 | 2104 | } |
| 2043 | 2105 | |
| 2106 | +function appendElectronHubOptions(model_list, groupModels = false) { | |
| 2107 | + const appendOption = (model, parent = null) => { | |
| 2108 | + (parent || $('#model_electronhub_select')).append( | |
| 2109 | + $('<option>', { | |
| 2110 | + value: model.id, | |
| 2111 | + text: model.name, | |
| 2112 | + })); | |
| 2113 | + }; | |
| 2114 | + | |
| 2115 | + if (groupModels) { | |
| 2116 | + model_list.forEach((models, vendor) => { | |
| 2117 | + const optgroup = $('<optgroup>').attr('label', vendor); | |
| 2118 | + | |
| 2119 | + models.forEach((model) => { | |
| 2120 | + appendOption(model, optgroup); | |
| 2121 | + }); | |
| 2122 | + | |
| 2123 | + $('#model_electronhub_select').append(optgroup); | |
| 2124 | + }); | |
| 2125 | + } else { | |
| 2126 | + model_list.forEach((model) => { | |
| 2127 | + appendOption(model); | |
| 2128 | + }); | |
| 2129 | + } | |
| 2130 | + | |
| 2131 | +} | |
| 2132 | + | |
| 2133 | +function electronHubSortBy(data, property = 'alphabetically') { | |
| 2134 | + return data.sort((a, b) => { | |
| 2135 | + if (property === 'context_length') { | |
| 2136 | + return b.tokens - a.tokens; | |
| 2137 | + } else if (property === 'pricing.input') { | |
| 2138 | + return parseFloat(a.pricing.input) - parseFloat(b.pricing.input); | |
| 2139 | + } else if (property === 'pricing.output') { | |
| 2140 | + return parseFloat(a.pricing.output) - parseFloat(b.pricing.output); | |
| 2141 | + } else { | |
| 2142 | + return a?.name && b?.name && a.name.localeCompare(b.name); | |
| 2143 | + } | |
| 2144 | + }); | |
| 2145 | +} | |
| 2146 | + | |
| 2147 | +function electronHubGroupByVendor(array) { | |
| 2148 | + return array.reduce((acc, curr) => { | |
| 2149 | + const vendor = String(curr?.name || curr?.id || 'Other').split(':')[0].trim() || 'Other'; | |
| 2150 | + | |
| 2151 | + if (!acc.has(vendor)) { | |
| 2152 | + acc.set(vendor, []); | |
| 2153 | + } | |
| 2154 | + | |
| 2155 | + acc.get(vendor).push(curr); | |
| 2156 | + | |
| 2157 | + return acc; | |
| 2158 | + }, new Map()); | |
| 2159 | +} | |
| 2160 | + | |
| 2044 | 2161 | function aimlapiGroupByVendor(array) { |
| 2045 | 2162 | return array.reduce((acc, curr) => { |
| 2046 | 2163 | const vendor = curr.info.developer; |
| @@ -2191,7 +2308,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } = | ||
| 2191 | 2308 | const useLogprobs = !!power_user.request_token_probabilities; |
| 2192 | 2309 | const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isAzureOpenAI || isCustom || isXAI || isAimlapi || isMoonshot); |
| 2193 | 2310 | |
| 2194 | 2311 | const logitBiasSources = [chat_completion_sources.OPENAI, chat_completion_sources.AZURE_OPENAI, chat_completion_sources.OPENROUTER, chat_completion_sources.ELECTRONHUB, chat_completion_sources.CUSTOM]; |
| 2195 | 2312 | if (oai_settings.bias_preset_selected |
| 2196 | 2313 | && logitBiasSources.includes(oai_settings.chat_completion_source) |
| 2197 | 2314 | && Array.isArray(oai_settings.bias_presets[oai_settings.bias_preset_selected]) |
| @@ -2353,19 +2470,24 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } = | ||
| 2353 | 2470 | } |
| 2354 | 2471 | |
| 2355 | 2472 | if (isXAI) { |
| 2356 | - if (generate_data.model.includes('grok-4')) { | |
| 2473 | + const model = generate_data.model; | |
| 2474 | + if (model.includes('grok-3-mini')) { | |
| 2357 | 2475 | delete generate_data.presence_penalty; |
| 2358 | 2476 | delete generate_data.frequency_penalty; |
| 2359 | 2477 | delete generate_data.stop; |
| 2478 | + } else { | |
| 2479 | + // As of 2025/09/21, only grok-3-mini accepts reasoning_effort | |
| 2360 | 2480 | delete generate_data.reasoning_effort; |
| 2361 | 2481 | } |
| 2362 | - if (generate_data.model.includes('grok-3-mini')) { | |
| 2482 | + | |
| 2483 | + if (model.includes('grok-4') || model.includes('grok-code')) { | |
| 2363 | 2484 | delete generate_data.presence_penalty; |
| 2364 | 2485 | delete generate_data.frequency_penalty; |
| 2365 | - } | |
| 2486 | + | |
| 2366 | - if (generate_data.model.includes('grok-vision')) { | |
| 2487 | + // grok-4-fast-non-reasoning accepts stop | |
| 2367 | - delete generate_data.tools; | |
| 2488 | + if (!model.includes('grok-4-fast-non-reasoning')) { | |
| 2368 | 2489 | delete generate_data.tool_choicestop; |
| 2490 | + } | |
| 2369 | 2491 | } |
| 2370 | 2492 | } |
| 2371 | 2493 | |
| @@ -2559,7 +2681,7 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov | ||
| 2559 | 2681 | state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || ''); |
| 2560 | 2682 | } |
| 2561 | 2683 | return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? ''; |
| 2562 | 2684 | } else if ([chat_completion_sources.CUSTOM, chat_completion_sources.POLLINATIONS, chat_completion_sources.AIMLAPI, chat_completion_sources.MOONSHOT, chat_completion_sources.COMETAPI, chat_completion_sources.ELECTRONHUB, chat_completion_sources.NANOGPT].includes(chat_completion_source)) { |
| 2563 | 2685 | if (show_thoughts) { |
| 2564 | 2686 | state.reasoning += |
| 2565 | 2687 | data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ?? |
| @@ -3523,6 +3645,8 @@ function loadOpenAISettings(data, settings) { | ||
| 3523 | 3645 | oai_settings.perplexity_model = settings.perplexity_model ?? default_settings.perplexity_model; |
| 3524 | 3646 | oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model; |
| 3525 | 3647 | oai_settings.electronhub_model = settings.electronhub_model ?? default_settings.electronhub_model; |
| 3648 | + oai_settings.electronhub_sort_models = settings.electronhub_sort_models ?? default_settings.electronhub_sort_models; | |
| 3649 | + oai_settings.electronhub_group_models = settings.electronhub_group_models ?? default_settings.electronhub_group_models; | |
| 3526 | 3650 | oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model; |
| 3527 | 3651 | oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model; |
| 3528 | 3652 | oai_settings.aimlapi_model = settings.aimlapi_model ?? default_settings.aimlapi_model; |
| @@ -3666,6 +3790,8 @@ function loadOpenAISettings(data, settings) { | ||
| 3666 | 3790 | $('#openrouter_allow_fallbacks').prop('checked', oai_settings.openrouter_allow_fallbacks); |
| 3667 | 3791 | $('#openrouter_providers_chat').val(oai_settings.openrouter_providers).trigger('change'); |
| 3668 | 3792 | $('#openrouter_middleout').val(oai_settings.openrouter_middleout); |
| 3793 | + $('#electronhub_sort_models').val(oai_settings.electronhub_sort_models); | |
| 3794 | + $('#electronhub_group_models').prop('checked', oai_settings.electronhub_group_models); | |
| 3669 | 3795 | $('#squash_system_messages').prop('checked', oai_settings.squash_system_messages); |
| 3670 | 3796 | $('#continue_prefill').prop('checked', oai_settings.continue_prefill); |
| 3671 | 3797 | $('#openai_function_calling').prop('checked', oai_settings.function_calling); |
| @@ -3928,6 +4054,8 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { | ||
| 3928 | 4054 | pollinations_model: settings.pollinations_model, |
| 3929 | 4055 | aimlapi_model: settings.aimlapi_model, |
| 3930 | 4056 | electronhub_model: settings.electronhub_model, |
| 4057 | + electronhub_sort_models: settings.electronhub_sort_models, | |
| 4058 | + electronhub_group_models: settings.electronhub_group_models, | |
| 3931 | 4059 | moonshot_model: settings.moonshot_model, |
| 3932 | 4060 | fireworks_model: settings.fireworks_model, |
| 3933 | 4061 | cometapi_model: settings.cometapi_model, |
| @@ -4532,75 +4660,8 @@ function getMistralMaxContext(model, isUnlocked) { | ||
| 4532 | 4660 | } |
| 4533 | 4661 | } |
| 4534 | 4662 | |
| 4535 | - const contextMap = { | |
| 4536 | - 'codestral-2405': 32768, | |
| 4537 | - 'codestral-2411-rc5': 262144, | |
| 4538 | - 'codestral-2412': 262144, | |
| 4539 | - 'codestral-2501': 262144, | |
| 4540 | - 'codestral-2508': 256000, | |
| 4541 | - 'codestral-latest': 256000, | |
| 4542 | - 'codestral-mamba-2407': 262144, | |
| 4543 | - 'codestral-mamba-latest': 262144, | |
| 4544 | - 'open-codestral-mamba': 262144, | |
| 4545 | - 'ministral-3b-2410': 131072, | |
| 4546 | - 'ministral-3b-latest': 131072, | |
| 4547 | - 'ministral-8b-2410': 131072, | |
| 4548 | - 'ministral-8b-latest': 131072, | |
| 4549 | - 'mistral-large-2407': 131072, | |
| 4550 | - 'mistral-large-2411': 131072, | |
| 4551 | - 'mistral-large-latest': 131072, | |
| 4552 | - 'mistral-large-pixtral-2411': 131072, | |
| 4553 | - 'mistral-tiny-2407': 131072, | |
| 4554 | - 'mistral-tiny-latest': 131072, | |
| 4555 | - 'open-mistral-nemo': 131072, | |
| 4556 | - 'open-mistral-nemo-2407': 131072, | |
| 4557 | - 'pixtral-12b': 131072, | |
| 4558 | - 'pixtral-12b-2409': 131072, | |
| 4559 | - 'pixtral-12b-latest': 131072, | |
| 4560 | - 'pixtral-large-2411': 131072, | |
| 4561 | - 'pixtral-large-latest': 131072, | |
| 4562 | - 'open-mixtral-8x22b': 65536, | |
| 4563 | - 'open-mixtral-8x22b-2404': 65536, | |
| 4564 | - 'mistral-embed': 32768, | |
| 4565 | - 'mistral-large-2402': 32768, | |
| 4566 | - 'mistral-medium': 131072, | |
| 4567 | - 'mistral-medium-2312': 32768, | |
| 4568 | - 'mistral-medium-2505': 131072, | |
| 4569 | - 'mistral-medium-2508': 262144, | |
| 4570 | - 'mistral-medium-latest': 262144, | |
| 4571 | - 'mistral-moderation-2411': 32768, | |
| 4572 | - 'mistral-moderation-latest': 32768, | |
| 4573 | - 'mistral-ocr-2503': 32768, | |
| 4574 | - 'mistral-ocr-latest': 32768, | |
| 4575 | - 'mistral-saba-2502': 32768, | |
| 4576 | - 'mistral-saba-latest': 32768, | |
| 4577 | - 'mistral-small': 32768, | |
| 4578 | - 'mistral-small-2312': 32768, | |
| 4579 | - 'mistral-small-2402': 32768, | |
| 4580 | - 'mistral-small-2409': 32768, | |
| 4581 | - 'mistral-small-2501': 32768, | |
| 4582 | - 'mistral-small-2503': 32768, | |
| 4583 | - 'mistral-small-2506': 131072, | |
| 4584 | - 'mistral-small-latest': 131072, | |
| 4585 | - 'mistral-tiny': 32768, | |
| 4586 | - 'mistral-tiny-2312': 32768, | |
| 4587 | - 'open-mistral-7b': 32768, | |
| 4588 | - 'open-mixtral-8x7b': 32768, | |
| 4589 | - 'devstral-small-2505': 131072, | |
| 4590 | - 'devstral-small-2507': 131072, | |
| 4591 | - 'devstral-small-latest': 131072, | |
| 4592 | - 'devstral-medium-latest': 131072, | |
| 4593 | - 'devstral-medium-2507': 131072, | |
| 4594 | - 'magistral-medium-latest': 40960, | |
| 4595 | - 'magistral-medium-2506': 40960, | |
| 4596 | - 'magistral-small-latest': 40960, | |
| 4597 | - 'magistral-small-2506': 40000, | |
| 4598 | - 'magistral-small-2507': 40960, | |
| 4599 | - 'magistral-medium-2507': 40960, | |
| 4600 | - }; | |
| 4601 | - | |
| 4602 | 4663 | // Return context size if model found, otherwise default to 32k |
| 4603 | - return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || 32768; | |
| 4664 | + return max_32k; | |
| 4604 | 4665 | } |
| 4605 | 4666 | |
| 4606 | 4667 | /** |
| @@ -4878,6 +4939,10 @@ async function onModelChange() { | ||
| 4878 | 4939 | } |
| 4879 | 4940 | |
| 4880 | 4941 | if ($(this).is('#model_xai_select')) { |
| 4942 | + if (!value) { | |
| 4943 | + console.debug('Null XAI model selected. Ignoring.'); | |
| 4944 | + return; | |
| 4945 | + } | |
| 4881 | 4946 | console.log('XAI model changed to', value); |
| 4882 | 4947 | oai_settings.xai_model = value; |
| 4883 | 4948 | } |
| @@ -4916,17 +4981,15 @@ async function onModelChange() { | ||
| 4916 | 4981 | if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(oai_settings.chat_completion_source)) { |
| 4917 | 4982 | if (oai_settings.max_context_unlocked) { |
| 4918 | 4983 | $('#openai_max_context').attr('max', max_2mil); |
| 4919 | 4984 | } else if (value.includes('gemini-12.5-proflash-image')) { |
| 4920 | - $('#openai_max_context').attr('max', max_2mil); | |
| 4921 | - } else if (value.includes('gemini-2.5-flash-image-preview')) { | |
| 4922 | 4985 | $('#openai_max_context').attr('max', max_32k); |
| 4923 | 4986 | } else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash') || value.includes('gemini-2.0-pro') || value.includes('gemini-exp') || value.includes('gemini-2.5-flash') || value.includes('gemini-2.5-pro') || value.includes('learnlm-2.0-flash') || value.includes('gemini-robotics')) { |
| 4924 | 4987 | $('#openai_max_context').attr('max', max_1mil); |
| 4925 | 4988 | } else if (value.includes('gemma-3-27b-it')) { |
| 4926 | 4989 | $('#openai_max_context').attr('max', max_128k); |
| 4927 | 4990 | } else if (value.includes('gemma-3n-e4b-it')) { |
| 4928 | 4991 | $('#openai_max_context').attr('max', max_8k); |
| 4929 | 4992 | } else if (value.includes('gemma-3') || value.includes('learnlm-1.5-pro-experimental')) { |
| 4930 | 4993 | $('#openai_max_context').attr('max', max_32k); |
| 4931 | 4994 | } else { |
| 4932 | 4995 | $('#openai_max_context').attr('max', max_32k); |
| @@ -4966,9 +5029,12 @@ async function onModelChange() { | ||
| 4966 | 5029 | |
| 4967 | 5030 | if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) { |
| 4968 | 5031 | if (oai_settings.max_context_unlocked) { |
| 4969 | 5032 | $('#openai_max_context').attr('max', max_200kunlocked_max); |
| 4970 | 5033 | } |
| 4971 | - else if (value == 'claude-2.1' || value.startsWith('claude-3') || value.startsWith('claude-opus') || value.startsWith('claude-sonnet')) { | |
| 5034 | + else if (value.startsWith('claude-sonnet-4-5')) { | |
| 5035 | + $('#openai_max_context').attr('max', max_1mil); | |
| 5036 | + } | |
| 5037 | + else if (value == 'claude-2.1' || value.startsWith('claude-3') || value.startsWith('claude-opus') || value.startsWith('claude-haiku') || value.startsWith('claude-sonnet')) { | |
| 4972 | 5038 | $('#openai_max_context').attr('max', max_200k); |
| 4973 | 5039 | } |
| 4974 | 5040 | else if (value.endsWith('100k') || value.startsWith('claude-2') || value === 'claude-instant-1.2') { |
| @@ -5093,6 +5159,8 @@ async function onModelChange() { | ||
| 5093 | 5159 | $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input'); |
| 5094 | 5160 | oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai); |
| 5095 | 5161 | $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input'); |
| 5162 | + | |
| 5163 | + calculateElectronHubCost(); | |
| 5096 | 5164 | } |
| 5097 | 5165 | |
| 5098 | 5166 | if (oai_settings.chat_completion_source === chat_completion_sources.NANOGPT) { |
| @@ -5144,11 +5212,14 @@ async function onModelChange() { | ||
| 5144 | 5212 | $('#openai_max_context').attr('max', unlocked_max); |
| 5145 | 5213 | } else if (oai_settings.xai_model.includes('grok-2-vision')) { |
| 5146 | 5214 | $('#openai_max_context').attr('max', max_32k); |
| 5147 | 5215 | } else if (oai_settings.xai_model.includes('grok-vision4-fast')) { |
| 5148 | 5216 | $('#openai_max_context').attr('max', max_8kmax_2mil); |
| 5149 | 5217 | } else if (oai_settings.xai_model.includes('grok-4')) { |
| 5150 | 5218 | $('#openai_max_context').attr('max', max_256k); |
| 5219 | + } else if (oai_settings.xai_model.includes('grok-code')) { | |
| 5220 | + $('#openai_max_context').attr('max', max_256k); | |
| 5151 | 5221 | } else { |
| 5222 | + // grok 2 and grok 3 | |
| 5152 | 5223 | $('#openai_max_context').attr('max', max_128k); |
| 5153 | 5224 | } |
| 5154 | 5225 | |
| @@ -5220,6 +5291,10 @@ async function onOpenrouterModelSortChange() { | ||
| 5220 | 5291 | await getStatusOpen(); |
| 5221 | 5292 | } |
| 5222 | 5293 | |
| 5294 | +async function onElectronHubModelSortChange() { | |
| 5295 | + await getStatusOpen(); | |
| 5296 | +} | |
| 5297 | + | |
| 5223 | 5298 | async function onNewPresetClick() { |
| 5224 | 5299 | const name = await Popup.show.input(t`Preset name:`, t`Hint: Use a character/group name to bind preset to a specific chat.`, oai_settings.preset_settings_openai); |
| 5225 | 5300 | |
| @@ -5677,11 +5752,11 @@ export function isImageInliningSupported() { | ||
| 5677 | 5752 | 'c4ai-aya-vision', |
| 5678 | 5753 | 'command-a-vision', |
| 5679 | 5754 | // Google AI Studio |
| 5680 | - 'gemini-1.5', | |
| 5681 | 5755 | 'gemini-2.0', |
| 5682 | 5756 | 'gemini-2.5', |
| 5683 | 5757 | 'gemini-exp-1206', |
| 5684 | 5758 | 'learnlm', |
| 5759 | + 'gemini-robotics', | |
| 5685 | 5760 | // MistralAI |
| 5686 | 5761 | 'mistral-small-2503', |
| 5687 | 5762 | 'mistral-small-2506', |
| @@ -5693,7 +5768,6 @@ export function isImageInliningSupported() { | ||
| 5693 | 5768 | // xAI (Grok) |
| 5694 | 5769 | 'grok-4', |
| 5695 | 5770 | 'grok-2-vision', |
| 5696 | - 'grok-vision', | |
| 5697 | 5771 | // Moonshot |
| 5698 | 5772 | 'moonshot-v1-8k-vision-preview', |
| 5699 | 5773 | 'moonshot-v1-32k-vision-preview', |
| @@ -5726,6 +5800,7 @@ export function isImageInliningSupported() { | ||
| 5726 | 5800 | case chat_completion_sources.COHERE: |
| 5727 | 5801 | return visionSupportedModels.some(model => oai_settings.cohere_model.includes(model)); |
| 5728 | 5802 | case chat_completion_sources.XAI: |
| 5803 | + // TODO: xAI's /models endpoint doesn't return modality info | |
| 5729 | 5804 | return visionSupportedModels.some(model => oai_settings.xai_model.includes(model)); |
| 5730 | 5805 | case chat_completion_sources.AIMLAPI: |
| 5731 | 5806 | return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.aimlapi_model)?.features?.includes('openai/chat-completion.vision')); |
| @@ -6117,12 +6192,14 @@ export function initOpenAI() { | ||
| 6117 | 6192 | oai_settings.openai_max_context = Number($(this).val()); |
| 6118 | 6193 | $('#openai_max_context_counter').val(`${$(this).val()}`); |
| 6119 | 6194 | calculateOpenRouterCost(); |
| 6195 | + calculateElectronHubCost(); | |
| 6120 | 6196 | saveSettingsDebounced(); |
| 6121 | 6197 | }); |
| 6122 | 6198 | |
| 6123 | 6199 | $('#openai_max_tokens').on('input', function () { |
| 6124 | 6200 | oai_settings.openai_max_tokens = Number($(this).val()); |
| 6125 | 6201 | calculateOpenRouterCost(); |
| 6202 | + calculateElectronHubCost(); | |
| 6126 | 6203 | saveSettingsDebounced(); |
| 6127 | 6204 | }); |
| 6128 | 6205 | |
| @@ -6326,6 +6403,16 @@ export function initOpenAI() { | ||
| 6326 | 6403 | saveSettingsDebounced(); |
| 6327 | 6404 | }); |
| 6328 | 6405 | |
| 6406 | + $('#electronhub_sort_models').on('input', function () { | |
| 6407 | + oai_settings.electronhub_sort_models = String($(this).val()); | |
| 6408 | + saveSettingsDebounced(); | |
| 6409 | + }); | |
| 6410 | + | |
| 6411 | + $('#electronhub_group_models').on('input', function () { | |
| 6412 | + oai_settings.electronhub_group_models = !!$(this).prop('checked'); | |
| 6413 | + saveSettingsDebounced(); | |
| 6414 | + }); | |
| 6415 | + | |
| 6329 | 6416 | $('#squash_system_messages').on('input', function () { |
| 6330 | 6417 | oai_settings.squash_system_messages = !!$(this).prop('checked'); |
| 6331 | 6418 | saveSettingsDebounced(); |
| @@ -6503,6 +6590,14 @@ export function initOpenAI() { | ||
| 6503 | 6590 | width: '100%', |
| 6504 | 6591 | templateResult: getAimlapiModelTemplate, |
| 6505 | 6592 | }); |
| 6593 | + $('#model_electronhub_select').select2({ | |
| 6594 | + placeholder: t`Select a model`, | |
| 6595 | + searchInputPlaceholder: t`Search models...`, | |
| 6596 | + searchInputCssClass: 'text_pole', | |
| 6597 | + width: '100%', | |
| 6598 | + templateResult: getElectronHubModelTemplate, | |
| 6599 | + matcher: textValueMatcher, | |
| 6600 | + }); | |
| 6506 | 6601 | $('#completion_prompt_manager_popup_entry_form_injection_trigger').select2({ |
| 6507 | 6602 | placeholder: t`All types (default)`, |
| 6508 | 6603 | width: '100%', |
| @@ -6549,6 +6644,8 @@ export function initOpenAI() { | ||
| 6549 | 6644 | $('#model_openrouter_select').on('change', onModelChange); |
| 6550 | 6645 | $('#openrouter_group_models').on('change', onOpenrouterModelSortChange); |
| 6551 | 6646 | $('#openrouter_sort_models').on('change', onOpenrouterModelSortChange); |
| 6647 | + $('#electronhub_group_models').on('change', onElectronHubModelSortChange); | |
| 6648 | + $('#electronhub_sort_models').on('change', onElectronHubModelSortChange); | |
| 6552 | 6649 | $('#model_ai21_select').on('change', onModelChange); |
| 6553 | 6650 | $('#model_mistralai_select').on('change', onModelChange); |
| 6554 | 6651 | $('#model_cohere_select').on('change', onModelChange); |
| @@ -4,9 +4,11 @@ import { | ||
| 4 | 4 | characters, |
| 5 | 5 | chat, |
| 6 | 6 | chat_metadata, |
| 7 | + createOrEditCharacter, | |
| 7 | 8 | default_user_avatar, |
| 8 | 9 | eventSource, |
| 9 | 10 | event_types, |
| 11 | + getCurrentChatId, | |
| 10 | 12 | getRequestHeaders, |
| 11 | 13 | getThumbnailUrl, |
| 12 | 14 | groupToEntity, |
| @@ -69,6 +71,9 @@ export let user_avatar = ''; | ||
| 69 | 71 | /** @type {FilterHelper} Filter helper for the persona list */ |
| 70 | 72 | export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick)); |
| 71 | 73 | |
| 74 | +/** @type {string} The last loaded chat id to remember for persona loading */ | |
| 75 | +let personaLastLoadedChatId = null; | |
| 76 | + | |
| 72 | 77 | /** @type {function(string): void} */ |
| 73 | 78 | let navigateToAvatar = () => { }; |
| 74 | 79 | |
| @@ -107,12 +112,16 @@ export function initUserAvatar(avatar) { | ||
| 107 | 112 | * @param {boolean} [options.toastPersonaNameChange=true] Whether to show a toast when the persona name is changed |
| 108 | 113 | * @param {boolean} [options.navigateToCurrent=false] Whether to navigate to the current persona after setting the avatar |
| 109 | 114 | */ |
| 110 | 115 | export async function setUserAvatar(imgfile, { toastPersonaNameChange = true, navigateToCurrent = false } = {}) { |
| 116 | + const currentUserAvatar = user_avatar; | |
| 111 | 117 | user_avatar = imgfile && typeof imgfile === 'string' ? imgfile : $(this).attr('data-avatar-id'); |
| 118 | + if (currentUserAvatar === user_avatar) { | |
| 119 | + return; | |
| 120 | + } | |
| 112 | 121 | reloadUserAvatar(); |
| 113 | 122 | updatePersonaUIStates({ navigateToCurrent: navigateToCurrent }); |
| 114 | 123 | selectCurrentPersona({ toastPersonaNameChange: toastPersonaNameChange }); |
| 115 | 124 | await retriggerFirstMessageOnEmptyChat(); |
| 116 | 125 | saveSettingsDebounced(); |
| 117 | 126 | $('.zoomed_avatar[forchar]').remove(); |
| 118 | 127 | } |
| @@ -732,13 +741,13 @@ export async function askForPersonaSelection(title, text, personas, { okButton = | ||
| 732 | 741 | /** |
| 733 | 742 | * Automatically selects a persona based on the given name if a matching persona exists. |
| 734 | 743 | * @param {string} name - The name to search for |
| 735 | 744 | * @returns {Promise<boolean>} True if a matching persona was found and selected, false otherwise |
| 736 | 745 | */ |
| 737 | 746 | export async function autoSelectPersona(name) { |
| 738 | 747 | for (const [key, value] of Object.entries(power_user.personas)) { |
| 739 | 748 | if (value === name) { |
| 740 | 749 | console.log(`Auto-selecting persona ${key} for name ${name}`); |
| 741 | 750 | await setUserAvatar(key); |
| 742 | 751 | return true; |
| 743 | 752 | } |
| 744 | 753 | } |
| @@ -1131,7 +1140,7 @@ function onPersonaDescriptionInput() { | ||
| 1131 | 1140 | object.description = power_user.persona_description; |
| 1132 | 1141 | } |
| 1133 | 1142 | |
| 1134 | 1143 | $(`.avatar-container[imgfiledata-avatar-id="${user_avatar}"] .ch_description`) |
| 1135 | 1144 | .text(power_user.persona_description || $('#user_avatar_block').attr('no_desc_text')) |
| 1136 | 1145 | .toggleClass('text_muted', !power_user.persona_description); |
| 1137 | 1146 | saveSettingsDebounced(); |
| @@ -1219,7 +1228,7 @@ function onPersonaDescriptionPositionInput() { | ||
| 1219 | 1228 | $('#persona_depth_position_settings').toggle(power_user.persona_description_position === persona_description_positions.AT_DEPTH); |
| 1220 | 1229 | } |
| 1221 | 1230 | |
| 1222 | 1231 | export function getOrCreatePersonaDescriptor() { |
| 1223 | 1232 | let object = power_user.persona_descriptions[user_avatar]; |
| 1224 | 1233 | |
| 1225 | 1234 | if (!object) { |
| @@ -1429,13 +1438,17 @@ function getPersonaTemporaryLockInfo() { | ||
| 1429 | 1438 | * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating whether a persona was selected |
| 1430 | 1439 | */ |
| 1431 | 1440 | async function loadPersonaForCurrentChat({ doRender = false } = {}) { |
| 1441 | + const currentChatId = getCurrentChatId(); | |
| 1442 | + if (currentChatId === personaLastLoadedChatId) return; | |
| 1443 | + personaLastLoadedChatId = currentChatId; | |
| 1444 | + | |
| 1432 | 1445 | // Cache persona list to check if they exist |
| 1433 | 1446 | const userAvatars = await getUserAvatars(doRender); |
| 1434 | 1447 | |
| 1435 | 1448 | // Check if the user avatar is set and exists in the list of user avatars |
| 1436 | 1449 | if (userAvatars.length && !userAvatars.includes(user_avatar)) { |
| 1437 | 1450 | console.log(`User avatar ${user_avatar} not found in user avatars list, pick the first available one`); |
| 1438 | 1451 | await setUserAvatar(userAvatars[0], { toastPersonaNameChange: false, navigateToCurrent: true }); |
| 1439 | 1452 | } |
| 1440 | 1453 | |
| 1441 | 1454 | // Define a persona for this chat |
| @@ -1530,7 +1543,7 @@ async function loadPersonaForCurrentChat({ doRender = false } = {}) { | ||
| 1530 | 1543 | // Persona avatar found, select it |
| 1531 | 1544 | if (chatPersona && user_avatar !== chatPersona) { |
| 1532 | 1545 | const willAutoLock = power_user.persona_auto_lock && user_avatar !== chat_metadata['persona']; |
| 1533 | 1546 | await setUserAvatar(chatPersona, { toastPersonaNameChange: false, navigateToCurrent: true }); |
| 1534 | 1547 | |
| 1535 | 1548 | if (power_user.persona_show_notifications) { |
| 1536 | 1549 | let message = t`Auto-selected persona based on ${connectType} connection.<br />Your messages will now be sent as ${power_user.personas[chatPersona]}.`; |
| @@ -1542,7 +1555,7 @@ async function loadPersonaForCurrentChat({ doRender = false } = {}) { | ||
| 1542 | 1555 | } |
| 1543 | 1556 | // Even if it's the same persona, we still might need to auto-lock to chat if that's enabled |
| 1544 | 1557 | else if (chatPersona && power_user.persona_auto_lock && !chat_metadata['persona']) { |
| 1545 | 1558 | await lockPersona('chat'); |
| 1546 | 1559 | } |
| 1547 | 1560 | |
| 1548 | 1561 | updatePersonaUIStates(); |
| @@ -1559,7 +1572,7 @@ async function loadPersonaForCurrentChat({ doRender = false } = {}) { | ||
| 1559 | 1572 | export function getConnectedPersonas(characterKey = undefined) { |
| 1560 | 1573 | characterKey ??= selected_group || characters[Number(this_chid)]?.avatar; |
| 1561 | 1574 | const connectedPersonas = Object.entries(power_user.persona_descriptions) |
| 1562 | 1575 | .filter(([_, desc{ connections }]) => desc.connections?.some(conn => conn.type === 'character' && conn.id === characterKey)) |
| 1563 | 1576 | .map(([key, _]) => key); |
| 1564 | 1577 | return connectedPersonas; |
| 1565 | 1578 | } |
| @@ -1606,7 +1619,7 @@ export async function showCharConnections() { | ||
| 1606 | 1619 | |
| 1607 | 1620 | // One of the persona was selected. So load it. |
| 1608 | 1621 | if (!isRemoving && selectedPersona) { |
| 1609 | 1622 | await setUserAvatar(selectedPersona, { toastPersonaNameChange: false }); |
| 1610 | 1623 | if (power_user.persona_show_notifications) { |
| 1611 | 1624 | toastr.success(t`Selected persona ${power_user.personas[selectedPersona]} for current chat.`, t`Connected Persona Selected`); |
| 1612 | 1625 | } |
| @@ -1736,12 +1749,16 @@ async function syncUserNameToPersona() { | ||
| 1736 | 1749 | |
| 1737 | 1750 | /** |
| 1738 | 1751 | * Retriggers the first message to reload it from the char definition. |
| 1739 | - * | |
| 1740 | - * Only works if only the first message is present, and not in group mode. | |
| 1741 | 1752 | */ |
| 1742 | 1753 | export async function retriggerFirstMessageOnEmptyChat() { |
| 1743 | - if (Number(this_chid) >= 0 && !selected_group && chat.length === 1) { | |
| 1754 | + if (chat_metadata.tainted) { | |
| 1744 | - $('#firstmessage_textarea').trigger('input'); | |
| 1755 | + return; | |
| 1756 | + } | |
| 1757 | + if (selected_group) { | |
| 1758 | + await reloadCurrentChat(); | |
| 1759 | + } | |
| 1760 | + if (!selected_group && Number(this_chid) >= 0 && chat.length === 1) { | |
| 1761 | + await createOrEditCharacter(); | |
| 1745 | 1762 | } |
| 1746 | 1763 | } |
| 1747 | 1764 | |
| @@ -1838,9 +1855,9 @@ async function lockPersonaCallback(_args, value) { | ||
| 1838 | 1855 | * Sets a persona name and optionally an avatar. |
| 1839 | 1856 | * @param {{mode: 'lookup' | 'temp' | 'all'}} namedArgs Named arguments |
| 1840 | 1857 | * @param {string} name Name to set |
| 1841 | 1858 | * @returns {Promise<string>} |
| 1842 | 1859 | */ |
| 1843 | 1860 | async function setNameCallback({ mode = 'all' }, name) { |
| 1844 | 1861 | if (!name) { |
| 1845 | 1862 | toastr.warning('You must specify a name to change to'); |
| 1846 | 1863 | return ''; |
| @@ -1858,7 +1875,7 @@ function setNameCallback({ mode = 'all' }, name) { | ||
| 1858 | 1875 | let persona = Object.entries(power_user.personas).find(([avatar, _]) => avatar === name)?.[1]; |
| 1859 | 1876 | if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1]; |
| 1860 | 1877 | if (persona) { |
| 1861 | 1878 | await autoSelectPersona(persona); |
| 1862 | 1879 | return ''; |
| 1863 | 1880 | } else if (mode === 'lookup') { |
| 1864 | 1881 | toastr.warning(`Persona ${name} not found`); |
| @@ -2016,9 +2033,9 @@ export async function initPersonas() { | ||
| 2016 | 2033 | $('#sync_name_button').on('click', syncUserNameToPersona); |
| 2017 | 2034 | $('#avatar_upload_file').on('change', changeUserAvatar); |
| 2018 | 2035 | |
| 2019 | 2036 | $(document).on('click', '#user_avatar_block .avatar-container', async function () { |
| 2020 | 2037 | const imgfile = $(this).attr('data-avatar-id'); |
| 2021 | 2038 | await setUserAvatar(imgfile); |
| 2022 | 2039 | }); |
| 2023 | 2040 | |
| 2024 | 2041 | $('#persona_rename_button').on('click', () => renamePersona(user_avatar)); |
| @@ -37,8 +37,8 @@ export const POPUP_RESULT = { | ||
| 37 | 37 | |
| 38 | 38 | /** |
| 39 | 39 | * @typedef {object} PopupOptions |
| 40 | 40 | * @property {string|boolean?} [okButton=null] - Custom text for the OK button,. orA set text will always show the button. `true` toor use`false` theto defaultexplicitly (Ifshow set,or hide the button. `null` will alwaysleave bethe displayed,behavior noand matterdisplay of the typebutton ofunchanged, based on the popup) type. |
| 41 | 41 | * @property {string|boolean?} [cancelButton=null] - Custom text for the Cancel button,. orA set text will always show the button. `true` toor use`false` theto defaultexplicitly (Ifshow set,or hide the button. `null` will alwaysleave bethe displayed,behavior noand matterdisplay of the typebutton ofunchanged, based on the popup) type. |
| 42 | 42 | * @property {number?} [rows=1] - The number of rows for the input field |
| 43 | 43 | * @property {boolean?} [wide=false] - Whether to display the popup in wide mode (wide screen, 1/1 aspect ratio) |
| 44 | 44 | * @property {boolean?} [wider=false] - Whether to display the popup in wider mode (just wider, no height scaling) |
| @@ -326,21 +326,31 @@ export class Popup { | ||
| 326 | 326 | |
| 327 | 327 | switch (type) { |
| 328 | 328 | case POPUP_TYPE.TEXT: { |
| 329 | + //Text shows OK if not explicitly set to false, and CANCEL only if defined as true or with a caption | |
| 330 | + if (okButton === false) this.okButton.style.display = 'none'; | |
| 329 | 331 | if (!cancelButton) this.cancelButton.style.display = 'none'; |
| 330 | 332 | break; |
| 331 | 333 | } |
| 332 | 334 | case POPUP_TYPE.CONFIRM: { |
| 335 | + // Confirm shows OK if not explicitly set to false, and CANCEL if not explicitly set to false | |
| 336 | + if (okButton === false) this.okButton.style.display = 'none'; | |
| 337 | + if (cancelButton === false) this.cancelButton.style.display = 'none'; | |
| 338 | + // Override default captions for confirm on OK->Yes, CANCEL->No | |
| 333 | 339 | if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-yes'); |
| 334 | 340 | if (!cancelButton) this.cancelButton.textContent = template.getAttribute('popup-button-no'); |
| 335 | 341 | break; |
| 336 | 342 | } |
| 337 | 343 | case POPUP_TYPE.INPUT: { |
| 338 | 344 | this.mainInput.style.display = 'block'; |
| 339 | - if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-save'); | |
| 345 | + // Input shows OK if not explicitly set to false, and CANCEL if not explicitly set to false | |
| 346 | + if (okButton === false) this.okButton.style.display = 'none'; | |
| 340 | 347 | if (cancelButton === false) this.cancelButton.style.display = 'none'; |
| 348 | + // Override default captions for input on OK->Save | |
| 349 | + if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-save'); | |
| 341 | 350 | break; |
| 342 | 351 | } |
| 343 | 352 | case POPUP_TYPE.DISPLAY: { |
| 353 | + // Display hides OK and CANCEL and all main button controls | |
| 344 | 354 | this.buttonControls.style.display = 'none'; |
| 345 | 355 | this.closeButton.style.display = 'block'; |
| 346 | 356 | break; |
| @@ -348,7 +358,6 @@ export class Popup { | ||
| 348 | 358 | case POPUP_TYPE.CROP: { |
| 349 | 359 | this.cropWrap.style.display = 'block'; |
| 350 | 360 | this.cropImage.src = cropImage; |
| 351 | - if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-crop'); | |
| 352 | 361 | $(this.cropImage).cropper({ |
| 353 | 362 | aspectRatio: cropAspect ?? 2 / 3, |
| 354 | 363 | autoCropArea: 1, |
| @@ -359,6 +368,11 @@ export class Popup { | ||
| 359 | 368 | this.cropData.want_resize = !power_user.never_resize_avatars; |
| 360 | 369 | }, |
| 361 | 370 | }); |
| 371 | + // Crop shows OK if not explicitly set to false, and CANCEL if not explicitly set to false | |
| 372 | + if (okButton === false) this.okButton.style.display = 'none'; | |
| 373 | + if (cancelButton === false) this.cancelButton.style.display = 'none'; | |
| 374 | + // Override default captions for crop on OK->Crop | |
| 375 | + if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-crop'); | |
| 362 | 376 | break; |
| 363 | 377 | } |
| 364 | 378 | default: { |
| @@ -137,6 +137,7 @@ export const power_user = { | ||
| 137 | 137 | streaming_fps: 30, |
| 138 | 138 | smooth_streaming: false, |
| 139 | 139 | smooth_streaming_speed: 50, |
| 140 | + stream_fade_in: false, | |
| 140 | 141 | |
| 141 | 142 | fast_ui_mode: true, |
| 142 | 143 | avatar_style: avatar_styles.ROUND, |
| @@ -1685,6 +1686,8 @@ export async function loadPowerUserSettings(settings, data) { | ||
| 1685 | 1686 | $('#smooth_streaming').prop('checked', power_user.smooth_streaming); |
| 1686 | 1687 | $('#smooth_streaming_speed').val(power_user.smooth_streaming_speed); |
| 1687 | 1688 | |
| 1689 | + $('#stream_fade_in').prop('checked', power_user.stream_fade_in); | |
| 1690 | + | |
| 1688 | 1691 | $('#font_scale').val(power_user.font_scale); |
| 1689 | 1692 | $('#font_scale_counter').val(power_user.font_scale); |
| 1690 | 1693 | |
| @@ -3493,6 +3496,11 @@ jQuery(() => { | ||
| 3493 | 3496 | saveSettingsDebounced(); |
| 3494 | 3497 | }); |
| 3495 | 3498 | |
| 3499 | + $('#stream_fade_in').on('input', function () { | |
| 3500 | + power_user.stream_fade_in = !!$(this).prop('checked'); | |
| 3501 | + saveSettingsDebounced(); | |
| 3502 | + }); | |
| 3503 | + | |
| 3496 | 3504 | $('input[name="font_scale"]').on('input', async function (e, data) { |
| 3497 | 3505 | const applyMode = data?.forced ? 'forced' : 'normal'; |
| 3498 | 3506 | power_user.font_scale = Number($(this).val()); |
| @@ -81,6 +81,9 @@ function autoSelectPreset() { | ||
| 81 | 81 | * @returns {PresetManager} Preset manager |
| 82 | 82 | */ |
| 83 | 83 | export function getPresetManager(apiId = '') { |
| 84 | + if (apiId === 'koboldhorde') { | |
| 85 | + apiId = 'kobold'; | |
| 86 | + } | |
| 84 | 87 | if (!apiId) { |
| 85 | 88 | apiId = main_api == 'koboldhorde' ? 'kobold' : main_api; |
| 86 | 89 | } |
| @@ -814,7 +817,7 @@ class PresetManager { | ||
| 814 | 817 | * Reads a preset extension field from the preset. |
| 815 | 818 | * @param {object} options |
| 816 | 819 | * @param {string} [options.name] Name of the preset. If not provided, uses the currently selected preset name. |
| 817 | 820 | * @param {string} options.path Path to the preset extension field, e.g. 'myextension.data'. If empty, reads the entire extensions object. |
| 818 | 821 | * @return {any} The value of the preset extension field, or null if not found. |
| 819 | 822 | */ |
| 820 | 823 | readPresetExtensionField({ name, path }) { |
| @@ -825,7 +828,7 @@ class PresetManager { | ||
| 825 | 828 | // Read from settings if the selected preset is the same as the provided name |
| 826 | 829 | if (settings && selectedName === presetName) { |
| 827 | 830 | const settingsExtensions = ensurePlainObject(settings.extensions || {}); |
| 828 | 831 | return path ? lodash.get(settingsExtensions, path, null) : settingsExtensions; |
| 829 | 832 | } |
| 830 | 833 | |
| 831 | 834 | // Otherwise, read from the preset by name |
| @@ -835,7 +838,7 @@ class PresetManager { | ||
| 835 | 838 | } |
| 836 | 839 | |
| 837 | 840 | const presetExtensions = ensurePlainObject(preset.extensions || {}); |
| 838 | 841 | const value = path ? lodash.get(presetExtensions, path, null) : presetExtensions; |
| 839 | 842 | return value; |
| 840 | 843 | } |
| 841 | 844 | |
| @@ -843,7 +846,7 @@ class PresetManager { | ||
| 843 | 846 | * Writes a value to a preset extension field. |
| 844 | 847 | * @param {object} options |
| 845 | 848 | * @param {string} [options.name] Name of the preset. If not provided, uses the currently selected preset name. |
| 846 | 849 | * @param {string} options.path Path to the preset extension field, e.g. 'myextension.data'. If empty, writes to the root of the extensions object. |
| 847 | 850 | * @param {any} options.value Value to write to the preset extension field. |
| 848 | 851 | * @return {Promise<void>} Resolves when the preset is saved. |
| 849 | 852 | */ |
| @@ -856,7 +859,7 @@ class PresetManager { | ||
| 856 | 859 | if (settings && selectedName === presetName) { |
| 857 | 860 | // Set the value at the specified path |
| 858 | 861 | settings.extensions = ensurePlainObject(settings.extensions || {}); |
| 859 | 862 | path ? lodash.set(settings.extensions, path, value) : (settings.extensions = value); |
| 860 | 863 | await saveSettings(); |
| 861 | 864 | } |
| 862 | 865 | |
| @@ -868,7 +871,7 @@ class PresetManager { | ||
| 868 | 871 | |
| 869 | 872 | // Set the value at the specified path |
| 870 | 873 | preset.extensions = ensurePlainObject(preset.extensions || {}); |
| 871 | 874 | path ? lodash.set(preset.extensions, path, value) : (preset.extensions = value); |
| 872 | 875 | |
| 873 | 876 | // Save the updated preset |
| 874 | 877 | await this.savePreset(presetName, preset, { skipUpdate: true }); |
| @@ -1033,10 +1036,11 @@ export async function initPresetManager() { | ||
| 1033 | 1036 | return; |
| 1034 | 1037 | } |
| 1035 | 1038 | |
| 1039 | + await eventSource.emit(event_types.PRESET_RENAMED_BEFORE, { apiId: apiId, oldName: oldName, newName: newName }); | |
| 1040 | + const extensions = presetManager.readPresetExtensionField({ name: oldName, path: '' }); | |
| 1036 | 1041 | await presetManager.renamePreset(newName); |
| 1037 | - | |
| 1042 | + await presetManager.writePresetExtensionField({ name: newName, path: '', value: extensions }); | |
| 1038 | 1043 | await eventSource.emit(event_types.PRESET_DELETEDPRESET_RENAMED, { apiId: apiId, nameoldName: oldName, newName: newName }); |
| 1039 | - await eventSource.emit(event_types.PRESET_CHANGED, { apiId: apiId, name: newName }); | |
| 1040 | 1044 | |
| 1041 | 1045 | if (apiId === 'openai') { |
| 1042 | 1046 | // This is a horrible mess, but prevents the renamed preset from being corrupted. |
| @@ -1116,13 +1120,13 @@ export async function initPresetManager() { | ||
| 1116 | 1120 | if (result) { |
| 1117 | 1121 | const successToast = !presetManager.isAdvancedFormatting() ? t`Preset deleted` : t`Template deleted`; |
| 1118 | 1122 | toastr.success(successToast); |
| 1123 | + await eventSource.emit(event_types.PRESET_DELETED, { apiId, name }); | |
| 1119 | 1124 | } else { |
| 1120 | 1125 | const warningToast = !presetManager.isAdvancedFormatting() ? t`Preset was not deleted from server` : t`Template was not deleted from server`; |
| 1121 | 1126 | toastr.warning(warningToast); |
| 1122 | 1127 | } |
| 1123 | 1128 | |
| 1124 | 1129 | saveSettingsDebounced(); |
| 1125 | - await eventSource.emit(event_types.PRESET_DELETED, { apiId: apiId, name: name }); | |
| 1126 | 1130 | }); |
| 1127 | 1131 | |
| 1128 | 1132 | $(document).on('click', '[data-preset-manager-restore]', async function () { |
| @@ -15,6 +15,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom | ||
| 15 | 15 | import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; |
| 16 | 16 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 17 | 17 | import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; |
| 18 | +import { applyStreamFadeIn } from './util/stream-fadein.js'; | |
| 18 | 19 | import { copyText, escapeRegex, isFalseBoolean, isTrueBoolean, setDatasetProperty, trimSpaces } from './utils.js'; |
| 19 | 20 | |
| 20 | 21 | /** |
| @@ -124,6 +125,8 @@ export function extractReasoningFromData(data, { | ||
| 124 | 125 | case chat_completion_sources.POLLINATIONS: |
| 125 | 126 | case chat_completion_sources.MOONSHOT: |
| 126 | 127 | case chat_completion_sources.COMETAPI: |
| 128 | + case chat_completion_sources.ELECTRONHUB: | |
| 129 | + case chat_completion_sources.NANOGPT: | |
| 127 | 130 | case chat_completion_sources.CUSTOM: { |
| 128 | 131 | return data?.choices?.[0]?.message?.reasoning_content |
| 129 | 132 | ?? data?.choices?.[0]?.message?.reasoning |
| @@ -495,7 +498,12 @@ export class ReasoningHandler { | ||
| 495 | 498 | // Update the reasoning message |
| 496 | 499 | const reasoning = trimSpaces(this.reasoningDisplayText ?? this.reasoning); |
| 497 | 500 | const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true); |
| 498 | - this.messageReasoningContentDom.innerHTML = displayReasoning; | |
| 501 | + | |
| 502 | + if (power_user.stream_fade_in) { | |
| 503 | + applyStreamFadeIn(this.messageReasoningContentDom, displayReasoning); | |
| 504 | + } else { | |
| 505 | + this.messageReasoningContentDom.innerHTML = displayReasoning; | |
| 506 | + } | |
| 499 | 507 | |
| 500 | 508 | // Update tooltip for hidden reasoning edit |
| 501 | 509 | /** @type {HTMLElement} */ |
| @@ -2,7 +2,7 @@ import { DOMPurify, moment } from '../lib.js'; | ||
| 2 | 2 | import { event_types, eventSource, getRequestHeaders } from '../script.js'; |
| 3 | 3 | import { t } from './i18n.js'; |
| 4 | 4 | import { chat_completion_sources } from './openai.js'; |
| 5 | 5 | import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; |
| 6 | 6 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 7 | 7 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; |
| 8 | 8 | import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| @@ -293,11 +293,13 @@ export let secret_state = {}; | ||
| 293 | 293 | * @param {string} key Secret key |
| 294 | 294 | * @param {string} value Secret value to write |
| 295 | 295 | * @param {string} [label] (Optional) Label for the key. If not provided, generated automatically. |
| 296 | + * @param {Object} [options] Additional options | |
| 297 | + * @param {boolean} [options.allowEmpty] Whether to allow writing empty values. If false and value is empty, the secret will be deleted. | |
| 296 | 298 | * @return {Promise<string?>} The ID of the newly created secret key, or null if no value is provided. |
| 297 | 299 | */ |
| 298 | 300 | export async function writeSecret(key, value, label, { allowEmpty } = {}) { |
| 299 | 301 | try { |
| 300 | 302 | if (!value && !allowEmpty) { |
| 301 | 303 | console.warn(`No value provided for ${key} in writeSecret, redirecting to deleteSecret`); |
| 302 | 304 | await deleteSecret(key); |
| 303 | 305 | return null; |
| @@ -558,7 +560,8 @@ async function openKeyManagerDialog(key) { | ||
| 558 | 560 | const template = $(await renderTemplateAsync('secretKeyManager', { name, key })); |
| 559 | 561 | template.find('button[data-action="add-secret"]').on('click', async function () { |
| 560 | 562 | let label = ''; |
| 561 | - const value = await Popup.show.input(t`Add Secret`, t`Enter the secret value:`, '', { | |
| 563 | + let result = POPUP_RESULT.CANCELLED; | |
| 564 | + const value = await Popup.show.input(t`Add Secret`, t`Enter the secret value (can be empty):`, '', { | |
| 562 | 565 | customInputs: [{ |
| 563 | 566 | id: 'newSecretLabel', |
| 564 | 567 | type: 'text', |
| @@ -567,13 +570,20 @@ async function openKeyManagerDialog(key) { | ||
| 567 | 570 | onClose: popup => { |
| 568 | 571 | if (popup.result) { |
| 569 | 572 | label = popup.inputResults.get('newSecretLabel').toString().trim(); |
| 573 | + result = popup.result; | |
| 570 | 574 | } |
| 571 | 575 | }, |
| 572 | 576 | }); |
| 573 | 577 | if (!value) { |
| 574 | - return; | |
| 578 | + if (result !== POPUP_RESULT.AFFIRMATIVE) { | |
| 579 | + return; | |
| 580 | + } | |
| 581 | + const allowEmpty = await Popup.show.confirm(t`No value entered`, t`No value was entered for the secret. Do you want to add an empty secret?`); | |
| 582 | + if (!allowEmpty) { | |
| 583 | + return; | |
| 584 | + } | |
| 575 | 585 | } |
| 576 | 586 | await writeSecret(key, value, label, { allowEmpty: true }); |
| 577 | 587 | await renderSecretsList(); |
| 578 | 588 | }); |
| 579 | 589 | |
| @@ -821,6 +831,13 @@ function registerSecretSlashCommands() { | ||
| 821 | 831 | isRequired: false, |
| 822 | 832 | typeList: [ARGUMENT_TYPE.STRING], |
| 823 | 833 | }), |
| 834 | + SlashCommandNamedArgument.fromProps({ | |
| 835 | + name: 'empty', | |
| 836 | + description: t`Whether to allow empty values.`, | |
| 837 | + isRequired: false, | |
| 838 | + typeList: [ARGUMENT_TYPE.BOOLEAN], | |
| 839 | + defaultValue: String(false), | |
| 840 | + }), | |
| 824 | 841 | ], |
| 825 | 842 | unnamedArgumentList: [ |
| 826 | 843 | SlashCommandArgument.fromProps({ |
| @@ -831,6 +848,7 @@ function registerSecretSlashCommands() { | ||
| 831 | 848 | ], |
| 832 | 849 | callback: async (args, value) => { |
| 833 | 850 | const quiet = isTrueBoolean(args?.quiet?.toString()); |
| 851 | + const allowEmpty = isTrueBoolean(args?.empty?.toString()); | |
| 834 | 852 | const key = args?.key?.toString()?.trim() || resolveSecretKey(); |
| 835 | 853 | |
| 836 | 854 | if (!key) { |
| @@ -849,7 +867,7 @@ function registerSecretSlashCommands() { | ||
| 849 | 867 | } |
| 850 | 868 | |
| 851 | 869 | const valueStr = value?.toString()?.trim(); |
| 852 | 870 | if (!valueStr && !allowEmpty) { |
| 853 | 871 | if (!quiet) { |
| 854 | 872 | toastr.error(t`No value provided for the secret key: ${key}`); |
| 855 | 873 | } |
| @@ -857,7 +875,7 @@ function registerSecretSlashCommands() { | ||
| 857 | 875 | } |
| 858 | 876 | |
| 859 | 877 | const label = args?.label?.toString()?.trim() || getLabel(); |
| 860 | 878 | const id = await writeSecret(key, valueStr, label, { allowEmpty }); |
| 861 | 879 | |
| 862 | 880 | if (!quiet) { |
| 863 | 881 | toastr.success(t`Secret has been written for the key: ${key}`); |
| @@ -4585,6 +4585,8 @@ export async function sendMessageAs(args, text) { | ||
| 4585 | 4585 | insertAt = chat.length + insertAt; |
| 4586 | 4586 | } |
| 4587 | 4587 | |
| 4588 | + chat_metadata['tainted'] = true; | |
| 4589 | + | |
| 4588 | 4590 | if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { |
| 4589 | 4591 | chat.splice(insertAt, 0, message); |
| 4590 | 4592 | await saveChatConditional(); |
| @@ -4635,6 +4637,8 @@ export async function sendNarratorMessage(args, text) { | ||
| 4635 | 4637 | insertAt = chat.length + insertAt; |
| 4636 | 4638 | } |
| 4637 | 4639 | |
| 4640 | + chat_metadata['tainted'] = true; | |
| 4641 | + | |
| 4638 | 4642 | if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { |
| 4639 | 4643 | chat.splice(insertAt, 0, message); |
| 4640 | 4644 | await saveChatConditional(); |
| @@ -4685,6 +4689,8 @@ export async function promptQuietForLoudResponse(who, text) { | ||
| 4685 | 4689 | }, |
| 4686 | 4690 | }; |
| 4687 | 4691 | |
| 4692 | + chat_metadata['tainted'] = true; | |
| 4693 | + | |
| 4688 | 4694 | chat.push(message); |
| 4689 | 4695 | await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); |
| 4690 | 4696 | addOneMessage(message); |
| @@ -4719,6 +4725,8 @@ async function sendCommentMessage(args, text) { | ||
| 4719 | 4725 | insertAt = chat.length + insertAt; |
| 4720 | 4726 | } |
| 4721 | 4727 | |
| 4728 | + chat_metadata['tainted'] = true; | |
| 4729 | + | |
| 4722 | 4730 | if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { |
| 4723 | 4731 | chat.splice(insertAt, 0, message); |
| 4724 | 4732 | await saveChatConditional(); |
| @@ -57,6 +57,7 @@ import { | ||
| 57 | 57 | import { |
| 58 | 58 | extension_settings, |
| 59 | 59 | ModuleWorkerWrapper, |
| 60 | + openThirdPartyExtensionMenu, | |
| 60 | 61 | renderExtensionTemplate, |
| 61 | 62 | renderExtensionTemplateAsync, |
| 62 | 63 | saveMetadataDebounced, |
| @@ -233,6 +234,7 @@ export function getContext() { | ||
| 233 | 234 | parseReasoningFromString, |
| 234 | 235 | unshallowCharacter, |
| 235 | 236 | unshallowGroupMembers, |
| 237 | + openThirdPartyExtensionMenu, | |
| 236 | 238 | symbols: { |
| 237 | 239 | ignore: IGNORE_SYMBOL, |
| 238 | 240 | }, |
| @@ -765,8 +765,13 @@ async function importTags(character, { importSetting = null } = {}) { | ||
| 765 | 765 | */ |
| 766 | 766 | async function handleTagImport(character, { importSetting = null } = {}) { |
| 767 | 767 | /** @type {string[]} */ |
| 768 | + const alreadyAssignedTags = tag_map[character.avatar] ?? []; | |
| 768 | 769 | const importTags = character.tags.map(t => t.trim()).filter(t => t) |
| 769 | 770 | .filter(t => !IMPORT_EXLCUDED_TAGS.includes(t)) |
| 771 | + .filter(t => { | |
| 772 | + const existingTag = getTag(t); | |
| 773 | + return !existingTag || !alreadyAssignedTags.includes(existingTag.id); | |
| 774 | + }) | |
| 770 | 775 | .slice(0, ANTI_TROLL_MAX_TAGS); |
| 771 | 776 | const existingTags = getExistingTags(importTags); |
| 772 | 777 | const newTags = importTags.filter(t => !existingTags.some(existingTag => existingTag.name.toLowerCase() === t.toLowerCase())) |
| @@ -22,8 +22,10 @@ | ||
| 22 | 22 | <li><tt>{{char}}</tt> – <span data-i18n="help_macros_16">the Character's name</span></li> |
| 23 | 23 | <li><tt>{{version}}</tt> – <span data-i18n="help_macros_17">the Character's version number</span></li> |
| 24 | 24 | <li><tt>{{charDepthPrompt}}</tt> – <span data-i18n="help_macros_charDepthPrompt">the Character's @ Depth Note</span></li> |
| 25 | + <li><tt>{{outlet::(name)}}</tt> – <span data-i18n="help_macros_outletName">the WI entry content for the outlet with the specified name</span></li> | |
| 25 | 26 | <li><tt>{{group}}</tt> – <span data-i18n="help_macros_18">a comma-separated list of group member names (including muted) or the character name in solo chats. Alias: {{charIfNotGroup}}</span></li> |
| 26 | 27 | <li><tt>{{groupNotMuted}}</tt> – <span data-i18n="help_groupNotMuted">the same as {{group}}, but excludes muted members</span></li> |
| 28 | + <li><tt>{{notChar}}</tt> – <span data-i18n="help_notChar">a comma-separated list of all participants in the conversation except for the current speaker ({{char}}). In group chats, this includes muted characters. When not in a generation, the list include all characters.</span></li> | |
| 27 | 29 | <li><tt>{{model}}</tt> – <span data-i18n="help_macros_19">a text generation model name for the currently selected API. </span><b data-i18n="Can be inaccurate!">Can be inaccurate!</b></li> |
| 28 | 30 | <li><tt>{{lastMessage}}</tt> – <span data-i18n="help_macros_20">the text of the latest chat message.</span></li> |
| 29 | 31 | <li><tt>{{lastUserMessage}}</tt> – <span data-i18n="help_macros_lastUser">the text of the latest user chat message.</span></li> |
| @@ -648,6 +648,57 @@ export function getTokenizerModel() { | ||
| 648 | 648 | } |
| 649 | 649 | } |
| 650 | 650 | |
| 651 | + if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB && oai_settings.electronhub_model) { | |
| 652 | + if (oai_settings.electronhub_model.includes('gpt-4o') || oai_settings.electronhub_model.includes('gpt-5')) { | |
| 653 | + return gpt4oTokenizer; | |
| 654 | + } | |
| 655 | + else if (oai_settings.electronhub_model.includes('gpt-4.1') || oai_settings.electronhub_model.includes('gpt-4.5')) { | |
| 656 | + return gpt4oTokenizer; | |
| 657 | + } | |
| 658 | + else if (oai_settings.electronhub_model.includes('gpt-4')) { | |
| 659 | + return gpt4Tokenizer; | |
| 660 | + } | |
| 661 | + else if (oai_settings.electronhub_model.includes('gpt-3.5-turbo')) { | |
| 662 | + return turboTokenizer; | |
| 663 | + } | |
| 664 | + else if (oai_settings.electronhub_model.includes('claude')) { | |
| 665 | + return claudeTokenizer; | |
| 666 | + } | |
| 667 | + else if (oai_settings.electronhub_model.includes('jamba')) { | |
| 668 | + return jambaTokenizer; | |
| 669 | + } | |
| 670 | + else if (oai_settings.electronhub_model.includes('deepseek') || oai_settings.electronhub_model.includes('sonar-reasoning') || oai_settings.electronhub_model.includes('r1')) { | |
| 671 | + return deepseekTokenizer; | |
| 672 | + } | |
| 673 | + else if (oai_settings.electronhub_model.includes('qwen')) { | |
| 674 | + return qwen2Tokenizer; | |
| 675 | + } | |
| 676 | + else if (oai_settings.electronhub_model.includes('gemma')) { | |
| 677 | + return gemmaTokenizer; | |
| 678 | + } | |
| 679 | + else if (oai_settings.electronhub_model.includes('mistral')) { | |
| 680 | + return mistralTokenizer; | |
| 681 | + } | |
| 682 | + else if (oai_settings.electronhub_model.includes('yi')) { | |
| 683 | + return yiTokenizer; | |
| 684 | + } | |
| 685 | + else if (oai_settings.electronhub_model.includes('llama3') || oai_settings.electronhub_model.includes('llama-3') || oai_settings.electronhub_model.startsWith('l3')) { | |
| 686 | + return llama3Tokenizer; | |
| 687 | + } | |
| 688 | + else if (oai_settings.electronhub_model.includes('llama')) { | |
| 689 | + return llamaTokenizer; | |
| 690 | + } | |
| 691 | + else if (oai_settings.electronhub_model.includes('command-a')) { | |
| 692 | + return commandATokenizer; | |
| 693 | + } | |
| 694 | + else if (oai_settings.electronhub_model.includes('command-r')) { | |
| 695 | + return commandRTokenizer; | |
| 696 | + } | |
| 697 | + else if (oai_settings.electronhub_model.includes('nemo')) { | |
| 698 | + return nemoTokenizer; | |
| 699 | + } | |
| 700 | + } | |
| 701 | + | |
| 651 | 702 | if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) { |
| 652 | 703 | if (oai_settings.cohere_model.includes('command-a')) { |
| 653 | 704 | return commandATokenizer; |
| @@ -526,12 +526,13 @@ export class ToolManager { | ||
| 526 | 526 | for (let choiceIndex = 0; choiceIndex < parsed.candidates.length; choiceIndex++) { |
| 527 | 527 | const candidate = parsed.candidates[choiceIndex]; |
| 528 | 528 | if (Array.isArray(candidate?.content?.parts)) { |
| 529 | 529 | for (let toolCallIndexpartIndex = 0; toolCallIndexpartIndex < candidate.content.parts.length; toolCallIndexpartIndex++) { |
| 530 | 530 | const part = candidate.content.parts[toolCallIndexpartIndex]; |
| 531 | 531 | if (part.functionCall) { |
| 532 | 532 | if (!Array.isArray(toolCalls[choiceIndex])) { |
| 533 | 533 | toolCalls[choiceIndex] = []; |
| 534 | 534 | } |
| 535 | + const toolCallIndex = toolCalls[choiceIndex].length; | |
| 535 | 536 | if (toolCalls[choiceIndex][toolCallIndex] === undefined) { |
| 536 | 537 | toolCalls[choiceIndex][toolCallIndex] = {}; |
| 537 | 538 | } |
| @@ -0,0 +1,69 @@ | ||
| 1 | +import { morphdom } from '../../lib.js'; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * Check if the current browser supports native segmentation function. | |
| 5 | + * @returns {boolean} True if the Segmenter is supported by the current browser. | |
| 6 | + */ | |
| 7 | +export function isSegmenterSupported() { | |
| 8 | + return typeof Intl.Segmenter === 'function'; | |
| 9 | +} | |
| 10 | + | |
| 11 | +/** | |
| 12 | + * Segment text in the given HTML content using Intl.Segmenter. | |
| 13 | + * @param {HTMLElement} htmlElement Target HTML element | |
| 14 | + * @param {string} htmlContent HTML content to segment | |
| 15 | + * @param {'word'|'grapheme'|'sentence'} [granularity='word'] Text split granularity | |
| 16 | + */ | |
| 17 | +export function segmentTextInElement(htmlElement, htmlContent, granularity = 'word') { | |
| 18 | + htmlElement.innerHTML = htmlContent; | |
| 19 | + | |
| 20 | + if (!isSegmenterSupported()) { | |
| 21 | + return; | |
| 22 | + } | |
| 23 | + | |
| 24 | + // TODO: Support more locales, make granularity configurable. | |
| 25 | + const segmenter = new Intl.Segmenter('en-US', { granularity }); | |
| 26 | + const textNodes = []; | |
| 27 | + const walker = document.createTreeWalker(htmlElement, NodeFilter.SHOW_TEXT); | |
| 28 | + while (walker.nextNode()) { | |
| 29 | + const textNode = /** @type {Text} */ (walker.currentNode); | |
| 30 | + | |
| 31 | + // Skip ancestors of code/pre | |
| 32 | + if (textNode.parentElement && textNode.parentElement.closest('pre, code')) { | |
| 33 | + continue; | |
| 34 | + } | |
| 35 | + | |
| 36 | + // Skip text nodes that are empty or only whitespace | |
| 37 | + if (/^\s*$/.test(textNode.data)) { | |
| 38 | + continue; | |
| 39 | + } | |
| 40 | + | |
| 41 | + textNodes.push(textNode); | |
| 42 | + } | |
| 43 | + | |
| 44 | + // Split every text node into segments using spans | |
| 45 | + for (const textNode of textNodes) { | |
| 46 | + const fragment = document.createDocumentFragment(); | |
| 47 | + const segments = segmenter.segment(textNode.data); | |
| 48 | + for (const segment of segments) { | |
| 49 | + // TODO: Apply a different class for different segment length/content? | |
| 50 | + // For now, just use a single class for all segments. | |
| 51 | + const span = document.createElement('span'); | |
| 52 | + span.innerText = segment.segment; | |
| 53 | + span.className = 'text_segment'; | |
| 54 | + fragment.appendChild(span); | |
| 55 | + } | |
| 56 | + textNode.replaceWith(fragment); | |
| 57 | + } | |
| 58 | +} | |
| 59 | + | |
| 60 | +/** | |
| 61 | + * Apply stream fade-in effect to the given message text element by morphing its content. | |
| 62 | + * @param {HTMLElement} messageTextElement Message text element | |
| 63 | + * @param {string} htmlContent New HTML content to apply | |
| 64 | + */ | |
| 65 | +export function applyStreamFadeIn(messageTextElement, htmlContent) { | |
| 66 | + const targetElement = /** @type {HTMLElement} */ (messageTextElement.cloneNode()); | |
| 67 | + segmentTextInElement(targetElement, htmlContent); | |
| 68 | + morphdom(messageTextElement, targetElement); | |
| 69 | +} | |
| @@ -3,10 +3,11 @@ import { | ||
| 3 | 3 | DOMPurify, |
| 4 | 4 | Readability, |
| 5 | 5 | isProbablyReaderable, |
| 6 | + lodash, | |
| 6 | 7 | } from '../lib.js'; |
| 7 | 8 | |
| 8 | 9 | import { getContext } from './extensions.js'; |
| 9 | 10 | import { characters, getRequestHeaders, processDroppedFiles, this_chid, user_avatar } from '../script.js'; |
| 10 | 11 | import { isMobile } from './RossAscends-mods.js'; |
| 11 | 12 | import { collapseNewlines, power_user } from './power-user.js'; |
| 12 | 13 | import { debounce_timeout } from './constants.js'; |
| @@ -15,6 +16,10 @@ import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; | ||
| 15 | 16 | import { getTagsList } from './tags.js'; |
| 16 | 17 | import { groups, selected_group } from './group-chats.js'; |
| 17 | 18 | import { getCurrentLocale, t } from './i18n.js'; |
| 19 | +import { importWorldInfo } from './world-info.js'; | |
| 20 | + | |
| 21 | +export const shiftUpByOne = (e, i, a) => a[i] = e + 1; | |
| 22 | +export const shiftDownByOne = (e, i, a) => a[i] = e - 1; | |
| 18 | 23 | |
| 19 | 24 | /** |
| 20 | 25 | * Pagination status string template. |
| @@ -25,6 +30,8 @@ export const PAGINATION_TEMPLATE = '<%= rangeStart %>-<%= rangeEnd %> .. <%= tot | ||
| 25 | 30 | export const localizePagination = function(container) { |
| 26 | 31 | container.find('[title="Next page"]').attr('title', t`Next page`); |
| 27 | 32 | container.find('[title="Previous page"]').attr('title', t`Previous page`); |
| 33 | + container.find('[title="First page"]').attr('title', t`First page`); | |
| 34 | + container.find('[title="Last page"]').attr('title', t`Last page`); | |
| 28 | 35 | }; |
| 29 | 36 | |
| 30 | 37 | /** |
| @@ -285,6 +292,15 @@ export function removeFromArray(array, item) { | ||
| 285 | 292 | } |
| 286 | 293 | |
| 287 | 294 | /** |
| 295 | + * Normalizes an array by removing duplicates, trimming strings, and filtering out empty values. | |
| 296 | + * @param {any[]} arr - The array to normalize. | |
| 297 | + * @returns {any[]} The normalized array. | |
| 298 | + */ | |
| 299 | +export function normalizeArray(arr) { | |
| 300 | + return [...new Set((arr ?? []).map(s => typeof s === 'string' ? s.trim() : s).filter(Boolean))]; | |
| 301 | +} | |
| 302 | + | |
| 303 | +/** | |
| 288 | 304 | * Checks if a string only contains digits. |
| 289 | 305 | * @param {string} str The string to check. |
| 290 | 306 | * @returns {boolean} True if the string only contains digits, false otherwise. |
| @@ -616,7 +632,7 @@ export function isElementInViewport(el) { | ||
| 616 | 632 | /** |
| 617 | 633 | * Returns a name that is unique among the names that exist. |
| 618 | 634 | * @param {string} name The name to check. |
| 619 | 635 | * @param {{ (yname: anystring): boolean; }} exists Function to check if name exists. |
| 620 | 636 | * @returns {string} A unique name. |
| 621 | 637 | */ |
| 622 | 638 | export function getUniqueName(name, exists) { |
| @@ -1633,13 +1649,18 @@ export function createThumbnail(dataUrl, maxWidth = null, maxHeight = null, type | ||
| 1633 | 1649 | * @param {{ (): boolean; }} condition The condition to wait for. |
| 1634 | 1650 | * @param {number} [timeout=1000] The timeout in milliseconds. |
| 1635 | 1651 | * @param {number} [interval=100] The interval in milliseconds. |
| 1652 | + * @param {object} [options] Options object | |
| 1653 | + * @param {boolean} [options.rejectOnTimeout=true] Whether to reject the promise on timeout or resolve it. | |
| 1636 | 1654 | * @returns {Promise<void>} A promise that resolves when the condition is true. |
| 1637 | 1655 | */ |
| 1638 | 1656 | export async function waitUntilCondition(condition, timeout = 1000, interval = 100, options = {}) { |
| 1657 | + const { rejectOnTimeout = true } = options; | |
| 1658 | + | |
| 1639 | 1659 | return new Promise((resolve, reject) => { |
| 1640 | 1660 | const timeoutId = setTimeout(() => { |
| 1641 | 1661 | clearInterval(intervalId); |
| 1642 | - reject(new Error('Timed out waiting for condition to be true')); | |
| 1662 | + const timeoutFn = rejectOnTimeout ? reject : resolve; | |
| 1663 | + timeoutFn(new Error('Timed out waiting for condition to be true')); | |
| 1643 | 1664 | }, timeout); |
| 1644 | 1665 | |
| 1645 | 1666 | const intervalId = setInterval(() => { |
| @@ -2560,3 +2581,110 @@ export function textValueMatcher(params, data) { | ||
| 2560 | 2581 | export function versionCompare(srcVersion, minVersion) { |
| 2561 | 2582 | return (srcVersion || '0.0.0').localeCompare(minVersion, undefined, { numeric: true, sensitivity: 'base' }) > -1; |
| 2562 | 2583 | } |
| 2584 | + | |
| 2585 | +/** | |
| 2586 | + * Sets up the scroll-to-top button functionality. | |
| 2587 | + * @param {object} params Parameters object | |
| 2588 | + * @param {string} params.scrollContainerId Scrollable container element ID | |
| 2589 | + * @param {string} params.buttonId Button element ID | |
| 2590 | + * @param {string} params.drawerId Drawer element ID | |
| 2591 | + * @param {number} [params.visibilityThreshold] Scroll position (px) to show the button (default: 300) | |
| 2592 | + * @returns {() => void} Cleanup function to remove event listeners | |
| 2593 | + */ | |
| 2594 | +export function setupScrollToTop({ scrollContainerId, buttonId, drawerId, visibilityThreshold = 300 }) { | |
| 2595 | + const scrollContainer = document.getElementById(scrollContainerId); | |
| 2596 | + const btn = document.getElementById(buttonId); | |
| 2597 | + const drawer = document.getElementById(drawerId); | |
| 2598 | + | |
| 2599 | + if (!btn || !drawer) { | |
| 2600 | + // Not fatal; the drawer or button may not exist in some builds. Use debug level. | |
| 2601 | + console.debug('Scroll-to-top: button or drawer not found during setup.'); | |
| 2602 | + return () => { /* noop cleanup */ }; | |
| 2603 | + } | |
| 2604 | + | |
| 2605 | + if (!scrollContainer) { | |
| 2606 | + console.debug('Scroll-to-top: scroll container not found during setup.'); | |
| 2607 | + return () => { /* noop cleanup */ }; | |
| 2608 | + } | |
| 2609 | + | |
| 2610 | + const updateButtonVisibility = () => btn.classList.toggle('visible', scrollContainer.scrollTop > visibilityThreshold); | |
| 2611 | + const updateButtonVisibilityThrottled = lodash.throttle(updateButtonVisibility, debounce_timeout.standard, { leading: true, trailing: true }); | |
| 2612 | + const onScroll = () => updateButtonVisibilityThrottled(); | |
| 2613 | + scrollContainer.addEventListener('scroll', onScroll, { passive: true }); | |
| 2614 | + | |
| 2615 | + // Scroll to top on click (button semantics provide keyboard activation natively) | |
| 2616 | + const onActivate = (/** @type {MouseEvent} */ e) => { | |
| 2617 | + e.preventDefault(); | |
| 2618 | + e.stopPropagation(); | |
| 2619 | + | |
| 2620 | + const userPrefersReduced = power_user.reduced_motion; | |
| 2621 | + scrollContainer.scrollTo({ top: 0, behavior: userPrefersReduced ? 'auto' : 'smooth' }); | |
| 2622 | + }; | |
| 2623 | + btn.addEventListener('click', onActivate); | |
| 2624 | + | |
| 2625 | + // Initial state check | |
| 2626 | + updateButtonVisibility(); | |
| 2627 | + | |
| 2628 | + // Return cleanup function for caller to hold and invoke when appropriate | |
| 2629 | + return () => { | |
| 2630 | + scrollContainer.removeEventListener('scroll', onScroll); | |
| 2631 | + btn.removeEventListener('click', onActivate); | |
| 2632 | + }; | |
| 2633 | +} | |
| 2634 | + | |
| 2635 | +/** | |
| 2636 | + * Imports content from an external URL. | |
| 2637 | + * @param {string} url URL or UUID of the content to import. | |
| 2638 | + * @param {Object} [options={}] Options object. | |
| 2639 | + * @param {string|null} [options.preserveFileName=null] Optional file name to use for the imported content. | |
| 2640 | + * @returns {Promise<void>} A promise that resolves when the import is complete. | |
| 2641 | + */ | |
| 2642 | +export async function importFromExternalUrl(url, { preserveFileName = null } = {}) { | |
| 2643 | + let request; | |
| 2644 | + | |
| 2645 | + if (isValidUrl(url)) { | |
| 2646 | + console.debug('Custom content import started for URL: ', url); | |
| 2647 | + request = await fetch('/api/content/importURL', { | |
| 2648 | + method: 'POST', | |
| 2649 | + headers: getRequestHeaders(), | |
| 2650 | + body: JSON.stringify({ url }), | |
| 2651 | + }); | |
| 2652 | + } else { | |
| 2653 | + console.debug('Custom content import started for Char UUID: ', url); | |
| 2654 | + request = await fetch('/api/content/importUUID', { | |
| 2655 | + method: 'POST', | |
| 2656 | + headers: getRequestHeaders(), | |
| 2657 | + body: JSON.stringify({ url }), | |
| 2658 | + }); | |
| 2659 | + } | |
| 2660 | + | |
| 2661 | + if (!request.ok) { | |
| 2662 | + toastr.info(request.statusText, 'Custom content import failed'); | |
| 2663 | + console.error('Custom content import failed', request.status, request.statusText); | |
| 2664 | + return; | |
| 2665 | + } | |
| 2666 | + | |
| 2667 | + const data = await request.blob(); | |
| 2668 | + const customContentType = request.headers.get('X-Custom-Content-Type'); | |
| 2669 | + let fileName = request.headers.get('Content-Disposition').split('filename=')[1].replace(/"/g, ''); | |
| 2670 | + const file = new File([data], fileName, { type: data.type }); | |
| 2671 | + | |
| 2672 | + const extraData = new Map(); | |
| 2673 | + if (preserveFileName) { | |
| 2674 | + fileName = preserveFileName; | |
| 2675 | + extraData.set(file, preserveFileName); | |
| 2676 | + } | |
| 2677 | + | |
| 2678 | + switch (customContentType) { | |
| 2679 | + case 'character': | |
| 2680 | + await processDroppedFiles([file], extraData); | |
| 2681 | + break; | |
| 2682 | + case 'lorebook': | |
| 2683 | + await importWorldInfo(file); | |
| 2684 | + break; | |
| 2685 | + default: | |
| 2686 | + toastr.warning('Unknown content type'); | |
| 2687 | + console.error('Unknown content type', customContentType); | |
| 2688 | + break; | |
| 2689 | + } | |
| 2690 | +} | |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import { Fuse } from '../lib.js'; |
| 2 | 2 | |
| 3 | 3 | import { saveSettings, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles, create_save, createOrEditCharacter, name1 } from '../script.js'; |
| 4 | 4 | import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean, getSanitizedFilename, checkOverwriteExistingData, getStringHash, parseStringArray, cancelDebounce, findChar, onlyUnique, equalsIgnoreCaseAndAccents, uuidv4, normalizeArray, getUniqueName } from './utils.js'; |
| 5 | 5 | import { extension_settings, getContext } from './extensions.js'; |
| 6 | 6 | import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js'; |
| 7 | 7 | import { isMobile } from './RossAscends-mods.js'; |
| @@ -22,6 +22,7 @@ import { StructuredCloneMap } from './util/StructuredCloneMap.js'; | ||
| 22 | 22 | import { renderTemplateAsync } from './templates.js'; |
| 23 | 23 | import { t } from './i18n.js'; |
| 24 | 24 | import { accountStorage } from './util/AccountStorage.js'; |
| 25 | +import { getOrCreatePersonaDescriptor, setPersonaDescription, user_avatar } from './personas.js'; | |
| 25 | 26 | |
| 26 | 27 | export const world_info_insertion_strategy = { |
| 27 | 28 | evenly: 0, |
| @@ -156,6 +157,7 @@ const KNOWN_DECORATORS = ['@@activate', '@@dont_activate']; | ||
| 156 | 157 | * @property {Array} worldInfoDepth - Array of depth entries |
| 157 | 158 | * @property {Array} anBefore - Array of entries before Author's Note |
| 158 | 159 | * @property {Array} anAfter - Array of entries after Author's Note |
| 160 | + * @property {{[key: string]: string[]}} outletEntries - Array of entries to be added to an outlet | |
| 159 | 161 | */ |
| 160 | 162 | |
| 161 | 163 | /** |
| @@ -166,6 +168,7 @@ const KNOWN_DECORATORS = ['@@activate', '@@dont_activate']; | ||
| 166 | 168 | * @property {any[]} WIDepthEntries The depth entries. |
| 167 | 169 | * @property {any[]} ANBeforeEntries The entries before Author's Note. |
| 168 | 170 | * @property {any[]} ANAfterEntries The entries after Author's Note. |
| 171 | + * @property {{[key: string]: string[]}} outletEntries - Array of entries to be added to an outlet | |
| 169 | 172 | * @property {Set<any>} allActivatedEntries All entries. |
| 170 | 173 | */ |
| 171 | 174 | |
| @@ -817,6 +820,7 @@ export const world_info_position = { | ||
| 817 | 820 | atDepth: 4, |
| 818 | 821 | EMTop: 5, |
| 819 | 822 | EMBottom: 6, |
| 823 | + outlet: 7, | |
| 820 | 824 | }; |
| 821 | 825 | |
| 822 | 826 | export const wi_anchor_position = { |
| @@ -866,6 +870,7 @@ export async function getWorldInfoPrompt(chat, maxContext, isDryRun, globalScanD | ||
| 866 | 870 | worldInfoDepth: activatedWorldInfo.WIDepthEntries ?? [], |
| 867 | 871 | anBefore: activatedWorldInfo.ANBeforeEntries ?? [], |
| 868 | 872 | anAfter: activatedWorldInfo.ANAfterEntries ?? [], |
| 873 | + outletEntries: activatedWorldInfo.outletEntries ?? {}, | |
| 869 | 874 | }; |
| 870 | 875 | } |
| 871 | 876 | |
| @@ -1037,39 +1042,69 @@ function registerWorldInfoSlashCommands() { | ||
| 1037 | 1042 | |
| 1038 | 1043 | /** |
| 1039 | 1044 | * Gets the name of the persona-bound lorebook. |
| 1040 | - * @returns {string} The name of the persona-bound lorebook | |
| 1045 | + * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments | |
| 1046 | + * @param {string} _unnamedArg not used | |
| 1047 | + * @returns {Promise<string>} The name of the persona-bound lorebook | |
| 1041 | 1048 | */ |
| 1042 | 1049 | async function getPersonaBookCallback({ name, create }, _unnamedArg) { |
| 1043 | 1050 | returnlet bookName = power_user.persona_description_lorebook || ''; |
| 1051 | + if (bookName) { | |
| 1052 | + return bookName; | |
| 1053 | + } | |
| 1054 | + | |
| 1055 | + if (isTrueBoolean(String(create))) { | |
| 1056 | + const newName = await createWorldWithName(name, `Persona Book ${name1}`.replace(/[^a-z0-9 -]/gi, '_').replace(/_{2,}/g, '_').substring(0, 64)); | |
| 1057 | + power_user.persona_description_lorebook = newName; | |
| 1058 | + setPersonaDescription(); | |
| 1059 | + saveSettingsDebounced(); | |
| 1060 | + return newName; | |
| 1061 | + } | |
| 1062 | + | |
| 1063 | + return ''; | |
| 1044 | 1064 | } |
| 1045 | 1065 | |
| 1046 | 1066 | /** |
| 1047 | 1067 | * Gets the name of the character-bound lorebook. |
| 1048 | 1068 | * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments |
| 1049 | 1069 | * @param {string} namecharacterIdentifier Character name |
| 1050 | 1070 | * @returns {Promise<string>} The name of the character-bound lorebook, a JSON string of the character's lorebooks, or an empty string |
| 1051 | 1071 | */ |
| 1052 | 1072 | async function getCharBookCallback({ type, name, create }, namecharacterIdentifier) { |
| 1053 | 1073 | const context = getContext(); |
| 1054 | 1074 | if (context.groupId && !namecharacterIdentifier) throw new Error('This command is not available in groups without providing a character name'); |
| 1055 | 1075 | type = String(type ?? '').trim().toLowerCase() || 'primary'; |
| 1056 | 1076 | namecharacterIdentifier = String(namecharacterIdentifier ?? '') || context.characters[context.characterId]?.avatar || null; |
| 1057 | 1077 | const character = findChar({ name: characterIdentifier }); |
| 1058 | 1078 | if (!character) { |
| 1059 | 1079 | toastr.error(t`Character not found.`); |
| 1060 | 1080 | return ''; |
| 1061 | 1081 | } |
| 1062 | 1082 | const books = []; |
| 1063 | 1083 | if (type === 'all' || type === 'primary' && character.data?.extensions?.world) { |
| 1064 | 1084 | books.push(character.data?.extensions?.world); |
| 1065 | 1085 | } |
| 1066 | 1086 | if (type === 'all' || type === 'additional') { |
| 1067 | 1087 | const fileName = getCharaFilename(context.characters.indexOf(character)); |
| 1068 | 1088 | const extraCharLore = world_info.charLore?.find((e) => e.name === fileName); |
| 1069 | 1089 | if (extraCharLore && Array.isArray(extraCharLore.extraBooks)) { |
| 1070 | 1090 | books.push(...extraCharLore.extraBooks.filter(onlyUnique).filter(Boolean)); |
| 1071 | 1091 | } |
| 1072 | 1092 | } |
| 1093 | + | |
| 1094 | + if (isTrueBoolean(String(create)) && books.length === 0) { | |
| 1095 | + const newName = await createWorldWithName(name, `Character Book ${character.name}`.replace(/[^a-z0-9 -]/gi, '_').replace(/_{2,}/g, '_').substring(0, 64)); | |
| 1096 | + // Also assign the book now - additional if requested, otherwise as primary | |
| 1097 | + if (type === 'additional') { | |
| 1098 | + await charUpdateAddAuxWorld(character.avatar, newName); | |
| 1099 | + } | |
| 1100 | + else { | |
| 1101 | + await charUpdatePrimaryWorld(newName); | |
| 1102 | + } | |
| 1103 | + // Refresh UI, if needed | |
| 1104 | + setWorldInfoButtonClass(this_chid); | |
| 1105 | + books.push(newName); | |
| 1106 | + } | |
| 1107 | + | |
| 1073 | 1108 | return type === 'primary' ? (books[0] ?? '') : JSON.stringify(books.filter(onlyUnique).filter(Boolean)); |
| 1074 | 1109 | } |
| 1075 | 1110 | |
| @@ -1090,10 +1125,23 @@ function registerWorldInfoSlashCommands() { | ||
| 1090 | 1125 | return chat_metadata[METADATA_KEY]; |
| 1091 | 1126 | } |
| 1092 | 1127 | |
| 1093 | - const name = (() => { | |
| 1128 | + if (isFalseBoolean(String(args.create))) { | |
| 1129 | + return ''; | |
| 1130 | + } | |
| 1131 | + | |
| 1132 | + const name = await createWorldWithName(args.name, `Chat Book ${getCurrentChatId()}`.replace(/[^a-z0-9 -]/gi, '_').replace(/_{2,}/g, '_').substring(0, 64)); | |
| 1133 | + | |
| 1134 | + chat_metadata[METADATA_KEY] = name; | |
| 1135 | + await saveMetadata(); | |
| 1136 | + $('.chat_lorebook_button').addClass('world_set'); | |
| 1137 | + return name; | |
| 1138 | + } | |
| 1139 | + | |
| 1140 | + async function createWorldWithName(possibleName = undefined, fallbackName = undefined) { | |
| 1141 | + let newName = (() => { | |
| 1094 | 1142 | // Use the provided name if it's not in use |
| 1095 | 1143 | if (typeof args.namepossibleName === 'string') { |
| 1096 | 1144 | const name = String(args.namepossibleName); |
| 1097 | 1145 | if (world_names.includes(name)) { |
| 1098 | 1146 | throw new Error('This World Info file name is already in use'); |
| 1099 | 1147 | } |
| @@ -1101,14 +1149,14 @@ function registerWorldInfoSlashCommands() { | ||
| 1101 | 1149 | } |
| 1102 | 1150 | |
| 1103 | 1151 | // Replace non-alphanumeric characters with underscores, cut to 64 characters |
| 1104 | - return `Chat Book ${getCurrentChatId()}`.replace(/[^a-z0-9]/gi, '_').replace(/_{2,}/g, '_').substring(0, 64); | |
| 1152 | + return fallbackName ?? `Lorebook (${uuidv4()})`; | |
| 1105 | 1153 | })(); |
| 1106 | - await createNewWorldInfo(name); | |
| 1107 | 1154 | |
| 1108 | - chat_metadata[METADATA_KEY] = name; | |
| 1155 | + // Make sure the name is unique | |
| 1109 | - await saveMetadata(); | |
| 1156 | + newName = getUniqueName(newName, world_names.includes.bind(world_names)); | |
| 1110 | - $('.chat_lorebook_button').addClass('world_set'); | |
| 1157 | + | |
| 1111 | - return name; | |
| 1158 | + await createNewWorldInfo(newName); | |
| 1159 | + return newName; | |
| 1112 | 1160 | } |
| 1113 | 1161 | |
| 1114 | 1162 | async function findBookEntryCallback(args, value) { |
| @@ -1545,6 +1593,15 @@ function registerWorldInfoSlashCommands() { | ||
| 1545 | 1593 | isRequired: false, |
| 1546 | 1594 | acceptsMultiple: false, |
| 1547 | 1595 | }), |
| 1596 | + SlashCommandNamedArgument.fromProps({ | |
| 1597 | + name: 'create', | |
| 1598 | + description: 'create a new lorebook if it doesn\'t exist', | |
| 1599 | + typeList: [ARGUMENT_TYPE.BOOLEAN], | |
| 1600 | + isRequired: false, | |
| 1601 | + acceptsMultiple: false, | |
| 1602 | + enumList: commonEnumProviders.boolean('trueFalse')(), | |
| 1603 | + defaultValue: 'true', | |
| 1604 | + }), | |
| 1548 | 1605 | ], |
| 1549 | 1606 | aliases: ['getchatlore', 'getchatwi'], |
| 1550 | 1607 | })); |
| @@ -1559,6 +1616,25 @@ function registerWorldInfoSlashCommands() { | ||
| 1559 | 1616 | name: 'getpersonabook', |
| 1560 | 1617 | callback: getPersonaBookCallback, |
| 1561 | 1618 | returns: 'lorebook name', |
| 1619 | + | |
| 1620 | + namedArgumentList: [ | |
| 1621 | + SlashCommandNamedArgument.fromProps({ | |
| 1622 | + name: 'name', | |
| 1623 | + description: 'lorebook name if creating a new one, will be auto-generated otherwise', | |
| 1624 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 1625 | + isRequired: false, | |
| 1626 | + acceptsMultiple: false, | |
| 1627 | + }), | |
| 1628 | + SlashCommandNamedArgument.fromProps({ | |
| 1629 | + name: 'create', | |
| 1630 | + description: 'create a new lorebook if it doesn\'t exist', | |
| 1631 | + typeList: [ARGUMENT_TYPE.BOOLEAN], | |
| 1632 | + isRequired: false, | |
| 1633 | + acceptsMultiple: false, | |
| 1634 | + enumList: commonEnumProviders.boolean('trueFalse')(), | |
| 1635 | + defaultValue: 'false', | |
| 1636 | + }), | |
| 1637 | + ], | |
| 1562 | 1638 | helpString: 'Get a name of the current persona-bound lorebook and pass it down the pipe. Returns empty string if persona lorebook is not set.', |
| 1563 | 1639 | aliases: ['getpersonalore', 'getpersonawi'], |
| 1564 | 1640 | })); |
| @@ -1574,6 +1650,22 @@ function registerWorldInfoSlashCommands() { | ||
| 1574 | 1650 | enumList: ['primary', 'additional', 'all'], |
| 1575 | 1651 | defaultValue: 'primary', |
| 1576 | 1652 | }), |
| 1653 | + SlashCommandNamedArgument.fromProps({ | |
| 1654 | + name: 'name', | |
| 1655 | + description: 'lorebook name if creating a new one, will be auto-generated otherwise', | |
| 1656 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 1657 | + isRequired: false, | |
| 1658 | + acceptsMultiple: false, | |
| 1659 | + }), | |
| 1660 | + SlashCommandNamedArgument.fromProps({ | |
| 1661 | + name: 'create', | |
| 1662 | + description: 'create a new lorebook if it doesn\'t exist', | |
| 1663 | + typeList: [ARGUMENT_TYPE.BOOLEAN], | |
| 1664 | + isRequired: false, | |
| 1665 | + acceptsMultiple: false, | |
| 1666 | + enumList: commonEnumProviders.boolean('trueFalse')(), | |
| 1667 | + defaultValue: 'false', | |
| 1668 | + }), | |
| 1577 | 1669 | ], |
| 1578 | 1670 | unnamedArgumentList: [ |
| 1579 | 1671 | SlashCommandArgument.fromProps({ |
| @@ -3425,6 +3517,19 @@ export async function getWorldEntry(name, data, entry) { | ||
| 3425 | 3517 | contentInput.val(entry.content).trigger('input', { skipCount: true, noSave: true }); |
| 3426 | 3518 | editTemplate.find('.editor_maximize').attr('data-for', contentInputId); |
| 3427 | 3519 | |
| 3520 | + // Outlet name | |
| 3521 | + const outletNameInput = editTemplate.find('input[name="outletName"]'); | |
| 3522 | + outletNameInput.data('uid', entry.uid); | |
| 3523 | + outletNameInput.on('input', async function (_, { noSave = false } = {}) { | |
| 3524 | + const uid = $(this).data('uid'); | |
| 3525 | + const value = $(this).val(); | |
| 3526 | + data.entries[uid].outletName = value; | |
| 3527 | + setWIOriginalDataValue(data, uid, 'extensions.outlet_name', data.entries[uid].outletName); | |
| 3528 | + !noSave && await saveWorldInfo(name, data); | |
| 3529 | + }); | |
| 3530 | + outletNameInput.val(entry.outletName ?? '').trigger('input', { noSave: true }); | |
| 3531 | + setTimeout(() => createEntryInputAutocomplete(outletNameInput, getOutletNameCallback(data), { allowMultiple: true }), 1); | |
| 3532 | + | |
| 3428 | 3533 | // Scan depth |
| 3429 | 3534 | const scanDepthInput = editTemplate.find('input[name="scanDepth"]'); |
| 3430 | 3535 | scanDepthInput.data('uid', entry.uid); |
| @@ -3597,63 +3702,105 @@ export async function getWorldEntry(name, data, entry) { | ||
| 3597 | 3702 | return headerTemplate; |
| 3598 | 3703 | } |
| 3599 | 3704 | |
| 3705 | + | |
| 3600 | 3706 | /** |
| 3601 | - * Get the inclusion groups for the autocomplete. | |
| 3707 | + * Builds a jQuery UI autocomplete callback: (control, request, response) => void | |
| 3602 | 3708 | * @param {anyobject} data[opt={}] WI- dataOptional arguments |
| 3603 | - * @returns {(input: any, output: any) => any} Callback function for the autocomplete | |
| 3709 | + * @param {{entries: Record<string, any>}} [opt.data] - Your WI data | |
| 3710 | + * @param {(entry:any)=>string|string[]|null|undefined} [opt.collectValues] - Extract values from one entry | |
| 3711 | + * @param {() => Iterable<string>} [opt.includeExtras] - Optional global extras to include | |
| 3712 | + * @param {(ctx:{result:string[], control:JQuery, input:any, haystack:string[]})=>string[]} [opt.postFilter] - Optional final filter step (for special rules like your "group" de-dupe logic) | |
| 3604 | 3713 | */ |
| 3605 | -function getInclusionGroupCallback(data) { | |
| 3714 | +function buildAutocompleteCallback({ data, collectValues, includeExtras = () => [], postFilter } = {}) { | |
| 3606 | 3715 | return function (control, input, output) { |
| 3607 | 3716 | const uid = $(control).data('uid'); |
| 3608 | - const thisGroups = String($(control).val()).split(/,\s*/).filter(x => x).map(x => x.toLowerCase()); | |
| 3717 | + | |
| 3609 | - const groups = new Set(); | |
| 3718 | + // Collect unique values from all *other* entries | |
| 3610 | - for (const entry of Object.values(data.entries)) { | |
| 3719 | + const values = new Set(); | |
| 3611 | - // Skip the groups of this entry, because auto-complete should only suggest the ones that are already available on other entries | |
| 3720 | + for (const entry of Object.values(data.entries ?? {})) { | |
| 3612 | 3721 | if (entry?.uid == uid) continue; |
| 3613 | - if (entry.group) { | |
| 3722 | + const raw = collectValues(entry); | |
| 3614 | - entry.group.split(/,\s*/).filter(x => x).forEach(x => groups.add(x)); | |
| 3723 | + if (raw == null) continue; | |
| 3724 | + const arr = Array.isArray(raw) ? raw : [raw]; | |
| 3725 | + for (const v of arr) { | |
| 3726 | + const s = String(v).trim(); | |
| 3727 | + if (s) values.add(s); | |
| 3615 | 3728 | } |
| 3616 | 3729 | } |
| 3617 | 3730 | |
| 3618 | - const haystack = Array.from(groups); | |
| 3731 | + // Add optional global extras | |
| 3619 | - haystack.sort((a, b) => a.localeCompare(b)); | |
| 3732 | + for (const v of includeExtras()) { | |
| 3620 | 3733 | const needles = input.termString(v).toLowerCasetrim(); |
| 3621 | - const hasExactMatch = haystack.findIndex(x => x.toLowerCase() == needle) !== -1; | |
| 3734 | + if (s) values.add(s); | |
| 3622 | - const result = haystack.filter(x => x.toLowerCase().includes(needle) && (!thisGroups.includes(x) || hasExactMatch && thisGroups.filter(g => g == x).length == 1)); | |
| 3735 | + } | |
| 3736 | + | |
| 3737 | + // Sort stable & locale-aware | |
| 3738 | + const haystack = Array.from(values).sort((a, b) => a.localeCompare(b)); | |
| 3739 | + | |
| 3740 | + // Case-insensitive contains | |
| 3741 | + const needle = String(input.term ?? '').toLowerCase(); | |
| 3742 | + let result = haystack.filter(x => x.toLowerCase().includes(needle)); | |
| 3743 | + | |
| 3744 | + // Optional final-pass semantics | |
| 3745 | + if (postFilter) { | |
| 3746 | + result = postFilter({ result, control: $(control), input, haystack }); | |
| 3747 | + } | |
| 3623 | 3748 | |
| 3624 | 3749 | output(result); |
| 3625 | 3750 | }; |
| 3626 | 3751 | } |
| 3627 | 3752 | |
| 3628 | -function getAutomationIdCallback(data) { | |
| 3753 | +/** | |
| 3629 | - return function (control, input, output) { | |
| 3754 | + * Splits a string into an array of strings, separated by commas and trimmed | |
| 3630 | - const uid = $(control).data('uid'); | |
| 3755 | + * @param {string} s - The string to split | |
| 3631 | - const ids = new Set(); | |
| 3756 | + * @returns {string[]} An array of strings, separated by commas and trimmed | |
| 3632 | - for (const entry of Object.values(data.entries)) { | |
| 3757 | + */ | |
| 3633 | - // Skip automation id of this entry, because auto-complete should only suggest the ones that are already available on other entries | |
| 3758 | +const splitCsv = s => String(s ?? '').split(/,\s*/).filter(Boolean); | |
| 3634 | - if (entry.uid == uid) continue; | |
| 3635 | - if (entry.automationId) { | |
| 3636 | - ids.add(String(entry.automationId)); | |
| 3637 | - } | |
| 3638 | - } | |
| 3639 | 3759 | |
| 3640 | - if ('quickReplyApi' in globalThis) { | |
| 3760 | +/** | |
| 3641 | - for (const automationId of globalThis.quickReplyApi.listAutomationIds()) { | |
| 3761 | + * Get the inclusion groups for the autocomplete. | |
| 3642 | - ids.add(String(automationId)); | |
| 3762 | + * @param {any} data WI data | |
| 3643 | - } | |
| 3763 | + * @returns {(input: any, output: any) => any} Callback function for the autocomplete | |
| 3644 | - } | |
| 3764 | + */ | |
| 3765 | +function getInclusionGroupCallback(data) { | |
| 3766 | + return buildAutocompleteCallback({ | |
| 3767 | + data, | |
| 3768 | + collectValues: entry => entry.group ? splitCsv(entry.group) : [], | |
| 3769 | + postFilter: ({ result, control, input, haystack }) => { | |
| 3770 | + const thisGroups = splitCsv(String($(control).val())); | |
| 3771 | + const needle = String(input.term ?? '').toLowerCase(); | |
| 3772 | + const hasExactMatch = haystack.some(x => x.toLowerCase() === needle); | |
| 3773 | + | |
| 3774 | + // include suggestion if it contains the needle AND | |
| 3775 | + // (not already present OR (exact match typed && appears only once)) | |
| 3776 | + return result.filter(x => | |
| 3777 | + !thisGroups.includes(x) || | |
| 3778 | + (hasExactMatch && thisGroups.filter(g => g === x).length === 1), | |
| 3779 | + ); | |
| 3780 | + }, | |
| 3781 | + }); | |
| 3782 | +} | |
| 3645 | 3783 | |
| 3646 | - const haystack = Array.from(ids); | |
| 3784 | +function getAutomationIdCallback(data) { | |
| 3647 | - haystack.sort((a, b) => a.localeCompare(b)); | |
| 3785 | + return buildAutocompleteCallback({ | |
| 3648 | - const needle = input.term.toLowerCase(); | |
| 3786 | + data, | |
| 3649 | - const result = haystack.filter(x => x.toLowerCase().includes(needle)); | |
| 3787 | + collectValues: entry => entry.automationId != null ? [String(entry.automationId)] : [], | |
| 3788 | + includeExtras: () => | |
| 3789 | + ('quickReplyApi' in globalThis && globalThis.quickReplyApi?.listAutomationIds) | |
| 3790 | + ? globalThis.quickReplyApi.listAutomationIds() | |
| 3791 | + : [], | |
| 3792 | + }); | |
| 3793 | +} | |
| 3650 | 3794 | |
| 3651 | - output(result); | |
| 3795 | +function getOutletNameCallback(data) { | |
| 3652 | - }; | |
| 3796 | + return buildAutocompleteCallback({ | |
| 3797 | + data, | |
| 3798 | + collectValues: entry => entry.position === world_info_position.outlet && entry.outletName ? [entry.outletName] : [], | |
| 3799 | + }); | |
| 3653 | 3800 | } |
| 3654 | 3801 | |
| 3655 | 3802 | /** |
| 3656 | 3803 | * Create an autocomplete for thean inclusioninput groupelement. |
| 3657 | 3804 | * @param {JQuery<HTMLElement>} input - Input element to attach the autocomplete to |
| 3658 | 3805 | * @param {(control: JQuery<HTMLElement>, input: any, output: any) => any} callback - Source data callbacks |
| 3659 | 3806 | * @param {object} [options={}] - Optional arguments |
| @@ -3770,6 +3917,7 @@ export const newWorldInfoEntryDefinition = { | ||
| 3770 | 3917 | probability: { default: 100, type: 'number' }, |
| 3771 | 3918 | useProbability: { default: true, type: 'boolean' }, |
| 3772 | 3919 | depth: { default: DEFAULT_DEPTH, type: 'number' }, |
| 3920 | + outletName: { default: '', type: 'string' }, | |
| 3773 | 3921 | group: { default: '', type: 'string' }, |
| 3774 | 3922 | groupOverride: { default: false, type: 'boolean' }, |
| 3775 | 3923 | groupWeight: { default: DEFAULT_WEIGHT, type: 'number' }, |
| @@ -3935,6 +4083,16 @@ export async function deleteWorldInfo(worldInfoName) { | ||
| 3935 | 4083 | } |
| 3936 | 4084 | } |
| 3937 | 4085 | |
| 4086 | + if (power_user.persona_description_lorebook === worldInfoName) { | |
| 4087 | + power_user.persona_description_lorebook = ''; | |
| 4088 | + if (power_user.personas[user_avatar]) { | |
| 4089 | + const object = getOrCreatePersonaDescriptor(); | |
| 4090 | + object.lorebook = ''; | |
| 4091 | + } | |
| 4092 | + $('#persona_lore_button').toggleClass('world_set', false); | |
| 4093 | + saveSettingsDebounced(); | |
| 4094 | + } | |
| 4095 | + | |
| 3938 | 4096 | return true; |
| 3939 | 4097 | } |
| 3940 | 4098 | |
| @@ -4281,7 +4439,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData | ||
| 4281 | 4439 | timedEffects.checkTimedEffects(); |
| 4282 | 4440 | |
| 4283 | 4441 | if (sortedEntries.length === 0) { |
| 4284 | 4442 | return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], ANBeforeEntries: [], ANAfterEntries: [], outletEntries: {}, allActivatedEntries: new Set() }; |
| 4285 | 4443 | } |
| 4286 | 4444 | |
| 4287 | 4445 | /** @type {number[]} Represents the delay levels for entries that are delayed until recursion */ |
| @@ -4682,6 +4840,8 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData | ||
| 4682 | 4840 | const ANTopEntries = []; |
| 4683 | 4841 | const ANBottomEntries = []; |
| 4684 | 4842 | const WIDepthEntries = []; |
| 4843 | + /** @type {{[key: string]: string[]}} */ | |
| 4844 | + const WIOutletEntries = {}; | |
| 4685 | 4845 | |
| 4686 | 4846 | // Appends from insertion order 999 to 1. Use unshift for this purpose |
| 4687 | 4847 | // TODO (kingbri): Change to use WI Anchor positioning instead of separate top/bottom arrays |
| @@ -4730,6 +4890,18 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData | ||
| 4730 | 4890 | } |
| 4731 | 4891 | break; |
| 4732 | 4892 | } |
| 4893 | + case world_info_position.outlet: { | |
| 4894 | + if (!entry.outletName) { | |
| 4895 | + console.warn(`[WI] Entry ${entry.uid} has position 'outlet' but no outlet name. Skipping.`); | |
| 4896 | + break; | |
| 4897 | + } | |
| 4898 | + if (Array.isArray(WIOutletEntries[entry.outletName])) { | |
| 4899 | + WIOutletEntries[entry.outletName].push(content); | |
| 4900 | + } else { | |
| 4901 | + WIOutletEntries[entry.outletName] = [content]; | |
| 4902 | + } | |
| 4903 | + break; | |
| 4904 | + } | |
| 4733 | 4905 | default: |
| 4734 | 4906 | break; |
| 4735 | 4907 | } |
| @@ -4751,7 +4923,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData | ||
| 4751 | 4923 | console.log(`[WI] ${isDryRun ? 'Hypothetically adding' : 'Adding'} ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values())); |
| 4752 | 4924 | console.debug(`[WI] --- DONE${isDryRun ? ' (DRY RUN)' : ''} ---`); |
| 4753 | 4925 | |
| 4754 | 4926 | return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, ANBeforeEntries: ANTopEntries, ANAfterEntries: ANBottomEntries, outletEntries: WIOutletEntries, allActivatedEntries: new Set(allActivatedEntries.values()) }; |
| 4755 | 4927 | } |
| 4756 | 4928 | |
| 4757 | 4929 | /** |
| @@ -4971,6 +5143,7 @@ function convertAgnaiMemoryBook(inputObj) { | ||
| 4971 | 5143 | displayIndex: index, |
| 4972 | 5144 | probability: 100, |
| 4973 | 5145 | useProbability: true, |
| 5146 | + outletName: '', | |
| 4974 | 5147 | group: '', |
| 4975 | 5148 | groupOverride: false, |
| 4976 | 5149 | groupWeight: DEFAULT_WEIGHT, |
| @@ -5015,6 +5188,7 @@ function convertRisuLorebook(inputObj) { | ||
| 5015 | 5188 | displayIndex: index, |
| 5016 | 5189 | probability: entry.activationPercent ?? 100, |
| 5017 | 5190 | useProbability: entry.activationPercent ?? true, |
| 5191 | + outletName: '', | |
| 5018 | 5192 | group: '', |
| 5019 | 5193 | groupOverride: false, |
| 5020 | 5194 | groupWeight: DEFAULT_WEIGHT, |
| @@ -5064,6 +5238,7 @@ function convertNovelLorebook(inputObj) { | ||
| 5064 | 5238 | displayIndex: index, |
| 5065 | 5239 | probability: 100, |
| 5066 | 5240 | useProbability: true, |
| 5241 | + outletName: '', | |
| 5067 | 5242 | group: '', |
| 5068 | 5243 | groupOverride: false, |
| 5069 | 5244 | groupWeight: DEFAULT_WEIGHT, |
| @@ -5114,6 +5289,7 @@ export function convertCharacterBook(characterBook) { | ||
| 5114 | 5289 | useProbability: entry.extensions?.useProbability ?? true, |
| 5115 | 5290 | depth: entry.extensions?.depth ?? DEFAULT_DEPTH, |
| 5116 | 5291 | selectiveLogic: entry.extensions?.selectiveLogic ?? world_info_logic.AND_ANY, |
| 5292 | + outletName: entry.extensions?.outlet_name ?? '', | |
| 5117 | 5293 | group: entry.extensions?.group ?? '', |
| 5118 | 5294 | groupOverride: entry.extensions?.group_override ?? false, |
| 5119 | 5295 | groupWeight: entry.extensions?.group_weight ?? DEFAULT_WEIGHT, |
| @@ -5558,6 +5734,90 @@ export async function moveWorldInfoEntry(sourceName, targetName, uid, { deleteOr | ||
| 5558 | 5734 | } |
| 5559 | 5735 | } |
| 5560 | 5736 | |
| 5737 | + | |
| 5738 | +/** | |
| 5739 | + * Updates the primary world info linked to a character. | |
| 5740 | + * Can also unset it to null. | |
| 5741 | + * @param {string} name - The name of the world info to link to the character. | |
| 5742 | + */ | |
| 5743 | +export async function charUpdatePrimaryWorld(name) { | |
| 5744 | + const previousValue = $('#character_world').val(); | |
| 5745 | + $('#character_world').val(name); | |
| 5746 | + | |
| 5747 | + console.debug('Character world selected:', name); | |
| 5748 | + | |
| 5749 | + if (menu_type == 'create') { | |
| 5750 | + create_save.world = name; | |
| 5751 | + return; | |
| 5752 | + } | |
| 5753 | + | |
| 5754 | + if (previousValue && !name) { | |
| 5755 | + try { | |
| 5756 | + // Dirty hack to remove embedded lorebook from character JSON data. | |
| 5757 | + const data = JSON.parse(String($('#character_json_data').val())); | |
| 5758 | + | |
| 5759 | + if (data?.data?.character_book) { | |
| 5760 | + data.data.character_book = undefined; | |
| 5761 | + } | |
| 5762 | + | |
| 5763 | + $('#character_json_data').val(JSON.stringify(data)); | |
| 5764 | + toastr.info(t`Embedded lorebook will be removed from this character.`); | |
| 5765 | + } catch { | |
| 5766 | + console.error('Failed to parse character JSON data.'); | |
| 5767 | + } | |
| 5768 | + } | |
| 5769 | + | |
| 5770 | + await createOrEditCharacter(); | |
| 5771 | + | |
| 5772 | + setWorldInfoButtonClass(undefined, !!name); | |
| 5773 | +} | |
| 5774 | + | |
| 5775 | +/** | |
| 5776 | + * Adds one or more auxiliary world books to a character. | |
| 5777 | + * @param {string} characterKey - The key of the character to add auxiliary world books to | |
| 5778 | + * @param {string|string[]} nameOrNames - The name or names of the auxiliary world books to add | |
| 5779 | + */ | |
| 5780 | +export async function charUpdateAddAuxWorld(characterKey, nameOrNames) { | |
| 5781 | + const fileName = getCharaFilename(null, { manualAvatarKey: characterKey }); | |
| 5782 | + const toAdd = Array.isArray(nameOrNames) ? nameOrNames : [nameOrNames]; | |
| 5783 | + updateAuxBooks(fileName, curr => [...curr, ...toAdd]); | |
| 5784 | +} | |
| 5785 | + | |
| 5786 | +/** | |
| 5787 | + * Replaces the entire list of auxiliary world books for a character. | |
| 5788 | + * @param {string} fileName - The filename of the character to update | |
| 5789 | + * @param {string[]} books - The new list of auxiliary world books to replace the existing list with | |
| 5790 | + */ | |
| 5791 | +export function charSetAuxWorlds(fileName, books) { | |
| 5792 | + updateAuxBooks(fileName, _ => Array.isArray(books) ? books : []); | |
| 5793 | +} | |
| 5794 | + | |
| 5795 | +function updateAuxBooks(fileName, computeNext) { | |
| 5796 | + if (!fileName) return; | |
| 5797 | + | |
| 5798 | + if (menu_type === 'create') { | |
| 5799 | + const current = create_save.extra_books ?? []; | |
| 5800 | + create_save.extra_books = normalizeArray(computeNext(current)); | |
| 5801 | + return; // no debounced save in create flow | |
| 5802 | + } | |
| 5803 | + | |
| 5804 | + const charLore = world_info.charLore ?? []; | |
| 5805 | + const idx = charLore.findIndex(e => e.name === fileName); | |
| 5806 | + const current = idx !== -1 ? (charLore[idx].extraBooks ?? []) : []; | |
| 5807 | + const next = normalizeArray(computeNext(current)); | |
| 5808 | + | |
| 5809 | + if (next.length === 0) { | |
| 5810 | + if (idx !== -1) charLore.splice(idx, 1); | |
| 5811 | + } else if (idx === -1) { | |
| 5812 | + charLore.push({ name: fileName, extraBooks: next }); | |
| 5813 | + } else { | |
| 5814 | + charLore[idx] = { ...charLore[idx], extraBooks: next }; | |
| 5815 | + } | |
| 5816 | + | |
| 5817 | + Object.assign(world_info, { charLore }); | |
| 5818 | + saveSettingsDebounced(); | |
| 5819 | +} | |
| 5820 | + | |
| 5561 | 5821 | export function initWorldInfo() { |
| 5562 | 5822 | $('#world_info').on('mousedown change', async function (e) { |
| 5563 | 5823 | // If there's no world names, don't do anything |
| @@ -133,6 +133,7 @@ | ||
| 133 | 133 | * { |
| 134 | 134 | box-sizing: border-box; |
| 135 | 135 | -webkit-font-smoothing: antialiased; |
| 136 | + -webkit-tap-highlight-color: transparent; | |
| 136 | 137 | -moz-osx-font-smoothing: grayscale; |
| 137 | 138 | text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor); |
| 138 | 139 | } |
| @@ -599,6 +600,14 @@ input[type='checkbox']:focus-visible { | ||
| 599 | 600 | max-height: var(--doc-height); |
| 600 | 601 | } |
| 601 | 602 | |
| 603 | +.mes_reasoning_details[data-state="thinking"] .mes_reasoning .text_segment, | |
| 604 | +.mes_text .text_segment { | |
| 605 | + animation-name: fade-in; | |
| 606 | + animation-timing-function: ease-in-out; | |
| 607 | + /* Not using variables for duration as they are zeroed when reduced motion is enabled */ | |
| 608 | + animation-duration: 300ms; | |
| 609 | +} | |
| 610 | + | |
| 602 | 611 | .mes .mes_timer, |
| 603 | 612 | .mes .mesIDDisplay, |
| 604 | 613 | .mes .tokenCounterDisplay { |
| @@ -2923,6 +2932,10 @@ select option:not(:checked) { | ||
| 2923 | 2932 | color: var(--white70a); |
| 2924 | 2933 | } |
| 2925 | 2934 | |
| 2935 | +select option[disabled]:not(:checked) { | |
| 2936 | + color: var(--white30a); | |
| 2937 | +} | |
| 2938 | + | |
| 2926 | 2939 | /*#######################################################################*/ |
| 2927 | 2940 | |
| 2928 | 2941 | #rm_api_block { |
| @@ -2940,6 +2953,7 @@ select option:not(:checked) { | ||
| 2940 | 2953 | text-align: center; |
| 2941 | 2954 | } |
| 2942 | 2955 | |
| 2956 | +.menu_button[disabled], | |
| 2943 | 2957 | .menu_button.disabled { |
| 2944 | 2958 | filter: brightness(75%) grayscale(1); |
| 2945 | 2959 | opacity: 0.5; |
| @@ -3001,6 +3015,11 @@ select option:not(:checked) { | ||
| 3001 | 3015 | display: block; |
| 3002 | 3016 | } |
| 3003 | 3017 | |
| 3018 | +/* Fix font size difference in API Connections dropdowns - Issue #4599 */ | |
| 3019 | +#custom_model_id { | |
| 3020 | + font-size: calc(var(--mainFontSize) * 0.95); | |
| 3021 | +} | |
| 3022 | + | |
| 3004 | 3023 | .menu_button.api_button:hover { |
| 3005 | 3024 | background-color: var(--active); |
| 3006 | 3025 | } |
| @@ -3286,6 +3305,10 @@ input[type=search]:focus::-webkit-search-cancel-button { | ||
| 3286 | 3305 | position: relative; |
| 3287 | 3306 | } |
| 3288 | 3307 | |
| 3308 | +#rm_group_top_bar:not(:has(#rm_group_activation_strategy option[value="0"]:checked)) label:has(#rm_group_allow_self_responses) { | |
| 3309 | + display: none; | |
| 3310 | +} | |
| 3311 | + | |
| 3289 | 3312 | .group_member .queue_position:not(:empty)::before { |
| 3290 | 3313 | content: "#"; |
| 3291 | 3314 | } |
| @@ -3675,8 +3698,8 @@ grammarly-extension { | ||
| 3675 | 3698 | min-width: calc(1.25em + 12px); |
| 3676 | 3699 | } |
| 3677 | 3700 | |
| 3678 | 3701 | .menu_button:not(.disabled):not([disabled]):hover, |
| 3679 | 3702 | .menu_button:not(.disabled):not([disabled]).active { |
| 3680 | 3703 | background-color: var(--white30a); |
| 3681 | 3704 | } |
| 3682 | 3705 | |
| @@ -5740,7 +5763,8 @@ body:not(.movingUI) .drawer-content.maximized { | ||
| 5740 | 5763 | } |
| 5741 | 5764 | |
| 5742 | 5765 | .paginationjs-pages ul li a { |
| 5743 | 5766 | padding: 0.05em 0.5em25em; |
| 5767 | + font-family: 'Font Awesome 6 Free'; | |
| 5744 | 5768 | text-decoration: none; |
| 5745 | 5769 | color: var(--SmartThemeBodyColor); |
| 5746 | 5770 | border: 1px solid var(--SmartThemeBorderColor); |
| @@ -124,6 +124,16 @@ const keyMigrationMap = [ | ||
| 124 | 124 | migrate: () => void 0, |
| 125 | 125 | remove: true, |
| 126 | 126 | }, |
| 127 | + { | |
| 128 | + oldKey: 'autheliaAuth', | |
| 129 | + newKey: 'sso.autheliaAuth', | |
| 130 | + migrate: (value) => value, | |
| 131 | + }, | |
| 132 | + { | |
| 133 | + oldKey: 'authentikAuth', | |
| 134 | + newKey: 'sso.authentikAuth', | |
| 135 | + migrate: (value) => value, | |
| 136 | + }, | |
| 127 | 137 | ]; |
| 128 | 138 | |
| 129 | 139 | /** |
| @@ -154,9 +154,9 @@ async function sendClaudeRequest(request, response) { | ||
| 154 | 154 | const useTools = Array.isArray(request.body.tools) && request.body.tools.length > 0; |
| 155 | 155 | const useSystemPrompt = Boolean(request.body.claude_use_sysprompt); |
| 156 | 156 | const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, useTools, getPromptNames(request)); |
| 157 | 157 | const useThinking = /^claude-(3-7|opus-4|sonnet-4|haiku-4-5)/.test(request.body.model); |
| 158 | 158 | const useWebSearch = /^claude-(3-5|3-7|opus-4|sonnet-4|haiku-4-5)/.test(request.body.model) && Boolean(request.body.enable_web_search); |
| 159 | 159 | const isOpus41isLimitedSampling = /^claude-(opus-4-1|sonnet-4-5|haiku-4-5)/.test(request.body.model); |
| 160 | 160 | const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m'; |
| 161 | 161 | let fixThinkingPrefill = false; |
| 162 | 162 | // Add custom stop sequences |
| @@ -226,7 +226,7 @@ async function sendClaudeRequest(request, response) { | ||
| 226 | 226 | betaHeaders.push('extended-cache-ttl-2025-04-11'); |
| 227 | 227 | } |
| 228 | 228 | |
| 229 | 229 | if (isOpus41isLimitedSampling) { |
| 230 | 230 | if (requestBody.top_p < 1) { |
| 231 | 231 | delete requestBody.temperature; |
| 232 | 232 | } else { |
| @@ -377,24 +377,16 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 377 | 377 | 'gemini-2.0-flash-exp-image-generation', |
| 378 | 378 | 'gemini-2.0-flash-preview-image-generation', |
| 379 | 379 | 'gemini-2.5-flash-image-preview', |
| 380 | + 'gemini-2.5-flash-image', | |
| 380 | 381 | ]; |
| 381 | 382 | |
| 382 | - // These models do not support setting the threshold to OFF at all. | |
| 383 | + const isThinkingConfigModel = m => /^gemini-2.5-(flash|pro)/.test(m) && !/-image(-preview)?$/.test(m); | |
| 383 | - const blockNoneModels = [ | |
| 384 | - 'gemini-1.5-pro-001', | |
| 385 | - 'gemini-1.5-flash-001', | |
| 386 | - 'gemini-1.5-flash-8b-exp-0827', | |
| 387 | - 'gemini-1.5-flash-8b-exp-0924', | |
| 388 | - ]; | |
| 389 | - | |
| 390 | - const isThinkingConfigModel = m => /^gemini-2.5-(flash|pro)/.test(m) && !/-image-preview$/.test(m); | |
| 391 | 384 | |
| 392 | 385 | const noSearchModels = [ |
| 393 | 386 | 'gemini-2.0-flash-lite', |
| 394 | 387 | 'gemini-2.0-flash-lite-001', |
| 395 | 388 | 'gemini-2.0-flash-lite-preview-02-05', |
| 396 | 389 | 'gemini-robotics-er-1.5-flash-8b-exp-0924preview', |
| 397 | - 'gemini-1.5-flash-8b-exp-0827', | |
| 398 | 390 | ]; |
| 399 | 391 | // #endregion |
| 400 | 392 | |
| @@ -413,15 +405,8 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 413 | 405 | const prompt = convertGooglePrompt(request.body.messages, model, useSystemPrompt, getPromptNames(request)); |
| 414 | 406 | let safetySettings = GEMINI_SAFETY; |
| 415 | 407 | |
| 416 | - if (blockNoneModels.includes(model)) { | |
| 417 | - safetySettings = GEMINI_SAFETY.map(setting => ({ ...setting, threshold: 'BLOCK_NONE' })); | |
| 418 | - } | |
| 419 | - | |
| 420 | 408 | if (enableWebSearch && !enableImageModality && !isGemma && !isLearnLM && !noSearchModels.includes(model)) { |
| 421 | - const searchTool = model.includes('1.5') | |
| 409 | + tools.push({ google_search: {} }); | |
| 422 | - ? ({ google_search_retrieval: {} }) | |
| 423 | - : ({ google_search: {} }); | |
| 424 | - tools.push(searchTool); | |
| 425 | 410 | } |
| 426 | 411 | |
| 427 | 412 | if (Array.isArray(request.body.tools) && request.body.tools.length > 0 && !enableImageModality && !isGemma) { |
| @@ -1012,7 +997,7 @@ async function sendXaiRequest(request, response) { | ||
| 1012 | 997 | bodyParams['stop'] = request.body.stop; |
| 1013 | 998 | } |
| 1014 | 999 | |
| 1015 | - if (request.body.reasoning_effort && ['grok-3-mini-beta', 'grok-3-mini-fast-beta'].includes(request.body.model)) { | |
| 1000 | + if (request.body.reasoning_effort) { | |
| 1016 | 1001 | bodyParams['reasoning_effort'] = request.body.reasoning_effort === 'high' ? 'high' : 'low'; |
| 1017 | 1002 | } |
| 1018 | 1003 | |
| @@ -1255,6 +1240,7 @@ async function sendElectronHubRequest(request, response) { | ||
| 1255 | 1240 | 'frequency_penalty': request.body.frequency_penalty, |
| 1256 | 1241 | 'top_p': request.body.top_p, |
| 1257 | 1242 | 'top_k': request.body.top_k, |
| 1243 | + 'logit_bias': request.body.logit_bias, | |
| 1258 | 1244 | 'seed': request.body.seed, |
| 1259 | 1245 | ...bodyParams, |
| 1260 | 1246 | }; |
| @@ -1848,9 +1834,9 @@ router.post('/generate', function (request, response) { | ||
| 1848 | 1834 | } |
| 1849 | 1835 | |
| 1850 | 1836 | const cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number'); |
| 1851 | 1837 | const isClaude3or4 = /anthropic\/claude-(3|opus-4|sonnet-4|haiku-4)/.test(request.body.model); |
| 1852 | 1838 | const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m'; |
| 1853 | 1839 | if (Array.isArray(request.body.messages) && Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && isClaude3or4) { |
| 1854 | 1840 | cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth, cacheTTL); |
| 1855 | 1841 | } |
| 1856 | 1842 | |
| @@ -2209,4 +2195,65 @@ multimodalModels.post('/electronhub', async (_req, res) => { | ||
| 2209 | 2195 | } |
| 2210 | 2196 | }); |
| 2211 | 2197 | |
| 2198 | +multimodalModels.post('/mistral', async (req, res) => { | |
| 2199 | + try { | |
| 2200 | + const key = readSecret(req.user.directories, SECRET_KEYS.MISTRALAI); | |
| 2201 | + | |
| 2202 | + if (!key) { | |
| 2203 | + return res.json([]); | |
| 2204 | + } | |
| 2205 | + | |
| 2206 | + const response = await fetch('https://api.mistral.ai/v1/models', { | |
| 2207 | + headers: { | |
| 2208 | + 'Authorization': `Bearer ${key}`, | |
| 2209 | + }, | |
| 2210 | + }); | |
| 2211 | + | |
| 2212 | + if (!response.ok) { | |
| 2213 | + return res.json([]); | |
| 2214 | + } | |
| 2215 | + | |
| 2216 | + /** @type {any} */ | |
| 2217 | + const data = await response.json(); | |
| 2218 | + const multimodalModels = data.data.filter(m => m.capabilities?.vision).map(m => m.id); | |
| 2219 | + return res.json(multimodalModels); | |
| 2220 | + } catch (error) { | |
| 2221 | + console.error(error); | |
| 2222 | + return res.sendStatus(500); | |
| 2223 | + } | |
| 2224 | +}); | |
| 2225 | + | |
| 2226 | +multimodalModels.post('/xai', async (req, res) => { | |
| 2227 | + try { | |
| 2228 | + const key = readSecret(req.user.directories, SECRET_KEYS.XAI); | |
| 2229 | + | |
| 2230 | + if (!key) { | |
| 2231 | + return res.json([]); | |
| 2232 | + } | |
| 2233 | + | |
| 2234 | + // xAI's /models endpoint doesn't return modality info, so we must use /language-models instead | |
| 2235 | + const response = await fetch('https://api.x.ai/v1/language-models', { | |
| 2236 | + headers: { | |
| 2237 | + 'Authorization': `Bearer ${key}`, | |
| 2238 | + }, | |
| 2239 | + }); | |
| 2240 | + | |
| 2241 | + if (!response.ok) { | |
| 2242 | + return res.json([]); | |
| 2243 | + } | |
| 2244 | + | |
| 2245 | + /** @type {any} */ | |
| 2246 | + const data = await response.json(); | |
| 2247 | + const multimodalModels = data.models.filter(m => m.input_modalities?.includes('image')).map(m => m.id); | |
| 2248 | + if (!multimodalModels.includes('grok-4-0709')) { | |
| 2249 | + // The endpoint says it doesn't support images, but it does | |
| 2250 | + multimodalModels.push('grok-4-0709'); | |
| 2251 | + } | |
| 2252 | + return res.json(multimodalModels); | |
| 2253 | + } catch (error) { | |
| 2254 | + console.error(error); | |
| 2255 | + return res.sendStatus(500); | |
| 2256 | + } | |
| 2257 | +}); | |
| 2258 | + | |
| 2212 | 2259 | router.use('/multimodal-models', multimodalModels); |
| @@ -108,6 +108,7 @@ class DiskCache { | ||
| 108 | 108 | dir: this.cachePath, |
| 109 | 109 | ttl: false, |
| 110 | 110 | forgiveParseErrors: true, |
| 111 | + expiredInterval: 0, | |
| 111 | 112 | // @ts-ignore |
| 112 | 113 | maxFileDescriptors: 100, |
| 113 | 114 | }); |
| @@ -688,6 +689,7 @@ function convertWorldInfoToCharacterBook(name, entries) { | ||
| 688 | 689 | useProbability: entry.useProbability ?? false, |
| 689 | 690 | depth: entry.depth ?? 4, |
| 690 | 691 | selectiveLogic: entry.selectiveLogic ?? 0, |
| 692 | + outlet_name: entry.outletName ?? '', | |
| 691 | 693 | group: entry.group ?? '', |
| 692 | 694 | group_override: entry.groupOverride ?? false, |
| 693 | 695 | group_weight: entry.groupWeight ?? null, |
| @@ -628,6 +628,7 @@ router.post('/import', validateAvatarUrlMiddleware, function (request, response) | ||
| 628 | 628 | const avatarUrl = (request.body.avatar_url).replace('.png', ''); |
| 629 | 629 | const characterName = request.body.character_name; |
| 630 | 630 | const userName = request.body.user_name || 'User'; |
| 631 | + const fileNames = []; | |
| 631 | 632 | |
| 632 | 633 | if (!request.file) { |
| 633 | 634 | return response.sendStatus(400); |
| @@ -662,6 +663,7 @@ router.post('/import', validateAvatarUrlMiddleware, function (request, response) | ||
| 662 | 663 | const handleChat = (chat) => { |
| 663 | 664 | const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`; |
| 664 | 665 | const filePath = path.join(request.user.directories.chats, avatarUrl, fileName); |
| 666 | + fileNames.push(fileName); | |
| 665 | 667 | writeFileAtomicSync(filePath, chat, 'utf8'); |
| 666 | 668 | }; |
| 667 | 669 | |
| @@ -673,7 +675,7 @@ router.post('/import', validateAvatarUrlMiddleware, function (request, response) | ||
| 673 | 675 | handleChat(chat); |
| 674 | 676 | } |
| 675 | 677 | |
| 676 | 678 | return response.send({ res: true, fileNames }); |
| 677 | 679 | } |
| 678 | 680 | |
| 679 | 681 | if (format === 'jsonl') { |
| @@ -700,13 +702,14 @@ router.post('/import', validateAvatarUrlMiddleware, function (request, response) | ||
| 700 | 702 | |
| 701 | 703 | const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`; |
| 702 | 704 | const filePath = path.join(request.user.directories.chats, avatarUrl, fileName); |
| 705 | + fileNames.push(fileName); | |
| 703 | 706 | if (flattenedChat !== data) { |
| 704 | 707 | writeFileAtomicSync(filePath, flattenedChat, 'utf8'); |
| 705 | 708 | } else { |
| 706 | 709 | fs.copyFileSync(pathToUpload, filePath); |
| 707 | 710 | } |
| 708 | 711 | fs.unlinkSync(pathToUpload); |
| 709 | 712 | response.send({ res: true, fileNames }); |
| 710 | 713 | } |
| 711 | 714 | } catch (error) { |
| 712 | 715 | console.error(error); |
| @@ -325,6 +325,104 @@ router.post('/generate-voice', async (request, response) => { | ||
| 325 | 325 | } |
| 326 | 326 | }); |
| 327 | 327 | |
| 328 | +// ElectronHub TTS proxy | |
| 329 | +router.post('/electronhub/generate-voice', async (request, response) => { | |
| 330 | + try { | |
| 331 | + const key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB); | |
| 332 | + | |
| 333 | + if (!key) { | |
| 334 | + console.warn('No ElectronHub key found'); | |
| 335 | + return response.sendStatus(400); | |
| 336 | + } | |
| 337 | + | |
| 338 | + const requestBody = { | |
| 339 | + input: request.body.input, | |
| 340 | + voice: request.body.voice, | |
| 341 | + speed: request.body.speed ?? 1, | |
| 342 | + temperature: request.body.temperature ?? undefined, | |
| 343 | + model: request.body.model || 'tts-1', | |
| 344 | + response_format: 'mp3', | |
| 345 | + }; | |
| 346 | + | |
| 347 | + // Optional provider-specific params | |
| 348 | + if (request.body.instructions) requestBody.instructions = request.body.instructions; | |
| 349 | + if (request.body.speaker_transcript) requestBody.speaker_transcript = request.body.speaker_transcript; | |
| 350 | + if (Number.isFinite(request.body.cfg_scale)) requestBody.cfg_scale = Number(request.body.cfg_scale); | |
| 351 | + if (Number.isFinite(request.body.cfg_filter_top_k)) requestBody.cfg_filter_top_k = Number(request.body.cfg_filter_top_k); | |
| 352 | + if (Number.isFinite(request.body.speech_rate)) requestBody.speech_rate = Number(request.body.speech_rate); | |
| 353 | + if (Number.isFinite(request.body.pitch_adjustment)) requestBody.pitch_adjustment = Number(request.body.pitch_adjustment); | |
| 354 | + if (request.body.emotional_style) requestBody.emotional_style = request.body.emotional_style; | |
| 355 | + | |
| 356 | + // Handle dynamic parameters sent from the frontend | |
| 357 | + const knownParams = new Set(Object.keys(requestBody)); | |
| 358 | + for (const key in request.body) { | |
| 359 | + if (!knownParams.has(key) && request.body[key] !== undefined) { | |
| 360 | + requestBody[key] = request.body[key]; | |
| 361 | + } | |
| 362 | + } | |
| 363 | + | |
| 364 | + // Clean undefineds | |
| 365 | + Object.keys(requestBody).forEach(k => requestBody[k] === undefined && delete requestBody[k]); | |
| 366 | + | |
| 367 | + console.debug('ElectronHub TTS request', requestBody); | |
| 368 | + | |
| 369 | + const result = await fetch('https://api.electronhub.ai/v1/audio/speech', { | |
| 370 | + method: 'POST', | |
| 371 | + headers: { | |
| 372 | + 'Content-Type': 'application/json', | |
| 373 | + Authorization: `Bearer ${key}`, | |
| 374 | + }, | |
| 375 | + body: JSON.stringify(requestBody), | |
| 376 | + }); | |
| 377 | + | |
| 378 | + if (!result.ok) { | |
| 379 | + const text = await result.text(); | |
| 380 | + console.warn('ElectronHub TTS request failed', result.statusText, text); | |
| 381 | + return response.status(500).send(text); | |
| 382 | + } | |
| 383 | + | |
| 384 | + const contentType = result.headers.get('content-type') || 'audio/mpeg'; | |
| 385 | + const buffer = await result.arrayBuffer(); | |
| 386 | + response.setHeader('Content-Type', contentType); | |
| 387 | + return response.send(Buffer.from(buffer)); | |
| 388 | + } catch (error) { | |
| 389 | + console.error('ElectronHub TTS generation failed', error); | |
| 390 | + response.status(500).send('Internal server error'); | |
| 391 | + } | |
| 392 | +}); | |
| 393 | + | |
| 394 | +// ElectronHub model list | |
| 395 | +router.post('/electronhub/models', async (request, response) => { | |
| 396 | + try { | |
| 397 | + const key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB); | |
| 398 | + | |
| 399 | + if (!key) { | |
| 400 | + console.warn('No ElectronHub key found'); | |
| 401 | + return response.sendStatus(400); | |
| 402 | + } | |
| 403 | + | |
| 404 | + const result = await fetch('https://api.electronhub.ai/v1/models', { | |
| 405 | + method: 'GET', | |
| 406 | + headers: { | |
| 407 | + Authorization: `Bearer ${key}`, | |
| 408 | + }, | |
| 409 | + }); | |
| 410 | + | |
| 411 | + if (!result.ok) { | |
| 412 | + const text = await result.text(); | |
| 413 | + console.warn('ElectronHub models request failed', result.statusText, text); | |
| 414 | + return response.status(500).send(text); | |
| 415 | + } | |
| 416 | + | |
| 417 | + const data = await result.json(); | |
| 418 | + const models = data && Array.isArray(data['data']) ? data['data'] : []; | |
| 419 | + return response.json(models); | |
| 420 | + } catch (error) { | |
| 421 | + console.error('ElectronHub models fetch failed', error); | |
| 422 | + response.status(500).send('Internal server error'); | |
| 423 | + } | |
| 424 | +}); | |
| 425 | + | |
| 328 | 426 | router.post('/generate-image', async (request, response) => { |
| 329 | 427 | try { |
| 330 | 428 | const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI); |
| @@ -561,12 +561,13 @@ router.post('/find', (request, response) => { | ||
| 561 | 561 | } |
| 562 | 562 | |
| 563 | 563 | const manager = new SecretManager(request.user.directories); |
| 564 | 564 | const secretValuestate = manager.readSecretgetSecretState(key, id); |
| 565 | 565 | |
| 566 | 566 | if (!secretValuestate[key]) { |
| 567 | 567 | return response.sendStatus(404); |
| 568 | 568 | } |
| 569 | 569 | |
| 570 | + const secretValue = manager.readSecret(key, id); | |
| 570 | 571 | return response.send({ value: secretValue }); |
| 571 | 572 | } catch (error) { |
| 572 | 573 | console.error('Error finding secret:', error); |
| @@ -187,7 +187,7 @@ function getSourceSettings(source, request) { | ||
| 187 | 187 | case 'palm': |
| 188 | 188 | case 'vertexai': |
| 189 | 189 | return { |
| 190 | 190 | model: String(request.body.model || 'text-embedding-004005'), |
| 191 | 191 | request: request, // Pass the request object to get API key and URL |
| 192 | 192 | }; |
| 193 | 193 | case 'mistral': |
| @@ -14,8 +14,9 @@ export default function getWebpackServeMiddleware() { | ||
| 14 | 14 | const publicLibConfig = getPublicLibConfig(); |
| 15 | 15 | const outputPath = publicLibConfig.output?.path; |
| 16 | 16 | const outputFile = publicLibConfig.output?.filename; |
| 17 | + const parsedPath = path.parse(req.path); | |
| 17 | 18 | |
| 18 | 19 | if (req.method === 'GET' && path.parse(reqparsedPath.path)dir === '/' && parsedPath.base === outputFile) { |
| 19 | 20 | return res.sendFile(outputFile, { root: outputPath }); |
| 20 | 21 | } |
| 21 | 22 | |
| @@ -24,7 +24,8 @@ import { serverDirectory } from './server-directory.js'; | ||
| 24 | 24 | export const KEY_PREFIX = 'user:'; |
| 25 | 25 | const AVATAR_PREFIX = 'avatar:'; |
| 26 | 26 | const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean'); |
| 27 | 27 | const AUTHELIA_AUTH = getConfigValue('sso.autheliaAuth', false, 'boolean'); |
| 28 | +const AUTHENTIK_AUTH = getConfigValue('sso.authentikAuth', false, 'boolean'); | |
| 28 | 29 | const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false, 'boolean'); |
| 29 | 30 | const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); |
| 30 | 31 | |
| @@ -511,6 +512,7 @@ export async function initUserStorage(dataRoot) { | ||
| 511 | 512 | await storage.init({ |
| 512 | 513 | dir: path.join(dataRoot, '_storage'), |
| 513 | 514 | ttl: false, // Never expire |
| 515 | + expiredInterval: 0, | |
| 514 | 516 | }); |
| 515 | 517 | |
| 516 | 518 | const keys = await getAllUserHandles(); |
| @@ -715,6 +717,10 @@ export async function tryAutoLogin(request, basicAuthMode) { | ||
| 715 | 717 | return true; |
| 716 | 718 | } |
| 717 | 719 | |
| 720 | + if (AUTHENTIK_AUTH && await authentikUserLogin(request)) { | |
| 721 | + return true; | |
| 722 | + } | |
| 723 | + | |
| 718 | 724 | if (basicAuthMode && PER_USER_BASIC_AUTH && await basicUserLogin(request)) { |
| 719 | 725 | return true; |
| 720 | 726 | } |
| @@ -745,20 +751,41 @@ async function singleUserLogin(request) { | ||
| 745 | 751 | } |
| 746 | 752 | |
| 747 | 753 | /** |
| 748 | 754 | * TriesAttempts auto-login withusing authliaan trustedAuthelia headersheader. |
| 749 | 755 | * https://www.authelia.com/integration/trusted-header-sso/introduction/ |
| 750 | 756 | * @param {import('express').Request} request Request object |
| 751 | 757 | * @returns {Promise<boolean>} Whether auto-login was performed |
| 752 | 758 | */ |
| 753 | 759 | async function autheliaUserLogin(request) { |
| 760 | + return headerUserLogin(request, 'Remote-User'); | |
| 761 | +} | |
| 762 | + | |
| 763 | +/** | |
| 764 | + * Attempts auto-login using an Authentik header. | |
| 765 | + * https://docs.goauthentik.io/add-secure-apps/providers/proxy/forward_auth/ | |
| 766 | + * @param {import('express').Request} request Request object | |
| 767 | + * @returns {Promise<boolean>} Whether auto-login was performed | |
| 768 | + */ | |
| 769 | +async function authentikUserLogin(request) { | |
| 770 | + return headerUserLogin(request, 'X-Authentik-Username'); | |
| 771 | +} | |
| 772 | + | |
| 773 | +/** | |
| 774 | + * Tries auto-login with a given header. | |
| 775 | + * @param {import('express').Request} request Request object | |
| 776 | + * @param {string} [header='Remote-User'] The header to use for the trusted user | |
| 777 | + * @returns {Promise<boolean>} Whether auto-login was performed | |
| 778 | + */ | |
| 779 | +async function headerUserLogin(request, header = 'Remote-User') { | |
| 754 | 780 | if (!request.session) { |
| 755 | 781 | return false; |
| 756 | 782 | } |
| 757 | 783 | |
| 758 | 784 | const remoteUser = request.get('Remote-User'header); |
| 759 | 785 | if (!remoteUser) { |
| 760 | 786 | return false; |
| 761 | 787 | } |
| 788 | + console.debug(`Attempting auto-login for user from header ${header}: ${remoteUser}`); | |
| 762 | 789 | |
| 763 | 790 | const userHandles = await getAllUserHandles(); |
| 764 | 791 | for (const userHandle of userHandles) { |