Merge pull request #4656 from SillyTavern/staging Staging
Signed| @@ -41,6 +41,16 @@ labels: | |||
| 41 | 🔬 This PR needs testing! | 41 | 🔬 This PR needs testing! |
| 42 | Any contributor can test and leave reviews, so feel free to help us out! | 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 | - name: 🟥 ⬤⬤⬤⬤⬤ | 54 | - name: 🟥 ⬤⬤⬤⬤⬤ |
| 45 | labeled: | 55 | labeled: |
| 46 | pr: | 56 | pr: |
| @@ -2,4 +2,4 @@ | |||
| 2 | 2 | ||
| 3 | ## Checklist: | 3 | ## Checklist: |
| 4 | 4 | ||
| 5 | - [ ] I have read the [Contributing guidelines](https://github.com/SillyTavern/SillyTavern/blob/release/CONTRIBUTING.md). | 5 | - [ ] I have read the [Contribution guidelines](https://github.com/SillyTavern/SillyTavern/blob/release/CONTRIBUTING.md). |
| @@ -15,8 +15,19 @@ jobs: | |||
| 15 | label-on-content: | 15 | label-on-content: |
| 16 | name: 🏷️ Label Issues by Content | 16 | name: 🏷️ Label Issues by Content |
| 17 | runs-on: ubuntu-latest | 17 | runs-on: ubuntu-latest |
| 18 | if: always() | ||
| 18 | 19 | ||
| 19 | steps: | 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 | - name: Checkout Repository | 31 | - name: Checkout Repository |
| 21 | # Checkout | 32 | # Checkout |
| 22 | # https://github.com/marketplace/actions/checkout | 33 | # https://github.com/marketplace/actions/checkout |
| @@ -32,13 +43,25 @@ jobs: | |||
| 32 | with: | 43 | with: |
| 33 | configuration-path: .github/issues-auto-labels.yml | 44 | configuration-path: .github/issues-auto-labels.yml |
| 34 | enable-versioned-regex: 0 | 45 | enable-versioned-regex: 0 |
| 35 | repo-token: ${{ secrets.GITHUB_TOKEN }} | 46 | repo-token: ${{ steps.app.outputs.token }} |
| 36 | 47 | ||
| 37 | label-on-labels: | 48 | label-on-labels: |
| 38 | name: 🏷️ Label Issues by Labels | 49 | name: 🏷️ Label Issues by Labels |
| 50 | needs: [label-on-content] | ||
| 39 | runs-on: ubuntu-latest | 51 | runs-on: ubuntu-latest |
| 52 | if: always() | ||
| 40 | 53 | ||
| 41 | steps: | 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 | - name: ✅ Add "👍 Approved" for relevant labels | 65 | - name: ✅ Add "👍 Approved" for relevant labels |
| 43 | if: contains(fromJSON('["👩💻 Good First Issue", "🙏 Help Wanted", "🪲 Confirmed", "⚠️ High Priority", "❕ Medium Priority", "💤 Low Priority"]'), github.event.label.name) | 66 | if: contains(fromJSON('["👩💻 Good First Issue", "🙏 Help Wanted", "🪲 Confirmed", "⚠️ High Priority", "❕ Medium Priority", "💤 Low Priority"]'), github.event.label.name) |
| 44 | # 🤖 Issues Helper | 67 | # 🤖 Issues Helper |
| @@ -46,7 +69,7 @@ jobs: | |||
| 46 | uses: actions-cool/issues-helper@v3.6.0 | 69 | uses: actions-cool/issues-helper@v3.6.0 |
| 47 | with: | 70 | with: |
| 48 | actions: 'add-labels' | 71 | actions: 'add-labels' |
| 49 | token: ${{ secrets.GITHUB_TOKEN }} | 72 | token: ${{ steps.app.outputs.token }} |
| 50 | labels: '👍 Approved' | 73 | labels: '👍 Approved' |
| 51 | 74 | ||
| 52 | - name: ❌ Remove progress labels when issue is marked done or stale | 75 | - name: ❌ Remove progress labels when issue is marked done or stale |
| @@ -56,7 +79,7 @@ jobs: | |||
| 56 | uses: actions-cool/issues-helper@v3.6.0 | 79 | uses: actions-cool/issues-helper@v3.6.0 |
| 57 | with: | 80 | with: |
| 58 | actions: 'remove-labels' | 81 | actions: 'remove-labels' |
| 59 | token: ${{ secrets.GITHUB_TOKEN }} | 82 | token: ${{ steps.app.outputs.token }} |
| 60 | labels: '🧑💻 In Progress,🤔 Unsure,🤔 Under Consideration' | 83 | labels: '🧑💻 In Progress,🤔 Unsure,🤔 Under Consideration' |
| 61 | 84 | ||
| 62 | - name: ❌ Remove temporary labels when confirmed labels are added | 85 | - name: ❌ Remove temporary labels when confirmed labels are added |
| @@ -66,7 +89,7 @@ jobs: | |||
| 66 | uses: actions-cool/issues-helper@v3.6.0 | 89 | uses: actions-cool/issues-helper@v3.6.0 |
| 67 | with: | 90 | with: |
| 68 | actions: 'remove-labels' | 91 | actions: 'remove-labels' |
| 69 | token: ${{ secrets.GITHUB_TOKEN }} | 92 | token: ${{ steps.app.outputs.token }} |
| 70 | labels: '🤔 Unsure,🤔 Under Consideration' | 93 | labels: '🤔 Unsure,🤔 Under Consideration' |
| 71 | 94 | ||
| 72 | - name: ❌ Remove no bug labels when "🪲 Confirmed" is added | 95 | - name: ❌ Remove no bug labels when "🪲 Confirmed" is added |
| @@ -76,32 +99,54 @@ jobs: | |||
| 76 | uses: actions-cool/issues-helper@v3.6.0 | 99 | uses: actions-cool/issues-helper@v3.6.0 |
| 77 | with: | 100 | with: |
| 78 | actions: 'remove-labels' | 101 | actions: 'remove-labels' |
| 79 | token: ${{ secrets.GITHUB_TOKEN }} | 102 | token: ${{ steps.app.outputs.token }} |
| 80 | labels: '✖️ Not Reproducible,✖️ Not A Bug' | 103 | labels: '✖️ Not Reproducible,✖️ Not A Bug' |
| 81 | 104 | ||
| 82 | remove-stale-label: | 105 | remove-stale-label: |
| 83 | name: 🗑️ Remove Stale Label on Comment | 106 | name: 🗑️ Remove Stale Label on Comment |
| 107 | needs: [label-on-content, label-on-labels] | ||
| 84 | runs-on: ubuntu-latest | 108 | runs-on: ubuntu-latest |
| 85 | # Only run this on new comments, to automatically remove the stale label | 109 | # Only run this on new comments, to automatically remove the stale label |
| 86 | if: github.event_name == 'issue_comment' && github.actor != 'github-actions[bot]' | 110 | if: always() && (github.event_name == 'issue_comment' && github.event.sender.type != 'Bot') |
| 87 | 111 | ||
| 88 | steps: | 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 | - name: Remove Stale Label | 123 | - name: Remove Stale Label |
| 90 | # 🤖 Issues Helper | 124 | # 🤖 Issues Helper |
| 91 | # https://github.com/marketplace/actions/issues-helper | 125 | # https://github.com/marketplace/actions/issues-helper |
| 92 | uses: actions-cool/issues-helper@v3.6.0 | 126 | uses: actions-cool/issues-helper@v3.6.0 |
| 93 | with: | 127 | with: |
| 94 | actions: 'remove-labels' | 128 | actions: 'remove-labels' |
| 95 | token: ${{ secrets.GITHUB_TOKEN }} | 129 | token: ${{ steps.app.outputs.token }} |
| 96 | issue-number: ${{ github.event.issue.number }} | 130 | issue-number: ${{ github.event.issue.number }} |
| 97 | labels: '⚰️ Stale,🕸️ Inactive,🚏 Awaiting User Response,🛑 No Response' | 131 | labels: '⚰️ Stale,🕸️ Inactive,🚏 Awaiting User Response,🛑 No Response' |
| 98 | 132 | ||
| 99 | write-auto-comments: | 133 | write-auto-comments: |
| 100 | name: 💬 Post Issue Comments Based on Labels | 134 | name: 💬 Post Issue Comments Based on Labels |
| 101 | needs: [label-on-content, label-on-labels] | 135 | needs: [label-on-content, label-on-labels, remove-stale-label] |
| 102 | runs-on: ubuntu-latest | 136 | runs-on: ubuntu-latest |
| 137 | if: always() | ||
| 103 | 138 | ||
| 104 | steps: | 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 | - name: Checkout Repository | 150 | - name: Checkout Repository |
| 106 | # Checkout | 151 | # Checkout |
| 107 | # https://github.com/marketplace/actions/checkout | 152 | # https://github.com/marketplace/actions/checkout |
| @@ -113,4 +158,4 @@ jobs: | |||
| 113 | uses: peaceiris/actions-label-commenter@v1.10.0 | 158 | uses: peaceiris/actions-label-commenter@v1.10.0 |
| 114 | with: | 159 | with: |
| 115 | config_file: .github/issues-auto-comments.yml | 160 | config_file: .github/issues-auto-comments.yml |
| 116 | github_token: ${{ secrets.GITHUB_TOKEN }} | 161 | github_token: ${{ steps.app.outputs.token }} |
| @@ -15,8 +15,19 @@ jobs: | |||
| 15 | update-linked-issues: | 15 | update-linked-issues: |
| 16 | name: 🔗 Mark Linked Issues Done on Push | 16 | name: 🔗 Mark Linked Issues Done on Push |
| 17 | runs-on: ubuntu-latest | 17 | runs-on: ubuntu-latest |
| 18 | if: always() | ||
| 18 | 19 | ||
| 19 | steps: | 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 | - name: Checkout Repository | 31 | - name: Checkout Repository |
| 21 | # Checkout | 32 | # Checkout |
| 22 | # https://github.com/marketplace/actions/checkout | 33 | # https://github.com/marketplace/actions/checkout |
| @@ -31,7 +42,7 @@ jobs: | |||
| 31 | - name: Label Linked Issues | 42 | - name: Label Linked Issues |
| 32 | id: label_linked_issues | 43 | id: label_linked_issues |
| 33 | env: | 44 | env: |
| 34 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | 45 | GH_TOKEN: ${{ steps.app.outputs.token }} |
| 35 | run: | | 46 | run: | |
| 36 | for ISSUE in $(echo $issues | jq -r '.[]'); do | 47 | for ISSUE in $(echo $issues | jq -r '.[]'); do |
| 37 | if [ "${{ github.ref }}" == "refs/heads/staging" ]; then | 48 | if [ "${{ github.ref }}" == "refs/heads/staging" ]; then |
| @@ -15,14 +15,25 @@ jobs: | |||
| 15 | mark-inactivity: | 15 | mark-inactivity: |
| 16 | name: ⏳ Mark Issues/PRs without Activity | 16 | name: ⏳ Mark Issues/PRs without Activity |
| 17 | runs-on: ubuntu-latest | 17 | runs-on: ubuntu-latest |
| 18 | if: always() | ||
| 18 | 19 | ||
| 19 | steps: | 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 | - name: Mark Issues/PRs without Activity | 31 | - name: Mark Issues/PRs without Activity |
| 21 | # Close Stale Issues and PRs | 32 | # Close Stale Issues and PRs |
| 22 | # https://github.com/marketplace/actions/close-stale-issues | 33 | # https://github.com/marketplace/actions/close-stale-issues |
| 23 | uses: actions/stale@v9.1.0 | 34 | uses: actions/stale@v9.1.0 |
| 24 | with: | 35 | with: |
| 25 | repo-token: ${{ secrets.GITHUB_TOKEN }} | 36 | repo-token: ${{ steps.app.outputs.token }} |
| 26 | days-before-stale: 183 | 37 | days-before-stale: 183 |
| 27 | days-before-close: 7 | 38 | days-before-close: 7 |
| 28 | operations-per-run: 30 | 39 | operations-per-run: 30 |
| @@ -47,16 +58,27 @@ jobs: | |||
| 47 | 58 | ||
| 48 | await-user-response: | 59 | await-user-response: |
| 49 | name: ⚠️ Mark Issues/PRs Awaiting User Response | 60 | name: ⚠️ Mark Issues/PRs Awaiting User Response |
| 61 | needs: [mark-inactivity] | ||
| 50 | runs-on: ubuntu-latest | 62 | runs-on: ubuntu-latest |
| 51 | needs: mark-inactivity | 63 | if: always() |
| 52 | 64 | ||
| 53 | steps: | 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 | - name: Mark Issues/PRs Awaiting User Response | 76 | - name: Mark Issues/PRs Awaiting User Response |
| 55 | # Close Stale Issues and PRs | 77 | # Close Stale Issues and PRs |
| 56 | # https://github.com/marketplace/actions/close-stale-issues | 78 | # https://github.com/marketplace/actions/close-stale-issues |
| 57 | uses: actions/stale@v9.1.0 | 79 | uses: actions/stale@v9.1.0 |
| 58 | with: | 80 | with: |
| 59 | repo-token: ${{ secrets.GITHUB_TOKEN }} | 81 | repo-token: ${{ steps.app.outputs.token }} |
| 60 | days-before-stale: 7 | 82 | days-before-stale: 7 |
| 61 | days-before-close: 7 | 83 | days-before-close: 7 |
| 62 | operations-per-run: 30 | 84 | operations-per-run: 30 |
| @@ -74,16 +96,27 @@ jobs: | |||
| 74 | 96 | ||
| 75 | alternative-exists: | 97 | alternative-exists: |
| 76 | name: 🔄 Mark Issues with Alternative Exists | 98 | name: 🔄 Mark Issues with Alternative Exists |
| 99 | needs: [mark-inactivity, await-user-response] | ||
| 77 | runs-on: ubuntu-latest | 100 | runs-on: ubuntu-latest |
| 78 | needs: await-user-response | 101 | if: always() |
| 79 | 102 | ||
| 80 | steps: | 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 | - name: Mark Issues with Alternative Exists | 114 | - name: Mark Issues with Alternative Exists |
| 82 | # Close Stale Issues and PRs | 115 | # Close Stale Issues and PRs |
| 83 | # https://github.com/marketplace/actions/close-stale-issues | 116 | # https://github.com/marketplace/actions/close-stale-issues |
| 84 | uses: actions/stale@v9.1.0 | 117 | uses: actions/stale@v9.1.0 |
| 85 | with: | 118 | with: |
| 86 | repo-token: ${{ secrets.GITHUB_TOKEN }} | 119 | repo-token: ${{ steps.app.outputs.token }} |
| 87 | days-before-stale: 7 | 120 | days-before-stale: 7 |
| 88 | days-before-close: 7 | 121 | days-before-close: 7 |
| 89 | operations-per-run: 30 | 122 | operations-per-run: 30 |
| @@ -15,14 +15,25 @@ jobs: | |||
| 15 | remove-labels: | 15 | remove-labels: |
| 16 | name: 🗑️ Remove Pending Labels on Close | 16 | name: 🗑️ Remove Pending Labels on Close |
| 17 | runs-on: ubuntu-latest | 17 | runs-on: ubuntu-latest |
| 18 | if: always() | ||
| 18 | 19 | ||
| 19 | steps: | 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 | - name: Remove Pending Labels on Close | 31 | - name: Remove Pending Labels on Close |
| 21 | # 🤖 Issues Helper | 32 | # 🤖 Issues Helper |
| 22 | # https://github.com/marketplace/actions/issues-helper | 33 | # https://github.com/marketplace/actions/issues-helper |
| 23 | uses: actions-cool/issues-helper@v3.6.0 | 34 | uses: actions-cool/issues-helper@v3.6.0 |
| 24 | with: | 35 | with: |
| 25 | actions: remove-labels | 36 | actions: remove-labels |
| 26 | token: ${{ secrets.GITHUB_TOKEN }} | 37 | token: ${{ steps.app.outputs.token }} |
| 27 | issue-number: ${{ github.event.issue.number || github.event.pull_request.number }} | 38 | issue-number: ${{ github.event.issue.number || github.event.pull_request.number }} |
| 28 | labels: '🚏 Awaiting User Response,🧑💻 In Progress,📌 Keep Open,🚫 Merge Conflicts,🔬 Needs Testing,🔨 Needs Work,⚰️ Stale,⛔ Waiting For External/Upstream' | 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 | label-maintainer: | 15 | label-maintainer: |
| 16 | name: 🏷️ Label if Author is a Repo Maintainer | 16 | name: 🏷️ Label if Author is a Repo Maintainer |
| 17 | runs-on: ubuntu-latest | 17 | runs-on: ubuntu-latest |
| 18 | if: contains(fromJson('["Cohee1207", "RossAscends", "Wolfsblvt"]'), github.actor) | 18 | if: always() && contains(fromJson('["Cohee1207", "RossAscends", "Wolfsblvt"]'), github.actor) |
| 19 | 19 | ||
| 20 | steps: | 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 | - name: Label if Author is a Repo Maintainer | 31 | - name: Label if Author is a Repo Maintainer |
| 22 | # 🤖 Issues Helper | 32 | # 🤖 Issues Helper |
| 23 | # https://github.com/marketplace/actions/issues-helper | 33 | # https://github.com/marketplace/actions/issues-helper |
| 24 | uses: actions-cool/issues-helper@v3.6.0 | 34 | uses: actions-cool/issues-helper@v3.6.0 |
| 25 | with: | 35 | with: |
| 26 | actions: 'add-labels' | 36 | actions: 'add-labels' |
| 27 | token: ${{ secrets.GITHUB_TOKEN }} | 37 | token: ${{ steps.app.outputs.token }} |
| 28 | issue-number: ${{ github.event.issue.number || github.event.pull_request.number }} | 38 | issue-number: ${{ github.event.issue.number || github.event.pull_request.number }} |
| 29 | labels: '👷 Maintainer' | 39 | labels: '👷 Maintainer' |
| @@ -16,7 +16,7 @@ jobs: | |||
| 16 | name: ✅ Check ESLint on PR | 16 | name: ✅ Check ESLint on PR |
| 17 | runs-on: ubuntu-latest | 17 | runs-on: ubuntu-latest |
| 18 | # Only needs to run when code is changed | 18 | # Only needs to run when code is changed |
| 19 | if: github.event.action == 'opened' || github.event.action == 'synchronize' | 19 | if: always() && (github.event.action == 'opened' || github.event.action == 'synchronize') |
| 20 | 20 | ||
| 21 | # Override permissions, linter likely needs write access to issues | 21 | # Override permissions, linter likely needs write access to issues |
| 22 | permissions: | 22 | permissions: |
| @@ -48,7 +48,7 @@ jobs: | |||
| 48 | # https://github.com/marketplace/actions/action-eslint | 48 | # https://github.com/marketplace/actions/action-eslint |
| 49 | uses: sibiraj-s/action-eslint@v3.0.1 | 49 | uses: sibiraj-s/action-eslint@v3.0.1 |
| 50 | with: | 50 | with: |
| 51 | token: ${{ secrets.GITHUB_TOKEN }} | 51 | token: ${{ secrets.GITHUB_TOKEN }} # ESLint can run with the original permissions |
| 52 | eslint-args: '--ignore-path=.gitignore --quiet' | 52 | eslint-args: '--ignore-path=.gitignore --quiet' |
| 53 | extensions: 'js' | 53 | extensions: 'js' |
| 54 | annotations: true | 54 | annotations: true |
| @@ -71,12 +71,22 @@ jobs: | |||
| 71 | pull-requests: write | 71 | pull-requests: write |
| 72 | 72 | ||
| 73 | steps: | 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 | - name: Label PR Size | 84 | - name: Label PR Size |
| 75 | # Pull Request Size Labeler | 85 | # Pull Request Size Labeler |
| 76 | # https://github.com/marketplace/actions/pull-request-size-labeler | 86 | # https://github.com/marketplace/actions/pull-request-size-labeler |
| 77 | uses: codelytv/pr-size-labeler@v1.10.2 | 87 | uses: codelytv/pr-size-labeler@v1.10.2 |
| 78 | with: | 88 | with: |
| 79 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | 89 | GITHUB_TOKEN: ${{ steps.app.outputs.token }} |
| 80 | xs_label: '🟩 ⬤○○○○' | 90 | xs_label: '🟩 ⬤○○○○' |
| 81 | xs_max_size: '20' | 91 | xs_max_size: '20' |
| 82 | s_label: '🟩 ⬤⬤○○○' | 92 | s_label: '🟩 ⬤⬤○○○' |
| @@ -95,9 +105,19 @@ jobs: | |||
| 95 | name: 🏷️ Label PR by Branches | 105 | name: 🏷️ Label PR by Branches |
| 96 | runs-on: ubuntu-latest | 106 | runs-on: ubuntu-latest |
| 97 | # Only label once when PR is created or when base branch is changed, to allow manual label removal | 107 | # Only label once when PR is created or when base branch is changed, to allow manual label removal |
| 98 | if: github.event.action == 'opened' || (github.event.action == 'synchronize' && github.event.changes.base) | 108 | if: always() && (github.event.action == 'opened' || (github.event.action == 'synchronize' && github.event.changes.base)) |
| 99 | 109 | ||
| 100 | steps: | 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 | - name: Checkout Repository | 121 | - name: Checkout Repository |
| 102 | # Checkout | 122 | # Checkout |
| 103 | # https://github.com/marketplace/actions/checkout | 123 | # https://github.com/marketplace/actions/checkout |
| @@ -109,15 +129,26 @@ jobs: | |||
| 109 | uses: actions/labeler@v5.0.0 | 129 | uses: actions/labeler@v5.0.0 |
| 110 | with: | 130 | with: |
| 111 | configuration-path: .github/pr-auto-labels-by-branch.yml | 131 | configuration-path: .github/pr-auto-labels-by-branch.yml |
| 112 | repo-token: ${{ secrets.GITHUB_TOKEN }} | 132 | repo-token: ${{ steps.app.outputs.token }} |
| 113 | 133 | ||
| 114 | label-by-files: | 134 | label-by-files: |
| 115 | name: 🏷️ Label PR by Files | 135 | name: 🏷️ Label PR by Files |
| 136 | needs: [label-by-branches] | ||
| 116 | runs-on: ubuntu-latest | 137 | runs-on: ubuntu-latest |
| 117 | # Only needs to run when code is changed | 138 | # Only needs to run when code is changed |
| 118 | if: github.event.action == 'opened' || github.event.action == 'synchronize' | 139 | if: always() && (github.event.action == 'opened' || github.event.action == 'synchronize') |
| 119 | 140 | ||
| 120 | steps: | 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 | - name: Checkout Repository | 152 | - name: Checkout Repository |
| 122 | # Checkout | 153 | # Checkout |
| 123 | # https://github.com/marketplace/actions/checkout | 154 | # https://github.com/marketplace/actions/checkout |
| @@ -127,15 +158,18 @@ jobs: | |||
| 127 | # Pull Request Labeler | 158 | # Pull Request Labeler |
| 128 | # https://github.com/marketplace/actions/labeler | 159 | # https://github.com/marketplace/actions/labeler |
| 129 | uses: actions/labeler@v5.0.0 | 160 | uses: actions/labeler@v5.0.0 |
| 161 | env: | ||
| 162 | GITHUB_TOKEN: ${{ steps.app.outputs.token }} # labeler action needs some handholding | ||
| 130 | with: | 163 | with: |
| 131 | configuration-path: .github/pr-auto-labels-by-files.yml | 164 | configuration-path: .github/pr-auto-labels-by-files.yml |
| 132 | repo-token: ${{ secrets.GITHUB_TOKEN }} | 165 | repo-token: ${{ steps.app.outputs.token }} |
| 133 | 166 | ||
| 134 | remove-stale-label: | 167 | remove-stale-label: |
| 135 | name: 🗑️ Remove Stale Label on Comment | 168 | name: 🗑️ Remove Stale Label on Comment |
| 169 | needs: [label-by-branches, label-by-files] | ||
| 136 | runs-on: ubuntu-latest | 170 | runs-on: ubuntu-latest |
| 137 | # Only runs on comments not done by the github actions bot | 171 | # Only runs on comments not done by the github actions bot |
| 138 | if: github.event_name == 'pull_request_review_comment' && github.actor != 'github-actions[bot]' | 172 | if: always() && (github.event_name == 'pull_request_review_comment' && github.event.sender.type != 'Bot') |
| 139 | 173 | ||
| 140 | # Override permissions, issue labeler needs issues write access | 174 | # Override permissions, issue labeler needs issues write access |
| 141 | permissions: | 175 | permissions: |
| @@ -144,19 +178,33 @@ jobs: | |||
| 144 | pull-requests: write | 178 | pull-requests: write |
| 145 | 179 | ||
| 146 | steps: | 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 | - name: Remove Stale Label | 194 | - name: Remove Stale Label |
| 195 | if: always() | ||
| 148 | # 🤖 Issues Helper | 196 | # 🤖 Issues Helper |
| 149 | # https://github.com/marketplace/actions/issues-helper | 197 | # https://github.com/marketplace/actions/issues-helper |
| 150 | uses: actions-cool/issues-helper@v3.6.0 | 198 | uses: actions-cool/issues-helper@v3.6.0 |
| 151 | with: | 199 | with: |
| 152 | actions: 'remove-labels' | 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 | issue-number: ${{ github.event.pull_request.number }} | 202 | issue-number: ${{ github.event.pull_request.number }} |
| 155 | labels: '⚰️ Stale' | 203 | labels: '⚰️ Stale' |
| 156 | 204 | ||
| 157 | check-merge-blocking-labels: | 205 | check-merge-blocking-labels: |
| 158 | name: 🚫 Check Merge Blocking Labels | 206 | name: 🚫 Check Merge Blocking Labels |
| 159 | needs: [label-by-branches, label-by-files] | 207 | needs: [label-by-branches, label-by-files, remove-stale-label] |
| 160 | runs-on: ubuntu-latest | 208 | runs-on: ubuntu-latest |
| 161 | # Run, even if the previous jobs were skipped/failed | 209 | # Run, even if the previous jobs were skipped/failed |
| 162 | if: always() | 210 | if: always() |
| @@ -206,12 +254,22 @@ jobs: | |||
| 206 | 254 | ||
| 207 | write-auto-comments: | 255 | write-auto-comments: |
| 208 | name: 💬 Post PR Comments Based on Labels | 256 | name: 💬 Post PR Comments Based on Labels |
| 209 | needs: [label-by-branches, label-by-files] | 257 | needs: [label-by-branches, label-by-files, remove-stale-label, check-merge-blocking-labels] |
| 210 | runs-on: ubuntu-latest | 258 | runs-on: ubuntu-latest |
| 211 | # Run, even if the previous jobs were skipped/failed | 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 | steps: | 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 | - name: Checkout Repository | 273 | - name: Checkout Repository |
| 216 | # Checkout | 274 | # Checkout |
| 217 | # https://github.com/marketplace/actions/checkout | 275 | # https://github.com/marketplace/actions/checkout |
| @@ -223,13 +281,17 @@ jobs: | |||
| 223 | uses: peaceiris/actions-label-commenter@v1.10.0 | 281 | uses: peaceiris/actions-label-commenter@v1.10.0 |
| 224 | with: | 282 | with: |
| 225 | config_file: .github/pr-auto-comments.yml | 283 | config_file: .github/pr-auto-comments.yml |
| 226 | github_token: ${{ secrets.GITHUB_TOKEN }} | 284 | github_token: ${{ steps.app.outputs.token }} |
| 227 | 285 | ||
| 228 | # 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. | 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 | update-linked-issues: | 287 | update-linked-issues: |
| 230 | name: 🔗 Mark Linked Issues Done on Staging Merge | 288 | name: 🔗 Mark Linked Issues Done on Staging Merge |
| 231 | runs-on: ubuntu-latest | 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 | # Override permissions, We need to be able to write to issues | 296 | # Override permissions, We need to be able to write to issues |
| 235 | permissions: | 297 | permissions: |
| @@ -238,6 +300,16 @@ jobs: | |||
| 238 | pull-requests: write | 300 | pull-requests: write |
| 239 | 301 | ||
| 240 | steps: | 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 | - name: Extract Linked Issues From PR Description | 313 | - name: Extract Linked Issues From PR Description |
| 242 | id: extract_issues | 314 | id: extract_issues |
| 243 | run: | | 315 | run: | |
| @@ -250,7 +322,7 @@ jobs: | |||
| 250 | PR_NUMBER=${{ github.event.pull_request.number }} | 322 | PR_NUMBER=${{ github.event.pull_request.number }} |
| 251 | REPO=${{ github.repository }} | 323 | REPO=${{ github.repository }} |
| 252 | API_URL="https://api.github.com/repos/$REPO/pulls/$PR_NUMBER/issues" | 324 | API_URL="https://api.github.com/repos/$REPO/pulls/$PR_NUMBER/issues" |
| 253 | ISSUES=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$API_URL" | jq -r '.[].number' | jq -R -s -c 'split("\n")[:-1]') | 325 | ISSUES=$(curl -s -H "Authorization: token ${{ steps.app.outputs.token }}" "$API_URL" | jq -r '.[].number' | jq -R -s -c 'split("\n")[:-1]') |
| 254 | echo "linked_issues=$ISSUES" >> $GITHUB_ENV | 326 | echo "linked_issues=$ISSUES" >> $GITHUB_ENV |
| 255 | 327 | ||
| 256 | - name: Merge Issue Lists | 328 | - name: Merge Issue Lists |
| @@ -262,7 +334,7 @@ jobs: | |||
| 262 | - name: Label Linked Issues | 334 | - name: Label Linked Issues |
| 263 | id: label_linked_issues | 335 | id: label_linked_issues |
| 264 | env: | 336 | env: |
| 265 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | 337 | GH_TOKEN: ${{ steps.app.outputs.token }} |
| 266 | run: | | 338 | run: | |
| 267 | for ISSUE in $(echo $final_issues | jq -r '.[]'); do | 339 | for ISSUE in $(echo $final_issues | jq -r '.[]'); do |
| 268 | gh issue edit $ISSUE -R ${{ github.repository }} --add-label "✅ Done (staging)" --remove-label "🧑💻 In Progress" | 340 | gh issue edit $ISSUE -R ${{ github.repository }} --add-label "✅ Done (staging)" --remove-label "🧑💻 In Progress" |
| @@ -15,14 +15,25 @@ jobs: | |||
| 15 | check-merge-conflicts: | 15 | check-merge-conflicts: |
| 16 | name: ⚔️ Check Merge Conflicts | 16 | name: ⚔️ Check Merge Conflicts |
| 17 | runs-on: ubuntu-latest | 17 | runs-on: ubuntu-latest |
| 18 | if: always() | ||
| 18 | 19 | ||
| 19 | steps: | 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 | - name: Check Merge Conflicts | 31 | - name: Check Merge Conflicts |
| 21 | # Label Conflicting Pull Requests | 32 | # Label Conflicting Pull Requests |
| 22 | # https://github.com/marketplace/actions/label-conflicting-pull-requests | 33 | # https://github.com/marketplace/actions/label-conflicting-pull-requests |
| 23 | uses: eps1lon/actions-label-merge-conflict@v3.0.3 | 34 | uses: eps1lon/actions-label-merge-conflict@v3.0.3 |
| 24 | with: | 35 | with: |
| 25 | dirtyLabel: '🚫 Merge Conflicts' | 36 | dirtyLabel: '🚫 Merge Conflicts' |
| 26 | repoToken: ${{ secrets.GITHUB_TOKEN }} | 37 | repoToken: ${{ steps.app.outputs.token }} |
| 27 | commentOnDirty: > | 38 | commentOnDirty: > |
| 28 | ⚠️ This PR has conflicts that need to be resolved before it can be merged. | 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 | .env | 55 | .env |
| 56 | /StartDev.bat | 56 | /StartDev.bat |
| 57 | yarn.lock | 57 | yarn.lock |
| 58 | *.code-workspace | ||
| \ No newline at end of file | 58 | \ No newline at end of file | |
| @@ -29,11 +29,22 @@ | |||
| 29 | - Updating GitHub Actions. | 29 | - Updating GitHub Actions. |
| 30 | - Hotfixing a critical bug. | 30 | - Hotfixing a critical bug. |
| 31 | 4. Project maintainers will test and can change your code before merging. | 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 | - What is the reason for a change? | 34 | - What is the reason for a change? |
| 34 | - What did you do to achieve this? | 35 | - What did you do to achieve this? |
| 35 | - How would a reviewer test the change? | 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 | ## Further reading | 49 | ## Further reading |
| 39 | 50 | ||
| @@ -82,6 +82,13 @@ requestProxy: | |||
| 82 | enableUserAccounts: false | 82 | enableUserAccounts: false |
| 83 | # Enable discreet login mode: hides user list on the login screen | 83 | # Enable discreet login mode: hides user list on the login screen |
| 84 | enableDiscreetLogin: false | 84 | enableDiscreetLogin: false |
| 85 | # If `basicAuthMode` and this are enabled then | ||
| 86 | # the username and passwords for basic auth are the same as those | ||
| 87 | # for the individual accounts | ||
| 88 | perUserBasicAuth: false | ||
| 89 | |||
| 90 | # -- SSO LOGIN CONFIGURATION -- | ||
| 91 | sso: | ||
| 85 | # Enable's authlia based auto login. Only enable this if you | 92 | # Enable's authlia based auto login. Only enable this if you |
| 86 | # have setup and installed Authelia as a middle-ware on your | 93 | # have setup and installed Authelia as a middle-ware on your |
| 87 | # reverse proxy | 94 | # reverse proxy |
| @@ -90,10 +97,15 @@ enableDiscreetLogin: false | |||
| 90 | # as that used for authlia. (Ensure the username in authlia | 97 | # as that used for authlia. (Ensure the username in authlia |
| 91 | # is an exact match in lowercase with that in sillytavern) | 98 | # is an exact match in lowercase with that in sillytavern) |
| 92 | autheliaAuth: false | 99 | autheliaAuth: false |
| 93 | # If `basicAuthMode` and this are enabled then | 100 | # Enable's authentik based auto login. Only enable this if you |
| 94 | # the username and passwords for basic auth are the same as those | 101 | # have setup and installed Authentik as a middle-ware on your |
| 95 | # for the individual accounts | 102 | # reverse proxy. |
| 96 | perUserBasicAuth: false | 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 | # Host whitelist configuration. Recommended if you're using a listen mode | 109 | # Host whitelist configuration. Recommended if you're using a listen mode |
| 98 | hostWhitelist: | 110 | hostWhitelist: |
| 99 | # Enable or disable host whitelisting | 111 | # Enable or disable host whitelisting |
| @@ -1,20 +1,23 @@ | |||
| 1 | { | 1 | { |
| 2 | "chat_completion_source": "openai", | 2 | "chat_completion_source": "openai", |
| 3 | "openai_model": "gpt-4-turbo", | 3 | "openai_model": "gpt-4-turbo", |
| 4 | "claude_model": "claude-3-5-sonnet-20240620", | 4 | "claude_model": "claude-sonnet-4-5", |
| 5 | "openrouter_model": "OR_Website", | 5 | "openrouter_model": "OR_Website", |
| 6 | "openrouter_use_fallback": false, | 6 | "openrouter_use_fallback": false, |
| 7 | "openrouter_group_models": false, | 7 | "openrouter_group_models": false, |
| 8 | "openrouter_sort_models": "alphabetically", | 8 | "openrouter_sort_models": "alphabetically", |
| 9 | "ai21_model": "jamba-large", | 9 | "ai21_model": "jamba-large", |
| 10 | "mistralai_model": "mistral-large-latest", | 10 | "mistralai_model": "mistral-large-latest", |
| 11 | "electronhub_model": "gpt-4o-mini", | ||
| 12 | "electronhub_sort_models": "alphabetically", | ||
| 13 | "electronhub_group_models": false, | ||
| 11 | "custom_model": "", | 14 | "custom_model": "", |
| 12 | "custom_url": "", | 15 | "custom_url": "", |
| 13 | "custom_include_body": "", | 16 | "custom_include_body": "", |
| 14 | "custom_exclude_body": "", | 17 | "custom_exclude_body": "", |
| 15 | "custom_include_headers": "", | 18 | "custom_include_headers": "", |
| 16 | "google_model": "gemini-pro", | 19 | "google_model": "gemini-2.5-pro", |
| 17 | "vertexai_model": "gemini-2.0-flash-001", | 20 | "vertexai_model": "gemini-2.5-pro", |
| 18 | "temperature": 1, | 21 | "temperature": 1, |
| 19 | "frequency_penalty": 0, | 22 | "frequency_penalty": 0, |
| 20 | "presence_penalty": 0, | 23 | "presence_penalty": 0, |
| @@ -620,7 +620,7 @@ | |||
| 620 | }, | 620 | }, |
| 621 | "wi_format": "{0}", | 621 | "wi_format": "{0}", |
| 622 | "openai_model": "gpt-4-turbo", | 622 | "openai_model": "gpt-4-turbo", |
| 623 | "claude_model": "claude-3-5-sonnet-20240620", | 623 | "claude_model": "claude-sonnet-4-5", |
| 624 | "ai21_model": "jamba-large", | 624 | "ai21_model": "jamba-large", |
| 625 | "openrouter_model": "OR_Website", | 625 | "openrouter_model": "OR_Website", |
| 626 | "reverse_proxy": "", | 626 | "reverse_proxy": "", |
| @@ -1,12 +1,12 @@ | |||
| 1 | { | 1 | { |
| 2 | "name": "sillytavern", | 2 | "name": "sillytavern", |
| 3 | "version": "1.13.4", | 3 | "version": "1.13.5", |
| 4 | "lockfileVersion": 3, | 4 | "lockfileVersion": 3, |
| 5 | "requires": true, | 5 | "requires": true, |
| 6 | "packages": { | 6 | "packages": { |
| 7 | "": { | 7 | "": { |
| 8 | "name": "sillytavern", | 8 | "name": "sillytavern", |
| 9 | "version": "1.13.4", | 9 | "version": "1.13.5", |
| 10 | "hasInstallScript": true, | 10 | "hasInstallScript": true, |
| 11 | "license": "AGPL-3.0", | 11 | "license": "AGPL-3.0", |
| 12 | "dependencies": { | 12 | "dependencies": { |
| @@ -113,7 +113,7 @@ | |||
| 113 | "type": "git", | 113 | "type": "git", |
| 114 | "url": "https://github.com/SillyTavern/SillyTavern.git" | 114 | "url": "https://github.com/SillyTavern/SillyTavern.git" |
| 115 | }, | 115 | }, |
| 116 | "version": "1.13.4", | 116 | "version": "1.13.5", |
| 117 | "scripts": { | 117 | "scripts": { |
| 118 | "start": "node server.js", | 118 | "start": "node server.js", |
| 119 | "debug": "node --inspect server.js", | 119 | "debug": "node --inspect server.js", |
| @@ -1,6 +1,5 @@ | |||
| 1 | /* Main Page Backgrounds */ | 1 | /* Main Page Backgrounds */ |
| 2 | #bg1, | 2 | #bg1 { |
| 3 | #bg_custom { | ||
| 4 | background-repeat: no-repeat; | 3 | background-repeat: no-repeat; |
| 5 | background-attachment: fixed; | 4 | background-attachment: fixed; |
| 6 | background-size: cover; | 5 | background-size: cover; |
| @@ -8,72 +7,46 @@ | |||
| 8 | width: 100%; | 7 | width: 100%; |
| 9 | height: 100%; | 8 | height: 100%; |
| 10 | transition: background-image var(--animation-duration-3x) ease-in-out; | 9 | transition: background-image var(--animation-duration-3x) ease-in-out; |
| 10 | z-index: -1; | ||
| 11 | } | 11 | } |
| 12 | 12 | ||
| 13 | /* Fitting options */ | 13 | /* Fitting options */ |
| 14 | #background_fitting { | 14 | #background_fitting { |
| 15 | max-width: 6em; | 15 | max-width: 8em; |
| 16 | } | 16 | } |
| 17 | 17 | ||
| 18 | /* Fill/Cover - scales to fill width while maintaining aspect ratio */ | 18 | /* Fill/Cover - scales to fill width while maintaining aspect ratio */ |
| 19 | #bg1.cover, | 19 | #bg1.cover { |
| 20 | #bg_custom.cover { | ||
| 21 | background-size: cover; | 20 | background-size: cover; |
| 22 | background-position: center; | 21 | background-position: center; |
| 23 | } | 22 | } |
| 24 | 23 | ||
| 25 | /* Fit/Contain - shows entire image maintaining aspect ratio */ | 24 | /* Fit/Contain - shows entire image maintaining aspect ratio */ |
| 26 | #bg1.contain, | 25 | #bg1.contain { |
| 27 | #bg_custom.contain { | ||
| 28 | background-size: contain; | 26 | background-size: contain; |
| 29 | background-position: center; | 27 | background-position: center; |
| 30 | background-repeat: no-repeat; | 28 | background-repeat: no-repeat; |
| 31 | } | 29 | } |
| 32 | 30 | ||
| 33 | /* Stretch - stretches to fill entire space */ | 31 | /* Stretch - stretches to fill entire space */ |
| 34 | #bg1.stretch, | 32 | #bg1.stretch { |
| 35 | #bg_custom.stretch { | ||
| 36 | background-size: 100% 100%; | 33 | background-size: 100% 100%; |
| 37 | } | 34 | } |
| 38 | 35 | ||
| 39 | /* Center - centers without scaling */ | 36 | /* Center - centers without scaling */ |
| 40 | #bg1.center, | 37 | #bg1.center { |
| 41 | #bg_custom.center { | ||
| 42 | background-size: auto; | 38 | background-size: auto; |
| 43 | background-position: center; | 39 | background-position: center; |
| 44 | background-repeat: no-repeat; | 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 | /* This is the main flex container for the entire drawer */ | 43 | /* This is the main flex container for the entire drawer */ |
| 67 | #Backgrounds.drawer-content.openDrawer.bg-drawer-layout { | 44 | .drawer-content.openDrawer.bg-drawer-layout { |
| 68 | display: flex; | 45 | display: flex; |
| 46 | } | ||
| 47 | |||
| 48 | .bg-drawer-layout { | ||
| 69 | flex-direction: column; | 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 | padding: 0; | 50 | padding: 0; |
| 78 | } | 51 | } |
| 79 | 52 | ||
| @@ -82,11 +55,31 @@ body.reduced-motion #bg_custom { | |||
| 82 | padding: 5px; | 55 | padding: 5px; |
| 83 | background-color: var(--SmartThemeBlurTintColor); | 56 | background-color: var(--SmartThemeBlurTintColor); |
| 84 | border-bottom: 1px solid var(--SmartThemeBorderColor); | 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 | display: flex; | 82 | display: flex; |
| 89 | align-items: center; | ||
| 90 | gap: 5px; | 83 | gap: 5px; |
| 91 | } | 84 | } |
| 92 | 85 | ||
| @@ -94,49 +87,92 @@ body.reduced-motion #bg_custom { | |||
| 94 | flex-grow: 1; | 87 | flex-grow: 1; |
| 95 | overflow-y: auto; | 88 | overflow-y: auto; |
| 96 | overflow-x: hidden; | 89 | overflow-x: hidden; |
| 97 | padding: 0 5px 5px; | 90 | padding: 0 5px 15px; |
| 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 | #bg-filter { | 102 | #bg-filter { |
| 101 | font-size: calc(var(--mainFontSize) * 0.95); | 103 | font-size: calc(var(--mainFontSize) * 0.95); |
| 102 | } | 104 | } |
| 103 | 105 | ||
| 104 | /* Thumbnail Menu & Buttons */ | 106 | /* Thumbnails */ |
| 107 | .bg_example:hover .BGSampleTitle { | ||
| 108 | opacity: 1; | ||
| 109 | } | ||
| 110 | |||
| 105 | .bg_example .mobile-only-menu-toggle { | 111 | .bg_example .mobile-only-menu-toggle { |
| 106 | display: none; | 112 | display: none; |
| 107 | } | 113 | } |
| 108 | 114 | ||
| 109 | .bg_example.flex-container { | 115 | .bg_example { |
| 110 | width: 30%; | ||
| 111 | max-width: 200px; | ||
| 112 | margin: 5px; | ||
| 113 | aspect-ratio: 16/9; | ||
| 114 | cursor: pointer; | 116 | cursor: pointer; |
| 115 | box-shadow: 0 0 7px var(--black50a); | 117 | box-shadow: 0 0 7px var(--black50a); |
| 116 | |||
| 117 | position: relative; | 118 | position: relative; |
| 118 | overflow: hidden; | 119 | overflow: hidden; |
| 119 | border-radius: 8px; | 120 | border-radius: 8px; |
| 120 | border: 0px solid transparent; | 121 | border: 0px solid transparent; |
| 121 | outline: 2px solid var(--SmartThemeBorderColor); | 122 | outline: 2px solid var(--SmartThemeBorderColor); |
| 122 | outline-offset: -1px; | 123 | outline-offset: -1px; |
| 124 | |||
| 125 | height: auto; | ||
| 126 | aspect-ratio: 16 / 9; | ||
| 123 | } | 127 | } |
| 124 | 128 | ||
| 125 | .bg_example.flex-container:focus-visible { | 129 | .bg_example:focus-visible { |
| 126 | outline-offset: inherit; | 130 | outline-offset: inherit; |
| 131 | outline-color: var(--interactable-outline-color); | ||
| 127 | } | 132 | } |
| 128 | 133 | ||
| 129 | .bg_example_img { | 134 | .bg_example.locked-background { |
| 135 | outline: 2px solid var(--golden); | ||
| 136 | outline-offset: 0; | ||
| 137 | } | ||
| 138 | |||
| 139 | .bg_example.locked-background::after { | ||
| 140 | content: '\f023'; | ||
| 141 | font-family: 'Font Awesome 6 Free'; | ||
| 142 | font-weight: 900; | ||
| 143 | |||
| 130 | position: absolute; | 144 | position: absolute; |
| 131 | top: -2px; | 145 | bottom: 5px; |
| 132 | left: -2px; | 146 | right: 5px; |
| 133 | right: -2px; | 147 | z-index: 4; |
| 134 | bottom: -2px; | 148 | color: var(--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 | .bg_example .jg-menu { | 178 | .bg_example .jg-menu { |
| @@ -145,9 +181,8 @@ body.reduced-motion #bg_custom { | |||
| 145 | top: 2px; | 181 | top: 2px; |
| 146 | right: 2px; | 182 | right: 2px; |
| 147 | background-color: rgba(0, 0, 0, 0.5); | 183 | background-color: rgba(0, 0, 0, 0.5); |
| 148 | border-radius: 8px; | 184 | border-radius: 5px; |
| 149 | gap: 3px; | 185 | padding: 3px 3px; |
| 150 | padding: 3px 5px; | ||
| 151 | z-index: 3; | 186 | z-index: 3; |
| 152 | backdrop-filter: blur(4px); | 187 | backdrop-filter: blur(4px); |
| 153 | border: 1px solid var(--SmartThemeBorderColor); | 188 | border: 1px solid var(--SmartThemeBorderColor); |
| @@ -170,14 +205,14 @@ body.reduced-motion #bg_custom { | |||
| 170 | 205 | ||
| 171 | .bg_example .jg-button { | 206 | .bg_example .jg-button { |
| 172 | display: flex; | 207 | display: flex; |
| 173 | width: 30px; | 208 | width: 24px; |
| 174 | height: 30px; | 209 | height: 24px; |
| 175 | align-items: center; | 210 | align-items: center; |
| 176 | justify-content: center; | 211 | justify-content: center; |
| 177 | color: white; | 212 | color: white; |
| 178 | padding: 5px; | 213 | padding: 5px; |
| 179 | font-size: 1.1em; | 214 | font-size: 1.1em; |
| 180 | border-radius: 6px; | 215 | border-radius: 5px; |
| 181 | transition: background-color var(--animation-duration) ease; | 216 | transition: background-color var(--animation-duration) ease; |
| 182 | } | 217 | } |
| 183 | 218 | ||
| @@ -185,16 +220,54 @@ body.reduced-motion #bg_custom { | |||
| 185 | background-color: rgba(255, 255, 255, 0.2); | 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 | display: none; | 246 | opacity: 1; |
| 247 | pointer-events: auto; | ||
| 194 | } | 248 | } |
| 195 | 249 | ||
| 196 | .bg_example.locked .jg-unlock { | 250 | #bg-scroll-top:hover { |
| 197 | display: flex; | 251 | filter: brightness(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 | .bg_example:not([custom="true"]) .jg-copy, | 273 | .bg_example:not([custom="true"]) .jg-copy, |
| @@ -10,6 +10,18 @@ | |||
| 10 | flex-basis: 100%; | 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 | #send_form.compact #leftSendForm, | 26 | #send_form.compact #leftSendForm, |
| 15 | #send_form.compact #rightSendForm { | 27 | #send_form.compact #rightSendForm { |
| @@ -21,6 +33,15 @@ | |||
| 21 | display: none; | 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 | .bg_button { | 45 | .bg_button { |
| 25 | font-size: 15px; | 46 | font-size: 15px; |
| 26 | } | 47 | } |
| @@ -40,6 +61,11 @@ | |||
| 40 | z-index: 4; | 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 | .bg_example .mobile-only-menu-toggle { | 69 | .bg_example .mobile-only-menu-toggle { |
| 44 | display: flex; | 70 | display: flex; |
| 45 | align-items: center; | 71 | align-items: center; |
| @@ -57,48 +83,13 @@ | |||
| 57 | backdrop-filter: blur(2px); | 83 | backdrop-filter: blur(2px); |
| 58 | } | 84 | } |
| 59 | 85 | ||
| 60 | #bg-header-controls { | 86 | .bg_example .jg-button { |
| 61 | flex-wrap: wrap; | 87 | width: 30px; |
| 62 | row-gap: 10px; | 88 | height: 30px; |
| 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 | #Backgrounds.drawer-content.openDrawer.bg-drawer-layout { | 95 | #Backgrounds.drawer-content.openDrawer.bg-drawer-layout { |
| @@ -429,10 +420,6 @@ | |||
| 429 | .horde_multiple_hint { | 420 | .horde_multiple_hint { |
| 430 | display: none; | 421 | display: none; |
| 431 | } | 422 | } |
| 432 | |||
| 433 | .bg_list { | ||
| 434 | width: unset; | ||
| 435 | } | ||
| 436 | } | 423 | } |
| 437 | 424 | ||
| 438 | /*landscape mode phones and ipads*/ | 425 | /*landscape mode phones and ipads*/ |
| @@ -301,6 +301,10 @@ select.keyselect+span.select2-container .select2-selection--multiple { | |||
| 301 | display: none; | 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 | .world_entry label[for="__invisible"] { | 308 | .world_entry label[for="__invisible"] { |
| 305 | visibility: hidden; | 309 | visibility: hidden; |
| 306 | pointer-events: none; | 310 | pointer-events: none; |
Binary file
Binary file
| @@ -49,7 +49,6 @@ | |||
| 49 | 49 | ||
| 50 | <body class="no-blur"> | 50 | <body class="no-blur"> |
| 51 | <div id="preloader"></div> | 51 | <div id="preloader"></div> |
| 52 | <div id="bg_custom"></div> | ||
| 53 | <div id="bg1"></div> | 52 | <div id="bg1"></div> |
| 54 | <div id="character_context_menu" class="hidden"> | 53 | <div id="character_context_menu" class="hidden"> |
| 55 | <ul> | 54 | <ul> |
| @@ -254,7 +253,7 @@ | |||
| 254 | </div> | 253 | </div> |
| 255 | <div id="common-gen-settings-block" class="width100p"> | 254 | <div id="common-gen-settings-block" class="width100p"> |
| 256 | <div id="pro-settings-block" class="flex-container gap10h5v justifyCenter"> | 255 | <div id="pro-settings-block" class="flex-container gap10h5v justifyCenter"> |
| 257 | <div id="amount_gen_block" class="alignitemscenter flex-container marginBot5 flexFlowColumn flexBasis48p flexGrow flexShrink gap0"> | 256 | <div id="amount_gen_block" class="range-block-range-and-counter alignitemscenter flex-container marginBot5 flexFlowColumn flexBasis48p flexGrow flexShrink gap0"> |
| 258 | <small data-i18n="response legth(tokens)">Response (tokens)</small> | 257 | <small data-i18n="response legth(tokens)">Response (tokens)</small> |
| 259 | <input class="neo-range-slider" type="range" id="amount_gen" name="volume" min="16" max="2048" step="1"> | 258 | <input class="neo-range-slider" type="range" id="amount_gen" name="volume" min="16" max="2048" step="1"> |
| 260 | <div data-randomization-disabled="true" class="wide100p"> | 259 | <div data-randomization-disabled="true" class="wide100p"> |
| @@ -285,7 +284,7 @@ | |||
| 285 | </label> | 284 | </label> |
| 286 | </div> | 285 | </div> |
| 287 | </div> | 286 | </div> |
| 288 | <div id="max_context_block" class="alignitemscenter flex-container marginBot5 flexFlowColumn flexBasis48p flexGrow flexShrink gap0"> | 287 | <div id="max_context_block" class="range-block-range-and-counter alignitemscenter flex-container marginBot5 flexFlowColumn flexBasis48p flexGrow flexShrink gap0"> |
| 289 | <small data-i18n="context size(tokens)">Context (tokens)</small> | 288 | <small data-i18n="context size(tokens)">Context (tokens)</small> |
| 290 | <input class="neo-range-slider" type="range" id="max_context" name="volume" min="512" max="8192" step="64"> | 289 | <input class="neo-range-slider" type="range" id="max_context" name="volume" min="512" max="8192" step="64"> |
| 291 | <div data-randomization-disabled="true" class="wide100p"> | 290 | <div data-randomization-disabled="true" class="wide100p"> |
| @@ -649,7 +648,7 @@ | |||
| 649 | Max Response Length (tokens) | 648 | Max Response Length (tokens) |
| 650 | </div> | 649 | </div> |
| 651 | <div class="wide100p"> | 650 | <div class="wide100p"> |
| 652 | <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536"> | 651 | <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536" step="1"> |
| 653 | </div> | 652 | </div> |
| 654 | </div> | 653 | </div> |
| 655 | <div class="range-block" data-source="openai,custom,xai,aimlapi,moonshot,azure_openai"> | 654 | <div class="range-block" data-source="openai,custom,xai,aimlapi,moonshot,azure_openai"> |
| @@ -676,6 +675,9 @@ | |||
| 676 | <div data-source="openrouter"> | 675 | <div data-source="openrouter"> |
| 677 | <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="openrouter_max_prompt_cost" data-i18n="Unknown">Unknown</span> | 676 | <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="openrouter_max_prompt_cost" data-i18n="Unknown">Unknown</span> |
| 678 | </div> | 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 | <hr> | 681 | <hr> |
| 680 | <div class="range-block"> | 682 | <div class="range-block"> |
| 681 | <label for="stream_toggle" title="Enable OpenAI completion streaming" data-i18n="[title]Enable OpenAI completion streaming" class="checkbox_label widthFreeExpand"> | 683 | <label for="stream_toggle" title="Enable OpenAI completion streaming" data-i18n="[title]Enable OpenAI completion streaming" class="checkbox_label widthFreeExpand"> |
| @@ -1463,14 +1465,12 @@ | |||
| 1463 | </div> | 1465 | </div> |
| 1464 | </div> | 1466 | </div> |
| 1465 | <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"> | 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 | <div class="range-block-title textAlignCenter"> | 1468 | <div class="range-block-title textAlignCenter flex-container justifyCenter alignitemscenter"> |
| 1467 | <small data-i18n="Sequence Breakers">Sequence Breakers</small> | 1469 | <small data-i18n="Sequence Breakers">Sequence Breakers</small> |
| 1468 | </div> | 1470 | </div> |
| 1469 | <div class="wide100p"> | ||
| 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 | <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 | </div> |
| 1472 | </div> | 1473 | </div> |
| 1473 | </div> | ||
| 1474 | <div data-tg-type="ooba, mancer, koboldcpp, tabby, llamacpp, aphrodite" id="dynatemp_block_ooba" class="wide100p"> | 1474 | <div data-tg-type="ooba, mancer, koboldcpp, tabby, llamacpp, aphrodite" id="dynatemp_block_ooba" class="wide100p"> |
| 1475 | <h4 class="wide100p textAlignCenter"> | 1475 | <h4 class="wide100p textAlignCenter"> |
| 1476 | <div class="flex-container alignitemscenter justifyCenter"> | 1476 | <div class="flex-container alignitemscenter justifyCenter"> |
| @@ -1502,7 +1502,7 @@ | |||
| 1502 | <div data-tg-type="ooba,infermaticai,koboldcpp,llamacpp,tabby" id="mirostat_block_ooba" class="wide100p"> | 1502 | <div data-tg-type="ooba,infermaticai,koboldcpp,llamacpp,tabby" id="mirostat_block_ooba" class="wide100p"> |
| 1503 | <h4 class="wide100p textAlignCenter"> | 1503 | <h4 class="wide100p textAlignCenter"> |
| 1504 | <label data-i18n="Mirostat (mode=1 is only for llama.cpp)">Mirostat</label> | 1504 | <label data-i18n="Mirostat (mode=1 is only for llama.cpp)">Mirostat</label> |
| 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."></div> | 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 | </h4> | 1506 | </h4> |
| 1507 | <div class="flex-container flexFlowRow gap10px flexShrink"> | 1507 | <div class="flex-container flexFlowRow gap10px flexShrink"> |
| 1508 | <div class="alignitemscenter flex-container marginBot5 flexFlowColumn flexGrow flexShrink gap0"> | 1508 | <div class="alignitemscenter flex-container marginBot5 flexFlowColumn flexGrow flexShrink gap0"> |
| @@ -1511,18 +1511,12 @@ | |||
| 1511 | <input class="neo-range-input" type="number" min="0" max="2" step="1" data-for="mirostat_mode_textgenerationwebui" id="mirostat_mode_counter_textgenerationwebui"> | 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 | </div> | 1512 | </div> |
| 1513 | <div class="alignitemscenter flex-container marginBot5 flexFlowColumn flexGrow flexShrink gap0"> | 1513 | <div class="alignitemscenter flex-container marginBot5 flexFlowColumn flexGrow flexShrink gap0"> |
| 1514 | <label> | ||
| 1515 | <small data-i18n="Mirostat Tau">Tau</small> | 1514 | <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 | <input class="neo-range-slider" type="range" id="mirostat_tau_textgenerationwebui" name="volume" min="0" max="20" step="0.01" /> | 1515 | <input class="neo-range-slider" type="range" id="mirostat_tau_textgenerationwebui" name="volume" min="0" max="20" step="0.01" /> |
| 1519 | <input class="neo-range-input" type="number" min="0" max="20" step="0.01" data-for="mirostat_tau_textgenerationwebui" id="mirostat_tau_counter_textgenerationwebui"> | 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 | </div> | 1517 | </div> |
| 1521 | <div class="alignitemscenter flex-container marginBot5 flexFlowColumn flexGrow flexShrink gap0"> | 1518 | <div class="alignitemscenter flex-container marginBot5 flexFlowColumn flexGrow flexShrink gap0"> |
| 1522 | <label> | ||
| 1523 | <small data-i18n="Mirostat Eta">Eta</small> | 1519 | <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 | <input class="neo-range-slider" type="range" id="mirostat_eta_textgenerationwebui" name="volume" min="0" max="1" step="0.01" /> | 1520 | <input class="neo-range-slider" type="range" id="mirostat_eta_textgenerationwebui" name="volume" min="0" max="1" step="0.01" /> |
| 1527 | <input class="neo-range-input" type="number" min="0" max="1" step="0.01" data-for="mirostat_eta_textgenerationwebui" id="mirostat_eta_counter_textgenerationwebui"> | 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 | </div> | 1522 | </div> |
| @@ -1583,18 +1577,14 @@ | |||
| 1583 | </label> | 1577 | </label> |
| 1584 | <label data-tg-type="ooba, tabby" class="checkbox_label flexGrow flexShrink" for="add_bos_token_textgenerationwebui"> | 1578 | <label data-tg-type="ooba, tabby" class="checkbox_label flexGrow flexShrink" for="add_bos_token_textgenerationwebui"> |
| 1585 | <input type="checkbox" id="add_bos_token_textgenerationwebui" /> | 1579 | <input type="checkbox" id="add_bos_token_textgenerationwebui" /> |
| 1586 | <label> | ||
| 1587 | <small data-i18n="Add BOS Token">Add BOS Token</small> | 1580 | <small data-i18n="Add BOS Token">Add BOS Token</small> |
| 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> | 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> |
| 1589 | </label> | 1582 | </label> |
| 1590 | </label> | ||
| 1591 | <label data-tg-type="ooba, llamacpp, tabby, koboldcpp, dreamgen" class="checkbox_label flexGrow flexShrink" for="ban_eos_token_textgenerationwebui"> | 1583 | <label data-tg-type="ooba, llamacpp, tabby, koboldcpp, dreamgen" class="checkbox_label flexGrow flexShrink" for="ban_eos_token_textgenerationwebui"> |
| 1592 | <input type="checkbox" id="ban_eos_token_textgenerationwebui" /> | 1584 | <input type="checkbox" id="ban_eos_token_textgenerationwebui" /> |
| 1593 | <label> | ||
| 1594 | <small data-i18n="Ban EOS Token">Ban EOS Token</small> | 1585 | <small data-i18n="Ban EOS Token">Ban EOS Token</small> |
| 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> | 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> |
| 1596 | </label> | 1587 | </label> |
| 1597 | </label> | ||
| 1598 | <label data-tg-type="vllm, aphrodite, infermaticai" class="checkbox_label" for="ignore_eos_token_textgenerationwebui"> | 1588 | <label data-tg-type="vllm, aphrodite, infermaticai" class="checkbox_label" for="ignore_eos_token_textgenerationwebui"> |
| 1599 | <input type="checkbox" id="ignore_eos_token_textgenerationwebui" /> | 1589 | <input type="checkbox" id="ignore_eos_token_textgenerationwebui" /> |
| 1600 | <small data-i18n="Ignore EOS Token">Ignore EOS Token</small> | 1590 | <small data-i18n="Ignore EOS Token">Ignore EOS Token</small> |
| @@ -1610,19 +1600,14 @@ | |||
| 1610 | </label> | 1600 | </label> |
| 1611 | <label data-tg-type="ooba, aphrodite, tabby" class="checkbox_label flexGrow flexShrink" for="temperature_last_textgenerationwebui"> | 1601 | <label data-tg-type="ooba, aphrodite, tabby" class="checkbox_label flexGrow flexShrink" for="temperature_last_textgenerationwebui"> |
| 1612 | <input type="checkbox" id="temperature_last_textgenerationwebui" /> | 1602 | <input type="checkbox" id="temperature_last_textgenerationwebui" /> |
| 1613 | <label> | ||
| 1614 | <small data-i18n="Temperature Last">Temperature Last</small> | 1603 | <small data-i18n="Temperature Last">Temperature Last</small> |
| 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> | 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> |
| 1616 | </label> | 1605 | </label> |
| 1617 | </label> | ||
| 1618 | <label data-tg-type="tabby" class="checkbox_label flexGrow flexShrink" for="speculative_ngram_textgenerationwebui"> | 1606 | <label data-tg-type="tabby" class="checkbox_label flexGrow flexShrink" for="speculative_ngram_textgenerationwebui"> |
| 1619 | <input type="checkbox" id="speculative_ngram_textgenerationwebui" /> | 1607 | <input type="checkbox" id="speculative_ngram_textgenerationwebui" /> |
| 1620 | <label> | ||
| 1621 | <small data-i18n="Speculative Ngram">Speculative Ngram</small> | 1608 | <small data-i18n="Speculative Ngram">Speculative Ngram</small> |
| 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> | 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> |
| 1623 | </label> | 1610 | </label> |
| 1624 | </label> | ||
| 1625 | |||
| 1626 | <label data-tg-type="vllm, aphrodite, infermaticai" class="checkbox_label" for="spaces_between_special_tokens_textgenerationwebui"> | 1611 | <label data-tg-type="vllm, aphrodite, infermaticai" class="checkbox_label" for="spaces_between_special_tokens_textgenerationwebui"> |
| 1627 | <input type="checkbox" id="spaces_between_special_tokens_textgenerationwebui" /> | 1612 | <input type="checkbox" id="spaces_between_special_tokens_textgenerationwebui" /> |
| 1628 | <small data-i18n="Spaces Between Special Tokens">Spaces Between Special Tokens</small> | 1613 | <small data-i18n="Spaces Between Special Tokens">Spaces Between Special Tokens</small> |
| @@ -1663,9 +1648,11 @@ | |||
| 1663 | </div> | 1648 | </div> |
| 1664 | </div> | 1649 | </div> |
| 1665 | </div> | 1650 | </div> |
| 1666 | <div class="range-block wide100p"> | 1651 | <div class="range-block wide100p" id="logit_bias_block_ooba"> |
| 1667 | <div id="logit_bias_textgenerationwebui" class="range-block-title title_restorable"> | 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 | <div id="textgen_logit_bias_new_entry" class="menu_button menu_button_icon"> | 1656 | <div id="textgen_logit_bias_new_entry" class="menu_button menu_button_icon"> |
| 1670 | <i class="fa-xs fa-solid fa-plus"></i> | 1657 | <i class="fa-xs fa-solid fa-plus"></i> |
| 1671 | <small data-i18n="Add">Add</small> | 1658 | <small data-i18n="Add">Add</small> |
| @@ -1678,7 +1665,7 @@ | |||
| 1678 | <div class="logit_bias_list"></div> | 1665 | <div class="logit_bias_list"></div> |
| 1679 | </div> | 1666 | </div> |
| 1680 | </div> | 1667 | </div> |
| 1681 | <div id="cfg_block_ooba" data-tg-type="ooba, tabby" class="wide100p"> | 1668 | <div id="cfg_block_ooba" data-tg-type="ooba, tabby" class="flex-container flexFlowColumn wide100p"> |
| 1682 | <hr class="width100p"> | 1669 | <hr class="width100p"> |
| 1683 | <h4 class="textAlignCenter"> | 1670 | <h4 class="textAlignCenter"> |
| 1684 | <span data-i18n="CFG">CFG</span> | 1671 | <span data-i18n="CFG">CFG</span> |
| @@ -1690,29 +1677,23 @@ | |||
| 1690 | <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"> | 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 | </div> | 1678 | </div> |
| 1692 | <div class="range-block"> | 1679 | <div class="range-block"> |
| 1693 | <div class="range-block-title justifyLeft"> | 1680 | <div class="alignitemscenter justifyCenter flex-container flexShrink"> |
| 1694 | <span data-i18n="Negative Prompt">Negative Prompt</span> | 1681 | <small data-i18n="Negative Prompt">Negative Prompt</small> |
| 1695 | <small> | ||
| 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> | 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> |
| 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 | </div> | 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 | </div> | 1685 | </div> |
| 1703 | </div> | 1686 | </div> |
| 1704 | <div id="grammar_block_ooba" data-tg-type="ooba,aphrodite" class="wide100p"> | 1687 | <div id="grammar_block_ooba" data-tg-type="ooba,aphrodite,tabby" class="wide100p"> |
| 1705 | <hr class="wide100p"> | 1688 | <hr class="wide100p"> |
| 1706 | <h4 class="wide100p textAlignCenter"> | 1689 | <h4 class="wide100p textAlignCenter"> |
| 1707 | <label> | 1690 | <div class="flex-container justifyCenter alignitemscenter"> |
| 1708 | <span data-i18n="Grammar String">Grammar String</span> | 1691 | <span data-i18n="Grammar String">Grammar String</span> |
| 1709 | <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> | 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 | <a href="https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md" target="_blank"> | 1693 | <a href="https://github.com/ggerganov/llama.cpp/blob/master/grammars/README.md" target="_blank"> |
| 1711 | <small> | ||
| 1712 | <div class="fa-solid fa-up-right-from-square note-link-span"></div> | 1694 | <div class="fa-solid fa-up-right-from-square note-link-span"></div> |
| 1713 | </small> | ||
| 1714 | </a> | 1695 | </a> |
| 1715 | </label> | 1696 | </div> |
| 1716 | </h4> | 1697 | </h4> |
| 1717 | <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> | 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 | </div> | 1699 | </div> |
| @@ -2077,7 +2058,7 @@ | |||
| 2077 | </span> | 2058 | </span> |
| 2078 | </div> | 2059 | </div> |
| 2079 | </div> | 2060 | </div> |
| 2080 | <div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub,azure_openai"> | 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 | <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand"> | 2062 | <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand"> |
| 2082 | <input id="openai_show_thoughts" type="checkbox" /> | 2063 | <input id="openai_show_thoughts" type="checkbox" /> |
| 2083 | <span data-i18n="Request model reasoning">Request model reasoning</span> | 2064 | <span data-i18n="Request model reasoning">Request model reasoning</span> |
| @@ -2144,7 +2125,7 @@ | |||
| 2144 | </div> | 2125 | </div> |
| 2145 | </div> | 2126 | </div> |
| 2146 | </div> | 2127 | </div> |
| 2147 | <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom,azure_openai"> | 2128 | <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom,electronhub,azure_openai"> |
| 2148 | <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias"> | 2129 | <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias"> |
| 2149 | Logit Bias | 2130 | Logit Bias |
| 2150 | </div> | 2131 | </div> |
| @@ -3035,6 +3016,10 @@ | |||
| 3035 | <h4 data-i18n="Claude Model">Claude Model</h4> | 3016 | <h4 data-i18n="Claude Model">Claude Model</h4> |
| 3036 | <select id="model_claude_select"> | 3017 | <select id="model_claude_select"> |
| 3037 | <optgroup label="Versions"> | 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 | <option value="claude-opus-4-1">claude-opus-4-1</option> | 3023 | <option value="claude-opus-4-1">claude-opus-4-1</option> |
| 3039 | <option value="claude-opus-4-1-20250805">claude-opus-4-1-20250805</option> | 3024 | <option value="claude-opus-4-1-20250805">claude-opus-4-1-20250805</option> |
| 3040 | <option value="claude-opus-4-0">claude-opus-4-0</option> | 3025 | <option value="claude-opus-4-0">claude-opus-4-0</option> |
| @@ -3082,7 +3067,7 @@ | |||
| 3082 | <div class="marginTopBot5"> | 3067 | <div class="marginTopBot5"> |
| 3083 | <div class="inline-drawer wide100p"> | 3068 | <div class="inline-drawer wide100p"> |
| 3084 | <div class="inline-drawer-toggle inline-drawer-header"> | 3069 | <div class="inline-drawer-toggle inline-drawer-header"> |
| 3085 | <b data-i18n="Model Order">OpenRouter Model Sorting</b> | 3070 | <b data-i18n="OpenRouter Model Sorting">OpenRouter Model Sorting</b> |
| 3086 | <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> | 3071 | <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> |
| 3087 | </div> | 3072 | </div> |
| 3088 | <div class="inline-drawer-content m-b-1"> | 3073 | <div class="inline-drawer-content m-b-1"> |
| @@ -3174,46 +3159,32 @@ | |||
| 3174 | <option value="gemini-2.5-pro-preview-06-05">gemini-2.5-pro-preview-06-05</option> | 3159 | <option value="gemini-2.5-pro-preview-06-05">gemini-2.5-pro-preview-06-05</option> |
| 3175 | <option value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option> | 3160 | <option value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option> |
| 3176 | <option value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option> | 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 | <option value="gemini-2.5-flash">gemini-2.5-flash</option> | 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 | <option value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option> | 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 | <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option> | 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 | <option value="gemini-2.5-flash-lite-preview-06-17">gemini-2.5-flash-lite-preview-06-17</option> | 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 | <option value="gemini-2.5-flash-image-preview">gemini-2.5-flash-image-preview</option> | 3169 | <option value="gemini-2.5-flash-image-preview">gemini-2.5-flash-image-preview</option> |
| 3184 | </optgroup> | 3170 | </optgroup> |
| 3185 | <optgroup label="Gemini 2.0"> | 3171 | <optgroup label="Gemini 2.0"> |
| 3186 | <option value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05 → 2.5-pro-exp-03-25</option> | 3172 | <option value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05 → 2.5-exp-03-25</option> |
| 3187 | <option value="gemini-2.0-pro-exp">gemini-2.0-pro-exp → 2.5-pro-exp-03-25</option> | 3173 | <option value="gemini-2.0-pro-exp">gemini-2.0-pro-exp → 2.5-exp-03-25</option> |
| 3188 | <option value="gemini-exp-1206">gemini-exp-1206 → 2.5-pro-exp-03-25</option> | 3174 | <option value="gemini-exp-1206">gemini-exp-1206 → 2.5-exp-03-25</option> |
| 3189 | <option value="gemini-2.0-flash-001">gemini-2.0-flash-001</option> | 3175 | <option value="gemini-2.0-flash-001">gemini-2.0-flash-001</option> |
| 3190 | <option value="gemini-2.0-flash-exp-image-generation">gemini-2.0-flash-exp-image-generation</option> | 3176 | <option value="gemini-2.0-flash-exp-image-generation">gemini-2.0-flash-exp-image-generation</option> |
| 3191 | <option value="gemini-2.0-flash-preview-image-generation">gemini-2.0-flash-preview-image-generation</option> | 3177 | <option value="gemini-2.0-flash-preview-image-generation">gemini-2.0-flash-preview-image-generation</option> |
| 3192 | <option value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option> | 3178 | <option value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option> |
| 3193 | <option value="gemini-2.0-flash">gemini-2.0-flash</option> | 3179 | <option value="gemini-2.0-flash">gemini-2.0-flash</option> |
| 3194 | <option value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21 → 2.5-flash-preview-04-17</option> | 3180 | <option value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21 → 2.5-flash-preview-05-20</option> |
| 3195 | <option value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219 → 2.5-flash-preview-04-17</option> | 3181 | <option value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219 → 2.5-flash-preview-05-20</option> |
| 3196 | <option value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp → 2.5-flash-preview-04-17</option> | 3182 | <option value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp → 2.5-flash-preview-05-20</option> |
| 3197 | <option value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option> | 3183 | <option value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option> |
| 3198 | <option value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option> | 3184 | <option value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option> |
| 3199 | <option value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option> | 3185 | <option value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option> |
| 3200 | <option value="gemini-2.0-flash-lite">gemini-2.0-flash-lite</option> | 3186 | <option value="gemini-2.0-flash-lite">gemini-2.0-flash-lite</option> |
| 3201 | </optgroup> | 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 | <optgroup label="Gemma"> | 3188 | <optgroup label="Gemma"> |
| 3218 | <option value="gemma-3n-e4b-it">gemma-3n-e4b-it</option> | 3189 | <option value="gemma-3n-e4b-it">gemma-3n-e4b-it</option> |
| 3219 | <option value="gemma-3n-e2b-it">gemma-3n-e2b-it</option> | 3190 | <option value="gemma-3n-e2b-it">gemma-3n-e2b-it</option> |
| @@ -3225,6 +3196,9 @@ | |||
| 3225 | <optgroup label="LearnLM"> | 3196 | <optgroup label="LearnLM"> |
| 3226 | <option value="learnlm-2.0-flash-experimental">learnlm-2.0-flash-experimental</option> | 3197 | <option value="learnlm-2.0-flash-experimental">learnlm-2.0-flash-experimental</option> |
| 3227 | </optgroup> | 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 | <optgroup id="google_other_models" label="Other"></optgroup> | 3202 | <optgroup id="google_other_models" label="Other"></optgroup> |
| 3229 | </select> | 3203 | </select> |
| 3230 | </div> | 3204 | </div> |
| @@ -3350,15 +3324,9 @@ | |||
| 3350 | <!-- data-mode="full" is for models that require a service account --> | 3324 | <!-- data-mode="full" is for models that require a service account --> |
| 3351 | <optgroup label="Gemini 2.5"> | 3325 | <optgroup label="Gemini 2.5"> |
| 3352 | <option value="gemini-2.5-pro">gemini-2.5-pro</option> | 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 | <option value="gemini-2.5-flash">gemini-2.5-flash</option> | 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 | <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option> | 3328 | <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option> |
| 3361 | <option value="gemini-2.5-flash-lite-preview-06-17">gemini-2.5-flash-lite-preview-06-17</option> | 3329 | <option value="gemini-2.5-flash-image">gemini-2.5-flash-image</option> |
| 3362 | <option value="gemini-2.5-flash-image-preview">gemini-2.5-flash-image-preview</option> | 3330 | <option value="gemini-2.5-flash-image-preview">gemini-2.5-flash-image-preview</option> |
| 3363 | </optgroup> | 3331 | </optgroup> |
| 3364 | <optgroup label="Gemini 2.0"> | 3332 | <optgroup label="Gemini 2.0"> |
| @@ -3383,67 +3351,7 @@ | |||
| 3383 | <div> | 3351 | <div> |
| 3384 | <h4 data-i18n="MistralAI Model">MistralAI Model</h4> | 3352 | <h4 data-i18n="MistralAI Model">MistralAI Model</h4> |
| 3385 | <select id="model_mistralai_select"> | 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 | </select> | 3355 | </select> |
| 3448 | </div> | 3356 | </div> |
| 3449 | </form> | 3357 | </form> |
| @@ -3473,6 +3381,9 @@ | |||
| 3473 | </div> | 3381 | </div> |
| 3474 | <div id="electronhub_form" data-source="electronhub"> | 3382 | <div id="electronhub_form" data-source="electronhub"> |
| 3475 | <h4 data-i18n="Electron Hub API Key">Electron Hub API Key</h4> | 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 | <div class="flex-container"> | 3387 | <div class="flex-container"> |
| 3477 | <input id="api_key_electronhub" name="api_key_electronhub" class="text_pole flex1" value="" type="text" autocomplete="off"> | 3388 | <input id="api_key_electronhub" name="api_key_electronhub" class="text_pole flex1" value="" type="text" autocomplete="off"> |
| 3478 | <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> | 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,11 +3391,44 @@ | |||
| 3480 | <div data-for="api_key_electronhub" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'."> | 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 | For privacy reasons, your API key will be hidden after you click 'Connect'. | 3392 | For privacy reasons, your API key will be hidden after you click 'Connect'. |
| 3482 | </div> | 3393 | </div> |
| 3394 | <div> | ||
| 3483 | <h4 data-i18n="Electron Hub Model">Electron Hub Model</h4> | 3395 | <h4 data-i18n="Electron Hub Model">Electron Hub Model</h4> |
| 3484 | <select id="model_electronhub_select"> | 3396 | <select id="model_electronhub_select"> |
| 3485 | <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> | 3397 | <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> |
| 3486 | </select> | 3398 | </select> |
| 3487 | </div> | 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> | ||
| 3431 | </div> | ||
| 3488 | <div id="nanogpt_form" data-source="nanogpt"> | 3432 | <div id="nanogpt_form" data-source="nanogpt"> |
| 3489 | <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4> | 3433 | <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4> |
| 3490 | <div class="flex-container"> | 3434 | <div class="flex-container"> |
| @@ -3666,19 +3610,13 @@ | |||
| 3666 | <div data-for="api_key_xai" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'."> | 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 | For privacy reasons, your API key will be hidden after you click 'Connect'. | 3611 | For privacy reasons, your API key will be hidden after you click 'Connect'. |
| 3668 | </div> | 3612 | </div> |
| 3613 | <div> | ||
| 3669 | <h4 data-i18n="xAI Model">xAI Model</h4> | 3614 | <h4 data-i18n="xAI Model">xAI Model</h4> |
| 3670 | <select id="model_xai_select"> | 3615 | <select id="model_xai_select"> |
| 3671 | <option value="grok-4-0709">grok-4-0709</option> | 3616 | <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> |
| 3672 | <option value="grok-3-beta">grok-3-beta</option> | ||
| 3673 | <option value="grok-3-fast-beta">grok-3-fast-beta</option> | ||
| 3674 | <option value="grok-3-mini-beta">grok-3-mini-beta</option> | ||
| 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> | 3617 | </select> |
| 3681 | </div> | 3618 | </div> |
| 3619 | </div> | ||
| 3682 | <div id="aimlapi_form" data-source="aimlapi"> | 3620 | <div id="aimlapi_form" data-source="aimlapi"> |
| 3683 | <h4> | 3621 | <h4> |
| 3684 | <span data-i18n="AI/ML API Key">AI/ML API Key</span> | 3622 | <span data-i18n="AI/ML API Key">AI/ML API Key</span> |
| @@ -4279,7 +4217,7 @@ | |||
| 4279 | Token Padding | 4217 | Token Padding |
| 4280 | </small> | 4218 | </small> |
| 4281 | </div> | 4219 | </div> |
| 4282 | <input id="token_padding" class="text_pole textarea_compact" type="number" min="-2048" max="2048" /> | 4220 | <input id="token_padding" class="text_pole textarea_compact" type="number" min="-2048" max="2048" step="1" /> |
| 4283 | </div> | 4221 | </div> |
| 4284 | </div> | 4222 | </div> |
| 4285 | <div> | 4223 | <div> |
| @@ -4959,7 +4897,6 @@ | |||
| 4959 | <small data-i18n="Smooth Streaming"> | 4897 | <small data-i18n="Smooth Streaming"> |
| 4960 | Smooth Streaming | 4898 | Smooth Streaming |
| 4961 | </small> | 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 | </div> | 4900 | </div> |
| 4964 | <div id="smooth_streaming_speed_control" class="flexBasis100p wide100p"> | 4901 | <div id="smooth_streaming_speed_control" class="flexBasis100p wide100p"> |
| 4965 | <input type="range" id="smooth_streaming_speed" name="smooth_streaming_speed" min="0" max="100" step="10" value="50"> | 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 | </div> | 4907 | </div> |
| 4971 | </div> | 4908 | </div> |
| 4972 | </label> | 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 | <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"> | 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 | <input id="play_message_sound" type="checkbox" /> | 4917 | <input id="play_message_sound" type="checkbox" /> |
| @@ -5339,15 +5281,18 @@ | |||
| 5339 | </div> | 5281 | </div> |
| 5340 | </div> | 5282 | </div> |
| 5341 | </div> | 5283 | </div> |
| 5342 | <div id="logo_block" class="drawer"> | 5284 | <div id="backgrounds-button" class="drawer"> |
| 5343 | <div id="site_logo" class="drawer-toggle drawer-header" title="Change Background Image" data-i18n="[title]Change Background Image"> | 5285 | <div id="backgrounds-drawer-toggle" class="drawer-toggle drawer-header" title="Change Background Image" data-i18n="[title]Change Background Image"> |
| 5344 | <div class="drawer-icon fa-solid fa-panorama fa-fw closedIcon"></div> | 5286 | <div class="drawer-icon fa-solid fa-panorama fa-fw closedIcon"></div> |
| 5345 | </div> | 5287 | </div> |
| 5346 | <div id="Backgrounds" class="drawer-content closedDrawer bg-drawer-layout"> | 5288 | <div id="Backgrounds" class="drawer-content closedDrawer bg-drawer-layout"> |
| 5347 | <div id="bg-header-fixed"> | 5289 | <div id="bg-header-fixed"> |
| 5348 | <div class="flex-container alignItemsBaseline wide100p"> | 5290 | <div class="bg-header-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 | <select id="background_fitting" class="text_pole" data-i18n="[title]Background Fitting" title="Background Fitting"> | 5296 | <select id="background_fitting" class="text_pole" data-i18n="[title]Background Fitting" title="Background Fitting"> |
| 5352 | <option value="classic" data-i18n="Classic">Classic</option> | 5297 | <option value="classic" data-i18n="Classic">Classic</option> |
| 5353 | <option value="cover" data-i18n="Cover">Cover</option> | 5298 | <option value="cover" data-i18n="Cover">Cover</option> |
| @@ -5359,16 +5304,25 @@ | |||
| 5359 | <i class="fa-solid fa-wand-magic"></i> | 5304 | <i class="fa-solid fa-wand-magic"></i> |
| 5360 | <span data-i18n="Auto-select">Auto-select</span> | 5305 | <span data-i18n="Auto-select">Auto-select</span> |
| 5361 | </div> | 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 | <i class="fa-solid fa-plus"></i> | 5308 | <div class="bg-header-row-2"> |
| 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 | </div> | 5310 | </div> |
| 5367 | </div> | 5311 | </div> |
| 5368 | <div id="bg-scrollable-content"> | 5312 | <div id="bg-scrollable-content"> |
| 5369 | <h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter"> | 5313 | <div class="heading-container-with-controls"> |
| 5314 | <h3 data-i18n="System Backgrounds" class="wide100p textAlignCenter heading-text"> | ||
| 5370 | System Backgrounds | 5315 | System Backgrounds |
| 5371 | </h3> | 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 | <div id="bg_menu_content" class="bg_list"> | 5326 | <div id="bg_menu_content" class="bg_list"> |
| 5373 | </div> | 5327 | </div> |
| 5374 | <h3 data-i18n="Chat Backgrounds" class="wide100p textAlignCenter"> | 5328 | <h3 data-i18n="Chat Backgrounds" class="wide100p textAlignCenter"> |
| @@ -5383,6 +5337,9 @@ | |||
| 5383 | <form id="form_bg_upload" style="display: none;"> | 5337 | <form id="form_bg_upload" style="display: none;"> |
| 5384 | <input type="file" id="add_bg_button" name="avatar" accept="image/jpeg,image/png,image/gif,image/bmp,image/svg+xml,video/*"> | 5338 | <input type="file" id="add_bg_button" name="avatar" accept="image/jpeg,image/png,image/gif,image/bmp,image/svg+xml,video/*"> |
| 5385 | </form> | 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 | </div> | 5343 | </div> |
| 5387 | </div> | 5344 | </div> |
| 5388 | <div id="extensions-settings-button" class="drawer"> | 5345 | <div id="extensions-settings-button" class="drawer"> |
| @@ -5540,7 +5497,10 @@ | |||
| 5540 | </div> | 5497 | </div> |
| 5541 | </div> | 5498 | </div> |
| 5542 | 5499 | ||
| 5543 | <h4 data-i18n="Persona Description">Persona Description</h4> | 5500 | <h4 class="flex-container alignItemsBaseline"> |
| 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 | <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> | 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 | <div class="flex-container justifySpaceBetween"> | 5506 | <div class="flex-container justifySpaceBetween"> |
| @@ -6291,7 +6251,7 @@ | |||
| 6291 | <div name="selectChatPopupHeader" class="flex-container alignitemscenter justifySpaceBetween flexGap10"> | 6251 | <div name="selectChatPopupHeader" class="flex-container alignitemscenter justifySpaceBetween flexGap10"> |
| 6292 | <div id="select_chat_import"> <!-- import chat popup header --> | 6252 | <div id="select_chat_import"> <!-- import chat popup header --> |
| 6293 | <form id="form_import_chat" action="javascript:void(null);" method="post" enctype="multipart/form-data" style="display: none;"> | 6253 | <form id="form_import_chat" action="javascript:void(null);" method="post" enctype="multipart/form-data" style="display: none;"> |
| 6294 | <input type="file" id="chat_import_file" accept=".json, .jsonl" name="avatar"> | 6254 | <input type="file" id="chat_import_file" accept=".json, .jsonl" multiple name="avatar"> |
| 6295 | <input id="chat_import_file_type" name="file_type" class="text_pole" value="" autocomplete="off" style="display: none;"> | 6255 | <input id="chat_import_file_type" name="file_type" class="text_pole" value="" autocomplete="off" style="display: none;"> |
| 6296 | <input id="chat_import_avatar_url" name="avatar_url" class="text_pole" value="" autocomplete="off" style="display: none;"> | 6256 | <input id="chat_import_avatar_url" name="avatar_url" class="text_pole" value="" autocomplete="off" style="display: none;"> |
| 6297 | <input id="chat_import_character_name" name="character_name" class="text_pole" value="" autocomplete="off" style="display: none;"> | 6257 | <input id="chat_import_character_name" name="character_name" class="text_pole" value="" autocomplete="off" style="display: none;"> |
| @@ -6322,10 +6282,9 @@ | |||
| 6322 | <i class="fa-solid fa-ellipsis-vertical"></i> | 6282 | <i class="fa-solid fa-ellipsis-vertical"></i> |
| 6323 | </div> | 6283 | </div> |
| 6324 | <div class="jg-menu"> | 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 | <div data-action="lock" class="jg-button jg-lock fa-solid fa-lock fa-fw pointer" data-i18n="[title]Lock" title="Lock"></div> | 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 | <div data-action="unlock" class="jg-button jg-unlock fa-solid fa-lock-open fa-fw pointer" data-i18n="[title]Unlock" title="Unlock"></div> | 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 | <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> | 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 | <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> | 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 | </div> | 6290 | </div> |
| @@ -6448,6 +6407,13 @@ | |||
| 6448 | </div> | 6407 | </div> |
| 6449 | </div> | 6408 | </div> |
| 6450 | <div name="perEntryOverridesBlock" class="flex-container wide100p alignitemscenter"> | 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 | <div class="world_entry_form_control flex1"> | 6417 | <div class="world_entry_form_control flex1"> |
| 6452 | <small class="textAlignCenter" data-i18n="Scan Depth">Scan Depth</small> | 6418 | <small class="textAlignCenter" data-i18n="Scan Depth">Scan Depth</small> |
| 6453 | <input class="text_pole margin0" name="scanDepth" type="number" placeholder="Use global setting" data-i18n="[placeholder]Use global setting" max="1000"> | 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 | <option value="4" data-role="2" data-i18n="at Depth AI"> | 6751 | <option value="4" data-role="2" data-i18n="at Depth AI"> |
| 6786 | @D 🤖 | 6752 | @D 🤖 |
| 6787 | </option> | 6753 | </option> |
| 6754 | <option value="7" data-role="" data-i18n="Outlet"> | ||
| 6755 | ➡️ Outlet | ||
| 6756 | </option> | ||
| 6788 | </select> | 6757 | </select> |
| 6789 | </div> | 6758 | </div> |
| 6790 | <div class="world_entry_form_control wi-enter-footer-text flex-container flexNoGap"> | 6759 | <div class="world_entry_form_control wi-enter-footer-text flex-container flexNoGap"> |
| @@ -139,9 +139,9 @@ | |||
| 139 | return el; | 139 | return el; |
| 140 | }, | 140 | }, |
| 141 | 141 | ||
| 142 | getPageLinkTag: function(index) { | 142 | getPageLinkTag: function(text) { |
| 143 | var pageLink = attributes.pageLink; | 143 | var pageLink = attributes.pageLink; |
| 144 | return pageLink ? `<a href="${pageLink}">${index}</a>` : `<a>${index}</a>`; | 144 | return pageLink ? `<a href="${pageLink}">${text}</a>` : `<a>${text}</a>`; |
| 145 | }, | 145 | }, |
| 146 | 146 | ||
| 147 | // Generate HTML for page numbers | 147 | // Generate HTML for page numbers |
| @@ -233,6 +233,8 @@ | |||
| 233 | 233 | ||
| 234 | var prevText = attributes.prevText; | 234 | var prevText = attributes.prevText; |
| 235 | var nextText = attributes.nextText; | 235 | var nextText = attributes.nextText; |
| 236 | var firstText = attributes.firstText; | ||
| 237 | var lastText = attributes.lastText; | ||
| 236 | var goButtonText = attributes.goButtonText; | 238 | var goButtonText = attributes.goButtonText; |
| 237 | 239 | ||
| 238 | var classPrefix = attributes.classPrefix; | 240 | var classPrefix = attributes.classPrefix; |
| @@ -240,6 +242,8 @@ | |||
| 240 | var ulClassName = attributes.ulClassName || ''; | 242 | var ulClassName = attributes.ulClassName || ''; |
| 241 | var prevClassName = attributes.prevClassName || ''; | 243 | var prevClassName = attributes.prevClassName || ''; |
| 242 | var nextClassName = attributes.nextClassName || ''; | 244 | var nextClassName = attributes.nextClassName || ''; |
| 245 | var firstClassName = attributes.firstClassName || ''; | ||
| 246 | var lastClassName = attributes.lastClassName || ''; | ||
| 243 | 247 | ||
| 244 | var html = ''; | 248 | var html = ''; |
| 245 | var sizeSelect = `<select class="J-paginationjs-size-select">`; | 249 | var sizeSelect = `<select class="J-paginationjs-size-select">`; |
| @@ -295,9 +299,11 @@ | |||
| 295 | if (showPrevious) { | 299 | if (showPrevious) { |
| 296 | if (currentPage <= 1) { | 300 | if (currentPage <= 1) { |
| 297 | if (!autoHidePrevious) { | 301 | if (!autoHidePrevious) { |
| 302 | html += `<li class="${classPrefix}-first ${disableClassName} ${firstClassName}"><a>${firstText}</a></li>`; | ||
| 298 | html += `<li class="${classPrefix}-prev ${disableClassName} ${prevClassName}"><a>${prevText}</a></li>`; | 303 | html += `<li class="${classPrefix}-prev ${disableClassName} ${prevClassName}"><a>${prevText}</a></li>`; |
| 299 | } | 304 | } |
| 300 | } else { | 305 | } else { |
| 306 | html += `<li class="${classPrefix}-first J-paginationjs-first ${firstClassName}" data-num="1" title="First page">${getPageLinkTag(firstText)}</li>`; | ||
| 301 | html += `<li class="${classPrefix}-prev J-paginationjs-previous ${prevClassName}" data-num="${currentPage - 1}" title="Previous page">${getPageLinkTag(prevText)}</li>`; | 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 | if (currentPage >= totalPage) { | 318 | if (currentPage >= totalPage) { |
| 313 | if (!autoHideNext) { | 319 | if (!autoHideNext) { |
| 314 | html += `<li class="${classPrefix}-next ${disableClassName} ${nextClassName}"><a>${nextText}</a></li>`; | 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 | } else { | 323 | } else { |
| 317 | html += `<li class="${classPrefix}-next J-paginationjs-next ${nextClassName}" data-num="${currentPage + 1}" title="Next page">${getPageLinkTag(nextText)}</li>`; | 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 | html += `</ul></div>`; | 328 | html += `</ul></div>`; |
| @@ -542,6 +550,14 @@ | |||
| 542 | this.go(this.model.pageNumber + 1, callback); | 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 | disable: function() { | 561 | disable: function() { |
| 546 | var self = this; | 562 | var self = this; |
| 547 | var source = self.isAsync ? 'async' : 'sync'; | 563 | var source = self.isAsync ? 'async' : 'sync'; |
| @@ -774,6 +790,38 @@ | |||
| 774 | if (!attributes.pageLink) return false; | 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 | // Go button click listener | 825 | // Go button click listener |
| 778 | el.on('click', '.J-paginationjs-go-button', function(event) { | 826 | el.on('click', '.J-paginationjs-go-button', function(event) { |
| 779 | var pageNumber = $('.J-paginationjs-go-pagenumber', el).val(); | 827 | var pageNumber = $('.J-paginationjs-go-pagenumber', el).val(); |
| @@ -833,6 +881,16 @@ | |||
| 833 | self.next(done); | 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 | // Disable | 894 | // Disable |
| 837 | container.on(eventPrefix + 'disable', function() { | 895 | container.on(eventPrefix + 'disable', function() { |
| 838 | self.disable(); | 896 | self.disable(); |
| @@ -892,6 +950,8 @@ | |||
| 892 | switch (options) { | 950 | switch (options) { |
| 893 | case 'previous': | 951 | case 'previous': |
| 894 | case 'next': | 952 | case 'next': |
| 953 | case 'first': | ||
| 954 | case 'last': | ||
| 895 | case 'go': | 955 | case 'go': |
| 896 | case 'disable': | 956 | case 'disable': |
| 897 | case 'enable': | 957 | case 'enable': |
| @@ -991,6 +1051,12 @@ | |||
| 991 | // 'Next' text | 1051 | // 'Next' text |
| 992 | nextText: '›', | 1052 | nextText: '›', |
| 993 | 1053 | ||
| 1054 | // 'First' text | ||
| 1055 | firstText: '«', | ||
| 1056 | |||
| 1057 | // 'Last' text | ||
| 1058 | lastText: '»', | ||
| 1059 | |||
| 994 | // Ellipsis text | 1060 | // Ellipsis text |
| 995 | ellipsisText: '...', | 1061 | ellipsisText: '...', |
| 996 | 1062 | ||
| @@ -1149,7 +1215,7 @@ | |||
| 1149 | 1215 | ||
| 1150 | // uninstall plugin | 1216 | // uninstall plugin |
| 1151 | function uninstallPlugin(target) { | 1217 | function uninstallPlugin(target) { |
| 1152 | var events = ['go', 'previous', 'next', 'disable', 'enable', 'refresh', 'show', 'hide', 'destroy']; | 1218 | var events = ['go', 'previous', 'next', 'first', 'last', 'disable', 'enable', 'refresh', 'show', 'hide', 'destroy']; |
| 1153 | 1219 | ||
| 1154 | // off all events | 1220 | // off all events |
| 1155 | $.each(events, function(index, value) { | 1221 | $.each(events, function(index, value) { |
| @@ -369,7 +369,7 @@ | |||
| 369 | "Anthropic's developer console": "وحدة تحكم المطور في Anthropic", | 369 | "Anthropic's developer console": "وحدة تحكم المطور في Anthropic", |
| 370 | "Claude Model": "نموذج Claude", | 370 | "Claude Model": "نموذج Claude", |
| 371 | "Window AI Model": "نموذج Window AI", | 371 | "Window AI Model": "نموذج Window AI", |
| 372 | "Model Order": "فرز نموذج OpenRouter", | 372 | "OpenRouter Model Sorting": "فرز نموذج OpenRouter", |
| 373 | "Alphabetically": "أبجديا", | 373 | "Alphabetically": "أبجديا", |
| 374 | "Price": "السعر (الأرخص)", | 374 | "Price": "السعر (الأرخص)", |
| 375 | "Context Size": "حجم السياق", | 375 | "Context Size": "حجم السياق", |
| @@ -369,7 +369,7 @@ | |||
| 369 | "Anthropic's developer console": "Anthropics Entwicklerkonsole", | 369 | "Anthropic's developer console": "Anthropics Entwicklerkonsole", |
| 370 | "Claude Model": "Claude-Modell", | 370 | "Claude Model": "Claude-Modell", |
| 371 | "Window AI Model": "Fenster AI-Modell", | 371 | "Window AI Model": "Fenster AI-Modell", |
| 372 | "Model Order": "Sortierung des OpenRouter-Modells", | 372 | "OpenRouter Model Sorting": "Sortierung des OpenRouter-Modells", |
| 373 | "Alphabetically": "Alphabetisch", | 373 | "Alphabetically": "Alphabetisch", |
| 374 | "Price": "Preis (am günstigsten)", | 374 | "Price": "Preis (am günstigsten)", |
| 375 | "Context Size": "Kontextgröße", | 375 | "Context Size": "Kontextgröße", |
| @@ -369,7 +369,7 @@ | |||
| 369 | "Anthropic's developer console": "la consola de desarrolladores de Anthropic", | 369 | "Anthropic's developer console": "la consola de desarrolladores de Anthropic", |
| 370 | "Claude Model": "Modelo de Claude", | 370 | "Claude Model": "Modelo de Claude", |
| 371 | "Window AI Model": "Modelo de Window AI", | 371 | "Window AI Model": "Modelo de Window AI", |
| 372 | "Model Order": "Clasificación de modelos de OpenRouter", | 372 | "OpenRouter Model Sorting": "Clasificación de modelos de OpenRouter", |
| 373 | "Alphabetically": "Alfabéticamente", | 373 | "Alphabetically": "Alfabéticamente", |
| 374 | "Price": "Precio (más barato)", | 374 | "Price": "Precio (más barato)", |
| 375 | "Context Size": "Tamaño del contexto", | 375 | "Context Size": "Tamaño del contexto", |
| @@ -348,7 +348,7 @@ | |||
| 348 | "Anthropic's developer console": "Console de développement d'Anthropic", | 348 | "Anthropic's developer console": "Console de développement d'Anthropic", |
| 349 | "Claude Model": "Modèle Claude", | 349 | "Claude Model": "Modèle Claude", |
| 350 | "Window AI Model": "Modèle Window AI", | 350 | "Window AI Model": "Modèle Window AI", |
| 351 | "Model Order": "Tri des modèles OpenRouter", | 351 | "OpenRouter Model Sorting": "Tri des modèles OpenRouter", |
| 352 | "Alphabetically": "Alphabétiquement", | 352 | "Alphabetically": "Alphabétiquement", |
| 353 | "Price": "Prix (le moins cher)", | 353 | "Price": "Prix (le moins cher)", |
| 354 | "Context Size": "Taille du contexte", | 354 | "Context Size": "Taille du contexte", |
| @@ -1566,7 +1566,7 @@ | |||
| 1566 | "These files are available for all characters in the current chat.": "Ces fichiers sont disponibles pour tous les personnages du chat actuel.", | 1566 | "These files are available for all characters in the current chat.": "Ces fichiers sont disponibles pour tous les personnages du chat actuel.", |
| 1567 | "Image Captioning": "Légende des images", | 1567 | "Image Captioning": "Légende des images", |
| 1568 | "Local": "Local", | 1568 | "Local": "Local", |
| 1569 | "Multimodal (OpenAI / Anthropic / llama / Google)": "Multimodal (OpenAI / Anthropic / llama / Google)", | 1569 | "Multimodal": "Multimodal", |
| 1570 | "Extras": "Extras", | 1570 | "Extras": "Extras", |
| 1571 | "Horde": "Horde", | 1571 | "Horde": "Horde", |
| 1572 | "API": "API", | 1572 | "API": "API", |
| @@ -367,7 +367,7 @@ | |||
| 367 | "Anthropic's developer console": "Uppbyggingaraðilar Forritara stjórnborð", | 367 | "Anthropic's developer console": "Uppbyggingaraðilar Forritara stjórnborð", |
| 368 | "Claude Model": "Claude módel", | 368 | "Claude Model": "Claude módel", |
| 369 | "Window AI Model": "Vindauga AI módel", | 369 | "Window AI Model": "Vindauga AI módel", |
| 370 | "Model Order": "OpenRouter líkanaflokkun", | 370 | "OpenRouter Model Sorting": "OpenRouter líkanaflokkun", |
| 371 | "Alphabetically": "Stafrófsröð", | 371 | "Alphabetically": "Stafrófsröð", |
| 372 | "Price": "Verð (ódýrast)", | 372 | "Price": "Verð (ódýrast)", |
| 373 | "Context Size": "Samhengisstærð", | 373 | "Context Size": "Samhengisstærð", |
| @@ -369,7 +369,7 @@ | |||
| 369 | "Anthropic's developer console": "Console dello sviluppatore di Anthropic", | 369 | "Anthropic's developer console": "Console dello sviluppatore di Anthropic", |
| 370 | "Claude Model": "Modello Claude", | 370 | "Claude Model": "Modello Claude", |
| 371 | "Window AI Model": "Modello AI di Window", | 371 | "Window AI Model": "Modello AI di Window", |
| 372 | "Model Order": "Ordinamento dei modelli OpenRouter", | 372 | "OpenRouter Model Sorting": "Ordinamento dei modelli OpenRouter", |
| 373 | "Alphabetically": "In ordine alfabetico", | 373 | "Alphabetically": "In ordine alfabetico", |
| 374 | "Price": "Prezzo (il più economico)", | 374 | "Price": "Prezzo (il più economico)", |
| 375 | "Context Size": "Dimensione del contesto", | 375 | "Context Size": "Dimensione del contesto", |
| @@ -369,7 +369,7 @@ | |||
| 369 | "Anthropic's developer console": "Anthropicの開発者コンソール", | 369 | "Anthropic's developer console": "Anthropicの開発者コンソール", |
| 370 | "Claude Model": "Claudeモデル", | 370 | "Claude Model": "Claudeモデル", |
| 371 | "Window AI Model": "Window AIモデル", | 371 | "Window AI Model": "Window AIモデル", |
| 372 | "Model Order": "OpenRouter モデルのソート", | 372 | "OpenRouter Model Sorting": "OpenRouter モデルのソート", |
| 373 | "Alphabetically": "アルファベット順", | 373 | "Alphabetically": "アルファベット順", |
| 374 | "Price": "価格(最安値)", | 374 | "Price": "価格(最安値)", |
| 375 | "Context Size": "コンテキストサイズ", | 375 | "Context Size": "コンテキストサイズ", |
| @@ -374,7 +374,7 @@ | |||
| 374 | "Slack and Poe cookies will not work here, do not bother trying.": "Slack과 Poe 쿠키는 여기서 작동하지 않습니다. 시도하지 마세요.", | 374 | "Slack and Poe cookies will not work here, do not bother trying.": "Slack과 Poe 쿠키는 여기서 작동하지 않습니다. 시도하지 마세요.", |
| 375 | "Claude Model": "Claude 모델", | 375 | "Claude Model": "Claude 모델", |
| 376 | "Window AI Model": "Window AI 모델", | 376 | "Window AI Model": "Window AI 모델", |
| 377 | "Model Order": "OpenRouter 모델 정렬", | 377 | "OpenRouter Model Sorting": "OpenRouter 모델 정렬", |
| 378 | "Alphabetically": "알파벳순", | 378 | "Alphabetically": "알파벳순", |
| 379 | "Price": "가격(가장 저렴)", | 379 | "Price": "가격(가장 저렴)", |
| 380 | "Context Size": "컨텍스트 크기", | 380 | "Context Size": "컨텍스트 크기", |
| @@ -367,7 +367,7 @@ | |||
| 367 | "Anthropic's developer console": "Anthropic's ontwikkelaarsconsole", | 367 | "Anthropic's developer console": "Anthropic's ontwikkelaarsconsole", |
| 368 | "Claude Model": "Claude-model", | 368 | "Claude Model": "Claude-model", |
| 369 | "Window AI Model": "Window AI-model", | 369 | "Window AI Model": "Window AI-model", |
| 370 | "Model Order": "Sorteren van OpenRouter-modellen", | 370 | "OpenRouter Model Sorting": "Sorteren van OpenRouter-modellen", |
| 371 | "Alphabetically": "Alfabetisch", | 371 | "Alphabetically": "Alfabetisch", |
| 372 | "Price": "Prijs (goedkoopste)", | 372 | "Price": "Prijs (goedkoopste)", |
| 373 | "Context Size": "Contextgrootte", | 373 | "Context Size": "Contextgrootte", |
| @@ -369,7 +369,7 @@ | |||
| 369 | "Anthropic's developer console": "console de desenvolvedor da Anthropic", | 369 | "Anthropic's developer console": "console de desenvolvedor da Anthropic", |
| 370 | "Claude Model": "Modelo Claude", | 370 | "Claude Model": "Modelo Claude", |
| 371 | "Window AI Model": "Modelo Window AI", | 371 | "Window AI Model": "Modelo Window AI", |
| 372 | "Model Order": "Classificação de modelo OpenRouter", | 372 | "OpenRouter Model Sorting": "Classificação de modelo OpenRouter", |
| 373 | "Alphabetically": "Alfabeticamente", | 373 | "Alphabetically": "Alfabeticamente", |
| 374 | "Price": "Preço (mais barato)", | 374 | "Price": "Preço (mais barato)", |
| 375 | "Context Size": "Tamanho do contexto", | 375 | "Context Size": "Tamanho do contexto", |
| @@ -24,7 +24,7 @@ | |||
| 24 | "Mirostat_Tau_desc": "Целевая перплексия.", | 24 | "Mirostat_Tau_desc": "Целевая перплексия.", |
| 25 | "Mirostat_Eta_desc": "Скорость обучения Mirostat.", | 25 | "Mirostat_Eta_desc": "Скорость обучения Mirostat.", |
| 26 | "Temperature Last": "Температура последней", | 26 | "Temperature Last": "Температура последней", |
| 27 | "LLaMA / Mistral / Yi models only": "Только для моделей LLaMA / Mistral / Yi. Перед этим обязательно выберите подходящий токенизатор.\nПоследовательности, которых не должно быть на выходе.\nОдна на строку. Текст или [идентификаторы токенов].\nМногие токены имеют пробел впереди. Используйте счетчик токенов, если не уверены, что правильно их определяете.", | 27 | "LLaMA / Mistral / Yi models only": "Только для моделей LLaMA / Mistral / Yi. Перед этим обязательно выберите подходящий токенизатор.\nПоследовательности, которых не должно быть на выходе.\nОдна на строку. Текст или [идентификаторы токенов].\nМногие токены имеют пробел впереди. Используйте счётчик токенов, если не уверены, что правильно их определяете.", |
| 28 | "Example: some text [42, 69, 1337]": "Пример:\nкакой-то текст\n[42, 69, 1337]", | 28 | "Example: some text [42, 69, 1337]": "Пример:\nкакой-то текст\n[42, 69, 1337]", |
| 29 | "Classifier Free Guidance. More helpful tip coming soon": "Classifier Free Guidance. Чуть позже опишем более подробно", | 29 | "Classifier Free Guidance. More helpful tip coming soon": "Classifier Free Guidance. Чуть позже опишем более подробно", |
| 30 | "Scale": "Scale", | 30 | "Scale": "Scale", |
| @@ -91,7 +91,7 @@ | |||
| 91 | "Impersonation prompt": "Промпт для перевоплощения", | 91 | "Impersonation prompt": "Промпт для перевоплощения", |
| 92 | "Prompt that is used for Impersonation function": "Промпт, применяемый при генерации действий от лица пользователя", | 92 | "Prompt that is used for Impersonation function": "Промпт, применяемый при генерации действий от лица пользователя", |
| 93 | "Logit Bias": "Смещение логитов", | 93 | "Logit Bias": "Смещение логитов", |
| 94 | "Helps to ban or reenforce the usage of certain words": "Запрещает или поощряет использование определенных слов", | 94 | "Helps to ban or reenforce the usage of certain words": "Запрещает или поощряет использование определенных слов.", |
| 95 | "View / Edit bias preset": "Просмотр / Редактирование пресета смещения", | 95 | "View / Edit bias preset": "Просмотр / Редактирование пресета смещения", |
| 96 | "Add bias entry": "Добавить правило смещения", | 96 | "Add bias entry": "Добавить правило смещения", |
| 97 | "Connect": "Подключиться", | 97 | "Connect": "Подключиться", |
| @@ -201,8 +201,8 @@ | |||
| 201 | "Bubbles": "Пузыри", | 201 | "Bubbles": "Пузыри", |
| 202 | "No Blur Effect": "Отключить размытие", | 202 | "No Blur Effect": "Отключить размытие", |
| 203 | "No Text Shadows": "Отключить тень текста", | 203 | "No Text Shadows": "Отключить тень текста", |
| 204 | "Waifu Mode": "Режим вайфу", | 204 | "Waifu Mode": "Режим визуальной новеллы", |
| 205 | "Message Timer": "Таймер сообщений", | 205 | "Message Timer": "Длительность генерации сообщений", |
| 206 | "Model Icon": "Значки моделей", | 206 | "Model Icon": "Значки моделей", |
| 207 | "Advanced Character Search": "Расширенный поиск по персонажам", | 207 | "Advanced Character Search": "Расширенный поиск по персонажам", |
| 208 | "Allow {{char}}: in bot messages": "Показывать {{char}}: в ответах", | 208 | "Allow {{char}}: in bot messages": "Показывать {{char}}: в ответах", |
| @@ -211,12 +211,12 @@ | |||
| 211 | "Lorebook Import Dialog": "Показывать окно импорта лорбука", | 211 | "Lorebook Import Dialog": "Показывать окно импорта лорбука", |
| 212 | "MUI Preset": "Пресет MUI:", | 212 | "MUI Preset": "Пресет MUI:", |
| 213 | "If set in the advanced character definitions, this field will be displayed in the characters list.": "Если это поле задано в расширенных параметрах персонажа, оно будет отображаться в списке персонажей.", | 213 | "If set in the advanced character definitions, this field will be displayed in the characters list.": "Если это поле задано в расширенных параметрах персонажа, оно будет отображаться в списке персонажей.", |
| 214 | "Relaxed API URLS": "Смягчённые адреса API", | 214 | "Relaxed API URLS": "\"Ленивые\" адреса API", |
| 215 | "Custom CSS": "Пользовательский CSS", | 215 | "Custom CSS": "Пользовательский CSS", |
| 216 | "Mancer Model": "Модель Mancer", | 216 | "Mancer Model": "Модель Mancer", |
| 217 | "API Type": "Тип API", | 217 | "API Type": "Тип API", |
| 218 | "Aphrodite API key": "Ключ от API Aphrodite", | 218 | "Aphrodite API key": "Ключ от API Aphrodite", |
| 219 | "Relax message trim in Groups": "Мягкая обрезка сообщений в группах", | 219 | "Relax message trim in Groups": "Смягчить требования к сообщениям ИИ в группах", |
| 220 | "Characters Hotswap": "HotSwap (смена персонажей на лету)", | 220 | "Characters Hotswap": "HotSwap (смена персонажей на лету)", |
| 221 | "Request token probabilities": "Запрашивать вероятность токена", | 221 | "Request token probabilities": "Запрашивать вероятность токена", |
| 222 | "Movable UI Panels": "Подвижные панели UI", | 222 | "Movable UI Panels": "Подвижные панели UI", |
| @@ -229,10 +229,10 @@ | |||
| 229 | "Text Shadow Width": "Размер теней текста", | 229 | "Text Shadow Width": "Размер теней текста", |
| 230 | "Swipes": "Свайпы", | 230 | "Swipes": "Свайпы", |
| 231 | "Miscellaneous": "Разное", | 231 | "Miscellaneous": "Разное", |
| 232 | "Background Sound Only": "Только фоновый звук", | 232 | "Background Sound Only": "Только фоновые звуки", |
| 233 | "Auto-load Last Chat": "Автозагрузка последнего чата", | 233 | "Auto-load Last Chat": "Автозагрузка последнего чата", |
| 234 | "Auto-save Message Edits": "Автоматически сохранять отредактированные сообщения", | 234 | "Auto-save Message Edits": "Автоматически сохранять отредактированные сообщения", |
| 235 | "Auto-fix Markdown": "Автоисправление разметки", | 235 | "Auto-fix Markdown": "Автоисправление разметки Markdown", |
| 236 | "Auto-scroll Chat": "Автоматическая прокрутка чата", | 236 | "Auto-scroll Chat": "Автоматическая прокрутка чата", |
| 237 | "Send on Enter": "Отправка на Enter", | 237 | "Send on Enter": "Отправка на Enter", |
| 238 | "Debug Menu": "Меню отладки", | 238 | "Debug Menu": "Меню отладки", |
| @@ -329,7 +329,7 @@ | |||
| 329 | "Global Lore First": "Сначала глобальный лор", | 329 | "Global Lore First": "Сначала глобальный лор", |
| 330 | "Recursive Scan": "Рекурсивное сканирование", | 330 | "Recursive Scan": "Рекурсивное сканирование", |
| 331 | "Case Sensitive": "Учитывать регистр", | 331 | "Case Sensitive": "Учитывать регистр", |
| 332 | "Alert On Overflow": "Оповещение о переполнении", | 332 | "Alert On Overflow": "Оповещение о превышении бюджета", |
| 333 | "Use Probability": "Использовать вероятность", | 333 | "Use Probability": "Использовать вероятность", |
| 334 | "Exclude from recursion": "Исключить из рекурсии", | 334 | "Exclude from recursion": "Исключить из рекурсии", |
| 335 | "Entry Title/Memo": "Название или заметка о записи", | 335 | "Entry Title/Memo": "Название или заметка о записи", |
| @@ -354,7 +354,7 @@ | |||
| 354 | "Chat Background": "Фон чата", | 354 | "Chat Background": "Фон чата", |
| 355 | "UI Background": "Фон UI", | 355 | "UI Background": "Фон UI", |
| 356 | "Mad Lab Mode": "Режим безумца", | 356 | "Mad Lab Mode": "Режим безумца", |
| 357 | "Show Message Token Count": "Счетчик токенов сообщения", | 357 | "Show Message Token Count": "Показывать кол-во токенов в сообщении", |
| 358 | "Compact Input Area (Mobile)": "Компактная зона ввода", | 358 | "Compact Input Area (Mobile)": "Компактная зона ввода", |
| 359 | "Zen Sliders": "Дзен слайдеры", | 359 | "Zen Sliders": "Дзен слайдеры", |
| 360 | "UI Border": "Границы UI", | 360 | "UI Border": "Границы UI", |
| @@ -363,7 +363,7 @@ | |||
| 363 | "Tags as Folders": "Теги как папки", | 363 | "Tags as Folders": "Теги как папки", |
| 364 | "Streaming FPS": "FPS для стриминга", | 364 | "Streaming FPS": "FPS для стриминга", |
| 365 | "Gestures": "Жесты", | 365 | "Gestures": "Жесты", |
| 366 | "Message IDs": "ID сообщений", | 366 | "Message IDs": "Номера сообщений", |
| 367 | "Prefer Character Card Prompt": "Приоритет промпту из карточки персонажа", | 367 | "Prefer Character Card Prompt": "Приоритет промпту из карточки персонажа", |
| 368 | "Prefer Character Card Jailbreak": "Приоритет джейлбрейку из карточки персонажа", | 368 | "Prefer Character Card Jailbreak": "Приоритет джейлбрейку из карточки персонажа", |
| 369 | "Press Send to continue": "Кнопка отправки продолжает сообщение", | 369 | "Press Send to continue": "Кнопка отправки продолжает сообщение", |
| @@ -372,7 +372,7 @@ | |||
| 372 | "Never resize avatars": "Не менять размер аватарок", | 372 | "Never resize avatars": "Не менять размер аватарок", |
| 373 | "Show avatar filenames": "Показывать названия файлов аватарок", | 373 | "Show avatar filenames": "Показывать названия файлов аватарок", |
| 374 | "Import Card Tags": "Импортировать теги карточки", | 374 | "Import Card Tags": "Импортировать теги карточки", |
| 375 | "Confirm message deletion": "Подтверждение удаления сообщений", | 375 | "Confirm message deletion": "Спрашивать при удалении сообщений", |
| 376 | "Spoiler Free Mode": "Режим без спойлеров", | 376 | "Spoiler Free Mode": "Режим без спойлеров", |
| 377 | "Auto-swipe": "Автоматические свайпы", | 377 | "Auto-swipe": "Автоматические свайпы", |
| 378 | "Minimum generated message length": "Минимальная длина сгенерированных сообщений", | 378 | "Minimum generated message length": "Минимальная длина сгенерированных сообщений", |
| @@ -392,11 +392,11 @@ | |||
| 392 | "Always show the full list of the Message Actions context items for chat messages, instead of hiding them behind '...'": "Всегда показывать полный список действий с сообщением, а не прятать их за '...'.", | 392 | "Always show the full list of the Message Actions context items for chat messages, instead of hiding them behind '...'": "Всегда показывать полный список действий с сообщением, а не прятать их за '...'.", |
| 393 | "Alternative UI for numeric sampling parameters with fewer steps": "Уменьшить кол-во шагов для параметров, регулируемых слайдерами.", | 393 | "Alternative UI for numeric sampling parameters with fewer steps": "Уменьшить кол-во шагов для параметров, регулируемых слайдерами.", |
| 394 | "Entirely unrestrict all numeric sampling parameters": "Снять ограничения со всех числовых сэмплеров.", | 394 | "Entirely unrestrict all numeric sampling parameters": "Снять ограничения со всех числовых сэмплеров.", |
| 395 | "Time the AI's message generation, and show the duration in the chat log": "Время генерации сообщений ИИ и его показ в журнале чата.", | 395 | "Time the AI's message generation, and show the duration in the chat log": "Засекать время, за которое было сгенерировано сообщение, и отображать эту информацию в чате.", |
| 396 | "Show a timestamp for each message in the chat log": "Показывать временную метку для каждого сообщения в журнале чата.", | 396 | "Show a timestamp for each message in the chat log": "Показывать временную метку для каждого сообщения в журнале чата.", |
| 397 | "Show an icon for the API that generated the message": "Показать значок API, сгенерировавшего сообщение.", | 397 | "Show an icon for the API that generated the message": "Показывать значок API, сгенерировавшего сообщение.", |
| 398 | "Show sequential message numbers in the chat log": "Показывать порядковые номера сообщений в журнале чата.", | 398 | "Show sequential message numbers in the chat log": "Показывать порядковые номера сообщений в чате.", |
| 399 | "Show the number of tokens in each message in the chat log": "Показать количество токенов в каждом сообщении в журнале чата.", | 399 | "Show the number of tokens in each message in the chat log": "Показывать количество токенов в каждом сообщении в чате.", |
| 400 | "Single-row message input area. Mobile only, no effect on PC": "Однорядная область ввода сообщений. Только для мобильных устройств, на ПК не работает.", | 400 | "Single-row message input area. Mobile only, no effect on PC": "Однорядная область ввода сообщений. Только для мобильных устройств, на ПК не работает.", |
| 401 | "In the Character Management panel, show quick selection buttons for favorited characters": "На панели управления персонажами будут отображены кнопки быстрого выбора для избранных персонажей.", | 401 | "In the Character Management panel, show quick selection buttons for favorited characters": "На панели управления персонажами будут отображены кнопки быстрого выбора для избранных персонажей.", |
| 402 | "Show tagged character folders in the character list": "Отобразить теговые папки с персонажами в списке персонажей.", | 402 | "Show tagged character folders in the character list": "Отобразить теговые папки с персонажами в списке персонажей.", |
| @@ -404,7 +404,7 @@ | |||
| 404 | "Only play a sound when ST's browser tab is unfocused": "Воспроизводить звук только тогда, когда вкладка браузера ST не выбрана.", | 404 | "Only play a sound when ST's browser tab is unfocused": "Воспроизводить звук только тогда, когда вкладка браузера ST не выбрана.", |
| 405 | "Reduce the formatting requirements on API URLs": "Снижение требований к форматированию URL-адресов API.", | 405 | "Reduce the formatting requirements on API URLs": "Снижение требований к форматированию URL-адресов API.", |
| 406 | "Ask to import the World Info/Lorebook for every new character with embedded lorebook. If unchecked, a brief message will be shown instead": "Спрашивать разрешение на импорт лорбука для всех персонажей со встроенным лорбуком. При выключенной опции вместо этого будет показываться короткое сообщение", | 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 | "Restore unsaved user input on page refresh": "Восстановление несохраненного пользовательского запроса при обновлении страницы.", | 407 | "Restore unsaved user input on page refresh": "Восстановление несохраненного пользовательского сообщения при обновлении страницы.", |
| 408 | "Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "Позволяет перемещать некоторые элементы интерфейса путем их перетаскивания. Только для ПК, на телефонах не работает.", | 408 | "Allow repositioning certain UI elements by dragging them. PC only, no effect on mobile": "Позволяет перемещать некоторые элементы интерфейса путем их перетаскивания. Только для ПК, на телефонах не работает.", |
| 409 | "MovingUI preset. Predefined/saved draggable positions": "Пресет для MovingUI. Предопределенные/сохраненные позиции для перетаскивания.", | 409 | "MovingUI preset. Predefined/saved draggable positions": "Пресет для MovingUI. Предопределенные/сохраненные позиции для перетаскивания.", |
| 410 | "Save movingUI changes to a new file": "Сохранение изменений MovingUI в новый файл.", | 410 | "Save movingUI changes to a new file": "Сохранение изменений MovingUI в новый файл.", |
| @@ -419,7 +419,7 @@ | |||
| 419 | "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "Показывать кнопки со стрелками на последнем сообщении в чате, чтобы генерировать альтернативные ответы ИИ. Как для ПК, так и для мобильных устройств.", | 419 | "Show arrow buttons on the last in-chat message to generate alternative AI responses. Both PC and mobile": "Показывать кнопки со стрелками на последнем сообщении в чате, чтобы генерировать альтернативные ответы ИИ. Как для ПК, так и для мобильных устройств.", |
| 420 | "Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "Позволяет использовать жесты смахивания на последнем сообщении в чате, чтобы вызвать альтернативную генерацию. Только для мобильных устройств, на ПК не работает.", | 420 | "Allow using swiping gestures on the last in-chat message to trigger swipe generation. Mobile only, no effect on PC": "Позволяет использовать жесты смахивания на последнем сообщении в чате, чтобы вызвать альтернативную генерацию. Только для мобильных устройств, на ПК не работает.", |
| 421 | "Save edits to messages without confirmation as you type": "Сохранять правки в сообщениях без подтверждения по мере ввода текста.", | 421 | "Save edits to messages without confirmation as you type": "Сохранять правки в сообщениях без подтверждения по мере ввода текста.", |
| 422 | "Skip encoding and characters in message text, allowing a subset of HTML markup as well as Markdown": "Не кодировать символы < и > в тексте сообщения, что позволяет использовать подмножество HTML-разметки, а также Markdown.", | 422 | "Skip encoding and characters in message text, allowing a subset of HTML markup as well as Markdown": "Не кодировать символы < и > в тексте сообщения, что позволяет использовать часть возможностей HTML-разметки, а также разметку Markdown.", |
| 423 | "Allow AI messages in groups to contain lines spoken by other group members": "Разрешить ИИ в группах генерировать строчки за других участников группы в своих сообщениях.", | 423 | "Allow AI messages in groups to contain lines spoken by other group members": "Разрешить ИИ в группах генерировать строчки за других участников группы в своих сообщениях.", |
| 424 | "Requests logprobs from the API for the Token Probabilities feature": "Запросить логпробы из API для функции Token Probabilities.", | 424 | "Requests logprobs from the API for the Token Probabilities feature": "Запросить логпробы из API для функции Token Probabilities.", |
| 425 | "Automatically reject and re-generate AI message based on configurable criteria": "Автоматическое отклонение и повторная генерация сообщений AI на основе настраиваемых критериев.", | 425 | "Automatically reject and re-generate AI message based on configurable criteria": "Автоматическое отклонение и повторная генерация сообщений AI на основе настраиваемых критериев.", |
| @@ -549,7 +549,7 @@ | |||
| 549 | "Character Management": "Управление персонажами", | 549 | "Character Management": "Управление персонажами", |
| 550 | "Locked = Character Management panel will stay open": "Закреплено = Панель управление персонажами останется открытой", | 550 | "Locked = Character Management panel will stay open": "Закреплено = Панель управление персонажами останется открытой", |
| 551 | "Select/Create Characters": "Выбрать/Создать персонажа", | 551 | "Select/Create Characters": "Выбрать/Создать персонажа", |
| 552 | "Token counts may be inaccurate and provided just for reference.": "Счетчик токенов может быть неточным, используйте как ориентир", | 552 | "Token counts may be inaccurate and provided just for reference.": "Счётчик токенов может быть неточным, используйте как ориентир", |
| 553 | "Click to select a new avatar for this character": "Нажмите чтобы выбрать новый аватар для этого персонажа", | 553 | "Click to select a new avatar for this character": "Нажмите чтобы выбрать новый аватар для этого персонажа", |
| 554 | "Example: [{{user}} is a 28-year-old Romanian cat girl.]": "Пример:\n [{{user}} is a 28-year-old Romanian cat girl.]", | 554 | "Example: [{{user}} is a 28-year-old Romanian cat girl.]": "Пример:\n [{{user}} is a 28-year-old Romanian cat girl.]", |
| 555 | "Toggle grid view": "Сменить вид сетки", | 555 | "Toggle grid view": "Сменить вид сетки", |
| @@ -718,7 +718,7 @@ | |||
| 718 | "Google Model": "Модель Google", | 718 | "Google Model": "Модель Google", |
| 719 | "Cohere API Key": "Ключ от API Cohere", | 719 | "Cohere API Key": "Ключ от API Cohere", |
| 720 | "Cohere Model": "Модель Cohere", | 720 | "Cohere Model": "Модель Cohere", |
| 721 | "Model Order": "Сортировка моделей OpenRouter", | 721 | "OpenRouter Model Sorting": "Сортировка моделей OpenRouter", |
| 722 | "Alphabetically": "По алфавиту", | 722 | "Alphabetically": "По алфавиту", |
| 723 | "Price": "По цене (наиболее низкая)", | 723 | "Price": "По цене (наиболее низкая)", |
| 724 | "Context Size": "По размеру контекста", | 724 | "Context Size": "По размеру контекста", |
| @@ -1024,10 +1024,10 @@ | |||
| 1024 | "image_inlining_hint_2": ". Также это можно сделать через меню", | 1024 | "image_inlining_hint_2": ". Также это можно сделать через меню", |
| 1025 | "image_inlining_hint_3": ".", | 1025 | "image_inlining_hint_3": ".", |
| 1026 | "Contest Winners": "Победители конкурса", | 1026 | "Contest Winners": "Победители конкурса", |
| 1027 | "Rename background": "Переименовать фон", | 1027 | "Rename Background": "Переименовать фон", |
| 1028 | "Lock": "Закрепить", | 1028 | "Lock": "Закрепить", |
| 1029 | "Unlock": "Открепить", | 1029 | "Unlock": "Открепить", |
| 1030 | "Delete background": "Удалить фон", | 1030 | "Delete Background": "Удалить фон", |
| 1031 | "Export all": "Экспортировать всё", | 1031 | "Export all": "Экспортировать всё", |
| 1032 | "Export all your prompts to a file": "Экспортировать все промпты в виде файла", | 1032 | "Export all your prompts to a file": "Экспортировать все промпты в виде файла", |
| 1033 | "Don't add character names.": "Не добавлять имя персонажа", | 1033 | "Don't add character names.": "Не добавлять имя персонажа", |
| @@ -1621,7 +1621,7 @@ | |||
| 1621 | "Auxiliary": "Вспомогательный", | 1621 | "Auxiliary": "Вспомогательный", |
| 1622 | "Post-History Instructions": "Инструкции после истории", | 1622 | "Post-History Instructions": "Инструкции после истории", |
| 1623 | "Current persona updated": "Текущая персона изменена", | 1623 | "Current persona updated": "Текущая персона изменена", |
| 1624 | "Your messages will now be sent as ${0}": "Ваши сообщения будут отправляться от лица ${0}", | 1624 | "Your messages will now be sent as ${0}": "Теперь вы будете подписываться как ${0}", |
| 1625 | "Copied!": "Скопировано!", | 1625 | "Copied!": "Скопировано!", |
| 1626 | "Are you sure you want to delete this message?": "Вы точно хотите удалить это сообщение?", | 1626 | "Are you sure you want to delete this message?": "Вы точно хотите удалить это сообщение?", |
| 1627 | "Delete Message": "Удалить сообщение", | 1627 | "Delete Message": "Удалить сообщение", |
| @@ -1977,7 +1977,7 @@ | |||
| 1977 | "Importing Tags": "Импорт тегов", | 1977 | "Importing Tags": "Импорт тегов", |
| 1978 | "Couldn't import tags:": "Не удалось импортировать теги:", | 1978 | "Couldn't import tags:": "Не удалось импортировать теги:", |
| 1979 | "Allow fallback models": "Разрешить резервные модели", | 1979 | "Allow fallback models": "Разрешить резервные модели", |
| 1980 | "Allow fallback providers": "Разрешить fallback-провайдеров", | 1980 | "Allow fallback providers": "Разрешить резервных провайдеров", |
| 1981 | "To use instruct formatting, switch to OpenRouter under Text Completion API.": "Переключитесь на OpenRouter в Text Completion API, чтобы использовать форматирование Instruct-режима.", | 1981 | "To use instruct formatting, switch to OpenRouter under Text Completion API.": "Переключитесь на OpenRouter в Text Completion API, чтобы использовать форматирование Instruct-режима.", |
| 1982 | "Select providers. No selection = all providers.": "Выберите провайдера. Нет выбранного = выбраны все.", | 1982 | "Select providers. No selection = all providers.": "Выберите провайдера. Нет выбранного = выбраны все.", |
| 1983 | "Model Providers": "Провайдеры моделей", | 1983 | "Model Providers": "Провайдеры моделей", |
| @@ -2406,7 +2406,7 @@ | |||
| 2406 | "${0} is a system assistant. Choose another character.": "Персонаж ${0} выбран в качестве системного помощника. Выберите другого.", | 2406 | "${0} is a system assistant. Choose another character.": "Персонаж ${0} выбран в качестве системного помощника. Выберите другого.", |
| 2407 | "Set ${0} as your assistant.": "Персонаж ${0} установлен в качестве помощника.", | 2407 | "Set ${0} as your assistant.": "Персонаж ${0} установлен в качестве помощника.", |
| 2408 | "${0} is no longer your assistant.": "Персонаж ${0} снят с роли помощника.", | 2408 | "${0} is no longer your assistant.": "Персонаж ${0} снят с роли помощника.", |
| 2409 | "Manage API keys": "Управление ключами", | 2409 | "Manage API keys": "Менеджер ключей", |
| 2410 | "Key:": "Ключ:", | 2410 | "Key:": "Ключ:", |
| 2411 | "No secrets saved.": "Ключей нет.", | 2411 | "No secrets saved.": "Ключей нет.", |
| 2412 | "Add Secret": "Добавить ключ", | 2412 | "Add Secret": "Добавить ключ", |
| @@ -2433,7 +2433,7 @@ | |||
| 2433 | "prompt_post_processing_semi_tools": "Semi-strict (чередовать роли, функции включены)", | 2433 | "prompt_post_processing_semi_tools": "Semi-strict (чередовать роли, функции включены)", |
| 2434 | "prompt_post_processing_strict_tools": "Strict (чередовать роли, сначала пользователь; функции включены)", | 2434 | "prompt_post_processing_strict_tools": "Strict (чередовать роли, сначала пользователь; функции включены)", |
| 2435 | "prompt_post_processing_single": "Только одно сообщение пользователя (функции отключены)", | 2435 | "prompt_post_processing_single": "Только одно сообщение пользователя (функции отключены)", |
| 2436 | "prompt_post_processing_merge_tools": "Объединять идущие подряд сообщение с одной ролью (функции включены)", | 2436 | "prompt_post_processing_merge_tools": "Объединять идущие подряд сообщения с одной ролью (функции включены)", |
| 2437 | "completions note prefix": "Постфикс ", | 2437 | "completions note prefix": "Постфикс ", |
| 2438 | "suffix will be added automatically.": "будет добавлен автоматически.", | 2438 | "suffix will be added automatically.": "будет добавлен автоматически.", |
| 2439 | "Tabby Model": "Модель Tabby", | 2439 | "Tabby Model": "Модель Tabby", |
| @@ -2548,7 +2548,7 @@ | |||
| 2548 | "Default (top of context)": "По умолчанию (наверху контекста)", | 2548 | "Default (top of context)": "По умолчанию (наверху контекста)", |
| 2549 | "Bind Model to Templates": "Связать модель с шаблоном", | 2549 | "Bind Model to Templates": "Связать модель с шаблоном", |
| 2550 | "Persona Auto Selected": "Персона автоматически изменена", | 2550 | "Persona Auto Selected": "Персона автоматически изменена", |
| 2551 | "Auto-selected persona based on ${0} connection.<br />Your messages will now be sent as ${1}.": "Персона выбрана автоматически на основе следующего критерия: ${0}<br />Ваши сообщения будут отправляться от лица ${1}", | 2551 | "Auto-selected persona based on ${0} connection.<br />Your messages will now be sent as ${1}.": "Персона выбрана автоматически на основе следующего критерия: ${0}<br />Теперь вы будете подписываться как ${1}", |
| 2552 | "Filter to specific generation types.": "Применять промпт только при определённых типах генерации.", | 2552 | "Filter to specific generation types.": "Применять промпт только при определённых типах генерации.", |
| 2553 | "Quiet": "Тихая", | 2553 | "Quiet": "Тихая", |
| 2554 | "Swipe": "Свайп", | 2554 | "Swipe": "Свайп", |
| @@ -2601,7 +2601,7 @@ | |||
| 2601 | "sd_snap_txt": "Корректировать автоматически выбранное разрешение", | 2601 | "sd_snap_txt": "Корректировать автоматически выбранное разрешение", |
| 2602 | "sd_snap": "Подгонять картинки с фиксированным соотношением сторон (фоны, портреты) к ближайшему известному разрешению (рекомендуется для SDXL)", | 2602 | "sd_snap": "Подгонять картинки с фиксированным соотношением сторон (фоны, портреты) к ближайшему известному разрешению (рекомендуется для SDXL)", |
| 2603 | "Source": "API", | 2603 | "Source": "API", |
| 2604 | "Hint: Save an API key in AI Horde API settings to use it here.": "Подсказка: сохраните ключ от API в настройках API на сайте AI Horde, чтобы использовать его автоматически.", | 2604 | "Hint: Save an API key in AI Horde API settings to use it here.": "Совет: сохраните ключ от API в настройках API AI Horde, чтобы автоматически подтянуть его сюда.", |
| 2605 | "Sanitize prompts (recommended)": "Прогонять промпты через санитайзер (рекомендуется)", | 2605 | "Sanitize prompts (recommended)": "Прогонять промпты через санитайзер (рекомендуется)", |
| 2606 | "Sampling method": "Метод сэмплинга", | 2606 | "Sampling method": "Метод сэмплинга", |
| 2607 | "Resolution": "Целевое разрешение", | 2607 | "Resolution": "Целевое разрешение", |
| @@ -2652,5 +2652,97 @@ | |||
| 2652 | "sd_auto_auth_warning_1": " запускайте Stable Diffusion с флагом", | 2652 | "sd_auto_auth_warning_1": " запускайте Stable Diffusion с флагом", |
| 2653 | "sd_auto_auth_warning_2": "! Адрес SD должен быть доступен с сервера SillyTavern.", | 2653 | "sd_auto_auth_warning_2": "! Адрес SD должен быть доступен с сервера SillyTavern.", |
| 2654 | "Upscale by": "Множитель апскейлинга", | 2654 | "Upscale by": "Множитель апскейлинга", |
| 2655 | "Hires steps (2nd pass)": "Кол-во шагов Hires (на втором проходе)" | 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 | "Anthropic's developer console": "консолі розробника Anthropic", | 369 | "Anthropic's developer console": "консолі розробника Anthropic", |
| 370 | "Claude Model": "Модель Claude", | 370 | "Claude Model": "Модель Claude", |
| 371 | "Window AI Model": "Модель Window AI", | 371 | "Window AI Model": "Модель Window AI", |
| 372 | "Model Order": "Сортування моделі OpenRouter", | 372 | "OpenRouter Model Sorting": "Сортування моделі OpenRouter", |
| 373 | "Alphabetically": "За алфавітом", | 373 | "Alphabetically": "За алфавітом", |
| 374 | "Price": "Ціна (найдешевша)", | 374 | "Price": "Ціна (найдешевша)", |
| 375 | "Context Size": "Розмір контексту", | 375 | "Context Size": "Розмір контексту", |
| @@ -369,7 +369,7 @@ | |||
| 369 | "Anthropic's developer console": "developer console của Anthropic", | 369 | "Anthropic's developer console": "developer console của Anthropic", |
| 370 | "Claude Model": "Model Claude", | 370 | "Claude Model": "Model Claude", |
| 371 | "Window AI Model": "Model Window AI", | 371 | "Window AI Model": "Model Window AI", |
| 372 | "Model Order": "Sắp xếp model OpenRouter", | 372 | "OpenRouter Model Sorting": "Sắp xếp model OpenRouter", |
| 373 | "Alphabetically": "Theo thứ tự bảng chữ cái", | 373 | "Alphabetically": "Theo thứ tự bảng chữ cái", |
| 374 | "Price": "Giá (rẻ nhất)", | 374 | "Price": "Giá (rẻ nhất)", |
| 375 | "Context Size": "Kích thước bối cảnh", | 375 | "Context Size": "Kích thước bối cảnh", |
| @@ -452,7 +452,7 @@ | |||
| 452 | "Claude Model": "Claude 模型", | 452 | "Claude Model": "Claude 模型", |
| 453 | "Allow fallback routes Description": "如果所选模型无法响应您的请求,则自动选择备用模型。", | 453 | "Allow fallback routes Description": "如果所选模型无法响应您的请求,则自动选择备用模型。", |
| 454 | "Allow fallback models": "允许后备模型", | 454 | "Allow fallback models": "允许后备模型", |
| 455 | "Model Order": "OpenRouter 模型顺序", | 455 | "OpenRouter Model Sorting": "OpenRouter 模型顺序", |
| 456 | "Alphabetically": "按字母顺序", | 456 | "Alphabetically": "按字母顺序", |
| 457 | "Price": "价格(最便宜)", | 457 | "Price": "价格(最便宜)", |
| 458 | "Context Size": "上下文长度", | 458 | "Context Size": "上下文长度", |
| @@ -1479,7 +1479,7 @@ | |||
| 1479 | "Image Captioning": "图像描述", | 1479 | "Image Captioning": "图像描述", |
| 1480 | "Source": "来源", | 1480 | "Source": "来源", |
| 1481 | "Local": "本地", | 1481 | "Local": "本地", |
| 1482 | "Multimodal (OpenAI / Anthropic / llama / Google)": "多模态(OpenAI / Anthropic / llama / Google)", | 1482 | "Multimodal": "多模态", |
| 1483 | "Extras": "更多", | 1483 | "Extras": "更多", |
| 1484 | "Horde": "Horde", | 1484 | "Horde": "Horde", |
| 1485 | "API": "API", | 1485 | "API": "API", |
| @@ -1646,8 +1646,9 @@ | |||
| 1646 | "macro for manual injection)": "宏用于手动注入)", | 1646 | "macro for manual injection)": "宏用于手动注入)", |
| 1647 | "Color": "颜色", | 1647 | "Color": "颜色", |
| 1648 | "Only apply color as accent": "仅应用颜色作为强调", | 1648 | "Only apply color as accent": "仅应用颜色作为强调", |
| 1649 | "ext_regex_new_global_script_desc": "新增「全局」正规表达式", | 1649 | "ext_regex_new_global_script_desc": "新增「全局」正则表达式", |
| 1650 | "ext_regex_new_scoped_script_desc": "新增「局部」正规表达式", | 1650 | "ext_regex_new_scoped_script_desc": "新增「局部」正则表达式", |
| 1651 | "ext_regex_new_preset_script_desc": "新增「预设」正则表达式", | ||
| 1651 | "ext_regex_debugger_active_rules": "激活的规则", | 1652 | "ext_regex_debugger_active_rules": "激活的规则", |
| 1652 | "ext_regex_debugger_save_order": "保存此顺序", | 1653 | "ext_regex_debugger_save_order": "保存此顺序", |
| 1653 | "ext_regex_debugger_testing_area": "测试区域", | 1654 | "ext_regex_debugger_testing_area": "测试区域", |
| @@ -1668,6 +1669,7 @@ | |||
| 1668 | "ext_regex_preset_delete": "删除当前预设", | 1669 | "ext_regex_preset_delete": "删除当前预设", |
| 1669 | "ext_regex_new_global_script": "新建全局正则", | 1670 | "ext_regex_new_global_script": "新建全局正则", |
| 1670 | "ext_regex_new_scoped_script": "新建局部正则", | 1671 | "ext_regex_new_scoped_script": "新建局部正则", |
| 1672 | "ext_regex_new_preset_script": "新建预设正则", | ||
| 1671 | "ext_regex_import_script": "导入正则", | 1673 | "ext_regex_import_script": "导入正则", |
| 1672 | "ext_regex_bulk_edit": "批量编辑", | 1674 | "ext_regex_bulk_edit": "批量编辑", |
| 1673 | "ext_regex_debugger_desc": "高级正则调试工具", | 1675 | "ext_regex_debugger_desc": "高级正则调试工具", |
| @@ -1677,9 +1679,13 @@ | |||
| 1677 | "ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中", | 1679 | "ext_regex_global_scripts_desc": "影响所有角色,保存在本地设定中", |
| 1678 | "No scripts found": "没有找到脚本", | 1680 | "No scripts found": "没有找到脚本", |
| 1679 | "ext_regex_scoped_scripts": "局部正则脚本", | 1681 | "ext_regex_scoped_scripts": "局部正则脚本", |
| 1682 | "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中", | ||
| 1683 | "ext_regex_preset_scripts": "预设正则脚本", | ||
| 1684 | "ext_regex_preset_scripts_desc": "只影响当前预设,保存在预设中", | ||
| 1680 | "ext_regex_disallow_scoped": "不允许使用局部正则", | 1685 | "ext_regex_disallow_scoped": "不允许使用局部正则", |
| 1681 | "ext_regex_allow_scoped": "允许使用局部正则", | 1686 | "ext_regex_allow_scoped": "允许使用局部正则", |
| 1682 | "ext_regex_scoped_scripts_desc": "只影响当前角色,保存在角色卡片中", | 1687 | "ext_regex_disallow_preset": "不允许使用预设正则", |
| 1688 | "ext_regex_allow_preset": "允许使用预设正则", | ||
| 1683 | "Regex Editor": "正则表达式编辑器", | 1689 | "Regex Editor": "正则表达式编辑器", |
| 1684 | "Test Mode": "测试模式", | 1690 | "Test Mode": "测试模式", |
| 1685 | "ext_regex_desc": "“正则”是一个使用“正则表达式”来查找/替换字符串的工具。如果您想了解更多信息,请点击标题旁边的“?”。", | 1691 | "ext_regex_desc": "“正则”是一个使用“正则表达式”来查找/替换字符串的工具。如果您想了解更多信息,请点击标题旁边的“?”。", |
| @@ -1728,10 +1734,16 @@ | |||
| 1728 | "ext_regex_disable_script": "禁用脚本", | 1734 | "ext_regex_disable_script": "禁用脚本", |
| 1729 | "ext_regex_enable_script": "启用脚本", | 1735 | "ext_regex_enable_script": "启用脚本", |
| 1730 | "ext_regex_edit_script": "编辑脚本", | 1736 | "ext_regex_edit_script": "编辑脚本", |
| 1731 | "ext_regex_move_to_global": "移至全局脚本", | 1737 | "ext_regex_move_to_global": "移至全局", |
| 1732 | "ext_regex_move_to_scoped": "移至作用域脚本", | 1738 | "ext_regex_move_to_scoped": "移至局部", |
| 1739 | "ext_regex_move_to_preset": "移至预设", | ||
| 1733 | "ext_regex_export_script": "导出脚本", | 1740 | "ext_regex_export_script": "导出脚本", |
| 1734 | "ext_regex_delete_script": "删除脚本", | 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 | "Trigger Stable Diffusion": "触发Stable Diffusion", | 1747 | "Trigger Stable Diffusion": "触发Stable Diffusion", |
| 1736 | "Abort current image generation task": "中止当前图像生成", | 1748 | "Abort current image generation task": "中止当前图像生成", |
| 1737 | "Stop Image Generation": "停止图像生成", | 1749 | "Stop Image Generation": "停止图像生成", |
| @@ -368,7 +368,7 @@ | |||
| 368 | "Anthropic's developer console": "Anthropic 的開發者控制台", | 368 | "Anthropic's developer console": "Anthropic 的開發者控制台", |
| 369 | "Claude Model": "Claude 模型", | 369 | "Claude Model": "Claude 模型", |
| 370 | "Window AI Model": "Window AI 模型", | 370 | "Window AI Model": "Window AI 模型", |
| 371 | "Model Order": "模型順序", | 371 | "OpenRouter Model Sorting": "模型順序", |
| 372 | "Alphabetically": "按字母順序", | 372 | "Alphabetically": "按字母順序", |
| 373 | "Price": "價格(最便宜的)", | 373 | "Price": "價格(最便宜的)", |
| 374 | "Context Size": "上下文長度", | 374 | "Context Size": "上下文長度", |
| @@ -1674,7 +1674,7 @@ | |||
| 1674 | "Message Template": "訊息範本", | 1674 | "Message Template": "訊息範本", |
| 1675 | "Model ID": "模型 ID", | 1675 | "Model ID": "模型 ID", |
| 1676 | "mui_reset": "重設", | 1676 | "mui_reset": "重設", |
| 1677 | "Multimodal (OpenAI / Anthropic / llama / Google)": "多模態(OpenAI/Anthropic/llama/Google)", | 1677 | "Multimodal": "多模態", |
| 1678 | "must be set in Tabby's config.yml to switch models.": "須在 Tabby's config.yml 中設定以切換模型。", | 1678 | "must be set in Tabby's config.yml to switch models.": "須在 Tabby's config.yml 中設定以切換模型。", |
| 1679 | "Names as Stop Strings": "將名稱用作停止字串", | 1679 | "Names as Stop Strings": "將名稱用作停止字串", |
| 1680 | "Never": "從不", | 1680 | "Never": "從不", |
| @@ -25,6 +25,16 @@ | |||
| 25 | "src": "img/apple-icon-144x144.png", | 25 | "src": "img/apple-icon-144x144.png", |
| 26 | "sizes": "144x144", | 26 | "sizes": "144x144", |
| 27 | "type": "image/png" | 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 | importEmbeddedWorldInfo, | 43 | importEmbeddedWorldInfo, |
| 44 | checkEmbeddedWorld, | 44 | checkEmbeddedWorld, |
| 45 | setWorldInfoButtonClass, | 45 | setWorldInfoButtonClass, |
| 46 | importWorldInfo, | ||
| 47 | wi_anchor_position, | 46 | wi_anchor_position, |
| 48 | world_info_include_names, | 47 | world_info_include_names, |
| 49 | initWorldInfo, | 48 | initWorldInfo, |
| 49 | charUpdatePrimaryWorld, | ||
| 50 | charSetAuxWorlds, | ||
| 50 | } from './scripts/world-info.js'; | 51 | } from './scripts/world-info.js'; |
| 51 | 52 | ||
| 52 | import { | 53 | import { |
| @@ -175,6 +176,9 @@ import { | |||
| 175 | localizePagination, | 176 | localizePagination, |
| 176 | renderPaginationDropdown, | 177 | renderPaginationDropdown, |
| 177 | paginationDropdownChangeHandler, | 178 | paginationDropdownChangeHandler, |
| 179 | importFromExternalUrl, | ||
| 180 | shiftUpByOne, | ||
| 181 | shiftDownByOne, | ||
| 178 | } from './scripts/utils.js'; | 182 | } from './scripts/utils.js'; |
| 179 | import { debounce_timeout, GENERATION_TYPE_TRIGGERS, IGNORE_SYMBOL, inject_ids } from './scripts/constants.js'; | 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 | import { getSystemMessageByType, initSystemMessages, SAFETY_CHAT, sendSystemMessage, system_message_types, system_messages } from './scripts/system-messages.js'; | 271 | import { getSystemMessageByType, initSystemMessages, SAFETY_CHAT, sendSystemMessage, system_message_types, system_messages } from './scripts/system-messages.js'; |
| 268 | import { event_types, eventSource } from './scripts/events.js'; | 272 | import { event_types, eventSource } from './scripts/events.js'; |
| 269 | import { initAccessibility } from './scripts/a11y.js'; | 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 | // API OBJECT FOR EXTERNAL WIRING | 277 | // API OBJECT FOR EXTERNAL WIRING |
| 272 | globalThis.SillyTavern = { | 278 | globalThis.SillyTavern = { |
| @@ -390,7 +396,7 @@ let isExportPopupOpen = false; | |||
| 390 | 396 | ||
| 391 | // Saved here for performance reasons | 397 | // Saved here for performance reasons |
| 392 | const messageTemplate = $('#message_template .mes'); | 398 | const messageTemplate = $('#message_template .mes'); |
| 393 | const chatElement = $('#chat'); | 399 | export const chatElement = $('#chat'); |
| 394 | 400 | ||
| 395 | let dialogueResolve = null; | 401 | let dialogueResolve = null; |
| 396 | let dialogueCloseStop = false; | 402 | let dialogueCloseStop = false; |
| @@ -403,6 +409,7 @@ let fav_ch_checked = false; | |||
| 403 | let scrollLock = false; | 409 | let scrollLock = false; |
| 404 | export let abortStatusCheck = new AbortController(); | 410 | export let abortStatusCheck = new AbortController(); |
| 405 | export let charDragDropHandler = null; | 411 | export let charDragDropHandler = null; |
| 412 | export let chatDragDropHandler = null; | ||
| 406 | 413 | ||
| 407 | /** @type {debounce_timeout} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */ | 414 | /** @type {debounce_timeout} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */ |
| 408 | export const DEFAULT_SAVE_EDIT_TIMEOUT = debounce_timeout.relaxed; | 415 | export const DEFAULT_SAVE_EDIT_TIMEOUT = debounce_timeout.relaxed; |
| @@ -637,6 +644,7 @@ async function firstLoadInit() { | |||
| 637 | 644 | ||
| 638 | showLoader(); | 645 | showLoader(); |
| 639 | registerPromptManagerMigration(); | 646 | registerPromptManagerMigration(); |
| 647 | initDomHandlers(); | ||
| 640 | initStandaloneMode(); | 648 | initStandaloneMode(); |
| 641 | initLibraryShims(); | 649 | initLibraryShims(); |
| 642 | addShowdownPatch(showdown); | 650 | addShowdownPatch(showdown); |
| @@ -1344,7 +1352,7 @@ export async function replaceCurrentChat() { | |||
| 1344 | } | 1352 | } |
| 1345 | 1353 | ||
| 1346 | export async function showMoreMessages(messagesToLoad = null) { | 1354 | export async function showMoreMessages(messagesToLoad = null) { |
| 1347 | const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid'); | 1355 | const firstDisplayedMesId = chatElement.children('.mes').first().attr('mesid'); |
| 1348 | let messageId = Number(firstDisplayedMesId); | 1356 | let messageId = Number(firstDisplayedMesId); |
| 1349 | let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER; | 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 | console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); | 1365 | console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); |
| 1358 | const prevHeight = $('#chat').prop('scrollHeight'); | 1366 | const prevHeight = chatElement.prop('scrollHeight'); |
| 1359 | const isButtonInView = isElementInViewport($('#show_more_messages')[0]); | 1367 | const isButtonInView = isElementInViewport($('#show_more_messages')[0]); |
| 1360 | 1368 | ||
| 1361 | while (messageId > 0 && count > 0) { | 1369 | while (messageId > 0 && count > 0) { |
| @@ -1370,8 +1378,8 @@ export async function showMoreMessages(messagesToLoad = null) { | |||
| 1370 | } | 1378 | } |
| 1371 | 1379 | ||
| 1372 | if (isButtonInView) { | 1380 | if (isButtonInView) { |
| 1373 | const newHeight = $('#chat').prop('scrollHeight'); | 1381 | const newHeight = chatElement.prop('scrollHeight'); |
| 1374 | $('#chat').scrollTop(newHeight - prevHeight); | 1382 | chatElement.scrollTop(newHeight - prevHeight); |
| 1375 | } | 1383 | } |
| 1376 | 1384 | ||
| 1377 | applyStylePins(); | 1385 | applyStylePins(); |
| @@ -1384,7 +1392,7 @@ export async function printMessages() { | |||
| 1384 | 1392 | ||
| 1385 | if (chat.length > count) { | 1393 | if (chat.length > count) { |
| 1386 | startIndex = chat.length - count; | 1394 | startIndex = chat.length - count; |
| 1387 | $('#chat').append('<div id="show_more_messages">Show more messages</div>'); | 1395 | chatElement.append('<div id="show_more_messages">Show more messages</div>'); |
| 1388 | } | 1396 | } |
| 1389 | 1397 | ||
| 1390 | for (let i = startIndex; i < chat.length; i++) { | 1398 | for (let i = startIndex; i < chat.length; i++) { |
| @@ -1407,8 +1415,8 @@ export async function printMessages() { | |||
| 1407 | } | 1415 | } |
| 1408 | } | 1416 | } |
| 1409 | 1417 | ||
| 1410 | $('#chat .mes').removeClass('last_mes'); | 1418 | chatElement.find('.mes').removeClass('last_mes'); |
| 1411 | $('#chat .mes').last().addClass('last_mes'); | 1419 | chatElement.find('.mes').last().addClass('last_mes'); |
| 1412 | hideSwipeButtons(); | 1420 | hideSwipeButtons(); |
| 1413 | showSwipeButtons(); | 1421 | showSwipeButtons(); |
| 1414 | scrollChatToBottom(); | 1422 | scrollChatToBottom(); |
| @@ -1441,7 +1449,7 @@ export async function clearChat() { | |||
| 1441 | if (is_delete_mode) { | 1449 | if (is_delete_mode) { |
| 1442 | $('#dialogue_del_mes_cancel').trigger('click'); | 1450 | $('#dialogue_del_mes_cancel').trigger('click'); |
| 1443 | } | 1451 | } |
| 1444 | $('#chat').children().remove(); | 1452 | chatElement.children().remove(); |
| 1445 | if ($('.zoomed_avatar[forChar]').length) { | 1453 | if ($('.zoomed_avatar[forChar]').length) { |
| 1446 | console.debug('saw avatars to remove'); | 1454 | console.debug('saw avatars to remove'); |
| 1447 | $('.zoomed_avatar[forChar]').remove(); | 1455 | $('.zoomed_avatar[forChar]').remove(); |
| @@ -1453,7 +1461,7 @@ export async function clearChat() { | |||
| 1453 | 1461 | ||
| 1454 | export async function deleteLastMessage() { | 1462 | export async function deleteLastMessage() { |
| 1455 | chat.length = chat.length - 1; | 1463 | chat.length = chat.length - 1; |
| 1456 | $('#chat').children('.mes').last().remove(); | 1464 | chatElement.children('.mes').last().remove(); |
| 1457 | await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); | 1465 | await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); |
| 1458 | } | 1466 | } |
| 1459 | 1467 | ||
| @@ -1796,7 +1804,7 @@ function getMessageFromTemplate({ | |||
| 1796 | * @param {boolean} [options.rerenderMessage=true] Whether to re-render the message content (inside <c>.mes_text</c>) | 1804 | * @param {boolean} [options.rerenderMessage=true] Whether to re-render the message content (inside <c>.mes_text</c>) |
| 1797 | */ | 1805 | */ |
| 1798 | export function updateMessageBlock(messageId, message, { rerenderMessage = true } = {}) { | 1806 | export function updateMessageBlock(messageId, message, { rerenderMessage = true } = {}) { |
| 1799 | const messageElement = $(`#chat [mesid="${messageId}"]`); | 1807 | const messageElement = chatElement.find(`[mesid="${messageId}"]`); |
| 1800 | if (rerenderMessage) { | 1808 | if (rerenderMessage) { |
| 1801 | const text = message?.extra?.display_text ?? message.mes; | 1809 | const text = message?.extra?.display_text ?? message.mes; |
| 1802 | messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false)); | 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 | // Add image to message | 1826 | // Add image to message |
| 1819 | if (mes.extra?.image) { | 1827 | if (mes.extra?.image) { |
| 1820 | const container = messageElement.find('.mes_img_container'); | 1828 | const container = messageElement.find('.mes_img_container'); |
| 1821 | const chatHeight = $('#chat').prop('scrollHeight'); | 1829 | const chatHeight = chatElement.prop('scrollHeight'); |
| 1822 | const image = messageElement.find('.mes_img'); | 1830 | const image = messageElement.find('.mes_img'); |
| 1823 | const text = messageElement.find('.mes_text'); | 1831 | const text = messageElement.find('.mes_text'); |
| 1824 | const isInline = !!mes.extra?.inline_image; | 1832 | const isInline = !!mes.extra?.inline_image; |
| @@ -1826,10 +1834,10 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { | |||
| 1826 | if (!adjustScroll) { | 1834 | if (!adjustScroll) { |
| 1827 | return; | 1835 | return; |
| 1828 | } | 1836 | } |
| 1829 | const scrollPosition = $('#chat').scrollTop(); | 1837 | const scrollPosition = chatElement.scrollTop(); |
| 1830 | const newChatHeight = $('#chat').prop('scrollHeight'); | 1838 | const newChatHeight = chatElement.prop('scrollHeight'); |
| 1831 | const diff = newChatHeight - chatHeight; | 1839 | const diff = newChatHeight - chatHeight; |
| 1832 | $('#chat').scrollTop(scrollPosition + diff); | 1840 | chatElement.scrollTop(scrollPosition + diff); |
| 1833 | }; | 1841 | }; |
| 1834 | image.off('load').on('load', function () { | 1842 | image.off('load').on('load', function () { |
| 1835 | image.removeAttr('alt'); | 1843 | image.removeAttr('alt'); |
| @@ -1876,16 +1884,16 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { | |||
| 1876 | const container = $('#message_video_template .mes_video_container').clone(); | 1884 | const container = $('#message_video_template .mes_video_container').clone(); |
| 1877 | messageElement.find('.mes_video_container').remove(); | 1885 | messageElement.find('.mes_video_container').remove(); |
| 1878 | messageElement.find('.mes_block').append(container); | 1886 | messageElement.find('.mes_block').append(container); |
| 1879 | const chatHeight = $('#chat').prop('scrollHeight'); | 1887 | const chatHeight = chatElement.prop('scrollHeight'); |
| 1880 | const video = container.find('.mes_video'); | 1888 | const video = container.find('.mes_video'); |
| 1881 | video.off('loadedmetadata').on('loadedmetadata', function () { | 1889 | video.off('loadedmetadata').on('loadedmetadata', function () { |
| 1882 | if (!adjustScroll) { | 1890 | if (!adjustScroll) { |
| 1883 | return; | 1891 | return; |
| 1884 | } | 1892 | } |
| 1885 | const scrollPosition = $('#chat').scrollTop(); | 1893 | const scrollPosition = chatElement.scrollTop(); |
| 1886 | const newChatHeight = $('#chat').prop('scrollHeight'); | 1894 | const newChatHeight = chatElement.prop('scrollHeight'); |
| 1887 | const diff = newChatHeight - chatHeight; | 1895 | const diff = newChatHeight - chatHeight; |
| 1888 | $('#chat').scrollTop(scrollPosition + diff); | 1896 | chatElement.scrollTop(scrollPosition + diff); |
| 1889 | }); | 1897 | }); |
| 1890 | 1898 | ||
| 1891 | video.attr('src', mes.extra?.video); | 1899 | video.attr('src', mes.extra?.video); |
| @@ -1927,7 +1935,7 @@ export function addCopyToCodeBlocks(messageElement) { | |||
| 1927 | e.stopPropagation(); | 1935 | e.stopPropagation(); |
| 1928 | }); | 1936 | }); |
| 1929 | copyButton.addEventListener('pointerup', async function () { | 1937 | copyButton.addEventListener('pointerup', async function () { |
| 1930 | const text = codeBlocks.get(i).innerText; | 1938 | const text = codeBlocks.get(i).textContent; |
| 1931 | await copyText(text); | 1939 | await copyText(text); |
| 1932 | toastr.info(t`Copied!`, '', { timeOut: 2000 }); | 1940 | toastr.info(t`Copied!`, '', { timeOut: 2000 }); |
| 1933 | }); | 1941 | }); |
| @@ -2040,7 +2048,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | |||
| 2040 | // Callers push the new message to chat before calling addOneMessage | 2048 | // Callers push the new message to chat before calling addOneMessage |
| 2041 | const newMessageId = typeof forceId == 'number' ? forceId : chat.length - 1; | 2049 | const newMessageId = typeof forceId == 'number' ? forceId : chat.length - 1; |
| 2042 | 2050 | ||
| 2043 | const newMessage = $(`#chat [mesid="${newMessageId}"]`); | 2051 | const newMessage = chatElement.find(`[mesid="${newMessageId}"]`); |
| 2044 | const isSmallSys = mes?.extra?.isSmallSys; | 2052 | const isSmallSys = mes?.extra?.isSmallSys; |
| 2045 | 2053 | ||
| 2046 | if (isSmallSys === true) { | 2054 | if (isSmallSys === true) { |
| @@ -2102,8 +2110,8 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | |||
| 2102 | } | 2110 | } |
| 2103 | 2111 | ||
| 2104 | if (showSwipes) { | 2112 | if (showSwipes) { |
| 2105 | $('#chat .mes').last().addClass('last_mes'); | 2113 | chatElement.find('.mes').last().addClass('last_mes'); |
| 2106 | $('#chat .mes').eq(-2).removeClass('last_mes'); | 2114 | chatElement.find('.mes').eq(-2).removeClass('last_mes'); |
| 2107 | hideSwipeButtons(); | 2115 | hideSwipeButtons(); |
| 2108 | showSwipeButtons(); | 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 | if (_replaceCharacterCard) { | 2290 | if (_replaceCharacterCard) { |
| 2256 | const fields = getCharacterCardFields(); | 2291 | const fields = getCharacterCardFields(); |
| 2257 | environment.charPrompt = fields.system || ''; | 2292 | environment.charPrompt = fields.system || ''; |
| @@ -2281,6 +2316,7 @@ export function substituteParams(content, _name1, _name2, _original, _group, _re | |||
| 2281 | environment.char = _name2 ?? name2; | 2316 | environment.char = _name2 ?? name2; |
| 2282 | environment.group = environment.charIfNotGroup = getGroupValue(true); | 2317 | environment.group = environment.charIfNotGroup = getGroupValue(true); |
| 2283 | environment.groupNotMuted = getGroupValue(false); | 2318 | environment.groupNotMuted = getGroupValue(false); |
| 2319 | environment.notChar = getNotCharValue(); | ||
| 2284 | environment.model = getGeneratingModel(); | 2320 | environment.model = getGeneratingModel(); |
| 2285 | 2321 | ||
| 2286 | if (additionalMacro && typeof additionalMacro === 'object') { | 2322 | if (additionalMacro && typeof additionalMacro === 'object') { |
| @@ -2905,8 +2941,12 @@ class StreamingProcessor { | |||
| 2905 | false, | 2941 | false, |
| 2906 | ); | 2942 | ); |
| 2907 | if (this.messageTextDom instanceof HTMLElement) { | 2943 | if (this.messageTextDom instanceof HTMLElement) { |
| 2944 | if (power_user.stream_fade_in) { | ||
| 2945 | applyStreamFadeIn(this.messageTextDom, formattedText); | ||
| 2946 | } else { | ||
| 2908 | this.messageTextDom.innerHTML = formattedText; | 2947 | this.messageTextDom.innerHTML = formattedText; |
| 2909 | } | 2948 | } |
| 2949 | } | ||
| 2910 | 2950 | ||
| 2911 | const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount, this.reasoningHandler.getDuration(), this.timeToFirstToken); | 2951 | const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount, this.reasoningHandler.getDuration(), this.timeToFirstToken); |
| 2912 | if (this.messageTimerDom instanceof HTMLElement) { | 2952 | if (this.messageTimerDom instanceof HTMLElement) { |
| @@ -2925,7 +2965,7 @@ class StreamingProcessor { | |||
| 2925 | async onFinishStreaming(messageId, text) { | 2965 | async onFinishStreaming(messageId, text) { |
| 2926 | this.markUIGenStopped(); | 2966 | this.markUIGenStopped(); |
| 2927 | await this.onProgressStreaming(messageId, text, true); | 2967 | await this.onProgressStreaming(messageId, text, true); |
| 2928 | addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`)); | 2968 | addCopyToCodeBlocks(chatElement.find(`.mes[mesid="${messageId}"]`)); |
| 2929 | 2969 | ||
| 2930 | await this.reasoningHandler.finish(messageId); | 2970 | await this.reasoningHandler.finish(messageId); |
| 2931 | 2971 | ||
| @@ -3164,6 +3204,11 @@ export async function generateRaw({ prompt = '', api = null, instructOverride = | |||
| 3164 | // construct final prompt from the input. Can either be a string or an array of chat-style messages. | 3204 | // construct final prompt from the input. Can either be a string or an array of chat-style messages. |
| 3165 | prompt = createRawPrompt(prompt, api, instructOverride, quietToLoud, systemPrompt, prefill); | 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 | try { | 3212 | try { |
| 3168 | if (responseLengthCustomized) { | 3213 | if (responseLengthCustomized) { |
| 3169 | TempResponseLength.save(api, responseLength); | 3214 | TempResponseLength.save(api, responseLength); |
| @@ -3171,6 +3216,23 @@ export async function generateRaw({ prompt = '', api = null, instructOverride = | |||
| 3171 | /** @type {object|any[]} */ | 3216 | /** @type {object|any[]} */ |
| 3172 | let generateData = {}; | 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 | switch (api) { | 3236 | switch (api) { |
| 3175 | case 'kobold': | 3237 | case 'kobold': |
| 3176 | case 'koboldhorde': | 3238 | case 'koboldhorde': |
| @@ -3250,6 +3312,7 @@ export async function generateRaw({ prompt = '', api = null, instructOverride = | |||
| 3250 | 3312 | ||
| 3251 | return message; | 3313 | return message; |
| 3252 | } finally { | 3314 | } finally { |
| 3315 | eventSource.removeListener(event_types.GENERATION_STOPPED, abortHook); | ||
| 3253 | if (responseLengthCustomized && TempResponseLength.isCustomized()) { | 3316 | if (responseLengthCustomized && TempResponseLength.isCustomized()) { |
| 3254 | TempResponseLength.restore(api); | 3317 | TempResponseLength.restore(api); |
| 3255 | TempResponseLength.removeEventHook(api, eventHook); | 3318 | TempResponseLength.removeEventHook(api, eventHook); |
| @@ -3353,7 +3416,7 @@ class TempResponseLength { | |||
| 3353 | */ | 3416 | */ |
| 3354 | function removeLastMessage() { | 3417 | function removeLastMessage() { |
| 3355 | return new Promise((resolve) => { | 3418 | return new Promise((resolve) => { |
| 3356 | const lastMes = $('#chat').children('.mes').last(); | 3419 | const lastMes = chatElement.children('.mes').last(); |
| 3357 | if (lastMes.length === 0) { | 3420 | if (lastMes.length === 0) { |
| 3358 | return resolve(); | 3421 | return resolve(); |
| 3359 | } | 3422 | } |
| @@ -3575,18 +3638,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | |||
| 3575 | creatorNotes, | 3638 | creatorNotes, |
| 3576 | } = getCharacterCardFields(); | 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 | // Depth prompt (character-specific A/N) | 3641 | // Depth prompt (character-specific A/N) |
| 3591 | removeDepthPrompts(); | 3642 | removeDepthPrompts(); |
| 3592 | const groupDepthPrompts = getGroupDepthPrompts(selected_group, Number(this_chid)); | 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 | let mesExamplesArray = parseMesExamples(mesExamples, isInstruct); | 3769 | let mesExamplesArray = parseMesExamples(mesExamples, isInstruct); |
| 3719 | 3770 | ||
| 3720 | ////////////////////////////////// | ||
| 3721 | // Extension added strings | ||
| 3722 | // Set non-WI AN | 3771 | // Set non-WI AN |
| 3723 | setFloatingPrompt(); | 3772 | setFloatingPrompt(); |
| 3724 | // Add persona description to prompt | ||
| 3725 | addPersonaDescriptionExtensionPrompt(); | ||
| 3726 | 3773 | ||
| 3727 | // Add WI to prompt (and also inject WI to AN value via hijack) | 3774 | // Add WI to prompt (and also inject WI to AN value via hijack) |
| 3728 | // Make quiet prompt available for WIAN | 3775 | // Make quiet prompt available for WIAN |
| @@ -3738,7 +3785,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | |||
| 3738 | creatorNotes: creatorNotes, | 3785 | creatorNotes: creatorNotes, |
| 3739 | trigger: GENERATION_TYPE_TRIGGERS.includes(type) ? type : 'normal', | 3786 | trigger: GENERATION_TYPE_TRIGGERS.includes(type) ? type : 'normal', |
| 3740 | }; | 3787 | }; |
| 3741 | const { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoExamples, worldInfoDepth } = await getWorldInfoPrompt(chatForWI, this_max_context, dryRun, globalScanData); | 3788 | const { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoExamples, worldInfoDepth, outletEntries } = await getWorldInfoPrompt(chatForWI, this_max_context, dryRun, globalScanData); |
| 3742 | setExtensionPrompt(inject_ids.QUIET_PROMPT, '', extension_prompt_types.IN_PROMPT, 0, true); | 3789 | setExtensionPrompt(inject_ids.QUIET_PROMPT, '', extension_prompt_types.IN_PROMPT, 0, true); |
| 3743 | 3790 | ||
| 3744 | // Add message example WI | 3791 | // Add message example WI |
| @@ -3770,17 +3817,38 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | |||
| 3770 | if (skipWIAN !== true) { | 3817 | if (skipWIAN !== true) { |
| 3771 | console.log('skipWIAN not active, adding WIAN'); | 3818 | console.log('skipWIAN not active, adding WIAN'); |
| 3772 | // Add all depth WI entries to prompt | 3819 | // Add all depth WI entries to prompt |
| 3773 | flushWIDepthInjections(); | 3820 | flushWIInjections(); |
| 3774 | if (Array.isArray(worldInfoDepth)) { | 3821 | if (Array.isArray(worldInfoDepth)) { |
| 3775 | worldInfoDepth.forEach((e) => { | 3822 | worldInfoDepth.forEach((e) => { |
| 3776 | const joinedEntries = e.entries.join('\n'); | 3823 | const joinedEntries = e.entries.join('\n'); |
| 3777 | setExtensionPrompt(inject_ids.CUSTOM_WI_DEPTH_ROLE(e.depth, e.role), joinedEntries, extension_prompt_types.IN_CHAT, e.depth, false, e.role); | 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 | } else { | 3832 | } else { |
| 3781 | console.log('skipping WIAN'); | 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 | // Collect before / after story string injections | 3852 | // Collect before / after story string injections |
| 3785 | const beforeScenarioAnchor = await getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT); | 3853 | const beforeScenarioAnchor = await getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT); |
| 3786 | const afterScenarioAnchor = await getExtensionPrompt(extension_prompt_types.IN_PROMPT); | 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 | // This operation will result in the injectedIndices indexes being off by one | 3913 | // This operation will result in the injectedIndices indexes being off by one |
| 3846 | coreChat.push({ mes: jailbreak, is_user: true }); | 3914 | coreChat.push({ mes: jailbreak, is_user: true }); |
| 3847 | // Add +1 to the elements to correct for the new PHI/Jailbreak message. | 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 | let chat2 = []; | 3921 | let chat2 = []; |
| 3854 | let continue_mag = ''; | 3922 | let continue_mag = ''; |
| 3855 | const userMessageIndices = []; | 3923 | let userMessageIndices = []; |
| 3856 | const lastUserMessageIndex = coreChat.findLastIndex(x => x.is_user); | 3924 | const lastUserMessageIndex = coreChat.findLastIndex(x => x.is_user); |
| 3857 | 3925 | ||
| 3858 | for (let i = coreChat.length - 1, j = 0; i >= 0; i--, j++) { | 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 | // Only add the chat in context if past the greeting message | 4019 | // Only add the chat in context if past the greeting message |
| 3952 | if (isContinue && (chat2.length > 1 || main_api === 'openai')) { | 4020 | if (isContinue && (chat2.length > 1 || main_api === 'openai')) { |
| 3953 | cyclePrompt = chat2.shift(); | 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 | // Collect enough messages to fill the context | 4027 | // Collect enough messages to fill the context |
| 3957 | let arrMes = new Array(chat2.length); | 4028 | let arrMes = new Array(chat2.length); |
| 3958 | let tokenCount = await getMessagesTokenCount(); | 4029 | let tokenCount = await getMessagesTokenCount(); |
| 3959 | let lastAddedIndex = -1; | 4030 | let lastAddedIndex = 0; |
| 3960 | 4031 | ||
| 3961 | // Pre-allocate all injections first. | 4032 | // Pre-allocate all injections first. |
| 3962 | // If it doesn't fit - user shot himself in the foot | 4033 | // If it doesn't fit - user shot himself in the foot |
| 3963 | for (const index of injectedIndices) { | 4034 | for (const index of injectedIndices) { |
| 4035 | // not needed for OAI prompting | ||
| 4036 | if (main_api == 'openai') { | ||
| 4037 | break; | ||
| 4038 | } | ||
| 4039 | |||
| 3964 | const item = chat2[index]; | 4040 | const item = chat2[index]; |
| 3965 | 4041 | ||
| 3966 | if (typeof item !== 'string') { | 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 | await eventSource.emit(event_types.GENERATE_AFTER_DATA, generate_data); | 4473 | await eventSource.emit(event_types.GENERATE_AFTER_DATA, generate_data, dryRun); |
| 4398 | 4474 | ||
| 4399 | if (dryRun) { | 4475 | if (dryRun) { |
| 4400 | return Promise.resolve(); | 4476 | return Promise.resolve(); |
| @@ -4703,7 +4779,7 @@ export function stopGeneration() { | |||
| 4703 | * @returns {Promise<number[]>} Array of indices where the extension prompts were injected | 4779 | * @returns {Promise<number[]>} Array of indices where the extension prompts were injected |
| 4704 | */ | 4780 | */ |
| 4705 | async function doChatInject(messages, isContinue) { | 4781 | async function doChatInject(messages, isContinue) { |
| 4706 | const injectedIndices = []; | 4782 | const injectedMessages = []; |
| 4707 | let totalInsertedMessages = 0; | 4783 | let totalInsertedMessages = 0; |
| 4708 | messages.reverse(); | 4784 | messages.reverse(); |
| 4709 | 4785 | ||
| @@ -4743,18 +4819,21 @@ async function doChatInject(messages, isContinue) { | |||
| 4743 | const injectIdx = Math.min(depth + totalInsertedMessages, messages.length); | 4819 | const injectIdx = Math.min(depth + totalInsertedMessages, messages.length); |
| 4744 | messages.splice(injectIdx, 0, ...roleMessages); | 4820 | messages.splice(injectIdx, 0, ...roleMessages); |
| 4745 | totalInsertedMessages += roleMessages.length; | 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 | messages.reverse(); | 4827 | messages.reverse(); |
| 4751 | return injectedIndices; | 4828 | return injectedIndices; |
| 4752 | } | 4829 | } |
| 4753 | 4830 | ||
| 4754 | function flushWIDepthInjections() { | 4831 | function flushWIInjections() { |
| 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 | for (const key of Object.keys(extension_prompts)) { | 4835 | for (const key of Object.keys(extension_prompts)) { |
| 4757 | if (key.startsWith(inject_ids.CUSTOM_WI_DEPTH)) { | 4836 | if (key.startsWith(depthPrefix) || key.startsWith(outletPrefix)) { |
| 4758 | delete extension_prompts[key]; | 4837 | delete extension_prompts[key]; |
| 4759 | } | 4838 | } |
| 4760 | } | 4839 | } |
| @@ -4775,7 +4854,7 @@ function unblockGeneration(type) { | |||
| 4775 | showSwipeButtons(); | 4854 | showSwipeButtons(); |
| 4776 | setGenerationProgress(0); | 4855 | setGenerationProgress(0); |
| 4777 | flushEphemeralStoppingStrings(); | 4856 | flushEphemeralStoppingStrings(); |
| 4778 | flushWIDepthInjections(); | 4857 | flushWIInjections(); |
| 4779 | } | 4858 | } |
| 4780 | 4859 | ||
| 4781 | export function getNextMessageId(type) { | 4860 | export function getNextMessageId(type) { |
| @@ -4977,6 +5056,8 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul | |||
| 4977 | await populateFileAttachment(message); | 5056 | await populateFileAttachment(message); |
| 4978 | statMesProcess(message, 'user', characters, this_chid, ''); | 5057 | statMesProcess(message, 'user', characters, this_chid, ''); |
| 4979 | 5058 | ||
| 5059 | chat_metadata['tainted'] = true; | ||
| 5060 | |||
| 4980 | if (typeof insertAt === 'number' && insertAt >= 0 && insertAt <= chat.length) { | 5061 | if (typeof insertAt === 'number' && insertAt >= 0 && insertAt <= chat.length) { |
| 4981 | chat.splice(insertAt, 0, message); | 5062 | chat.splice(insertAt, 0, message); |
| 4982 | await saveChatConditional(); | 5063 | await saveChatConditional(); |
| @@ -5110,18 +5191,18 @@ export async function duplicateCharacter() { | |||
| 5110 | } | 5191 | } |
| 5111 | 5192 | ||
| 5112 | function setInContextMessages(msgInContextCount, type) { | 5193 | function setInContextMessages(msgInContextCount, type) { |
| 5113 | $('#chat .mes').removeClass('lastInContext'); | 5194 | chatElement.find('.mes').removeClass('lastInContext'); |
| 5114 | 5195 | ||
| 5115 | if (type === 'swipe' || type === 'regenerate' || type === 'continue') { | 5196 | if (type === 'swipe' || type === 'regenerate' || type === 'continue') { |
| 5116 | msgInContextCount++; | 5197 | msgInContextCount++; |
| 5117 | } | 5198 | } |
| 5118 | 5199 | ||
| 5119 | const lastMessageBlock = $('#chat .mes:not([is_system="true"])').eq(-msgInContextCount); | 5200 | const lastMessageBlock = chatElement.find('.mes:not([is_system="true"])').eq(-msgInContextCount); |
| 5120 | lastMessageBlock.addClass('lastInContext'); | 5201 | lastMessageBlock.addClass('lastInContext'); |
| 5121 | 5202 | ||
| 5122 | if (lastMessageBlock.length === 0) { | 5203 | if (lastMessageBlock.length === 0) { |
| 5123 | const firstMessageId = getFirstDisplayedMessageId(); | 5204 | const firstMessageId = getFirstDisplayedMessageId(); |
| 5124 | $(`#chat .mes[mesid="${firstMessageId}"`).addClass('lastInContext'); | 5205 | chatElement.find(`.mes[mesid="${firstMessageId}"`).addClass('lastInContext'); |
| 5125 | } | 5206 | } |
| 5126 | 5207 | ||
| 5127 | // Update last id to chat. No metadata save on purpose, gets hopefully saved via another call | 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 | chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); | 5760 | chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); |
| 5680 | } | 5761 | } |
| 5681 | const chat_id = (chat.length - 1); | 5762 | const chat_id = (chat.length - 1); |
| 5682 | await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type); | 5763 | !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type); |
| 5683 | addOneMessage(chat[chat_id], { type: 'swipe' }); | 5764 | addOneMessage(chat[chat_id], { type: 'swipe' }); |
| 5684 | await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type); | 5765 | !fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type); |
| 5685 | } else { | 5766 | } else { |
| 5686 | chat[chat.length - 1]['mes'] = getMessage; | 5767 | chat[chat.length - 1]['mes'] = getMessage; |
| 5687 | } | 5768 | } |
| @@ -5703,9 +5784,9 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title | |||
| 5703 | chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); | 5784 | chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); |
| 5704 | } | 5785 | } |
| 5705 | const chat_id = (chat.length - 1); | 5786 | const chat_id = (chat.length - 1); |
| 5706 | await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type); | 5787 | !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type); |
| 5707 | addOneMessage(chat[chat_id], { type: 'swipe' }); | 5788 | addOneMessage(chat[chat_id], { type: 'swipe' }); |
| 5708 | await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type); | 5789 | !fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type); |
| 5709 | } else if (type === 'appendFinal') { | 5790 | } else if (type === 'appendFinal') { |
| 5710 | oldMessage = chat[chat.length - 1]['mes']; | 5791 | oldMessage = chat[chat.length - 1]['mes']; |
| 5711 | console.debug('Trying to appendFinal.'); | 5792 | console.debug('Trying to appendFinal.'); |
| @@ -5724,9 +5805,9 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title | |||
| 5724 | chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); | 5805 | chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0); |
| 5725 | } | 5806 | } |
| 5726 | const chat_id = (chat.length - 1); | 5807 | const chat_id = (chat.length - 1); |
| 5727 | await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type); | 5808 | !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type); |
| 5728 | addOneMessage(chat[chat_id], { type: 'swipe' }); | 5809 | addOneMessage(chat[chat_id], { type: 'swipe' }); |
| 5729 | await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type); | 5810 | !fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type); |
| 5730 | 5811 | ||
| 5731 | } else { | 5812 | } else { |
| 5732 | console.debug('entering chat update routine for non-swipe post'); | 5813 | console.debug('entering chat update routine for non-swipe post'); |
| @@ -6617,7 +6698,7 @@ async function getChatResult() { | |||
| 6617 | } | 6698 | } |
| 6618 | 6699 | ||
| 6619 | function getFirstMessage() { | 6700 | function getFirstMessage() { |
| 6620 | const firstMes = characters[this_chid].first_mes || ''; | 6701 | const firstMes = characters[this_chid]?.first_mes || ''; |
| 6621 | const alternateGreetings = characters[this_chid]?.data?.alternate_greetings; | 6702 | const alternateGreetings = characters[this_chid]?.data?.alternate_greetings; |
| 6622 | 6703 | ||
| 6623 | const message = { | 6704 | const message = { |
| @@ -6639,7 +6720,12 @@ function getFirstMessage() { | |||
| 6639 | 6720 | ||
| 6640 | message['swipe_id'] = 0; | 6721 | message['swipe_id'] = 0; |
| 6641 | message['swipes'] = swipes; | 6722 | message['swipes'] = swipes; |
| 6642 | message['swipe_info'] = []; | 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 | return message; | 6731 | return message; |
| @@ -6752,7 +6838,7 @@ export function changeMainAPI() { | |||
| 6752 | $('#ai_module_block_novel').css('display', 'none'); | 6838 | $('#ai_module_block_novel').css('display', 'none'); |
| 6753 | } | 6839 | } |
| 6754 | 6840 | ||
| 6755 | $('#prompt_cost_block').toggle(selectedVal === 'textgenerationwebui'); | 6841 | $('#prompt_cost_block').toggle(selectedVal === 'textgenerationwebui' && textgen_settings.type === textgen_types.OPENROUTER); |
| 6756 | 6842 | ||
| 6757 | // Hide common settings for OpenAI | 6843 | // Hide common settings for OpenAI |
| 6758 | console.debug('value?', selectedVal); | 6844 | console.debug('value?', selectedVal); |
| @@ -7274,8 +7360,9 @@ export function getCurrentChatDetails() { | |||
| 7274 | * The function first fetches the chats, processes them, and then displays them in | 7360 | * The function first fetches the chats, processes them, and then displays them in |
| 7275 | * the HTML. It also has a built-in search functionality that allows filtering the | 7361 | * the HTML. It also has a built-in search functionality that allows filtering the |
| 7276 | * displayed chats based on a search query. | 7362 | * displayed chats based on a search query. |
| 7363 | * @param {string[]} hightlightNames - An array of chat names to highlight | ||
| 7277 | */ | 7364 | */ |
| 7278 | export async function displayPastChats() { | 7365 | export async function displayPastChats(hightlightNames = []) { |
| 7279 | $('#select_chat_div').empty(); | 7366 | $('#select_chat_div').empty(); |
| 7280 | $('#select_chat_search').val('').off('input'); | 7367 | $('#select_chat_search').val('').off('input'); |
| 7281 | 7368 | ||
| @@ -7284,10 +7371,10 @@ export async function displayPastChats() { | |||
| 7284 | const displayName = chatDetails.characterName; | 7371 | const displayName = chatDetails.characterName; |
| 7285 | const avatarImg = chatDetails.avatarImgURL; | 7372 | const avatarImg = chatDetails.avatarImgURL; |
| 7286 | 7373 | ||
| 7287 | await displayChats('', currentChat, displayName, avatarImg, selected_group); | 7374 | await displayChats('', currentChat, displayName, avatarImg, selected_group, hightlightNames); |
| 7288 | 7375 | ||
| 7289 | const debouncedDisplay = debounce((searchQuery) => { | 7376 | const debouncedDisplay = debounce((searchQuery) => { |
| 7290 | displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group); | 7377 | displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group, []); |
| 7291 | }); | 7378 | }); |
| 7292 | 7379 | ||
| 7293 | // Define the search input listener | 7380 | // Define the search input listener |
| @@ -7303,7 +7390,7 @@ export async function displayPastChats() { | |||
| 7303 | }, 200); | 7390 | }, 200); |
| 7304 | } | 7391 | } |
| 7305 | 7392 | ||
| 7306 | async function displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group) { | 7393 | async function displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group, highlightNames) { |
| 7307 | try { | 7394 | try { |
| 7308 | const trimExtension = (fileName) => String(fileName).replace('.jsonl', ''); | 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 | $('#select_chat_div').append(template); | 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 | } catch (error) { | 7440 | } catch (error) { |
| 7348 | console.error('Error loading chats:', error); | 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 | * Selects the right menu for displaying the character editor. | 7571 | * Selects the right menu for displaying the character editor. |
| 7479 | * @param {number|string} chid Character array index | 7572 | * @param {string} chid Character array index |
| 7480 | * @param {object} [param1] Options for the switch | 7573 | * @param {object} [param1] Options for the switch |
| 7481 | * @param {boolean} [param1.switchMenu=true] Whether to switch the menu | 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 | $('#character_media_allowed_icon').toggle(externalMediaState); | 7647 | $('#character_media_allowed_icon').toggle(externalMediaState); |
| 7555 | $('#character_media_forbidden_icon').toggle(!externalMediaState); | 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 | saveSettingsDebounced(); | 7655 | saveSettingsDebounced(); |
| 7558 | } | 7656 | } |
| 7559 | 7657 | ||
| @@ -7836,7 +7934,7 @@ export function showSwipeButtons() { | |||
| 7836 | }; | 7934 | }; |
| 7837 | } | 7935 | } |
| 7838 | 7936 | ||
| 7839 | const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`); | 7937 | const currentMessage = chatElement.children().filter(`[mesid="${chat.length - 1}"]`); |
| 7840 | const swipeId = chat[chat.length - 1].swipe_id; | 7938 | const swipeId = chat[chat.length - 1].swipe_id; |
| 7841 | const swipeCounterText = formatSwipeCounter((swipeId + 1), chat[chat.length - 1].swipes.length); | 7939 | const swipeCounterText = formatSwipeCounter((swipeId + 1), chat[chat.length - 1].swipes.length); |
| 7842 | const swipeRight = currentMessage.find('.swipe_right'); | 7940 | const swipeRight = currentMessage.find('.swipe_right'); |
| @@ -7872,42 +7970,45 @@ export function hideSwipeButtons() { | |||
| 7872 | /** | 7970 | /** |
| 7873 | * Deletes a swipe from the chat. | 7971 | * Deletes a swipe from the chat. |
| 7874 | * | 7972 | * |
| 7875 | * @param {number?} swipeId - The ID of the swipe to delete. If not provided, the current swipe will be deleted. | 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 | * @returns {Promise<number>|undefined} - The ID of the new swipe after deletion. | 7975 | * @returns {Promise<number>|undefined} - The ID of the new swipe after deletion. |
| 7877 | */ | 7976 | */ |
| 7878 | export async function deleteSwipe(swipeId = null) { | 7977 | export async function deleteSwipe(swipeId = null, messageId = chat.length - 1) { |
| 7879 | if (swipeId && (isNaN(swipeId) || swipeId < 0)) { | 7978 | if (swipeId && (isNaN(swipeId) || swipeId < 0)) { |
| 7880 | toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`); | 7979 | toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`); |
| 7881 | return; | 7980 | return; |
| 7882 | } | 7981 | } |
| 7883 | 7982 | ||
| 7884 | const lastMessage = chat[chat.length - 1]; | 7983 | const message = chat[messageId]; |
| 7885 | if (!lastMessage || !Array.isArray(lastMessage.swipes) || !lastMessage.swipes.length) { | 7984 | if (!message || !Array.isArray(message.swipes) || !message.swipes.length) { |
| 7886 | toastr.warning(t`No messages to delete swipes from.`); | 7985 | toastr.warning(t`No messages to delete swipes from.`); |
| 7887 | return; | 7986 | return; |
| 7888 | } | 7987 | } |
| 7889 | 7988 | ||
| 7890 | if (lastMessage.swipes.length <= 1) { | 7989 | if (message.swipes.length <= 1) { |
| 7891 | toastr.warning(t`Can't delete the last swipe.`); | 7990 | toastr.warning(t`Can't delete the last swipe.`); |
| 7892 | return; | 7991 | return; |
| 7893 | } | 7992 | } |
| 7894 | 7993 | ||
| 7895 | swipeId = swipeId ?? lastMessage.swipe_id; | 7994 | swipeId = swipeId ?? message.swipe_id; |
| 7896 | 7995 | ||
| 7897 | if (swipeId < 0 || swipeId >= lastMessage.swipes.length) { | 7996 | if (swipeId < 0 || swipeId >= message.swipes.length) { |
| 7898 | toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`); | 7997 | toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`); |
| 7899 | return; | 7998 | return; |
| 7900 | } | 7999 | } |
| 7901 | 8000 | ||
| 7902 | lastMessage.swipes.splice(swipeId, 1); | 8001 | message.swipes.splice(swipeId, 1); |
| 7903 | 8002 | ||
| 7904 | if (Array.isArray(lastMessage.swipe_info) && lastMessage.swipe_info.length) { | 8003 | if (Array.isArray(message.swipe_info) && message.swipe_info.length) { |
| 7905 | lastMessage.swipe_info.splice(swipeId, 1); | 8004 | message.swipe_info.splice(swipeId, 1); |
| 7906 | } | 8005 | } |
| 7907 | 8006 | ||
| 7908 | // Select the next swipe, or the one before if it was the last one | 8007 | // Select the next swipe, or the one before if it was the last one |
| 7909 | const newSwipeId = Math.min(swipeId, lastMessage.swipes.length - 1); | 8008 | const newSwipeId = Math.min(swipeId, message.swipes.length - 1); |
| 7910 | syncSwipeToMes(null, newSwipeId); | 8009 | syncSwipeToMes(messageId, newSwipeId); |
| 8010 | |||
| 8011 | await eventSource.emit(event_types.MESSAGE_SWIPE_DELETED, { messageId, swipeId, newSwipeId }); | ||
| 7911 | 8012 | ||
| 7912 | await saveChatConditional(); | 8013 | await saveChatConditional(); |
| 7913 | await reloadCurrentChat(); | 8014 | await reloadCurrentChat(); |
| @@ -7957,9 +8058,11 @@ export async function saveChatConditional() { | |||
| 7957 | /** | 8058 | /** |
| 7958 | * Saves the chat to the server. | 8059 | * Saves the chat to the server. |
| 7959 | * @param {FormData} formData Form data to send to the server. | 8060 | * @param {FormData} formData Form data to send to the server. |
| 7960 | * @param {EventTarget} eventTarget Event target to trigger the event on. | 8061 | * @param {object} [options={}] Options for the 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 | async function importCharacterChat(formData, eventTarget) { | 8065 | async function importCharacterChat(formData, { refresh = true } = {}) { |
| 7963 | const fetchResult = await fetch('/api/chats/import', { | 8066 | const fetchResult = await fetch('/api/chats/import', { |
| 7964 | method: 'POST', | 8067 | method: 'POST', |
| 7965 | body: formData, | 8068 | body: formData, |
| @@ -7969,26 +8072,25 @@ async function importCharacterChat(formData, eventTarget) { | |||
| 7969 | 8072 | ||
| 7970 | if (fetchResult.ok) { | 8073 | if (fetchResult.ok) { |
| 7971 | const data = await fetchResult.json(); | 8074 | const data = await fetchResult.json(); |
| 7972 | if (data.res) { | 8075 | if (data.res && refresh) { |
| 7973 | await displayPastChats(); | 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 | function updateViewMessageIds(startFromZero = false) { | 8084 | function updateViewMessageIds(startFromZero = false) { |
| 7983 | const minId = startFromZero ? 0 : getFirstDisplayedMessageId(); | 8085 | const minId = startFromZero ? 0 : getFirstDisplayedMessageId(); |
| 7984 | 8086 | ||
| 7985 | $('#chat').find('.mes').each(function (index, element) { | 8087 | chatElement.find('.mes').each(function (index, element) { |
| 7986 | $(element).attr('mesid', minId + index); | 8088 | $(element).attr('mesid', minId + index); |
| 7987 | $(element).find('.mesIDDisplay').text(`#${minId + index}`); | 8089 | $(element).find('.mesIDDisplay').text(`#${minId + index}`); |
| 7988 | }); | 8090 | }); |
| 7989 | 8091 | ||
| 7990 | $('#chat .mes').removeClass('last_mes'); | 8092 | chatElement.find('.mes').removeClass('last_mes'); |
| 7991 | $('#chat .mes').last().addClass('last_mes'); | 8093 | chatElement.find('.mes').last().addClass('last_mes'); |
| 7992 | 8094 | ||
| 7993 | updateEditArrowClasses(); | 8095 | updateEditArrowClasses(); |
| 7994 | } | 8096 | } |
| @@ -8000,14 +8102,14 @@ export function getFirstDisplayedMessageId() { | |||
| 8000 | } | 8102 | } |
| 8001 | 8103 | ||
| 8002 | function updateEditArrowClasses() { | 8104 | function updateEditArrowClasses() { |
| 8003 | $('#chat .mes .mes_edit_up').removeClass('disabled'); | 8105 | chatElement.find('.mes .mes_edit_up').removeClass('disabled'); |
| 8004 | $('#chat .mes .mes_edit_down').removeClass('disabled'); | 8106 | chatElement.find('.mes .mes_edit_down').removeClass('disabled'); |
| 8005 | 8107 | ||
| 8006 | if (this_edit_mes_id !== undefined) { | 8108 | if (this_edit_mes_id !== undefined) { |
| 8007 | const down = $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_down`); | 8109 | const down = chatElement.find(`.mes[mesid="${this_edit_mes_id}"] .mes_edit_down`); |
| 8008 | const up = $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_up`); | 8110 | const up = chatElement.find(`.mes[mesid="${this_edit_mes_id}"] .mes_edit_up`); |
| 8009 | const lastId = Number($('#chat .mes').last().attr('mesid')); | 8111 | const lastId = Number(chatElement.find('.mes').last().attr('mesid')); |
| 8010 | const firstId = Number($('#chat .mes').first().attr('mesid')); | 8112 | const firstId = Number(chatElement.find('.mes').first().attr('mesid')); |
| 8011 | 8113 | ||
| 8012 | if (lastId == Number(this_edit_mes_id)) { | 8114 | if (lastId == Number(this_edit_mes_id)) { |
| 8013 | down.addClass('disabled'); | 8115 | down.addClass('disabled'); |
| @@ -8026,7 +8128,7 @@ function updateEditArrowClasses() { | |||
| 8026 | export function closeMessageEditor(what = 'all') { | 8128 | export function closeMessageEditor(what = 'all') { |
| 8027 | if (what === 'message' || what === 'all') { | 8129 | if (what === 'message' || what === 'all') { |
| 8028 | if (this_edit_mes_id) { | 8130 | if (this_edit_mes_id) { |
| 8029 | $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).trigger('click'); | 8131 | chatElement.find(`.mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).trigger('click'); |
| 8030 | } | 8132 | } |
| 8031 | } | 8133 | } |
| 8032 | if (what === 'reasoning' || what === 'all') { | 8134 | if (what === 'reasoning' || what === 'all') { |
| @@ -8081,61 +8183,16 @@ async function openCharacterWorldPopup() { | |||
| 8081 | const selectedValue = $(this).val(); | 8183 | const selectedValue = $(this).val(); |
| 8082 | const worldIndex = selectedValue !== '' ? Number(selectedValue) : NaN; | 8184 | const worldIndex = selectedValue !== '' ? Number(selectedValue) : NaN; |
| 8083 | const name = !isNaN(worldIndex) ? world_names[worldIndex] : ''; | 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 | function handleExtrasWorldSelect() { | 8189 | function handleExtrasWorldSelect(evt) { |
| 8115 | const selectedValues = $(this).val(); | 8190 | const el = evt?.currentTarget ?? this; |
| 8116 | const selectedWorlds = Array.isArray(selectedValues) ? selectedValues : []; | 8191 | const selectedValues = $(el).val(); |
| 8117 | let charLore = world_info.charLore ?? []; | 8192 | const selected = Array.isArray(selectedValues) ? selectedValues : []; |
| 8118 | const tempExtraBooks = selectedWorlds.map((index) => world_names[index]).filter(Boolean); | 8193 | const fileName = getCharaFilename(null, {}); |
| 8119 | const existingCharIndex = charLore.findIndex((e) => e.name === fileName); | 8194 | const nextList = selected.map(i => world_names[i]).filter(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 | // --- Populate Dropdowns --- | 8198 | // --- Populate Dropdowns --- |
| @@ -8258,7 +8315,7 @@ function addAlternateGreeting(template, greeting, index, getArray, popup) { | |||
| 8258 | * Creates or edits a character based on the form data. | 8315 | * Creates or edits a character based on the form data. |
| 8259 | * @param {Event} [e] Event that triggered the function call. | 8316 | * @param {Event} [e] Event that triggered the function call. |
| 8260 | */ | 8317 | */ |
| 8261 | async function createOrEditCharacter(e) { | 8318 | export async function createOrEditCharacter(e) { |
| 8262 | $('#rm_info_avatar').html(''); | 8319 | $('#rm_info_avatar').html(''); |
| 8263 | const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0))); | 8320 | const formData = new FormData(/** @type {HTMLFormElement} */($('#form_create').get(0))); |
| 8264 | formData.set('fav', String(fav_ch_checked)); | 8321 | formData.set('fav', String(fav_ch_checked)); |
| @@ -8701,10 +8758,10 @@ export function swipe_right(_event = null, { source, repeated } = {}) { | |||
| 8701 | easing: animation_easing, | 8758 | easing: animation_easing, |
| 8702 | queue: false, | 8759 | queue: false, |
| 8703 | complete: async function () { | 8760 | complete: async function () { |
| 8704 | const is_animation_scroll = ($('#chat').scrollTop() >= ($('#chat').prop('scrollHeight') - $('#chat').outerHeight()) - 10); | 8761 | const is_animation_scroll = (chatElement.scrollTop() >= (chatElement.prop('scrollHeight') - chatElement.outerHeight()) - 10); |
| 8705 | //console.log(parseInt(chat[chat.length-1]['swipe_id'])); | 8762 | //console.log(parseInt(chat[chat.length-1]['swipe_id'])); |
| 8706 | //console.log(chat[chat.length-1]['swipes'].length); | 8763 | //console.log(chat[chat.length-1]['swipes'].length); |
| 8707 | const swipeMessage = $('#chat').find('[mesid="' + (chat.length - 1) + '"]'); | 8764 | const swipeMessage = chatElement.find('[mesid="' + (chat.length - 1) + '"]'); |
| 8708 | if (run_generate && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) { | 8765 | if (run_generate && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) { |
| 8709 | //shows "..." while generating | 8766 | //shows "..." while generating |
| 8710 | swipeMessage.find('.mes_text').html('...'); | 8767 | swipeMessage.find('.mes_text').html('...'); |
| @@ -8737,12 +8794,12 @@ export function swipe_right(_event = null, { source, repeated } = {}) { | |||
| 8737 | queue: false, | 8794 | queue: false, |
| 8738 | progress: function () { | 8795 | progress: function () { |
| 8739 | // Scroll the chat down as the message expands | 8796 | // Scroll the chat down as the message expands |
| 8740 | if (is_animation_scroll) $('#chat').scrollTop($('#chat')[0].scrollHeight); | 8797 | if (is_animation_scroll) chatElement.scrollTop(chatElement[0].scrollHeight); |
| 8741 | }, | 8798 | }, |
| 8742 | complete: function () { | 8799 | complete: function () { |
| 8743 | this_mes_div.css('height', 'auto'); | 8800 | this_mes_div.css('height', 'auto'); |
| 8744 | // Scroll the chat down to the bottom once the animation is complete | 8801 | // Scroll the chat down to the bottom once the animation is complete |
| 8745 | if (is_animation_scroll) $('#chat').scrollTop($('#chat')[0].scrollHeight); | 8802 | if (is_animation_scroll) chatElement.scrollTop(chatElement[0].scrollHeight); |
| 8746 | }, | 8803 | }, |
| 8747 | }); | 8804 | }); |
| 8748 | this_mes_div.children('.mes_block').transition({ | 8805 | this_mes_div.children('.mes_block').transition({ |
| @@ -8888,6 +8945,8 @@ async function importCharacter(file, { preserveFileName = '', importTags = false | |||
| 8888 | return; | 8945 | return; |
| 8889 | } | 8946 | } |
| 8890 | 8947 | ||
| 8948 | const exists = preserveFileName ? characters.find(character => character.avatar === preserveFileName) : undefined; | ||
| 8949 | |||
| 8891 | const format = ext[1].toLowerCase(); | 8950 | const format = ext[1].toLowerCase(); |
| 8892 | $('#character_import_file_type').val(format); | 8951 | $('#character_import_file_type').val(format); |
| 8893 | const formData = new FormData(); | 8952 | const formData = new FormData(); |
| @@ -8914,10 +8973,20 @@ async function importCharacter(file, { preserveFileName = '', importTags = false | |||
| 8914 | } | 8973 | } |
| 8915 | 8974 | ||
| 8916 | if (data.file_name !== undefined) { | 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 | $('#character_search_bar').val('').trigger('input'); | 8983 | $('#character_search_bar').val('').trigger('input'); |
| 8918 | 8984 | ||
| 8985 | if (exists) { | ||
| 8986 | toastr.success(t`Character Replaced: ${String(data.file_name).replace('.png', '')}`); | ||
| 8987 | } else { | ||
| 8919 | toastr.success(t`Character Created: ${String(data.file_name).replace('.png', '')}`); | 8988 | toastr.success(t`Character Created: ${String(data.file_name).replace('.png', '')}`); |
| 8920 | let avatarFileName = `${data.file_name}.png`; | 8989 | } |
| 8921 | if (importTags) { | 8990 | if (importTags) { |
| 8922 | await importCharactersTags([avatarFileName]); | 8991 | await importCharactersTags([avatarFileName]); |
| 8923 | selectImportedChar(data.file_name); | 8992 | selectImportedChar(data.file_name); |
| @@ -8972,7 +9041,7 @@ export async function doNewChat({ deleteCurrentChat = false } = {}) { | |||
| 8972 | 9041 | ||
| 8973 | if (selected_group) { | 9042 | if (selected_group) { |
| 8974 | await createNewGroupChat(selected_group); | 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 | else { | 9046 | else { |
| 8978 | //RossAscends: added character name to new chat filenames and replaced Date.now() with humanizedDateTime; | 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 | * Forces the update of the chat name for a remote character. | 9173 | * Forces the update of the chat name for a remote character. |
| 9077 | * @param {string|number} characterId Character ID to update chat name for | 9174 | * @param {string|number} characterId Character ID to update chat name for |
| 9078 | * @param {string} newName New name for the chat | 9175 | * @param {string} newName New name for the chat |
| @@ -9137,6 +9234,22 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {}) | |||
| 9137 | characterKey = [characterKey]; | 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 | for (const key of characterKey) { | 9253 | for (const key of characterKey) { |
| 9141 | const character = characters.find(x => x.avatar == key); | 9254 | const character = characters.find(x => x.avatar == key); |
| 9142 | if (!character) { | 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 | $('#favorite_button').on('click', function () { | 9865 | $('#favorite_button').on('click', function () { |
| 9747 | updateFavButtonState(!fav_ch_checked); | 9866 | updateFavButtonState(!fav_ch_checked); |
| 9748 | if (menu_type != 'create') { | 9867 | if (menu_type != 'create') { |
| @@ -9940,24 +10059,7 @@ jQuery(async function () { | |||
| 9940 | } | 10059 | } |
| 9941 | 10060 | ||
| 9942 | else if (id == 'option_close_chat') { | 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 | else if (id === 'option_settings') { | 10065 | else if (id === 'option_settings') { |
| @@ -10021,15 +10123,15 @@ jQuery(async function () { | |||
| 10021 | }); | 10123 | }); |
| 10022 | 10124 | ||
| 10023 | if (this_del_mes >= 0) { | 10125 | if (this_del_mes >= 0) { |
| 10024 | $(`.mes[mesid="${this_del_mes}"]`).nextAll('div').remove(); | 10126 | chatElement.find(`.mes[mesid="${this_del_mes}"]`).nextAll('div').remove(); |
| 10025 | $(`.mes[mesid="${this_del_mes}"]`).remove(); | 10127 | chatElement.find(`.mes[mesid="${this_del_mes}"]`).remove(); |
| 10026 | chat.length = this_del_mes; | 10128 | chat.length = this_del_mes; |
| 10027 | chat_metadata['tainted'] = true; | 10129 | chat_metadata['tainted'] = true; |
| 10028 | await saveChatConditional(); | 10130 | await saveChatConditional(); |
| 10029 | chatElement.scrollTop(chatElement[0].scrollHeight); | 10131 | chatElement.scrollTop(chatElement[0].scrollHeight); |
| 10030 | await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); | 10132 | await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); |
| 10031 | $('#chat .mes').removeClass('last_mes'); | 10133 | chatElement.find('.mes').removeClass('last_mes'); |
| 10032 | $('#chat .mes').last().addClass('last_mes'); | 10134 | chatElement.find('.mes').last().addClass('last_mes'); |
| 10033 | } else { | 10135 | } else { |
| 10034 | console.log('this_del_mes is not >= 0, not deleting'); | 10136 | console.log('this_del_mes is not >= 0, not deleting'); |
| 10035 | } | 10137 | } |
| @@ -10134,9 +10236,9 @@ jQuery(async function () { | |||
| 10134 | return; | 10236 | return; |
| 10135 | }*/ | 10237 | }*/ |
| 10136 | 10238 | ||
| 10137 | let chatScrollPosition = $('#chat').scrollTop(); | 10239 | let chatScrollPosition = chatElement.scrollTop(); |
| 10138 | if (this_edit_mes_id !== undefined) { | 10240 | if (this_edit_mes_id !== undefined) { |
| 10139 | let mes_edited = $(`#chat [mesid="${this_edit_mes_id}"]`).find('.mes_edit_done'); | 10241 | let mes_edited = chatElement.find(`[mesid="${this_edit_mes_id}"]`).find('.mes_edit_done'); |
| 10140 | if (Number(edit_mes_id) == chat.length - 1) { //if the generating swipe (...) | 10242 | if (Number(edit_mes_id) == chat.length - 1) { //if the generating swipe (...) |
| 10141 | let run_edit = true; | 10243 | let run_edit = true; |
| 10142 | if (chat[edit_mes_id]['swipe_id'] !== undefined) { | 10244 | if (chat[edit_mes_id]['swipe_id'] !== undefined) { |
| @@ -10195,7 +10297,7 @@ jQuery(async function () { | |||
| 10195 | String(edit_textarea.val()).length, | 10297 | String(edit_textarea.val()).length, |
| 10196 | ); | 10298 | ); |
| 10197 | if (Number(this_edit_mes_id) === chat.length - 1) { | 10299 | if (Number(this_edit_mes_id) === chat.length - 1) { |
| 10198 | $('#chat').scrollTop(chatScrollPosition); | 10300 | chatElement.scrollTop(chatScrollPosition); |
| 10199 | } | 10301 | } |
| 10200 | 10302 | ||
| 10201 | updateEditArrowClasses(); | 10303 | updateEditArrowClasses(); |
| @@ -10313,7 +10415,7 @@ jQuery(async function () { | |||
| 10313 | 10415 | ||
| 10314 | hideSwipeButtons(); | 10416 | hideSwipeButtons(); |
| 10315 | const targetId = Number(this_edit_mes_id) - 1; | 10417 | const targetId = Number(this_edit_mes_id) - 1; |
| 10316 | const target = $(`#chat .mes[mesid="${targetId}"]`); | 10418 | const target = chatElement.find(`.mes[mesid="${targetId}"]`); |
| 10317 | const root = $(this).closest('.mes'); | 10419 | const root = $(this).closest('.mes'); |
| 10318 | 10420 | ||
| 10319 | if (root.length === 0 || target.length === 0) { | 10421 | if (root.length === 0 || target.length === 0) { |
| @@ -10342,7 +10444,7 @@ jQuery(async function () { | |||
| 10342 | 10444 | ||
| 10343 | hideSwipeButtons(); | 10445 | hideSwipeButtons(); |
| 10344 | const targetId = Number(this_edit_mes_id) + 1; | 10446 | const targetId = Number(this_edit_mes_id) + 1; |
| 10345 | const target = $(`#chat .mes[mesid="${targetId}"]`); | 10447 | const target = chatElement.find(`.mes[mesid="${targetId}"]`); |
| 10346 | const root = $(this).closest('.mes'); | 10448 | const root = $(this).closest('.mes'); |
| 10347 | 10449 | ||
| 10348 | if (root.length === 0 || target.length === 0) { | 10450 | if (root.length === 0 || target.length === 0) { |
| @@ -10414,7 +10516,7 @@ jQuery(async function () { | |||
| 10414 | if (deleteOnlySwipe) { | 10516 | if (deleteOnlySwipe) { |
| 10415 | const message = chat[this_edit_mes_id]; | 10517 | const message = chat[this_edit_mes_id]; |
| 10416 | const swipe_id = message.swipe_id; | 10518 | const swipe_id = message.swipe_id; |
| 10417 | await deleteSwipe(swipe_id); | 10519 | await deleteSwipe(swipe_id, Number(this_edit_mes_id)); |
| 10418 | return; | 10520 | return; |
| 10419 | } | 10521 | } |
| 10420 | 10522 | ||
| @@ -10519,41 +10621,45 @@ jQuery(async function () { | |||
| 10519 | }); | 10621 | }); |
| 10520 | 10622 | ||
| 10521 | $('#chat_import_file').on('change', async function (e) { | 10623 | $('#chat_import_file').on('change', async function (e) { |
| 10522 | const targetElement = /** @type {HTMLInputElement} */ (e.target); | 10624 | const targetElement = e.target; |
| 10523 | if (!(targetElement instanceof HTMLInputElement)) { | 10625 | const formElement = document.getElementById('form_import_chat'); |
| 10626 | if (!(targetElement instanceof HTMLInputElement) || !(formElement instanceof HTMLFormElement)) { | ||
| 10524 | return; | 10627 | return; |
| 10525 | } | 10628 | } |
| 10526 | const file = targetElement.files[0]; | ||
| 10527 | 10629 | ||
| 10528 | if (!file) { | 10630 | const importedFileNames = []; |
| 10529 | return; | ||
| 10530 | } | ||
| 10531 | 10631 | ||
| 10632 | for (const file of targetElement.files) { | ||
| 10532 | const ext = file.name.match(/\.(\w+)$/); | 10633 | const ext = file.name.match(/\.(\w+)$/); |
| 10533 | if ( | 10634 | const format = ext?.[1]?.toLowerCase(); |
| 10534 | !ext || | 10635 | |
| 10535 | (ext[1].toLowerCase() != 'json' && ext[1].toLowerCase() != 'jsonl') | 10636 | if (!['json', 'jsonl'].includes(format)) { |
| 10536 | ) { | 10637 | toastr.warning(t`Only JSON and JSONL files are supported for chat imports.`); |
| 10537 | return; | 10638 | continue; |
| 10538 | } | 10639 | } |
| 10539 | 10640 | ||
| 10540 | if (selected_group && file.name.endsWith('.json')) { | 10641 | if (selected_group && format === 'json') { |
| 10541 | toastr.warning('Only SillyTavern\'s own format is supported for group chat imports. Sorry!'); | 10642 | toastr.warning(t`Only SillyTavern's own format is supported for group chat imports. Sorry!`); |
| 10542 | return; | 10643 | continue; |
| 10543 | } | 10644 | } |
| 10544 | 10645 | ||
| 10545 | const format = ext[1].toLowerCase(); | 10646 | const formData = new FormData(formElement); |
| 10546 | $('#chat_import_file_type').val(format); | 10647 | formData.set('file_type', format); |
| 10648 | formData.set('avatar', file); | ||
| 10649 | formData.set('user_name', name1); | ||
| 10547 | 10650 | ||
| 10548 | const formData = new FormData(/** @type {HTMLFormElement} */($('#form_import_chat').get(0))); | 10651 | const importFn = selected_group ? importGroupChat : importCharacterChat; |
| 10549 | formData.append('user_name', name1); | 10652 | const result = await importFn(formData, { refresh: false }); |
| 10550 | $('#select_chat_div').html(''); | 10653 | importedFileNames.push(...result); |
| 10654 | } | ||
| 10551 | 10655 | ||
| 10552 | if (selected_group) { | 10656 | if (importedFileNames.length > 0) { |
| 10553 | await importGroupChat(formData, e.originalEvent.target); | 10657 | toastr.success(t`Successfully imported ${importedFileNames.length} chat(s).`); |
| 10554 | } else { | ||
| 10555 | await importCharacterChat(formData, e.originalEvent.target); | ||
| 10556 | } | 10658 | } |
| 10659 | |||
| 10660 | await displayPastChats(importedFileNames); | ||
| 10661 | |||
| 10662 | targetElement.value = ''; | ||
| 10557 | }); | 10663 | }); |
| 10558 | 10664 | ||
| 10559 | $('#rm_button_group_chats').on('click', function () { | 10665 | $('#rm_button_group_chats').on('click', function () { |
| @@ -10775,7 +10881,7 @@ jQuery(async function () { | |||
| 10775 | return; | 10881 | return; |
| 10776 | } | 10882 | } |
| 10777 | if (isEditVisible && power_user.auto_save_msg_edits === true) { | 10883 | if (isEditVisible && power_user.auto_save_msg_edits === true) { |
| 10778 | $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).trigger('click'); | 10884 | chatElement.find(`.mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).trigger('click'); |
| 10779 | closeMessageEditor('reasoning'); | 10885 | closeMessageEditor('reasoning'); |
| 10780 | $('#send_textarea').trigger('focus'); | 10886 | $('#send_textarea').trigger('focus'); |
| 10781 | return; | 10887 | return; |
| @@ -10819,27 +10925,66 @@ jQuery(async function () { | |||
| 10819 | } | 10925 | } |
| 10820 | } break; | 10926 | } break; |
| 10821 | case 'replace_update': { | 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 | |
| 10930 | const POPUP_RESULT_URL = POPUP_RESULT.CUSTOM1, POPUP_RESULT_FILE = POPUP_RESULT.CUSTOM2; | ||
| 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 | }); | ||
| 10948 | |||
| 10949 | // Remember the chat currently selected, so we can reload it after the replacement | ||
| 10950 | const currentChatFile = characters[this_chid]['chat']; | ||
| 10951 | async function postReplace() { | ||
| 10952 | await openCharacterChat(currentChatFile); | ||
| 10953 | } | ||
| 10954 | |||
| 10955 | switch (result) { | ||
| 10956 | case POPUP_RESULT_FILE: { | ||
| 10824 | async function uploadReplacementCard(e) { | 10957 | async function uploadReplacementCard(e) { |
| 10825 | const file = e.target.files[0]; | 10958 | const file = e.target.files[0]; |
| 10826 | |||
| 10827 | if (!file) { | 10959 | if (!file) { |
| 10828 | return; | 10960 | return; |
| 10829 | } | 10961 | } |
| 10830 | 10962 | ||
| 10831 | try { | 10963 | try { |
| 10832 | const chatFile = characters[this_chid]['chat']; | ||
| 10833 | const data = new Map(); | 10964 | const data = new Map(); |
| 10834 | data.set(file, characters[this_chid].avatar); | 10965 | data.set(file, characters[this_chid].avatar); |
| 10835 | await processDroppedFiles([file], data); | 10966 | await processDroppedFiles([file], data); |
| 10836 | await openCharacterChat(chatFile); | 10967 | await postReplace(); |
| 10837 | await fetch(getThumbnailUrl('avatar', characters[this_chid].avatar), { cache: 'reload' }); | ||
| 10838 | } catch { | 10968 | } catch { |
| 10839 | toastr.error('Failed to replace the character card.', 'Something went wrong'); | 10969 | toastr.error('Failed to replace the character card.', 'Something went wrong'); |
| 10840 | } | 10970 | } |
| 10841 | } | 10971 | } |
| 10842 | $('#character_replace_file').off('change').on('change', uploadReplacementCard).trigger('click'); | 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; | ||
| 10987 | } | ||
| 10843 | } | 10988 | } |
| 10844 | } break; | 10989 | } break; |
| 10845 | case 'import_tags': { | 10990 | case 'import_tags': { |
| @@ -10881,7 +11026,6 @@ jQuery(async function () { | |||
| 10881 | if (!(e.target instanceof HTMLElement)) { | 11026 | if (!(e.target instanceof HTMLElement)) { |
| 10882 | return; | 11027 | return; |
| 10883 | } | 11028 | } |
| 10884 | e.target.focus(); | ||
| 10885 | e.target.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true })); | 11029 | e.target.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true })); |
| 10886 | }); | 11030 | }); |
| 10887 | 11031 | ||
| @@ -10950,47 +11094,7 @@ jQuery(async function () { | |||
| 10950 | const inputs = String(input).split('\n').map(x => x.trim()).filter(x => x.length > 0); | 11094 | const inputs = String(input).split('\n').map(x => x.trim()).filter(x => x.length > 0); |
| 10951 | 11095 | ||
| 10952 | for (const url of inputs) { | 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 | await processDroppedFiles(files); | 11105 | await processDroppedFiles(files); |
| 11002 | }, { noAnimation: true }); | 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 | $('#charListGridToggle').on('click', async () => { | 11116 | $('#charListGridToggle').on('click', async () => { |
| 11005 | doCharListDisplaySwitch(); | 11117 | doCharListDisplaySwitch(); |
| 11006 | }); | 11118 | }); |
| @@ -766,6 +766,7 @@ export function initRossMods() { | |||
| 766 | } | 766 | } |
| 767 | }); | 767 | }); |
| 768 | 768 | ||
| 769 | if (!isMobile()) { //only read/set pin states on non-mobile devices | ||
| 769 | // read the state of right Nav Lock and apply to rightnav classlist | 770 | // read the state of right Nav Lock and apply to rightnav classlist |
| 770 | $(RPanelPin).prop('checked', accountStorage.getItem('NavLockOn') == 'true'); | 771 | $(RPanelPin).prop('checked', accountStorage.getItem('NavLockOn') == 'true'); |
| 771 | if (accountStorage.getItem('NavLockOn') == 'true') { | 772 | if (accountStorage.getItem('NavLockOn') == 'true') { |
| @@ -804,6 +805,8 @@ export function initRossMods() { | |||
| 804 | $(WorldInfo).addClass('pinnedOpen'); | 805 | $(WorldInfo).addClass('pinnedOpen'); |
| 805 | $(WIDrawerIcon).addClass('drawerPinnedOpen'); | 806 | $(WIDrawerIcon).addClass('drawerPinnedOpen'); |
| 806 | } | 807 | } |
| 808 | } | ||
| 809 | |||
| 807 | 810 | ||
| 808 | //save state of Right nav being open or closed | 811 | //save state of Right nav being open or closed |
| 809 | $('#rightNavDrawerIcon').on('click', function () { | 812 | $('#rightNavDrawerIcon').on('click', function () { |
| @@ -3,7 +3,8 @@ import { chat_metadata, eventSource, event_types, generateQuietPrompt, getCurren | |||
| 3 | import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; | 3 | import { openThirdPartyExtensionMenu, saveMetadataDebounced } from './extensions.js'; |
| 4 | import { SlashCommand } from './slash-commands/SlashCommand.js'; | 4 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 5 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | 5 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 6 | import { createThumbnail, flashHighlight, getBase64Async, stringFormat } from './utils.js'; | 6 | import { createThumbnail, flashHighlight, getBase64Async, stringFormat, debounce, setupScrollToTop } from './utils.js'; |
| 7 | import { debounce_timeout } from './constants.js'; | ||
| 7 | import { t } from './i18n.js'; | 8 | import { t } from './i18n.js'; |
| 8 | import { Popup } from './popup.js'; | 9 | import { Popup } from './popup.js'; |
| 9 | 10 | ||
| @@ -15,6 +16,11 @@ const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkY | |||
| 15 | const PNG_PIXEL_BLOB = new Blob([Uint8Array.from(atob(PNG_PIXEL), c => c.charCodeAt(0))], { type: 'image/png' }); | 16 | const PNG_PIXEL_BLOB = new Blob([Uint8Array.from(atob(PNG_PIXEL), c => c.charCodeAt(0))], { type: 'image/png' }); |
| 16 | const PLACEHOLDER_IMAGE = `url('data:image/png;base64,${PNG_PIXEL}')`; | 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 | * Storage for frontend-generated background thumbnails. | 25 | * Storage for frontend-generated background thumbnails. |
| 20 | * This is used to store thumbnails for backgrounds that cannot be generated on the server. | 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 | animation: false, | 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 | export function loadBackgroundSettings(settings) { | 101 | export function loadBackgroundSettings(settings) { |
| 49 | let backgroundSettings = settings.background; | 102 | let backgroundSettings = settings.background; |
| 50 | if (!backgroundSettings || !backgroundSettings.name || !backgroundSettings.url) { | 103 | if (!backgroundSettings || !backgroundSettings.name || !backgroundSettings.url) { |
| @@ -56,10 +109,21 @@ export function loadBackgroundSettings(settings) { | |||
| 56 | if (!Object.hasOwn(backgroundSettings, 'animation')) { | 109 | if (!Object.hasOwn(backgroundSettings, 'animation')) { |
| 57 | backgroundSettings.animation = false; | 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 | setBackground(backgroundSettings.name, backgroundSettings.url); | 122 | setBackground(backgroundSettings.name, backgroundSettings.url); |
| 60 | setFittingClass(backgroundSettings.fitting); | 123 | setFittingClass(backgroundSettings.fitting); |
| 61 | $('#background_fitting').val(backgroundSettings.fitting); | 124 | $('#background_fitting').val(backgroundSettings.fitting); |
| 62 | $('#background_thumbnails_animation').prop('checked', background_settings.animation); | 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 | async function forceSetBackground(backgroundInfo) { | 133 | async function forceSetBackground(backgroundInfo) { |
| 70 | saveBackgroundMetadata(backgroundInfo.url); | 134 | saveBackgroundMetadata(backgroundInfo.url); |
| 71 | setCustomBackground(); | 135 | $('#bg1').css('background-image', backgroundInfo.url); |
| 72 | 136 | ||
| 73 | const list = chat_metadata[LIST_METADATA_KEY] || []; | 137 | const list = chat_metadata[LIST_METADATA_KEY] || []; |
| 74 | const bg = backgroundInfo.path; | 138 | const bg = backgroundInfo.path; |
| 75 | list.push(bg); | 139 | list.push(bg); |
| 76 | chat_metadata[LIST_METADATA_KEY] = list; | 140 | chat_metadata[LIST_METADATA_KEY] = list; |
| 77 | saveMetadataDebounced(); | 141 | saveMetadataDebounced(); |
| 78 | await getChatBackgroundsList(); | 142 | renderChatBackgrounds(); |
| 79 | highlightNewBackground(bg); | 143 | highlightNewBackground(bg); |
| 80 | highlightLockedBackground(); | 144 | highlightLockedBackground(); |
| 81 | } | 145 | } |
| 82 | 146 | ||
| 83 | async function onChatChanged() { | 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 | |||
| 95 | async function getChatBackgroundsList() { | ||
| 96 | const list = chat_metadata[LIST_METADATA_KEY]; | ||
| 97 | const listEmpty = !Array.isArray(list) || list.length === 0; | ||
| 98 | 149 | ||
| 99 | $('#bg_custom_content').empty(); | 150 | $('#bg1').css('background-image', lockedUrl || background_settings.url); |
| 100 | $('#bg_chat_hint').toggle(listEmpty); | ||
| 101 | 151 | ||
| 102 | if (listEmpty) { | 152 | renderChatBackgrounds(); |
| 103 | return; | 153 | highlightLockedBackground(); |
| 104 | } | 154 | highlightSelectedBackground(); |
| 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 | function getBackgroundPath(fileUrl) { | 157 | function getBackgroundPath(fileUrl) { |
| @@ -115,59 +159,53 @@ function getBackgroundPath(fileUrl) { | |||
| 115 | } | 159 | } |
| 116 | 160 | ||
| 117 | function highlightLockedBackground() { | 161 | function highlightLockedBackground() { |
| 118 | $('.bg_example').removeClass('locked'); | 162 | $('.bg_example.locked-background').removeClass('locked-background'); |
| 119 | 163 | ||
| 120 | const lockedBackground = chat_metadata[BG_METADATA_KEY]; | 164 | const lockedBackgroundUrl = chat_metadata[BG_METADATA_KEY]; |
| 121 | 165 | ||
| 122 | if (!lockedBackground) { | 166 | if (lockedBackgroundUrl) { |
| 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 | * Locks the background for the current chat | 174 | * Locks the background for the current chat |
| 136 | * @param {Event} e Click event | 175 | * @param {Event|null} event |
| 137 | * @returns {string} Empty string | ||
| 138 | */ | 176 | */ |
| 139 | function onLockBackgroundClick(e) { | 177 | function onLockBackgroundClick(event = 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 | highlightLockedBackground(); | 189 | highlightLockedBackground(); |
| 154 | return ''; | ||
| 155 | } | 190 | } |
| 156 | 191 | ||
| 157 | /** | 192 | /** |
| 158 | * Locks the background for the current chat | 193 | * Unlocks the background for the current chat |
| 159 | * @param {Event} e Click event | 194 | * @param {Event|null} _event |
| 160 | * @returns {string} Empty string | ||
| 161 | */ | 195 | */ |
| 162 | function onUnlockBackgroundClick(e) { | 196 | function onUnlockBackgroundClick(_event = null) { |
| 163 | e?.stopPropagation(); | 197 | // Delete the lock from the chat's metadata. |
| 164 | removeBackgroundMetadata(); | 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 | highlightLockedBackground(); | 204 | highlightLockedBackground(); |
| 167 | return ''; | 205 | highlightSelectedBackground(); |
| 168 | } | 206 | } |
| 169 | 207 | ||
| 170 | function hasCustomBackground() { | 208 | function isChatBackgroundLocked() { |
| 171 | return chat_metadata[BG_METADATA_KEY]; | 209 | return chat_metadata[BG_METADATA_KEY]; |
| 172 | } | 210 | } |
| 173 | 211 | ||
| @@ -181,54 +219,22 @@ function removeBackgroundMetadata() { | |||
| 181 | saveMetadataDebounced(); | 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 | function onSelectBackgroundClick() { | 222 | function onSelectBackgroundClick() { |
| 200 | const isCustom = $(this).attr('custom') === 'true'; | 223 | const bgFile = $(this).attr('bgfile'); |
| 201 | const relativeBgImage = getUrlParameter(this); | 224 | const backgroundCssUrl = 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 | saveBackgroundMetadata(relativeBgImage); | 228 | saveBackgroundMetadata(backgroundCssUrl); |
| 211 | setCustomBackground(); | 229 | $('#bg1').css('background-image', backgroundCssUrl); |
| 212 | highlightLockedBackground(); | 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 | 235 | ||
| 218 | // Custom background is set. Do not override the layer below | 236 | // Update UI highlights to reflect the changes. |
| 219 | if (customBg !== 'none') { | 237 | highlightSelectedBackground(); |
| 220 | return; | ||
| 221 | } | ||
| 222 | |||
| 223 | const bgFile = $(this).attr('bgfile'); | ||
| 224 | const backgroundUrl = getBackgroundPath(bgFile); | ||
| 225 | |||
| 226 | // Fetching to browser memory to reduce flicker | ||
| 227 | fetch(backgroundUrl).then(() => { | ||
| 228 | setBackground(bgFile, relativeBgImage); | ||
| 229 | }).catch(() => { | ||
| 230 | console.log('Background could not be set: ' + backgroundUrl); | ||
| 231 | }); | ||
| 232 | } | 238 | } |
| 233 | 239 | ||
| 234 | async function onCopyToSystemBackgroundClick(e) { | 240 | async function onCopyToSystemBackgroundClick(e) { |
| @@ -257,7 +263,7 @@ async function onCopyToSystemBackgroundClick(e) { | |||
| 257 | const index = list.indexOf(bgNames.oldBg); | 263 | const index = list.indexOf(bgNames.oldBg); |
| 258 | list.splice(index, 1); | 264 | list.splice(index, 1); |
| 259 | saveMetadataDebounced(); | 265 | saveMetadataDebounced(); |
| 260 | await getChatBackgroundsList(); | 266 | renderChatBackgrounds(); |
| 261 | } | 267 | } |
| 262 | 268 | ||
| 263 | /** | 269 | /** |
| @@ -382,29 +388,32 @@ async function onDeleteBackgroundClick(e) { | |||
| 382 | list.splice(index, 1); | 388 | list.splice(index, 1); |
| 383 | } | 389 | } |
| 384 | 390 | ||
| 385 | const siblingSelector = '.bg_example:not(#form_bg_download)'; | 391 | if (bg === background_settings.name) { |
| 392 | const siblingSelector = '.bg_example'; | ||
| 386 | const nextBg = bgToDelete.next(siblingSelector); | 393 | const nextBg = bgToDelete.next(siblingSelector); |
| 387 | const prevBg = bgToDelete.prev(siblingSelector); | 394 | const prevBg = bgToDelete.prev(siblingSelector); |
| 388 | const anyBg = $(siblingSelector); | ||
| 389 | 395 | ||
| 390 | if (nextBg.length > 0) { | 396 | if (nextBg.length > 0) { |
| 391 | nextBg.trigger('click'); | 397 | nextBg.trigger('click'); |
| 392 | } else if (prevBg.length > 0) { | 398 | } else if (prevBg.length > 0) { |
| 393 | prevBg.trigger('click'); | 399 | prevBg.trigger('click'); |
| 394 | } else { | 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 | bgToDelete.remove(); | 408 | bgToDelete.remove(); |
| 399 | 409 | ||
| 400 | if (url === chat_metadata[BG_METADATA_KEY]) { | 410 | if (url === chat_metadata[BG_METADATA_KEY]) { |
| 401 | removeBackgroundMetadata(); | 411 | removeBackgroundMetadata(); |
| 402 | unsetCustomBackground(); | ||
| 403 | highlightLockedBackground(); | 412 | highlightLockedBackground(); |
| 404 | } | 413 | } |
| 405 | 414 | ||
| 406 | if (isCustom) { | 415 | if (isCustom) { |
| 407 | await getChatBackgroundsList(); | 416 | renderChatBackgrounds(); |
| 408 | saveMetadataDebounced(); | 417 | saveMetadataDebounced(); |
| 409 | } | 418 | } |
| 410 | } | 419 | } |
| @@ -445,6 +454,47 @@ async function autoBackgroundCommand() { | |||
| 445 | return ''; | 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 | export async function getBackgrounds() { | 498 | export async function getBackgrounds() { |
| 449 | const response = await fetch('/api/backgrounds/all', { | 499 | const response = await fetch('/api/backgrounds/all', { |
| 450 | method: 'POST', | 500 | method: 'POST', |
| @@ -454,12 +504,9 @@ export async function getBackgrounds() { | |||
| 454 | if (response.ok) { | 504 | if (response.ok) { |
| 455 | const { images, config } = await response.json(); | 505 | const { images, config } = await response.json(); |
| 456 | Object.assign(THUMBNAIL_CONFIG, config); | 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 | lazyLoadObserver = new IntersectionObserver((entries, observer) => { | 528 | lazyLoadObserver = new IntersectionObserver((entries, observer) => { |
| 482 | entries.forEach(entry => { | 529 | entries.forEach(entry => { |
| 483 | if (entry.target instanceof HTMLElement && entry.isIntersecting) { | 530 | if (entry.target instanceof HTMLElement && entry.isIntersecting) { |
| 484 | const target = entry.target; | 531 | const clipper = entry.target; |
| 485 | const bg = target.getAttribute('bgfile'); | 532 | const parentThumbnail = clipper.closest('.bg_example'); |
| 486 | const isCustom = target.getAttribute('custom') === 'true'; | 533 | |
| 534 | if (parentThumbnail) { | ||
| 535 | const bg = parentThumbnail.getAttribute('bgfile'); | ||
| 536 | const isCustom = parentThumbnail.getAttribute('custom') === 'true'; | ||
| 487 | resolveImageUrl(bg, isCustom) | 537 | resolveImageUrl(bg, isCustom) |
| 488 | .then(url => { target.style.backgroundImage = url; }) | 538 | .then(url => { clipper.style.backgroundImage = url; }) |
| 489 | .catch(() => { target.style.backgroundImage = PLACEHOLDER_IMAGE; }); | 539 | .catch(() => { clipper.style.backgroundImage = PLACEHOLDER_IMAGE; }); |
| 490 | target.classList.remove('lazy-load-background'); | 540 | } |
| 491 | observer.unobserve(target); | 541 | |
| 542 | clipper.classList.remove('lazy-load-background'); | ||
| 543 | observer.unobserve(clipper); | ||
| 492 | } | 544 | } |
| 493 | }); | 545 | }); |
| 494 | }, options); | 546 | }, options); |
| @@ -529,30 +581,11 @@ async function resolveImageUrl(bg, isCustom) { | |||
| 529 | return `url("${thumbnailUrl}")`; | 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 | async function setBackground(bg, url) { | 584 | async function setBackground(bg, url) { |
| 585 | // Only change the visual background if one is not locked for the current chat. | ||
| 586 | if (!isChatBackgroundLocked()) { | ||
| 555 | $('#bg1').css('background-image', url); | 587 | $('#bg1').css('background-image', url); |
| 588 | } | ||
| 556 | background_settings.name = bg; | 589 | background_settings.name = bg; |
| 557 | background_settings.url = url; | 590 | background_settings.url = url; |
| 558 | saveSettingsDebounced(); | 591 | saveSettingsDebounced(); |
| @@ -680,25 +713,39 @@ function highlightNewBackground(bg) { | |||
| 680 | * @param {string} fitting Fitting type | 713 | * @param {string} fitting Fitting type |
| 681 | */ | 714 | */ |
| 682 | function setFittingClass(fitting) { | 715 | function setFittingClass(fitting) { |
| 683 | const backgrounds = $('#bg1, #bg_custom'); | 716 | const backgrounds = $('#bg1'); |
| 684 | for (const option of ['cover', 'contain', 'stretch', 'center']) { | 717 | for (const option of ['cover', 'contain', 'stretch', 'center']) { |
| 685 | backgrounds.toggleClass(option, option === fitting); | 718 | backgrounds.toggleClass(option, option === fitting); |
| 686 | } | 719 | } |
| 687 | background_settings.fitting = fitting; | 720 | background_settings.fitting = fitting; |
| 688 | } | 721 | } |
| 689 | 722 | ||
| 690 | function onBackgroundFilterInput() { | 723 | function highlightSelectedBackground() { |
| 691 | const filterValue = String($(this).val()).toLowerCase(); | 724 | $('.bg_example.selected-background').removeClass('selected-background'); |
| 692 | $('#bg_menu_content > div').each(function () { | 725 | |
| 693 | const $bgContent = $(this); | 726 | // The "selected" highlight should always reflect the global background setting. |
| 694 | if ($bgContent.attr('title').toLowerCase().includes(filterValue)) { | 727 | const activeUrl = background_settings.url; |
| 695 | $bgContent.show(); | 728 | |
| 696 | } else { | 729 | if (activeUrl) { |
| 697 | $bgContent.hide(); | 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'); | ||
| 698 | } | 734 | } |
| 735 | } | ||
| 736 | |||
| 737 | function onBackgroundFilterInput() { | ||
| 738 | const filterValue = String($('#bg-filter').val()).toLowerCase(); | ||
| 739 | $('#bg_menu_content > .bg_example, #bg_custom_content > .bg_example').each(function () { | ||
| 740 | const $bg = $(this); | ||
| 741 | const title = $bg.attr('title') || ''; | ||
| 742 | const hasMatch = title.toLowerCase().includes(filterValue); | ||
| 743 | $bg.toggle(hasMatch); | ||
| 699 | }); | 744 | }); |
| 700 | } | 745 | } |
| 701 | 746 | ||
| 747 | const debouncedOnBackgroundFilterInput = debounce(onBackgroundFilterInput, debounce_timeout.standard); | ||
| 748 | |||
| 702 | export function initBackgrounds() { | 749 | export function initBackgrounds() { |
| 703 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); | 750 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); |
| 704 | eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground); | 751 | eventSource.on(event_types.FORCE_SET_BACKGROUND, forceSetBackground); |
| @@ -715,6 +762,11 @@ export function initBackgrounds() { | |||
| 715 | $context.addClass('mobile-menu-open'); | 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 | .off('click', '.jg-button').on('click', '.jg-button', function (e) { | 770 | .off('click', '.jg-button').on('click', '.jg-button', function (e) { |
| 719 | e.stopPropagation(); | 771 | e.stopPropagation(); |
| 720 | const action = $(this).data('action'); | 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 | $('#auto_background').on('click', autoBackgroundCommand); | 799 | $('#auto_background').on('click', autoBackgroundCommand); |
| 742 | $('#add_bg_button').on('change', onBackgroundUploadSelected); | 800 | $('#add_bg_button').on('change', onBackgroundUploadSelected); |
| 743 | $('#bg-filter').on('input', onBackgroundFilterInput); | 801 | $('#bg-filter').on('input', () => debouncedOnBackgroundFilterInput()); |
| 744 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 802 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 745 | name: 'lockbg', | 803 | name: 'lockbg', |
| 746 | callback: () => onLockBackgroundClick(new CustomEvent('click')), | 804 | callback: () => { |
| 805 | onLockBackgroundClick(); | ||
| 806 | return ''; | ||
| 807 | }, | ||
| 747 | aliases: ['bglock'], | 808 | aliases: ['bglock'], |
| 748 | helpString: 'Locks a background for the currently selected chat', | 809 | helpString: 'Locks a background for the currently selected chat', |
| 749 | })); | 810 | })); |
| 750 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 811 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 751 | name: 'unlockbg', | 812 | name: 'unlockbg', |
| 752 | callback: () => onUnlockBackgroundClick(new CustomEvent('click')), | 813 | callback: () => { |
| 814 | onUnlockBackgroundClick(); | ||
| 815 | return ''; | ||
| 816 | }, | ||
| 753 | aliases: ['bgunlock'], | 817 | aliases: ['bgunlock'], |
| 754 | helpString: 'Unlocks a background for the currently selected chat', | 818 | helpString: 'Unlocks a background for the currently selected chat', |
| 755 | })); | 819 | })); |
| @@ -774,4 +838,10 @@ export function initBackgrounds() { | |||
| 774 | await getBackgrounds(); | 838 | await getBackgrounds(); |
| 775 | await onChatChanged(); | 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 | return ''; | 750 | return ''; |
| 751 | } | 751 | } |
| 752 | 752 | ||
| 753 | const div = document.createElement('div'); | ||
| 754 | const html = converter.makeHtml(substituteParams(text)); | 753 | const html = converter.makeHtml(substituteParams(text)); |
| 755 | div.innerHTML = html; | 754 | const parsedDocument = new DOMParser().parseFromString(html, 'text/html'); |
| 756 | const styleElements = Array.from(div.querySelectorAll('style')); | 755 | const styleElements = Array.from(parsedDocument.querySelectorAll('style')); |
| 757 | return styleElements | 756 | return styleElements |
| 758 | .filter(s => s.textContent.trim().length > 0) | 757 | .filter(s => s.textContent.trim().length > 0) |
| 759 | .map(s => s.textContent.trim()) | 758 | .map(s => s.textContent.trim()) |
| @@ -51,6 +51,7 @@ export const inject_ids = { | |||
| 51 | DEPTH_PROMPT_INDEX: (index) => `DEPTH_PROMPT_${index}`, | 51 | DEPTH_PROMPT_INDEX: (index) => `DEPTH_PROMPT_${index}`, |
| 52 | CUSTOM_WI_DEPTH: 'customDepthWI', | 52 | CUSTOM_WI_DEPTH: 'customDepthWI', |
| 53 | CUSTOM_WI_DEPTH_ROLE: (depth, role) => `customDepthWI_${depth}_${role}`, | 53 | CUSTOM_WI_DEPTH_ROLE: (depth, role) => `customDepthWI_${depth}_${role}`, |
| 54 | CUSTOM_WI_OUTLET: (key) => `customWIOutlet_${key}`, | ||
| 54 | }; | 55 | }; |
| 55 | 56 | ||
| 56 | export const COMETAPI_IGNORE_PATTERNS = [ | 57 | export const COMETAPI_IGNORE_PATTERNS = [ |
| @@ -49,6 +49,7 @@ import EventSourceStream from './sse-stream.js'; | |||
| 49 | * @property {string} [custom_url] - Optional custom URL | 49 | * @property {string} [custom_url] - Optional custom URL |
| 50 | * @property {string} [reverse_proxy] - Optional reverse proxy URL | 50 | * @property {string} [reverse_proxy] - Optional reverse proxy URL |
| 51 | * @property {string} [proxy_password] - Optional proxy password | 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 | /** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */ | 55 | /** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */ |
| @@ -414,7 +415,7 @@ export class ChatCompletionService { | |||
| 414 | * @param {ChatCompletionPayload} custom | 415 | * @param {ChatCompletionPayload} custom |
| 415 | * @returns {ChatCompletionPayload} | 416 | * @returns {ChatCompletionPayload} |
| 416 | */ | 417 | */ |
| 417 | static createRequestData({ stream = false, messages, model, chat_completion_source, max_tokens, temperature, custom_url, reverse_proxy, proxy_password, ...props }) { | 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 | const payload = { | 419 | const payload = { |
| 419 | stream, | 420 | stream, |
| 420 | messages, | 421 | messages, |
| @@ -425,6 +426,7 @@ export class ChatCompletionService { | |||
| 425 | custom_url, | 426 | custom_url, |
| 426 | reverse_proxy, | 427 | reverse_proxy, |
| 427 | proxy_password, | 428 | proxy_password, |
| 429 | custom_prompt_post_processing, | ||
| 428 | use_makersuite_sysprompt: true, | 430 | use_makersuite_sysprompt: true, |
| 429 | claude_use_sysprompt: true, | 431 | claude_use_sysprompt: true, |
| 430 | ...props, | 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 | MESSAGE_FILE_EMBEDDED: 'message_file_embedded', | 12 | MESSAGE_FILE_EMBEDDED: 'message_file_embedded', |
| 13 | MESSAGE_REASONING_EDITED: 'message_reasoning_edited', | 13 | MESSAGE_REASONING_EDITED: 'message_reasoning_edited', |
| 14 | MESSAGE_REASONING_DELETED: 'message_reasoning_deleted', | 14 | MESSAGE_REASONING_DELETED: 'message_reasoning_deleted', |
| 15 | MESSAGE_SWIPE_DELETED: 'message_swipe_deleted', | ||
| 15 | MORE_MESSAGES_LOADED: 'more_messages_loaded', | 16 | MORE_MESSAGES_LOADED: 'more_messages_loaded', |
| 16 | IMPERSONATE_READY: 'impersonate_ready', | 17 | IMPERSONATE_READY: 'impersonate_ready', |
| 17 | CHAT_CHANGED: 'chat_id_changed', | 18 | CHAT_CHANGED: 'chat_id_changed', |
| @@ -36,6 +37,7 @@ export const event_types = { | |||
| 36 | OAI_PRESET_IMPORT_READY: 'oai_preset_import_ready', | 37 | OAI_PRESET_IMPORT_READY: 'oai_preset_import_ready', |
| 37 | WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated', | 38 | WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated', |
| 38 | WORLDINFO_UPDATED: 'worldinfo_updated', | 39 | WORLDINFO_UPDATED: 'worldinfo_updated', |
| 40 | CHARACTER_EDITOR_OPENED: 'character_editor_opened', | ||
| 39 | CHARACTER_EDITED: 'character_edited', | 41 | CHARACTER_EDITED: 'character_edited', |
| 40 | CHARACTER_PAGE_LOADED: 'character_page_loaded', | 42 | CHARACTER_PAGE_LOADED: 'character_page_loaded', |
| 41 | CHARACTER_GROUP_OVERLAY_STATE_CHANGE_BEFORE: 'character_group_overlay_state_change_before', | 43 | CHARACTER_GROUP_OVERLAY_STATE_CHANGE_BEFORE: 'character_group_overlay_state_change_before', |
| @@ -85,6 +87,8 @@ export const event_types = { | |||
| 85 | SECRET_EDITED: 'secret_edited', | 87 | SECRET_EDITED: 'secret_edited', |
| 86 | PRESET_CHANGED: 'preset_changed', | 88 | PRESET_CHANGED: 'preset_changed', |
| 87 | PRESET_DELETED: 'preset_deleted', | 89 | PRESET_DELETED: 'preset_deleted', |
| 90 | PRESET_RENAMED: 'preset_renamed', | ||
| 91 | PRESET_RENAMED_BEFORE: 'preset_renamed_before', | ||
| 88 | MAIN_API_CHANGED: 'main_api_changed', | 92 | MAIN_API_CHANGED: 'main_api_changed', |
| 89 | WORLDINFO_ENTRIES_LOADED: 'worldinfo_entries_loaded', | 93 | WORLDINFO_ENTRIES_LOADED: 'worldinfo_entries_loaded', |
| 90 | }; | 94 | }; |
| @@ -190,7 +190,10 @@ export const extension_settings = { | |||
| 190 | regex: [], | 190 | regex: [], |
| 191 | /** @type {import('./extensions/regex/index.js').RegexPreset[]} */ | 191 | /** @type {import('./extensions/regex/index.js').RegexPreset[]} */ |
| 192 | regex_presets: [], | 192 | regex_presets: [], |
| 193 | /** @type {string[]} */ | ||
| 193 | character_allowed_regex: [], | 194 | character_allowed_regex: [], |
| 195 | /** @type {Record<string, string[]>} */ | ||
| 196 | preset_allowed_regex: {}, | ||
| 194 | tts: {}, | 197 | tts: {}, |
| 195 | sd: { | 198 | sd: { |
| 196 | prompts: {}, | 199 | prompts: {}, |
| @@ -10,7 +10,7 @@ import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js'; | |||
| 10 | import { executeSlashCommandsWithOptions } from '../../slash-commands.js'; | 10 | import { executeSlashCommandsWithOptions } from '../../slash-commands.js'; |
| 11 | import { accountStorage } from '../../util/AccountStorage.js'; | 11 | import { accountStorage } from '../../util/AccountStorage.js'; |
| 12 | import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js'; | 12 | import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js'; |
| 13 | import { t } from '../../i18n.js'; | 13 | import { t, translate } from '../../i18n.js'; |
| 14 | export { MODULE_NAME }; | 14 | export { MODULE_NAME }; |
| 15 | 15 | ||
| 16 | const MODULE_NAME = 'assets'; | 16 | const MODULE_NAME = 'assets'; |
| @@ -60,6 +60,36 @@ const KNOWN_TYPES = { | |||
| 60 | 'blip': t`Blip sounds`, | 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 | async function downloadAssetsList(url) { | 93 | async function downloadAssetsList(url) { |
| 64 | updateCurrentAssets().then(async function () { | 94 | updateCurrentAssets().then(async function () { |
| 65 | fetch(url, { cache: 'no-cache' }) | 95 | fetch(url, { cache: 'no-cache' }) |
| @@ -88,7 +118,8 @@ async function downloadAssetsList(url) { | |||
| 88 | $('#assets_type_select').append($('<option />', { value: '', text: t`All` })); | 118 | $('#assets_type_select').append($('<option />', { value: '', text: t`All` })); |
| 89 | 119 | ||
| 90 | for (const type of assetTypes) { | 120 | for (const type of assetTypes) { |
| 91 | const option = $('<option />', { value: type, text: t([KNOWN_TYPES[type] || type]) }); | 121 | const text = translate(KNOWN_TYPES[type] || type); |
| 122 | const option = $('<option />', { value: type, text: text }); | ||
| 92 | $('#assets_type_select').append(option); | 123 | $('#assets_type_select').append(option); |
| 93 | } | 124 | } |
| 94 | 125 | ||
| @@ -184,10 +215,11 @@ async function downloadAssetsList(url) { | |||
| 184 | const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`; | 215 | const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`; |
| 185 | const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple'; | 216 | const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple'; |
| 186 | const toolTag = assetType === 'extension' && asset['tool']; | 217 | const toolTag = assetType === 'extension' && asset['tool']; |
| 218 | const author = url && assetType === 'extension' ? getAuthorFromUrl(url) : EMPTY_AUTHOR; | ||
| 187 | 219 | ||
| 188 | const assetBlock = $('<i></i>') | 220 | const assetBlock = $('<i></i>') |
| 189 | .append(element) | 221 | .append(element) |
| 190 | .append(`<div class="flex-container flexFlowColumn flexNoGap"> | 222 | .append(`<div class="flex-container flexFlowColumn flexNoGap wide100p overflowHidden"> |
| 191 | <span class="asset-name flex-container alignitemscenter"> | 223 | <span class="asset-name flex-container alignitemscenter"> |
| 192 | <b>${displayName}</b> | 224 | <b>${displayName}</b> |
| 193 | <a class="asset_preview" href="${url}" target="_blank" title="${title}"> | 225 | <a class="asset_preview" href="${url}" target="_blank" title="${title}"> |
| @@ -195,6 +227,8 @@ async function downloadAssetsList(url) { | |||
| 195 | </a>` + | 227 | </a>` + |
| 196 | (toolTag ? '<span class="tag" title="' + t`Adds a function tool` + '"><i class="fa-solid fa-sm fa-wrench"></i> ' + | 228 | (toolTag ? '<span class="tag" title="' + t`Adds a function tool` + '"><i class="fa-solid fa-sm fa-wrench"></i> ' + |
| 197 | t`Tool` + '</span>' : '') + | 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 | `</span> | 232 | `</span> |
| 199 | <small class="asset-description"> | 233 | <small class="asset-description"> |
| 200 | ${description} | 234 | ${description} |
| @@ -40,9 +40,10 @@ | |||
| 40 | flex-direction: row; | 40 | flex-direction: row; |
| 41 | align-items: center; | 41 | align-items: center; |
| 42 | justify-content: left; | 42 | justify-content: left; |
| 43 | padding: 5px; | 43 | padding: 10px 5px; |
| 44 | font-style: normal; | 44 | font-style: normal; |
| 45 | gap: 5px; | 45 | gap: 5px; |
| 46 | border-bottom: 1px solid var(--SmartThemeBorderColor); | ||
| 46 | } | 47 | } |
| 47 | 48 | ||
| 48 | .assets-list-div i span:first-of-type { | 49 | .assets-list-div i span:first-of-type { |
| @@ -173,3 +174,27 @@ | |||
| 173 | opacity: 0.9; | 174 | opacity: 0.9; |
| 174 | margin-left: 2px; | 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 | import { ensureImageFormatSupported, getBase64Async, getFileExtension, isTrueBoolean, saveBase64AsFile } from '../../utils.js'; | 1 | import { ensureImageFormatSupported, getBase64Async, getFileExtension, isTrueBoolean, saveBase64AsFile } from '../../utils.js'; |
| 2 | import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js'; | 2 | import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 3 | import { appendMediaToMessage, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js'; | 3 | import { appendMediaToMessage, chat_metadata, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js'; |
| 4 | import { getMessageTimeStamp } from '../../RossAscends-mods.js'; | 4 | import { getMessageTimeStamp } from '../../RossAscends-mods.js'; |
| 5 | import { SECRET_KEYS, secret_state } from '../../secrets.js'; | 5 | import { SECRET_KEYS, secret_state } from '../../secrets.js'; |
| 6 | import { getMultimodalCaption } from '../shared.js'; | 6 | import { getMultimodalCaption } from '../shared.js'; |
| @@ -174,6 +174,7 @@ async function sendCaptionedMessage(caption, image) { | |||
| 174 | inline_image: !!extension_settings.caption.show_in_chat, | 174 | inline_image: !!extension_settings.caption.show_in_chat, |
| 175 | }, | 175 | }, |
| 176 | }; | 176 | }; |
| 177 | chat_metadata['tainted'] = true; | ||
| 177 | context.chat.push(message); | 178 | context.chat.push(message); |
| 178 | const messageId = context.chat.length - 1; | 179 | const messageId = context.chat.length - 1; |
| 179 | await eventSource.emit(event_types.MESSAGE_SENT, messageId); | 180 | await eventSource.emit(event_types.MESSAGE_SENT, messageId); |
| @@ -549,6 +550,8 @@ jQuery(async function () { | |||
| 549 | await processEndpoint('pollinations', '/api/backends/chat-completions/multimodal-models/pollinations'); | 550 | await processEndpoint('pollinations', '/api/backends/chat-completions/multimodal-models/pollinations'); |
| 550 | await processEndpoint('nanogpt', '/api/backends/chat-completions/multimodal-models/nanogpt'); | 551 | await processEndpoint('nanogpt', '/api/backends/chat-completions/multimodal-models/nanogpt'); |
| 551 | await processEndpoint('electronhub', '/api/backends/chat-completions/multimodal-models/electronhub'); | 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 | await addSettings(); | 557 | await addSettings(); |
| @@ -626,6 +629,11 @@ jQuery(async function () { | |||
| 626 | extension_settings.caption.ollama_custom_model = String($('#caption_ollama_custom_model').val()).trim(); | 629 | extension_settings.caption.ollama_custom_model = String($('#caption_ollama_custom_model').val()).trim(); |
| 627 | saveSettingsDebounced(); | 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 | const onMessageEvent = async (index) => { | 638 | const onMessageEvent = async (index) => { |
| 631 | if (!extension_settings.caption.auto_mode) { | 639 | if (!extension_settings.caption.auto_mode) { |
| @@ -9,7 +9,7 @@ | |||
| 9 | <label for="caption_source" data-i18n="Source">Source</label> | 9 | <label for="caption_source" data-i18n="Source">Source</label> |
| 10 | <select id="caption_source" class="text_pole"> | 10 | <select id="caption_source" class="text_pole"> |
| 11 | <option value="local" data-i18n="Local">Local</option> | 11 | <option value="local" data-i18n="Local">Local</option> |
| 12 | <option value="multimodal" data-i18n="Multimodal (OpenAI / Anthropic / llama / Google)">Multimodal (OpenAI / Anthropic / llama / Google)</option> | 12 | <option value="multimodal" data-i18n="Multimodal">Multimodal</option> |
| 13 | <option value="extras" data-i18n="Extras">Extras (deprecated)</option> | 13 | <option value="extras" data-i18n="Extras">Extras (deprecated)</option> |
| 14 | <option value="horde" data-i18n="Horde">Horde</option> | 14 | <option value="horde" data-i18n="Horde">Horde</option> |
| 15 | </select> | 15 | </select> |
| @@ -18,7 +18,7 @@ | |||
| 18 | <label for="caption_multimodal_api" data-i18n="API">API</label> | 18 | <label for="caption_multimodal_api" data-i18n="API">API</label> |
| 19 | <select id="caption_multimodal_api" class="flex1 text_pole"> | 19 | <select id="caption_multimodal_api" class="flex1 text_pole"> |
| 20 | <option value="aimlapi">AI/ML API</option> | 20 | <option value="aimlapi">AI/ML API</option> |
| 21 | <option value="anthropic">Anthropic</option> | 21 | <option value="anthropic">Claude</option> |
| 22 | <option value="cohere">Cohere</option> | 22 | <option value="cohere">Cohere</option> |
| 23 | <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option> | 23 | <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option> |
| 24 | <option value="electronhub">Electron Hub</option> | 24 | <option value="electronhub">Electron Hub</option> |
| @@ -40,23 +40,17 @@ | |||
| 40 | </select> | 40 | </select> |
| 41 | </div> | 41 | </div> |
| 42 | <div class="flex1 flex-container flexFlowColumn flexNoGap"> | 42 | <div class="flex1 flex-container flexFlowColumn flexNoGap"> |
| 43 | <label for="caption_multimodal_model" data-i18n="Model">Model</label> | 43 | <label for="caption_multimodal_model" class="flex-container justifySpaceBetween"> |
| 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 | <select id="caption_multimodal_model" class="flex1 text_pole"> | 49 | <select id="caption_multimodal_model" class="flex1 text_pole"> |
| 45 | <!-- AI/ML API, OpenRouter, Pollinations, NanoGPT are added externally by JavaScript --> | 50 | <!-- AI/ML API, OpenRouter, Pollinations, NanoGPT, Mistral, xAI are added externally by JavaScript --> |
| 46 | <option data-type="cohere" value="c4ai-aya-vision-8b">c4ai-aya-vision-8b</option> | 51 | <option data-type="cohere" value="c4ai-aya-vision-8b">c4ai-aya-vision-8b</option> |
| 47 | <option data-type="cohere" value="c4ai-aya-vision-32b">c4ai-aya-vision-32b</option> | 52 | <option data-type="cohere" value="c4ai-aya-vision-32b">c4ai-aya-vision-32b</option> |
| 48 | <option data-type="cohere" value="command-a-vision-07-2025">command-a-vision-07-2025</option> | 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 | <option data-type="moonshot" value="moonshot-v1-8k-vision-preview">moonshot-v1-8k-vision-preview</option> | 54 | <option data-type="moonshot" value="moonshot-v1-8k-vision-preview">moonshot-v1-8k-vision-preview</option> |
| 61 | <option data-type="moonshot" value="moonshot-v1-32k-vision-preview">moonshot-v1-32k-vision-preview</option> | 55 | <option data-type="moonshot" value="moonshot-v1-32k-vision-preview">moonshot-v1-32k-vision-preview</option> |
| 62 | <option data-type="moonshot" value="moonshot-v1-128k-vision-preview">moonshot-v1-128k-vision-preview</option> | 56 | <option data-type="moonshot" value="moonshot-v1-128k-vision-preview">moonshot-v1-128k-vision-preview</option> |
| @@ -87,6 +81,10 @@ | |||
| 87 | <option data-type="openai" value="o4-mini-2025-04-16">o4-mini-2025-04-16</option> | 81 | <option data-type="openai" value="o4-mini-2025-04-16">o4-mini-2025-04-16</option> |
| 88 | <option data-type="openai" value="gpt-4.5-preview">gpt-4.5-preview</option> | 82 | <option data-type="openai" value="gpt-4.5-preview">gpt-4.5-preview</option> |
| 89 | <option data-type="openai" value="gpt-4.5-preview-2025-02-27">gpt-4.5-preview-2025-02-27</option> | 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 | <option data-type="anthropic" value="claude-opus-4-1">claude-opus-4-1</option> | 88 | <option data-type="anthropic" value="claude-opus-4-1">claude-opus-4-1</option> |
| 91 | <option data-type="anthropic" value="claude-opus-4-1-20250805">claude-opus-4-1-20250805</option> | 89 | <option data-type="anthropic" value="claude-opus-4-1-20250805">claude-opus-4-1-20250805</option> |
| 92 | <option data-type="anthropic" value="claude-opus-4-0">claude-opus-4-0</option> | 90 | <option data-type="anthropic" value="claude-opus-4-0">claude-opus-4-0</option> |
| @@ -106,49 +104,33 @@ | |||
| 106 | <option data-type="google" value="gemini-2.5-pro-preview-06-05">gemini-2.5-pro-preview-06-05</option> | 104 | <option data-type="google" value="gemini-2.5-pro-preview-06-05">gemini-2.5-pro-preview-06-05</option> |
| 107 | <option data-type="google" value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option> | 105 | <option data-type="google" value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option> |
| 108 | <option data-type="google" value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option> | 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 | <option data-type="google" value="gemini-2.5-flash">gemini-2.5-flash</option> | 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 | <option data-type="google" value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option> | 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 | <option data-type="google" value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option> | 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 | <option data-type="google" value="gemini-2.5-flash-lite-preview-06-17">gemini-2.5-flash-lite-preview-06-17</option> | 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 | <option data-type="google" value="gemini-2.5-flash-image-preview">gemini-2.5-flash-image-preview</option> | 114 | <option data-type="google" value="gemini-2.5-flash-image-preview">gemini-2.5-flash-image-preview</option> |
| 116 | <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> | 115 | <option data-type="google" value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05 → 2.5-exp-03-25</option> |
| 117 | <option data-type="google" value="gemini-2.0-pro-exp">gemini-2.0-pro-exp → 2.5-pro-exp-03-25</option> | 116 | <option data-type="google" value="gemini-2.0-pro-exp">gemini-2.0-pro-exp → 2.5-exp-03-25</option> |
| 118 | <option data-type="google" value="gemini-exp-1206">gemini-exp-1206 → 2.5-pro-exp-03-25</option> | 117 | <option data-type="google" value="gemini-exp-1206">gemini-exp-1206 → 2.5-exp-03-25</option> |
| 119 | <option data-type="google" value="gemini-2.0-flash-001">gemini-2.0-flash-001</option> | 118 | <option data-type="google" value="gemini-2.0-flash-001">gemini-2.0-flash-001</option> |
| 120 | <option data-type="google" value="gemini-2.0-flash-exp-image-generation">gemini-2.0-flash-exp-image-generation</option> | 119 | <option data-type="google" value="gemini-2.0-flash-exp-image-generation">gemini-2.0-flash-exp-image-generation</option> |
| 121 | <option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option> | 120 | <option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option> |
| 122 | <option data-type="google" value="gemini-2.0-flash">gemini-2.0-flash</option> | 121 | <option data-type="google" value="gemini-2.0-flash">gemini-2.0-flash</option> |
| 123 | <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-04-17</option> | 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-5-20</option> |
| 124 | <option data-type="google" value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219 → 2.5-flash-preview-04-17</option> | 123 | <option data-type="google" value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219 → 2.5-flash-preview-05-20</option> |
| 125 | <option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp → 2.5-flash-preview-04-17</option> | 124 | <option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp → 2.5-flash-preview-05-20</option> |
| 126 | <option data-type="google" value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option> | 125 | <option data-type="google" value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option> |
| 127 | <option data-type="google" value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option> | 126 | <option data-type="google" value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option> |
| 128 | <option data-type="google" value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option> | 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 | <option data-type="google" value="learnlm-2.0-flash-experimental">learnlm-2.0-flash-experimental</option> | 128 | <option data-type="google" value="learnlm-2.0-flash-experimental">learnlm-2.0-flash-experimental</option> |
| 142 | <option data-type="google" value="learnlm-1.5-pro-experimental">learnlm-1.5-pro-experimental</option> | 129 | <option data-type="google" value="gemini-robotics-er-1.5-preview">gemini-robotics-er-1.5-preview</option> |
| 143 | <option data-type="vertexai" value="gemini-2.5-pro">gemini-2.5-pro</option> | 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 | <option data-type="vertexai" value="gemini-2.5-flash">gemini-2.5-flash</option> | 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 | <option data-type="vertexai" value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option> | 132 | <option data-type="vertexai" value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option> |
| 151 | <option data-type="vertexai" value="gemini-2.5-flash-lite-preview-06-17">gemini-2.5-flash-lite-preview-06-17</option> | 133 | <option data-type="vertexai" value="gemini-2.5-flash-image">gemini-2.5-flash-image</option> |
| 152 | <option data-type="vertexai" value="gemini-2.5-flash-image-preview">gemini-2.5-flash-image-preview</option> | 134 | <option data-type="vertexai" value="gemini-2.5-flash-image-preview">gemini-2.5-flash-image-preview</option> |
| 153 | <option data-type="vertexai" value="gemini-2.0-flash-001">gemini-2.0-flash-001</option> | 135 | <option data-type="vertexai" value="gemini-2.0-flash-001">gemini-2.0-flash-001</option> |
| 154 | <option data-type="vertexai" value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option> | 136 | <option data-type="vertexai" value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option> |
| @@ -174,9 +156,6 @@ | |||
| 174 | <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option> | 156 | <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option> |
| 175 | <option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option> | 157 | <option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option> |
| 176 | <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option> | 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 | </select> | 159 | </select> |
| 181 | </div> | 160 | </div> |
| 182 | <div data-type="ollama"> | 161 | <div data-type="ollama"> |
| @@ -1,6 +1,6 @@ | |||
| 1 | import { DOMPurify, Fuse } from '../../../lib.js'; | 1 | import { DOMPurify, Fuse } from '../../../lib.js'; |
| 2 | 2 | ||
| 3 | import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js'; | 3 | import { event_types, eventSource, main_api, online_status, saveSettingsDebounced } from '../../../script.js'; |
| 4 | import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js'; | 4 | import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 5 | import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js'; | 5 | import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js'; |
| 6 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; | 6 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| @@ -11,7 +11,7 @@ import { SlashCommandDebugController } from '../../slash-commands/SlashCommandDe | |||
| 11 | import { enumTypes, SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; | 11 | import { enumTypes, SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 12 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | 12 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| 13 | import { SlashCommandScope } from '../../slash-commands/SlashCommandScope.js'; | 13 | import { SlashCommandScope } from '../../slash-commands/SlashCommandScope.js'; |
| 14 | import { collapseSpaces, getUniqueName, isFalseBoolean, uuidv4 } from '../../utils.js'; | 14 | import { collapseSpaces, getUniqueName, isFalseBoolean, uuidv4, waitUntilCondition } from '../../utils.js'; |
| 15 | import { t } from '../../i18n.js'; | 15 | import { t } from '../../i18n.js'; |
| 16 | import { getSecretLabelById } from '../../secrets.js'; | 16 | import { getSecretLabelById } from '../../secrets.js'; |
| 17 | 17 | ||
| @@ -167,6 +167,12 @@ const profilesProvider = () => [ | |||
| 167 | * @property {string} [stop-strings] Custom Stopping Strings | 167 | * @property {string} [stop-strings] Custom Stopping Strings |
| 168 | * @property {string} [start-reply-with] Start Reply With | 168 | * @property {string} [start-reply-with] Start Reply With |
| 169 | * @property {string} [reasoning-template] Reasoning Template | 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 | * @property {string[]} [exclude] Commands to exclude | 176 | * @property {string[]} [exclude] Commands to exclude |
| 171 | */ | 177 | */ |
| 172 | 178 | ||
| @@ -684,6 +690,13 @@ async function renderDetailsContent(detailsContent) { | |||
| 684 | defaultValue: 'true', | 690 | defaultValue: 'true', |
| 685 | enumList: commonEnumProviders.boolean('trueFalse')(), | 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 | callback: async (args, value) => { | 701 | callback: async (args, value) => { |
| 689 | if (!value || typeof value !== 'string') { | 702 | if (!value || typeof value !== 'string') { |
| @@ -715,6 +728,13 @@ async function renderDetailsContent(detailsContent) { | |||
| 715 | 728 | ||
| 716 | if (shouldAwait) { | 729 | if (shouldAwait) { |
| 717 | await awaitPromise; | 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 | return profile.name; | 740 | return profile.name; |
| @@ -993,7 +993,7 @@ function doPopout(e) { | |||
| 993 | originalElement.empty(); | 993 | originalElement.empty(); |
| 994 | originalElement.html('<div class="flex-container alignitemscenter justifyCenter wide100p"><small>Currently popped out</small></div>'); | 994 | originalElement.html('<div class="flex-container alignitemscenter justifyCenter wide100p"><small>Currently popped out</small></div>'); |
| 995 | newElement.append(controlBarHtml).append(originalHTMLClone); | 995 | newElement.append(controlBarHtml).append(originalHTMLClone); |
| 996 | $('body').append(newElement); | 996 | $('#movingDivs').append(newElement); |
| 997 | newElement.transition({ opacity: 1, duration: animation_duration, easing: animation_easing }); | 997 | newElement.transition({ opacity: 1, duration: animation_duration, easing: animation_easing }); |
| 998 | $('#summaryExtensionDrawerContents').addClass('scrollableInnerFull'); | 998 | $('#summaryExtensionDrawerContents').addClass('scrollableInnerFull'); |
| 999 | setMemoryContext(prevSummaryBoxContents, false); //paste prev summary box contents into popout box | 999 | setMemoryContext(prevSummaryBoxContents, false); //paste prev summary box contents into popout box |
| @@ -11,6 +11,7 @@ | |||
| 11 | <button | 11 | <button |
| 12 | id="regex_debugger_save_order" | 12 | id="regex_debugger_save_order" |
| 13 | class="menu_button menu_button_icon interactable" | 13 | class="menu_button menu_button_icon interactable" |
| 14 | data-i18n="[title]ext_regex_debugger_save_order_help" | ||
| 14 | title="Save current rule order" | 15 | title="Save current rule order" |
| 15 | tabindex="0" | 16 | tabindex="0" |
| 16 | > | 17 | > |
| @@ -53,6 +54,7 @@ | |||
| 53 | <button | 54 | <button |
| 54 | id="regex_debugger_run_test" | 55 | id="regex_debugger_run_test" |
| 55 | class="menu_button menu_button_icon interactable" | 56 | class="menu_button menu_button_icon interactable" |
| 57 | data-i18n="[title]ext_regex_debugger_run_test_help" | ||
| 56 | title="Run the test pipeline" | 58 | title="Run the test pipeline" |
| 57 | tabindex="0" | 59 | tabindex="0" |
| 58 | > | 60 | > |
| @@ -115,6 +117,7 @@ | |||
| 115 | <div | 117 | <div |
| 116 | id="regex_debugger_expand_steps" | 118 | id="regex_debugger_expand_steps" |
| 117 | class="menu_button menu_button_icon" | 119 | class="menu_button menu_button_icon" |
| 120 | data-i18n="[title]Expand view" | ||
| 118 | title="Expand view" | 121 | title="Expand view" |
| 119 | > | 122 | > |
| 120 | <i class="fa-solid fa-expand"></i> | 123 | <i class="fa-solid fa-expand"></i> |
| @@ -132,6 +135,7 @@ | |||
| 132 | <div | 135 | <div |
| 133 | id="regex_debugger_expand_final" | 136 | id="regex_debugger_expand_final" |
| 134 | class="menu_button menu_button_icon" | 137 | class="menu_button menu_button_icon" |
| 138 | data-i18n="[title]Expand view" | ||
| 135 | title="Expand view" | 139 | title="Expand view" |
| 136 | > | 140 | > |
| 137 | <i class="fa-solid fa-expand"></i> | 141 | <i class="fa-solid fa-expand"></i> |
| @@ -158,7 +162,7 @@ | |||
| 158 | <code class="rule-regex"></code> | 162 | <code class="rule-regex"></code> |
| 159 | <small class="rule-scope"></small> | 163 | <small class="rule-scope"></small> |
| 160 | </div> | 164 | </div> |
| 161 | <div class="menu_button menu_button_icon edit_rule" title="Edit Rule"> | 165 | <div class="menu_button menu_button_icon edit_rule" data-i18n="[title]Edit Rule" title="Edit Rule"> |
| 162 | <i class="fa-solid fa-pencil"></i> | 166 | <i class="fa-solid fa-pencil"></i> |
| 163 | </div> | 167 | </div> |
| 164 | </li> | 168 | </li> |
| @@ -16,6 +16,10 @@ | |||
| 16 | <i class="fa-solid fa-address-card"></i> | 16 | <i class="fa-solid fa-address-card"></i> |
| 17 | <small data-i18n="ext_regex_new_scoped_script">+ Scoped</small> | 17 | <small data-i18n="ext_regex_new_scoped_script">+ Scoped</small> |
| 18 | </div> | 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 | <div id="import_regex" class="menu_button menu_button_icon"> | 23 | <div id="import_regex" class="menu_button menu_button_icon"> |
| 20 | <i class="fa-solid fa-file-import"></i> | 24 | <i class="fa-solid fa-file-import"></i> |
| 21 | <small data-i18n="ext_regex_import_script">Import</small> | 25 | <small data-i18n="ext_regex_import_script">Import</small> |
| @@ -31,7 +35,8 @@ | |||
| 31 | <small data-i18n="ext_regex_debugger">Debugger</small> | 35 | <small data-i18n="ext_regex_debugger">Debugger</small> |
| 32 | </div> | 36 | </div> |
| 33 | </div> | 37 | </div> |
| 34 | <div class="regex_bulk_operations flex-container justifyCenter"> | 38 | <hr class="regex_bulk_operations_hr" /> |
| 39 | <div class="regex_bulk_operations flex-container"> | ||
| 35 | <div id="bulk_select_all_toggle" class="menu_button menu_button_icon" title="Toggle Select All"> | 40 | <div id="bulk_select_all_toggle" class="menu_button menu_button_icon" title="Toggle Select All"> |
| 36 | <i class="fa-solid fa-check-double"></i> | 41 | <i class="fa-solid fa-check-double"></i> |
| 37 | </div> | 42 | </div> |
| @@ -43,6 +48,18 @@ | |||
| 43 | <i class="fa-solid fa-toggle-off"></i> | 48 | <i class="fa-solid fa-toggle-off"></i> |
| 44 | <small data-i18n="Disable">Disable</small> | 49 | <small data-i18n="Disable">Disable</small> |
| 45 | </div> | 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 | <div id="bulk_export_regex" class="menu_button menu_button_icon"> | 63 | <div id="bulk_export_regex" class="menu_button menu_button_icon"> |
| 47 | <i class="fa-solid fa-file-export"></i> | 64 | <i class="fa-solid fa-file-export"></i> |
| 48 | <small data-i18n="Export">Export</small> | 65 | <small data-i18n="Export">Export</small> |
| @@ -69,7 +86,7 @@ | |||
| 69 | </div> | 86 | </div> |
| 70 | </div> | 87 | </div> |
| 71 | <hr /> | 88 | <hr /> |
| 72 | <div id="global_scripts_block" class="padding5"> | 89 | <div id="global_scripts_block"> |
| 73 | <div> | 90 | <div> |
| 74 | <strong data-i18n="ext_regex_global_scripts">Global Scripts</strong> | 91 | <strong data-i18n="ext_regex_global_scripts">Global Scripts</strong> |
| 75 | </div> | 92 | </div> |
| @@ -79,7 +96,7 @@ | |||
| 79 | <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> | 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 | </div> | 97 | </div> |
| 81 | <hr /> | 98 | <hr /> |
| 82 | <div id="scoped_scripts_block" class="padding5"> | 99 | <div id="scoped_scripts_block"> |
| 83 | <div class="flex-container alignItemsBaseline"> | 100 | <div class="flex-container alignItemsBaseline"> |
| 84 | <strong class="flex1" data-i18n="ext_regex_scoped_scripts">Scoped Scripts</strong> | 101 | <strong class="flex1" data-i18n="ext_regex_scoped_scripts">Scoped Scripts</strong> |
| 85 | <label id="toggle_scoped_regex" class="checkbox flex-container" for="regex_scoped_toggle"> | 102 | <label id="toggle_scoped_regex" class="checkbox flex-container" for="regex_scoped_toggle"> |
| @@ -93,6 +110,21 @@ | |||
| 93 | </small> | 110 | </small> |
| 94 | <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> | 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 | </div> | 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 | </div> | 128 | </div> |
| 97 | </div> | 129 | </div> |
| 98 | </div> | 130 | </div> |
| @@ -1,16 +1,228 @@ | |||
| 1 | import { characters, substituteParams, substituteParamsExtended, this_chid } from '../../../script.js'; | 1 | import { characters, saveSettingsDebounced, substituteParams, substituteParamsExtended, this_chid } from '../../../script.js'; |
| 2 | import { extension_settings } from '../../extensions.js'; | 2 | import { extension_settings, writeExtensionField } from '../../extensions.js'; |
| 3 | import { getPresetManager } from '../../preset-manager.js'; | ||
| 3 | import { regexFromString } from '../../utils.js'; | 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 | * @enum {number} Where the regex script should be applied | 222 | * @enum {number} Where the regex script should be applied |
| 223 | * @readonly | ||
| 12 | */ | 224 | */ |
| 13 | const regex_placement = { | 225 | export const regex_placement = { |
| 14 | /** | 226 | /** |
| 15 | * @deprecated MD Display is deprecated. Do not use. | 227 | * @deprecated MD Display is deprecated. Do not use. |
| 16 | */ | 228 | */ |
| @@ -23,6 +235,10 @@ const regex_placement = { | |||
| 23 | REASONING: 6, | 235 | REASONING: 6, |
| 24 | }; | 236 | }; |
| 25 | 237 | ||
| 238 | /** | ||
| 239 | * @enum {number} How to substitute parameters in the find regex | ||
| 240 | * @readonly | ||
| 241 | */ | ||
| 26 | export const substitute_find_regex = { | 242 | export const substitute_find_regex = { |
| 27 | NONE: 0, | 243 | NONE: 0, |
| 28 | RAW: 1, | 244 | RAW: 1, |
| @@ -51,22 +267,6 @@ function sanitizeRegexMacro(x) { | |||
| 51 | }) : x; | 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 | * Parent function to fetch a regexed version of a raw string | 271 | * Parent function to fetch a regexed version of a raw string |
| 72 | * @param {string} rawString The raw string to be regexed | 272 | * @param {string} rawString The raw string to be regexed |
| @@ -75,7 +275,7 @@ function getScopedRegex() { | |||
| 75 | * @returns {string} The regexed string | 275 | * @returns {string} The regexed string |
| 76 | * @typedef {{characterOverride?: string, isMarkdown?: boolean, isPrompt?: boolean, isEdit?: boolean, depth?: number }} RegexParams The parameters to use for the regex script | 276 | * @typedef {{characterOverride?: string, isMarkdown?: boolean, isPrompt?: boolean, isEdit?: boolean, depth?: number }} RegexParams The parameters to use for the regex script |
| 77 | */ | 277 | */ |
| 78 | function getRegexedString(rawString, placement, { characterOverride, isMarkdown, isPrompt, isEdit, depth } = {}) { | 278 | export function getRegexedString(rawString, placement, { characterOverride, isMarkdown, isPrompt, isEdit, depth } = {}) { |
| 79 | // WTF have you passed me? | 279 | // WTF have you passed me? |
| 80 | if (typeof rawString !== 'string') { | 280 | if (typeof rawString !== 'string') { |
| 81 | console.warn('getRegexedString: rawString is not a string. Returning empty string.'); | 281 | console.warn('getRegexedString: rawString is not a string. Returning empty string.'); |
| @@ -87,7 +287,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown, | |||
| 87 | return finalString; | 287 | return finalString; |
| 88 | } | 288 | } |
| 89 | 289 | ||
| 90 | const allRegex = [...(extension_settings.regex ?? []), ...(getScopedRegex() ?? [])]; | 290 | const allRegex = getRegexScripts({ allowedOnly: true }); |
| 91 | allRegex.forEach((script) => { | 291 | allRegex.forEach((script) => { |
| 92 | if ( | 292 | if ( |
| 93 | // Script applies to Markdown and input is Markdown | 293 | // Script applies to Markdown and input is Markdown |
| @@ -126,13 +326,13 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown, | |||
| 126 | 326 | ||
| 127 | /** | 327 | /** |
| 128 | * Runs the provided regex script on the given string | 328 | * Runs the provided regex script on the given string |
| 129 | * @param {import('./index.js').RegexScript} regexScript The regex script to run | 329 | * @param {RegexScript} regexScript The regex script to run |
| 130 | * @param {string} rawString The string to run the regex script on | 330 | * @param {string} rawString The string to run the regex script on |
| 131 | * @param {RegexScriptParams} params The parameters to use for the regex script | 331 | * @param {RegexScriptParams} params The parameters to use for the regex script |
| 132 | * @returns {string} The new string | 332 | * @returns {string} The new string |
| 133 | * @typedef {{characterOverride?: string}} RegexScriptParams The parameters to use for the regex script | 333 | * @typedef {{characterOverride?: string}} RegexScriptParams The parameters to use for the regex script |
| 134 | */ | 334 | */ |
| 135 | function runRegexScript(regexScript, rawString, { characterOverride } = {}) { | 335 | export function runRegexScript(regexScript, rawString, { characterOverride } = {}) { |
| 136 | let newString = rawString; | 336 | let newString = rawString; |
| 137 | if (!regexScript || !!(regexScript.disabled) || !regexScript?.findRegex || !rawString) { | 337 | if (!regexScript || !!(regexScript.disabled) || !regexScript?.findRegex || !rawString) { |
| 138 | return newString; | 338 | return newString; |
| @@ -15,5 +15,11 @@ | |||
| 15 | Scoped Scripts | 15 | Scoped Scripts |
| 16 | </span> | 16 | </span> |
| 17 | </label> | 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 | </div> | 24 | </div> |
| 19 | </div> | 25 | </div> |
| @@ -1,5 +1,5 @@ | |||
| 1 | import { characters, eventSource, event_types, getCurrentChatId, messageFormatting, reloadCurrentChat, saveSettingsDebounced, this_chid } from '../../../script.js'; | 1 | import { characters, eventSource, event_types, getCurrentChatId, messageFormatting, reloadCurrentChat, saveSettingsDebounced, this_chid } from '../../../script.js'; |
| 2 | import { extension_settings, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js'; | 2 | import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 3 | import { selected_group } from '../../group-chats.js'; | 3 | import { selected_group } from '../../group-chats.js'; |
| 4 | import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; | 4 | import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; |
| 5 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; | 5 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| @@ -7,10 +7,14 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from ' | |||
| 7 | import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; | 7 | import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 8 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; | 8 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 9 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | 9 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| 10 | import { download, equalsIgnoreCaseAndAccents, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4, escapeHtml } from '../../utils.js'; | 10 | import { download, equalsIgnoreCaseAndAccents, escapeHtml, getFileText, getSortableDelay, isFalseBoolean, isTrueBoolean, regexFromString, setInfoBlock, uuidv4 } from '../../utils.js'; |
| 11 | import { regex_placement, runRegexScript, substitute_find_regex } from './engine.js'; | 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 | import { t } from '../../i18n.js'; | 12 | import { t } from '../../i18n.js'; |
| 13 | import { accountStorage } from '../../util/AccountStorage.js'; | 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 | const sanitizeFileName = name => name.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '_').toLowerCase(); | 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 | * @property {boolean} isSelected - Whether the preset is currently selected | 34 | * @property {boolean} isSelected - Whether the preset is currently selected |
| 31 | * @property {RegexPresetItem[]} global - The list of global preset items | 35 | * @property {RegexPresetItem[]} global - The list of global preset items |
| 32 | * @property {RegexPresetItem[]} scoped - The list of scoped preset items | 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 | * @typedef {object} RegexPresetState | 41 | * @typedef {object} RegexPresetState |
| 37 | * @property {string[]} global - List of enabled global regex script IDs | 42 | * @property {string[]} global - List of enabled global regex script IDs |
| 38 | * @property {string[]} scoped - List of enabled scoped regex script IDs | 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 | class RegexPresetManager { | 47 | class RegexPresetManager { |
| @@ -65,12 +71,14 @@ class RegexPresetManager { | |||
| 65 | * @returns {RegexPresetState} The current state object | 71 | * @returns {RegexPresetState} The current state object |
| 66 | */ | 72 | */ |
| 67 | captureCurrentState() { | 73 | captureCurrentState() { |
| 68 | const globalScripts = this.regexListToPresetItems(extension_settings.regex) || []; | 74 | const globalScripts = this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.GLOBAL)); |
| 69 | const scopedScripts = this.regexListToPresetItems(characters[this_chid]?.data?.extensions?.regex_scripts) || []; | 75 | const scopedScripts = this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.SCOPED)); |
| 76 | const presetScripts = this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.PRESET)); | ||
| 70 | 77 | ||
| 71 | return { | 78 | return { |
| 72 | global: globalScripts.map(item => item.id).sort(), | 79 | global: globalScripts.map(item => item.id).sort(), |
| 73 | scoped: scopedScripts.map(item => item.id).sort(), | 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 | const global2 = state2.global || []; | 95 | const global2 = state2.global || []; |
| 88 | const scoped1 = state1.scoped || []; | 96 | const scoped1 = state1.scoped || []; |
| 89 | const scoped2 = state2.scoped || []; | 97 | const scoped2 = state2.scoped || []; |
| 98 | const preset1 = state1.preset || []; | ||
| 99 | const preset2 = state2.preset || []; | ||
| 90 | 100 | ||
| 91 | if (global1.length !== global2.length || scoped1.length !== scoped2.length) { | 101 | if (global1.length !== global2.length || scoped1.length !== scoped2.length) { |
| 92 | return true; | 102 | return true; |
| 93 | } | 103 | } |
| 94 | 104 | ||
| 95 | return !global1.every(id => global2.includes(id)) || | 105 | return !global1.every(id => global2.includes(id)) || |
| 96 | !scoped1.every(id => scoped2.includes(id)); | 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 | return; | 373 | return; |
| 363 | } | 374 | } |
| 364 | 375 | ||
| 365 | // Apply to both global and scoped lists | 376 | // Apply preset to all lists |
| 366 | await this.applyPresetList({ | 377 | for (const scriptType of Object.values(SCRIPT_TYPES)) { |
| 367 | presetList: preset.global, | ||
| 368 | targetList: extension_settings.regex, | ||
| 369 | saveFunction: () => saveSettingsDebounced(), | ||
| 370 | }); | ||
| 371 | await this.applyPresetList({ | 378 | await this.applyPresetList({ |
| 372 | presetList: preset.scoped, | 379 | presetList: { |
| 373 | targetList: characters[this_chid]?.data?.extensions?.regex_scripts, | 380 | [SCRIPT_TYPES.GLOBAL]: preset.global, |
| 374 | saveFunction: (scripts) => writeExtensionField(this_chid, 'regex_scripts', scripts), | 381 | [SCRIPT_TYPES.SCOPED]: preset.scoped, |
| 382 | [SCRIPT_TYPES.PRESET]: preset.preset, | ||
| 383 | }[scriptType], | ||
| 384 | targetList: getScriptsByType(scriptType), | ||
| 385 | saveFunction: scripts => saveScriptsByType(scripts, scriptType), | ||
| 375 | }); | 386 | }); |
| 387 | } | ||
| 376 | 388 | ||
| 377 | // Render the changes to the UI | 389 | // Render the changes to the UI |
| 378 | await loadRegexScripts(); | 390 | await loadRegexScripts(); |
| @@ -418,8 +430,9 @@ class RegexPresetManager { | |||
| 418 | id: id, | 430 | id: id, |
| 419 | name: name, | 431 | name: name, |
| 420 | isSelected: false, | 432 | isSelected: false, |
| 421 | global: this.regexListToPresetItems(extension_settings.regex), | 433 | global: this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.GLOBAL)), |
| 422 | scoped: this.regexListToPresetItems(characters[this_chid]?.data?.extensions?.regex_scripts), | 434 | scoped: this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.SCOPED)), |
| 435 | preset: this.regexListToPresetItems(getScriptsByType(SCRIPT_TYPES.PRESET)), | ||
| 423 | }; | 436 | }; |
| 424 | 437 | ||
| 425 | if (isUpdate) { | 438 | if (isUpdate) { |
| @@ -465,15 +478,6 @@ class RegexPresetManager { | |||
| 465 | const presetManager = new RegexPresetManager(); | 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 | * Toggle the icon for the "select all" checkbox in the regex settings. | 481 | * Toggle the icon for the "select all" checkbox in the regex settings. |
| 478 | * - Use `fa-check-double` when the checkbox is unchecked (indicating all scripts are not selected). | 482 | * - Use `fa-check-double` when the checkbox is unchecked (indicating all scripts are not selected). |
| 479 | * - Use `fa-minus` when the checkbox is checked (indicating all scripts are selected). | 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 | * Saves a regex script to the extension settings or character data. | 505 | * Saves a regex script to the extension settings or character data. |
| 490 | * @param {import('../../char-data.js').RegexScriptData} regexScript | 506 | * @param {import('../../char-data.js').RegexScriptData} regexScript |
| 491 | * @param {number} existingScriptIndex Index of the existing script | 507 | * @param {number} existingScriptIndex Index of the existing script |
| 492 | * @param {boolean} isScoped Is the script scoped to a character? | 508 | * @param {SCRIPT_TYPES} scriptType Type of the script |
| 509 | * @param {boolean} [saveSettings=true] Whether to save the settings immediately | ||
| 493 | * @returns {Promise<void>} | 510 | * @returns {Promise<void>} |
| 494 | */ | 511 | */ |
| 495 | async function saveRegexScript(regexScript, existingScriptIndex, isScoped) { | 512 | async function saveRegexScript(regexScript, existingScriptIndex, scriptType, saveSettings = true) { |
| 496 | // If not editing | 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 | // Assign a UUID if it doesn't exist | 516 | // Assign a UUID if it doesn't exist |
| 500 | if (!regexScript.id) { | 517 | if (!regexScript.id) { |
| @@ -523,23 +540,26 @@ async function saveRegexScript(regexScript, existingScriptIndex, isScoped) { | |||
| 523 | array.push(regexScript); | 540 | array.push(regexScript); |
| 524 | } | 541 | } |
| 525 | 542 | ||
| 526 | if (isScoped) { | 543 | if (scriptType === SCRIPT_TYPES.SCOPED) { |
| 527 | await writeExtensionField(this_chid, 'regex_scripts', array); | 544 | await saveScriptsByType(array, SCRIPT_TYPES.SCOPED); |
| 528 | 545 | allowScopedScripts(characters?.[this_chid]); | |
| 529 | // Add the character to the allowed list | ||
| 530 | if (!extension_settings.character_allowed_regex.includes(characters[this_chid].avatar)) { | ||
| 531 | extension_settings.character_allowed_regex.push(characters[this_chid].avatar); | ||
| 532 | } | 546 | } |
| 547 | |||
| 548 | if (scriptType === SCRIPT_TYPES.PRESET) { | ||
| 549 | await saveScriptsByType(array, SCRIPT_TYPES.PRESET); | ||
| 550 | allowPresetScripts(getCurrentPresetAPI(), getCurrentPresetName()); | ||
| 533 | } | 551 | } |
| 534 | 552 | ||
| 553 | if (saveSettings) { | ||
| 535 | saveSettingsDebounced(); | 554 | saveSettingsDebounced(); |
| 536 | await loadRegexScripts(); | 555 | await loadRegexScripts(); |
| 537 | 556 | ||
| 538 | // Reload the current chat to undo previous markdown | 557 | // Reload the current chat to undo previous markdown |
| 539 | const currentChatId = getCurrentChatId(); | 558 | const currentChatId = getCurrentChatId(); |
| 540 | if (currentChatId !== undefined && currentChatId !== null) { | 559 | if (currentChatId) { |
| 541 | await reloadCurrentChat(); | 560 | await reloadCurrentChat(); |
| 542 | } | 561 | } |
| 562 | } | ||
| 543 | 563 | ||
| 544 | const debuggerPopup = $('#regex_debugger_popup'); | 564 | const debuggerPopup = $('#regex_debugger_popup'); |
| 545 | if (debuggerPopup.length) { | 565 | if (debuggerPopup.length) { |
| @@ -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 | const existingScriptIndex = array.findIndex((script) => script.id === id); | 580 | const existingScriptIndex = array.findIndex(script => script.id === id); |
| 554 | if (existingScriptIndex !== -1) { | 581 | if (existingScriptIndex !== -1) { |
| 555 | array.splice(existingScriptIndex, 1); | 582 | array.splice(existingScriptIndex, 1); |
| 556 | 583 | ||
| 557 | if (isScoped) { | 584 | switch (scriptType) { |
| 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; | ||
| 559 | } | 596 | } |
| 560 | 597 | if (saveSettings) { | |
| 561 | saveSettingsDebounced(); | 598 | saveSettingsDebounced(); |
| 562 | await loadRegexScripts(); | 599 | await loadRegexScripts(); |
| 563 | } | 600 | } |
| 564 | } | 601 | } |
| 602 | } | ||
| 603 | |||
| 604 | /** | ||
| 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); | ||
| 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); | ||
| 625 | } | ||
| 565 | 626 | ||
| 566 | async function loadRegexScripts() { | 627 | async function loadRegexScripts() { |
| 567 | $('#saved_regex_scripts').empty(); | 628 | $('#saved_regex_scripts').empty(); |
| 568 | $('#saved_scoped_scripts').empty(); | 629 | $('#saved_scoped_scripts').empty(); |
| 630 | $('#saved_preset_scripts').empty(); | ||
| 569 | setToggleAllIcon(false); | 631 | setToggleAllIcon(false); |
| 570 | 632 | ||
| 571 | const scriptTemplate = $(await renderExtensionTemplateAsync('regex', 'scriptTemplate')); | 633 | const scriptTemplate = $(await renderExtensionTemplateAsync('regex', 'scriptTemplate')); |
| @@ -574,20 +636,20 @@ async function loadRegexScripts() { | |||
| 574 | * Renders a script to the UI. | 636 | * Renders a script to the UI. |
| 575 | * @param {string} container Container to render the script to | 637 | * @param {string} container Container to render the script to |
| 576 | * @param {import('../../char-data.js').RegexScriptData} script Script data | 638 | * @param {import('../../char-data.js').RegexScriptData} script Script data |
| 577 | * @param {boolean} isScoped Script is scoped to a character | 639 | * @param {SCRIPT_TYPES} scriptType Type of the script |
| 578 | * @param {number} index Index of the script in the array | 640 | * @param {number} index Index of the script in the array |
| 579 | */ | 641 | */ |
| 580 | function renderScript(container, script, isScoped, index) { | 642 | function renderScript(container, script, scriptType, index) { |
| 581 | // Have to clone here | 643 | // Have to clone here |
| 582 | const scriptHtml = scriptTemplate.clone(); | 644 | const scriptHtml = scriptTemplate.clone(); |
| 583 | const save = () => saveRegexScript(script, index, isScoped); | 645 | const save = () => saveRegexScript(script, index, scriptType); |
| 584 | 646 | ||
| 585 | if (!script.id) { | 647 | if (!script.id) { |
| 586 | script.id = uuidv4(); | 648 | script.id = uuidv4(); |
| 587 | } | 649 | } |
| 588 | 650 | ||
| 589 | scriptHtml.attr('id', script.id); | 651 | scriptHtml.attr('id', script.id); |
| 590 | scriptHtml.find('.regex_script_name').text(script.scriptName); | 652 | scriptHtml.find('.regex_script_name').text(script.scriptName).attr('title', script.scriptName); |
| 591 | scriptHtml.find('.disable_regex').prop('checked', script.disabled ?? false) | 653 | scriptHtml.find('.disable_regex').prop('checked', script.disabled ?? false) |
| 592 | .on('input', async function () { | 654 | .on('input', async function () { |
| 593 | script.disabled = !!$(this).prop('checked'); | 655 | script.disabled = !!$(this).prop('checked'); |
| @@ -600,7 +662,7 @@ async function loadRegexScripts() { | |||
| 600 | scriptHtml.find('.disable_regex').prop('checked', false).trigger('input'); | 662 | scriptHtml.find('.disable_regex').prop('checked', false).trigger('input'); |
| 601 | }); | 663 | }); |
| 602 | scriptHtml.find('.edit_existing_regex').on('click', async function () { | 664 | scriptHtml.find('.edit_existing_regex').on('click', async function () { |
| 603 | await onRegexEditorOpenClick(scriptHtml.attr('id'), isScoped); | 665 | await onRegexEditorOpenClick(scriptHtml.attr('id'), scriptType); |
| 604 | }); | 666 | }); |
| 605 | scriptHtml.find('.move_to_global').on('click', async function () { | 667 | scriptHtml.find('.move_to_global').on('click', async function () { |
| 606 | const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to global?`, POPUP_TYPE.CONFIRM); | 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 | if (!confirm) { | 670 | if (!confirm) { |
| 609 | return; | 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 | scriptHtml.find('.move_to_scoped').on('click', async function () { | 675 | scriptHtml.find('.move_to_scoped').on('click', async function () { |
| 616 | if (this_chid === undefined) { | 676 | if (this_chid === undefined) { |
| 617 | toastr.error(t`No character selected.`); | 677 | toastr.error(t`No character selected.`); |
| 618 | return; | 678 | return; |
| 619 | } | 679 | } |
| 620 | |||
| 621 | if (selected_group) { | 680 | if (selected_group) { |
| 622 | toastr.error(t`Cannot edit scoped scripts in group chats.`); | 681 | toastr.error(t`Cannot edit scoped scripts in group chats.`); |
| 623 | return; | 682 | return; |
| 624 | } | 683 | } |
| 625 | |||
| 626 | const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to scoped?`, POPUP_TYPE.CONFIRM); | 684 | const confirm = await callGenericPopup(t`Are you sure you want to move this regex script to scoped?`, POPUP_TYPE.CONFIRM); |
| 627 | |||
| 628 | if (!confirm) { | 685 | if (!confirm) { |
| 629 | return; | 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 | scriptHtml.find('.export_regex').on('click', async function () { | 700 | scriptHtml.find('.export_regex').on('click', async function () { |
| 636 | const fileName = `regex-${sanitizeFileName(script.scriptName)}.json`; | 701 | const fileName = `regex-${sanitizeFileName(script.scriptName)}.json`; |
| @@ -639,39 +704,65 @@ async function loadRegexScripts() { | |||
| 639 | }); | 704 | }); |
| 640 | scriptHtml.find('.delete_regex').on('click', async function () { | 705 | scriptHtml.find('.delete_regex').on('click', async function () { |
| 641 | const confirm = await callGenericPopup(t`Are you sure you want to delete this regex script?`, POPUP_TYPE.CONFIRM); | 706 | const confirm = await callGenericPopup(t`Are you sure you want to delete this regex script?`, POPUP_TYPE.CONFIRM); |
| 642 | |||
| 643 | if (!confirm) { | 707 | if (!confirm) { |
| 644 | return; | 708 | return; |
| 645 | } | 709 | } |
| 646 | 710 | await deleteRegexScript(script.id, scriptType); | |
| 647 | await deleteRegexScript({ id: script.id, isScoped }); | ||
| 648 | await reloadCurrentChat(); | 711 | await reloadCurrentChat(); |
| 649 | }); | 712 | }); |
| 650 | scriptHtml.find('.regex_bulk_checkbox').on('change', function () { | 713 | scriptHtml.find('.regex_bulk_checkbox').on('change', function () { |
| 714 | setMoveButtonsVisibility(); | ||
| 651 | const checkboxes = $('#regex_container .regex_bulk_checkbox'); | 715 | const checkboxes = $('#regex_container .regex_bulk_checkbox'); |
| 652 | const allAreChecked = checkboxes.length === checkboxes.filter(':checked').length; | 716 | const allAreChecked = checkboxes.length === checkboxes.filter(':checked').length; |
| 653 | setToggleAllIcon(allAreChecked); | 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 | $(container).append(scriptHtml); | 744 | $(container).append(scriptHtml); |
| 657 | } | 745 | } |
| 658 | 746 | ||
| 659 | extension_settings?.regex?.forEach((script, index) => renderScript('#saved_regex_scripts', script, false, index)); | 747 | getScriptsByType(SCRIPT_TYPES.GLOBAL).forEach((script, index) => renderScript('#saved_regex_scripts', script, SCRIPT_TYPES.GLOBAL, index)); |
| 660 | characters[this_chid]?.data?.extensions?.regex_scripts?.forEach((script, index) => renderScript('#saved_scoped_scripts', script, true, index)); | 748 | getScriptsByType(SCRIPT_TYPES.SCOPED).forEach((script, index) => renderScript('#saved_scoped_scripts', script, SCRIPT_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 | $('#regex_scoped_toggle').prop('checked', isAllowed); | 752 | $('#regex_preset_toggle').prop('checked', isPresetScriptsAllowed(getCurrentPresetAPI(), getCurrentPresetName())); |
| 753 | |||
| 754 | setMoveButtonsVisibility(); | ||
| 664 | } | 755 | } |
| 665 | 756 | ||
| 666 | /** | 757 | /** |
| 667 | * Opens the regex editor. | 758 | * Opens the regex editor. |
| 668 | * @param {string|boolean} existingId Existing ID | 759 | * @param {string|boolean} existingId Existing ID |
| 669 | * @param {boolean} isScoped Is the script scoped to a character? | 760 | * @param {SCRIPT_TYPES} scriptType Type of the script |
| 670 | * @returns {Promise<void>} | 761 | * @returns {Promise<void>} |
| 671 | */ | 762 | */ |
| 672 | async function onRegexEditorOpenClick(existingId, isScoped) { | 763 | async function onRegexEditorOpenClick(existingId, scriptType) { |
| 673 | const editorHtml = $(await renderExtensionTemplateAsync('regex', 'editor')); | 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 | // If an ID exists, fill in all the values | 767 | // If an ID exists, fill in all the values |
| 677 | let existingScriptIndex = -1; | 768 | let existingScriptIndex = -1; |
| @@ -776,7 +867,7 @@ async function onRegexEditorOpenClick(existingId, isScoped) { | |||
| 776 | maxDepth: parseInt(String(editorHtml.find('input[name="max_depth"]').val())), | 867 | maxDepth: parseInt(String(editorHtml.find('input[name="max_depth"]').val())), |
| 777 | }; | 868 | }; |
| 778 | 869 | ||
| 779 | saveRegexScript(newRegexScript, existingScriptIndex, isScoped); | 870 | saveRegexScript(newRegexScript, existingScriptIndex, scriptType); |
| 780 | } | 871 | } |
| 781 | } | 872 | } |
| 782 | 873 | ||
| @@ -969,28 +1060,35 @@ function populateDebuggerRuleList(container) { | |||
| 969 | 1060 | ||
| 970 | const allScripts = getRegexScripts(); | 1061 | const allScripts = getRegexScripts(); |
| 971 | if (!allScripts || allScripts.length === 0) { | 1062 | if (!allScripts || allScripts.length === 0) { |
| 972 | rulesContainer.append('<div class="regex-debugger-no-rules">No regex rules found.</div>'); | 1063 | rulesContainer.append('<div class="regex-debugger-no-rules">' + t`No regex rules found.` + '</div>'); |
| 973 | return; | 1064 | return; |
| 974 | } | 1065 | } |
| 975 | 1066 | ||
| 976 | const globalScriptIds = new Set((extension_settings.regex ?? []).map(s => s.id)); | 1067 | const globalScriptIds = new Set(getScriptsByType(SCRIPT_TYPES.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 | const globalScripts = []; | 1070 | const globalScripts = []; |
| 978 | const scopedScripts = []; | 1071 | const scopedScripts = []; |
| 1072 | const presetScripts = []; | ||
| 979 | 1073 | ||
| 980 | allScripts.forEach(script => { | 1074 | allScripts.forEach(script => { |
| 981 | const scriptCopy = structuredClone(script); // Use structuredClone for deep copy | 1075 | const scriptCopy = structuredClone(script); // Use structuredClone for deep copy |
| 982 | if (globalScriptIds.has(script.id)) { | 1076 | if (globalScriptIds.has(script.id)) { |
| 983 | // @ts-ignore | 1077 | // @ts-ignore |
| 984 | scriptCopy.isScoped = false; | 1078 | scriptCopy.type = SCRIPT_TYPES.GLOBAL; |
| 985 | globalScripts.push(scriptCopy); | 1079 | globalScripts.push(scriptCopy); |
| 986 | } else { | 1080 | } else if (scopedScriptIds.has(script.id)) { |
| 987 | // @ts-ignore | 1081 | // @ts-ignore |
| 988 | scriptCopy.isScoped = true; | 1082 | scriptCopy.type = SCRIPT_TYPES.SCOPED; |
| 989 | scopedScripts.push(scriptCopy); | 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 | container.data('allScripts', [...globalScripts, ...scopedScripts]); | 1091 | container.data('allScripts', [...globalScripts, ...scopedScripts, ...presetScripts]); |
| 994 | 1092 | ||
| 995 | const renderRule = (script) => { | 1093 | const renderRule = (script) => { |
| 996 | if (!script.id) script.id = uuidv4(); | 1094 | if (!script.id) script.id = uuidv4(); |
| @@ -1002,10 +1100,18 @@ function populateDebuggerRuleList(container) { | |||
| 1002 | ruleElement.find('.rule-name').text(script.scriptName); | 1100 | ruleElement.find('.rule-name').text(script.scriptName); |
| 1003 | ruleElement.find('.rule-regex').text(script.findRegex); | 1101 | ruleElement.find('.rule-regex').text(script.findRegex); |
| 1004 | // @ts-ignore | 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 | ruleElement.find('.rule-enabled').prop('checked', !script.disabled); | 1112 | ruleElement.find('.rule-enabled').prop('checked', !script.disabled); |
| 1007 | // @ts-ignore | 1113 | // @ts-ignore |
| 1008 | ruleElement.find('.edit_rule').on('click', () => onRegexEditorOpenClick(script.id, script.isScoped)); | 1114 | ruleElement.find('.edit_rule').on('click', () => onRegexEditorOpenClick(script.id, script.type)); |
| 1009 | 1115 | ||
| 1010 | ruleElement.on('click', function (event) { | 1116 | ruleElement.on('click', function (event) { |
| 1011 | if ($(event.target).is('input, .menu_button, .menu_button i')) { | 1117 | if ($(event.target).is('input, .menu_button, .menu_button i')) { |
| @@ -1035,18 +1141,25 @@ function populateDebuggerRuleList(container) { | |||
| 1035 | }; | 1141 | }; |
| 1036 | 1142 | ||
| 1037 | if (globalScripts.length > 0) { | 1143 | if (globalScripts.length > 0) { |
| 1038 | rulesContainer.append('<div class="list-header regex-debugger-list-header">Global Rules</div>'); | 1144 | rulesContainer.append('<div class="list-header regex-debugger-list-header">' + t`Global Rules` + '</div>'); |
| 1039 | const globalList = $('<ul id="regex_debugger_rules_global" class="sortable-list"></ul>'); | 1145 | const globalList = $('<ul id="regex_debugger_rules_global" class="sortable-list"></ul>'); |
| 1040 | globalScripts.forEach(script => globalList.append(renderRule(script))); | 1146 | globalScripts.forEach(script => globalList.append(renderRule(script))); |
| 1041 | rulesContainer.append(globalList); | 1147 | rulesContainer.append(globalList); |
| 1042 | } | 1148 | } |
| 1043 | 1149 | ||
| 1044 | if (scopedScripts.length > 0) { | 1150 | if (scopedScripts.length > 0) { |
| 1045 | rulesContainer.append('<div class="list-header regex-debugger-list-header">Scoped Rules</div>'); | 1151 | rulesContainer.append('<div class="list-header regex-debugger-list-header">' + t`Scoped Rules` + '</div>'); |
| 1046 | const scopedList = $('<ul id="regex_debugger_rules_scoped" class="sortable-list"></ul>'); | 1152 | const scopedList = $('<ul id="regex_debugger_rules_scoped" class="sortable-list"></ul>'); |
| 1047 | scopedScripts.forEach(script => scopedList.append(renderRule(script))); | 1153 | scopedScripts.forEach(script => scopedList.append(renderRule(script))); |
| 1048 | rulesContainer.append(scopedList); | 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 | debuggerHtml.find('#regex_debugger_rules_global').sortable({ delay: getSortableDelay() }).disableSelection(); | 1178 | debuggerHtml.find('#regex_debugger_rules_global').sortable({ delay: getSortableDelay() }).disableSelection(); |
| 1066 | // @ts-ignore | 1179 | // @ts-ignore |
| 1067 | debuggerHtml.find('#regex_debugger_rules_scoped').sortable({ delay: getSortableDelay() }).disableSelection(); | 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 | debuggerHtml.find('#regex_debugger_run_test').on('click', function () { | 1184 | debuggerHtml.find('#regex_debugger_run_test').on('click', function () { |
| 1070 | const allScripts = debuggerHtml.data('allScripts'); | 1185 | const allScripts = debuggerHtml.data('allScripts'); |
| 1071 | const orderedRuleIds = [ | 1186 | const orderedRuleIds = [ |
| 1072 | ...$('#regex_debugger_rules_global').find('li.regex-debugger-rule').map((i, el) => $(el).data('id')).get(), | 1187 | ...$('#regex_debugger_rules_global').find('li.regex-debugger-rule').map((i, el) => $(el).data('id')).get(), |
| 1073 | ...$('#regex_debugger_rules_scoped').find('li.regex-debugger-rule').map((i, el) => $(el).data('id')).get(), | 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 | const rawInput = String($('#regex_debugger_raw_input').val()); | 1192 | const rawInput = String($('#regex_debugger_raw_input').val()); |
| @@ -1106,9 +1222,9 @@ async function onRegexDebuggerOpenClick() { | |||
| 1106 | // Set the ID on the TOP-LEVEL element that is being appended. | 1222 | // Set the ID on the TOP-LEVEL element that is being appended. |
| 1107 | stepElement.find('>:first-child').attr('id', `step-result-${script.id}`); | 1223 | stepElement.find('>:first-child').attr('id', `step-result-${script.id}`); |
| 1108 | const stepHeader = stepElement.find('.step-header'); | 1224 | const stepHeader = stepElement.find('.step-header'); |
| 1109 | stepHeader.find('strong').text(`After: ${script.scriptName}`); | 1225 | stepHeader.find('strong').text(t`After:` + ` ${script.scriptName}`); |
| 1110 | 1226 | ||
| 1111 | const metricsHtml = `<span class="step-metrics">Captured: ${result.charsCaptured}, Added: +${result.charsAdded}, Removed: -${result.charsRemoved}</span>`; | 1227 | const metricsHtml = '<span class="step-metrics">' + t`Captured:` + ` ${result.charsCaptured}, ` + t`Added:` + ` +${result.charsAdded}, ` + t`Removed:` + ` -${result.charsRemoved}</span>`; |
| 1112 | stepHeader.append(metricsHtml); | 1228 | stepHeader.append(metricsHtml); |
| 1113 | 1229 | ||
| 1114 | if (displayMode === 'highlight') { | 1230 | if (displayMode === 'highlight') { |
| @@ -1128,7 +1244,7 @@ async function onRegexDebuggerOpenClick() { | |||
| 1128 | 1244 | ||
| 1129 | const summaryHtml = ` | 1245 | const summaryHtml = ` |
| 1130 | <div id="regex_debugger_final_summary" class="regex-debugger-summary"> | 1246 | <div id="regex_debugger_final_summary" class="regex-debugger-summary"> |
| 1131 | <strong>Total Captured:</strong> ${totalCharsCaptured} | <strong>Total Added:</strong> +${totalCharsAdded} | <strong>Total Removed:</strong> -${totalCharsRemoved} | 1247 | <strong>` + t`Total Captured:` + `</strong> ${totalCharsCaptured} | <strong>` + t`Total Added:` + `</strong> +${totalCharsAdded} | <strong>` + t`Total Removed:` + `</strong> -${totalCharsRemoved} |
| 1132 | </div> | 1248 | </div> |
| 1133 | `; | 1249 | `; |
| 1134 | finalOutput.before(summaryHtml); | 1250 | finalOutput.before(summaryHtml); |
| @@ -1148,11 +1264,13 @@ async function onRegexDebuggerOpenClick() { | |||
| 1148 | const allKnownScripts = getRegexScripts(); | 1264 | const allKnownScripts = getRegexScripts(); |
| 1149 | const newGlobalScripts = $('#regex_debugger_rules_global').children('li').map((_, el) => allKnownScripts.find(s => s.id === $(el).data('id'))).get().filter(Boolean); | 1265 | const newGlobalScripts = $('#regex_debugger_rules_global').children('li').map((_, el) => allKnownScripts.find(s => s.id === $(el).data('id'))).get().filter(Boolean); |
| 1150 | const newScopedScripts = $('#regex_debugger_rules_scoped').children('li').map((_, el) => allKnownScripts.find(s => s.id === $(el).data('id'))).get().filter(Boolean); | 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 | extension_settings.regex = newGlobalScripts; | 1269 | extension_settings.regex = newGlobalScripts; |
| 1153 | if (this_chid !== undefined) { | 1270 | if (this_chid !== undefined) { |
| 1154 | await writeExtensionField(this_chid, 'regex_scripts', newScopedScripts); | 1271 | await saveScriptsByType(newScopedScripts, SCRIPT_TYPES.SCOPED); |
| 1155 | } | 1272 | } |
| 1273 | await saveScriptsByType(newPresetScripts, SCRIPT_TYPES.PRESET); | ||
| 1156 | 1274 | ||
| 1157 | saveSettingsDebounced(); | 1275 | saveSettingsDebounced(); |
| 1158 | await loadRegexScripts(); | 1276 | await loadRegexScripts(); |
| @@ -1164,6 +1282,8 @@ async function onRegexDebuggerOpenClick() { | |||
| 1164 | currentPopupContent.find('#regex_debugger_rules_global').sortable({ delay: getSortableDelay() }).disableSelection(); | 1282 | currentPopupContent.find('#regex_debugger_rules_global').sortable({ delay: getSortableDelay() }).disableSelection(); |
| 1165 | // @ts-ignore | 1283 | // @ts-ignore |
| 1166 | currentPopupContent.find('#regex_debugger_rules_scoped').sortable({ delay: getSortableDelay() }).disableSelection(); | 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 | debuggerHtml.find('#regex_debugger_expand_steps').on('click', function () { | 1289 | debuggerHtml.find('#regex_debugger_expand_steps').on('click', function () { |
| @@ -1204,13 +1324,13 @@ async function onRegexDebuggerOpenClick() { | |||
| 1204 | }); | 1324 | }); |
| 1205 | 1325 | ||
| 1206 | popupContainer.append(navPanel).append(contentPanel); | 1326 | popupContainer.append(navPanel).append(contentPanel); |
| 1207 | callGenericPopup(popupContainer, POPUP_TYPE.TEXT, 'Step-by-step Transformation', { wide: true, allowVerticalScrolling: false }); | 1327 | callGenericPopup(popupContainer, POPUP_TYPE.TEXT, t`Step-by-step Transformation`, { wide: true, allowVerticalScrolling: false }); |
| 1208 | }); | 1328 | }); |
| 1209 | 1329 | ||
| 1210 | debuggerHtml.find('#regex_debugger_expand_final').on('click', function () { | 1330 | debuggerHtml.find('#regex_debugger_expand_final').on('click', function () { |
| 1211 | const content = $('#regex_debugger_final_output').html(); | 1331 | const content = $('#regex_debugger_final_output').html(); |
| 1212 | const popupContent = $('<div class="regex-popup-content"></div>').html(content); | 1332 | const popupContent = $('<div class="regex-popup-content"></div>').html(content); |
| 1213 | callGenericPopup(popupContent, POPUP_TYPE.TEXT, 'Final Output', { wide: true, large: true, allowVerticalScrolling: true }); | 1333 | callGenericPopup(popupContent, POPUP_TYPE.TEXT, t`Final Output`, { wide: true, large: true, allowVerticalScrolling: true }); |
| 1214 | }); | 1334 | }); |
| 1215 | 1335 | ||
| 1216 | await callGenericPopup(debuggerHtml.children(), POPUP_TYPE.TEXT, '', { wide: true, allowVerticalScrolling: true }); | 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 | if (performSave) { | 1412 | if (performSave) { |
| 1298 | saveSettingsDebounced(); | 1413 | saveSettingsDebounced(); |
| 1299 | } | 1414 | } |
| @@ -1364,10 +1479,10 @@ async function toggleRegexCallback(args, scriptName) { | |||
| 1364 | break; | 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 | const index = isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts?.indexOf(script) : scripts.indexOf(script); | 1483 | const index = getScriptsByType(scriptType).indexOf(script); |
| 1369 | 1484 | ||
| 1370 | await saveRegexScript(script, index, isScoped); | 1485 | await saveRegexScript(script, index, scriptType); |
| 1371 | if (script.disabled) { | 1486 | if (script.disabled) { |
| 1372 | !quiet && toastr.success(t`Regex script '${scriptName}' has been disabled.`); | 1487 | !quiet && toastr.success(t`Regex script '${scriptName}' has been disabled.`); |
| 1373 | } else { | 1488 | } else { |
| @@ -1379,10 +1494,10 @@ async function toggleRegexCallback(args, scriptName) { | |||
| 1379 | 1494 | ||
| 1380 | /** | 1495 | /** |
| 1381 | * Performs the import of the regex object. | 1496 | * Performs the import of the regex object. |
| 1382 | * @param {Object} regexScript Input object | 1497 | * @param {RegexScript} regexScript Input object |
| 1383 | * @param {boolean} isScoped Is the script scoped to a character? | 1498 | * @param {SCRIPT_TYPES} scriptType The type of script to import as |
| 1384 | */ | 1499 | */ |
| 1385 | async function onRegexImportObjectChange(regexScript, isScoped) { | 1500 | async function onRegexImportObjectChange(regexScript, scriptType) { |
| 1386 | try { | 1501 | try { |
| 1387 | if (!regexScript.scriptName) { | 1502 | if (!regexScript.scriptName) { |
| 1388 | throw new Error('No script name provided.'); | 1503 | throw new Error('No script name provided.'); |
| @@ -1391,11 +1506,21 @@ async function onRegexImportObjectChange(regexScript, isScoped) { | |||
| 1391 | // Assign a new UUID | 1506 | // Assign a new UUID |
| 1392 | regexScript.id = uuidv4(); | 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 | array.push(regexScript); | 1510 | array.push(regexScript); |
| 1396 | 1511 | ||
| 1397 | if (isScoped) { | 1512 | switch (scriptType) { |
| 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 | saveSettingsDebounced(); | 1526 | saveSettingsDebounced(); |
| @@ -1411,9 +1536,9 @@ async function onRegexImportObjectChange(regexScript, isScoped) { | |||
| 1411 | /** | 1536 | /** |
| 1412 | * Performs the import of the regex file. | 1537 | * Performs the import of the regex file. |
| 1413 | * @param {File} file Input file | 1538 | * @param {File} file Input file |
| 1414 | * @param {boolean} isScoped Is the script scoped to a character? | 1539 | * @param {SCRIPT_TYPES} scriptType The type of script to import as |
| 1415 | */ | 1540 | */ |
| 1416 | async function onRegexImportFileChange(file, isScoped) { | 1541 | async function onRegexImportFileChange(file, scriptType) { |
| 1417 | if (!file) { | 1542 | if (!file) { |
| 1418 | toastr.error('No file provided.'); | 1543 | toastr.error('No file provided.'); |
| 1419 | return; | 1544 | return; |
| @@ -1423,10 +1548,10 @@ async function onRegexImportFileChange(file, isScoped) { | |||
| 1423 | const regexScripts = JSON.parse(await getFileText(file)); | 1548 | const regexScripts = JSON.parse(await getFileText(file)); |
| 1424 | if (Array.isArray(regexScripts)) { | 1549 | if (Array.isArray(regexScripts)) { |
| 1425 | for (const regexScript of regexScripts) { | 1550 | for (const regexScript of regexScripts) { |
| 1426 | await onRegexImportObjectChange(regexScript, isScoped); | 1551 | await onRegexImportObjectChange(regexScript, scriptType); |
| 1427 | } | 1552 | } |
| 1428 | } else { | 1553 | } else { |
| 1429 | await onRegexImportObjectChange(regexScripts, isScoped); | 1554 | await onRegexImportObjectChange(regexScripts, scriptType); |
| 1430 | } | 1555 | } |
| 1431 | } catch (error) { | 1556 | } catch (error) { |
| 1432 | console.log(error); | 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 | function purgeEmbeddedRegexScripts({ character }) { | 1587 | function purgeEmbeddedRegexScripts({ character }) { |
| 1439 | const avatar = character?.avatar; | 1588 | const avatar = character?.avatar; |
| 1440 | 1589 | if (!avatar) { | |
| 1441 | if (avatar && extension_settings.character_allowed_regex?.includes(avatar)) { | 1590 | return; |
| 1442 | const index = extension_settings.character_allowed_regex.indexOf(avatar); | ||
| 1443 | if (index !== -1) { | ||
| 1444 | extension_settings.character_allowed_regex.splice(index, 1); | ||
| 1445 | saveSettingsDebounced(); | ||
| 1446 | } | 1591 | } |
| 1592 | const checkKey = `AlertRegex_${avatar}`; | ||
| 1593 | if (accountStorage.getItem(checkKey)) { | ||
| 1594 | accountStorage.removeItem(checkKey); | ||
| 1447 | } | 1595 | } |
| 1596 | disallowScopedScripts(characters?.[this_chid]); | ||
| 1448 | } | 1597 | } |
| 1449 | 1598 | ||
| 1450 | async function checkEmbeddedRegexScripts() { | 1599 | function purgePresetEmbeddedRegexScripts({ apiId, name }) { |
| 1600 | const checkKey = `AlertRegex_${apiId}_${name}`; | ||
| 1601 | if (accountStorage.getItem(checkKey)) { | ||
| 1602 | accountStorage.removeItem(checkKey); | ||
| 1603 | } | ||
| 1604 | disallowPresetScripts(apiId, name); | ||
| 1605 | } | ||
| 1606 | |||
| 1607 | async function checkCharEmbeddedRegexScripts() { | ||
| 1451 | const chid = this_chid; | 1608 | const chid = this_chid; |
| 1452 | 1609 | ||
| 1453 | if (chid !== undefined && !selected_group) { | 1610 | if (chid !== undefined && !selected_group) { |
| 1454 | const avatar = characters[chid]?.avatar; | 1611 | const character = characters[chid]; |
| 1455 | const scripts = characters[chid]?.data?.extensions?.regex_scripts; | 1612 | const scripts = getScriptsByType(SCRIPT_TYPES.SCOPED); |
| 1456 | 1613 | ||
| 1457 | if (Array.isArray(scripts) && scripts.length > 0) { | 1614 | if (Array.isArray(scripts) && scripts.length > 0) { |
| 1458 | if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) { | 1615 | if (!isScopedScriptsAllowed(character)) { |
| 1459 | const checkKey = `AlertRegex_${characters[chid].avatar}`; | 1616 | const checkKey = `AlertRegex_${character.avatar}`; |
| 1460 | |||
| 1461 | if (!accountStorage.getItem(checkKey)) { | 1617 | if (!accountStorage.getItem(checkKey)) { |
| 1462 | accountStorage.setItem(checkKey, 'true'); | 1618 | accountStorage.setItem(checkKey, 'true'); |
| 1463 | const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {}); | 1619 | const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {}); |
| 1464 | const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Yes' }); | 1620 | const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, ''); |
| 1465 | 1621 | ||
| 1466 | if (result) { | 1622 | if (result) { |
| 1467 | extension_settings.character_allowed_regex.push(avatar); | 1623 | allowScopedScripts(character); |
| 1468 | await reloadCurrentChat(); | 1624 | await reloadCurrentChat(); |
| 1469 | saveSettingsDebounced(); | ||
| 1470 | } | 1625 | } |
| 1471 | } | 1626 | } |
| 1472 | } | 1627 | } |
| 1473 | } | 1628 | } |
| 1474 | } | 1629 | } |
| 1475 | 1630 | ||
| 1476 | loadRegexScripts(); | 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 | // Workaround for loading in sequence with other extensions | 1709 | // Workaround for loading in sequence with other extensions |
| @@ -1497,7 +1727,7 @@ jQuery(async () => { | |||
| 1497 | const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown')); | 1727 | const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown')); |
| 1498 | $('#regex_container').append(settingsHtml); | 1728 | $('#regex_container').append(settingsHtml); |
| 1499 | $('#open_regex_editor').on('click', function () { | 1729 | $('#open_regex_editor').on('click', function () { |
| 1500 | onRegexEditorOpenClick(false, false); | 1730 | onRegexEditorOpenClick(false, SCRIPT_TYPES.GLOBAL); |
| 1501 | }); | 1731 | }); |
| 1502 | $('#open_regex_debugger').on('click', onRegexDebuggerOpenClick); | 1732 | $('#open_regex_debugger').on('click', onRegexDebuggerOpenClick); |
| 1503 | $('#open_scoped_editor').on('click', function () { | 1733 | $('#open_scoped_editor').on('click', function () { |
| @@ -1511,19 +1741,23 @@ jQuery(async () => { | |||
| 1511 | return; | 1741 | return; |
| 1512 | } | 1742 | } |
| 1513 | 1743 | ||
| 1514 | onRegexEditorOpenClick(false, true); | 1744 | onRegexEditorOpenClick(false, SCRIPT_TYPES.SCOPED); |
| 1745 | }); | ||
| 1746 | $('#open_preset_editor').on('click', function () { | ||
| 1747 | onRegexEditorOpenClick(false, SCRIPT_TYPES.PRESET); | ||
| 1515 | }); | 1748 | }); |
| 1516 | $('#import_regex_file').on('change', async function () { | 1749 | $('#import_regex_file').on('change', async function () { |
| 1517 | let target = 'global'; | 1750 | let target = SCRIPT_TYPES.GLOBAL; |
| 1518 | const template = $(await renderExtensionTemplateAsync('regex', 'importTarget')); | 1751 | const template = $(await renderExtensionTemplateAsync('regex', 'importTarget')); |
| 1519 | template.find('#regex_import_target_global').on('input', () => target = 'global'); | 1752 | template.find('#regex_import_target_global').on('input', () => (target = SCRIPT_TYPES.GLOBAL)); |
| 1520 | template.find('#regex_import_target_scoped').on('input', () => target = 'scoped'); | 1753 | template.find('#regex_import_target_scoped').on('input', () => (target = SCRIPT_TYPES.SCOPED)); |
| 1754 | template.find('#regex_import_target_preset').on('input', () => (target = SCRIPT_TYPES.PRESET)); | ||
| 1521 | 1755 | ||
| 1522 | await callGenericPopup(template, POPUP_TYPE.TEXT); | 1756 | await callGenericPopup(template, POPUP_TYPE.TEXT); |
| 1523 | 1757 | ||
| 1524 | const inputElement = this instanceof HTMLInputElement && this; | 1758 | const inputElement = this instanceof HTMLInputElement && this; |
| 1525 | for (const file of inputElement.files) { | 1759 | for (const file of inputElement.files) { |
| 1526 | await onRegexImportFileChange(file, target === 'scoped'); | 1760 | await onRegexImportFileChange(file, target); |
| 1527 | } | 1761 | } |
| 1528 | inputElement.value = ''; | 1762 | inputElement.value = ''; |
| 1529 | }); | 1763 | }); |
| @@ -1531,13 +1765,6 @@ jQuery(async () => { | |||
| 1531 | $('#import_regex_file').trigger('click'); | 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 | $('#bulk_select_all_toggle').on('click', async function () { | 1768 | $('#bulk_select_all_toggle').on('click', async function () { |
| 1542 | const checkboxes = $('#regex_container .regex_bulk_checkbox'); | 1769 | const checkboxes = $('#regex_container .regex_bulk_checkbox'); |
| 1543 | if (checkboxes.length === 0) { | 1770 | if (checkboxes.length === 0) { |
| @@ -1549,32 +1776,98 @@ jQuery(async () => { | |||
| 1549 | 1776 | ||
| 1550 | checkboxes.prop('checked', newState); | 1777 | checkboxes.prop('checked', newState); |
| 1551 | setToggleAllIcon(newState); | 1778 | setToggleAllIcon(newState); |
| 1779 | setMoveButtonsVisibility(); | ||
| 1552 | }); | 1780 | }); |
| 1553 | 1781 | ||
| 1554 | $('#bulk_enable_regex').on('click', async function () { | 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 | if (scripts.length === 0) { | 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 | return; | 1802 | return; |
| 1559 | } | 1803 | } |
| 1804 | const scriptTypesToSave = new Set(); | ||
| 1560 | for (const script of scripts) { | 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 | saveSettingsDebounced(); | 1815 | saveSettingsDebounced(); |
| 1564 | await loadRegexScripts(); | 1816 | await loadRegexScripts(); |
| 1565 | }); | ||
| 1566 | 1817 | ||
| 1567 | $('#bulk_disable_regex').on('click', async function () { | 1818 | // Reload the current chat to undo previous markdown |
| 1568 | const scripts = getSelectedScripts().filter(script => !script.disabled); | 1819 | const currentChatId = getCurrentChatId(); |
| 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 | if (scripts.length === 0) { | 1831 | if (scripts.length === 0) { |
| 1570 | toastr.warning(t`No regex scripts selected for disabling.`); | 1832 | toastr.warning(t`No regex scripts selected for moving.`); |
| 1571 | return; | 1833 | return; |
| 1572 | } | 1834 | } |
| 1573 | for (const script of scripts) { | 1835 | for (const script of scripts) { |
| 1574 | script.disabled = true; | 1836 | await moveRegexScript(script, toType, getScriptType(script), false); |
| 1575 | } | 1837 | } |
| 1838 | |||
| 1576 | saveSettingsDebounced(); | 1839 | saveSettingsDebounced(); |
| 1577 | await loadRegexScripts(); | 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 | $('#bulk_delete_regex').on('click', async function () { | 1873 | $('#bulk_delete_regex').on('click', async function () { |
| @@ -1583,16 +1876,16 @@ jQuery(async () => { | |||
| 1583 | toastr.warning(t`No regex scripts selected for deletion.`); | 1876 | toastr.warning(t`No regex scripts selected for deletion.`); |
| 1584 | return; | 1877 | return; |
| 1585 | } | 1878 | } |
| 1586 | const confirm = await callGenericPopup('Are you sure you want to delete the selected regex scripts?', POPUP_TYPE.CONFIRM); | 1879 | const confirm = await callGenericPopup(t`Are you sure you want to delete the selected regex scripts?`, POPUP_TYPE.CONFIRM); |
| 1587 | if (!confirm) { | 1880 | if (!confirm) { |
| 1588 | return; | 1881 | return; |
| 1589 | } | 1882 | } |
| 1590 | for (const script of scripts) { | 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 | saveSettingsDebounced(); | 1886 | saveSettingsDebounced(); |
| 1887 | await loadRegexScripts(); | ||
| 1888 | await reloadCurrentChat(); | ||
| 1596 | }); | 1889 | }); |
| 1597 | 1890 | ||
| 1598 | $('#bulk_export_regex').on('click', async function () { | 1891 | $('#bulk_export_regex').on('click', async function () { |
| @@ -1610,13 +1903,18 @@ jQuery(async () => { | |||
| 1610 | let sortableDatas = [ | 1903 | let sortableDatas = [ |
| 1611 | { | 1904 | { |
| 1612 | selector: '#saved_regex_scripts', | 1905 | selector: '#saved_regex_scripts', |
| 1613 | setter: x => extension_settings.regex = x, | 1906 | setter: scripts => saveScriptsByType(scripts, SCRIPT_TYPES.GLOBAL), |
| 1614 | getter: () => extension_settings.regex ?? [], | 1907 | getter: () => getScriptsByType(SCRIPT_TYPES.GLOBAL), |
| 1615 | }, | 1908 | }, |
| 1616 | { | 1909 | { |
| 1617 | selector: '#saved_scoped_scripts', | 1910 | selector: '#saved_scoped_scripts', |
| 1618 | setter: x => writeExtensionField(this_chid, 'regex_scripts', x), | 1911 | setter: scripts => saveScriptsByType(scripts, SCRIPT_TYPES.SCOPED), |
| 1619 | getter: () => characters[this_chid]?.data?.extensions?.regex_scripts ?? [], | 1912 | getter: () => getScriptsByType(SCRIPT_TYPES.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 | for (const { selector, setter, getter } of sortableDatas) { | 1920 | for (const { selector, setter, getter } of sortableDatas) { |
| @@ -1638,6 +1936,7 @@ jQuery(async () => { | |||
| 1638 | saveSettingsDebounced(); | 1936 | saveSettingsDebounced(); |
| 1639 | 1937 | ||
| 1640 | console.debug(`Regex scripts in ${selector} reordered`); | 1938 | console.debug(`Regex scripts in ${selector} reordered`); |
| 1939 | await reloadCurrentChat(); | ||
| 1641 | await loadRegexScripts(); | 1940 | await loadRegexScripts(); |
| 1642 | }, | 1941 | }, |
| 1643 | }); | 1942 | }); |
| @@ -1655,17 +1954,26 @@ jQuery(async () => { | |||
| 1655 | } | 1954 | } |
| 1656 | 1955 | ||
| 1657 | const isEnable = !!$(this).prop('checked'); | 1956 | const isEnable = !!$(this).prop('checked'); |
| 1658 | const avatar = characters[this_chid].avatar; | 1957 | const character = characters[this_chid]; |
| 1659 | 1958 | ||
| 1660 | if (isEnable) { | 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 | } else { | 1961 | } else { |
| 1665 | const index = extension_settings.character_allowed_regex.indexOf(avatar); | 1962 | disallowScopedScripts(character); |
| 1666 | if (index !== -1) { | ||
| 1667 | extension_settings.character_allowed_regex.splice(index, 1); | ||
| 1668 | } | 1963 | } |
| 1964 | |||
| 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 | saveSettingsDebounced(); | 1979 | saveSettingsDebounced(); |
| @@ -1676,11 +1984,57 @@ jQuery(async () => { | |||
| 1676 | // @ts-ignore | 1984 | // @ts-ignore |
| 1677 | $('#saved_regex_scripts').sortable('enable'); | 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 | const localEnumProviders = { | 2027 | const localEnumProviders = { |
| 1680 | regexScripts: () => getRegexScripts().map(script => { | 2028 | regexScripts: () => |
| 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); |
| 2032 | return new SlashCommandEnumValue( | ||
| 2033 | script.scriptName, | ||
| 2034 | `${enumIcons.getStateIcon(!script.disabled)} [${typename}] ${script.findRegex}`, | ||
| 2035 | color, | ||
| 2036 | icon, | ||
| 2037 | ); | ||
| 1684 | }), | 2038 | }), |
| 1685 | }; | 2039 | }; |
| 1686 | 2040 | ||
| @@ -1750,8 +2104,12 @@ jQuery(async () => { | |||
| 1750 | `, | 2104 | `, |
| 1751 | })); | 2105 | })); |
| 1752 | 2106 | ||
| 1753 | eventSource.on(event_types.CHAT_CHANGED, checkEmbeddedRegexScripts); | 2107 | eventSource.on(event_types.MAIN_API_CHANGED, onMainApiChanged); |
| 2108 | eventSource.on(event_types.CHAT_CHANGED, checkCharEmbeddedRegexScripts); | ||
| 1754 | eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts); | 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 | presetManager.setupEventListeners(); | 2114 | presetManager.setupEventListeners(); |
| 1757 | presetManager.registerSlashCommands(); | 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 | <div class="regex-script-label flex-container flexnowrap"> | 1 | <div class="regex-script-label flex-container flexnowrap"> |
| 2 | <input type="checkbox" class="regex_bulk_checkbox" /> | 2 | <input type="checkbox" class="regex_bulk_checkbox" /> |
| 3 | <span class="drag-handle menu-handle">☰</span> | 3 | <span class="drag-handle menu-handle">☰</span> |
| 4 | <div class="regex_script_name flexGrow overflow-hidden"></div> | 4 | <div class="regex_script_name flex1 overflow-hidden"></div> |
| 5 | <div class="flex-container flexnowrap"> | 5 | <div class="flex-container flexnowrap"> |
| 6 | <label class="checkbox flex-container" for="regex_disable"> | 6 | <label class="checkbox flex-container margin-r5" for="regex_disable"> |
| 7 | <input type="checkbox" name="regex_disable" class="disable_regex" /> | 7 | <input type="checkbox" name="regex_disable" class="disable_regex" /> |
| 8 | <span class="regex-toggle-on fa-solid fa-toggle-on" data-i18n="[title]ext_regex_disable_script" title="Disable script"></span> | 8 | <span class="regex-toggle-on fa-solid fa-toggle-on" data-i18n="[title]ext_regex_disable_script" title="Disable script"></span> |
| 9 | <span class="regex-toggle-off fa-solid fa-toggle-off" data-i18n="[title]ext_regex_enable_script" title="Enable script"></span> | 9 | <span class="regex-toggle-off fa-solid fa-toggle-off" data-i18n="[title]ext_regex_enable_script" title="Enable script"></span> |
| 10 | </label> | 10 | </label> |
| 11 | <div class="edit_existing_regex menu_button" data-i18n="[title]ext_regex_edit_script" title="Edit script"> | 11 | <label class="menu_button regex_script_expand" title="Show more options" data-i18n="[title]Show more options"> |
| 12 | <i class="fa-solid fa-pencil"></i> | 12 | <input type="checkbox" name="regex_expand" /> |
| 13 | </div> | 13 | <span class="fa-solid fa-ellipsis"></span> |
| 14 | </label> | ||
| 15 | <div class="flex-container regex_script_buttons"> | ||
| 14 | <div class="move_to_global menu_button" data-i18n="[title]ext_regex_move_to_global" title="Move to global scripts"> | 16 | <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> | 17 | <i class="fa-solid fa-globe"></i> |
| 16 | </div> | 18 | </div> |
| 17 | <div class="move_to_scoped menu_button" data-i18n="[title]ext_regex_move_to_scoped" title="Move to scoped scripts"> | 19 | <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> | 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> | ||
| 19 | </div> | 24 | </div> |
| 20 | <div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script"> | 25 | <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> | 26 | <i class="fa-solid fa-file-export"></i> |
| 22 | </div> | 27 | </div> |
| 28 | </div> | ||
| 29 | <div class="edit_existing_regex menu_button" data-i18n="[title]ext_regex_edit_script" title="Edit script"> | ||
| 30 | <i class="fa-solid fa-pencil"></i> | ||
| 31 | </div> | ||
| 23 | <div class="delete_regex menu_button" data-i18n="[title]ext_regex_delete_script" title="Delete script"> | 32 | <div class="delete_regex menu_button" data-i18n="[title]ext_regex_delete_script" title="Delete script"> |
| 24 | <i class="fa-solid fa-trash"></i> | 33 | <i class="fa-solid fa-trash"></i> |
| 25 | </div> | 34 | </div> |
| @@ -24,39 +24,33 @@ | |||
| 24 | text-align: center; | 24 | text-align: center; |
| 25 | } | 25 | } |
| 26 | 26 | ||
| 27 | #scoped_scripts_block { | 27 | #scoped_scripts_block, |
| 28 | #preset_scripts_block { | ||
| 28 | opacity: 1; | 29 | opacity: 1; |
| 29 | transition: opacity var(--animation-duration-2x) ease-in-out; | 30 | transition: opacity var(--animation-duration-2x) ease-in-out; |
| 30 | } | 31 | } |
| 31 | 32 | ||
| 32 | #scoped_scripts_block .move_to_scoped { | 33 | #scoped_scripts_block .move_to_scoped, |
| 34 | #global_scripts_block .move_to_global, | ||
| 35 | #preset_scripts_block .move_to_preset { | ||
| 33 | display: none; | 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 | opacity: 0.5; | 41 | opacity: 0.5; |
| 42 | } | 42 | } |
| 43 | 43 | ||
| 44 | .enable_scoped:checked~.regex-toggle-on { | 44 | .enable_scoped:checked~.regex-toggle-on, |
| 45 | .enable_scoped:not(:checked)~.regex-toggle-off { | ||
| 45 | display: block; | 46 | display: block; |
| 46 | } | 47 | } |
| 47 | 48 | ||
| 48 | .enable_scoped:checked~.regex-toggle-off { | 49 | .enable_scoped:checked~.regex-toggle-off, |
| 49 | display: none; | ||
| 50 | } | ||
| 51 | |||
| 52 | .enable_scoped:not(:checked)~.regex-toggle-on { | 50 | .enable_scoped:not(:checked)~.regex-toggle-on { |
| 53 | display: none; | 51 | display: none; |
| 54 | } | 52 | } |
| 55 | 53 | ||
| 56 | .enable_scoped:not(:checked)~.regex-toggle-off { | ||
| 57 | display: block; | ||
| 58 | } | ||
| 59 | |||
| 60 | .regex-script-label { | 54 | .regex-script-label { |
| 61 | align-items: baseline; | 55 | align-items: baseline; |
| 62 | border: 1px solid var(--SmartThemeBorderColor); | 56 | border: 1px solid var(--SmartThemeBorderColor); |
| @@ -92,19 +86,13 @@ input.enable_scoped { | |||
| 92 | cursor: pointer; | 86 | cursor: pointer; |
| 93 | } | 87 | } |
| 94 | 88 | ||
| 95 | .disable_regex:checked~.regex-toggle-off { | 89 | .disable_regex:checked~.regex-toggle-on, |
| 96 | display: block; | ||
| 97 | } | ||
| 98 | |||
| 99 | .disable_regex:checked~.regex-toggle-on { | ||
| 100 | display: none; | ||
| 101 | } | ||
| 102 | |||
| 103 | .disable_regex:not(:checked)~.regex-toggle-off { | 90 | .disable_regex:not(:checked)~.regex-toggle-off { |
| 104 | display: none; | 91 | display: none; |
| 105 | } | 92 | } |
| 106 | 93 | ||
| 107 | .disable_regex:not(:checked)~.regex-toggle-on { | 94 | .disable_regex:not(:checked)~.regex-toggle-on, |
| 95 | .disable_regex:checked~.regex-toggle-off { | ||
| 108 | display: block; | 96 | display: block; |
| 109 | } | 97 | } |
| 110 | 98 | ||
| @@ -139,7 +127,8 @@ input.enable_scoped { | |||
| 139 | } | 127 | } |
| 140 | 128 | ||
| 141 | .regex_settings .regex_bulk_operations, | 129 | .regex_settings .regex_bulk_operations, |
| 142 | .regex_settings .regex_bulk_checkbox { | 130 | .regex_settings .regex_bulk_checkbox, |
| 131 | .regex_settings .regex_bulk_operations_hr { | ||
| 143 | display: none; | 132 | display: none; |
| 144 | } | 133 | } |
| 145 | 134 | ||
| @@ -147,6 +136,26 @@ input.enable_scoped { | |||
| 147 | display: flex; | 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 | .regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_checkbox { | 143 | .regex_settings:has(#regex_bulk_edit:checked) .regex_bulk_checkbox { |
| 151 | display: inline-grid; | 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 | custom_url: profile['api-url'], | 410 | custom_url: profile['api-url'], |
| 411 | reverse_proxy: proxyPreset?.url, | 411 | reverse_proxy: proxyPreset?.url, |
| 412 | proxy_password: proxyPreset?.password, | 412 | proxy_password: proxyPreset?.password, |
| 413 | custom_prompt_post_processing: profile['prompt-post-processing'], | ||
| 413 | ...overridePayload, | 414 | ...overridePayload, |
| 414 | }, { | 415 | }, { |
| 415 | presetName: includePreset ? profile.preset : undefined, | 416 | presetName: includePreset ? profile.preset : undefined, |
| @@ -1,6 +1,6 @@ | |||
| 1 | <div id="sd_dropdown"> | 1 | <div id="sd_dropdown"> |
| 2 | <ul class="list-group"> | 2 | <ul class="list-group"> |
| 3 | <span>Send me a picture of:</span> | 3 | <span data-i18n="Send me a picture of:">Send me a picture of:</span> |
| 4 | <li class="list-group-item" id="sd_you" data-value="you" data-i18n="sd_Yourself">Yourself</li> | 4 | <li class="list-group-item" id="sd_you" data-value="you" data-i18n="sd_Yourself">Yourself</li> |
| 5 | <li class="list-group-item" id="sd_face" data-value="face" data-i18n="sd_Your_Face">Your Face</li> | 5 | <li class="list-group-item" id="sd_face" data-value="face" data-i18n="sd_Your_Face">Your Face</li> |
| 6 | <li class="list-group-item" id="sd_me" data-value="me" data-i18n="sd_Me">Me</li> | 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 | models = await loadStabilityModels(); | 1705 | models = await loadStabilityModels(); |
| 1706 | break; | 1706 | break; |
| 1707 | case sources.huggingface: | 1707 | case sources.huggingface: |
| 1708 | models = [{ value: '', text: '<Enter Model ID above>' }]; | 1708 | models = [{ value: '', text: t`<Enter Model ID above>` }]; |
| 1709 | break; | 1709 | break; |
| 1710 | case sources.electronhub: | 1710 | case sources.electronhub: |
| 1711 | models = await loadElectronHubModels(); | 1711 | models = await loadElectronHubModels(); |
| @@ -90,15 +90,15 @@ | |||
| 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> | 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 | </div> | 91 | </div> |
| 92 | <div data-sd-source="huggingface"> | 92 | <div data-sd-source="huggingface"> |
| 93 | <i>Hint: Save an API key in the Hugging Face (Text Completion) API settings to use it here.</i> | 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 | <label for="sd_huggingface_model_id" data-i18n="Model ID">Model ID</label> | 94 | <label for="sd_huggingface_model_id" data-i18n="Model ID">Model ID</label> |
| 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="" /> | 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 | </div> | 96 | </div> |
| 97 | <div data-sd-source="electronhub"> | 97 | <div data-sd-source="electronhub"> |
| 98 | <i>Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.</i> | 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 | </div> | 99 | </div> |
| 100 | <div data-sd-source="nanogpt"> | 100 | <div data-sd-source="nanogpt"> |
| 101 | <i>Hint: Save an API key in the NanoGPT (Chat Completion) API settings to use it here.</i> | 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 | </div> | 102 | </div> |
| 103 | <div data-sd-source="vlad"> | 103 | <div data-sd-source="vlad"> |
| 104 | <label for="sd_vlad_url">SD.Next API URL</label> | 104 | <label for="sd_vlad_url">SD.Next API URL</label> |
| @@ -143,7 +143,7 @@ | |||
| 143 | View my Anlas | 143 | View my Anlas |
| 144 | </div> | 144 | </div> |
| 145 | </div> | 145 | </div> |
| 146 | <i>Hint: Save an API key in the NovelAI API settings to use it here.</i> | 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 | </div> | 147 | </div> |
| 148 | <div data-sd-source="aimlapi"> | 148 | <div data-sd-source="aimlapi"> |
| 149 | <div class="flex-container flexnowrap alignItemsBaseline marginBot5"> | 149 | <div class="flex-container flexnowrap alignItemsBaseline marginBot5"> |
| @@ -204,7 +204,7 @@ | |||
| 204 | <a href="https://pollinations.ai">Pollinations.ai</a> | 204 | <a href="https://pollinations.ai">Pollinations.ai</a> |
| 205 | </p> | 205 | </p> |
| 206 | <div class="flex-container"> | 206 | <div class="flex-container"> |
| 207 | <label class="flex1 checkbox_label" for="sd_pollinations_enhance" title="Enables prompt enhancing (passes prompts through an LLM to add detail)."> | 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 | <input id="sd_pollinations_enhance" type="checkbox" /> | 208 | <input id="sd_pollinations_enhance" type="checkbox" /> |
| 209 | <span data-i18n="Enhance"> | 209 | <span data-i18n="Enhance"> |
| 210 | Enhance | 210 | Enhance |
| @@ -297,7 +297,7 @@ | |||
| 297 | </div> | 297 | </div> |
| 298 | </div> | 298 | </div> |
| 299 | <div class="flex-container"> | 299 | <div class="flex-container"> |
| 300 | <label class="flex1 checkbox_label" for="sd_google_enhance" title="Enables prompt enhancing (passes prompts through an LLM to add detail)."> | 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 | <input id="sd_google_enhance" type="checkbox" /> | 301 | <input id="sd_google_enhance" type="checkbox" /> |
| 302 | <span data-i18n="Enhance"> | 302 | <span data-i18n="Enhance"> |
| 303 | Enhance | 303 | Enhance |
| @@ -1,3 +1,8 @@ | |||
| 1 | .minimax_tts_settings>.tts_block { | ||
| 2 | gap: 5px; | ||
| 3 | margin: 5px 0; | ||
| 4 | } | ||
| 5 | |||
| 1 | .minimax-custom-item { | 6 | .minimax-custom-item { |
| 2 | display: flex; | 7 | display: flex; |
| 3 | justify-content: space-between; | 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 | // Set initial values from the settings | 94 | // Set initial values from the settings |
| 95 | $('#tts_endpoint').val(this.settings.provider_endpoint); | 95 | $('#tts_endpoint').val(this.settings.provider_endpoint).on('change', this.onSettingsChange.bind(this)); |
| 96 | $('#text_lang').val(this.settings.text_lang); | 96 | $('#text_lang').val(this.settings.text_lang).on('change', this.onSettingsChange.bind(this)); |
| 97 | $('#prompt_lang').val(this.settings.prompt_lang); | 97 | $('#prompt_lang').val(this.settings.prompt_lang).on('change', this.onSettingsChange.bind(this)); |
| 98 | |||
| 99 | 98 | ||
| 100 | await this.checkReady(); | 99 | await this.checkReady(); |
| 101 | 100 | ||
| @@ -108,7 +107,7 @@ class GptSovitsV2Provider { | |||
| 108 | } | 107 | } |
| 109 | 108 | ||
| 110 | async onRefreshClick() { | 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 | import { TtsWebuiProvider } from './tts-webui.js'; | 33 | import { TtsWebuiProvider } from './tts-webui.js'; |
| 34 | import { PollinationsTtsProvider } from './pollinations.js'; | 34 | import { PollinationsTtsProvider } from './pollinations.js'; |
| 35 | import { MiniMaxTtsProvider } from './minimax.js'; | 35 | import { MiniMaxTtsProvider } from './minimax.js'; |
| 36 | import { ElectronHubTtsProvider } from './electronhub.js'; | ||
| 36 | 37 | ||
| 37 | const UPDATE_INTERVAL = 1000; | 38 | const UPDATE_INTERVAL = 1000; |
| 38 | const wrapper = new ModuleWorkerWrapper(moduleWorker); | 39 | const wrapper = new ModuleWorkerWrapper(moduleWorker); |
| @@ -123,6 +124,7 @@ const ttsProviders = { | |||
| 123 | 'CosyVoice (Unofficial)': CosyVoiceProvider, | 124 | 'CosyVoice (Unofficial)': CosyVoiceProvider, |
| 124 | Edge: EdgeTtsProvider, | 125 | Edge: EdgeTtsProvider, |
| 125 | ElevenLabs: ElevenLabsTtsProvider, | 126 | ElevenLabs: ElevenLabsTtsProvider, |
| 127 | 'Electron Hub': ElectronHubTtsProvider, | ||
| 126 | 'Google Translate': GoogleTranslateTtsProvider, | 128 | 'Google Translate': GoogleTranslateTtsProvider, |
| 127 | 'Google Gemini TTS': GoogleNativeTtsProvider, | 129 | 'Google Gemini TTS': GoogleNativeTtsProvider, |
| 128 | GSVI: GSVITtsProvider, | 130 | GSVI: GSVITtsProvider, |
| @@ -19,9 +19,9 @@ class MiniMaxTtsProvider { | |||
| 19 | apiHost: 'https://api.minimax.io', | 19 | apiHost: 'https://api.minimax.io', |
| 20 | model: 'speech-02-hd', | 20 | model: 'speech-02-hd', |
| 21 | voiceMap: {}, | 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 | audioSampleRate: 32000, | 25 | audioSampleRate: 32000, |
| 26 | bitrate: 128000, | 26 | bitrate: 128000, |
| 27 | format: 'mp3', | 27 | format: 'mp3', |
| @@ -84,15 +84,15 @@ class MiniMaxTtsProvider { | |||
| 84 | 84 | ||
| 85 | <div class="tts_block"> | 85 | <div class="tts_block"> |
| 86 | <label for="minimax_tts_speed">Speed: <span id="minimax_tts_speed_output"></span></label> | 86 | <label for="minimax_tts_speed">Speed: <span id="minimax_tts_speed_output"></span></label> |
| 87 | <input id="minimax_tts_speed" type="range" value="${this.defaultSettings.speed}" min="0.5" max="2.0" step="0.1" /> | 87 | <input id="minimax_tts_speed" type="range" value="${this.defaultSettings.speed.default}" min="${this.defaultSettings.speed.min}" max="${this.defaultSettings.speed.max}" step="${this.defaultSettings.speed.step}" /> |
| 88 | </div> | 88 | </div> |
| 89 | <div class="tts_block"> | 89 | <div class="tts_block"> |
| 90 | <label for="minimax_tts_volume">Volume: <span id="minimax_tts_volume_output"></span></label> | 90 | <label for="minimax_tts_volume">Volume: <span id="minimax_tts_volume_output"></span></label> |
| 91 | <input id="minimax_tts_volume" type="range" value="${this.defaultSettings.volume}" min="0.1" max="2.0" step="0.1" /> | 91 | <input id="minimax_tts_volume" type="range" value="${this.defaultSettings.volume.default}" min="${this.defaultSettings.volume.min}" max="${this.defaultSettings.volume.max}" step="${this.defaultSettings.volume.step}" /> |
| 92 | </div> | 92 | </div> |
| 93 | <div class="tts_block"> | 93 | <div class="tts_block"> |
| 94 | <label for="minimax_tts_pitch">Pitch: <span id="minimax_tts_pitch_output"></span></label> | 94 | <label for="minimax_tts_pitch">Pitch: <span id="minimax_tts_pitch_output"></span></label> |
| 95 | <input id="minimax_tts_pitch" type="range" value="${this.defaultSettings.pitch}" min="0.5" max="2.0" step="0.1" /> | 95 | <input id="minimax_tts_pitch" type="range" value="${this.defaultSettings.pitch.default}" min="${this.defaultSettings.pitch.min}" max="${this.defaultSettings.pitch.max}" step="${this.defaultSettings.pitch.step}" /> |
| 96 | </div> | 96 | </div> |
| 97 | <div class="tts_block"> | 97 | <div class="tts_block"> |
| 98 | <label for="minimax_tts_format">Audio Format</label> | 98 | <label for="minimax_tts_format">Audio Format</label> |
| @@ -190,14 +190,14 @@ class MiniMaxTtsProvider { | |||
| 190 | this.settings.apiHost = $('#minimax_tts_api_host').val(); | 190 | this.settings.apiHost = $('#minimax_tts_api_host').val(); |
| 191 | this.settings.speed = parseFloat($('#minimax_tts_speed').val().toString()); | 191 | this.settings.speed = parseFloat($('#minimax_tts_speed').val().toString()); |
| 192 | this.settings.volume = parseFloat($('#minimax_tts_volume').val().toString()); | 192 | this.settings.volume = parseFloat($('#minimax_tts_volume').val().toString()); |
| 193 | this.settings.pitch = parseFloat($('#minimax_tts_pitch').val().toString()); | 193 | this.settings.pitch = parseInt($('#minimax_tts_pitch').val().toString()); |
| 194 | this.settings.model = $('#minimax_tts_model').find(':selected').val(); | 194 | this.settings.model = $('#minimax_tts_model').find(':selected').val(); |
| 195 | this.settings.format = $('#minimax_tts_format').find(':selected').val(); | 195 | this.settings.format = $('#minimax_tts_format').find(':selected').val(); |
| 196 | this.settings.customVoiceId = $('#minimax_tts_custom_voice_id').val(); | 196 | this.settings.customVoiceId = $('#minimax_tts_custom_voice_id').val(); |
| 197 | 197 | ||
| 198 | $('#minimax_tts_speed_output').text(this.settings.speed.toFixed(1)); | 198 | $('#minimax_tts_speed_output').text(this.settings.speed.toFixed(1)); |
| 199 | $('#minimax_tts_volume_output').text(this.settings.volume.toFixed(1)); | 199 | $('#minimax_tts_volume_output').text(this.settings.volume.toFixed(1)); |
| 200 | $('#minimax_tts_pitch_output').text(this.settings.pitch.toFixed(1)); | 200 | $('#minimax_tts_pitch_output').text(this.settings.pitch); |
| 201 | 201 | ||
| 202 | saveTtsProviderSettings(); | 202 | saveTtsProviderSettings(); |
| 203 | } | 203 | } |
| @@ -458,6 +458,16 @@ class MiniMaxTtsProvider { | |||
| 458 | // Only accept keys defined in defaultSettings | 458 | // Only accept keys defined in defaultSettings |
| 459 | this.settings = { ...this.defaultSettings }; | 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 | for (const key in settings) { | 471 | for (const key in settings) { |
| 462 | if (key in this.settings) { | 472 | if (key in this.settings) { |
| 463 | this.settings[key] = settings[key]; | 473 | this.settings[key] = settings[key]; |
| @@ -470,6 +480,21 @@ class MiniMaxTtsProvider { | |||
| 470 | if (!this.settings.customModels) this.settings.customModels = []; | 480 | if (!this.settings.customModels) this.settings.customModels = []; |
| 471 | if (!this.settings.customVoices) this.settings.customVoices = []; | 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 | $('#minimax_tts_api_host').val(this.settings.apiHost || 'https://api.minimax.io'); | 498 | $('#minimax_tts_api_host').val(this.settings.apiHost || 'https://api.minimax.io'); |
| 474 | $('#minimax_tts_model').val(this.settings.model); | 499 | $('#minimax_tts_model').val(this.settings.model); |
| 475 | $('#minimax_tts_speed').val(this.settings.speed); | 500 | $('#minimax_tts_speed').val(this.settings.speed); |
| @@ -546,7 +571,7 @@ class MiniMaxTtsProvider { | |||
| 546 | 571 | ||
| 547 | $('#minimax_tts_speed_output').text(this.settings.speed.toFixed(1)); | 572 | $('#minimax_tts_speed_output').text(this.settings.speed.toFixed(1)); |
| 548 | $('#minimax_tts_volume_output').text(this.settings.volume.toFixed(1)); | 573 | $('#minimax_tts_volume_output').text(this.settings.volume.toFixed(1)); |
| 549 | $('#minimax_tts_pitch_output').text(this.settings.pitch.toFixed(1)); | 574 | $('#minimax_tts_pitch_output').text(this.settings.pitch); |
| 550 | 575 | ||
| 551 | // Initialize custom configuration display | 576 | // Initialize custom configuration display |
| 552 | this.updateCustomModelsDisplay(); | 577 | this.updateCustomModelsDisplay(); |
| @@ -756,17 +781,20 @@ class MiniMaxTtsProvider { | |||
| 756 | throw error; | 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 | const requestBody = { | 787 | const requestBody = { |
| 760 | text: inputText, | 788 | text: inputText, |
| 761 | voiceId: voiceId, | 789 | voiceId: voiceId, |
| 762 | apiHost: this.settings.apiHost, | 790 | apiHost: this.settings.apiHost, |
| 763 | model: this.settings.model || 'speech-02-hd', | 791 | model: this.settings.model || this.defaultSettings.model, |
| 764 | speed: Number(this.settings.speed) || 1.0, | 792 | speed: clamp(Number(this.settings.speed) || this.defaultSettings.speed.default, this.defaultSettings.speed.min, this.defaultSettings.speed.max), |
| 765 | volume: Number(this.settings.volume) || 1.0, | 793 | volume: clamp(Number(this.settings.volume) || this.defaultSettings.volume.default, this.defaultSettings.volume.min, this.defaultSettings.volume.max), |
| 766 | pitch: Number(this.settings.pitch) || 1.0, | 794 | pitch: clamp(Math.round(Number(this.settings.pitch)) || this.defaultSettings.pitch.default, this.defaultSettings.pitch.min, this.defaultSettings.pitch.max), |
| 767 | audioSampleRate: Number(this.settings.audioSampleRate) || 32000, | 795 | audioSampleRate: Number(this.settings.audioSampleRate) || this.defaultSettings.audioSampleRate, |
| 768 | bitrate: Number(this.settings.bitrate) || 128000, | 796 | bitrate: Number(this.settings.bitrate) || this.defaultSettings.bitrate, |
| 769 | format: this.settings.format || 'mp3', | 797 | format: this.settings.format || this.defaultSettings.format, |
| 770 | language: language, | 798 | language: language, |
| 771 | }; | 799 | }; |
| 772 | 800 | ||
| @@ -12,7 +12,7 @@ | |||
| 12 | <div class="tts_block"> | 12 | <div class="tts_block"> |
| 13 | <select id="tts_provider" class="flex1"> | 13 | <select id="tts_provider" class="flex1"> |
| 14 | </select> | 14 | </select> |
| 15 | <input id="tts_refresh" class="menu_button" type="submit" value="Reload" /> | 15 | <input id="tts_refresh" data-i18n="[value]tts_refresh" class="menu_button" type="submit" value="Reload" /> |
| 16 | </div> | 16 | </div> |
| 17 | <div> | 17 | <div> |
| 18 | <label class="checkbox_label" for="tts_enabled"> | 18 | <label class="checkbox_label" for="tts_enabled"> |
| @@ -88,7 +88,7 @@ | |||
| 88 | <form id="tts_provider_settings"> | 88 | <form id="tts_provider_settings"> |
| 89 | </form> | 89 | </form> |
| 90 | <div class="tts_buttons"> | 90 | <div class="tts_buttons"> |
| 91 | <input id="tts_voices" class="menu_button" type="submit" value="Available voices" /> | 91 | <input id="tts_voices" class="menu_button" data-i18n="[value]Available voices" type="submit" value="Available voices" /> |
| 92 | </div> | 92 | </div> |
| 93 | </div> | 93 | </div> |
| 94 | </div> | 94 | </div> |
| @@ -2,6 +2,7 @@ import { isMobile } from '../../RossAscends-mods.js'; | |||
| 2 | import { getPreviewString } from './index.js'; | 2 | import { getPreviewString } from './index.js'; |
| 3 | import { saveTtsProviderSettings } from './index.js'; | 3 | import { saveTtsProviderSettings } from './index.js'; |
| 4 | export { SystemTtsProvider }; | 4 | export { SystemTtsProvider }; |
| 5 | import { t } from '../../i18n.js'; | ||
| 5 | 6 | ||
| 6 | /** | 7 | /** |
| 7 | * Chunkify | 8 | * Chunkify |
| @@ -96,13 +97,13 @@ class SystemTtsProvider { | |||
| 96 | 97 | ||
| 97 | get settingsHtml() { | 98 | get settingsHtml() { |
| 98 | if (!('speechSynthesis' in window)) { | 99 | if (!('speechSynthesis' in window)) { |
| 99 | return 'Your browser or operating system doesn\'t support speech synthesis'; | 100 | return t`Your browser or operating system doesn't support speech synthesis`; |
| 100 | } | 101 | } |
| 101 | 102 | ||
| 102 | return `<p>Uses the voices provided by your operating system</p> | 103 | return '<p>' + t`Uses the voices provided by your operating system` + `</p> |
| 103 | <label for="system_tts_rate">Rate: <span id="system_tts_rate_output"></span></label> | 104 | <label for="system_tts_rate">` + t`Rate:` + ` <span id="system_tts_rate_output"></span></label> |
| 104 | <input id="system_tts_rate" type="range" value="${this.defaultSettings.rate}" min="0.1" max="2" step="0.01" /> | 105 | <input id="system_tts_rate" type="range" value="${this.defaultSettings.rate}" min="0.1" max="2" step="0.01" /> |
| 105 | <label for="system_tts_pitch">Pitch: <span id="system_tts_pitch_output"></span></label> | 106 | <label for="system_tts_pitch">` + t`Pitch:` + ` <span id="system_tts_pitch_output"></span></label> |
| 106 | <input id="system_tts_pitch" type="range" value="${this.defaultSettings.pitch}" min="0" max="2" step="0.01" />`; | 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 | ollama_keep: false, | 66 | ollama_keep: false, |
| 67 | vllm_model: '', | 67 | vllm_model: '', |
| 68 | webllm_model: '', | 68 | webllm_model: '', |
| 69 | google_model: 'text-embedding-004', | 69 | google_model: 'text-embedding-005', |
| 70 | summarize: false, | 70 | summarize: false, |
| 71 | summarize_sent: false, | 71 | summarize_sent: false, |
| 72 | summary_source: 'main', | 72 | summary_source: 'main', |
| @@ -140,6 +140,7 @@ | |||
| 140 | <option value="gemini-embedding-001">gemini-embedding-001</option> | 140 | <option value="gemini-embedding-001">gemini-embedding-001</option> |
| 141 | <option value="gemini-embedding-exp-03-07">gemini-embedding-exp-03-07</option> | 141 | <option value="gemini-embedding-exp-03-07">gemini-embedding-exp-03-07</option> |
| 142 | <option value="text-embedding-004">text-embedding-004</option> | 142 | <option value="text-embedding-004">text-embedding-004</option> |
| 143 | <option value="text-embedding-005">text-embedding-005</option> | ||
| 143 | <option value="embedding-001">embedding-001</option> | 144 | <option value="embedding-001">embedding-001</option> |
| 144 | </select> | 145 | </select> |
| 145 | </div> | 146 | </div> |
| @@ -76,6 +76,7 @@ import { | |||
| 76 | depth_prompt_role_default, | 76 | depth_prompt_role_default, |
| 77 | shouldAutoContinue, | 77 | shouldAutoContinue, |
| 78 | unshallowCharacter, | 78 | unshallowCharacter, |
| 79 | chatElement, | ||
| 79 | } from '../script.js'; | 80 | } from '../script.js'; |
| 80 | import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js'; | 81 | import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js'; |
| 81 | import { FILTER_TYPES, FilterHelper } from './filters.js'; | 82 | import { FILTER_TYPES, FilterHelper } from './filters.js'; |
| @@ -236,17 +237,13 @@ export async function getGroupChat(groupId, reload = false) { | |||
| 236 | const chat_id = group.chat_id; | 237 | const chat_id = group.chat_id; |
| 237 | const data = await loadGroupChat(chat_id); | 238 | const data = await loadGroupChat(chat_id); |
| 238 | const metadata = group.chat_metadata ?? {}; | 239 | const metadata = group.chat_metadata ?? {}; |
| 239 | let freshChat = false; | 240 | const freshChat = !metadata.tainted; |
| 240 | 241 | ||
| 241 | await loadItemizedPrompts(getCurrentChatId()); | 242 | await loadItemizedPrompts(getCurrentChatId()); |
| 242 | 243 | ||
| 243 | if (Array.isArray(data) && data.length) { | ||
| 244 | data[0].is_group = true; | ||
| 245 | chat.splice(0, chat.length, ...data); | ||
| 246 | await printMessages(); | ||
| 247 | } else { | ||
| 248 | freshChat = !metadata.tainted; | ||
| 249 | if (group && Array.isArray(group.members) && freshChat) { | 244 | if (group && Array.isArray(group.members) && freshChat) { |
| 245 | chat.splice(0, chat.length); | ||
| 246 | chatElement.find('.mes').remove(); | ||
| 250 | for (let member of group.members) { | 247 | for (let member of group.members) { |
| 251 | const character = characters.find(x => x.avatar === member || x.name === member); | 248 | const character = characters.find(x => x.avatar === member || x.name === member); |
| 252 | if (!character) { | 249 | if (!character) { |
| @@ -266,7 +263,11 @@ export async function getGroupChat(groupId, reload = false) { | |||
| 266 | await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1), 'first_message'); | 263 | await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1), 'first_message'); |
| 267 | } | 264 | } |
| 268 | await saveGroupChat(groupId, false); | 265 | await saveGroupChat(groupId, false); |
| 269 | } | 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 | updateChatMetadata(metadata, true); | 273 | updateChatMetadata(metadata, true); |
| @@ -2018,7 +2019,14 @@ export async function deleteGroupChatByName(groupId, chatName) { | |||
| 2018 | await eventSource.emit(event_types.GROUP_CHAT_DELETED, chatName); | 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 | const group = groups.find(x => x.id === groupId); | 2030 | const group = groups.find(x => x.id === groupId); |
| 2023 | 2031 | ||
| 2024 | if (!group || !group.chats.includes(chatId)) { | 2032 | if (!group || !group.chats.includes(chatId)) { |
| @@ -2026,10 +2034,13 @@ export async function deleteGroupChat(groupId, chatId) { | |||
| 2026 | } | 2034 | } |
| 2027 | 2035 | ||
| 2028 | group.chats.splice(group.chats.indexOf(chatId), 1); | 2036 | group.chats.splice(group.chats.indexOf(chatId), 1); |
| 2029 | group.chat_metadata = {}; | ||
| 2030 | group.chat_id = ''; | ||
| 2031 | delete group.past_metadata[chatId]; | 2037 | delete group.past_metadata[chatId]; |
| 2038 | |||
| 2039 | if (group.chat_id === chatId) { | ||
| 2040 | group.chat_id = ''; | ||
| 2041 | group.chat_metadata = {}; | ||
| 2032 | updateChatMetadata(group.chat_metadata, true); | 2042 | updateChatMetadata(group.chat_metadata, true); |
| 2043 | } | ||
| 2033 | 2044 | ||
| 2034 | const response = await fetch('/api/chats/group/delete', { | 2045 | const response = await fetch('/api/chats/group/delete', { |
| 2035 | method: 'POST', | 2046 | method: 'POST', |
| @@ -2038,11 +2049,13 @@ export async function deleteGroupChat(groupId, chatId) { | |||
| 2038 | }); | 2049 | }); |
| 2039 | 2050 | ||
| 2040 | if (response.ok) { | 2051 | if (response.ok) { |
| 2052 | if (jumpToNewChat) { | ||
| 2041 | if (group.chats.length) { | 2053 | if (group.chats.length) { |
| 2042 | await openGroupChat(groupId, group.chats[group.chats.length - 1]); | 2054 | await openGroupChat(groupId, group.chats[group.chats.length - 1]); |
| 2043 | } else { | 2055 | } else { |
| 2044 | await createNewGroupChat(groupId); | 2056 | await createNewGroupChat(groupId); |
| 2045 | } | 2057 | } |
| 2058 | } | ||
| 2046 | 2059 | ||
| 2047 | await eventSource.emit(event_types.GROUP_CHAT_DELETED, chatId); | 2060 | await eventSource.emit(event_types.GROUP_CHAT_DELETED, chatId); |
| 2048 | } | 2061 | } |
| @@ -2051,9 +2064,11 @@ export async function deleteGroupChat(groupId, chatId) { | |||
| 2051 | /** | 2064 | /** |
| 2052 | * Imports a group chat from a file and adds it to the group. | 2065 | * Imports a group chat from a file and adds it to the group. |
| 2053 | * @param {FormData} formData Form data to send to the server | 2066 | * @param {FormData} formData Form data to send to the server |
| 2054 | * @param {EventTarget} eventTarget Element that triggered the import | 2067 | * @param {object} [options={}] Options for 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 | export async function importGroupChat(formData, eventTarget) { | 2071 | export async function importGroupChat(formData, { refresh = true } = {}) { |
| 2057 | const fetchResult = await fetch('/api/chats/group/import', { | 2072 | const fetchResult = await fetch('/api/chats/group/import', { |
| 2058 | method: 'POST', | 2073 | method: 'POST', |
| 2059 | headers: getRequestHeaders({ omitContentType: true }), | 2074 | headers: getRequestHeaders({ omitContentType: true }), |
| @@ -2070,14 +2085,16 @@ export async function importGroupChat(formData, eventTarget) { | |||
| 2070 | if (group) { | 2085 | if (group) { |
| 2071 | group.chats.push(chatId); | 2086 | group.chats.push(chatId); |
| 2072 | await editGroup(selected_group, true, true); | 2087 | await editGroup(selected_group, true, true); |
| 2088 | if (refresh) { | ||
| 2073 | await displayPastChats(); | 2089 | await displayPastChats(); |
| 2074 | } | 2090 | } |
| 2075 | } | 2091 | } |
| 2076 | } | 2092 | } |
| 2077 | 2093 | ||
| 2078 | if (eventTarget instanceof HTMLInputElement) { | 2094 | return data?.fileNames || []; |
| 2079 | eventTarget.value = ''; | ||
| 2080 | } | 2095 | } |
| 2096 | |||
| 2097 | return []; | ||
| 2081 | } | 2098 | } |
| 2082 | 2099 | ||
| 2083 | export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) { | 2100 | export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) { |
| @@ -2147,7 +2164,7 @@ function doCurMemberListPopout() { | |||
| 2147 | // Remove pagination from popout | 2164 | // Remove pagination from popout |
| 2148 | newElement.find('.group_pagination').empty(); | 2165 | newElement.find('.group_pagination').empty(); |
| 2149 | 2166 | ||
| 2150 | $('body').append(newElement); | 2167 | $('#movingDivs').append(newElement); |
| 2151 | loadMovingUIState(); | 2168 | loadMovingUIState(); |
| 2152 | $('#groupMemberListPopout').fadeIn(animation_duration); | 2169 | $('#groupMemberListPopout').fadeIn(animation_duration); |
| 2153 | dragElement(newElement); | 2170 | dragElement(newElement); |
| @@ -1,10 +1,11 @@ | |||
| 1 | import { Handlebars, moment, seedrandom, droll } from '../lib.js'; | 1 | import { Handlebars, moment, seedrandom, droll } from '../lib.js'; |
| 2 | import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams, eventSource, event_types } from '../script.js'; | 2 | import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams, eventSource, event_types, extension_prompts } from '../script.js'; |
| 3 | import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } from './utils.js'; | 3 | import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } from './utils.js'; |
| 4 | import { textgenerationwebui_banned_in_macros } from './textgen-settings.js'; | 4 | import { textgenerationwebui_banned_in_macros } from './textgen-settings.js'; |
| 5 | import { getInstructMacros } from './instruct-mode.js'; | 5 | import { getInstructMacros } from './instruct-mode.js'; |
| 6 | import { getVariableMacros } from './variables.js'; | 6 | import { getVariableMacros } from './variables.js'; |
| 7 | import { isMobile } from './RossAscends-mods.js'; | 7 | import { isMobile } from './RossAscends-mods.js'; |
| 8 | import { inject_ids } from './constants.js'; | ||
| 8 | 9 | ||
| 9 | /** | 10 | /** |
| 10 | * @typedef Macro | 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 | * Registers a global macro that can be used anywhere where substitution is allowed. | 78 | * Registers a global macro that can be used anywhere where substitution is allowed. |
| 60 | * @param {string} key Macro name (key) | 79 | * @param {string} key Macro name (key) |
| 61 | * @param {string|MacroFunction} value A string or a function that returns a string | 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 | * Substitutes {{macro}} parameters in a string. | 491 | * Substitutes {{macro}} parameters in a string. |
| 463 | * @param {string} content - The string to substitute parameters in. | 492 | * @param {string} content - The string to substitute parameters in. |
| 464 | * @param {EnvObject} env - Map of macro names to the values they'll be substituted with. If the param | 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 | { regex: /{{datetimeformat +([^}]*)}}/gi, replace: (_, format) => moment().format(format) }, | 547 | { regex: /{{datetimeformat +([^}]*)}}/gi, replace: (_, format) => moment().format(format) }, |
| 519 | { regex: /{{idle_duration}}/gi, replace: () => getTimeSinceLastMessage() }, | 548 | { regex: /{{idle_duration}}/gi, replace: () => getTimeSinceLastMessage() }, |
| 520 | { regex: /{{time_UTC([-+]\d+)}}/gi, replace: (_, offset) => moment().utc().utcOffset(parseInt(offset, 10)).format('LT') }, | 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 | getTimeDiffMacro(), | 551 | getTimeDiffMacro(), |
| 522 | getBannedWordsMacro(), | 552 | getBannedWordsMacro(), |
| 523 | getRandomReplaceMacro(), | 553 | getRandomReplaceMacro(), |
| @@ -276,6 +276,8 @@ export const settingsToUpdate = { | |||
| 276 | perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true], | 276 | perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true], |
| 277 | groq_model: ['#model_groq_select', 'groq_model', false, true], | 277 | groq_model: ['#model_groq_select', 'groq_model', false, true], |
| 278 | electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true], | 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 | nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true], | 281 | nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true], |
| 280 | deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true], | 282 | deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true], |
| 281 | aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true], | 283 | aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true], |
| @@ -370,15 +372,17 @@ const default_settings = { | |||
| 370 | scenario_format: default_scenario_format, | 372 | scenario_format: default_scenario_format, |
| 371 | personality_format: default_personality_format, | 373 | personality_format: default_personality_format, |
| 372 | openai_model: 'gpt-4-turbo', | 374 | openai_model: 'gpt-4-turbo', |
| 373 | claude_model: 'claude-3-5-sonnet-20240620', | 375 | claude_model: 'claude-sonnet-4-5', |
| 374 | google_model: 'gemini-1.5-pro', | 376 | google_model: 'gemini-2.5-pro', |
| 375 | vertexai_model: 'gemini-2.0-flash-001', | 377 | vertexai_model: 'gemini-2.5-pro', |
| 376 | ai21_model: 'jamba-large', | 378 | ai21_model: 'jamba-large', |
| 377 | mistralai_model: 'mistral-large-latest', | 379 | mistralai_model: 'mistral-large-latest', |
| 378 | cohere_model: 'command-r-plus', | 380 | cohere_model: 'command-r-plus', |
| 379 | perplexity_model: 'sonar-pro', | 381 | perplexity_model: 'sonar-pro', |
| 380 | groq_model: 'llama-3.3-70b-versatile', | 382 | groq_model: 'llama-3.3-70b-versatile', |
| 381 | electronhub_model: 'gpt-4o-mini', | 383 | electronhub_model: 'gpt-4o-mini', |
| 384 | electronhub_sort_models: 'alphabetically', | ||
| 385 | electronhub_group_models: false, | ||
| 382 | nanogpt_model: 'gpt-4o-mini', | 386 | nanogpt_model: 'gpt-4o-mini', |
| 383 | deepseek_model: 'deepseek-chat', | 387 | deepseek_model: 'deepseek-chat', |
| 384 | aimlapi_model: 'gpt-4o-mini-2024-07-18', | 388 | aimlapi_model: 'gpt-4o-mini-2024-07-18', |
| @@ -464,15 +468,17 @@ const oai_settings = { | |||
| 464 | scenario_format: default_scenario_format, | 468 | scenario_format: default_scenario_format, |
| 465 | personality_format: default_personality_format, | 469 | personality_format: default_personality_format, |
| 466 | openai_model: 'gpt-4-turbo', | 470 | openai_model: 'gpt-4-turbo', |
| 467 | claude_model: 'claude-3-5-sonnet-20240620', | 471 | claude_model: 'claude-sonnet-4-5', |
| 468 | google_model: 'gemini-1.5-pro', | 472 | google_model: 'gemini-2.5-pro', |
| 469 | vertexai_model: 'gemini-2.0-flash-001', | 473 | vertexai_model: 'gemini-2.5-pro', |
| 470 | ai21_model: 'jamba-large', | 474 | ai21_model: 'jamba-large', |
| 471 | mistralai_model: 'mistral-large-latest', | 475 | mistralai_model: 'mistral-large-latest', |
| 472 | cohere_model: 'command-r-plus', | 476 | cohere_model: 'command-r-plus', |
| 473 | perplexity_model: 'sonar-pro', | 477 | perplexity_model: 'sonar-pro', |
| 474 | groq_model: 'llama-3.1-70b-versatile', | 478 | groq_model: 'llama-3.1-70b-versatile', |
| 475 | electronhub_model: 'gpt-4o-mini', | 479 | electronhub_model: 'gpt-4o-mini', |
| 480 | electronhub_sort_models: 'alphabetically', | ||
| 481 | electronhub_group_models: false, | ||
| 476 | nanogpt_model: 'gpt-4o-mini', | 482 | nanogpt_model: 'gpt-4o-mini', |
| 477 | deepseek_model: 'deepseek-chat', | 483 | deepseek_model: 'deepseek-chat', |
| 478 | aimlapi_model: 'gpt-4-turbo', | 484 | aimlapi_model: 'gpt-4-turbo', |
| @@ -1710,6 +1716,59 @@ function calculateOpenRouterCost() { | |||
| 1710 | $('#openrouter_max_prompt_cost').text(cost); | 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 | function saveModelList(data) { | 1772 | function saveModelList(data) { |
| 1714 | model_list = data.map((model) => ({ ...model })); | 1773 | model_list = data.map((model) => ({ ...model })); |
| 1715 | model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id)); | 1774 | model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id)); |
| @@ -1775,22 +1834,10 @@ function saveModelList(data) { | |||
| 1775 | } | 1834 | } |
| 1776 | 1835 | ||
| 1777 | if (oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) { | 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 | 1838 | ||
| 1789 | const otherOptionsGroup = mistralModelSelect.querySelector('#mistralai_other_models'); | ||
| 1790 | for (const model of model_list.filter(model => model?.capabilities?.completion_chat)) { | 1839 | for (const model of model_list.filter(model => model?.capabilities?.completion_chat)) { |
| 1791 | if (!options.some(option => option.value === model.id) && otherOptionsGroup) { | 1840 | $('#model_mistralai_select').append(new Option(model.id, model.id)); |
| 1792 | otherOptionsGroup.append(new Option(model.id, model.id)); | ||
| 1793 | } | ||
| 1794 | } | 1841 | } |
| 1795 | 1842 | ||
| 1796 | const selectedModel = model_list.find(model => model.id === oai_settings.mistralai_model); | 1843 | const selectedModel = model_list.find(model => model.id === oai_settings.mistralai_model); |
| @@ -1800,19 +1847,16 @@ function saveModelList(data) { | |||
| 1800 | 1847 | ||
| 1801 | $('#model_mistralai_select').val(oai_settings.mistralai_model).trigger('change'); | 1848 | $('#model_mistralai_select').val(oai_settings.mistralai_model).trigger('change'); |
| 1802 | } | 1849 | } |
| 1803 | } | ||
| 1804 | 1850 | ||
| 1805 | if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) { | 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 | $('#model_electronhub_select').empty(); | 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 | const selectedModel = model_list.find(model => model.id === oai_settings.electronhub_model); | 1861 | const selectedModel = model_list.find(model => model.id === oai_settings.electronhub_model); |
| 1818 | if (model_list.length > 0 && (!selectedModel || !oai_settings.electronhub_model)) { | 1862 | if (model_list.length > 0 && (!selectedModel || !oai_settings.electronhub_model)) { |
| @@ -1984,6 +2028,24 @@ function saveModelList(data) { | |||
| 1984 | .append(new Option(modelId || 'None', modelId || '', true, true)) | 2028 | .append(new Option(modelId || 'None', modelId || '', true, true)) |
| 1985 | .trigger('change'); | 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 | function appendOpenRouterOptions(model_list, groupModels = false, sort = false) { | 2051 | function appendOpenRouterOptions(model_list, groupModels = false, sort = false) { |
| @@ -2041,6 +2103,61 @@ function openRouterGroupByVendor(array) { | |||
| 2041 | }, new Map()); | 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 | function aimlapiGroupByVendor(array) { | 2161 | function aimlapiGroupByVendor(array) { |
| 2045 | return array.reduce((acc, curr) => { | 2162 | return array.reduce((acc, curr) => { |
| 2046 | const vendor = curr.info.developer; | 2163 | const vendor = curr.info.developer; |
| @@ -2191,7 +2308,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } = | |||
| 2191 | const useLogprobs = !!power_user.request_token_probabilities; | 2308 | const useLogprobs = !!power_user.request_token_probabilities; |
| 2192 | const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isAzureOpenAI || isCustom || isXAI || isAimlapi || isMoonshot); | 2309 | const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isAzureOpenAI || isCustom || isXAI || isAimlapi || isMoonshot); |
| 2193 | 2310 | ||
| 2194 | const logitBiasSources = [chat_completion_sources.OPENAI, chat_completion_sources.AZURE_OPENAI, chat_completion_sources.OPENROUTER, chat_completion_sources.CUSTOM]; | 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 | if (oai_settings.bias_preset_selected | 2312 | if (oai_settings.bias_preset_selected |
| 2196 | && logitBiasSources.includes(oai_settings.chat_completion_source) | 2313 | && logitBiasSources.includes(oai_settings.chat_completion_source) |
| 2197 | && Array.isArray(oai_settings.bias_presets[oai_settings.bias_preset_selected]) | 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 | if (isXAI) { | 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 | delete generate_data.presence_penalty; | 2475 | delete generate_data.presence_penalty; |
| 2358 | delete generate_data.frequency_penalty; | 2476 | delete generate_data.frequency_penalty; |
| 2359 | delete generate_data.stop; | 2477 | delete generate_data.stop; |
| 2478 | } else { | ||
| 2479 | // As of 2025/09/21, only grok-3-mini accepts reasoning_effort | ||
| 2360 | delete generate_data.reasoning_effort; | 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 | delete generate_data.presence_penalty; | 2484 | delete generate_data.presence_penalty; |
| 2364 | delete generate_data.frequency_penalty; | 2485 | delete generate_data.frequency_penalty; |
| 2486 | |||
| 2487 | // grok-4-fast-non-reasoning accepts stop | ||
| 2488 | if (!model.includes('grok-4-fast-non-reasoning')) { | ||
| 2489 | delete generate_data.stop; | ||
| 2365 | } | 2490 | } |
| 2366 | if (generate_data.model.includes('grok-vision')) { | ||
| 2367 | delete generate_data.tools; | ||
| 2368 | delete generate_data.tool_choice; | ||
| 2369 | } | 2491 | } |
| 2370 | } | 2492 | } |
| 2371 | 2493 | ||
| @@ -2559,7 +2681,7 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov | |||
| 2559 | state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || ''); | 2681 | state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || ''); |
| 2560 | } | 2682 | } |
| 2561 | return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? ''; | 2683 | return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? ''; |
| 2562 | } 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].includes(chat_completion_source)) { | 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 | if (show_thoughts) { | 2685 | if (show_thoughts) { |
| 2564 | state.reasoning += | 2686 | state.reasoning += |
| 2565 | data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ?? | 2687 | data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ?? |
| @@ -3523,6 +3645,8 @@ function loadOpenAISettings(data, settings) { | |||
| 3523 | oai_settings.perplexity_model = settings.perplexity_model ?? default_settings.perplexity_model; | 3645 | oai_settings.perplexity_model = settings.perplexity_model ?? default_settings.perplexity_model; |
| 3524 | oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model; | 3646 | oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model; |
| 3525 | oai_settings.electronhub_model = settings.electronhub_model ?? default_settings.electronhub_model; | 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 | oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model; | 3650 | oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model; |
| 3527 | oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model; | 3651 | oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model; |
| 3528 | oai_settings.aimlapi_model = settings.aimlapi_model ?? default_settings.aimlapi_model; | 3652 | oai_settings.aimlapi_model = settings.aimlapi_model ?? default_settings.aimlapi_model; |
| @@ -3666,6 +3790,8 @@ function loadOpenAISettings(data, settings) { | |||
| 3666 | $('#openrouter_allow_fallbacks').prop('checked', oai_settings.openrouter_allow_fallbacks); | 3790 | $('#openrouter_allow_fallbacks').prop('checked', oai_settings.openrouter_allow_fallbacks); |
| 3667 | $('#openrouter_providers_chat').val(oai_settings.openrouter_providers).trigger('change'); | 3791 | $('#openrouter_providers_chat').val(oai_settings.openrouter_providers).trigger('change'); |
| 3668 | $('#openrouter_middleout').val(oai_settings.openrouter_middleout); | 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 | $('#squash_system_messages').prop('checked', oai_settings.squash_system_messages); | 3795 | $('#squash_system_messages').prop('checked', oai_settings.squash_system_messages); |
| 3670 | $('#continue_prefill').prop('checked', oai_settings.continue_prefill); | 3796 | $('#continue_prefill').prop('checked', oai_settings.continue_prefill); |
| 3671 | $('#openai_function_calling').prop('checked', oai_settings.function_calling); | 3797 | $('#openai_function_calling').prop('checked', oai_settings.function_calling); |
| @@ -3928,6 +4054,8 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { | |||
| 3928 | pollinations_model: settings.pollinations_model, | 4054 | pollinations_model: settings.pollinations_model, |
| 3929 | aimlapi_model: settings.aimlapi_model, | 4055 | aimlapi_model: settings.aimlapi_model, |
| 3930 | electronhub_model: settings.electronhub_model, | 4056 | electronhub_model: settings.electronhub_model, |
| 4057 | electronhub_sort_models: settings.electronhub_sort_models, | ||
| 4058 | electronhub_group_models: settings.electronhub_group_models, | ||
| 3931 | moonshot_model: settings.moonshot_model, | 4059 | moonshot_model: settings.moonshot_model, |
| 3932 | fireworks_model: settings.fireworks_model, | 4060 | fireworks_model: settings.fireworks_model, |
| 3933 | cometapi_model: settings.cometapi_model, | 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 | // Return context size if model found, otherwise default to 32k | 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 | if ($(this).is('#model_xai_select')) { | 4941 | if ($(this).is('#model_xai_select')) { |
| 4942 | if (!value) { | ||
| 4943 | console.debug('Null XAI model selected. Ignoring.'); | ||
| 4944 | return; | ||
| 4945 | } | ||
| 4881 | console.log('XAI model changed to', value); | 4946 | console.log('XAI model changed to', value); |
| 4882 | oai_settings.xai_model = value; | 4947 | oai_settings.xai_model = value; |
| 4883 | } | 4948 | } |
| @@ -4916,17 +4981,15 @@ async function onModelChange() { | |||
| 4916 | if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(oai_settings.chat_completion_source)) { | 4981 | if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(oai_settings.chat_completion_source)) { |
| 4917 | if (oai_settings.max_context_unlocked) { | 4982 | if (oai_settings.max_context_unlocked) { |
| 4918 | $('#openai_max_context').attr('max', max_2mil); | 4983 | $('#openai_max_context').attr('max', max_2mil); |
| 4919 | } else if (value.includes('gemini-1.5-pro')) { | 4984 | } else if (value.includes('gemini-2.5-flash-image')) { |
| 4920 | $('#openai_max_context').attr('max', max_2mil); | ||
| 4921 | } else if (value.includes('gemini-2.5-flash-image-preview')) { | ||
| 4922 | $('#openai_max_context').attr('max', max_32k); | 4985 | $('#openai_max_context').attr('max', max_32k); |
| 4923 | } 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')) { | 4986 | } else if (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 | $('#openai_max_context').attr('max', max_1mil); | 4987 | $('#openai_max_context').attr('max', max_1mil); |
| 4925 | } else if (value.includes('gemma-3-27b-it')) { | 4988 | } else if (value.includes('gemma-3-27b-it')) { |
| 4926 | $('#openai_max_context').attr('max', max_128k); | 4989 | $('#openai_max_context').attr('max', max_128k); |
| 4927 | } else if (value.includes('gemma-3n-e4b-it')) { | 4990 | } else if (value.includes('gemma-3n-e4b-it')) { |
| 4928 | $('#openai_max_context').attr('max', max_8k); | 4991 | $('#openai_max_context').attr('max', max_8k); |
| 4929 | } else if (value.includes('gemma-3') || value.includes('learnlm-1.5-pro-experimental')) { | 4992 | } else if (value.includes('gemma-3')) { |
| 4930 | $('#openai_max_context').attr('max', max_32k); | 4993 | $('#openai_max_context').attr('max', max_32k); |
| 4931 | } else { | 4994 | } else { |
| 4932 | $('#openai_max_context').attr('max', max_32k); | 4995 | $('#openai_max_context').attr('max', max_32k); |
| @@ -4966,9 +5029,12 @@ async function onModelChange() { | |||
| 4966 | 5029 | ||
| 4967 | if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) { | 5030 | if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) { |
| 4968 | if (oai_settings.max_context_unlocked) { | 5031 | if (oai_settings.max_context_unlocked) { |
| 4969 | $('#openai_max_context').attr('max', max_200k); | 5032 | $('#openai_max_context').attr('max', unlocked_max); |
| 5033 | } | ||
| 5034 | else if (value.startsWith('claude-sonnet-4-5')) { | ||
| 5035 | $('#openai_max_context').attr('max', max_1mil); | ||
| 4970 | } | 5036 | } |
| 4971 | else if (value == 'claude-2.1' || value.startsWith('claude-3') || value.startsWith('claude-opus') || value.startsWith('claude-sonnet')) { | 5037 | else if (value == 'claude-2.1' || value.startsWith('claude-3') || value.startsWith('claude-opus') || value.startsWith('claude-haiku') || value.startsWith('claude-sonnet')) { |
| 4972 | $('#openai_max_context').attr('max', max_200k); | 5038 | $('#openai_max_context').attr('max', max_200k); |
| 4973 | } | 5039 | } |
| 4974 | else if (value.endsWith('100k') || value.startsWith('claude-2') || value === 'claude-instant-1.2') { | 5040 | else if (value.endsWith('100k') || value.startsWith('claude-2') || value === 'claude-instant-1.2') { |
| @@ -5093,6 +5159,8 @@ async function onModelChange() { | |||
| 5093 | $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input'); | 5159 | $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input'); |
| 5094 | oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai); | 5160 | oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai); |
| 5095 | $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input'); | 5161 | $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input'); |
| 5162 | |||
| 5163 | calculateElectronHubCost(); | ||
| 5096 | } | 5164 | } |
| 5097 | 5165 | ||
| 5098 | if (oai_settings.chat_completion_source === chat_completion_sources.NANOGPT) { | 5166 | if (oai_settings.chat_completion_source === chat_completion_sources.NANOGPT) { |
| @@ -5144,11 +5212,14 @@ async function onModelChange() { | |||
| 5144 | $('#openai_max_context').attr('max', unlocked_max); | 5212 | $('#openai_max_context').attr('max', unlocked_max); |
| 5145 | } else if (oai_settings.xai_model.includes('grok-2-vision')) { | 5213 | } else if (oai_settings.xai_model.includes('grok-2-vision')) { |
| 5146 | $('#openai_max_context').attr('max', max_32k); | 5214 | $('#openai_max_context').attr('max', max_32k); |
| 5147 | } else if (oai_settings.xai_model.includes('grok-vision')) { | 5215 | } else if (oai_settings.xai_model.includes('grok-4-fast')) { |
| 5148 | $('#openai_max_context').attr('max', max_8k); | 5216 | $('#openai_max_context').attr('max', max_2mil); |
| 5149 | } else if (oai_settings.xai_model.includes('grok-4')) { | 5217 | } else if (oai_settings.xai_model.includes('grok-4')) { |
| 5150 | $('#openai_max_context').attr('max', max_256k); | 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 | } else { | 5221 | } else { |
| 5222 | // grok 2 and grok 3 | ||
| 5152 | $('#openai_max_context').attr('max', max_128k); | 5223 | $('#openai_max_context').attr('max', max_128k); |
| 5153 | } | 5224 | } |
| 5154 | 5225 | ||
| @@ -5220,6 +5291,10 @@ async function onOpenrouterModelSortChange() { | |||
| 5220 | await getStatusOpen(); | 5291 | await getStatusOpen(); |
| 5221 | } | 5292 | } |
| 5222 | 5293 | ||
| 5294 | async function onElectronHubModelSortChange() { | ||
| 5295 | await getStatusOpen(); | ||
| 5296 | } | ||
| 5297 | |||
| 5223 | async function onNewPresetClick() { | 5298 | async function onNewPresetClick() { |
| 5224 | 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); | 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 | 'c4ai-aya-vision', | 5752 | 'c4ai-aya-vision', |
| 5678 | 'command-a-vision', | 5753 | 'command-a-vision', |
| 5679 | // Google AI Studio | 5754 | // Google AI Studio |
| 5680 | 'gemini-1.5', | ||
| 5681 | 'gemini-2.0', | 5755 | 'gemini-2.0', |
| 5682 | 'gemini-2.5', | 5756 | 'gemini-2.5', |
| 5683 | 'gemini-exp-1206', | 5757 | 'gemini-exp-1206', |
| 5684 | 'learnlm', | 5758 | 'learnlm', |
| 5759 | 'gemini-robotics', | ||
| 5685 | // MistralAI | 5760 | // MistralAI |
| 5686 | 'mistral-small-2503', | 5761 | 'mistral-small-2503', |
| 5687 | 'mistral-small-2506', | 5762 | 'mistral-small-2506', |
| @@ -5693,7 +5768,6 @@ export function isImageInliningSupported() { | |||
| 5693 | // xAI (Grok) | 5768 | // xAI (Grok) |
| 5694 | 'grok-4', | 5769 | 'grok-4', |
| 5695 | 'grok-2-vision', | 5770 | 'grok-2-vision', |
| 5696 | 'grok-vision', | ||
| 5697 | // Moonshot | 5771 | // Moonshot |
| 5698 | 'moonshot-v1-8k-vision-preview', | 5772 | 'moonshot-v1-8k-vision-preview', |
| 5699 | 'moonshot-v1-32k-vision-preview', | 5773 | 'moonshot-v1-32k-vision-preview', |
| @@ -5726,6 +5800,7 @@ export function isImageInliningSupported() { | |||
| 5726 | case chat_completion_sources.COHERE: | 5800 | case chat_completion_sources.COHERE: |
| 5727 | return visionSupportedModels.some(model => oai_settings.cohere_model.includes(model)); | 5801 | return visionSupportedModels.some(model => oai_settings.cohere_model.includes(model)); |
| 5728 | case chat_completion_sources.XAI: | 5802 | case chat_completion_sources.XAI: |
| 5803 | // TODO: xAI's /models endpoint doesn't return modality info | ||
| 5729 | return visionSupportedModels.some(model => oai_settings.xai_model.includes(model)); | 5804 | return visionSupportedModels.some(model => oai_settings.xai_model.includes(model)); |
| 5730 | case chat_completion_sources.AIMLAPI: | 5805 | case chat_completion_sources.AIMLAPI: |
| 5731 | return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.aimlapi_model)?.features?.includes('openai/chat-completion.vision')); | 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 | oai_settings.openai_max_context = Number($(this).val()); | 6192 | oai_settings.openai_max_context = Number($(this).val()); |
| 6118 | $('#openai_max_context_counter').val(`${$(this).val()}`); | 6193 | $('#openai_max_context_counter').val(`${$(this).val()}`); |
| 6119 | calculateOpenRouterCost(); | 6194 | calculateOpenRouterCost(); |
| 6195 | calculateElectronHubCost(); | ||
| 6120 | saveSettingsDebounced(); | 6196 | saveSettingsDebounced(); |
| 6121 | }); | 6197 | }); |
| 6122 | 6198 | ||
| 6123 | $('#openai_max_tokens').on('input', function () { | 6199 | $('#openai_max_tokens').on('input', function () { |
| 6124 | oai_settings.openai_max_tokens = Number($(this).val()); | 6200 | oai_settings.openai_max_tokens = Number($(this).val()); |
| 6125 | calculateOpenRouterCost(); | 6201 | calculateOpenRouterCost(); |
| 6202 | calculateElectronHubCost(); | ||
| 6126 | saveSettingsDebounced(); | 6203 | saveSettingsDebounced(); |
| 6127 | }); | 6204 | }); |
| 6128 | 6205 | ||
| @@ -6326,6 +6403,16 @@ export function initOpenAI() { | |||
| 6326 | saveSettingsDebounced(); | 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 | $('#squash_system_messages').on('input', function () { | 6416 | $('#squash_system_messages').on('input', function () { |
| 6330 | oai_settings.squash_system_messages = !!$(this).prop('checked'); | 6417 | oai_settings.squash_system_messages = !!$(this).prop('checked'); |
| 6331 | saveSettingsDebounced(); | 6418 | saveSettingsDebounced(); |
| @@ -6503,6 +6590,14 @@ export function initOpenAI() { | |||
| 6503 | width: '100%', | 6590 | width: '100%', |
| 6504 | templateResult: getAimlapiModelTemplate, | 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 | $('#completion_prompt_manager_popup_entry_form_injection_trigger').select2({ | 6601 | $('#completion_prompt_manager_popup_entry_form_injection_trigger').select2({ |
| 6507 | placeholder: t`All types (default)`, | 6602 | placeholder: t`All types (default)`, |
| 6508 | width: '100%', | 6603 | width: '100%', |
| @@ -6549,6 +6644,8 @@ export function initOpenAI() { | |||
| 6549 | $('#model_openrouter_select').on('change', onModelChange); | 6644 | $('#model_openrouter_select').on('change', onModelChange); |
| 6550 | $('#openrouter_group_models').on('change', onOpenrouterModelSortChange); | 6645 | $('#openrouter_group_models').on('change', onOpenrouterModelSortChange); |
| 6551 | $('#openrouter_sort_models').on('change', onOpenrouterModelSortChange); | 6646 | $('#openrouter_sort_models').on('change', onOpenrouterModelSortChange); |
| 6647 | $('#electronhub_group_models').on('change', onElectronHubModelSortChange); | ||
| 6648 | $('#electronhub_sort_models').on('change', onElectronHubModelSortChange); | ||
| 6552 | $('#model_ai21_select').on('change', onModelChange); | 6649 | $('#model_ai21_select').on('change', onModelChange); |
| 6553 | $('#model_mistralai_select').on('change', onModelChange); | 6650 | $('#model_mistralai_select').on('change', onModelChange); |
| 6554 | $('#model_cohere_select').on('change', onModelChange); | 6651 | $('#model_cohere_select').on('change', onModelChange); |
| @@ -4,9 +4,11 @@ import { | |||
| 4 | characters, | 4 | characters, |
| 5 | chat, | 5 | chat, |
| 6 | chat_metadata, | 6 | chat_metadata, |
| 7 | createOrEditCharacter, | ||
| 7 | default_user_avatar, | 8 | default_user_avatar, |
| 8 | eventSource, | 9 | eventSource, |
| 9 | event_types, | 10 | event_types, |
| 11 | getCurrentChatId, | ||
| 10 | getRequestHeaders, | 12 | getRequestHeaders, |
| 11 | getThumbnailUrl, | 13 | getThumbnailUrl, |
| 12 | groupToEntity, | 14 | groupToEntity, |
| @@ -69,6 +71,9 @@ export let user_avatar = ''; | |||
| 69 | /** @type {FilterHelper} Filter helper for the persona list */ | 71 | /** @type {FilterHelper} Filter helper for the persona list */ |
| 70 | export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick)); | 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 | /** @type {function(string): void} */ | 77 | /** @type {function(string): void} */ |
| 73 | let navigateToAvatar = () => { }; | 78 | let navigateToAvatar = () => { }; |
| 74 | 79 | ||
| @@ -107,12 +112,16 @@ export function initUserAvatar(avatar) { | |||
| 107 | * @param {boolean} [options.toastPersonaNameChange=true] Whether to show a toast when the persona name is changed | 112 | * @param {boolean} [options.toastPersonaNameChange=true] Whether to show a toast when the persona name is changed |
| 108 | * @param {boolean} [options.navigateToCurrent=false] Whether to navigate to the current persona after setting the avatar | 113 | * @param {boolean} [options.navigateToCurrent=false] Whether to navigate to the current persona after setting the avatar |
| 109 | */ | 114 | */ |
| 110 | export function setUserAvatar(imgfile, { toastPersonaNameChange = true, navigateToCurrent = false } = {}) { | 115 | export async function setUserAvatar(imgfile, { toastPersonaNameChange = true, navigateToCurrent = false } = {}) { |
| 116 | const currentUserAvatar = user_avatar; | ||
| 111 | user_avatar = imgfile && typeof imgfile === 'string' ? imgfile : $(this).attr('data-avatar-id'); | 117 | user_avatar = imgfile && typeof imgfile === 'string' ? imgfile : $(this).attr('data-avatar-id'); |
| 118 | if (currentUserAvatar === user_avatar) { | ||
| 119 | return; | ||
| 120 | } | ||
| 112 | reloadUserAvatar(); | 121 | reloadUserAvatar(); |
| 113 | updatePersonaUIStates({ navigateToCurrent: navigateToCurrent }); | 122 | updatePersonaUIStates({ navigateToCurrent: navigateToCurrent }); |
| 114 | selectCurrentPersona({ toastPersonaNameChange: toastPersonaNameChange }); | 123 | selectCurrentPersona({ toastPersonaNameChange: toastPersonaNameChange }); |
| 115 | retriggerFirstMessageOnEmptyChat(); | 124 | await retriggerFirstMessageOnEmptyChat(); |
| 116 | saveSettingsDebounced(); | 125 | saveSettingsDebounced(); |
| 117 | $('.zoomed_avatar[forchar]').remove(); | 126 | $('.zoomed_avatar[forchar]').remove(); |
| 118 | } | 127 | } |
| @@ -732,13 +741,13 @@ export async function askForPersonaSelection(title, text, personas, { okButton = | |||
| 732 | /** | 741 | /** |
| 733 | * Automatically selects a persona based on the given name if a matching persona exists. | 742 | * Automatically selects a persona based on the given name if a matching persona exists. |
| 734 | * @param {string} name - The name to search for | 743 | * @param {string} name - The name to search for |
| 735 | * @returns {boolean} True if a matching persona was found and selected, false otherwise | 744 | * @returns {Promise<boolean>} True if a matching persona was found and selected, false otherwise |
| 736 | */ | 745 | */ |
| 737 | export function autoSelectPersona(name) { | 746 | export async function autoSelectPersona(name) { |
| 738 | for (const [key, value] of Object.entries(power_user.personas)) { | 747 | for (const [key, value] of Object.entries(power_user.personas)) { |
| 739 | if (value === name) { | 748 | if (value === name) { |
| 740 | console.log(`Auto-selecting persona ${key} for name ${name}`); | 749 | console.log(`Auto-selecting persona ${key} for name ${name}`); |
| 741 | setUserAvatar(key); | 750 | await setUserAvatar(key); |
| 742 | return true; | 751 | return true; |
| 743 | } | 752 | } |
| 744 | } | 753 | } |
| @@ -1131,7 +1140,7 @@ function onPersonaDescriptionInput() { | |||
| 1131 | object.description = power_user.persona_description; | 1140 | object.description = power_user.persona_description; |
| 1132 | } | 1141 | } |
| 1133 | 1142 | ||
| 1134 | $(`.avatar-container[imgfile="${user_avatar}"] .ch_description`) | 1143 | $(`.avatar-container[data-avatar-id="${user_avatar}"] .ch_description`) |
| 1135 | .text(power_user.persona_description || $('#user_avatar_block').attr('no_desc_text')) | 1144 | .text(power_user.persona_description || $('#user_avatar_block').attr('no_desc_text')) |
| 1136 | .toggleClass('text_muted', !power_user.persona_description); | 1145 | .toggleClass('text_muted', !power_user.persona_description); |
| 1137 | saveSettingsDebounced(); | 1146 | saveSettingsDebounced(); |
| @@ -1219,7 +1228,7 @@ function onPersonaDescriptionPositionInput() { | |||
| 1219 | $('#persona_depth_position_settings').toggle(power_user.persona_description_position === persona_description_positions.AT_DEPTH); | 1228 | $('#persona_depth_position_settings').toggle(power_user.persona_description_position === persona_description_positions.AT_DEPTH); |
| 1220 | } | 1229 | } |
| 1221 | 1230 | ||
| 1222 | function getOrCreatePersonaDescriptor() { | 1231 | export function getOrCreatePersonaDescriptor() { |
| 1223 | let object = power_user.persona_descriptions[user_avatar]; | 1232 | let object = power_user.persona_descriptions[user_avatar]; |
| 1224 | 1233 | ||
| 1225 | if (!object) { | 1234 | if (!object) { |
| @@ -1429,13 +1438,17 @@ function getPersonaTemporaryLockInfo() { | |||
| 1429 | * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating whether a persona was selected | 1438 | * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating whether a persona was selected |
| 1430 | */ | 1439 | */ |
| 1431 | async function loadPersonaForCurrentChat({ doRender = false } = {}) { | 1440 | async function loadPersonaForCurrentChat({ doRender = false } = {}) { |
| 1441 | const currentChatId = getCurrentChatId(); | ||
| 1442 | if (currentChatId === personaLastLoadedChatId) return; | ||
| 1443 | personaLastLoadedChatId = currentChatId; | ||
| 1444 | |||
| 1432 | // Cache persona list to check if they exist | 1445 | // Cache persona list to check if they exist |
| 1433 | const userAvatars = await getUserAvatars(doRender); | 1446 | const userAvatars = await getUserAvatars(doRender); |
| 1434 | 1447 | ||
| 1435 | // Check if the user avatar is set and exists in the list of user avatars | 1448 | // Check if the user avatar is set and exists in the list of user avatars |
| 1436 | if (userAvatars.length && !userAvatars.includes(user_avatar)) { | 1449 | if (userAvatars.length && !userAvatars.includes(user_avatar)) { |
| 1437 | console.log(`User avatar ${user_avatar} not found in user avatars list, pick the first available one`); | 1450 | console.log(`User avatar ${user_avatar} not found in user avatars list, pick the first available one`); |
| 1438 | setUserAvatar(userAvatars[0], { toastPersonaNameChange: false, navigateToCurrent: true }); | 1451 | await setUserAvatar(userAvatars[0], { toastPersonaNameChange: false, navigateToCurrent: true }); |
| 1439 | } | 1452 | } |
| 1440 | 1453 | ||
| 1441 | // Define a persona for this chat | 1454 | // Define a persona for this chat |
| @@ -1530,7 +1543,7 @@ async function loadPersonaForCurrentChat({ doRender = false } = {}) { | |||
| 1530 | // Persona avatar found, select it | 1543 | // Persona avatar found, select it |
| 1531 | if (chatPersona && user_avatar !== chatPersona) { | 1544 | if (chatPersona && user_avatar !== chatPersona) { |
| 1532 | const willAutoLock = power_user.persona_auto_lock && user_avatar !== chat_metadata['persona']; | 1545 | const willAutoLock = power_user.persona_auto_lock && user_avatar !== chat_metadata['persona']; |
| 1533 | setUserAvatar(chatPersona, { toastPersonaNameChange: false, navigateToCurrent: true }); | 1546 | await setUserAvatar(chatPersona, { toastPersonaNameChange: false, navigateToCurrent: true }); |
| 1534 | 1547 | ||
| 1535 | if (power_user.persona_show_notifications) { | 1548 | if (power_user.persona_show_notifications) { |
| 1536 | let message = t`Auto-selected persona based on ${connectType} connection.<br />Your messages will now be sent as ${power_user.personas[chatPersona]}.`; | 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 | // Even if it's the same persona, we still might need to auto-lock to chat if that's enabled | 1556 | // Even if it's the same persona, we still might need to auto-lock to chat if that's enabled |
| 1544 | else if (chatPersona && power_user.persona_auto_lock && !chat_metadata['persona']) { | 1557 | else if (chatPersona && power_user.persona_auto_lock && !chat_metadata['persona']) { |
| 1545 | lockPersona('chat'); | 1558 | await lockPersona('chat'); |
| 1546 | } | 1559 | } |
| 1547 | 1560 | ||
| 1548 | updatePersonaUIStates(); | 1561 | updatePersonaUIStates(); |
| @@ -1559,7 +1572,7 @@ async function loadPersonaForCurrentChat({ doRender = false } = {}) { | |||
| 1559 | export function getConnectedPersonas(characterKey = undefined) { | 1572 | export function getConnectedPersonas(characterKey = undefined) { |
| 1560 | characterKey ??= selected_group || characters[Number(this_chid)]?.avatar; | 1573 | characterKey ??= selected_group || characters[Number(this_chid)]?.avatar; |
| 1561 | const connectedPersonas = Object.entries(power_user.persona_descriptions) | 1574 | const connectedPersonas = Object.entries(power_user.persona_descriptions) |
| 1562 | .filter(([_, desc]) => desc.connections?.some(conn => conn.type === 'character' && conn.id === characterKey)) | 1575 | .filter(([_, { connections }]) => connections?.some(conn => conn.id === characterKey)) |
| 1563 | .map(([key, _]) => key); | 1576 | .map(([key, _]) => key); |
| 1564 | return connectedPersonas; | 1577 | return connectedPersonas; |
| 1565 | } | 1578 | } |
| @@ -1606,7 +1619,7 @@ export async function showCharConnections() { | |||
| 1606 | 1619 | ||
| 1607 | // One of the persona was selected. So load it. | 1620 | // One of the persona was selected. So load it. |
| 1608 | if (!isRemoving && selectedPersona) { | 1621 | if (!isRemoving && selectedPersona) { |
| 1609 | setUserAvatar(selectedPersona, { toastPersonaNameChange: false }); | 1622 | await setUserAvatar(selectedPersona, { toastPersonaNameChange: false }); |
| 1610 | if (power_user.persona_show_notifications) { | 1623 | if (power_user.persona_show_notifications) { |
| 1611 | toastr.success(t`Selected persona ${power_user.personas[selectedPersona]} for current chat.`, t`Connected Persona Selected`); | 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 | * Retriggers the first message to reload it from the char definition. | 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 | export function retriggerFirstMessageOnEmptyChat() { | 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 | * Sets a persona name and optionally an avatar. | 1855 | * Sets a persona name and optionally an avatar. |
| 1839 | * @param {{mode: 'lookup' | 'temp' | 'all'}} namedArgs Named arguments | 1856 | * @param {{mode: 'lookup' | 'temp' | 'all'}} namedArgs Named arguments |
| 1840 | * @param {string} name Name to set | 1857 | * @param {string} name Name to set |
| 1841 | * @returns {string} | 1858 | * @returns {Promise<string>} |
| 1842 | */ | 1859 | */ |
| 1843 | function setNameCallback({ mode = 'all' }, name) { | 1860 | async function setNameCallback({ mode = 'all' }, name) { |
| 1844 | if (!name) { | 1861 | if (!name) { |
| 1845 | toastr.warning('You must specify a name to change to'); | 1862 | toastr.warning('You must specify a name to change to'); |
| 1846 | return ''; | 1863 | return ''; |
| @@ -1858,7 +1875,7 @@ function setNameCallback({ mode = 'all' }, name) { | |||
| 1858 | let persona = Object.entries(power_user.personas).find(([avatar, _]) => avatar === name)?.[1]; | 1875 | let persona = Object.entries(power_user.personas).find(([avatar, _]) => avatar === name)?.[1]; |
| 1859 | if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1]; | 1876 | if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1]; |
| 1860 | if (persona) { | 1877 | if (persona) { |
| 1861 | autoSelectPersona(persona); | 1878 | await autoSelectPersona(persona); |
| 1862 | return ''; | 1879 | return ''; |
| 1863 | } else if (mode === 'lookup') { | 1880 | } else if (mode === 'lookup') { |
| 1864 | toastr.warning(`Persona ${name} not found`); | 1881 | toastr.warning(`Persona ${name} not found`); |
| @@ -2016,9 +2033,9 @@ export async function initPersonas() { | |||
| 2016 | $('#sync_name_button').on('click', syncUserNameToPersona); | 2033 | $('#sync_name_button').on('click', syncUserNameToPersona); |
| 2017 | $('#avatar_upload_file').on('change', changeUserAvatar); | 2034 | $('#avatar_upload_file').on('change', changeUserAvatar); |
| 2018 | 2035 | ||
| 2019 | $(document).on('click', '#user_avatar_block .avatar-container', function () { | 2036 | $(document).on('click', '#user_avatar_block .avatar-container', async function () { |
| 2020 | const imgfile = $(this).attr('data-avatar-id'); | 2037 | const imgfile = $(this).attr('data-avatar-id'); |
| 2021 | setUserAvatar(imgfile); | 2038 | await setUserAvatar(imgfile); |
| 2022 | }); | 2039 | }); |
| 2023 | 2040 | ||
| 2024 | $('#persona_rename_button').on('click', () => renamePersona(user_avatar)); | 2041 | $('#persona_rename_button').on('click', () => renamePersona(user_avatar)); |
| @@ -37,8 +37,8 @@ export const POPUP_RESULT = { | |||
| 37 | 37 | ||
| 38 | /** | 38 | /** |
| 39 | * @typedef {object} PopupOptions | 39 | * @typedef {object} PopupOptions |
| 40 | * @property {string|boolean?} [okButton=null] - Custom text for the OK button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup) | 40 | * @property {string|boolean?} [okButton=null] - Custom text for the OK button. A set text will always show the button. `true` or `false` to explicitly show or hide the button. `null` will leave the behavior and display of the button unchanged, based on the popup type. |
| 41 | * @property {string|boolean?} [cancelButton=null] - Custom text for the Cancel button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup) | 41 | * @property {string|boolean?} [cancelButton=null] - Custom text for the Cancel button. A set text will always show the button. `true` or `false` to explicitly show or hide the button. `null` will leave the behavior and display of the button unchanged, based on the popup type. |
| 42 | * @property {number?} [rows=1] - The number of rows for the input field | 42 | * @property {number?} [rows=1] - The number of rows for the input field |
| 43 | * @property {boolean?} [wide=false] - Whether to display the popup in wide mode (wide screen, 1/1 aspect ratio) | 43 | * @property {boolean?} [wide=false] - Whether to display the popup in wide mode (wide screen, 1/1 aspect ratio) |
| 44 | * @property {boolean?} [wider=false] - Whether to display the popup in wider mode (just wider, no height scaling) | 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 | switch (type) { | 327 | switch (type) { |
| 328 | case POPUP_TYPE.TEXT: { | 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 | if (!cancelButton) this.cancelButton.style.display = 'none'; | 331 | if (!cancelButton) this.cancelButton.style.display = 'none'; |
| 330 | break; | 332 | break; |
| 331 | } | 333 | } |
| 332 | case POPUP_TYPE.CONFIRM: { | 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 | if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-yes'); | 339 | if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-yes'); |
| 334 | if (!cancelButton) this.cancelButton.textContent = template.getAttribute('popup-button-no'); | 340 | if (!cancelButton) this.cancelButton.textContent = template.getAttribute('popup-button-no'); |
| 335 | break; | 341 | break; |
| 336 | } | 342 | } |
| 337 | case POPUP_TYPE.INPUT: { | 343 | case POPUP_TYPE.INPUT: { |
| 338 | this.mainInput.style.display = 'block'; | 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 | if (cancelButton === false) this.cancelButton.style.display = 'none'; | 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 | break; | 350 | break; |
| 342 | } | 351 | } |
| 343 | case POPUP_TYPE.DISPLAY: { | 352 | case POPUP_TYPE.DISPLAY: { |
| 353 | // Display hides OK and CANCEL and all main button controls | ||
| 344 | this.buttonControls.style.display = 'none'; | 354 | this.buttonControls.style.display = 'none'; |
| 345 | this.closeButton.style.display = 'block'; | 355 | this.closeButton.style.display = 'block'; |
| 346 | break; | 356 | break; |
| @@ -348,7 +358,6 @@ export class Popup { | |||
| 348 | case POPUP_TYPE.CROP: { | 358 | case POPUP_TYPE.CROP: { |
| 349 | this.cropWrap.style.display = 'block'; | 359 | this.cropWrap.style.display = 'block'; |
| 350 | this.cropImage.src = cropImage; | 360 | this.cropImage.src = cropImage; |
| 351 | if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-crop'); | ||
| 352 | $(this.cropImage).cropper({ | 361 | $(this.cropImage).cropper({ |
| 353 | aspectRatio: cropAspect ?? 2 / 3, | 362 | aspectRatio: cropAspect ?? 2 / 3, |
| 354 | autoCropArea: 1, | 363 | autoCropArea: 1, |
| @@ -359,6 +368,11 @@ export class Popup { | |||
| 359 | this.cropData.want_resize = !power_user.never_resize_avatars; | 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 | break; | 376 | break; |
| 363 | } | 377 | } |
| 364 | default: { | 378 | default: { |
| @@ -137,6 +137,7 @@ export const power_user = { | |||
| 137 | streaming_fps: 30, | 137 | streaming_fps: 30, |
| 138 | smooth_streaming: false, | 138 | smooth_streaming: false, |
| 139 | smooth_streaming_speed: 50, | 139 | smooth_streaming_speed: 50, |
| 140 | stream_fade_in: false, | ||
| 140 | 141 | ||
| 141 | fast_ui_mode: true, | 142 | fast_ui_mode: true, |
| 142 | avatar_style: avatar_styles.ROUND, | 143 | avatar_style: avatar_styles.ROUND, |
| @@ -1685,6 +1686,8 @@ export async function loadPowerUserSettings(settings, data) { | |||
| 1685 | $('#smooth_streaming').prop('checked', power_user.smooth_streaming); | 1686 | $('#smooth_streaming').prop('checked', power_user.smooth_streaming); |
| 1686 | $('#smooth_streaming_speed').val(power_user.smooth_streaming_speed); | 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 | $('#font_scale').val(power_user.font_scale); | 1691 | $('#font_scale').val(power_user.font_scale); |
| 1689 | $('#font_scale_counter').val(power_user.font_scale); | 1692 | $('#font_scale_counter').val(power_user.font_scale); |
| 1690 | 1693 | ||
| @@ -3493,6 +3496,11 @@ jQuery(() => { | |||
| 3493 | saveSettingsDebounced(); | 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 | $('input[name="font_scale"]').on('input', async function (e, data) { | 3504 | $('input[name="font_scale"]').on('input', async function (e, data) { |
| 3497 | const applyMode = data?.forced ? 'forced' : 'normal'; | 3505 | const applyMode = data?.forced ? 'forced' : 'normal'; |
| 3498 | power_user.font_scale = Number($(this).val()); | 3506 | power_user.font_scale = Number($(this).val()); |
| @@ -81,6 +81,9 @@ function autoSelectPreset() { | |||
| 81 | * @returns {PresetManager} Preset manager | 81 | * @returns {PresetManager} Preset manager |
| 82 | */ | 82 | */ |
| 83 | export function getPresetManager(apiId = '') { | 83 | export function getPresetManager(apiId = '') { |
| 84 | if (apiId === 'koboldhorde') { | ||
| 85 | apiId = 'kobold'; | ||
| 86 | } | ||
| 84 | if (!apiId) { | 87 | if (!apiId) { |
| 85 | apiId = main_api == 'koboldhorde' ? 'kobold' : main_api; | 88 | apiId = main_api == 'koboldhorde' ? 'kobold' : main_api; |
| 86 | } | 89 | } |
| @@ -814,7 +817,7 @@ class PresetManager { | |||
| 814 | * Reads a preset extension field from the preset. | 817 | * Reads a preset extension field from the preset. |
| 815 | * @param {object} options | 818 | * @param {object} options |
| 816 | * @param {string} [options.name] Name of the preset. If not provided, uses the currently selected preset name. | 819 | * @param {string} [options.name] Name of the preset. If not provided, uses the currently selected preset name. |
| 817 | * @param {string} options.path Path to the preset extension field, e.g. 'myextension.data'. | 820 | * @param {string} options.path Path to the preset extension field, e.g. 'myextension.data'. If empty, reads the entire extensions object. |
| 818 | * @return {any} The value of the preset extension field, or null if not found. | 821 | * @return {any} The value of the preset extension field, or null if not found. |
| 819 | */ | 822 | */ |
| 820 | readPresetExtensionField({ name, path }) { | 823 | readPresetExtensionField({ name, path }) { |
| @@ -825,7 +828,7 @@ class PresetManager { | |||
| 825 | // Read from settings if the selected preset is the same as the provided name | 828 | // Read from settings if the selected preset is the same as the provided name |
| 826 | if (settings && selectedName === presetName) { | 829 | if (settings && selectedName === presetName) { |
| 827 | const settingsExtensions = ensurePlainObject(settings.extensions || {}); | 830 | const settingsExtensions = ensurePlainObject(settings.extensions || {}); |
| 828 | return lodash.get(settingsExtensions, path, null); | 831 | return path ? lodash.get(settingsExtensions, path, null) : settingsExtensions; |
| 829 | } | 832 | } |
| 830 | 833 | ||
| 831 | // Otherwise, read from the preset by name | 834 | // Otherwise, read from the preset by name |
| @@ -835,7 +838,7 @@ class PresetManager { | |||
| 835 | } | 838 | } |
| 836 | 839 | ||
| 837 | const presetExtensions = ensurePlainObject(preset.extensions || {}); | 840 | const presetExtensions = ensurePlainObject(preset.extensions || {}); |
| 838 | const value = lodash.get(presetExtensions, path, null); | 841 | const value = path ? lodash.get(presetExtensions, path, null) : presetExtensions; |
| 839 | return value; | 842 | return value; |
| 840 | } | 843 | } |
| 841 | 844 | ||
| @@ -843,7 +846,7 @@ class PresetManager { | |||
| 843 | * Writes a value to a preset extension field. | 846 | * Writes a value to a preset extension field. |
| 844 | * @param {object} options | 847 | * @param {object} options |
| 845 | * @param {string} [options.name] Name of the preset. If not provided, uses the currently selected preset name. | 848 | * @param {string} [options.name] Name of the preset. If not provided, uses the currently selected preset name. |
| 846 | * @param {string} options.path Path to the preset extension field, e.g. 'myextension.data'. | 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 | * @param {any} options.value Value to write to the preset extension field. | 850 | * @param {any} options.value Value to write to the preset extension field. |
| 848 | * @return {Promise<void>} Resolves when the preset is saved. | 851 | * @return {Promise<void>} Resolves when the preset is saved. |
| 849 | */ | 852 | */ |
| @@ -856,7 +859,7 @@ class PresetManager { | |||
| 856 | if (settings && selectedName === presetName) { | 859 | if (settings && selectedName === presetName) { |
| 857 | // Set the value at the specified path | 860 | // Set the value at the specified path |
| 858 | settings.extensions = ensurePlainObject(settings.extensions || {}); | 861 | settings.extensions = ensurePlainObject(settings.extensions || {}); |
| 859 | lodash.set(settings.extensions, path, value); | 862 | path ? lodash.set(settings.extensions, path, value) : (settings.extensions = value); |
| 860 | await saveSettings(); | 863 | await saveSettings(); |
| 861 | } | 864 | } |
| 862 | 865 | ||
| @@ -868,7 +871,7 @@ class PresetManager { | |||
| 868 | 871 | ||
| 869 | // Set the value at the specified path | 872 | // Set the value at the specified path |
| 870 | preset.extensions = ensurePlainObject(preset.extensions || {}); | 873 | preset.extensions = ensurePlainObject(preset.extensions || {}); |
| 871 | lodash.set(preset.extensions, path, value); | 874 | path ? lodash.set(preset.extensions, path, value) : (preset.extensions = value); |
| 872 | 875 | ||
| 873 | // Save the updated preset | 876 | // Save the updated preset |
| 874 | await this.savePreset(presetName, preset, { skipUpdate: true }); | 877 | await this.savePreset(presetName, preset, { skipUpdate: true }); |
| @@ -1033,10 +1036,11 @@ export async function initPresetManager() { | |||
| 1033 | return; | 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 | await presetManager.renamePreset(newName); | 1041 | await presetManager.renamePreset(newName); |
| 1037 | 1042 | await presetManager.writePresetExtensionField({ name: newName, path: '', value: extensions }); | |
| 1038 | await eventSource.emit(event_types.PRESET_DELETED, { apiId: apiId, name: oldName }); | 1043 | await eventSource.emit(event_types.PRESET_RENAMED, { apiId: apiId, oldName: oldName, newName: newName }); |
| 1039 | await eventSource.emit(event_types.PRESET_CHANGED, { apiId: apiId, name: newName }); | ||
| 1040 | 1044 | ||
| 1041 | if (apiId === 'openai') { | 1045 | if (apiId === 'openai') { |
| 1042 | // This is a horrible mess, but prevents the renamed preset from being corrupted. | 1046 | // This is a horrible mess, but prevents the renamed preset from being corrupted. |
| @@ -1116,13 +1120,13 @@ export async function initPresetManager() { | |||
| 1116 | if (result) { | 1120 | if (result) { |
| 1117 | const successToast = !presetManager.isAdvancedFormatting() ? t`Preset deleted` : t`Template deleted`; | 1121 | const successToast = !presetManager.isAdvancedFormatting() ? t`Preset deleted` : t`Template deleted`; |
| 1118 | toastr.success(successToast); | 1122 | toastr.success(successToast); |
| 1123 | await eventSource.emit(event_types.PRESET_DELETED, { apiId, name }); | ||
| 1119 | } else { | 1124 | } else { |
| 1120 | const warningToast = !presetManager.isAdvancedFormatting() ? t`Preset was not deleted from server` : t`Template was not deleted from server`; | 1125 | const warningToast = !presetManager.isAdvancedFormatting() ? t`Preset was not deleted from server` : t`Template was not deleted from server`; |
| 1121 | toastr.warning(warningToast); | 1126 | toastr.warning(warningToast); |
| 1122 | } | 1127 | } |
| 1123 | 1128 | ||
| 1124 | saveSettingsDebounced(); | 1129 | saveSettingsDebounced(); |
| 1125 | await eventSource.emit(event_types.PRESET_DELETED, { apiId: apiId, name: name }); | ||
| 1126 | }); | 1130 | }); |
| 1127 | 1131 | ||
| 1128 | $(document).on('click', '[data-preset-manager-restore]', async function () { | 1132 | $(document).on('click', '[data-preset-manager-restore]', async function () { |
| @@ -15,6 +15,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom | |||
| 15 | import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; | 15 | import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; |
| 16 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | 16 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; |
| 17 | import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; | 17 | import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; |
| 18 | import { applyStreamFadeIn } from './util/stream-fadein.js'; | ||
| 18 | import { copyText, escapeRegex, isFalseBoolean, isTrueBoolean, setDatasetProperty, trimSpaces } from './utils.js'; | 19 | import { copyText, escapeRegex, isFalseBoolean, isTrueBoolean, setDatasetProperty, trimSpaces } from './utils.js'; |
| 19 | 20 | ||
| 20 | /** | 21 | /** |
| @@ -124,6 +125,8 @@ export function extractReasoningFromData(data, { | |||
| 124 | case chat_completion_sources.POLLINATIONS: | 125 | case chat_completion_sources.POLLINATIONS: |
| 125 | case chat_completion_sources.MOONSHOT: | 126 | case chat_completion_sources.MOONSHOT: |
| 126 | case chat_completion_sources.COMETAPI: | 127 | case chat_completion_sources.COMETAPI: |
| 128 | case chat_completion_sources.ELECTRONHUB: | ||
| 129 | case chat_completion_sources.NANOGPT: | ||
| 127 | case chat_completion_sources.CUSTOM: { | 130 | case chat_completion_sources.CUSTOM: { |
| 128 | return data?.choices?.[0]?.message?.reasoning_content | 131 | return data?.choices?.[0]?.message?.reasoning_content |
| 129 | ?? data?.choices?.[0]?.message?.reasoning | 132 | ?? data?.choices?.[0]?.message?.reasoning |
| @@ -495,7 +498,12 @@ export class ReasoningHandler { | |||
| 495 | // Update the reasoning message | 498 | // Update the reasoning message |
| 496 | const reasoning = trimSpaces(this.reasoningDisplayText ?? this.reasoning); | 499 | const reasoning = trimSpaces(this.reasoningDisplayText ?? this.reasoning); |
| 497 | const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true); | 500 | const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true); |
| 501 | |||
| 502 | if (power_user.stream_fade_in) { | ||
| 503 | applyStreamFadeIn(this.messageReasoningContentDom, displayReasoning); | ||
| 504 | } else { | ||
| 498 | this.messageReasoningContentDom.innerHTML = displayReasoning; | 505 | this.messageReasoningContentDom.innerHTML = displayReasoning; |
| 506 | } | ||
| 499 | 507 | ||
| 500 | // Update tooltip for hidden reasoning edit | 508 | // Update tooltip for hidden reasoning edit |
| 501 | /** @type {HTMLElement} */ | 509 | /** @type {HTMLElement} */ |
| @@ -2,7 +2,7 @@ import { DOMPurify, moment } from '../lib.js'; | |||
| 2 | import { event_types, eventSource, getRequestHeaders } from '../script.js'; | 2 | import { event_types, eventSource, getRequestHeaders } from '../script.js'; |
| 3 | import { t } from './i18n.js'; | 3 | import { t } from './i18n.js'; |
| 4 | import { chat_completion_sources } from './openai.js'; | 4 | import { chat_completion_sources } from './openai.js'; |
| 5 | import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js'; | 5 | import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; |
| 6 | import { SlashCommand } from './slash-commands/SlashCommand.js'; | 6 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 7 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; | 7 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; |
| 8 | import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; | 8 | import { enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| @@ -293,11 +293,13 @@ export let secret_state = {}; | |||
| 293 | * @param {string} key Secret key | 293 | * @param {string} key Secret key |
| 294 | * @param {string} value Secret value to write | 294 | * @param {string} value Secret value to write |
| 295 | * @param {string} [label] (Optional) Label for the key. If not provided, generated automatically. | 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 | * @return {Promise<string?>} The ID of the newly created secret key, or null if no value is provided. | 298 | * @return {Promise<string?>} The ID of the newly created secret key, or null if no value is provided. |
| 297 | */ | 299 | */ |
| 298 | export async function writeSecret(key, value, label) { | 300 | export async function writeSecret(key, value, label, { allowEmpty } = {}) { |
| 299 | try { | 301 | try { |
| 300 | if (!value) { | 302 | if (!value && !allowEmpty) { |
| 301 | console.warn(`No value provided for ${key} in writeSecret, redirecting to deleteSecret`); | 303 | console.warn(`No value provided for ${key} in writeSecret, redirecting to deleteSecret`); |
| 302 | await deleteSecret(key); | 304 | await deleteSecret(key); |
| 303 | return null; | 305 | return null; |
| @@ -558,7 +560,8 @@ async function openKeyManagerDialog(key) { | |||
| 558 | const template = $(await renderTemplateAsync('secretKeyManager', { name, key })); | 560 | const template = $(await renderTemplateAsync('secretKeyManager', { name, key })); |
| 559 | template.find('button[data-action="add-secret"]').on('click', async function () { | 561 | template.find('button[data-action="add-secret"]').on('click', async function () { |
| 560 | let label = ''; | 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 | customInputs: [{ | 565 | customInputs: [{ |
| 563 | id: 'newSecretLabel', | 566 | id: 'newSecretLabel', |
| 564 | type: 'text', | 567 | type: 'text', |
| @@ -567,13 +570,20 @@ async function openKeyManagerDialog(key) { | |||
| 567 | onClose: popup => { | 570 | onClose: popup => { |
| 568 | if (popup.result) { | 571 | if (popup.result) { |
| 569 | label = popup.inputResults.get('newSecretLabel').toString().trim(); | 572 | label = popup.inputResults.get('newSecretLabel').toString().trim(); |
| 573 | result = popup.result; | ||
| 570 | } | 574 | } |
| 571 | }, | 575 | }, |
| 572 | }); | 576 | }); |
| 573 | if (!value) { | 577 | if (!value) { |
| 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) { | ||
| 574 | return; | 583 | return; |
| 575 | } | 584 | } |
| 576 | await writeSecret(key, value, label); | 585 | } |
| 586 | await writeSecret(key, value, label, { allowEmpty: true }); | ||
| 577 | await renderSecretsList(); | 587 | await renderSecretsList(); |
| 578 | }); | 588 | }); |
| 579 | 589 | ||
| @@ -821,6 +831,13 @@ function registerSecretSlashCommands() { | |||
| 821 | isRequired: false, | 831 | isRequired: false, |
| 822 | typeList: [ARGUMENT_TYPE.STRING], | 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 | unnamedArgumentList: [ | 842 | unnamedArgumentList: [ |
| 826 | SlashCommandArgument.fromProps({ | 843 | SlashCommandArgument.fromProps({ |
| @@ -831,6 +848,7 @@ function registerSecretSlashCommands() { | |||
| 831 | ], | 848 | ], |
| 832 | callback: async (args, value) => { | 849 | callback: async (args, value) => { |
| 833 | const quiet = isTrueBoolean(args?.quiet?.toString()); | 850 | const quiet = isTrueBoolean(args?.quiet?.toString()); |
| 851 | const allowEmpty = isTrueBoolean(args?.empty?.toString()); | ||
| 834 | const key = args?.key?.toString()?.trim() || resolveSecretKey(); | 852 | const key = args?.key?.toString()?.trim() || resolveSecretKey(); |
| 835 | 853 | ||
| 836 | if (!key) { | 854 | if (!key) { |
| @@ -849,7 +867,7 @@ function registerSecretSlashCommands() { | |||
| 849 | } | 867 | } |
| 850 | 868 | ||
| 851 | const valueStr = value?.toString()?.trim(); | 869 | const valueStr = value?.toString()?.trim(); |
| 852 | if (!valueStr) { | 870 | if (!valueStr && !allowEmpty) { |
| 853 | if (!quiet) { | 871 | if (!quiet) { |
| 854 | toastr.error(t`No value provided for the secret key: ${key}`); | 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 | const label = args?.label?.toString()?.trim() || getLabel(); | 877 | const label = args?.label?.toString()?.trim() || getLabel(); |
| 860 | const id = await writeSecret(key, valueStr, label); | 878 | const id = await writeSecret(key, valueStr, label, { allowEmpty }); |
| 861 | 879 | ||
| 862 | if (!quiet) { | 880 | if (!quiet) { |
| 863 | toastr.success(t`Secret has been written for the key: ${key}`); | 881 | toastr.success(t`Secret has been written for the key: ${key}`); |
| @@ -4585,6 +4585,8 @@ export async function sendMessageAs(args, text) { | |||
| 4585 | insertAt = chat.length + insertAt; | 4585 | insertAt = chat.length + insertAt; |
| 4586 | } | 4586 | } |
| 4587 | 4587 | ||
| 4588 | chat_metadata['tainted'] = true; | ||
| 4589 | |||
| 4588 | if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { | 4590 | if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { |
| 4589 | chat.splice(insertAt, 0, message); | 4591 | chat.splice(insertAt, 0, message); |
| 4590 | await saveChatConditional(); | 4592 | await saveChatConditional(); |
| @@ -4635,6 +4637,8 @@ export async function sendNarratorMessage(args, text) { | |||
| 4635 | insertAt = chat.length + insertAt; | 4637 | insertAt = chat.length + insertAt; |
| 4636 | } | 4638 | } |
| 4637 | 4639 | ||
| 4640 | chat_metadata['tainted'] = true; | ||
| 4641 | |||
| 4638 | if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { | 4642 | if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { |
| 4639 | chat.splice(insertAt, 0, message); | 4643 | chat.splice(insertAt, 0, message); |
| 4640 | await saveChatConditional(); | 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 | chat.push(message); | 4694 | chat.push(message); |
| 4689 | await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); | 4695 | await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); |
| 4690 | addOneMessage(message); | 4696 | addOneMessage(message); |
| @@ -4719,6 +4725,8 @@ async function sendCommentMessage(args, text) { | |||
| 4719 | insertAt = chat.length + insertAt; | 4725 | insertAt = chat.length + insertAt; |
| 4720 | } | 4726 | } |
| 4721 | 4727 | ||
| 4728 | chat_metadata['tainted'] = true; | ||
| 4729 | |||
| 4722 | if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { | 4730 | if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) { |
| 4723 | chat.splice(insertAt, 0, message); | 4731 | chat.splice(insertAt, 0, message); |
| 4724 | await saveChatConditional(); | 4732 | await saveChatConditional(); |
| @@ -57,6 +57,7 @@ import { | |||
| 57 | import { | 57 | import { |
| 58 | extension_settings, | 58 | extension_settings, |
| 59 | ModuleWorkerWrapper, | 59 | ModuleWorkerWrapper, |
| 60 | openThirdPartyExtensionMenu, | ||
| 60 | renderExtensionTemplate, | 61 | renderExtensionTemplate, |
| 61 | renderExtensionTemplateAsync, | 62 | renderExtensionTemplateAsync, |
| 62 | saveMetadataDebounced, | 63 | saveMetadataDebounced, |
| @@ -233,6 +234,7 @@ export function getContext() { | |||
| 233 | parseReasoningFromString, | 234 | parseReasoningFromString, |
| 234 | unshallowCharacter, | 235 | unshallowCharacter, |
| 235 | unshallowGroupMembers, | 236 | unshallowGroupMembers, |
| 237 | openThirdPartyExtensionMenu, | ||
| 236 | symbols: { | 238 | symbols: { |
| 237 | ignore: IGNORE_SYMBOL, | 239 | ignore: IGNORE_SYMBOL, |
| 238 | }, | 240 | }, |
| @@ -765,8 +765,13 @@ async function importTags(character, { importSetting = null } = {}) { | |||
| 765 | */ | 765 | */ |
| 766 | async function handleTagImport(character, { importSetting = null } = {}) { | 766 | async function handleTagImport(character, { importSetting = null } = {}) { |
| 767 | /** @type {string[]} */ | 767 | /** @type {string[]} */ |
| 768 | const alreadyAssignedTags = tag_map[character.avatar] ?? []; | ||
| 768 | const importTags = character.tags.map(t => t.trim()).filter(t => t) | 769 | const importTags = character.tags.map(t => t.trim()).filter(t => t) |
| 769 | .filter(t => !IMPORT_EXLCUDED_TAGS.includes(t)) | 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 | .slice(0, ANTI_TROLL_MAX_TAGS); | 775 | .slice(0, ANTI_TROLL_MAX_TAGS); |
| 771 | const existingTags = getExistingTags(importTags); | 776 | const existingTags = getExistingTags(importTags); |
| 772 | const newTags = importTags.filter(t => !existingTags.some(existingTag => existingTag.name.toLowerCase() === t.toLowerCase())) | 777 | const newTags = importTags.filter(t => !existingTags.some(existingTag => existingTag.name.toLowerCase() === t.toLowerCase())) |
| @@ -22,8 +22,10 @@ | |||
| 22 | <li><tt>{{char}}</tt> – <span data-i18n="help_macros_16">the Character's name</span></li> | 22 | <li><tt>{{char}}</tt> – <span data-i18n="help_macros_16">the Character's name</span></li> |
| 23 | <li><tt>{{version}}</tt> – <span data-i18n="help_macros_17">the Character's version number</span></li> | 23 | <li><tt>{{version}}</tt> – <span data-i18n="help_macros_17">the Character's version number</span></li> |
| 24 | <li><tt>{{charDepthPrompt}}</tt> – <span data-i18n="help_macros_charDepthPrompt">the Character's @ Depth Note</span></li> | 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 | <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 | <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 | <li><tt>{{groupNotMuted}}</tt> – <span data-i18n="help_groupNotMuted">the same as {{group}}, but excludes muted members</span></li> | 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 | <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> | 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 | <li><tt>{{lastMessage}}</tt> – <span data-i18n="help_macros_20">the text of the latest chat message.</span></li> | 30 | <li><tt>{{lastMessage}}</tt> – <span data-i18n="help_macros_20">the text of the latest chat message.</span></li> |
| 29 | <li><tt>{{lastUserMessage}}</tt> – <span data-i18n="help_macros_lastUser">the text of the latest user chat message.</span></li> | 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 | if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) { | 702 | if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) { |
| 652 | if (oai_settings.cohere_model.includes('command-a')) { | 703 | if (oai_settings.cohere_model.includes('command-a')) { |
| 653 | return commandATokenizer; | 704 | return commandATokenizer; |
| @@ -526,12 +526,13 @@ export class ToolManager { | |||
| 526 | for (let choiceIndex = 0; choiceIndex < parsed.candidates.length; choiceIndex++) { | 526 | for (let choiceIndex = 0; choiceIndex < parsed.candidates.length; choiceIndex++) { |
| 527 | const candidate = parsed.candidates[choiceIndex]; | 527 | const candidate = parsed.candidates[choiceIndex]; |
| 528 | if (Array.isArray(candidate?.content?.parts)) { | 528 | if (Array.isArray(candidate?.content?.parts)) { |
| 529 | for (let toolCallIndex = 0; toolCallIndex < candidate.content.parts.length; toolCallIndex++) { | 529 | for (let partIndex = 0; partIndex < candidate.content.parts.length; partIndex++) { |
| 530 | const part = candidate.content.parts[toolCallIndex]; | 530 | const part = candidate.content.parts[partIndex]; |
| 531 | if (part.functionCall) { | 531 | if (part.functionCall) { |
| 532 | if (!Array.isArray(toolCalls[choiceIndex])) { | 532 | if (!Array.isArray(toolCalls[choiceIndex])) { |
| 533 | toolCalls[choiceIndex] = []; | 533 | toolCalls[choiceIndex] = []; |
| 534 | } | 534 | } |
| 535 | const toolCallIndex = toolCalls[choiceIndex].length; | ||
| 535 | if (toolCalls[choiceIndex][toolCallIndex] === undefined) { | 536 | if (toolCalls[choiceIndex][toolCallIndex] === undefined) { |
| 536 | toolCalls[choiceIndex][toolCallIndex] = {}; | 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 | DOMPurify, | 3 | DOMPurify, |
| 4 | Readability, | 4 | Readability, |
| 5 | isProbablyReaderable, | 5 | isProbablyReaderable, |
| 6 | lodash, | ||
| 6 | } from '../lib.js'; | 7 | } from '../lib.js'; |
| 7 | 8 | ||
| 8 | import { getContext } from './extensions.js'; | 9 | import { getContext } from './extensions.js'; |
| 9 | import { characters, getRequestHeaders, this_chid, user_avatar } from '../script.js'; | 10 | import { characters, getRequestHeaders, processDroppedFiles, this_chid, user_avatar } from '../script.js'; |
| 10 | import { isMobile } from './RossAscends-mods.js'; | 11 | import { isMobile } from './RossAscends-mods.js'; |
| 11 | import { collapseNewlines, power_user } from './power-user.js'; | 12 | import { collapseNewlines, power_user } from './power-user.js'; |
| 12 | import { debounce_timeout } from './constants.js'; | 13 | import { debounce_timeout } from './constants.js'; |
| @@ -15,6 +16,10 @@ import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; | |||
| 15 | import { getTagsList } from './tags.js'; | 16 | import { getTagsList } from './tags.js'; |
| 16 | import { groups, selected_group } from './group-chats.js'; | 17 | import { groups, selected_group } from './group-chats.js'; |
| 17 | import { getCurrentLocale, t } from './i18n.js'; | 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 | * Pagination status string template. | 25 | * Pagination status string template. |
| @@ -25,6 +30,8 @@ export const PAGINATION_TEMPLATE = '<%= rangeStart %>-<%= rangeEnd %> .. <%= tot | |||
| 25 | export const localizePagination = function(container) { | 30 | export const localizePagination = function(container) { |
| 26 | container.find('[title="Next page"]').attr('title', t`Next page`); | 31 | container.find('[title="Next page"]').attr('title', t`Next page`); |
| 27 | container.find('[title="Previous page"]').attr('title', t`Previous page`); | 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 | * Checks if a string only contains digits. | 304 | * Checks if a string only contains digits. |
| 289 | * @param {string} str The string to check. | 305 | * @param {string} str The string to check. |
| 290 | * @returns {boolean} True if the string only contains digits, false otherwise. | 306 | * @returns {boolean} True if the string only contains digits, false otherwise. |
| @@ -616,7 +632,7 @@ export function isElementInViewport(el) { | |||
| 616 | /** | 632 | /** |
| 617 | * Returns a name that is unique among the names that exist. | 633 | * Returns a name that is unique among the names that exist. |
| 618 | * @param {string} name The name to check. | 634 | * @param {string} name The name to check. |
| 619 | * @param {{ (y: any): boolean; }} exists Function to check if name exists. | 635 | * @param {{ (name: string): boolean; }} exists Function to check if name exists. |
| 620 | * @returns {string} A unique name. | 636 | * @returns {string} A unique name. |
| 621 | */ | 637 | */ |
| 622 | export function getUniqueName(name, exists) { | 638 | export function getUniqueName(name, exists) { |
| @@ -1633,13 +1649,18 @@ export function createThumbnail(dataUrl, maxWidth = null, maxHeight = null, type | |||
| 1633 | * @param {{ (): boolean; }} condition The condition to wait for. | 1649 | * @param {{ (): boolean; }} condition The condition to wait for. |
| 1634 | * @param {number} [timeout=1000] The timeout in milliseconds. | 1650 | * @param {number} [timeout=1000] The timeout in milliseconds. |
| 1635 | * @param {number} [interval=100] The interval in milliseconds. | 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 | * @returns {Promise<void>} A promise that resolves when the condition is true. | 1654 | * @returns {Promise<void>} A promise that resolves when the condition is true. |
| 1637 | */ | 1655 | */ |
| 1638 | export async function waitUntilCondition(condition, timeout = 1000, interval = 100) { | 1656 | export async function waitUntilCondition(condition, timeout = 1000, interval = 100, options = {}) { |
| 1657 | const { rejectOnTimeout = true } = options; | ||
| 1658 | |||
| 1639 | return new Promise((resolve, reject) => { | 1659 | return new Promise((resolve, reject) => { |
| 1640 | const timeoutId = setTimeout(() => { | 1660 | const timeoutId = setTimeout(() => { |
| 1641 | clearInterval(intervalId); | 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 | }, timeout); | 1664 | }, timeout); |
| 1644 | 1665 | ||
| 1645 | const intervalId = setInterval(() => { | 1666 | const intervalId = setInterval(() => { |
| @@ -2560,3 +2581,110 @@ export function textValueMatcher(params, data) { | |||
| 2560 | export function versionCompare(srcVersion, minVersion) { | 2581 | export function versionCompare(srcVersion, minVersion) { |
| 2561 | return (srcVersion || '0.0.0').localeCompare(minVersion, undefined, { numeric: true, sensitivity: 'base' }) > -1; | 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 | import { Fuse } from '../lib.js'; | 1 | import { Fuse } from '../lib.js'; |
| 2 | 2 | ||
| 3 | import { saveSettings, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js'; | 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 | 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 } from './utils.js'; | 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 | import { extension_settings, getContext } from './extensions.js'; | 5 | import { extension_settings, getContext } from './extensions.js'; |
| 6 | import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js'; | 6 | import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js'; |
| 7 | import { isMobile } from './RossAscends-mods.js'; | 7 | import { isMobile } from './RossAscends-mods.js'; |
| @@ -22,6 +22,7 @@ import { StructuredCloneMap } from './util/StructuredCloneMap.js'; | |||
| 22 | import { renderTemplateAsync } from './templates.js'; | 22 | import { renderTemplateAsync } from './templates.js'; |
| 23 | import { t } from './i18n.js'; | 23 | import { t } from './i18n.js'; |
| 24 | import { accountStorage } from './util/AccountStorage.js'; | 24 | import { accountStorage } from './util/AccountStorage.js'; |
| 25 | import { getOrCreatePersonaDescriptor, setPersonaDescription, user_avatar } from './personas.js'; | ||
| 25 | 26 | ||
| 26 | export const world_info_insertion_strategy = { | 27 | export const world_info_insertion_strategy = { |
| 27 | evenly: 0, | 28 | evenly: 0, |
| @@ -156,6 +157,7 @@ const KNOWN_DECORATORS = ['@@activate', '@@dont_activate']; | |||
| 156 | * @property {Array} worldInfoDepth - Array of depth entries | 157 | * @property {Array} worldInfoDepth - Array of depth entries |
| 157 | * @property {Array} anBefore - Array of entries before Author's Note | 158 | * @property {Array} anBefore - Array of entries before Author's Note |
| 158 | * @property {Array} anAfter - Array of entries after Author's Note | 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 | * @property {any[]} WIDepthEntries The depth entries. | 168 | * @property {any[]} WIDepthEntries The depth entries. |
| 167 | * @property {any[]} ANBeforeEntries The entries before Author's Note. | 169 | * @property {any[]} ANBeforeEntries The entries before Author's Note. |
| 168 | * @property {any[]} ANAfterEntries The entries after Author's Note. | 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 | * @property {Set<any>} allActivatedEntries All entries. | 172 | * @property {Set<any>} allActivatedEntries All entries. |
| 170 | */ | 173 | */ |
| 171 | 174 | ||
| @@ -817,6 +820,7 @@ export const world_info_position = { | |||
| 817 | atDepth: 4, | 820 | atDepth: 4, |
| 818 | EMTop: 5, | 821 | EMTop: 5, |
| 819 | EMBottom: 6, | 822 | EMBottom: 6, |
| 823 | outlet: 7, | ||
| 820 | }; | 824 | }; |
| 821 | 825 | ||
| 822 | export const wi_anchor_position = { | 826 | export const wi_anchor_position = { |
| @@ -866,6 +870,7 @@ export async function getWorldInfoPrompt(chat, maxContext, isDryRun, globalScanD | |||
| 866 | worldInfoDepth: activatedWorldInfo.WIDepthEntries ?? [], | 870 | worldInfoDepth: activatedWorldInfo.WIDepthEntries ?? [], |
| 867 | anBefore: activatedWorldInfo.ANBeforeEntries ?? [], | 871 | anBefore: activatedWorldInfo.ANBeforeEntries ?? [], |
| 868 | anAfter: activatedWorldInfo.ANAfterEntries ?? [], | 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 | * Gets the name of the persona-bound lorebook. | 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 | function getPersonaBookCallback() { | 1049 | async function getPersonaBookCallback({ name, create }, _unnamedArg) { |
| 1043 | return power_user.persona_description_lorebook || ''; | 1050 | let 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 | * Gets the name of the character-bound lorebook. | 1067 | * Gets the name of the character-bound lorebook. |
| 1048 | * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments | 1068 | * @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments |
| 1049 | * @param {string} name Character name | 1069 | * @param {string} characterIdentifier Character name |
| 1050 | * @returns {string} The name of the character-bound lorebook, a JSON string of the character's lorebooks, or an empty string | 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 | function getCharBookCallback({ type }, name) { | 1072 | async function getCharBookCallback({ type, name, create }, characterIdentifier) { |
| 1053 | const context = getContext(); | 1073 | const context = getContext(); |
| 1054 | if (context.groupId && !name) throw new Error('This command is not available in groups without providing a character name'); | 1074 | if (context.groupId && !characterIdentifier) throw new Error('This command is not available in groups without providing a character name'); |
| 1055 | type = String(type ?? '').trim().toLowerCase() || 'primary'; | 1075 | type = String(type ?? '').trim().toLowerCase() || 'primary'; |
| 1056 | name = String(name ?? '') || context.characters[context.characterId]?.avatar || null; | 1076 | characterIdentifier = String(characterIdentifier ?? '') || context.characters[context.characterId]?.avatar || null; |
| 1057 | const character = findChar({ name }); | 1077 | const character = findChar({ name: characterIdentifier }); |
| 1058 | if (!character) { | 1078 | if (!character) { |
| 1059 | toastr.error(t`Character not found.`); | 1079 | toastr.error(t`Character not found.`); |
| 1060 | return ''; | 1080 | return ''; |
| 1061 | } | 1081 | } |
| 1062 | const books = []; | 1082 | const books = []; |
| 1063 | if (type === 'all' || type === 'primary') { | 1083 | if (type === 'all' || type === 'primary' && character.data?.extensions?.world) { |
| 1064 | books.push(character.data?.extensions?.world); | 1084 | books.push(character.data.extensions.world); |
| 1065 | } | 1085 | } |
| 1066 | if (type === 'all' || type === 'additional') { | 1086 | if (type === 'all' || type === 'additional') { |
| 1067 | const fileName = getCharaFilename(context.characters.indexOf(character)); | 1087 | const fileName = getCharaFilename(context.characters.indexOf(character)); |
| 1068 | const extraCharLore = world_info.charLore?.find((e) => e.name === fileName); | 1088 | const extraCharLore = world_info.charLore?.find((e) => e.name === fileName); |
| 1069 | if (extraCharLore && Array.isArray(extraCharLore.extraBooks)) { | 1089 | if (extraCharLore && Array.isArray(extraCharLore.extraBooks)) { |
| 1070 | books.push(...extraCharLore.extraBooks); | 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 | return type === 'primary' ? (books[0] ?? '') : JSON.stringify(books.filter(onlyUnique).filter(Boolean)); | 1108 | return type === 'primary' ? (books[0] ?? '') : JSON.stringify(books.filter(onlyUnique).filter(Boolean)); |
| 1074 | } | 1109 | } |
| 1075 | 1110 | ||
| @@ -1090,10 +1125,23 @@ function registerWorldInfoSlashCommands() { | |||
| 1090 | return chat_metadata[METADATA_KEY]; | 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 | // Use the provided name if it's not in use | 1142 | // Use the provided name if it's not in use |
| 1095 | if (typeof args.name === 'string') { | 1143 | if (typeof possibleName === 'string') { |
| 1096 | const name = String(args.name); | 1144 | const name = String(possibleName); |
| 1097 | if (world_names.includes(name)) { | 1145 | if (world_names.includes(name)) { |
| 1098 | throw new Error('This World Info file name is already in use'); | 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 | // Replace non-alphanumeric characters with underscores, cut to 64 characters | 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 | async function findBookEntryCallback(args, value) { | 1162 | async function findBookEntryCallback(args, value) { |
| @@ -1545,6 +1593,15 @@ function registerWorldInfoSlashCommands() { | |||
| 1545 | isRequired: false, | 1593 | isRequired: false, |
| 1546 | acceptsMultiple: false, | 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 | aliases: ['getchatlore', 'getchatwi'], | 1606 | aliases: ['getchatlore', 'getchatwi'], |
| 1550 | })); | 1607 | })); |
| @@ -1559,6 +1616,25 @@ function registerWorldInfoSlashCommands() { | |||
| 1559 | name: 'getpersonabook', | 1616 | name: 'getpersonabook', |
| 1560 | callback: getPersonaBookCallback, | 1617 | callback: getPersonaBookCallback, |
| 1561 | returns: 'lorebook name', | 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 | 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.', | 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 | aliases: ['getpersonalore', 'getpersonawi'], | 1639 | aliases: ['getpersonalore', 'getpersonawi'], |
| 1564 | })); | 1640 | })); |
| @@ -1574,6 +1650,22 @@ function registerWorldInfoSlashCommands() { | |||
| 1574 | enumList: ['primary', 'additional', 'all'], | 1650 | enumList: ['primary', 'additional', 'all'], |
| 1575 | defaultValue: 'primary', | 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 | unnamedArgumentList: [ | 1670 | unnamedArgumentList: [ |
| 1579 | SlashCommandArgument.fromProps({ | 1671 | SlashCommandArgument.fromProps({ |
| @@ -3425,6 +3517,19 @@ export async function getWorldEntry(name, data, entry) { | |||
| 3425 | contentInput.val(entry.content).trigger('input', { skipCount: true, noSave: true }); | 3517 | contentInput.val(entry.content).trigger('input', { skipCount: true, noSave: true }); |
| 3426 | editTemplate.find('.editor_maximize').attr('data-for', contentInputId); | 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 | // Scan depth | 3533 | // Scan depth |
| 3429 | const scanDepthInput = editTemplate.find('input[name="scanDepth"]'); | 3534 | const scanDepthInput = editTemplate.find('input[name="scanDepth"]'); |
| 3430 | scanDepthInput.data('uid', entry.uid); | 3535 | scanDepthInput.data('uid', entry.uid); |
| @@ -3597,63 +3702,105 @@ export async function getWorldEntry(name, data, entry) { | |||
| 3597 | return headerTemplate; | 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 | * @param {any} data WI data | 3708 | * @param {object} [opt={}] - Optional 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 | return function (control, input, output) { | 3715 | return function (control, input, output) { |
| 3607 | const uid = $(control).data('uid'); | 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 | if (entry.uid == uid) continue; | 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 | const needle = input.term.toLowerCase(); | 3733 | const s = String(v).trim(); |
| 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 | output(result); | 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 |
| 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 | }); | ||
| 3644 | } | 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 | * Create an autocomplete for the inclusion group. | 3803 | * Create an autocomplete for an input element. |
| 3657 | * @param {JQuery<HTMLElement>} input - Input element to attach the autocomplete to | 3804 | * @param {JQuery<HTMLElement>} input - Input element to attach the autocomplete to |
| 3658 | * @param {(control: JQuery<HTMLElement>, input: any, output: any) => any} callback - Source data callbacks | 3805 | * @param {(control: JQuery<HTMLElement>, input: any, output: any) => any} callback - Source data callbacks |
| 3659 | * @param {object} [options={}] - Optional arguments | 3806 | * @param {object} [options={}] - Optional arguments |
| @@ -3770,6 +3917,7 @@ export const newWorldInfoEntryDefinition = { | |||
| 3770 | probability: { default: 100, type: 'number' }, | 3917 | probability: { default: 100, type: 'number' }, |
| 3771 | useProbability: { default: true, type: 'boolean' }, | 3918 | useProbability: { default: true, type: 'boolean' }, |
| 3772 | depth: { default: DEFAULT_DEPTH, type: 'number' }, | 3919 | depth: { default: DEFAULT_DEPTH, type: 'number' }, |
| 3920 | outletName: { default: '', type: 'string' }, | ||
| 3773 | group: { default: '', type: 'string' }, | 3921 | group: { default: '', type: 'string' }, |
| 3774 | groupOverride: { default: false, type: 'boolean' }, | 3922 | groupOverride: { default: false, type: 'boolean' }, |
| 3775 | groupWeight: { default: DEFAULT_WEIGHT, type: 'number' }, | 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 | return true; | 4096 | return true; |
| 3939 | } | 4097 | } |
| 3940 | 4098 | ||
| @@ -4281,7 +4439,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData | |||
| 4281 | timedEffects.checkTimedEffects(); | 4439 | timedEffects.checkTimedEffects(); |
| 4282 | 4440 | ||
| 4283 | if (sortedEntries.length === 0) { | 4441 | if (sortedEntries.length === 0) { |
| 4284 | return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], ANBeforeEntries: [], ANAfterEntries: [], allActivatedEntries: new Set() }; | 4442 | return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], ANBeforeEntries: [], ANAfterEntries: [], outletEntries: {}, allActivatedEntries: new Set() }; |
| 4285 | } | 4443 | } |
| 4286 | 4444 | ||
| 4287 | /** @type {number[]} Represents the delay levels for entries that are delayed until recursion */ | 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 | const ANTopEntries = []; | 4840 | const ANTopEntries = []; |
| 4683 | const ANBottomEntries = []; | 4841 | const ANBottomEntries = []; |
| 4684 | const WIDepthEntries = []; | 4842 | const WIDepthEntries = []; |
| 4843 | /** @type {{[key: string]: string[]}} */ | ||
| 4844 | const WIOutletEntries = {}; | ||
| 4685 | 4845 | ||
| 4686 | // Appends from insertion order 999 to 1. Use unshift for this purpose | 4846 | // Appends from insertion order 999 to 1. Use unshift for this purpose |
| 4687 | // TODO (kingbri): Change to use WI Anchor positioning instead of separate top/bottom arrays | 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 | break; | 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 | default: | 4905 | default: |
| 4734 | break; | 4906 | break; |
| 4735 | } | 4907 | } |
| @@ -4751,7 +4923,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData | |||
| 4751 | console.log(`[WI] ${isDryRun ? 'Hypothetically adding' : 'Adding'} ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values())); | 4923 | console.log(`[WI] ${isDryRun ? 'Hypothetically adding' : 'Adding'} ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values())); |
| 4752 | console.debug(`[WI] --- DONE${isDryRun ? ' (DRY RUN)' : ''} ---`); | 4924 | console.debug(`[WI] --- DONE${isDryRun ? ' (DRY RUN)' : ''} ---`); |
| 4753 | 4925 | ||
| 4754 | return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, ANBeforeEntries: ANTopEntries, ANAfterEntries: ANBottomEntries, allActivatedEntries: new Set(allActivatedEntries.values()) }; | 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 | displayIndex: index, | 5143 | displayIndex: index, |
| 4972 | probability: 100, | 5144 | probability: 100, |
| 4973 | useProbability: true, | 5145 | useProbability: true, |
| 5146 | outletName: '', | ||
| 4974 | group: '', | 5147 | group: '', |
| 4975 | groupOverride: false, | 5148 | groupOverride: false, |
| 4976 | groupWeight: DEFAULT_WEIGHT, | 5149 | groupWeight: DEFAULT_WEIGHT, |
| @@ -5015,6 +5188,7 @@ function convertRisuLorebook(inputObj) { | |||
| 5015 | displayIndex: index, | 5188 | displayIndex: index, |
| 5016 | probability: entry.activationPercent ?? 100, | 5189 | probability: entry.activationPercent ?? 100, |
| 5017 | useProbability: entry.activationPercent ?? true, | 5190 | useProbability: entry.activationPercent ?? true, |
| 5191 | outletName: '', | ||
| 5018 | group: '', | 5192 | group: '', |
| 5019 | groupOverride: false, | 5193 | groupOverride: false, |
| 5020 | groupWeight: DEFAULT_WEIGHT, | 5194 | groupWeight: DEFAULT_WEIGHT, |
| @@ -5064,6 +5238,7 @@ function convertNovelLorebook(inputObj) { | |||
| 5064 | displayIndex: index, | 5238 | displayIndex: index, |
| 5065 | probability: 100, | 5239 | probability: 100, |
| 5066 | useProbability: true, | 5240 | useProbability: true, |
| 5241 | outletName: '', | ||
| 5067 | group: '', | 5242 | group: '', |
| 5068 | groupOverride: false, | 5243 | groupOverride: false, |
| 5069 | groupWeight: DEFAULT_WEIGHT, | 5244 | groupWeight: DEFAULT_WEIGHT, |
| @@ -5114,6 +5289,7 @@ export function convertCharacterBook(characterBook) { | |||
| 5114 | useProbability: entry.extensions?.useProbability ?? true, | 5289 | useProbability: entry.extensions?.useProbability ?? true, |
| 5115 | depth: entry.extensions?.depth ?? DEFAULT_DEPTH, | 5290 | depth: entry.extensions?.depth ?? DEFAULT_DEPTH, |
| 5116 | selectiveLogic: entry.extensions?.selectiveLogic ?? world_info_logic.AND_ANY, | 5291 | selectiveLogic: entry.extensions?.selectiveLogic ?? world_info_logic.AND_ANY, |
| 5292 | outletName: entry.extensions?.outlet_name ?? '', | ||
| 5117 | group: entry.extensions?.group ?? '', | 5293 | group: entry.extensions?.group ?? '', |
| 5118 | groupOverride: entry.extensions?.group_override ?? false, | 5294 | groupOverride: entry.extensions?.group_override ?? false, |
| 5119 | groupWeight: entry.extensions?.group_weight ?? DEFAULT_WEIGHT, | 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 | export function initWorldInfo() { | 5821 | export function initWorldInfo() { |
| 5562 | $('#world_info').on('mousedown change', async function (e) { | 5822 | $('#world_info').on('mousedown change', async function (e) { |
| 5563 | // If there's no world names, don't do anything | 5823 | // If there's no world names, don't do anything |
| @@ -133,6 +133,7 @@ | |||
| 133 | * { | 133 | * { |
| 134 | box-sizing: border-box; | 134 | box-sizing: border-box; |
| 135 | -webkit-font-smoothing: antialiased; | 135 | -webkit-font-smoothing: antialiased; |
| 136 | -webkit-tap-highlight-color: transparent; | ||
| 136 | -moz-osx-font-smoothing: grayscale; | 137 | -moz-osx-font-smoothing: grayscale; |
| 137 | text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor); | 138 | text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor); |
| 138 | } | 139 | } |
| @@ -599,6 +600,14 @@ input[type='checkbox']:focus-visible { | |||
| 599 | max-height: var(--doc-height); | 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 | .mes .mes_timer, | 611 | .mes .mes_timer, |
| 603 | .mes .mesIDDisplay, | 612 | .mes .mesIDDisplay, |
| 604 | .mes .tokenCounterDisplay { | 613 | .mes .tokenCounterDisplay { |
| @@ -2923,6 +2932,10 @@ select option:not(:checked) { | |||
| 2923 | color: var(--white70a); | 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 | #rm_api_block { | 2941 | #rm_api_block { |
| @@ -2940,6 +2953,7 @@ select option:not(:checked) { | |||
| 2940 | text-align: center; | 2953 | text-align: center; |
| 2941 | } | 2954 | } |
| 2942 | 2955 | ||
| 2956 | .menu_button[disabled], | ||
| 2943 | .menu_button.disabled { | 2957 | .menu_button.disabled { |
| 2944 | filter: brightness(75%) grayscale(1); | 2958 | filter: brightness(75%) grayscale(1); |
| 2945 | opacity: 0.5; | 2959 | opacity: 0.5; |
| @@ -3001,6 +3015,11 @@ select option:not(:checked) { | |||
| 3001 | display: block; | 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 | .menu_button.api_button:hover { | 3023 | .menu_button.api_button:hover { |
| 3005 | background-color: var(--active); | 3024 | background-color: var(--active); |
| 3006 | } | 3025 | } |
| @@ -3286,6 +3305,10 @@ input[type=search]:focus::-webkit-search-cancel-button { | |||
| 3286 | position: relative; | 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 | .group_member .queue_position:not(:empty)::before { | 3312 | .group_member .queue_position:not(:empty)::before { |
| 3290 | content: "#"; | 3313 | content: "#"; |
| 3291 | } | 3314 | } |
| @@ -3675,8 +3698,8 @@ grammarly-extension { | |||
| 3675 | min-width: calc(1.25em + 12px); | 3698 | min-width: calc(1.25em + 12px); |
| 3676 | } | 3699 | } |
| 3677 | 3700 | ||
| 3678 | .menu_button:not(.disabled):hover, | 3701 | .menu_button:not(.disabled):not([disabled]):hover, |
| 3679 | .menu_button:not(.disabled).active { | 3702 | .menu_button:not(.disabled):not([disabled]).active { |
| 3680 | background-color: var(--white30a); | 3703 | background-color: var(--white30a); |
| 3681 | } | 3704 | } |
| 3682 | 3705 | ||
| @@ -5740,7 +5763,8 @@ body:not(.movingUI) .drawer-content.maximized { | |||
| 5740 | } | 5763 | } |
| 5741 | 5764 | ||
| 5742 | .paginationjs-pages ul li a { | 5765 | .paginationjs-pages ul li a { |
| 5743 | padding: 0.05em 0.5em; | 5766 | padding: 0.25em; |
| 5767 | font-family: 'Font Awesome 6 Free'; | ||
| 5744 | text-decoration: none; | 5768 | text-decoration: none; |
| 5745 | color: var(--SmartThemeBodyColor); | 5769 | color: var(--SmartThemeBodyColor); |
| 5746 | border: 1px solid var(--SmartThemeBorderColor); | 5770 | border: 1px solid var(--SmartThemeBorderColor); |
| @@ -124,6 +124,16 @@ const keyMigrationMap = [ | |||
| 124 | migrate: () => void 0, | 124 | migrate: () => void 0, |
| 125 | remove: true, | 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 | const useTools = Array.isArray(request.body.tools) && request.body.tools.length > 0; | 154 | const useTools = Array.isArray(request.body.tools) && request.body.tools.length > 0; |
| 155 | const useSystemPrompt = Boolean(request.body.claude_use_sysprompt); | 155 | const useSystemPrompt = Boolean(request.body.claude_use_sysprompt); |
| 156 | const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, useTools, getPromptNames(request)); | 156 | const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, useTools, getPromptNames(request)); |
| 157 | const useThinking = /^claude-(3-7|opus-4|sonnet-4)/.test(request.body.model); | 157 | const useThinking = /^claude-(3-7|opus-4|sonnet-4|haiku-4-5)/.test(request.body.model); |
| 158 | const useWebSearch = /^claude-(3-5|3-7|opus-4|sonnet-4)/.test(request.body.model) && Boolean(request.body.enable_web_search); | 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 | const isOpus41 = /^claude-opus-4-1/.test(request.body.model); | 159 | const isLimitedSampling = /^claude-(opus-4-1|sonnet-4-5|haiku-4-5)/.test(request.body.model); |
| 160 | const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m'; | 160 | const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m'; |
| 161 | let fixThinkingPrefill = false; | 161 | let fixThinkingPrefill = false; |
| 162 | // Add custom stop sequences | 162 | // Add custom stop sequences |
| @@ -226,7 +226,7 @@ async function sendClaudeRequest(request, response) { | |||
| 226 | betaHeaders.push('extended-cache-ttl-2025-04-11'); | 226 | betaHeaders.push('extended-cache-ttl-2025-04-11'); |
| 227 | } | 227 | } |
| 228 | 228 | ||
| 229 | if (isOpus41) { | 229 | if (isLimitedSampling) { |
| 230 | if (requestBody.top_p < 1) { | 230 | if (requestBody.top_p < 1) { |
| 231 | delete requestBody.temperature; | 231 | delete requestBody.temperature; |
| 232 | } else { | 232 | } else { |
| @@ -377,24 +377,16 @@ async function sendMakerSuiteRequest(request, response) { | |||
| 377 | 'gemini-2.0-flash-exp-image-generation', | 377 | 'gemini-2.0-flash-exp-image-generation', |
| 378 | 'gemini-2.0-flash-preview-image-generation', | 378 | 'gemini-2.0-flash-preview-image-generation', |
| 379 | 'gemini-2.5-flash-image-preview', | 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 | const noSearchModels = [ | 385 | const noSearchModels = [ |
| 393 | 'gemini-2.0-flash-lite', | 386 | 'gemini-2.0-flash-lite', |
| 394 | 'gemini-2.0-flash-lite-001', | 387 | 'gemini-2.0-flash-lite-001', |
| 395 | 'gemini-2.0-flash-lite-preview-02-05', | 388 | 'gemini-2.0-flash-lite-preview-02-05', |
| 396 | 'gemini-1.5-flash-8b-exp-0924', | 389 | 'gemini-robotics-er-1.5-preview', |
| 397 | 'gemini-1.5-flash-8b-exp-0827', | ||
| 398 | ]; | 390 | ]; |
| 399 | // #endregion | 391 | // #endregion |
| 400 | 392 | ||
| @@ -413,15 +405,8 @@ async function sendMakerSuiteRequest(request, response) { | |||
| 413 | const prompt = convertGooglePrompt(request.body.messages, model, useSystemPrompt, getPromptNames(request)); | 405 | const prompt = convertGooglePrompt(request.body.messages, model, useSystemPrompt, getPromptNames(request)); |
| 414 | let safetySettings = GEMINI_SAFETY; | 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 | if (enableWebSearch && !enableImageModality && !isGemma && !isLearnLM && !noSearchModels.includes(model)) { | 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 | if (Array.isArray(request.body.tools) && request.body.tools.length > 0 && !enableImageModality && !isGemma) { | 412 | if (Array.isArray(request.body.tools) && request.body.tools.length > 0 && !enableImageModality && !isGemma) { |
| @@ -1012,7 +997,7 @@ async function sendXaiRequest(request, response) { | |||
| 1012 | bodyParams['stop'] = request.body.stop; | 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 | bodyParams['reasoning_effort'] = request.body.reasoning_effort === 'high' ? 'high' : 'low'; | 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 | 'frequency_penalty': request.body.frequency_penalty, | 1240 | 'frequency_penalty': request.body.frequency_penalty, |
| 1256 | 'top_p': request.body.top_p, | 1241 | 'top_p': request.body.top_p, |
| 1257 | 'top_k': request.body.top_k, | 1242 | 'top_k': request.body.top_k, |
| 1243 | 'logit_bias': request.body.logit_bias, | ||
| 1258 | 'seed': request.body.seed, | 1244 | 'seed': request.body.seed, |
| 1259 | ...bodyParams, | 1245 | ...bodyParams, |
| 1260 | }; | 1246 | }; |
| @@ -1848,9 +1834,9 @@ router.post('/generate', function (request, response) { | |||
| 1848 | } | 1834 | } |
| 1849 | 1835 | ||
| 1850 | const cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number'); | 1836 | const cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number'); |
| 1851 | const isClaude3or4 = /anthropic\/claude-(3|opus-4|sonnet-4)/.test(request.body.model); | 1837 | const isClaude3or4 = /anthropic\/claude-(3|opus-4|sonnet-4|haiku-4)/.test(request.body.model); |
| 1852 | const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m'; | 1838 | const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m'; |
| 1853 | if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && isClaude3or4) { | 1839 | if (Array.isArray(request.body.messages) && Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && isClaude3or4) { |
| 1854 | cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth, cacheTTL); | 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 | router.use('/multimodal-models', multimodalModels); | 2259 | router.use('/multimodal-models', multimodalModels); |
| @@ -108,6 +108,7 @@ class DiskCache { | |||
| 108 | dir: this.cachePath, | 108 | dir: this.cachePath, |
| 109 | ttl: false, | 109 | ttl: false, |
| 110 | forgiveParseErrors: true, | 110 | forgiveParseErrors: true, |
| 111 | expiredInterval: 0, | ||
| 111 | // @ts-ignore | 112 | // @ts-ignore |
| 112 | maxFileDescriptors: 100, | 113 | maxFileDescriptors: 100, |
| 113 | }); | 114 | }); |
| @@ -688,6 +689,7 @@ function convertWorldInfoToCharacterBook(name, entries) { | |||
| 688 | useProbability: entry.useProbability ?? false, | 689 | useProbability: entry.useProbability ?? false, |
| 689 | depth: entry.depth ?? 4, | 690 | depth: entry.depth ?? 4, |
| 690 | selectiveLogic: entry.selectiveLogic ?? 0, | 691 | selectiveLogic: entry.selectiveLogic ?? 0, |
| 692 | outlet_name: entry.outletName ?? '', | ||
| 691 | group: entry.group ?? '', | 693 | group: entry.group ?? '', |
| 692 | group_override: entry.groupOverride ?? false, | 694 | group_override: entry.groupOverride ?? false, |
| 693 | group_weight: entry.groupWeight ?? null, | 695 | group_weight: entry.groupWeight ?? null, |
| @@ -628,6 +628,7 @@ router.post('/import', validateAvatarUrlMiddleware, function (request, response) | |||
| 628 | const avatarUrl = (request.body.avatar_url).replace('.png', ''); | 628 | const avatarUrl = (request.body.avatar_url).replace('.png', ''); |
| 629 | const characterName = request.body.character_name; | 629 | const characterName = request.body.character_name; |
| 630 | const userName = request.body.user_name || 'User'; | 630 | const userName = request.body.user_name || 'User'; |
| 631 | const fileNames = []; | ||
| 631 | 632 | ||
| 632 | if (!request.file) { | 633 | if (!request.file) { |
| 633 | return response.sendStatus(400); | 634 | return response.sendStatus(400); |
| @@ -662,6 +663,7 @@ router.post('/import', validateAvatarUrlMiddleware, function (request, response) | |||
| 662 | const handleChat = (chat) => { | 663 | const handleChat = (chat) => { |
| 663 | const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`; | 664 | const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`; |
| 664 | const filePath = path.join(request.user.directories.chats, avatarUrl, fileName); | 665 | const filePath = path.join(request.user.directories.chats, avatarUrl, fileName); |
| 666 | fileNames.push(fileName); | ||
| 665 | writeFileAtomicSync(filePath, chat, 'utf8'); | 667 | writeFileAtomicSync(filePath, chat, 'utf8'); |
| 666 | }; | 668 | }; |
| 667 | 669 | ||
| @@ -673,7 +675,7 @@ router.post('/import', validateAvatarUrlMiddleware, function (request, response) | |||
| 673 | handleChat(chat); | 675 | handleChat(chat); |
| 674 | } | 676 | } |
| 675 | 677 | ||
| 676 | return response.send({ res: true }); | 678 | return response.send({ res: true, fileNames }); |
| 677 | } | 679 | } |
| 678 | 680 | ||
| 679 | if (format === 'jsonl') { | 681 | if (format === 'jsonl') { |
| @@ -700,13 +702,14 @@ router.post('/import', validateAvatarUrlMiddleware, function (request, response) | |||
| 700 | 702 | ||
| 701 | const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`; | 703 | const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`; |
| 702 | const filePath = path.join(request.user.directories.chats, avatarUrl, fileName); | 704 | const filePath = path.join(request.user.directories.chats, avatarUrl, fileName); |
| 705 | fileNames.push(fileName); | ||
| 703 | if (flattenedChat !== data) { | 706 | if (flattenedChat !== data) { |
| 704 | writeFileAtomicSync(filePath, flattenedChat, 'utf8'); | 707 | writeFileAtomicSync(filePath, flattenedChat, 'utf8'); |
| 705 | } else { | 708 | } else { |
| 706 | fs.copyFileSync(pathToUpload, filePath); | 709 | fs.copyFileSync(pathToUpload, filePath); |
| 707 | } | 710 | } |
| 708 | fs.unlinkSync(pathToUpload); | 711 | fs.unlinkSync(pathToUpload); |
| 709 | response.send({ res: true }); | 712 | response.send({ res: true, fileNames }); |
| 710 | } | 713 | } |
| 711 | } catch (error) { | 714 | } catch (error) { |
| 712 | console.error(error); | 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 | router.post('/generate-image', async (request, response) => { | 426 | router.post('/generate-image', async (request, response) => { |
| 329 | try { | 427 | try { |
| 330 | const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI); | 428 | const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI); |
| @@ -561,12 +561,13 @@ router.post('/find', (request, response) => { | |||
| 561 | } | 561 | } |
| 562 | 562 | ||
| 563 | const manager = new SecretManager(request.user.directories); | 563 | const manager = new SecretManager(request.user.directories); |
| 564 | const secretValue = manager.readSecret(key, id); | 564 | const state = manager.getSecretState(); |
| 565 | 565 | ||
| 566 | if (!secretValue) { | 566 | if (!state[key]) { |
| 567 | return response.sendStatus(404); | 567 | return response.sendStatus(404); |
| 568 | } | 568 | } |
| 569 | 569 | ||
| 570 | const secretValue = manager.readSecret(key, id); | ||
| 570 | return response.send({ value: secretValue }); | 571 | return response.send({ value: secretValue }); |
| 571 | } catch (error) { | 572 | } catch (error) { |
| 572 | console.error('Error finding secret:', error); | 573 | console.error('Error finding secret:', error); |
| @@ -187,7 +187,7 @@ function getSourceSettings(source, request) { | |||
| 187 | case 'palm': | 187 | case 'palm': |
| 188 | case 'vertexai': | 188 | case 'vertexai': |
| 189 | return { | 189 | return { |
| 190 | model: String(request.body.model || 'text-embedding-004'), | 190 | model: String(request.body.model || 'text-embedding-005'), |
| 191 | request: request, // Pass the request object to get API key and URL | 191 | request: request, // Pass the request object to get API key and URL |
| 192 | }; | 192 | }; |
| 193 | case 'mistral': | 193 | case 'mistral': |
| @@ -14,8 +14,9 @@ export default function getWebpackServeMiddleware() { | |||
| 14 | const publicLibConfig = getPublicLibConfig(); | 14 | const publicLibConfig = getPublicLibConfig(); |
| 15 | const outputPath = publicLibConfig.output?.path; | 15 | const outputPath = publicLibConfig.output?.path; |
| 16 | const outputFile = publicLibConfig.output?.filename; | 16 | const outputFile = publicLibConfig.output?.filename; |
| 17 | const parsedPath = path.parse(req.path); | ||
| 17 | 18 | ||
| 18 | if (req.method === 'GET' && path.parse(req.path).base === outputFile) { | 19 | if (req.method === 'GET' && parsedPath.dir === '/' && parsedPath.base === outputFile) { |
| 19 | return res.sendFile(outputFile, { root: outputPath }); | 20 | return res.sendFile(outputFile, { root: outputPath }); |
| 20 | } | 21 | } |
| 21 | 22 | ||
| @@ -24,7 +24,8 @@ import { serverDirectory } from './server-directory.js'; | |||
| 24 | export const KEY_PREFIX = 'user:'; | 24 | export const KEY_PREFIX = 'user:'; |
| 25 | const AVATAR_PREFIX = 'avatar:'; | 25 | const AVATAR_PREFIX = 'avatar:'; |
| 26 | const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean'); | 26 | const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean'); |
| 27 | const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false, 'boolean'); | 27 | const AUTHELIA_AUTH = getConfigValue('sso.autheliaAuth', false, 'boolean'); |
| 28 | const AUTHENTIK_AUTH = getConfigValue('sso.authentikAuth', false, 'boolean'); | ||
| 28 | const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false, 'boolean'); | 29 | const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false, 'boolean'); |
| 29 | const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); | 30 | const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); |
| 30 | 31 | ||
| @@ -511,6 +512,7 @@ export async function initUserStorage(dataRoot) { | |||
| 511 | await storage.init({ | 512 | await storage.init({ |
| 512 | dir: path.join(dataRoot, '_storage'), | 513 | dir: path.join(dataRoot, '_storage'), |
| 513 | ttl: false, // Never expire | 514 | ttl: false, // Never expire |
| 515 | expiredInterval: 0, | ||
| 514 | }); | 516 | }); |
| 515 | 517 | ||
| 516 | const keys = await getAllUserHandles(); | 518 | const keys = await getAllUserHandles(); |
| @@ -715,6 +717,10 @@ export async function tryAutoLogin(request, basicAuthMode) { | |||
| 715 | return true; | 717 | return true; |
| 716 | } | 718 | } |
| 717 | 719 | ||
| 720 | if (AUTHENTIK_AUTH && await authentikUserLogin(request)) { | ||
| 721 | return true; | ||
| 722 | } | ||
| 723 | |||
| 718 | if (basicAuthMode && PER_USER_BASIC_AUTH && await basicUserLogin(request)) { | 724 | if (basicAuthMode && PER_USER_BASIC_AUTH && await basicUserLogin(request)) { |
| 719 | return true; | 725 | return true; |
| 720 | } | 726 | } |
| @@ -745,20 +751,41 @@ async function singleUserLogin(request) { | |||
| 745 | } | 751 | } |
| 746 | 752 | ||
| 747 | /** | 753 | /** |
| 748 | * Tries auto-login with authlia trusted headers. | 754 | * Attempts auto-login using an Authelia header. |
| 749 | * https://www.authelia.com/integration/trusted-header-sso/introduction/ | 755 | * https://www.authelia.com/integration/trusted-header-sso/introduction/ |
| 750 | * @param {import('express').Request} request Request object | 756 | * @param {import('express').Request} request Request object |
| 751 | * @returns {Promise<boolean>} Whether auto-login was performed | 757 | * @returns {Promise<boolean>} Whether auto-login was performed |
| 752 | */ | 758 | */ |
| 753 | async function autheliaUserLogin(request) { | 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 | if (!request.session) { | 780 | if (!request.session) { |
| 755 | return false; | 781 | return false; |
| 756 | } | 782 | } |
| 757 | 783 | ||
| 758 | const remoteUser = request.get('Remote-User'); | 784 | const remoteUser = request.get(header); |
| 759 | if (!remoteUser) { | 785 | if (!remoteUser) { |
| 760 | return false; | 786 | return false; |
| 761 | } | 787 | } |
| 788 | console.debug(`Attempting auto-login for user from header ${header}: ${remoteUser}`); | ||
| 762 | 789 | ||
| 763 | const userHandles = await getAllUserHandles(); | 790 | const userHandles = await getAllUserHandles(); |
| 764 | for (const userHandle of userHandles) { | 791 | for (const userHandle of userHandles) { |