mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
* set isForced to true on input
* make floating auto-complete follow horizontal scrolling
* add callable closure vars
* changes to /let and /var for callable closures
* fix error message
* fix scope for closure arguments
* if should return the pipe result from closures
* use /run to call closures and no arguments on immediate closures
* throw exception from QRs window-function if no match
* when to show autocomplete vs info only
* autocomplete positioning
* autocomplete styling
* add theming to autocomplete (theme, dark, light)
* improve autocomplete show/hide logic and editor selection
* use blur tint color instead of chat tint color and use blur setting
* cleanup and docs
* use scope macros for QR args
* add enter to select autocomplete
* fix no executor found
* cleanup and comment
* fix alias list in help string
* fallback to empty string piped value if null or undefined
* fix typo
* blur textarea on ctrl+enter execute (and refocus after)
* stop executeSlashCommand if parser throws
* move /let and /var callbacks into functions
* switch textarea to monospace when value starts with slash
* add double pipe a pipe breaker
* fix /? slash
* remove some logging
* add "/:name" as shorthand for "/run name" after all
* move shit around
* fix error message
* use testRunShorthandEnd
* use parseQuotedValue and parseValue to determine name for "/:"
QR labels and set names can include spaces
* add some adjustments to make autocomplete work properly
some hint in there about "/:" would still be nice
* add autocomplete style selector
* only strip quotes from subcommand if they are at both ends
* fix JSDoc
* escaping
* allow open quotes on dry run
* throwing shit at the wall for /: autocomplete
* escapes only for symbols
* clean up autocomplete
* improve performance
* fix scope macros
* remove unescaping of pipes
* fix macros in scope copy
* fix "/? slash"
* don't run parser for getNameAt if text has not changed
* fix options filter
* re-enable blur listener
* restore selection on non-replace select
* fix for escaping first character of value
* add support for {{pipe}} and {{var::}} closures
* add index support to var macro
* add scoped var macro to macro help
* more escape fixes
* reduce autocomplete render debounce
* cleanup
* restore old escape handling and parser flag for strict escaping
* fix "no match" autocomplete message
* add dummy commands for comments and parser flag
* fix type annotations
* somewhat safer macro replacements
* fix autocomplete select on blank / "no match"
* fix cutting off handled part in substitution
* add parser flag REPLACE_GETVAR
Replaces all {{getvar::}} and {{getglobalvar::}} macros with {{var::}}.
Inserts a series of command executors before the command with the macros that:
- save {{pipe}} to a var
- call /getvar or /getglobalvar to get the variable used in the macro
- call /let to save the retrieved variable
- return the saved {{pipe}} value
This helps to avoid double-substitutions when the var values contain text that could be interpreted as macros.
* remove old parser
* fix send on enter when no match
* deal with pipes in quoted values (loose escaping)
* add default parser flags to user settings
* allow quoted values in unnamed argument
* set parser flag without explicit state to "on"
* add click hint on parser error toast
* dirty more detailed cmd defs
* remove name from unnamed arg
* move autocomplete into class and floating with details
* replace jQuery's trigger('input') on #send_textarea with native events because jQuery does not dispatch the native event
* fix ctrl+space
* fix arrow navigation
* add comments
* fix pointer block
* add static fromProps
* fix up dummy commands
* migrate all commands to addCommandObject
* remove commented comment command
* fix alias in details
* add range as argument type
* switch to addCommandObject
* switch to addCommandObject
* fix height
* fix floating details position on left
* re-enable blur event
* use auto width for full details on floating autocomplete
* auto-size floating full details
* fix typo
* re-enable blur listener
* don't prevent enter when selected item is fully typed out
* add autocomplete details tooltips
* add language to slash command examples
* move makeItem into option and command and fix click select
* use autocomplete parts in /? slash
* fix alias formatting
* add language to slash command examples
* fix details position on initial input history
* small screen styles
* replace registerSlashCommand with detailed declarations
* put name on first line
* add missing returns
* fix missing comma
* fix alias display in autocomplete list
* remove args from help string
* move parser settings to its own section
* jsdoc
* hljs stscript lang
* add hljs to autocomplete help examples
* add missing import
* apply autocomplete colors to stscript codeblocks (hljs)
* add fromProps
* cache autocomplete elements
* towards generic autocomplete
* remove unused imports
* fix blanks
* add return types
* re-enable blur
* fix blank check
* Caption messages by id
* add aborting command execution
* fix return type
* fix chat input font reset
* add slash command progress indicator
* add missing return
* mark registerSlashCommand deprecated
* why??
* separate abort logic for commands
* remove parsing of quoted values from unnamed arg
* add adjustable autocomplete width
* revert stop button pulse
* add progress and pause/abort to QR editor
* add resize event on autocomplete width change
* add key= argument to all get vars
* refactoring
* introduce NamedArgumentAsignment
* add TODOs
* refactoring
* record start and end of named arg assignment
* refactoring
* prevent duplicate calls to show
* refactoring
* remove macro ac
* add secondary autocomplete and enum descriptions
* add syntax highlighting to QR editor
* add enum descriptions to /while
* add /let key=... to scope variable names
* add unnamed argument assignment class and unnamed argument splitting
* fix QR editor style
* remove dash before autocomplete help text
* add autocomplete for unnamed enums
* fix remaining dom after holding backslash
* fix for unnamed enums
* fix autocomplete for /parser-flag
* add parser-flag enum help
* fix type annotations
* fix autocomplete result for /:
* add colored autocomplete type icons
* collapse second line autocomplete help if empty
* mark optional named args in autocomplete
* fix when what
* remove duplicate debug buttons
* dispatch input on autocomplete select
* prevent grow from editor syntax layer
* add auto-adjust qr editor caret color
* remove text-shadow from autocomplete
* join value strings in /let and /var
* add /abort syntax highlight
* fix attempting secondary result when there is none
* rename settings headers and split autocomplete / stscript
* add parser flag tooltips
* add tooltips to chat width stops
* fix typo
* return clone of help item
* fix enum string
* don't make optional notice for autocomplete arguments smaller
* avoid scrollbar in chat input
* add rudimentary macro autocomplete
* strip macro from helptext
* finally remove closure delimiters around root
* cleanup
* fix index stuff for removed closure delimiters
* fix type hint
* add child commands to progress indicator
* include sub-separator in macro autocomplete
* remove all mentions of interruptsGeneration and purge
* remove unused imports
* fix syntax highlight with newline at end of input
* cleanup select pointer events
* coalesce onProgress call
* add regex to STscript syntax highlighting
* fix closure end
* fix autocomplete type icon alignment
* adjustments for small screens
* fix removing wrong element
* add missing "at=" arg to /sys, /comment, /sendas
* add font scale setting for autocomplete
* add target=_blank for parser flag links
* fix for searching enums
* remove REGEXP_MODE from hljs
just causes trouble
* fix autocomplete in closures
* fix typo
* fix type hint
* Get rid of scroll bar on load
* Add type hint for /send name argument. Fix 'at' types
* Add 'negative' arg hint to /sd command
* reenable blur event
* Allow /summarize to process any text
* Compact layout of script toggles
* Expand CSS by default
* fix double ranger indicator and adjust to narrow container
* make custom css input fill available vertical space
* reduce scroll lag
* use default cursor on scrollbar
* Clean-up module loading in index.html
* fix tab indent with hljs
---------
Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
262 lines
8.6 KiB
JavaScript
262 lines
8.6 KiB
JavaScript
import { chat, chat_metadata, eventSource, event_types, getRequestHeaders } from '../../../script.js';
|
|
import { extension_settings } from '../../extensions.js';
|
|
import { QuickReplyApi } from './api/QuickReplyApi.js';
|
|
import { AutoExecuteHandler } from './src/AutoExecuteHandler.js';
|
|
import { QuickReply } from './src/QuickReply.js';
|
|
import { QuickReplyConfig } from './src/QuickReplyConfig.js';
|
|
import { QuickReplySet } from './src/QuickReplySet.js';
|
|
import { QuickReplySettings } from './src/QuickReplySettings.js';
|
|
import { SlashCommandHandler } from './src/SlashCommandHandler.js';
|
|
import { ButtonUi } from './src/ui/ButtonUi.js';
|
|
import { SettingsUi } from './src/ui/SettingsUi.js';
|
|
|
|
|
|
|
|
|
|
const _VERBOSE = true;
|
|
export const log = (...msg) => _VERBOSE ? console.log('[QR2]', ...msg) : null;
|
|
export const warn = (...msg) => _VERBOSE ? console.warn('[QR2]', ...msg) : null;
|
|
/**
|
|
* Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked.
|
|
* @param {Function} func The function to debounce.
|
|
* @param {Number} [timeout=300] The timeout in milliseconds.
|
|
* @returns {Function} The debounced function.
|
|
*/
|
|
export function debounceAsync(func, timeout = 300) {
|
|
let timer;
|
|
/**@type {Promise}*/
|
|
let debouncePromise;
|
|
/**@type {Function}*/
|
|
let debounceResolver;
|
|
return (...args) => {
|
|
clearTimeout(timer);
|
|
if (!debouncePromise) {
|
|
debouncePromise = new Promise(resolve => {
|
|
debounceResolver = resolve;
|
|
});
|
|
}
|
|
timer = setTimeout(() => {
|
|
debounceResolver(func.apply(this, args));
|
|
debouncePromise = null;
|
|
}, timeout);
|
|
return debouncePromise;
|
|
};
|
|
}
|
|
|
|
|
|
const defaultConfig = {
|
|
setList: [{
|
|
set: 'Default',
|
|
isVisible: true,
|
|
}],
|
|
};
|
|
|
|
const defaultSettings = {
|
|
isEnabled: false,
|
|
isCombined: false,
|
|
config: defaultConfig,
|
|
};
|
|
|
|
|
|
/** @type {Boolean}*/
|
|
let isReady = false;
|
|
/** @type {Function[]}*/
|
|
let executeQueue = [];
|
|
/** @type {QuickReplySettings}*/
|
|
let settings;
|
|
/** @type {SettingsUi} */
|
|
let manager;
|
|
/** @type {ButtonUi} */
|
|
let buttons;
|
|
/** @type {AutoExecuteHandler} */
|
|
let autoExec;
|
|
/** @type {QuickReplyApi} */
|
|
export let quickReplyApi;
|
|
|
|
|
|
|
|
|
|
const loadSets = async () => {
|
|
const response = await fetch('/api/settings/get', {
|
|
method: 'POST',
|
|
headers: getRequestHeaders(),
|
|
body: JSON.stringify({}),
|
|
});
|
|
|
|
if (response.ok) {
|
|
const setList = (await response.json()).quickReplyPresets ?? [];
|
|
for (const set of setList) {
|
|
if (set.version !== 2) {
|
|
// migrate old QR set
|
|
set.version = 2;
|
|
set.disableSend = set.quickActionEnabled ?? false;
|
|
set.placeBeforeInput = set.placeBeforeInputEnabled ?? false;
|
|
set.injectInput = set.AutoInputInject ?? false;
|
|
set.qrList = set.quickReplySlots.map((slot,idx)=>{
|
|
const qr = {};
|
|
qr.id = idx + 1;
|
|
qr.label = slot.label ?? '';
|
|
qr.title = slot.title ?? '';
|
|
qr.message = slot.mes ?? '';
|
|
qr.isHidden = slot.hidden ?? false;
|
|
qr.executeOnStartup = slot.autoExecute_appStartup ?? false;
|
|
qr.executeOnUser = slot.autoExecute_userMessage ?? false;
|
|
qr.executeOnAi = slot.autoExecute_botMessage ?? false;
|
|
qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false;
|
|
qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false;
|
|
qr.automationId = slot.automationId ?? '';
|
|
qr.contextList = (slot.contextMenu ?? []).map(it=>({
|
|
set: it.preset,
|
|
isChained: it.chain,
|
|
}));
|
|
return qr;
|
|
});
|
|
}
|
|
if (set.version == 2) {
|
|
QuickReplySet.list.push(QuickReplySet.from(JSON.parse(JSON.stringify(set))));
|
|
}
|
|
}
|
|
// need to load QR lists after all sets are loaded to be able to resolve context menu entries
|
|
setList.forEach((set, idx)=>{
|
|
QuickReplySet.list[idx].qrList = set.qrList.map(it=>QuickReply.from(it));
|
|
QuickReplySet.list[idx].init();
|
|
});
|
|
log('sets: ', QuickReplySet.list);
|
|
}
|
|
};
|
|
|
|
const loadSettings = async () => {
|
|
if (!extension_settings.quickReplyV2) {
|
|
if (!extension_settings.quickReply) {
|
|
extension_settings.quickReplyV2 = defaultSettings;
|
|
} else {
|
|
extension_settings.quickReplyV2 = {
|
|
isEnabled: extension_settings.quickReply.quickReplyEnabled ?? false,
|
|
isCombined: false,
|
|
isPopout: false,
|
|
config: {
|
|
setList: [{
|
|
set: extension_settings.quickReply.selectedPreset ?? extension_settings.quickReply.name ?? 'Default',
|
|
isVisible: true,
|
|
}],
|
|
},
|
|
};
|
|
}
|
|
}
|
|
try {
|
|
settings = QuickReplySettings.from(extension_settings.quickReplyV2);
|
|
} catch (ex) {
|
|
settings = QuickReplySettings.from(defaultSettings);
|
|
}
|
|
};
|
|
|
|
const executeIfReadyElseQueue = async (functionToCall, args) => {
|
|
if (isReady) {
|
|
log('calling', { functionToCall, args });
|
|
await functionToCall(...args);
|
|
} else {
|
|
log('queueing', { functionToCall, args });
|
|
executeQueue.push(async()=>await functionToCall(...args));
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
const init = async () => {
|
|
await loadSets();
|
|
await loadSettings();
|
|
log('settings: ', settings);
|
|
|
|
manager = new SettingsUi(settings);
|
|
document.querySelector('#extensions_settings2').append(await manager.render());
|
|
|
|
buttons = new ButtonUi(settings);
|
|
buttons.show();
|
|
settings.onSave = ()=>buttons.refresh();
|
|
|
|
window['executeQuickReplyByName'] = async(name, args = {}) => {
|
|
let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])]
|
|
.map(it=>it.set.qrList)
|
|
.flat()
|
|
.find(it=>it.label == name)
|
|
;
|
|
if (!qr) {
|
|
let [setName, ...qrName] = name.split('.');
|
|
qrName = qrName.join('.');
|
|
let qrs = QuickReplySet.get(setName);
|
|
if (qrs) {
|
|
qr = qrs.qrList.find(it=>it.label == qrName);
|
|
}
|
|
}
|
|
if (qr && qr.onExecute) {
|
|
return await qr.execute(args, false, true);
|
|
} else {
|
|
throw new Error(`No Quick Reply found for "${name}".`);
|
|
}
|
|
};
|
|
|
|
quickReplyApi = new QuickReplyApi(settings, manager);
|
|
const slash = new SlashCommandHandler(quickReplyApi);
|
|
slash.init();
|
|
autoExec = new AutoExecuteHandler(settings);
|
|
|
|
eventSource.on(event_types.APP_READY, async()=>await finalizeInit());
|
|
|
|
window['quickReplyApi'] = quickReplyApi;
|
|
};
|
|
const finalizeInit = async () => {
|
|
log('executing startup');
|
|
await autoExec.handleStartup();
|
|
log('/executing startup');
|
|
|
|
log(`executing queue (${executeQueue.length} items)`);
|
|
while (executeQueue.length > 0) {
|
|
const func = executeQueue.shift();
|
|
await func();
|
|
}
|
|
log('/executing queue');
|
|
isReady = true;
|
|
log('READY');
|
|
};
|
|
await init();
|
|
|
|
const onChatChanged = async (chatIdx) => {
|
|
log('CHAT_CHANGED', chatIdx);
|
|
if (chatIdx) {
|
|
settings.chatConfig = QuickReplyConfig.from(chat_metadata.quickReply ?? {});
|
|
} else {
|
|
settings.chatConfig = null;
|
|
}
|
|
manager.rerender();
|
|
buttons.refresh();
|
|
|
|
await autoExec.handleChatChanged();
|
|
};
|
|
eventSource.on(event_types.CHAT_CHANGED, (...args)=>executeIfReadyElseQueue(onChatChanged, args));
|
|
|
|
const onUserMessage = async () => {
|
|
await autoExec.handleUser();
|
|
};
|
|
eventSource.on(event_types.USER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onUserMessage, args));
|
|
|
|
const onAiMessage = async (messageId) => {
|
|
if (['...'].includes(chat[messageId]?.mes)) {
|
|
log('QR auto-execution suppressed for swiped message');
|
|
return;
|
|
}
|
|
|
|
await autoExec.handleAi();
|
|
};
|
|
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onAiMessage, args));
|
|
|
|
const onGroupMemberDraft = async () => {
|
|
await autoExec.handleGroupMemberDraft();
|
|
};
|
|
eventSource.on(event_types.GROUP_MEMBER_DRAFTED, (...args) => executeIfReadyElseQueue(onGroupMemberDraft, args));
|
|
|
|
const onWIActivation = async (entries) => {
|
|
await autoExec.handleWIActivation(entries);
|
|
};
|
|
eventSource.on(event_types.WORLD_INFO_ACTIVATED, (...args) => executeIfReadyElseQueue(onWIActivation, args));
|