Compare commits

...

28 Commits

Author SHA1 Message Date
LenAnderson 2b9b72e98a fix blank check 2024-04-25 20:40:23 -04:00
LenAnderson 71b180ea2f Merge branch 'staging' into parser-v2 2024-04-25 20:34:03 -04:00
LenAnderson 26104bf738 re-enable blur 2024-04-25 20:33:42 -04:00
LenAnderson 8afc480fad add return types 2024-04-25 18:51:12 -04:00
LenAnderson a9e9bf8aac fix blanks 2024-04-25 18:51:07 -04:00
LenAnderson d097fd16f9 remove unused imports 2024-04-25 18:50:48 -04:00
LenAnderson d220f3e6f9 towards generic autocomplete 2024-04-25 18:31:14 -04:00
LenAnderson e531da615e cache autocomplete elements 2024-04-25 18:02:06 -04:00
LenAnderson 12a2f54095 add fromProps 2024-04-25 16:08:53 -04:00
LenAnderson b542c4395e apply autocomplete colors to stscript codeblocks (hljs) 2024-04-25 15:45:58 -04:00
LenAnderson c16965730c add missing import 2024-04-25 15:45:33 -04:00
LenAnderson a4483a4419 add hljs to autocomplete help examples 2024-04-25 15:45:17 -04:00
LenAnderson 3ff367a221 hljs stscript lang 2024-04-25 15:44:48 -04:00
LenAnderson 613ab6834e jsdoc 2024-04-25 15:44:10 -04:00
LenAnderson a909c70553 move parser settings to its own section 2024-04-25 15:41:35 -04:00
Cohee babb4cb57b Fix tag key for 0-index 2024-04-25 18:15:38 +03:00
Cohee 9fbb012697 Merge branch 'release' into staging 2024-04-25 12:56:17 +03:00
Cohee 0070950911 Revert grid view spacing 2024-04-25 12:26:21 +03:00
Cohee 62cf611fdc
Merge pull request #2138 from Wolfsblvt/fix-expression-list-resolve
Fix expression list resolve
2024-04-25 11:00:34 +03:00
RossAscends 75814433a6 dont default to hiding avatars on new installs 2024-04-25 14:42:48 +09:00
LenAnderson cf7c270046 remove args from help string 2024-04-24 23:58:58 -04:00
RossAscends e59a5b4449 toggle to hide chat avatars 2024-04-25 12:51:56 +09:00
Wolfsblvt 161e512805 Fix expression list resolve
- New expression api "LLM" still queried local classify model for expressions, fixed by returning default list
- Fixed failed API calls crashing Expressions extension
2024-04-25 04:29:20 +02:00
Cohee f90f370fed
Merge pull request #2134 from StefanDanielSchwarz/Phi-Instruct-presets
Phi Instruct context+instruct presets
2024-04-25 01:44:12 +03:00
Stefan Daniel Schwarz d34a0ee20e Phi Instruct context+instruct presets 2024-04-24 23:47:04 +02:00
Cohee 47b6562605
Merge pull request #2112 from SillyTavern/staging
Staging
2024-04-21 21:24:12 +03:00
deffcolony 1c9b89fdcc Create issue-auto-comments.yml 2024-04-16 12:48:29 +02:00
deffcolony 035dbe4901 added issue/pr label workflows
3 months of inactivity Bot posts a comment to remind about it and assigns a stale label No further activity - 5 work days passes bot closes the issue
2024-04-15 16:41:43 +02:00
29 changed files with 1448 additions and 728 deletions

2
.github/close-label.yml vendored Normal file
View File

@ -0,0 +1,2 @@
🐛 Bug: ✅ Fixed
🦄 Feature Request: ✅ Implemented

62
.github/issue-auto-comments.yml vendored Normal file
View File

@ -0,0 +1,62 @@
comment:
footer: |
---
> I am a bot, and this is an automated message 🤖
labels:
- name: ✖️ Invalid
labeled:
issue:
action: close
body: >
Hello @{{ issue.user.login }} your ticket has been marked as invalid.
Please ensure you follow the issue template, provide all requested info,
and be sure to check the docs + previous issues prior to raising tickets.
pr:
body: Thank you @{{ pull_request.user.login }} for suggesting this. Please follow the pull request templates.
action: close
- name: 👩‍💻 Good First Issue
labeled:
issue:
body: >
This issue has been marked as a good first issue for first-time contributors to implement!
This is a great way to support the project, while also improving your skills, you'll also be credited as a contributor once your PR is merged.
If you're new to SillyTavern [here are a collection of resources](https://docs.sillytavern.app/)
If you need any support at all, feel free to reach out via [Discord](https://discord.gg/sillytavern).
- name: ❌ wontfix
labeled:
issue:
action: close
body: >
This ticked has been marked as 'wontfix', which usually means it is out-of-scope, or not feasible at this time.
You can still fork the project and make the changes yourself.
- name: ✅ Fixed
labeled:
issue:
body: >
Hello @{{ issue.user.login }}! It looks like all or part of this issue has now been implemented.
- name: ‼️ High Priority
labeled:
issue:
body: >
This ticket has been marked as high priority, and has been bumped to the top of the priority list.
You should expect an implementation to be pushed out soon. Thank you for your patience.
- name: 💀 Spam
labeled:
issue:
action: close
locking: lock
lock_reason: spam
body: >
This issue has been identified as spam, and is now locked.
Users who repeatedly raise spam issues may be blocked or reported.
- name: ⛔ Don't Merge
labeled:
pr:
body: This PR has been temporarily blocked from merging.

View File

@ -0,0 +1,28 @@
# Based on a label applied to an issue, the bot will add a comment with some additional info
name: 🎯 Auto-Reply to Labeled Tickets
on:
issues:
types:
- labeled
- unlabeled
pull_request_target:
types:
- labeled
- unlabeled
permissions:
contents: read
issues: write
pull-requests: write
jobs:
comment:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Label Commenter
uses: peaceiris/actions-label-commenter@v1
with:
config_file: .github/issue-auto-comments.yml
github_token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}

View File

@ -0,0 +1,16 @@
# Detect and label pull requests that have merge conflicts
name: 🏗️ Check Merge Conflicts
on:
push:
branches:
- staging
jobs:
check-conflicts:
runs-on: ubuntu-latest
steps:
- uses: mschilde/auto-label-merge-conflicts@master
with:
CONFLICT_LABEL_NAME: "🚫 Merge Conflicts"
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
MAX_RETRIES: 5
WAIT_MS: 5000

View File

@ -0,0 +1,82 @@
# Closes any issues that no longer have user interaction
name: 🎯 Close Stale Issues
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *' # Runs every day at midnight UTC
jobs:
stale:
runs-on: ubuntu-latest
steps:
# Comment on, then close issues that haven't been updated for ages
- name: Close Stale Issues
uses: actions/stale@v4
with:
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
days-before-stale: 360
days-before-close: 5
operations-per-run: 30
remove-stale-when-updated: true
enable-statistics: true
stale-issue-message: >
This issue has gone 3 months without an update. To keep the ticket open, please indicate that it is still relevant in a comment below.
Otherwise it will be closed in 5 working days.
stale-pr-message: >
This PR is stale because it has been open 6 weeks with no activity. Either remove the stale label or comment below with a short update,
otherwise this PR will be closed in 5 days.
close-issue-message: >
This issue was automatically closed because it has been stalled for over 1 year with no activity.
close-pr-message: >
This pull request was automatically closed because it has been stalled for over 1 year with no activity.
stale-issue-label: '⚰️ Stale'
close-issue-label: '🕸️ Inactive'
stale-pr-label: '⚰️ Stale'
close-pr-label: '🕸️ Inactive'
exempt-issue-labels: '📌 Keep Open'
exempt-pr-labels: '📌 Keep Open'
labels-to-add-when-unstale: '📌 Keep Open'
# Comment on, then close issues that required a response from the user, but didn't get one
- name: Close Issues without Response
uses: actions/stale@v4
with:
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
days-before-stale: 5
days-before-close: 3
operations-per-run: 30
remove-stale-when-updated: true
stale-issue-message: >
Hi! Looks like additional info is required for this issue to be addressed.
Don't forget to provide this within the next few days to keep your ticket open.
close-issue-message: 'Issue closed due to no response from user.'
only-labels: '🚏 Awaiting User Response'
labels-to-remove-when-unstale: '🚏 Awaiting User Response, 🛑 No Response'
stale-issue-label: '🛑 No Response'
close-issue-label: '🕸️ Inactive'
exempt-issue-labels: '📌 Keep Open'
exempt-pr-labels: '📌 Keep Open'
# Comment on issues that we should have replied to
- name: Notify Repo Owner to Respond
uses: actions/stale@v4
with:
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
days-before-stale: 7
days-before-close: 365
operations-per-run: 30
remove-stale-when-updated: true
stale-issue-message: Hey SillyTavern, - Don't forget to respond!
stale-pr-message: Hey SillyTavern, - Don't forget to respond!
only-labels: '👤 Awaiting Maintainer Response'
labels-to-remove-when-unstale: '👤 Awaiting Maintainer Response'
close-issue-message: 'Closed due to no response from repo author for over a year'
close-pr-message: 'Closed due to no response from repo author for over a year'
stale-issue-label: '👤 Awaiting Maintainer Response'
stale-pr-label: '👤 Awaiting Maintainer Response'
close-issue-label: '🕸️ Inactive'
close-pr-label: '🕸️ Inactive'
exempt-issue-labels: '📌 Keep Open'
exempt-pr-labels: '📌 Keep Open'

39
.github/workflows/get-pr-size.yml vendored Normal file
View File

@ -0,0 +1,39 @@
# Adds a comment to new PRs, showing the compressed size and size difference of new code
# And also labels the PR based on the number of lines changes
name: 🌈 Check PR Size
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# Find and comment with compressed size
- name: Get Compressed Size
uses: preactjs/compressed-size-action@v2
with:
repo-token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
pattern: './dist/**/*.{js,css,html}'
strip-hash: '\\b\\w{8}\\.'
exclude: '**/node_modules/**'
minimum-change-threshold: 100
# Check number of lines of code added
- name: Label based on Lines of Code
uses: codelytv/pr-size-labeler@v1
with:
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
xs_max_size: '10'
s_max_size: '100'
m_max_size: '500'
l_max_size: '1000'
s_label: '🟩 PR - Small'
m_label: '🟨 PR - Medium'
l_label: '🟧 PR - Large'
xl_label: '🟥 PR - XL'
fail_if_xl: 'false'
message_if_xl: >
It looks like this PR is very large (over 1000 lines).
Try to avoid addressing multiple issues in a single PR, and
in the future consider breaking large tasks down into smaller steps.
This it to make reviewing, testing, reverting and general quality management easier.

View File

@ -0,0 +1,17 @@
# When a new comment is added to an issue, if it had the Stale or Awaiting User Response labels, then those labels will be removed
name: 🎯 Remove Pending Labels on Close
on:
issues:
types: [closed]
jobs:
remove-labels:
runs-on: ubuntu-latest
steps:
- name: Remove Labels when Closed
uses: actions-cool/issues-helper@v2
with:
actions: remove-labels
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
labels: '🚏 Awaiting User Response,⚰️ Stale,👤 Awaiting Maintainer Response'

View File

@ -0,0 +1,42 @@
# When a new comment is added to an issue, if it had the Stale or Awaiting User Response labels, then those labels will be removed
name: 🎯 Add/ Remove Awaiting Response Labels
on:
issue_comment:
types: [created]
jobs:
remove-stale:
runs-on: ubuntu-latest
if: ${{ github.event.comment.author_association != 'COLLABORATOR' && github.event.comment.author_association != 'OWNER' }}
steps:
- name: Remove Stale labels when Updated
uses: actions-cool/issues-helper@v2
with:
actions: remove-labels
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
labels: '🚏 Awaiting User Response,⚰️ Stale'
add-awaiting-author:
runs-on: ubuntu-latest
if: ${{!github.event.issue.pull_request && github.event.comment.author_association != 'COLLABORATOR' && github.event.comment.author_association != 'OWNER' && github.event.issue.state == 'open' }}
steps:
- name: Add Awaiting Author labels when Updated
uses: actions-cool/issues-helper@v2
with:
actions: add-labels
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
labels: '👤 Awaiting Maintainer Response'
remove-awaiting-author:
runs-on: ubuntu-latest
if: ${{ github.event.comment.author_association == 'OWNER' }}
steps:
- name: Remove Awaiting Author labels when Updated
uses: actions-cool/issues-helper@v2
with:
actions: remove-labels
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
labels: '👤 Awaiting Maintainer Response'

View File

@ -539,6 +539,10 @@
"filename": "presets/context/Llama 3 Instruct.json",
"type": "context"
},
{
"filename": "presets/context/Phi.json",
"type": "context"
},
{
"filename": "presets/instruct/Adventure.json",
"type": "instruct"
@ -631,6 +635,10 @@
"filename": "presets/instruct/Llama 3 Instruct.json",
"type": "instruct"
},
{
"filename": "presets/instruct/Phi.json",
"type": "instruct"
},
{
"filename": "presets/moving-ui/Default.json",
"type": "moving_ui"

View File

@ -0,0 +1,12 @@
{
"story_string": "<|system|>\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}}<|end|>\n",
"example_separator": "",
"chat_start": "",
"use_stop_strings": false,
"allow_jailbreak": false,
"always_force_name2": true,
"trim_sentences": false,
"include_newline": false,
"single_line": false,
"name": "Phi"
}

View File

@ -0,0 +1,24 @@
{
"system_prompt": "Write {{char}}'s next reply in this fictional roleplay with {{user}}.",
"input_sequence": "<|user|>\n",
"output_sequence": "<|assistant|>\n",
"first_output_sequence": "",
"last_output_sequence": "",
"system_sequence_prefix": "",
"system_sequence_suffix": "",
"stop_sequence": "<|end|>",
"wrap": false,
"macro": true,
"names": true,
"names_force_groups": true,
"activation_regex": "",
"skip_examples": false,
"output_suffix": "<|end|>\n",
"input_suffix": "<|end|>\n",
"system_sequence": "<|system|>\n",
"system_suffix": "<|end|>\n",
"user_alignment_message": "",
"last_system_sequence": "",
"system_same_as_user": false,
"name": "Phi"
}

View File

@ -19,7 +19,8 @@ body.no-timer .mes_timer,
body.no-timestamps .timestamp,
body.no-tokenCount .tokenCounterDisplay,
body.no-mesIDDisplay .mesIDDisplay,
body.no-modelIcons .icon-svg {
body.no-modelIcons .icon-svg,
body.hideChatAvatars .mesAvatarWrapper .avatar {
display: none !important;
}
@ -123,10 +124,16 @@ body.charListGrid #rm_print_characters_block .bogus_folder_select_back .avatar {
}
/* Hack for keeping the spacing */
/*
body.charListGrid #rm_print_characters_block .ch_add_placeholder {
display: flex !important;
opacity: 0;
}
*/
body.charListGrid #rm_print_characters_block .ch_additional_info {
display: none;
}
/*big avatars mode page-wide changes*/

View File

@ -3520,7 +3520,7 @@
<i class="fa-fw fa-solid fa-user-shield"></i>
<span data-i18n="Account">Account</span>
</div>
<div id="admin_button" class="margin0 menu_button_icon menu_button" >
<div id="admin_button" class="margin0 menu_button_icon menu_button">
<i class="fa-fw fa-solid fa-user-tie"></i>
<span data-i18n="Admin Panel">Admin Panel</span>
</div>
@ -3776,6 +3776,10 @@
<input id="mesIDDisplayEnabled" type="checkbox" />
<span data-i18n="Message IDs">Message IDs</span>
</label>
<label data-newbie-hidden for="hideChatAvatarsEnabled" class="checkbox_label" title="Hide avatars in chat messages." data-i18n="[title]Hide avatars in chat messages.">
<input id="hideChatAvatarsEnabled" type="checkbox" />
<span data-i18n="Hide Chat Avatars">Hide Chat Avatars</span>
</label>
<label data-newbie-hidden for="messageTokensEnabled" class="checkbox_label" title="Show the number of tokens for each message in the chat log." data-i18n="[title]Show the number of tokens in each message in the chat log">
<input id="messageTokensEnabled" type="checkbox" />
<span data-i18n="Show Message Token Count">Message Token Count</span>
@ -3800,33 +3804,6 @@
</label>
</div>
<h4><span data-i18n="Miscellaneous">Miscellaneous</span></h4>
<div title="Determines how STscript commands are found for autocomplete." data-i18n="[title]Determines how STscript commands are found for autocomplete.">
<label for="stscript_matching" data-i18n="STscript Matching">STscript Matching</label>
<select id="stscript_matching">
<option data-i18n="Starts with" value="strict">Starts with</option>
<option data-i18n="Includes" value="includes">Includes</option>
<option data-i18n="Fuzzy" value="fuzzy">Fuzzy</option>
</select>
</div>
<div title="Sets the style of the autocomplete for STscript." data-i18n="[title]Sets the style of the autocomplete for STscript.">
<label for="stscript_autocomplete_style" data-i18n="STscript Autocomplete Style">STscript Autocomplete Style</label>
<select id="stscript_autocomplete_style">
<option data-i18n="Follow Theme" value="theme">Follow Theme</option>
<option data-i18n="Dark" value="dark">Dark</option>
<option data-i18n="Light" value="light">Light</option>
</select>
</div>
<div title="Sets default flags for the STscript parser." data-i18n="[title]Sets default flags for the STscript parser.">
<label data-i18n="STscript Parser Flags">STscript Parser Flags</label>
<label class="checkbox_label" title="STRICT_ESCAPING." data-i18n="[title]STRICT_ESCAPING">
<input id="stscript_parser_flag_strict_escaping" type="checkbox" />
<span data-i18n="STRICT_ESCAPING">STRICT_ESCAPING</span>
</label>
<label class="checkbox_label" title="REPLACE_GETVAR." data-i18n="[title]REPLACE_GETVAR">
<input id="stscript_parser_flag_replace_getvar" type="checkbox" />
<span data-i18n="REPLACE_GETVAR">REPLACE_GETVAR</span>
</label>
</div>
<div title="If set in the advanced character definitions, this field will be displayed in the characters list." data-i18n="[title]If set in the advanced character definitions, this field will be displayed in the characters list.">
<label for="aux_field" data-i18n="Aux List Field">Aux List Field</label>
<select id="aux_field">
@ -4048,12 +4025,48 @@
</div>
</div>
</div>
<div data-newbie-hidden class="flex-container">
<div id="reload_chat" class="menu_button whitespacenowrap" data-i18n="[title]Reload and redraw the currently open chat" title="Reload and redraw the currently open chat.">
<span data-i18n="Reload Chat">Reload Chat</span>
<div name="STscriptToggles">
<h4 data-i18n="STscript Settings">STscript Settings</h4>
<div title="Determines how STscript commands are found for autocomplete." data-i18n="[title]Determines how STscript commands are found for autocomplete.">
<label for="stscript_matching" data-i18n="Autocomplete Matching">Autocomplete Matching</label>
<select id="stscript_matching">
<option data-i18n="Starts with" value="strict">Starts with</option>
<option data-i18n="Includes" value="includes">Includes</option>
<option data-i18n="Fuzzy" value="fuzzy">Fuzzy</option>
</select>
</div>
<div id="debug_menu" class="menu_button whitespacenowrap" data-i18n="Debug Menu">
Debug Menu
<div title="Sets the style of the autocomplete for STscript." data-i18n="[title]Sets the style of the autocomplete for STscript.">
<label for="stscript_autocomplete_style" data-i18n="Autocomplete Style">Autocomplete Style</label>
<div class="flex-container flexFlowRow alignItemsBaseline">
<select id="stscript_autocomplete_style">
<option data-i18n="Follow Theme" value="theme">Follow Theme</option>
<option data-i18n="Dark" value="dark">Dark</option>
<option data-i18n="Light" value="light">Light</option>
</select>
<div class="menu_button fa-solid fa-pen-to-square" title="Customize colors"></div>
</div>
</div>
<div title="Sets default flags for the STscript parser." data-i18n="[title]Sets default flags for the STscript parser.">
<label data-i18n="Parser Flags">Parser Flags</label>
<label class="checkbox_label" title="STRICT_ESCAPING." data-i18n="[title]STRICT_ESCAPING">
<input id="stscript_parser_flag_strict_escaping" type="checkbox" />
<span data-i18n="STRICT_ESCAPING">STRICT_ESCAPING</span>
</label>
<label class="checkbox_label" title="REPLACE_GETVAR." data-i18n="[title]REPLACE_GETVAR">
<input id="stscript_parser_flag_replace_getvar" type="checkbox" />
<span data-i18n="REPLACE_GETVAR">REPLACE_GETVAR</span>
</label>
</div>
</div>
<div name="DebugToggles">
<h4 data-i18n="Debug Options">Debug Options</h4>
<div data-newbie-hidden class="flex-container">
<div id="reload_chat" class="menu_button whitespacenowrap" data-i18n="[title]Reload and redraw the currently open chat" title="Reload and redraw the currently open chat.">
<span data-i18n="Reload Chat">Reload Chat</span>
</div>
<div id="debug_menu" class="menu_button whitespacenowrap" data-i18n="Debug Menu">
Debug Menu
</div>
</div>
</div>
</div>
@ -6093,4 +6106,4 @@
</script>
</body>
</html>
</html>

View File

@ -1273,13 +1273,10 @@ async function getExpressionsList() {
* @returns {Promise<string[]>}
*/
async function resolveExpressionsList() {
// get something for offline mode (default images)
if (!modules.includes('classify') && extension_settings.expressions.api == EXPRESSION_API.extras) {
return DEFAULT_EXPRESSIONS;
}
// See if we can retrieve a specific expression list from the API
try {
if (extension_settings.expressions.api == EXPRESSION_API.extras) {
// Check Extras api first, if enabled and that module active
if (extension_settings.expressions.api == EXPRESSION_API.extras && modules.includes('classify')) {
const url = new URL(getApiUrl());
url.pathname = '/api/classify/labels';
@ -1294,7 +1291,10 @@ async function getExpressionsList() {
expressionsList = data.labels;
return expressionsList;
}
} else {
}
// If running the local classify model (not using the LLM), we ask that one
if (extension_settings.expressions.api == EXPRESSION_API.local) {
const apiResult = await fetch('/api/extra/classify/labels', {
method: 'POST',
headers: getRequestHeaders(),
@ -1306,11 +1306,12 @@ async function getExpressionsList() {
return expressionsList;
}
}
}
catch (error) {
} catch (error) {
console.log(error);
return [];
}
// If there was no specific list, or an error, just return the default expressions
return DEFAULT_EXPRESSIONS;
}
const result = await resolveExpressionsList();

View File

@ -175,10 +175,6 @@ export class SlashCommandHandler {
</li>
</ul>
</div>
<div>
<strong>Arguments:</strong>
<pre>${qrArgs}</pre>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update',

View File

@ -181,6 +181,7 @@ let power_user = {
timestamps_enabled: true,
timestamp_model_icon: false,
mesIDDisplay_enabled: false,
hideChatAvatars_enabled: false,
max_context_unlocked: false,
message_token_count_enabled: false,
expand_message_actions: false,
@ -306,6 +307,7 @@ const storage_keys = {
timestamps_enabled: 'TimestampsEnabled',
timestamp_model_icon: 'TimestampModelIcon',
mesIDDisplay_enabled: 'mesIDDisplayEnabled',
hideChatAvatars_enabled: 'hideChatAvatarsEnabled',
message_token_count_enabled: 'MessageTokenCountEnabled',
expand_message_actions: 'ExpandMessageActions',
enableZenSliders: 'enableZenSliders',
@ -475,6 +477,17 @@ function switchMesIDDisplay() {
$('#mesIDDisplayEnabled').prop('checked', power_user.mesIDDisplay_enabled);
}
function switchHideChatAvatars() {
const value = localStorage.getItem(storage_keys.hideChatAvatars_enabled);
power_user.hideChatAvatars_enabled = value === null ? false : value == 'true';
/*console.log(`
localstorage value:${value},
poweruser after:${power_user.hideChatAvatars_enabled}`)
*/
$('body').toggleClass('hideChatAvatars', power_user.hideChatAvatars_enabled);
$('#hideChatAvatarsEnabled').prop('checked', power_user.hideChatAvatars_enabled);
}
function switchMessageActions() {
const value = localStorage.getItem(storage_keys.expand_message_actions);
power_user.expand_message_actions = value === null ? false : value == 'true';
@ -1280,6 +1293,13 @@ async function applyTheme(name) {
switchMesIDDisplay();
},
},
{
key: 'hideChatAvatars_enabled',
action: async () => {
localStorage.setItem(storage_keys.hideChatAvatars_enabled, Boolean(power_user.hideChatAvatars_enabled));
switchHideChatAvatars();
},
},
{
key: 'expand_message_actions',
action: async () => {
@ -1402,6 +1422,7 @@ switchTimer();
switchTimestamps();
switchIcons();
switchMesIDDisplay();
switchHideChatAvatars();
switchTokenCount();
switchMessageActions();
@ -1453,6 +1474,7 @@ function loadPowerUserSettings(settings, data) {
const timer = localStorage.getItem(storage_keys.timer_enabled);
const timestamps = localStorage.getItem(storage_keys.timestamps_enabled);
const mesIDDisplay = localStorage.getItem(storage_keys.mesIDDisplay_enabled);
const hideChatAvatars = localStorage.getItem(storage_keys.hideChatAvatars_enabled);
const expandMessageActions = localStorage.getItem(storage_keys.expand_message_actions);
const enableZenSliders = localStorage.getItem(storage_keys.enableZenSliders);
const enableLabMode = localStorage.getItem(storage_keys.enableLabMode);
@ -1476,6 +1498,7 @@ function loadPowerUserSettings(settings, data) {
power_user.timer_enabled = timer === null ? true : timer == 'true';
power_user.timestamps_enabled = timestamps === null ? true : timestamps == 'true';
power_user.mesIDDisplay_enabled = mesIDDisplay === null ? true : mesIDDisplay == 'true';
power_user.hideChatAvatars_enabled = hideChatAvatars === null ? true : hideChatAvatars == 'true';
power_user.expand_message_actions = expandMessageActions === null ? true : expandMessageActions == 'true';
power_user.enableZenSliders = enableZenSliders === null ? false : enableZenSliders == 'true';
power_user.enableLabMode = enableLabMode === null ? false : enableLabMode == 'true';
@ -1561,6 +1584,7 @@ function loadPowerUserSettings(settings, data) {
$('#messageTimestampsEnabled').prop('checked', power_user.timestamps_enabled);
$('#messageModelIconEnabled').prop('checked', power_user.timestamp_model_icon);
$('#mesIDDisplayEnabled').prop('checked', power_user.mesIDDisplay_enabled);
$('#hideChatAvatarsEndabled').prop('checked', power_user.hideChatAvatars_enabled);
$('#prefer_character_prompt').prop('checked', power_user.prefer_character_prompt);
$('#prefer_character_jailbreak').prop('checked', power_user.prefer_character_jailbreak);
$('#enableZenSliders').prop('checked', power_user.enableZenSliders).trigger('input');
@ -1570,10 +1594,13 @@ function loadPowerUserSettings(settings, data) {
$('#chat_width_slider').val(power_user.chat_width);
$('#token_padding').val(power_user.token_padding);
$('#aux_field').val(power_user.aux_field);
$('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy');
$('#stscript_autocomplete_style').val(power_user.stscript.autocomplete_style ?? 'theme');
document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete_style);
$('#stscript_parser_flag_strict_escaping').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] ?? false);
$('#stscript_parser_flag_replace_getvar').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] ?? false);
$('#restore_user_input').prop('checked', power_user.restore_user_input);
$('#chat_truncation').val(power_user.chat_truncation);
@ -2174,6 +2201,7 @@ async function saveTheme(name = undefined) {
timestamp_model_icon: power_user.timestamp_model_icon,
mesIDDisplay_enabled: power_user.mesIDDisplay_enabled,
hideChatAvatars_enabled: power_user.hideChatAvatars_enabled,
message_token_count_enabled: power_user.message_token_count_enabled,
expand_message_actions: power_user.expand_message_actions,
enableZenSliders: power_user.enableZenSliders,
@ -2397,7 +2425,7 @@ async function doRandomChat(_, tagName) {
.map(x => x[0]) // Map the character avatar
.filter(x => characters.find(y => y.avatar === x)); // Filter out characters that don't exist
const randomCharacter = taggedCharacters[Math.floor(Math.random() * taggedCharacters.length)];
const randomIndex = characters.findIndex(x => x.avatar === randomCharacter);
const randomIndex = characters.findIndex(x => x.avatar === randomCharacter);
if (randomIndex === -1) {
return;
}
@ -3400,6 +3428,13 @@ $(document).ready(() => {
switchMesIDDisplay();
});
$('#hideChatAvatarsEnabled').on('input', function () {
const value = !!$(this).prop('checked');
power_user.hideChatAvatars_enabled = value;
localStorage.setItem(storage_keys.hideChatAvatars_enabled, Boolean(power_user.hideChatAvatars_enabled));
switchHideChatAvatars();
});
$('#hotswapEnabled').on('input', function () {
const value = !!$(this).prop('checked');
power_user.hotswap_enabled = value;
@ -3531,6 +3566,7 @@ $(document).ready(() => {
$('#stscript_autocomplete_style').on('change', function () {
const value = $(this).find(':selected').val();
power_user.stscript.autocomplete_style = String(value);
document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete_style);
saveSettingsDebounced();
});

View File

@ -39,7 +39,6 @@ import {
} from '../script.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js';
import { SlashCommandExecutor } from './slash-commands/SlashCommandExecutor.js';
import { getMessageTimeStamp } from './RossAscends-mods.js';
import { hideChatMessageRange } from './chats.js';
import { getContext, saveMetadataDebounced } from './extensions.js';
@ -50,14 +49,14 @@ import { autoSelectPersona } from './personas.js';
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';
import { debounce, delay, escapeRegex, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
import { delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
import { registerVariableCommands, resolveVariable } from './variables.js';
import { background_settings } from './backgrounds.js';
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
import { NAME_RESULT_TYPE, SlashCommandParserNameResult } from './slash-commands/SlashCommandParserNameResult.js';
import { OPTION_TYPE, SlashCommandAutoCompleteOption, SlashCommandFuzzyScore } from './slash-commands/SlashCommandAutoCompleteOption.js';
import { SlashCommandAutoCompleteOption } from './slash-commands/SlashCommandAutoCompleteOption.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { SlashCommandAutoComplete } from './slash-commands/SlashCommandAutoComplete.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
@ -2608,7 +2607,176 @@ async function executeSlashCommands(text, handleParserErrors = true, scope = nul
* @param {Boolean} isFloating Whether to show the auto complete as a floating window (e.g., large QR editor)
*/
export async function setSlashCommandAutoComplete(textarea, isFloating = false) {
const ac = new SlashCommandAutoComplete(textarea, isFloating);
const parser = new SlashCommandParser();
const ac = new SlashCommandAutoComplete(
textarea,
() => ac.text[0] == '/',
async(text, index) => await parser.getNameAt(text, index),
isFloating,
);
//TODO remove macro demo
{
const macroList = [
['pipe', 'only for slash command batching. Replaced with the returned result of the previous command.'],
['newline', 'just inserts a newline.'],
['trim', 'trims newlines surrounding this macro.'],
['noop', 'no operation, just an empty string.'],
['original', 'global prompts defined in API settings. Only valid in Advanced Definitions prompt overrides.'],
['input', 'the user input'],
['charPrompt', 'the Character\'s Main Prompt override'],
['charJailbreak', 'the Character\'s Jailbreak Prompt override'],
['description', 'the Character\'s Description'],
['personality', 'the Character\'s Personality'],
['scenario', 'the Character\'s Scenario'],
['persona', 'your current Persona Description'],
['mesExamples', 'the Character\'s Dialogue Examples'],
['mesExamplesRaw', 'unformatted Dialogue Examples (only for Story String)'],
['user', 'your current Persona username'],
['char', 'the Character\'s name'],
['group', 'a comma-separated list of group member names or the character name in solo chats. Alias: {{charIfNotGroup}}'],
['model', 'a text generation model name for the currently selected API. Can be inaccurate!'],
['lastMessage', 'the text of the latest chat message.'],
['lastMessageId', 'index # of the latest chat message. Useful for slash command batching.'],
['firstIncludedMessageId', 'the ID of the first message included in the context. Requires generation to be ran at least once in the current session.'],
['currentSwipeId', 'the 1-based ID of the current swipe in the last chat message. Empty string if the last message is user or prompt-hidden.'],
['lastSwipeId', 'the number of swipes in the last chat message. Empty string if the last message is user or prompt-hidden.'],
['// (note)', 'you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.'],
['time', 'the current time'],
['date', 'the current date'],
['weekday', 'the current weekday'],
['isotime', 'the current ISO time (24-hour clock)'],
['isodate', 'the current ISO date (YYYY-MM-DD)'],
['datetimeformat …', 'the current date/time in the specified format, e. g. for German date/time: {{datetimeformat DD.MM.YYYY HH:mm}}'],
['datetimeformat DD.MM.YYYY HH', 'the current date/time in the specified format, e. g. for German date/time: '],
['time_UTC±#', 'the current time in the specified UTC time zone offset, e.g. UTC-4 or UTC+2'],
['idle_duration', 'the time since the last user message was sent'],
['bias "text here"', 'sets a behavioral bias for the AI until the next user input. Quotes around the text are important.'],
['roll', 'rolls a dice. (ex: >{{roll:1d6}} will roll a 6-sided dice and return a number between 1 and 6)'],
['roll', 'rolls a dice. (ex: will roll a 6-sided dice and return a number between 1 and 6)'],
['random', 'returns a random item from the list. (ex: {{random:1,2,3,4}} will return 1 of the 4 numbers at random. Works with text lists too.'],
['random', 'returns a random item from the list. (ex: will return 1 of the 4 numbers at random. Works with text lists too.'],
['random', 'alternative syntax for random that allows to use commas in the list items.'],
['pick', 'picks a random item from the list. Works the same as {{random}}, with the same possible syntax options, but the pick will stay consistent for this chat once picked and won\'t be re-rolled on consecutive messages and prompt processing.'],
['banned "text here"', 'dynamically add text in the quotes to banned words sequences, if Text Generation WebUI backend used. Do nothing for others backends. Can be used anywhere (Character description, WI, AN, etc.) Quotes around the text are important.'],
['maxPrompt', 'max allowed prompt length in tokens = (context size - response length)'],
['exampleSeparator', 'context template example dialogues separator'],
['chatStart', 'context template chat start line'],
['systemPrompt', 'main system prompt (either character prompt override if chosen, or instructSystemPrompt)'],
['instructSystemPrompt', 'instruct system prompt'],
['instructSystemPromptPrefix', 'instruct system prompt prefix sequence'],
['instructSystemPromptSuffix', 'instruct system prompt suffix sequence'],
['instructUserPrefix', 'instruct user prefix sequence'],
['instructUserSuffix', 'instruct user suffix sequence'],
['instructAssistantPrefix', 'instruct assistant prefix sequence'],
['instructAssistantSuffix', 'instruct assistant suffix sequence'],
['instructFirstAssistantPrefix', 'instruct assistant first output sequence'],
['instructLastAssistantPrefix', 'instruct assistant last output sequence'],
['instructSystemPrefix', 'instruct system message prefix sequence'],
['instructSystemSuffix', 'instruct system message suffix sequence'],
['instructSystemInstructionPrefix', 'instruct system instruction prefix'],
['instructUserFiller', 'instruct first user message filler'],
['instructStop', 'instruct stop sequence'],
['getvar', 'replaced with the value of the local variable "name"'],
['setvar', 'replaced with empty string, sets the local variable "name" to "value"'],
['addvar', 'replaced with empty strings, adds a numeric value of "increment" to the local variable "name"'],
['incvar', 'replaced with the result of the increment of value of the variable "name" by 1'],
['decvar', 'replaced with the result of the decrement of value of the variable "name" by 1'],
['getglobalvar', 'replaced with the value of the global variable "name"'],
['setglobalvar', 'replaced with empty string, sets the global variable "name" to "value"'],
['addglobalvar', 'replaced with empty string, adds a numeric value of "increment" to the global variable "name"'],
['incglobalvar', 'replaced with the result of the increment of value of the global variable "name" by 1'],
['decglobalvar', 'replaced with the result of the decrement of value of the global variable "name" by 1'],
['var', 'replaced with the value of the scoped variable "name"'],
['var', 'replaced with the value of item at index (for arrays / lists or objects / dictionaries) of the scoped variable "name"'],
];
class MacroOption extends SlashCommandAutoCompleteOption {
item;
constructor(item) {
super(item[0], item[0]);
this.item = item;
}
renderItem() {
let li;
li = this.makeItem(this.name, '{}', true, [], [], null, this.item[1]);
li.setAttribute('data-name', this.name);
return li;
}
renderDetails() {
const frag = document.createDocumentFragment();
const specs = document.createElement('div'); {
specs.classList.add('specs');
const name = document.createElement('div'); {
name.classList.add('name');
name.classList.add('monospace');
name.textContent = this.value.toString();
specs.append(name);
}
frag.append(specs);
}
const help = document.createElement('span'); {
help.classList.add('help');
help.textContent = this.item[1];
frag.append(help);
}
return frag;
}
}
const options = macroList.map(m=>new MacroOption(m));
let text;
const stack = [];
let macroIndex = [];
const mac = new SlashCommandAutoComplete(
textarea,
()=> mac.text[0] != '/',
async(newText, index) => {
if (text != newText) {
while (stack.pop());
while (macroIndex.pop());
text = newText;
let remaining = text;
let idx = remaining.search(/{{|}}/);
while (idx > -1) {
const symbol = remaining.slice(idx, idx + 2);
remaining = remaining.slice(idx + 2);
if (symbol == '{{') {
const item = {
name: /\w+/.exec(remaining)?.[0] ?? '',
start: idx + 2,
end: null,
};
macroIndex.push(item);
stack.push(item);
} else {
const item = stack.pop();
if (item) {
item.end = idx;
}
}
idx = remaining.search(/{{|}}/);
}
}
const executor = macroIndex.filter(it=>it.start <= index && (it.end >= index || it.end == null))
.slice(-1)[0]
?? null
;
if (executor) {
const result = new SlashCommandParserNameResult(
NAME_RESULT_TYPE.CLOSURE,
executor.name,
executor.start,
options,
false,
()=>`No matching macros for "{{${result.name}}}"`,
()=>'No macros found!',
);
return result;
}
return null;
},
isFloating,
);
}
}
/**@type {HTMLTextAreaElement} */
const sendTextarea = document.querySelector('#send_textarea');

View File

@ -1,4 +1,5 @@
import { SlashCommandArgument, SlashCommandNamedArgument } from './SlashCommandArgument.js';
import { SlashCommandClosure } from './SlashCommandClosure.js';
@ -7,7 +8,7 @@ export class SlashCommand {
* Creates a SlashCommand from a properties object.
* @param {Object} props
* @param {string} [props.name]
* @param {Function} [props.callback]
* @param {(namedArguments:Object<string, string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>} [props.callback]
* @param {string} [props.helpString]
* @param {boolean} [props.interruptsGeneration]
* @param {boolean} [props.purgeFromMessage]
@ -25,7 +26,7 @@ export class SlashCommand {
/**@type {string}*/ name;
/**@type {Function}*/ callback;
/**@type {(namedArguments:Object<string, string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>}*/ callback;
/**@type {string}*/ helpString;
/**@type {boolean}*/ interruptsGeneration = true;
/**@type {boolean}*/ purgeFromMessage = true;
@ -34,202 +35,252 @@ export class SlashCommand {
/**@type {SlashCommandNamedArgument[]}*/ namedArgumentList = [];
/**@type {SlashCommandArgument[]}*/ unnamedArgumentList = [];
get helpStringFormatted() {
let aliases = '';
if (this.aliases?.length > 0) {
aliases = ' (alias: ';
aliases += this.aliases
.map(it=>`<span class="monospace">/${it}</span>`)
.join(', ')
;
aliases += ')';
}
return `<span class="monospace">/${this.name}</span> ${this.helpString}${aliases}`;
}
/**@type {Object.<string, HTMLElement>}*/ helpCache = {};
/**@type {Object.<string, DocumentFragment>}*/ helpDetailsCache = {};
renderHelpItem(key = null) {
key = key ?? this.name;
const typeIcon = '/';
const li = document.createElement('li'); {
li.classList.add('item');
const type = document.createElement('span'); {
type.classList.add('type');
type.classList.add('monospace');
type.textContent = typeIcon;
li.append(type);
}
const specs = document.createElement('span'); {
specs.classList.add('specs');
const name = document.createElement('span'); {
name.classList.add('name');
name.classList.add('monospace');
name.textContent = '/';
key.split('').forEach(char=>{
const span = document.createElement('span'); {
span.textContent = char;
name.append(span);
}
});
specs.append(name);
if (!this.helpCache[key]) {
const typeIcon = '/';
const li = document.createElement('li'); {
li.classList.add('item');
const type = document.createElement('span'); {
type.classList.add('type');
type.classList.add('monospace');
type.textContent = typeIcon;
li.append(type);
}
const body = document.createElement('span'); {
body.classList.add('body');
const args = document.createElement('span'); {
args.classList.add('arguments');
for (const arg of this.namedArgumentList) {
const argItem = document.createElement('span'); {
argItem.classList.add('argument');
argItem.classList.add('namedArgument');
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
if (arg.acceptsMultiple) argItem.classList.add('multiple');
const name = document.createElement('span'); {
name.classList.add('argument-name');
name.textContent = arg.name;
argItem.append(name);
}
if (arg.enumList.length > 0) {
const enums = document.createElement('span'); {
enums.classList.add('argument-enums');
for (const e of arg.enumList) {
const enumItem = document.createElement('span'); {
enumItem.classList.add('argument-enum');
enumItem.textContent = e;
enums.append(enumItem);
}
}
argItem.append(enums);
}
} else {
const types = document.createElement('span'); {
types.classList.add('argument-types');
for (const t of arg.typeList) {
const type = document.createElement('span'); {
type.classList.add('argument-type');
type.textContent = t;
types.append(type);
}
}
argItem.append(types);
}
}
args.append(argItem);
const specs = document.createElement('span'); {
specs.classList.add('specs');
const name = document.createElement('span'); {
name.classList.add('name');
name.classList.add('monospace');
name.textContent = '/';
key.split('').forEach(char=>{
const span = document.createElement('span'); {
span.textContent = char;
name.append(span);
}
}
for (const arg of this.unnamedArgumentList) {
const argItem = document.createElement('span'); {
argItem.classList.add('argument');
argItem.classList.add('unnamedArgument');
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
if (arg.acceptsMultiple) argItem.classList.add('multiple');
if (arg.enumList.length > 0) {
const enums = document.createElement('span'); {
enums.classList.add('argument-enums');
for (const e of arg.enumList) {
const enumItem = document.createElement('span'); {
enumItem.classList.add('argument-enum');
enumItem.textContent = e;
enums.append(enumItem);
}
}
argItem.append(enums);
}
} else {
const types = document.createElement('span'); {
types.classList.add('argument-types');
for (const t of arg.typeList) {
const type = document.createElement('span'); {
type.classList.add('argument-type');
type.textContent = t;
types.append(type);
}
}
argItem.append(types);
}
}
args.append(argItem);
}
}
body.append(args);
});
specs.append(name);
}
const returns = document.createElement('span'); {
returns.classList.add('returns');
returns.textContent = this.returns ?? 'void';
body.append(returns);
}
specs.append(body);
}
li.append(specs);
}
const help = document.createElement('span'); {
help.classList.add('help');
const content = document.createElement('span'); {
content.classList.add('helpContent');
content.innerHTML = this.helpString;
const text = content.textContent;
content.innerHTML = '';
content.textContent = text;
help.append(content);
}
li.append(help);
}
if (this.aliases.length > 0) {
const aliases = document.createElement('span'); {
aliases.classList.add('aliases');
aliases.append(' (alias: ');
for (const aliasName of this.aliases) {
const alias = document.createElement('span'); {
alias.classList.add('monospace');
alias.textContent = `/${aliasName}`;
aliases.append(alias);
}
}
aliases.append(')');
// li.append(aliases);
}
}
}
return li;
}
renderHelpDetails(key = null) {
const frag = document.createDocumentFragment();
key = key ?? this.name;
const cmd = this;
const namedArguments = cmd.namedArgumentList ?? [];
const unnamedArguments = cmd.unnamedArgumentList ?? [];
const returnType = cmd.returns ?? 'void';
const helpString = cmd.helpString ?? 'NO DETAILS';
const aliasList = [cmd.name, ...(cmd.aliases ?? [])].filter(it=>it != key);
const specs = document.createElement('div'); {
specs.classList.add('specs');
const name = document.createElement('div'); {
name.classList.add('name');
name.classList.add('monospace');
name.title = 'command name';
name.textContent = `/${key}`;
specs.append(name);
}
const body = document.createElement('div'); {
body.classList.add('body');
const args = document.createElement('ul'); {
args.classList.add('arguments');
for (const arg of namedArguments) {
const listItem = document.createElement('li'); {
listItem.classList.add('argumentItem');
const argSpec = document.createElement('div'); {
argSpec.classList.add('argumentSpec');
const argItem = document.createElement('div'); {
const body = document.createElement('span'); {
body.classList.add('body');
const args = document.createElement('span'); {
args.classList.add('arguments');
for (const arg of this.namedArgumentList) {
const argItem = document.createElement('span'); {
argItem.classList.add('argument');
argItem.classList.add('namedArgument');
argItem.title = `${arg.isRequired ? '' : 'optional '}named argument`;
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
if (arg.acceptsMultiple) argItem.classList.add('multiple');
const name = document.createElement('span'); {
name.classList.add('argument-name');
name.title = `${argItem.title} - name`;
name.textContent = arg.name;
argItem.append(name);
}
if (arg.enumList.length > 0) {
const enums = document.createElement('span'); {
enums.classList.add('argument-enums');
for (const e of arg.enumList) {
const enumItem = document.createElement('span'); {
enumItem.classList.add('argument-enum');
enumItem.textContent = e;
enums.append(enumItem);
}
}
argItem.append(enums);
}
} else {
const types = document.createElement('span'); {
types.classList.add('argument-types');
for (const t of arg.typeList) {
const type = document.createElement('span'); {
type.classList.add('argument-type');
type.textContent = t;
types.append(type);
}
}
argItem.append(types);
}
}
args.append(argItem);
}
}
for (const arg of this.unnamedArgumentList) {
const argItem = document.createElement('span'); {
argItem.classList.add('argument');
argItem.classList.add('unnamedArgument');
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
if (arg.acceptsMultiple) argItem.classList.add('multiple');
if (arg.enumList.length > 0) {
const enums = document.createElement('span'); {
enums.classList.add('argument-enums');
for (const e of arg.enumList) {
const enumItem = document.createElement('span'); {
enumItem.classList.add('argument-enum');
enumItem.textContent = e;
enums.append(enumItem);
}
}
argItem.append(enums);
}
} else {
const types = document.createElement('span'); {
types.classList.add('argument-types');
for (const t of arg.typeList) {
const type = document.createElement('span'); {
type.classList.add('argument-type');
type.textContent = t;
types.append(type);
}
}
argItem.append(types);
}
}
args.append(argItem);
}
}
body.append(args);
}
const returns = document.createElement('span'); {
returns.classList.add('returns');
returns.textContent = this.returns ?? 'void';
body.append(returns);
}
specs.append(body);
}
li.append(specs);
}
const help = document.createElement('span'); {
help.classList.add('help');
const content = document.createElement('span'); {
content.classList.add('helpContent');
content.innerHTML = this.helpString;
const text = content.textContent;
content.innerHTML = '';
content.textContent = text;
help.append(content);
}
li.append(help);
}
if (this.aliases.length > 0) {
const aliases = document.createElement('span'); {
aliases.classList.add('aliases');
aliases.append(' (alias: ');
for (const aliasName of this.aliases) {
const alias = document.createElement('span'); {
alias.classList.add('monospace');
alias.textContent = `/${aliasName}`;
aliases.append(alias);
}
}
aliases.append(')');
// li.append(aliases);
}
}
}
this.helpCache[key] = li;
}
return this.helpCache[key];
}
renderHelpDetails(key = null) {
key = key ?? this.name;
if (!this.helpDetailsCache[key]) {
const frag = document.createDocumentFragment();
const cmd = this;
const namedArguments = cmd.namedArgumentList ?? [];
const unnamedArguments = cmd.unnamedArgumentList ?? [];
const returnType = cmd.returns ?? 'void';
const helpString = cmd.helpString ?? 'NO DETAILS';
const aliasList = [cmd.name, ...(cmd.aliases ?? [])].filter(it=>it != key);
const specs = document.createElement('div'); {
specs.classList.add('specs');
const name = document.createElement('div'); {
name.classList.add('name');
name.classList.add('monospace');
name.title = 'command name';
name.textContent = `/${key}`;
specs.append(name);
}
const body = document.createElement('div'); {
body.classList.add('body');
const args = document.createElement('ul'); {
args.classList.add('arguments');
for (const arg of namedArguments) {
const listItem = document.createElement('li'); {
listItem.classList.add('argumentItem');
const argSpec = document.createElement('div'); {
argSpec.classList.add('argumentSpec');
const argItem = document.createElement('div'); {
argItem.classList.add('argument');
argItem.classList.add('namedArgument');
argItem.title = `${arg.isRequired ? '' : 'optional '}named argument`;
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
if (arg.acceptsMultiple) argItem.classList.add('multiple');
const name = document.createElement('span'); {
name.classList.add('argument-name');
name.title = `${argItem.title} - name`;
name.textContent = arg.name;
argItem.append(name);
}
if (arg.enumList.length > 0) {
const enums = document.createElement('span'); {
enums.classList.add('argument-enums');
enums.title = `${argItem.title} - accepted values`;
for (const e of arg.enumList) {
const enumItem = document.createElement('span'); {
enumItem.classList.add('argument-enum');
enumItem.textContent = e;
enums.append(enumItem);
}
}
argItem.append(enums);
}
} else {
const types = document.createElement('span'); {
types.classList.add('argument-types');
types.title = `${argItem.title} - accepted types`;
for (const t of arg.typeList) {
const type = document.createElement('span'); {
type.classList.add('argument-type');
type.textContent = t;
types.append(type);
}
}
argItem.append(types);
}
}
argSpec.append(argItem);
}
if (arg.defaultValue !== null) {
const argDefault = document.createElement('div'); {
argDefault.classList.add('argument-default');
argDefault.title = 'default value';
argDefault.textContent = arg.defaultValue.toString();
argSpec.append(argDefault);
}
}
listItem.append(argSpec);
}
const desc = document.createElement('div'); {
desc.classList.add('argument-description');
desc.innerHTML = arg.description;
listItem.append(desc);
}
args.append(listItem);
}
}
for (const arg of unnamedArguments) {
const listItem = document.createElement('li'); {
listItem.classList.add('argumentItem');
const argItem = document.createElement('div'); {
argItem.classList.add('argument');
argItem.classList.add('unnamedArgument');
argItem.title = `${arg.isRequired ? '' : 'optional '}unnamed argument`;
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
if (arg.acceptsMultiple) argItem.classList.add('multiple');
if (arg.enumList.length > 0) {
const enums = document.createElement('span'); {
enums.classList.add('argument-enums');
@ -257,102 +308,52 @@ export class SlashCommand {
argItem.append(types);
}
}
argSpec.append(argItem);
listItem.append(argItem);
}
if (arg.defaultValue !== null) {
const argDefault = document.createElement('div'); {
argDefault.classList.add('argument-default');
argDefault.title = 'default value';
argDefault.textContent = arg.defaultValue.toString();
argSpec.append(argDefault);
}
const desc = document.createElement('div'); {
desc.classList.add('argument-description');
desc.innerHTML = arg.description;
listItem.append(desc);
}
listItem.append(argSpec);
args.append(listItem);
}
const desc = document.createElement('div'); {
desc.classList.add('argument-description');
desc.innerHTML = arg.description;
listItem.append(desc);
}
args.append(listItem);
}
body.append(args);
}
const returns = document.createElement('span'); {
returns.classList.add('returns');
returns.title = [null, undefined, 'void'].includes(returnType) ? 'command does not return anything' : 'return value';
returns.textContent = returnType ?? 'void';
body.append(returns);
}
specs.append(body);
}
frag.append(specs);
}
const help = document.createElement('span'); {
help.classList.add('help');
help.innerHTML = helpString;
for (const code of help.querySelectorAll('pre > code')) {
code.classList.add('language-stscript');
hljs.highlightElement(code);
}
frag.append(help);
}
if (aliasList.length > 0) {
const aliases = document.createElement('span'); {
aliases.classList.add('aliases');
for (const aliasName of aliasList) {
const alias = document.createElement('span'); {
alias.classList.add('alias');
alias.textContent = `/${aliasName}`;
aliases.append(alias);
}
}
for (const arg of unnamedArguments) {
const listItem = document.createElement('li'); {
listItem.classList.add('argumentItem');
const argItem = document.createElement('div'); {
argItem.classList.add('argument');
argItem.classList.add('unnamedArgument');
argItem.title = `${arg.isRequired ? '' : 'optional '}unnamed argument`;
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
if (arg.acceptsMultiple) argItem.classList.add('multiple');
if (arg.enumList.length > 0) {
const enums = document.createElement('span'); {
enums.classList.add('argument-enums');
enums.title = `${argItem.title} - accepted values`;
for (const e of arg.enumList) {
const enumItem = document.createElement('span'); {
enumItem.classList.add('argument-enum');
enumItem.textContent = e;
enums.append(enumItem);
}
}
argItem.append(enums);
}
} else {
const types = document.createElement('span'); {
types.classList.add('argument-types');
types.title = `${argItem.title} - accepted types`;
for (const t of arg.typeList) {
const type = document.createElement('span'); {
type.classList.add('argument-type');
type.textContent = t;
types.append(type);
}
}
argItem.append(types);
}
}
listItem.append(argItem);
}
const desc = document.createElement('div'); {
desc.classList.add('argument-description');
desc.innerHTML = arg.description;
listItem.append(desc);
}
args.append(listItem);
}
}
body.append(args);
frag.append(aliases);
}
const returns = document.createElement('span'); {
returns.classList.add('returns');
returns.title = [null, undefined, 'void'].includes(returnType) ? 'command does not return anything' : 'return value';
returns.textContent = returnType ?? 'void';
body.append(returns);
}
specs.append(body);
}
frag.append(specs);
this.helpDetailsCache[key] = frag;
}
const help = document.createElement('span'); {
help.classList.add('help');
help.innerHTML = helpString;
frag.append(help);
}
if (aliasList.length > 0) {
const aliases = document.createElement('span'); {
aliases.classList.add('aliases');
for (const aliasName of aliasList) {
const alias = document.createElement('span'); {
alias.classList.add('alias');
alias.textContent = `/${aliasName}`;
aliases.append(alias);
}
}
frag.append(aliases);
}
}
return frag;
return this.helpDetailsCache[key].cloneNode(true);
}
}

View File

@ -19,6 +19,30 @@ export const ARGUMENT_TYPE = {
export class SlashCommandArgument {
/**
* Creates an unnamed argument from a poperties object.
* @param {Object} props
* @param {string} props.description description of the argument
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} props.typeList default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE)
* @param {boolean} [props.isRequired] default: false - whether the argument is required (false = optional argument)
* @param {boolean} [props.acceptsMultiple] default: false - whether argument accepts multiple values
* @param {string|SlashCommandClosure} [props.defaultValue] default value if no value is provided
* @param {string|string[]} [props.enumList] list of accepted values
*/
static fromProps(props) {
return new SlashCommandArgument(
props.description,
props.typeList ?? [ARGUMENT_TYPE.STRING],
props.isRequired ?? false,
props.acceptsMultiple ?? false,
props.defaultValue ?? null,
props.enumList ?? [],
);
}
/**@type {string}*/ description;
/**@type {ARGUMENT_TYPE[]}*/ typeList = [];
/**@type {boolean}*/ isRequired = false;
@ -26,6 +50,7 @@ export class SlashCommandArgument {
/**@type {string|SlashCommandClosure}*/ defaultValue;
/**@type {string[]}*/ enumList = [];
/**
* @param {string} description
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types
@ -45,9 +70,38 @@ export class SlashCommandArgument {
export class SlashCommandNamedArgument extends SlashCommandArgument {
/**
* Creates an unnamed argument from a poperties object.
* @param {Object} props
* @param {string} props.name the argument's name
* @param {string[]} [props.aliasList] list of aliases
* @param {string} props.description description of the argument
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} props.typeList default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE)
* @param {boolean} [props.isRequired] default: false - whether the argument is required (false = optional argument)
* @param {boolean} [props.acceptsMultiple] default: false - whether argument accepts multiple values
* @param {string|SlashCommandClosure} [props.defaultValue] default value if no value is provided
* @param {string|string[]} [props.enumList] list of accepted values
*/
static fromProps(props) {
return new SlashCommandNamedArgument(
props.name,
props.description,
props.typeList ?? [ARGUMENT_TYPE.STRING],
props.isRequired ?? false,
props.acceptsMultiple ?? false,
props.defaultValue ?? null,
props.enumList ?? [],
props.aliasList ?? [],
);
}
/**@type {string}*/ name;
/**@type {string[]}*/ aliasList = [];
/**
* @param {string} name
* @param {string} description

View File

@ -1,15 +1,15 @@
import { power_user } from '../power-user.js';
import { debounce, escapeRegex } from '../utils.js';
import { OPTION_TYPE, SlashCommandAutoCompleteOption, SlashCommandFuzzyScore } from './SlashCommandAutoCompleteOption.js';
import { SlashCommandParser } from './SlashCommandParser.js';
import { SlashCommandAutoCompleteOption, SlashCommandFuzzyScore } from './SlashCommandAutoCompleteOption.js';
import { SlashCommandBlankAutoCompleteOption } from './SlashCommandBlankAutoCompleteOption.js';
// eslint-disable-next-line no-unused-vars
import { NAME_RESULT_TYPE, SlashCommandParserNameResult } from './SlashCommandParserNameResult.js';
import { SlashCommandParserNameResult } from './SlashCommandParserNameResult.js';
export class SlashCommandAutoComplete {
/**@type {HTMLTextAreaElement}*/ textarea;
/**@type {boolean}*/ isFloating = false;
/**@type {SlashCommandParser}*/ parser;
/**@type {()=>boolean}*/ checkIfActivate;
/**@type {(text:string, index:number) => Promise<SlashCommandParserNameResult>}*/ getNameAt;
/**@type {boolean}*/ isActive = false;
/**@type {boolean}*/ isReplaceable = false;
@ -17,7 +17,7 @@ export class SlashCommandAutoComplete {
/**@type {string}*/ text;
/**@type {SlashCommandParserNameResult}*/ parserResult;
/**@type {string}*/ slashCommand;
/**@type {string}*/ name;
/**@type {boolean}*/ startQuote;
/**@type {boolean}*/ endQuote;
@ -55,14 +55,16 @@ export class SlashCommandAutoComplete {
/**
* @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete.
* @param {() => boolean} checkIfActivate
* @param {(text: string, index: number) => Promise<SlashCommandParserNameResult>} getNameAt
* @param {boolean} isFloating Whether autocomplete should float at the keyboard cursor.
*/
constructor(textarea, isFloating = false) {
constructor(textarea, checkIfActivate, getNameAt, isFloating = false) {
this.textarea = textarea;
this.checkIfActivate = checkIfActivate;
this.getNameAt = getNameAt;
this.isFloating = isFloating;
this.parser = new SlashCommandParser();
this.domWrap = document.createElement('div'); {
this.domWrap.classList.add('slashCommandAutoComplete-wrap');
if (isFloating) this.domWrap.classList.add('isFloating');
@ -90,32 +92,16 @@ export class SlashCommandAutoComplete {
textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt));
textarea.addEventListener('click', ()=>this.isActive ? this.show() : null);
textarea.addEventListener('selectionchange', ()=>this.show());
// textarea.addEventListener('blur', ()=>this.hide());
textarea.addEventListener('blur', ()=>this.hide());
if (isFloating) {
textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());
}
window.addEventListener('resize', ()=>this.updatePositionDebounced());
}
/**
* Build a cache of DOM list items for autocomplete of slash commands.
*/
buildCache() {
if (!this.hasCache) {
this.hasCache = true;
// init by appending all command options
Object.keys(this.parser.commands).forEach(key=>{
const cmd = this.parser.commands[key];
this.items[key] = this.makeItem(new SlashCommandAutoCompleteOption(OPTION_TYPE.COMMAND, cmd, key));
});
}
}
/**
*
* @param {SlashCommandAutoCompleteOption} option
* @returns
*/
makeItem(option) {
const li = option.renderItem();
@ -154,7 +140,7 @@ export class SlashCommandAutoComplete {
break;
}
case 'includes': {
const start = item.name.toLowerCase().search(this.slashCommand);
const start = item.name.toLowerCase().search(this.name);
chars.forEach((it, idx)=>{
if (idx < start) {
it.classList.remove('matched');
@ -248,161 +234,92 @@ export class SlashCommandAutoComplete {
this.text = this.textarea.value;
// only show with textarea in focus
if (document.activeElement != this.textarea) return this.hide();
// only show for slash commands
//TODO activation-requirements could be provided as a function
if (this.text[0] != '/') return this.hide();
// only show if provider wants to
if (!this.checkIfActivate()) return this.hide();
this.buildCache();
// request parser to get command executor (potentially "incomplete", i.e. not an actual existing command) for
// request provider to get name result (potentially "incomplete", i.e. not an actual existing name) for
// cursor position
//TODO nameProvider function provided when instantiating?
this.parserResult = this.parser.getNameAt(this.text, this.textarea.selectionStart);
//TODO options could be fully provided by the name source
switch (this.parserResult?.type) {
case NAME_RESULT_TYPE.CLOSURE: {
this.startQuote = this.text[this.parserResult.start - 2] == '"';
this.endQuote = this.startQuote && this.text[this.parserResult.start - 2 + this.parserResult.name.length + 1] == '"';
try {
const qrApi = (await import('../extensions/quick-reply/index.js')).quickReplyApi;
this.parserResult.optionList.push(...qrApi.listSets()
.map(set=>qrApi.listQuickReplies(set).map(qr=>`${set}.${qr}`))
.flat()
.map(qr=>new SlashCommandAutoCompleteOption(OPTION_TYPE.QUICK_REPLY, qr, qr)),
);
} catch { /* empty */ }
break;
}
case NAME_RESULT_TYPE.COMMAND: {
this.parserResult.optionList.push(...Object.keys(this.parser.commands)
.map(key=>new SlashCommandAutoCompleteOption(OPTION_TYPE.COMMAND, this.parser.commands[key], key)),
);
break;
}
default: {
// no result
break;
}
}
this.slashCommand = this.parserResult?.name?.toLowerCase() ?? '';
// do autocomplete if triggered by a user input and we either don't have an executor or the cursor is at the end
// of the name part of the command
//TODO whether the input is quotable could be an option (given by the parserResult?)
switch (this.parserResult?.type) {
case NAME_RESULT_TYPE.CLOSURE: {
this.isReplaceable = isInput && (!this.parserResult ? true : this.textarea.selectionStart == this.parserResult.start - 2 + this.parserResult.name.length + (this.startQuote ? 1 : 0));
break;
}
default: // no result
case NAME_RESULT_TYPE.COMMAND: {
this.isReplaceable = isInput && (!this.parserResult ? true : this.textarea.selectionStart == this.parserResult.start - 2 + this.parserResult.name.length);
break;
}
}
// if [forced (ctrl+space) or user input] and cursor is in the middle of the name part (not at the end)
if (isForced || isInput) {
//TODO input quotable (see above)
switch (this.parserResult?.type) {
case NAME_RESULT_TYPE.CLOSURE: {
if (this.textarea.selectionStart >= this.parserResult.start - 2 && this.textarea.selectionStart <= this.parserResult.start - 2 + this.parserResult.name.length + (this.startQuote ? 1 : 0)) {
this.slashCommand = this.slashCommand.slice(0, this.textarea.selectionStart - (this.parserResult.start - 2) - (this.startQuote ? 1 : 0));
this.parserResult.name = this.slashCommand;
this.isReplaceable = true;
}
break;
}
case NAME_RESULT_TYPE.COMMAND: {
if (this.textarea.selectionStart >= this.parserResult.start - 2 && this.textarea.selectionStart <= this.parserResult.start - 2 + this.parserResult.name.length) {
this.slashCommand = this.slashCommand.slice(0, this.textarea.selectionStart - (this.parserResult.start - 2));
this.parserResult.name = this.slashCommand;
this.isReplaceable = true;
}
break;
}
default: {
// no result
break;
}
}
}
this.fuzzyRegex = new RegExp(`^(.*?)${this.slashCommand.split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i');
const matchers = {
'strict': (name) => name.toLowerCase().startsWith(this.slashCommand),
'includes': (name) => name.toLowerCase().includes(this.slashCommand),
'fuzzy': (name) => this.fuzzyRegex.test(name),
};
this.parserResult = await this.getNameAt(this.text, this.textarea.selectionStart);
// don't show if no executor found, i.e. cursor's area is not a command
if (!this.parserResult) return this.hide();
else {
let matchingOptions = this.parserResult.optionList
.filter(it => this.isReplaceable || it.name == '' ? matchers[this.matchType](it.name) : it.name.toLowerCase() == this.slashCommand) // Filter by the input
.filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx)
;
this.result = matchingOptions
.filter((it,idx) => matchingOptions.indexOf(it) == idx)
.map(option => {
let li;
//TODO makeItem should be handled in the option class
switch (option.type) {
case OPTION_TYPE.QUICK_REPLY: {
li = this.makeItem(option);
break;
}
case OPTION_TYPE.VARIABLE_NAME: {
li = this.makeItem(option);
break;
}
case OPTION_TYPE.COMMAND: {
li = this.items[option.name];
break;
}
}
option.replacer = option.name.includes(' ') || this.startQuote || this.endQuote ? `"${option.name}"` : `${option.name}`;
option.dom = li;
if (this.matchType == 'fuzzy') this.fuzzyScore(option);
this.updateName(option);
return option;
}) // Map to the help string and score
.toSorted(this.matchType == 'fuzzy' ? this.fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name)) // sort by score (if fuzzy) or name
;
// need to know if name *can* be inside quotes, and then check if quotes are already there
if (this.parserResult.canBeQuoted) {
this.startQuote = this.text[this.parserResult.start] == '"';
this.endQuote = this.startQuote && this.text[this.parserResult.start + this.parserResult.name.length + 1] == '"';
} else {
this.startQuote = false;
this.endQuote = false;
}
// use lowercase name for matching
this.name = this.parserResult?.name?.toLowerCase() ?? '';
// do autocomplete if triggered by a user input and we either don't have an executor or the cursor is at the end
// of the name part of the command
this.isReplaceable = isInput && (!this.parserResult ? true : this.textarea.selectionStart == this.parserResult.start + this.parserResult.name.length + (this.startQuote ? 1 : 0));
// if [forced (ctrl+space) or user input] and cursor is in the middle of the name part (not at the end)
if (isForced || isInput) {
if (this.textarea.selectionStart >= this.parserResult.start && this.textarea.selectionStart <= this.parserResult.start + this.parserResult.name.length + (this.startQuote ? 1 : 0)) {
this.name = this.name.slice(0, this.textarea.selectionStart - (this.parserResult.start) - (this.startQuote ? 1 : 0));
this.parserResult.name = this.name;
this.isReplaceable = true;
}
}
// only build the fuzzy regex if match type is set to fuzzy
if (this.matchType == 'fuzzy') {
this.fuzzyRegex = new RegExp(`^(.*?)${this.name.split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i');
}
//TODO maybe move the matchers somewhere else; a single match function? matchType is available as property
const matchers = {
'strict': (name) => name.toLowerCase().startsWith(this.name),
'includes': (name) => name.toLowerCase().includes(this.name),
'fuzzy': (name) => this.fuzzyRegex.test(name),
};
this.result = this.parserResult.optionList
// filter the list of options by the partial name according to the matching type
.filter(it => this.isReplaceable || it.name == '' ? matchers[this.matchType](it.name) : it.name.toLowerCase() == this.name)
// remove aliases
.filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx)
// update remaining options
.map(option => {
// build element
option.dom = this.makeItem(option);
// update replacer and add quotes if necessary
if (this.parserResult.canBeQuoted) {
option.replacer = option.name.includes(' ') || this.startQuote || this.endQuote ? `"${option.name}"` : `${option.name}`;
} else {
option.replacer = option.name;
}
// calculate fuzzy score if matching is fuzzy
if (this.matchType == 'fuzzy') this.fuzzyScore(option);
// update the name to highlight the matched chars
this.updateName(option);
return option;
})
// sort by fuzzy score or alphabetical
.toSorted(this.matchType == 'fuzzy' ? this.fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name))
;
if (this.result.length == 0) {
// no result and no input? hide autocomplete
if (!isInput) {
return this.hide();
}
// otherwise add "no match" notice
const option = new SlashCommandAutoCompleteOption(
OPTION_TYPE.BLANK,
null,
'',
const option = new SlashCommandBlankAutoCompleteOption(
this.name.length ?
this.parserResult.makeNoMatchText()
: this.parserResult.makeNoOptionstext()
,
);
switch (this.parserResult?.type) {
//TODO "no-match" text should be an option (in parserResult?)
case NAME_RESULT_TYPE.CLOSURE: {
const li = document.createElement('li'); {
li.textContent = this.slashCommand.length ?
`No matching variables in scope and no matching Quick Replies for "${this.slashCommand}"`
: 'No variables in scope and no Quick Replies found.';
}
option.dom = li;
this.result.push(option);
break;
}
case NAME_RESULT_TYPE.COMMAND: {
const li = document.createElement('li'); {
li.textContent = `No matching commands for "/${this.slashCommand}"`;
}
option.dom = li;
this.result.push(option);
break;
}
}
this.result.push(option);
} else if (this.result.length == 1 && this.parserResult && this.result[0].name == this.parserResult.name) {
// only one result that is exactly the current value? just show hint, no autocomplete
this.isReplaceable = false;
@ -434,30 +351,6 @@ export class SlashCommandAutoComplete {
// render autocomplete list
if (this.isReplaceable) {
this.dom.innerHTML = '';
this.dom.classList.remove('defaultDark');
this.dom.classList.remove('defaultLight');
this.dom.classList.remove('defaultThemed');
this.detailsDom.classList.remove('defaultDark');
this.detailsDom.classList.remove('defaultLight');
this.detailsDom.classList.remove('defaultThemed');
switch (power_user.stscript.autocomplete_style ?? 'theme') {
case 'dark': {
this.dom.classList.add('defaultDark');
this.detailsDom.classList.add('defaultDark');
break;
}
case 'light': {
this.dom.classList.add('defaultLight');
this.detailsDom.classList.add('defaultLight');
break;
}
case 'theme':
default: {
this.dom.classList.add('defaultThemed');
this.detailsDom.classList.add('defaultThemed');
break;
}
}
const frag = document.createDocumentFragment();
for (const item of this.result) {
if (item == this.selectedItem) {
@ -661,10 +554,10 @@ export class SlashCommandAutoComplete {
*/
async select() {
if (this.isReplaceable && this.selectedItem.value !== null) {
this.textarea.value = `${this.text.slice(0, this.parserResult.start - 2)}${this.selectedItem.replacer}${this.text.slice(this.parserResult.start - 2 + this.parserResult.name.length + (this.startQuote ? 1 : 0) + (this.endQuote ? 1 : 0))}`;
this.textarea.value = `${this.text.slice(0, this.parserResult.start)}${this.selectedItem.replacer}${this.text.slice(this.parserResult.start + this.parserResult.name.length + (this.startQuote ? 1 : 0) + (this.endQuote ? 1 : 0))}`;
await this.pointerup;
this.textarea.focus();
this.textarea.selectionStart = this.parserResult.start - 2 + this.selectedItem.replacer.length;
this.textarea.selectionStart = this.parserResult.start + this.selectedItem.replacer.length;
this.textarea.selectionEnd = this.textarea.selectionStart;
this.show();
} else {
@ -727,8 +620,8 @@ export class SlashCommandAutoComplete {
}
case 'Enter': {
// pick the selected item to autocomplete
if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.type == OPTION_TYPE.BLANK) break;
if (this.selectedItem.name == this.slashCommand) break;
if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
if (this.selectedItem.name == this.name) break;
evt.preventDefault();
evt.stopImmediatePropagation();
this.select();
@ -736,7 +629,7 @@ export class SlashCommandAutoComplete {
}
case 'Tab': {
// pick the selected item to autocomplete
if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.type == OPTION_TYPE.BLANK) break;
if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
evt.preventDefault();
evt.stopImmediatePropagation();
this.select();

View File

@ -2,15 +2,6 @@ import { SlashCommand } from './SlashCommand.js';
/**@readonly*/
/**@enum {Number}*/
export const OPTION_TYPE = {
'COMMAND': 1,
'QUICK_REPLY': 2,
'VARIABLE_NAME': 3,
'BLANK': 4,
};
export class SlashCommandFuzzyScore {
/**@type {number}*/ start;
/**@type {number}*/ longestConsecutive;
@ -27,7 +18,6 @@ export class SlashCommandFuzzyScore {
export class SlashCommandAutoCompleteOption {
/**@type {OPTION_TYPE}*/ type;
/**@type {string|SlashCommand}*/ value;
/**@type {string}*/ name;
/**@type {SlashCommandFuzzyScore}*/ score;
@ -36,39 +26,15 @@ export class SlashCommandAutoCompleteOption {
/**
* @param {OPTION_TYPE} type
* @param {string|SlashCommand} value
* @param {string} name
*/
constructor(type, value, name) {
this.type = type;
constructor(value, name) {
this.value = value;
this.name = name;
}
renderItem() {
let li;
switch (this.type) {
case OPTION_TYPE.COMMAND: {
/**@type {SlashCommand}*/
// @ts-ignore
const cmd = this.value;
li = cmd.renderHelpItem(this.name);
break;
}
case OPTION_TYPE.QUICK_REPLY: {
li = this.makeItem(this.name, 'QR', true);
break;
}
case OPTION_TYPE.VARIABLE_NAME: {
li = this.makeItem(this.name, '𝑥', true);
break;
}
}
li.setAttribute('data-name', this.name);
return li;
}
makeItem(key, typeIcon, noSlash, namedArguments = [], unnamedArguments = [], returnType = 'void', helpString = '', aliasList = []) {
const li = document.createElement('li'); {
li.classList.add('item');
@ -174,7 +140,7 @@ export class SlashCommandAutoCompleteOption {
const returns = document.createElement('span'); {
returns.classList.add('returns');
returns.textContent = returnType ?? 'void';
body.append(returns);
// body.append(returns);
}
specs.append(body);
}
@ -207,85 +173,23 @@ export class SlashCommandAutoCompleteOption {
// li.append(aliases);
}
}
// gotta listen to pointerdown (happens before textarea-blur)
li.addEventListener('pointerdown', ()=>{
// gotta catch pointerup to restore focus to textarea (blurs after pointerdown)
this.pointerup = new Promise(resolve=>{
const resolver = ()=>{
window.removeEventListener('pointerup', resolver);
resolve();
};
window.addEventListener('pointerup', resolver);
});
this.select();
});
}
return li;
}
/**
* @returns {HTMLElement}
*/
renderItem() {
throw new Error(`${this.constructor.name}.renderItem() is not implemented`);
}
/**
* @returns {DocumentFragment}
*/
renderDetails() {
switch (this.type) {
case OPTION_TYPE.COMMAND: {
return this.renderCommandDetails();
}
case OPTION_TYPE.QUICK_REPLY: {
return this.renderQuickReplyDetails();
}
case OPTION_TYPE.VARIABLE_NAME: {
return this.renderVariableDetails();
}
default: {
return this.renderBlankDetails();
}
}
}
renderBlankDetails() {
return 'BLANK';
}
renderQuickReplyDetails() {
const frag = document.createDocumentFragment();
const specs = document.createElement('div'); {
specs.classList.add('specs');
const name = document.createElement('div'); {
name.classList.add('name');
name.classList.add('monospace');
name.textContent = this.value.toString();
specs.append(name);
}
frag.append(specs);
}
const help = document.createElement('span'); {
help.classList.add('help');
help.textContent = 'Quick Reply';
frag.append(help);
}
return frag;
}
renderVariableDetails() {
const frag = document.createDocumentFragment();
const specs = document.createElement('div'); {
specs.classList.add('specs');
const name = document.createElement('div'); {
name.classList.add('name');
name.classList.add('monospace');
name.textContent = this.value.toString();
specs.append(name);
}
frag.append(specs);
}
const help = document.createElement('span'); {
help.classList.add('help');
help.textContent = 'scoped variable';
frag.append(help);
}
return frag;
}
renderCommandDetails() {
const key = this.name;
/**@type {SlashCommand} */
// @ts-ignore
const cmd = this.value;
return cmd.renderHelpDetails(key);
throw new Error(`${this.constructor.name}.renderDetails() is not implemented`);
}
}

View File

@ -0,0 +1,27 @@
import { SlashCommandAutoCompleteOption } from './SlashCommandAutoCompleteOption.js';
export class SlashCommandBlankAutoCompleteOption extends SlashCommandAutoCompleteOption {
/**
* @param {string} value
*/
constructor(value) {
super(value, value);
this.dom = this.renderItem();
}
renderItem() {
const li = document.createElement('li'); {
li.classList.add('item');
li.classList.add('blank');
li.textContent = this.name;
}
return li;
}
renderDetails() {
const frag = document.createDocumentFragment();
return frag;
}
}

View File

@ -0,0 +1,30 @@
import { SlashCommand } from './SlashCommand.js';
import { SlashCommandAutoCompleteOption } from './SlashCommandAutoCompleteOption.js';
export class SlashCommandCommandAutoCompleteOption extends SlashCommandAutoCompleteOption {
/**@type {SlashCommand} */
get cmd() {
// @ts-ignore
return this.value;
}
/**
* @param {SlashCommand} value
* @param {string} name
*/
constructor(value, name) {
super(value, name);
}
renderItem() {
let li;
li = this.cmd.renderHelpItem(this.name);
li.setAttribute('data-name', this.name);
return li;
}
renderDetails() {
return this.cmd.renderHelpDetails(this.name);
}
}

View File

@ -1,14 +1,16 @@
import { power_user } from '../power-user.js';
import { isTrueBoolean, uuidv4 } from '../utils.js';
import { SlashCommand } from './SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './SlashCommandArgument.js';
import { OPTION_TYPE, SlashCommandAutoCompleteOption } from './SlashCommandAutoCompleteOption.js';
import { ARGUMENT_TYPE, SlashCommandArgument } from './SlashCommandArgument.js';
import { SlashCommandClosure } from './SlashCommandClosure.js';
import { SlashCommandCommandAutoCompleteOption } from './SlashCommandCommandAutoCompleteOption.js';
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
import { SlashCommandParserError } from './SlashCommandParserError.js';
import { NAME_RESULT_TYPE, SlashCommandParserNameResult } from './SlashCommandParserNameResult.js';
import { SlashCommandQuickReplyAutoCompleteOption } from './SlashCommandQuickReplyAutoCompleteOption.js';
// eslint-disable-next-line no-unused-vars
import { SlashCommandScope } from './SlashCommandScope.js';
import { SlashCommandVariableAutoCompleteOption } from './SlashCommandVariableAutoCompleteOption.js';
/**@readonly*/
/**@enum {Number}*/
@ -178,35 +180,105 @@ export class SlashCommandParser {
relevance: 0,
};
const COMMENT = {
scope: 'comment',
begin: /\/[/#]/,
end: /\||$|:}/,
contains: [],
};
const LET = {
begin: [
/\/(let|var)\s+/,
],
beginScope: {
1: 'variable',
},
end: /\||$|:}/,
contains: [],
};
const SETVAR = {
begin: /\/(setvar|setglobalvar)\s+/,
beginScope: 'variable',
end: /\||$|:}/,
excludeEnd: true,
contains: [],
};
const GETVAR = {
begin: /\/(getvar|getglobalvar)\s+/,
beginScope: 'variable',
end: /\||$|:}/,
excludeEnd: true,
contains: [],
};
const RUN = {
match: [
/\/:/,
/(".+?(?<!\\)") |(\S+?) /,
],
className: {
1: 'variable.language',
2: 'title.function.invoke',
},
contains: [], // defined later
};
const COMMAND = {
scope: 'command',
begin: /\/\S+/,
beginScope: 'title.function',
end: /\||$|:}/,
end: /\||$|(?=:})/,
excludeEnd: true,
contains: [], // defined later
};
const CLOSURE = {
scope: 'closure',
begin: /{:/,
end: /:}/,
end: /:}(\(\))?/,
beginScope: 'punctuation',
endScope: 'punctuation',
contains: [], // defined later
};
const CLOSURE_ARGS = {
scope: 'params',
begin: /:}\(/,
end: /\)/,
contains: [],
};
const NAMED_ARG = {
scope: 'type',
scope: 'property',
begin: /\w+=/,
end: '',
};
const MACRO = {
scope: 'operator',
scope: 'variable',
begin: /{{/,
end: /}}/,
};
RUN.contains.push(
hljs.BACKSLASH_ESCAPE,
NAMED_ARG,
hljs.QUOTE_STRING_MODE,
NUMBER,
MACRO,
CLOSURE,
);
LET.contains.push(
hljs.BACKSLASH_ESCAPE,
NAMED_ARG,
hljs.QUOTE_STRING_MODE,
NUMBER,
MACRO,
CLOSURE,
);
SETVAR.contains.push(
hljs.BACKSLASH_ESCAPE,
NAMED_ARG,
hljs.QUOTE_STRING_MODE,
NUMBER,
MACRO,
CLOSURE,
);
GETVAR.contains.push(
hljs.BACKSLASH_ESCAPE,
NAMED_ARG,
hljs.QUOTE_STRING_MODE,
NUMBER,
MACRO,
CLOSURE,
);
COMMAND.contains.push(
hljs.BACKSLASH_ESCAPE,
NAMED_ARG,
@ -214,35 +286,33 @@ export class SlashCommandParser {
NUMBER,
MACRO,
CLOSURE,
CLOSURE_ARGS,
);
CLOSURE.contains.push(
hljs.BACKSLASH_ESCAPE,
COMMENT,
NAMED_ARG,
hljs.QUOTE_STRING_MODE,
NUMBER,
MACRO,
RUN,
LET,
GETVAR,
SETVAR,
COMMAND,
'self',
CLOSURE_ARGS,
);
CLOSURE_ARGS.contains.push(
hljs.BACKSLASH_ESCAPE,
NAMED_ARG,
hljs.QUOTE_STRING_MODE,
NUMBER,
MACRO,
CLOSURE,
'self',
);
hljs.registerLanguage('stscript', ()=>({
case_insensitive: false,
keywords: ['|'],
contains: [
hljs.BACKSLASH_ESCAPE,
COMMENT,
RUN,
LET,
GETVAR,
SETVAR,
COMMAND,
CLOSURE,
CLOSURE_ARGS,
],
}));
}
@ -277,7 +347,7 @@ export class SlashCommandParser {
* @param {*} text The text to parse.
* @param {*} index Index to check for names (cursor position).
*/
getNameAt(text, index) {
async getNameAt(text, index) {
if (this.text != `{:${text}:}`) {
try {
this.parse(text, false);
@ -300,22 +370,43 @@ export class SlashCommandParser {
;
if (childClosure !== null) return null;
if (executor.name == ':') {
return new SlashCommandParserNameResult(
const options = this.scopeIndex[this.commandIndex.indexOf(executor)]
?.allVariableNames
?.map(it=>new SlashCommandVariableAutoCompleteOption(it))
?? []
;
try {
const qrApi = (await import('../extensions/quick-reply/index.js')).quickReplyApi;
options.push(...qrApi.listSets()
.map(set=>qrApi.listQuickReplies(set).map(qr=>`${set}.${qr}`))
.flat()
.map(qr=>new SlashCommandQuickReplyAutoCompleteOption(qr)),
);
} catch { /* empty */ }
const result = new SlashCommandParserNameResult(
NAME_RESULT_TYPE.CLOSURE,
executor.value.toString(),
executor.start,
this.scopeIndex[this.commandIndex.indexOf(executor)]
?.allVariableNames
?.map(it=>new SlashCommandAutoCompleteOption(OPTION_TYPE.VARIABLE_NAME, it, it))
?? []
,
executor.start - 2,
options,
true,
()=>`No matching variables in scope and no matching Quick Replies for "${result.name}"`,
()=>'No variables in scope and no Quick Replies found.',
);
return result;
}
return new SlashCommandParserNameResult(
const result = new SlashCommandParserNameResult(
NAME_RESULT_TYPE.COMMAND,
executor.name,
executor.start,
executor.start - 2,
Object
.keys(this.commands)
.map(key=>new SlashCommandCommandAutoCompleteOption(this.commands[key], key))
,
false,
()=>`No matching slash commands for "/${result.name}"`,
()=>'No slash commands found!',
);
return result;
}
return null;
}

View File

@ -15,6 +15,9 @@ export class SlashCommandParserNameResult {
/**@type {string} */ name;
/**@type {number} */ start;
/**@type {SlashCommandAutoCompleteOption[]} */ optionList = [];
/**@type {boolean} */ canBeQuoted = false;
/**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`;
/**@type {()=>string} */ makeNoOptionstext = ()=>'No options';
/**
@ -22,11 +25,17 @@ export class SlashCommandParserNameResult {
* @param {string} name Name (potentially partial) of the name at the requested index.
* @param {number} start Index where the name starts.
* @param {SlashCommandAutoCompleteOption[]} optionList A list of autocomplete options found in the current scope.
* @param {boolean} canBeQuoted Whether the name can be inside quotes.
* @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found.
* @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against.
*/
constructor(type, name, start, optionList = []) {
constructor(type, name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) {
this.type = type;
this.name = name;
this.start = start;
this.optionList = optionList;
this.canBeQuoted = canBeQuoted;
this.noMatchText = makeNoMatchText ?? this.makeNoMatchText;
this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionstext;
}
}

View File

@ -0,0 +1,39 @@
import { SlashCommandAutoCompleteOption } from './SlashCommandAutoCompleteOption.js';
export class SlashCommandQuickReplyAutoCompleteOption extends SlashCommandAutoCompleteOption {
/**
* @param {string} value
*/
constructor(value) {
super(value, value);
}
renderItem() {
let li;
li = this.makeItem(this.name, 'QR', true);
li.setAttribute('data-name', this.name);
return li;
}
renderDetails() {
const frag = document.createDocumentFragment();
const specs = document.createElement('div'); {
specs.classList.add('specs');
const name = document.createElement('div'); {
name.classList.add('name');
name.classList.add('monospace');
name.textContent = this.value.toString();
specs.append(name);
}
frag.append(specs);
}
const help = document.createElement('span'); {
help.classList.add('help');
help.textContent = 'Quick Reply';
frag.append(help);
}
return frag;
}
}

View File

@ -0,0 +1,39 @@
import { SlashCommandAutoCompleteOption } from './SlashCommandAutoCompleteOption.js';
export class SlashCommandVariableAutoCompleteOption extends SlashCommandAutoCompleteOption {
/**
* @param {string} value
*/
constructor(value) {
super(value, value);
}
renderItem() {
let li;
li = this.makeItem(this.name, '𝑥', true);
li.setAttribute('data-name', this.name);
return li;
}
renderDetails() {
const frag = document.createDocumentFragment();
const specs = document.createElement('div'); {
specs.classList.add('specs');
const name = document.createElement('div'); {
name.classList.add('name');
name.classList.add('monospace');
name.textContent = this.value.toString();
specs.append(name);
}
frag.append(specs);
}
const help = document.createElement('span'); {
help.classList.add('help');
help.textContent = 'scoped variable';
frag.append(help);
}
return frag;
}
}

View File

@ -384,7 +384,7 @@ function getTagKey() {
return selected_group;
}
if (this_chid && menu_type === 'character_edit') {
if (this_chid !== undefined && menu_type === 'character_edit') {
return characters[this_chid].avatar;
}

View File

@ -1064,40 +1064,6 @@ select {
order: 3;
}
.slashCommandAutoComplete.defaultDark {
--ac-color-border: rgb(69 69 69);
--ac-color-background: rgb(32 32 32);
--ac-color-text: rgb(204 204 204);
--ac-color-matched-background: transparent;
--ac-color-matched-text: rgb(108 171 251);
--ac-color-selected-background: rgb(32 57 92);
--ac-color-selected-text: rgb(255 255 255);
--ac-color-hovered-background: rgb(43 45 46);
--ac-color-hovered-text: rgb(204 204 204);
}
.slashCommandAutoComplete.defaultLight {
--ac-color-border: rgb(200 200 200);
--ac-color-background: rgb(248 248 248);
--ac-color-text: rgb(59 59 59);
--ac-color-matched-background: transparent;
--ac-color-matched-text: rgb(61 104 188);
--ac-color-selected-background: rgb(232 232 232);
--ac-color-selected-text: rgb(0 0 0);
--ac-color-hovered-background: rgb(242 242 242);
--ac-color-hovered-text: rgb(59 59 59);
}
.slashCommandAutoComplete.defaultThemed {
--ac-color-border: var(--SmartThemeBorderColor);
--ac-color-background: var(--SmartThemeBlurTintColor);
--ac-color-text: var(--SmartThemeEmColor);
--ac-color-matched-background: transparent;
--ac-color-matched-text: var(--SmartThemeQuoteColor);
--ac-color-selected-background: color-mix(in srgb, rgb(128 128 128) 75%, var(--SmartThemeChatTintColor));
--ac-color-selected-text: var(--SmartThemeBodyColor);
--ac-color-hovered-background: color-mix(in srgb, rgb(128 128 128) 30%, var(--SmartThemeChatTintColor));
--ac-color-hovered-text: var(--SmartThemeEmColor);
}
.slashCommandAutoComplete-wrap {
--targetOffset: 0;
--direction: column;
@ -1202,23 +1168,94 @@ select {
}
}
body[data-stscript-style="dark"] {
--ac-style-color-border: rgba(69 69 69 / 1);
--ac-style-color-background: rgba(32 32 32 / 1);
--ac-style-color-text: rgba(204 204 204 / 1);
--ac-style-color-matchedBackground: rgba(0 0 0 / 0);
--ac-style-color-matchedText: rgba(108 171 251 / 1);
--ac-style-color-selectedBackground: rgba(32 57 92 / 1);
--ac-style-color-selectedText: rgba(255 255 255 / 1);
--ac-style-color-hoveredBackground: rgba(43 45 46 / 1);
--ac-style-color-hoveredText: rgba(204 204 204 / 1);
--ac-style-color-argName: rgba(171 209 239 / 1);
--ac-style-color-type: rgba(131 199 177 / 1);
--ac-style-color-cmd: rgba(219 219 173 / 1);
--ac-style-color-symbol: rgba(115 156 211 / 1);
--ac-style-color-string: rgba(190 146 122 / 1);
--ac-style-color-number: rgba(188 205 170 / 1);
--ac-style-color-variable: rgba(131 193 252 / 1);
--ac-style-color-variableLanguage: rgba(98 160 251 / 1);
--ac-style-color-punctuation: rgba(242 214 48 / 1);
--ac-style-color-punctuationL1: rgba(195 118 210 / 1);
--ac-style-color-punctuationL2: rgba(98 160 251 / 1);
--ac-style-color-currentParenthesis: rgba(195 118 210 / 1);
--ac-style-color-comment: rgba(122 151 90 / 1);
}
body[data-stscript-style="light"] {
--ac-style-color-border: rgba(200 200 200 / 1);
--ac-style-color-background: rgba(248 248 248 / 1);
--ac-style-color-text: rgba(59 59 59 / 1);
--ac-style-color-matchedBackground: rgba(0 0 0 / 0);
--ac-style-color-matchedText: rgba(61 104 188 / 1);
--ac-style-color-selectedBackground: rgba(232 232 232 / 1);
--ac-style-color-selectedText: rgba(0 0 0 / 1);
--ac-style-color-hoveredBackground: rgba(242 242 242 / 1);
--ac-style-color-hoveredText: rgba(59 59 59 / 1);
--ac-style-color-argName: rgba(16 24 125 / 1);
--ac-style-color-type: rgba(80 127 152 / 1);
--ac-style-color-cmd: rgba(113 94 43 / 1);
--ac-style-color-symbol: rgba(36 37 249 / 1);
--ac-style-color-string: rgba(139 31 24 / 1);
--ac-style-color-number: rgba(76 132 91 / 1);
--ac-style-color-variable: rgba(16 24 125 / 1);
--ac-style-color-currentParenthesis: rgba(195 118 210 / 1);
--ac-style-color-comment: rgba(70 126 26 / 1);
}
body[data-stscript-style="theme"] {
--ac-style-color-border: var(--SmartThemeBorderColor);
--ac-style-color-background: var(--SmartThemeBlurTintColor);
--ac-style-color-text: var(--SmartThemeEmColor);
--ac-style-color-matchedBackground: rgba(0 0 0 / 0);
--ac-style-color-matchedText: var(--SmartThemeQuoteColor);
--ac-style-color-selectedBackground: color-mix(in srgb, rgb(128 128 128) 75%, var(--SmartThemeChatTintColor));
--ac-style-color-selectedText: var(--SmartThemeBodyColor);
--ac-style-color-hoveredBackground: color-mix(in srgb, rgb(128 128 128) 30%, var(--SmartThemeChatTintColor));
--ac-style-color-hoveredText: var(--SmartThemeEmColor);
--ac-style-color-argName: rgba(171 209 239 / 1);
--ac-style-color-type: rgba(131 199 177 / 1);
--ac-style-color-cmd: rgba(219 219 173 / 1);
--ac-style-color-symbol: rgba(115 156 211 / 1);
--ac-style-color-string: rgba(190 146 122 / 1);
--ac-style-color-variable: rgba(131 193 252 / 1);
--ac-style-color-currentParenthesis: rgba(195 118 210 / 1);
--ac-style-color-comment: rgba(122 151 90 / 1);
}
.slashCommandAutoComplete, .slashCommandAutoComplete-details {
--ac-color-border: rgb(69 69 69);
--ac-color-background: rgb(32 32 32);
--ac-color-text: rgb(204 204 204);
--ac-color-matched-background: transparent;
--ac-color-matched-text: rgb(108 171 251);
--ac-color-selected-background: rgb(32 57 92);
--ac-color-selected-text: rgb(255 255 255);
--ac-color-hovered-background: rgb(43 45 46);
--ac-color-hovered-text: rgb(204 204 204);
--ac-color-arg-name: rgb(171 209 239);
--ac-color-type: rgb(131 199 177);
--ac-color-cmd: rgb(219 219 173);
--ac-color-symbol: rgb(115 156 211);
--ac-color-string: rgb(190 146 122);
--ac-color-variable: rgb(131 193 252);
--ac-color-current-parenthesis: rgb(195 118 210);
--ac-color-border: var(--ac-style-color-border, rgba(69 69 69 / 1));
--ac-color-background: var(--ac-style-color-background, rgba(32 32 32 / 1));
--ac-color-text: var(--ac-style-color-text, rgba(204 204 204 / 1));
--ac-color-matchedBackground: var(--ac-style-color-matchedBackground, rgba(0 0 0 / 0));
--ac-color-matchedText: var(--ac-style-color-matchedText, rgba(108 171 251 / 1));
--ac-color-selectedBackground: var(--ac-style-color-selectedBackground, rgba(32 57 92 / 1));
--ac-color-selectedText: var(--ac-style-color-selectedText, rgba(255 255 255 / 1));
--ac-color-hoveredBackground: var(--ac-style-color-hoveredBackground, rgba(43 45 46 / 1));
--ac-color-hoveredText: var(--ac-style-color-hoveredText, rgba(204 204 204 / 1));
--ac-color-argName: var(--ac-style-color-argName, rgba(171 209 239 / 1));
--ac-color-type: var(--ac-style-color-type, rgba(131 199 177 / 1));
--ac-color-cmd: var(--ac-style-color-cmd, rgba(219 219 173 / 1));
--ac-color-symbol: var(--ac-style-color-symbol, rgba(115 156 211 / 1));
--ac-color-string: var(--ac-style-color-string, rgba(190 146 122 / 1));
--ac-color-number: var(--ac-style-color-number, rgba(188 205 170 / 1));
--ac-color-variable: var(--ac-style-color-variable, rgba(131 193 252 / 1));
--ac-color-variableLanguage: var(--ac-style-color-variableLanguage, rgba(98 160 251 / 1));
--ac-color-punctuation: var(--ac-style-color-punctuation, rgba(242 214 48 / 1));
--ac-color-punctuationL1: var(--ac-style-color-punctuationL1, rgba(195 118 210 / 1));
--ac-color-punctuationL2: var(--ac-style-color-punctuationL2, rgba(98 160 251 / 1));
--ac-color-currentParenthesis: var(--ac-style-color-currentParenthesis, rgba(195 118 210 / 1));
--ac-color-comment: var(--ac-style-color-comment, rgba(122 151 90 / 1));
--bottom: 50vh;
background: var(--ac-color-background);
backdrop-filter: blur(var(--SmartThemeBlurStrength));
@ -1235,6 +1272,44 @@ select {
text-align: left;
z-index: 10000;
}
body[data-stscript-style] .hljs.language-stscript {
* { text-shadow: none !important; }
text-shadow: none !important;
background-color: var(--ac-style-color-background);
color: var(--ac-style-color-text);
.hljs-title.function_ { color: var(--ac-style-color-cmd); }
.hljs-title.function_.invoke__ { color: var(--ac-style-color-cmd); }
.hljs-string { color: var(--ac-style-color-string); }
.hljs-number { color: var(--ac-style-color-number); }
.hljs-variable { color: var(--ac-style-color-variable); }
.hljs-variable.language_ { color: var(--ac-style-color-variableLanguage); }
.hljs-property { color: var(--ac-style-color-argName); }
.hljs-punctuation { color: var(--ac-style-color-punctuation); }
.hljs-keyword { color: var(--ac-style-color-variableLanguage); }
.hljs-comment { color: var(--ac-style-color-comment); }
.hljs-closure {
> .hljs-punctuation { color: var(--ac-style-color-punctuation); }
.hljs-closure {
> .hljs-punctuation { color: var(--ac-style-color-punctuationL1); }
.hljs-closure {
> .hljs-punctuation { color: var(--ac-style-color-punctuationL2); }
.hljs-closure {
> .hljs-punctuation { color: var(--ac-style-color-punctuation); }
.hljs-closure {
> .hljs-punctuation { color: var(--ac-style-color-punctuationL1); }
.hljs-closure {
> .hljs-punctuation { color: var(--ac-style-color-punctuationL2); }
}
}
}
}
}
}
}
.slashCommandAutoComplete {
padding-bottom: 1px;
/* position: absolute; */
@ -1251,13 +1326,17 @@ select {
gap: 0.5em;
font-size: 0.8em;
display: contents;
&.blank {
display: block;
grid-column: 1 / 4;
}
&:hover > * {
background-color: var(--ac-color-hovered-background);
color: var(--ac-color-hovered-text);
background-color: var(--ac-color-hoveredBackground);
color: var(--ac-color-hoveredText);
}
&.selected > * {
background-color: var(--ac-color-selected-background);
color: var(--ac-color-selected-text);
background-color: var(--ac-color-selectedBackground);
color: var(--ac-color-selectedText);
}
> * {
height: 100%;
@ -1282,8 +1361,8 @@ select {
align-items: flex-start;
> .name {
> .matched {
background-color: var(--ac-color-matched-background);
color: var(--ac-color-matched-text);
background-color: var(--ac-color-matchedBackground);
color: var(--ac-color-matchedText);
font-weight: bold;
}
}
@ -1338,7 +1417,7 @@ select {
margin: 0;
padding-left: 1.25em;
> .argumentItem::marker {
color: color-mix(in srgb, var(--ac-color-text), var(--ac-color-background));
color: color-mix(in srgb, var(--ac-color-text), var(--ac-style-color-background));
}
.argumentSpec {
@ -1395,7 +1474,6 @@ select {
> .help {
padding: 0 0.5em 0.5em 0.5em;
div {
margin-block-start: 1em;
margin-block-end: 1em;
}
ul {
@ -1406,10 +1484,12 @@ select {
margin: 0;
> code {
display: block;
padding: 0;
}
}
}
> .aliases {
padding: 0 0.5em 0.5em 0.5em;
&:before { content: '(alias: '; }
> .alias {
font-family: monospace;
@ -1448,7 +1528,7 @@ select {
color: var(--ac-color-text);
}
> .argument-name {
color: var(--ac-color-arg-name);
color: var(--ac-color-argName);
}
}
&.unnamedArgument {