| import { hljs, morphdom } from '../../../../lib.js'; |
| import { POPUP_RESULT, POPUP_TYPE, Popup } from '../../../popup.js'; |
| import { setSlashCommandAutoComplete } from '../../../slash-commands.js'; |
| import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js'; |
| import { SlashCommandBreakPoint } from '../../../slash-commands/SlashCommandBreakPoint.js'; |
| import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js'; |
| import { SlashCommandClosureResult } from '../../../slash-commands/SlashCommandClosureResult.js'; |
| import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js'; |
| import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecutor.js'; |
| import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js'; |
| import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js'; |
| import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js'; |
| import { accountStorage } from '../../../util/AccountStorage.js'; |
| import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js'; |
| import { log, quickReplyApi, warn } from '../index.js'; |
| import { QuickReplyContextLink } from './QuickReplyContextLink.js'; |
| import { QuickReplySet } from './QuickReplySet.js'; |
| import { ContextMenu } from './ui/ctx/ContextMenu.js'; |
|
|
| export class QuickReply { |
| |
| |
| |
| static from(props) { |
| props.contextList = (props.contextList ?? []).map(( it)=>QuickReplyContextLink.from(it)); |
| return Object.assign(new this(), props); |
| } |
|
|
|
|
|
|
|
|
| id; |
| icon; |
| label = ''; |
| showLabel = false; |
| title = ''; |
| message = ''; |
|
|
| contextList; |
|
|
| preventAutoExecute = true; |
| isHidden = false; |
| executeOnStartup = false; |
| executeOnUser = false; |
| executeOnAi = false; |
| executeOnChatChange = false; |
| executeOnGroupMemberDraft = false; |
| executeOnNewChat = false; |
| executeBeforeGeneration = false; |
| automationId = ''; |
|
|
| onExecute; |
| onDebug; |
| onDelete; |
| onUpdate; |
| onInsertBefore; |
| onTransfer; |
|
|
|
|
| dom; |
| domIcon; |
| domLabel; |
| settingsDom; |
| settingsDomIcon; |
| settingsDomLabel; |
| settingsDomMessage; |
|
|
| editorPopup; |
| editorDom; |
|
|
| editorMessage; |
| editorMessageLabel; |
| editorSyntax; |
| editorExecuteBtn; |
| editorExecuteBtnPause; |
| editorExecuteBtnStop; |
| editorExecuteProgress; |
| editorExecuteErrors; |
| editorExecuteResult; |
| editorDebugState; |
| editorExecutePromise; |
| isExecuting; |
| abortController; |
| debugController; |
|
|
|
|
| get hasContext() { |
| return this.contextList && this.contextList.filter(it => it.set).length > 0; |
| } |
|
|
|
|
|
|
|
|
| unrender() { |
| this.dom?.remove(); |
| this.dom = null; |
| } |
| updateRender() { |
| if (!this.dom) return; |
| this.dom.title = this.title || this.message; |
| if (this.icon) { |
| this.domIcon.classList.remove('qr--hidden'); |
| if (this.showLabel) this.domLabel.classList.remove('qr--hidden'); |
| else this.domLabel.classList.add('qr--hidden'); |
| } else { |
| this.domIcon.classList.add('qr--hidden'); |
| this.domLabel.classList.remove('qr--hidden'); |
| } |
| this.domLabel.textContent = this.label; |
| this.dom.classList[this.hasContext ? 'add' : 'remove']('qr--hasCtx'); |
| } |
| render() { |
| this.unrender(); |
| if (!this.dom) { |
| const root = document.createElement('div'); { |
| this.dom = root; |
| root.classList.add('qr--button'); |
| root.classList.add('menu_button'); |
| if (this.hasContext) { |
| root.classList.add('qr--hasCtx'); |
| } |
| root.title = this.title || this.message; |
| root.addEventListener('contextmenu', (evt) => { |
| log('contextmenu', this, this.hasContext); |
| if (this.hasContext) { |
| evt.preventDefault(); |
| evt.stopPropagation(); |
| const menu = new ContextMenu(this); |
| menu.show(evt); |
| } |
| }); |
| root.addEventListener('click', (evt)=>{ |
| if (evt.ctrlKey) { |
| this.showEditor(); |
| return; |
| } |
| this.execute(); |
| }); |
| const icon = document.createElement('div'); { |
| this.domIcon = icon; |
| icon.classList.add('qr--button-icon'); |
| icon.classList.add('fa-solid'); |
| if (!this.icon) icon.classList.add('qr--hidden'); |
| else icon.classList.add(this.icon); |
| root.append(icon); |
| } |
| const lbl = document.createElement('div'); { |
| this.domLabel = lbl; |
| lbl.classList.add('qr--button-label'); |
| if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden'); |
| lbl.textContent = this.label; |
| root.append(lbl); |
| } |
| const expander = document.createElement('div'); { |
| expander.classList.add('qr--button-expander'); |
| expander.textContent = '⋮'; |
| expander.title = 'Open context menu'; |
| expander.addEventListener('click', (evt) => { |
| evt.stopPropagation(); |
| evt.preventDefault(); |
| const menu = new ContextMenu(this); |
| menu.show(evt); |
| }); |
| root.append(expander); |
| } |
| } |
| } |
| return this.dom; |
| } |
|
|
|
|
|
|
|
|
| renderSettings(idx) { |
| if (!this.settingsDom) { |
| const item = document.createElement('div'); { |
| this.settingsDom = item; |
| item.classList.add('qr--set-item'); |
| item.setAttribute('data-order', String(idx)); |
| item.setAttribute('data-id', String(this.id)); |
| const adder = document.createElement('div'); { |
| adder.classList.add('qr--set-itemAdder'); |
| const actions = document.createElement('div'); { |
| actions.classList.add('qr--actions'); |
| const addNew = document.createElement('div'); { |
| addNew.classList.add('qr--action'); |
| addNew.classList.add('qr--add'); |
| addNew.classList.add('menu_button'); |
| addNew.classList.add('menu_button_icon'); |
| addNew.classList.add('fa-solid'); |
| addNew.classList.add('fa-plus'); |
| addNew.title = 'Add quick reply'; |
| addNew.addEventListener('click', ()=>this.onInsertBefore()); |
| actions.append(addNew); |
| } |
| const paste = document.createElement('div'); { |
| paste.classList.add('qr--action'); |
| paste.classList.add('qr--paste'); |
| paste.classList.add('menu_button'); |
| paste.classList.add('menu_button_icon'); |
| paste.classList.add('fa-solid'); |
| paste.classList.add('fa-paste'); |
| paste.title = 'Add quick reply from clipboard'; |
| paste.addEventListener('click', async()=>{ |
| const text = await navigator.clipboard.readText(); |
| this.onInsertBefore(text); |
| }); |
| actions.append(paste); |
| } |
| const importFile = document.createElement('div'); { |
| importFile.classList.add('qr--action'); |
| importFile.classList.add('qr--importFile'); |
| importFile.classList.add('menu_button'); |
| importFile.classList.add('menu_button_icon'); |
| importFile.classList.add('fa-solid'); |
| importFile.classList.add('fa-file-import'); |
| importFile.title = 'Add quick reply from JSON file'; |
| importFile.addEventListener('click', async()=>{ |
| const inp = document.createElement('input'); { |
| inp.type = 'file'; |
| inp.accept = '.json'; |
| inp.addEventListener('change', async()=>{ |
| if (inp.files.length > 0) { |
| for (const file of inp.files) { |
| const text = await file.text(); |
| this.onInsertBefore(text); |
| } |
| } |
| }); |
| inp.click(); |
| } |
| }); |
| actions.append(importFile); |
| } |
| adder.append(actions); |
| } |
| item.append(adder); |
| } |
| const itemContent = document.createElement('div'); { |
| itemContent.classList.add('qr--content'); |
| const drag = document.createElement('div'); { |
| drag.classList.add('drag-handle'); |
| drag.classList.add('ui-sortable-handle'); |
| drag.textContent = '☰'; |
| itemContent.append(drag); |
| } |
| const lblContainer = document.createElement('div'); { |
| lblContainer.classList.add('qr--set-itemLabelContainer'); |
| const icon = document.createElement('div'); { |
| this.settingsDomIcon = icon; |
| icon.title = 'Click to change icon'; |
| icon.classList.add('qr--set-itemIcon'); |
| icon.classList.add('menu_button'); |
| icon.classList.add('fa-fw'); |
| if (this.icon) { |
| icon.classList.add('fa-solid'); |
| icon.classList.add(this.icon); |
| } |
| icon.addEventListener('click', async()=>{ |
| let value = await showFontAwesomePicker(); |
| this.updateIcon(value); |
| }); |
| lblContainer.append(icon); |
| } |
| const lbl = document.createElement('input'); { |
| this.settingsDomLabel = lbl; |
| lbl.classList.add('qr--set-itemLabel'); |
| lbl.classList.add('text_pole'); |
| lbl.value = this.label; |
| lbl.addEventListener('input', ()=>this.updateLabel(lbl.value)); |
| lblContainer.append(lbl); |
| } |
| itemContent.append(lblContainer); |
| } |
| item.append(itemContent); |
| } |
| const optContainer = document.createElement('div'); { |
| optContainer.classList.add('qr--set-optionsContainer'); |
| const opt = document.createElement('div'); { |
| opt.classList.add('qr--action'); |
| opt.classList.add('menu_button'); |
| opt.classList.add('fa-fw'); |
| opt.classList.add('fa-solid'); |
| opt.textContent = '⁝'; |
| opt.title = 'Additional options:\n - large editor\n - context menu\n - auto-execution\n - tooltip'; |
| opt.addEventListener('click', ()=>this.showEditor()); |
| optContainer.append(opt); |
| } |
| itemContent.append(optContainer); |
| } |
| const mes = document.createElement('textarea'); { |
| this.settingsDomMessage = mes; |
| mes.id = `qr--set--item${this.id}`; |
| mes.classList.add('qr--set-itemMessage'); |
| mes.value = this.message; |
| |
| $(mes).on('input', ()=>this.updateMessage(mes.value)); |
| itemContent.append(mes); |
| } |
| const actions = document.createElement('div'); { |
| actions.classList.add('qr--actions'); |
| const move = document.createElement('div'); { |
| move.classList.add('qr--action'); |
| move.classList.add('menu_button'); |
| move.classList.add('fa-fw'); |
| move.classList.add('fa-solid'); |
| move.classList.add('fa-truck-arrow-right'); |
| move.title = 'Move quick reply to other set'; |
| move.addEventListener('click', ()=>this.onTransfer(this)); |
| actions.append(move); |
| } |
| const copy = document.createElement('div'); { |
| copy.classList.add('qr--action'); |
| copy.classList.add('menu_button'); |
| copy.classList.add('fa-fw'); |
| copy.classList.add('fa-solid'); |
| copy.classList.add('fa-copy'); |
| copy.title = 'Copy quick reply to clipboard'; |
| copy.addEventListener('click', async()=>{ |
| await navigator.clipboard.writeText(JSON.stringify(this)); |
| copy.classList.add('qr--success'); |
| await delay(3010); |
| copy.classList.remove('qr--success'); |
| }); |
| actions.append(copy); |
| } |
| const cut = document.createElement('div'); { |
| cut.classList.add('qr--action'); |
| cut.classList.add('menu_button'); |
| cut.classList.add('fa-fw'); |
| cut.classList.add('fa-solid'); |
| cut.classList.add('fa-cut'); |
| cut.title = 'Cut quick reply to clipboard (copy and remove)'; |
| cut.addEventListener('click', async()=>{ |
| await navigator.clipboard.writeText(JSON.stringify(this)); |
| this.delete(); |
| }); |
| actions.append(cut); |
| } |
| const exp = document.createElement('div'); { |
| exp.classList.add('qr--action'); |
| exp.classList.add('menu_button'); |
| exp.classList.add('fa-fw'); |
| exp.classList.add('fa-solid'); |
| exp.classList.add('fa-file-export'); |
| exp.title = 'Export quick reply as file'; |
| exp.addEventListener('click', ()=>{ |
| const blob = new Blob([JSON.stringify(this)], { type:'text' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); { |
| a.href = url; |
| a.download = `${this.label}.qr.json`; |
| a.click(); |
| } |
| }); |
| actions.append(exp); |
| } |
| const del = document.createElement('div'); { |
| del.classList.add('qr--action'); |
| del.classList.add('menu_button'); |
| del.classList.add('fa-fw'); |
| del.classList.add('fa-solid'); |
| del.classList.add('fa-trash-can'); |
| del.classList.add('redWarningBG'); |
| del.title = 'Remove Quick Reply\n---\nShift+Click to skip confirmation'; |
| del.addEventListener('click', async(evt)=>{ |
| if (!evt.shiftKey) { |
| const result = await Popup.show.confirm( |
| 'Remove Quick Reply', |
| 'Are you sure you want to remove this Quick Reply?', |
| ); |
| if (result != POPUP_RESULT.AFFIRMATIVE) { |
| return; |
| } |
| } |
| this.delete(); |
| }); |
| actions.append(del); |
| } |
| itemContent.append(actions); |
| } |
| } |
| } |
| return this.settingsDom; |
| } |
| unrenderSettings() { |
| this.settingsDom?.remove(); |
| } |
|
|
| async showEditor() { |
| const response = await fetch('/scripts/extensions/quick-reply/html/qrEditor.html', { cache: 'no-store' }); |
| if (response.ok) { |
| this.template = document.createRange().createContextualFragment(await response.text()).querySelector('#qr--modalEditor'); |
| |
| |
| const dom = this.template.cloneNode(true); |
| this.editorDom = dom; |
| this.editorPopup = new Popup(dom, POPUP_TYPE.TEXT, undefined, { okButton: 'OK', wide: true, large: true, rows: 1 }); |
| const popupResult = this.editorPopup.show(); |
|
|
| |
| |
| const icon = dom.querySelector('#qr--modal-icon'); |
| if (this.icon) { |
| icon.classList.add('fa-solid'); |
| icon.classList.add(this.icon); |
| } |
| else { |
| icon.textContent = '…'; |
| } |
| icon.addEventListener('click', async()=>{ |
| let value = await showFontAwesomePicker(); |
| if (value === null) return; |
| if (this.icon) icon.classList.remove(this.icon); |
| if (value == '') { |
| icon.classList.remove('fa-solid'); |
| icon.textContent = '…'; |
| } else { |
| icon.textContent = ''; |
| icon.classList.add('fa-solid'); |
| icon.classList.add(value); |
| } |
| this.updateIcon(value); |
| }); |
| |
| const showLabel = dom.querySelector('#qr--modal-showLabel'); |
| showLabel.checked = this.showLabel; |
| showLabel.addEventListener('click', ()=>{ |
| this.updateShowLabel(showLabel.checked); |
| }); |
| |
| const label = dom.querySelector('#qr--modal-label'); |
| label.value = this.label; |
| label.addEventListener('input', ()=>{ |
| this.updateLabel(label.value); |
| }); |
| let switcherList; |
| |
| dom.querySelector('#qr--modal-switcher').addEventListener('click', (evt)=>{ |
| if (switcherList) { |
| switcherList.remove(); |
| switcherList = null; |
| return; |
| } |
| const list = document.createElement('ul'); { |
| switcherList = list; |
| list.classList.add('qr--modal-switcherList'); |
| const makeList = (qrs)=>{ |
| const setItem = document.createElement('li'); { |
| setItem.classList.add('qr--modal-switcherItem'); |
| setItem.addEventListener('click', ()=>{ |
| list.innerHTML = ''; |
| for (const qrs of quickReplyApi.listSets()) { |
| const item = document.createElement('li'); { |
| item.classList.add('qr--modal-switcherItem'); |
| item.addEventListener('click', ()=>{ |
| list.innerHTML = ''; |
| makeList(quickReplyApi.getSetByName(qrs)); |
| }); |
| const lbl = document.createElement('div'); { |
| lbl.classList.add('qr--label'); |
| lbl.textContent = qrs; |
| item.append(lbl); |
| } |
| list.append(item); |
| } |
| } |
| }); |
| const lbl = document.createElement('div'); { |
| lbl.classList.add('qr--label'); |
| const icon = document.createElement('i'); { |
| icon.classList.add('fa-solid'); |
| icon.classList.add('fa-arrow-alt-circle-right'); |
| icon.classList.add('menu_button'); |
| lbl.append(icon); |
| } |
| const text = document.createElement('span'); { |
| text.textContent = 'Switch QR Sets...'; |
| lbl.append(text); |
| } |
| setItem.append(lbl); |
| } |
| list.append(setItem); |
| } |
| const addItem = document.createElement('li'); { |
| addItem.classList.add('qr--modal-switcherItem'); |
| addItem.addEventListener('click', ()=>{ |
| const qr = quickReplyApi.getSetByQr(this).addQuickReply(); |
| this.editorPopup.completeAffirmative(); |
| qr.showEditor(); |
| }); |
| const lbl = document.createElement('div'); { |
| lbl.classList.add('qr--label'); |
| const icon = document.createElement('i'); { |
| icon.classList.add('fa-solid'); |
| icon.classList.add('fa-plus'); |
| icon.classList.add('menu_button'); |
| lbl.append(icon); |
| } |
| const text = document.createElement('span'); { |
| text.textContent = 'Add QR'; |
| lbl.append(text); |
| } |
| addItem.append(lbl); |
| } |
| list.append(addItem); |
| } |
| for (const qr of qrs.qrList.toSorted((a,b)=>a.label.toLowerCase().localeCompare(b.label.toLowerCase()))) { |
| const item = document.createElement('li'); { |
| item.classList.add('qr--modal-switcherItem'); |
| if (qr == this) item.classList.add('qr--current'); |
| else item.addEventListener('click', ()=>{ |
| this.editorPopup.completeAffirmative(); |
| qr.showEditor(); |
| }); |
| const lbl = document.createElement('div'); { |
| lbl.classList.add('qr--label'); |
| lbl.textContent = qr.label; |
| item.append(lbl); |
| } |
| const id = document.createElement('div'); { |
| id.classList.add('qr--id'); |
| id.textContent = qr.id.toString(); |
| item.append(id); |
| } |
| const mes = document.createElement('div'); { |
| mes.classList.add('qr--message'); |
| mes.textContent = qr.message; |
| item.append(mes); |
| } |
| list.append(item); |
| } |
| } |
| }; |
| makeList(quickReplyApi.getSetByQr(this)); |
| } |
| label.parentElement.append(list); |
| }); |
| |
| const title = dom.querySelector('#qr--modal-title'); |
| title.value = this.title; |
| title.addEventListener('input', () => { |
| this.updateTitle(title.value); |
| }); |
| |
| const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner'); |
| this.editorSyntax = messageSyntaxInner; |
| |
| const wrap = dom.querySelector('#qr--modal-wrap'); |
| wrap.checked = JSON.parse(accountStorage.getItem('qr--wrap') ?? 'false'); |
| wrap.addEventListener('click', () => { |
| accountStorage.setItem('qr--wrap', JSON.stringify(wrap.checked)); |
| updateWrap(); |
| }); |
| const updateWrap = () => { |
| if (wrap.checked) { |
| message.style.whiteSpace = 'pre-wrap'; |
| messageSyntaxInner.style.whiteSpace = 'pre-wrap'; |
| if (this.clone) { |
| this.clone.style.whiteSpace = 'pre-wrap'; |
| } |
| } else { |
| message.style.whiteSpace = 'pre'; |
| messageSyntaxInner.style.whiteSpace = 'pre'; |
| if (this.clone) { |
| this.clone.style.whiteSpace = 'pre'; |
| } |
| } |
| updateScrollDebounced(); |
| }; |
| const updateScroll = (evt) => { |
| let left = message.scrollLeft; |
| let top = message.scrollTop; |
| if (evt) { |
| evt.preventDefault(); |
| left = message.scrollLeft + evt.deltaX; |
| top = message.scrollTop + evt.deltaY; |
| message.scrollTo({ |
| behavior: 'instant', |
| left, |
| top, |
| }); |
| } |
| messageSyntaxInner.scrollTo({ |
| behavior: 'instant', |
| left, |
| top, |
| }); |
| }; |
| const updateScrollDebounced = updateScroll; |
| const updateSyntaxEnabled = ()=>{ |
| if (syntax.checked) { |
| dom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax'); |
| } else { |
| dom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax'); |
| } |
| }; |
| |
| const tabSize = dom.querySelector('#qr--modal-tabSize'); |
| tabSize.value = JSON.parse(accountStorage.getItem('qr--tabSize') ?? '4'); |
| const updateTabSize = () => { |
| message.style.tabSize = tabSize.value; |
| messageSyntaxInner.style.tabSize = tabSize.value; |
| updateScrollDebounced(); |
| }; |
| tabSize.addEventListener('change', () => { |
| accountStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value))); |
| updateTabSize(); |
| }); |
| |
| const executeShortcut = dom.querySelector('#qr--modal-executeShortcut'); |
| executeShortcut.checked = JSON.parse(accountStorage.getItem('qr--executeShortcut') ?? 'true'); |
| executeShortcut.addEventListener('click', () => { |
| accountStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked)); |
| }); |
| |
| const syntax = dom.querySelector('#qr--modal-syntax'); |
| syntax.checked = JSON.parse(accountStorage.getItem('qr--syntax') ?? 'true'); |
| syntax.addEventListener('click', () => { |
| accountStorage.setItem('qr--syntax', JSON.stringify(syntax.checked)); |
| updateSyntaxEnabled(); |
| }); |
| |
| if (navigator.keyboard) { |
| |
| navigator.keyboard.getLayoutMap().then(it=>dom.querySelector('#qr--modal-commentKey').textContent = it.get('Backslash')); |
| } else { |
| dom.querySelector('#qr--modal-commentKey').closest('small').remove(); |
| } |
| this.editorMessageLabel = dom.querySelector('label[for="qr--modal-message"]'); |
| |
| const message = dom.querySelector('#qr--modal-message'); |
| this.editorMessage = message; |
| message.value = this.message; |
| const updateMessageDebounced = debounce((value)=>this.updateMessage(value), 10); |
| message.addEventListener('input', () => { |
| updateMessageDebounced(message.value); |
| updateScrollDebounced(); |
| }, { passive:true }); |
| const getLineStart = ()=>{ |
| const start = message.selectionStart; |
| let lineStart; |
| if (start == 0 || message.value[start - 1] == '\n') { |
| |
| |
| lineStart = start; |
| } else { |
| |
| |
| lineStart = message.value.lastIndexOf('\n', start - 1) + 1; |
| } |
| return lineStart; |
| }; |
| message.addEventListener('keydown', async(evt) => { |
| if (this.isExecuting) return; |
| if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) { |
| |
| evt.preventDefault(); |
| const start = message.selectionStart; |
| const end = message.selectionEnd; |
| if (end - start > 0 && message.value.substring(start, end).includes('\n')) { |
| evt.stopImmediatePropagation(); |
| evt.stopPropagation(); |
| const lineStart = getLineStart(); |
| message.selectionStart = lineStart; |
| const affectedLines = message.value.substring(lineStart, end).split('\n'); |
| |
| document.execCommand('insertText', false, `\t${affectedLines.join('\n\t')}`); |
| message.selectionStart = start + 1; |
| message.selectionEnd = end + affectedLines.length; |
| message.dispatchEvent(new Event('input', { bubbles:true })); |
| } else if (!(ac.isReplaceable && ac.isActive)) { |
| evt.stopImmediatePropagation(); |
| evt.stopPropagation(); |
| |
| document.execCommand('insertText', false, '\t'); |
| message.dispatchEvent(new Event('input', { bubbles:true })); |
| } |
| } else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) { |
| |
| evt.preventDefault(); |
| evt.stopImmediatePropagation(); |
| evt.stopPropagation(); |
| const start = message.selectionStart; |
| const end = message.selectionEnd; |
| const lineStart = getLineStart(); |
| message.selectionStart = lineStart; |
| const affectedLines = message.value.substring(lineStart, end).split('\n'); |
| const newText = affectedLines.map(it=>it.replace(/^\t/, '')).join('\n'); |
| const delta = affectedLines.join('\n').length - newText.length; |
| |
| if (delta > 0) { |
| if (newText == '') { |
| document.execCommand('delete', false); |
| } else { |
| document.execCommand('insertText', false, newText); |
| } |
| message.selectionStart = start - (affectedLines[0].startsWith('\t') ? 1 : 0); |
| message.selectionEnd = end - delta; |
| message.dispatchEvent(new Event('input', { bubbles:true })); |
| } else { |
| message.selectionStart = start; |
| } |
| } else if (evt.key == 'Enter' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey && !(ac.isReplaceable && ac.isActive)) { |
| |
| const start = message.selectionStart; |
| let lineStart = getLineStart(); |
| const indent = /^([^\S\n]*)/.exec(message.value.slice(lineStart))[1] ?? ''; |
| if (indent.length) { |
| evt.stopImmediatePropagation(); |
| evt.stopPropagation(); |
| evt.preventDefault(); |
| |
| document.execCommand('insertText', false, `\n${indent}`); |
| message.selectionStart = start + 1 + indent.length; |
| message.selectionEnd = message.selectionStart; |
| message.dispatchEvent(new Event('input', { bubbles:true })); |
| } |
| } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) { |
| if (executeShortcut.checked) { |
| |
| evt.stopImmediatePropagation(); |
| evt.stopPropagation(); |
| evt.preventDefault(); |
| const selectionStart = message.selectionStart; |
| const selectionEnd = message.selectionEnd; |
| message.blur(); |
| await this.executeFromEditor(); |
| if (document.activeElement != message) { |
| message.focus(); |
| message.selectionStart = selectionStart; |
| message.selectionEnd = selectionEnd; |
| } |
| } |
| } else if (evt.key == 'F9' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey) { |
| |
| evt.stopImmediatePropagation(); |
| evt.stopPropagation(); |
| evt.preventDefault(); |
| preBreakPointStart = message.selectionStart; |
| preBreakPointEnd = message.selectionEnd; |
| toggleBreakpoint(); |
| } else if (evt.code == 'Backslash' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) { |
| |
| |
| evt.stopImmediatePropagation(); |
| evt.stopPropagation(); |
| evt.preventDefault(); |
| |
| const parser = new SlashCommandParser(); |
| parser.parse(message.value, false); |
| const start = message.selectionStart; |
| const end = message.selectionEnd; |
| const comment = parser.commandIndex.findLast(it=>it.name == '*' && (it.start <= start && it.end >= start || it.start <= end && it.end >= end)); |
| if (comment) { |
| |
| let content = message.value.slice(comment.start + 1, comment.end - 1); |
| let len = content.length; |
| content = content.replace(/^ /, ''); |
| const offsetStart = len - content.length; |
| len = content.length; |
| content = content.replace(/ $/, ''); |
| const offsetEnd = len - content.length; |
| message.selectionStart = comment.start - 1; |
| message.selectionEnd = comment.end + 1; |
| |
| document.execCommand('insertText', false, content); |
| message.selectionStart = start - (start >= comment.start ? 2 + offsetStart : 0); |
| message.selectionEnd = end - 2 - offsetStart - (end >= comment.end ? 2 + offsetEnd : 0); |
| } else { |
| |
| const lineStart = getLineStart(); |
| const lineEnd = message.value.indexOf('\n', end); |
| message.selectionStart = lineStart; |
| message.selectionEnd = lineEnd; |
| |
| document.execCommand('insertText', false, `/* ${message.value.slice(lineStart, lineEnd)} *|`); |
| message.selectionStart = start + 3; |
| message.selectionEnd = end + 3; |
| } |
| message.dispatchEvent(new Event('input', { bubbles:true })); |
| } |
| }); |
| const ac = await setSlashCommandAutoComplete(message, true); |
| message.addEventListener('wheel', (evt)=>{ |
| updateScrollDebounced(evt); |
| }); |
| |
| message.addEventListener('scroll', (evt)=>{ |
| updateScrollDebounced(); |
| }); |
| let preBreakPointStart; |
| let preBreakPointEnd; |
| |
| |
| |
| const removeBreakpoint = (bp)=>{ |
| |
| let start = bp.start - 1; |
| |
| while (message.value[start] != '/') start--; |
| |
| while (/[^\S\n]/.test(message.value[start - 1])) start--; |
| |
| if (message.value[start - 1] == '\n') start--; |
| let end = bp.end; |
| |
| while (/\s/.test(message.value[end])) end++; |
| |
| if (message.value[end] == '|') end++; |
| message.selectionStart = start; |
| message.selectionEnd = end; |
| |
| document.execCommand('insertText', false, ''); |
| message.dispatchEvent(new Event('input', { bubbles:true })); |
| let postStart = preBreakPointStart; |
| let postEnd = preBreakPointEnd; |
| |
| if (preBreakPointStart <= start) { |
| |
| } else if (preBreakPointStart > start && preBreakPointEnd < end) { |
| |
| postStart = start; |
| } else if (preBreakPointStart >= end) { |
| |
| postStart = preBreakPointStart - (end - start); |
| } |
| if (preBreakPointEnd <= start) { |
| |
| } else if (preBreakPointEnd > start && preBreakPointEnd < end) { |
| |
| postEnd = start; |
| } else if (preBreakPointEnd >= end) { |
| |
| postEnd = preBreakPointEnd - (end - start); |
| } |
| return { start:postStart, end:postEnd }; |
| }; |
| |
| |
| |
| const addBreakpoint = (cmd)=>{ |
| |
| let start = cmd.start - 1; |
| let indent = ''; |
| |
| while (message.value[start] != '/') start--; |
| |
| while (/[^\S\n]/.test(message.value[start - 1])) { |
| start--; |
| indent += message.value[start]; |
| } |
| |
| if (message.value[start - 1] == '\n') { |
| start--; |
| indent = `\n${indent}`; |
| } |
| const breakpointText = `${indent}/breakpoint |`; |
| message.selectionStart = start; |
| message.selectionEnd = start; |
| |
| document.execCommand('insertText', false, breakpointText); |
| message.dispatchEvent(new Event('input', { bubbles:true })); |
| return breakpointText.length; |
| }; |
| const toggleBreakpoint = ()=>{ |
| const idx = message.selectionStart; |
| let postStart = preBreakPointStart; |
| let postEnd = preBreakPointEnd; |
| const parser = new SlashCommandParser(); |
| parser.parse(message.value, false); |
| const cmdIdx = parser.commandIndex.findLastIndex(it=>it.start <= idx); |
| if (cmdIdx > -1) { |
| const cmd = parser.commandIndex[cmdIdx]; |
| if (cmd instanceof SlashCommandBreakPoint) { |
| const bp = cmd; |
| const { start, end } = removeBreakpoint(bp); |
| postStart = start; |
| postEnd = end; |
| } else if (parser.commandIndex[cmdIdx - 1] instanceof SlashCommandBreakPoint) { |
| const bp = parser.commandIndex[cmdIdx - 1]; |
| const { start, end } = removeBreakpoint(bp); |
| postStart = start; |
| postEnd = end; |
| } else { |
| const len = addBreakpoint(cmd); |
| postStart += len; |
| postEnd += len; |
| } |
| message.selectionStart = postStart; |
| message.selectionEnd = postEnd; |
| } |
| }; |
| message.addEventListener('pointerdown', (evt)=>{ |
| if (!evt.ctrlKey || !evt.altKey) return; |
| preBreakPointStart = message.selectionStart; |
| preBreakPointEnd = message.selectionEnd; |
| }); |
| message.addEventListener('pointerup', async(evt)=>{ |
| if (!evt.ctrlKey || !evt.altKey || message.selectionStart != message.selectionEnd) return; |
| toggleBreakpoint(); |
| }); |
| |
| const resizeListener = debounce((evt) => { |
| updateScrollDebounced(evt); |
| if (document.activeElement == message) { |
| message.blur(); |
| message.focus(); |
| } |
| }); |
| window.addEventListener('resize', resizeListener); |
| updateSyntaxEnabled(); |
| const updateSyntax = ()=>{ |
| if (messageSyntaxInner && syntax.checked) { |
| morphdom( |
| messageSyntaxInner, |
| `<div>${hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value}</div>`, |
| { childrenOnly: true }, |
| ); |
| updateScrollDebounced(); |
| } |
| }; |
| let lastSyntaxUpdate = 0; |
| const fpsTime = 1000 / 30; |
| let lastMessageValue = null; |
| let wasSyntax = null; |
| const updateSyntaxLoop = ()=>{ |
| const now = Date.now(); |
| |
| if (now - lastSyntaxUpdate < fpsTime) return requestAnimationFrame(updateSyntaxLoop); |
| |
| if (!messageSyntaxInner || !message) return requestAnimationFrame(updateSyntaxLoop); |
| |
| if (!messageSyntaxInner.closest('body')) return; |
| |
| if (this.isExecuting) { |
| lastMessageValue = null; |
| return requestAnimationFrame(updateSyntaxLoop); |
| } |
| |
| if (wasSyntax == syntax.checked && lastMessageValue == message.value) return requestAnimationFrame(updateSyntaxLoop); |
| wasSyntax = syntax.checked; |
| lastSyntaxUpdate = now; |
| lastMessageValue = message.value; |
| updateSyntax(); |
| requestAnimationFrame(updateSyntaxLoop); |
| }; |
| requestAnimationFrame(()=>updateSyntaxLoop()); |
| message.style.setProperty('text-shadow', 'none', 'important'); |
| updateWrap(); |
| updateTabSize(); |
|
|
| |
| |
| const tpl = dom.querySelector('#qr--ctxItem'); |
| const linkList = dom.querySelector('#qr--ctxEditor'); |
| const fillQrSetSelect = (select, link) => { |
| [{ name: 'Select a QR set' }, ...QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase()))].forEach(qrs => { |
| const opt = document.createElement('option'); { |
| opt.value = qrs.name; |
| opt.textContent = qrs.name; |
| opt.selected = qrs.name == link.set?.name; |
| select.append(opt); |
| } |
| }); |
| }; |
| const addCtxItem = (link, idx) => { |
| |
| |
| const itemDom = tpl.content.querySelector('.qr--ctxItem').cloneNode(true); { |
| itemDom.setAttribute('data-order', String(idx)); |
|
|
| |
| const select = itemDom.querySelector('.qr--set'); |
| fillQrSetSelect(select, link); |
| select.addEventListener('change', () => { |
| link.set = QuickReplySet.get(select.value); |
| this.updateContext(); |
| }); |
|
|
| |
| const chain = itemDom.querySelector('.qr--isChained'); |
| chain.checked = link.isChained; |
| chain.addEventListener('click', () => { |
| link.isChained = chain.checked; |
| this.updateContext(); |
| }); |
|
|
| itemDom.querySelector('.qr--delete').addEventListener('click', () => { |
| itemDom.remove(); |
| this.contextList.splice(this.contextList.indexOf(link), 1); |
| this.updateContext(); |
| }); |
|
|
| linkList.append(itemDom); |
| } |
| }; |
| [...this.contextList].forEach((link, idx) => addCtxItem(link, idx)); |
| dom.querySelector('#qr--ctxAdd').addEventListener('click', () => { |
| const link = new QuickReplyContextLink(); |
| this.contextList.push(link); |
| addCtxItem(link, this.contextList.length - 1); |
| }); |
| const onContextSort = () => { |
| this.contextList = Array.from(linkList.querySelectorAll('.qr--ctxItem')).map((it,idx) => { |
| const link = this.contextList[Number(it.getAttribute('data-order'))]; |
| it.setAttribute('data-order', String(idx)); |
| return link; |
| }); |
| this.updateContext(); |
| }; |
| |
| $(linkList).sortable({ |
| delay: getSortableDelay(), |
| stop: () => onContextSort(), |
| }); |
|
|
| |
| |
| const preventAutoExecute = dom.querySelector('#qr--preventAutoExecute'); |
| preventAutoExecute.checked = this.preventAutoExecute; |
| preventAutoExecute.addEventListener('click', ()=>{ |
| this.preventAutoExecute = preventAutoExecute.checked; |
| this.updateContext(); |
| }); |
| |
| const isHidden = dom.querySelector('#qr--isHidden'); |
| isHidden.checked = this.isHidden; |
| isHidden.addEventListener('click', ()=>{ |
| this.isHidden = isHidden.checked; |
| this.updateContext(); |
| }); |
| |
| const executeOnStartup = dom.querySelector('#qr--executeOnStartup'); |
| executeOnStartup.checked = this.executeOnStartup; |
| executeOnStartup.addEventListener('click', ()=>{ |
| this.executeOnStartup = executeOnStartup.checked; |
| this.updateContext(); |
| }); |
| |
| const executeOnUser = dom.querySelector('#qr--executeOnUser'); |
| executeOnUser.checked = this.executeOnUser; |
| executeOnUser.addEventListener('click', ()=>{ |
| this.executeOnUser = executeOnUser.checked; |
| this.updateContext(); |
| }); |
| |
| const executeOnAi = dom.querySelector('#qr--executeOnAi'); |
| executeOnAi.checked = this.executeOnAi; |
| executeOnAi.addEventListener('click', ()=>{ |
| this.executeOnAi = executeOnAi.checked; |
| this.updateContext(); |
| }); |
| |
| const executeOnChatChange = dom.querySelector('#qr--executeOnChatChange'); |
| executeOnChatChange.checked = this.executeOnChatChange; |
| executeOnChatChange.addEventListener('click', ()=>{ |
| this.executeOnChatChange = executeOnChatChange.checked; |
| this.updateContext(); |
| }); |
| |
| const executeOnGroupMemberDraft = dom.querySelector('#qr--executeOnGroupMemberDraft'); |
| executeOnGroupMemberDraft.checked = this.executeOnGroupMemberDraft; |
| executeOnGroupMemberDraft.addEventListener('click', ()=>{ |
| this.executeOnGroupMemberDraft = executeOnGroupMemberDraft.checked; |
| this.updateContext(); |
| }); |
| |
| const executeBeforeGeneration = dom.querySelector('#qr--executeBeforeGeneration'); |
| executeBeforeGeneration.checked = this.executeBeforeGeneration; |
| executeBeforeGeneration.addEventListener('click', ()=>{ |
| this.executeBeforeGeneration = executeBeforeGeneration.checked; |
| this.updateContext(); |
| }); |
| |
| const executeOnNewChat = dom.querySelector('#qr--executeOnNewChat'); |
| executeOnNewChat.checked = this.executeOnNewChat; |
| executeOnNewChat.addEventListener('click', ()=>{ |
| this.executeOnNewChat = executeOnNewChat.checked; |
| this.updateContext(); |
| }); |
| |
| const automationId = dom.querySelector('#qr--automationId'); |
| automationId.value = this.automationId; |
| automationId.addEventListener('input', () => { |
| this.automationId = automationId.value; |
| this.updateContext(); |
| }); |
|
|
| |
| const executeProgress = dom.querySelector('#qr--modal-executeProgress'); |
| this.editorExecuteProgress = executeProgress; |
| |
| const executeErrors = dom.querySelector('#qr--modal-executeErrors'); |
| this.editorExecuteErrors = executeErrors; |
| |
| const executeResult = dom.querySelector('#qr--modal-executeResult'); |
| this.editorExecuteResult = executeResult; |
| |
| const debugState = dom.querySelector('#qr--modal-debugState'); |
| this.editorDebugState = debugState; |
| |
| const executeBtn = dom.querySelector('#qr--modal-execute'); |
| this.editorExecuteBtn = executeBtn; |
| executeBtn.addEventListener('click', async()=>{ |
| await this.executeFromEditor(); |
| }); |
| |
| const executeBtnPause = dom.querySelector('#qr--modal-pause'); |
| this.editorExecuteBtnPause = executeBtnPause; |
| executeBtnPause.addEventListener('click', async()=>{ |
| if (this.abortController) { |
| if (this.abortController.signal.paused) { |
| this.abortController.continue('Continue button clicked'); |
| this.editorExecuteProgress.classList.remove('qr--paused'); |
| } else { |
| this.abortController.pause('Pause button clicked'); |
| this.editorExecuteProgress.classList.add('qr--paused'); |
| } |
| } |
| }); |
| |
| const executeBtnStop = dom.querySelector('#qr--modal-stop'); |
| this.editorExecuteBtnStop = executeBtnStop; |
| executeBtnStop.addEventListener('click', async()=>{ |
| this.abortController?.abort('Stop button clicked'); |
| }); |
|
|
| |
| const inputOg = document.querySelector('#send_textarea'); |
| const inputMirror = dom.querySelector('#qr--modal-send_textarea'); |
| |
| inputMirror.value = inputOg.value; |
| const inputOgMo = new MutationObserver(muts=>{ |
| if (muts.find(it=>[...it.removedNodes].includes(inputMirror) || [...it.removedNodes].find(n=>n.contains(inputMirror)))) { |
| inputOg.removeEventListener('input', inputOgListener); |
| } |
| }); |
| inputOgMo.observe(document.body, { childList:true }); |
| const inputOgListener = ()=>{ |
| |
| inputMirror.value = inputOg.value; |
| }; |
| inputOg.addEventListener('input', inputOgListener); |
| inputMirror.addEventListener('input', ()=>{ |
| |
| inputOg.value = inputMirror.value; |
| }); |
|
|
| |
| const resumeBtn = dom.querySelector('#qr--modal-resume'); |
| resumeBtn.addEventListener('click', ()=>{ |
| this.debugController?.resume(); |
| }); |
| |
| const stepBtn = dom.querySelector('#qr--modal-step'); |
| stepBtn.addEventListener('click', ()=>{ |
| this.debugController?.step(); |
| }); |
| |
| const stepIntoBtn = dom.querySelector('#qr--modal-stepInto'); |
| stepIntoBtn.addEventListener('click', ()=>{ |
| this.debugController?.stepInto(); |
| }); |
| |
| const stepOutBtn = dom.querySelector('#qr--modal-stepOut'); |
| stepOutBtn.addEventListener('click', ()=>{ |
| this.debugController?.stepOut(); |
| }); |
| |
| const minimizeBtn = dom.querySelector('#qr--modal-minimize'); |
| minimizeBtn.addEventListener('click', ()=>{ |
| this.editorDom.classList.add('qr--minimized'); |
| }); |
| const maximizeBtn = dom.querySelector('#qr--modal-maximize'); |
| maximizeBtn.addEventListener('click', ()=>{ |
| this.editorDom.classList.remove('qr--minimized'); |
| }); |
| |
| let isResizing = false; |
| let resizeStart; |
| let wStart; |
| |
| const resizeHandle = dom.querySelector('#qr--resizeHandle'); |
| resizeHandle.addEventListener('pointerdown', (evt)=>{ |
| if (isResizing) return; |
| isResizing = true; |
| evt.preventDefault(); |
| resizeStart = evt.x; |
| |
| wStart = dom.querySelector('#qr--qrOptions').offsetWidth; |
| const dragListener = debounce((evt)=>{ |
| const w = wStart + resizeStart - evt.x; |
| |
| dom.querySelector('#qr--qrOptions').style.setProperty('--width', `${w}px`); |
| }, 5); |
| window.addEventListener('pointerup', ()=>{ |
| |
| window.removeEventListener('pointermove', dragListener); |
| isResizing = false; |
| }, { once:true }); |
| |
| window.addEventListener('pointermove', dragListener); |
| }); |
|
|
| await popupResult; |
|
|
| window.removeEventListener('resize', resizeListener); |
| } else { |
| warn('failed to fetch qrEditor template'); |
| } |
| } |
|
|
| getEditorPosition(start, end, message = null) { |
| const inputRect = this.editorMessage.getBoundingClientRect(); |
| const style = window.getComputedStyle(this.editorMessage); |
| if (!this.clone) { |
| this.clone = document.createElement('div'); |
| for (const key of style) { |
| this.clone.style[key] = style[key]; |
| } |
| this.clone.style.position = 'fixed'; |
| this.clone.style.visibility = 'hidden'; |
| const mo = new MutationObserver(muts=>{ |
| if (muts.find(it=>[...it.removedNodes].includes(this.editorMessage) || [...it.removedNodes].find(n=>n.contains(this.editorMessage)))) { |
| this.clone?.remove(); |
| this.clone = null; |
| } |
| }); |
| mo.observe(document.body, { childList:true }); |
| } |
| document.body.append(this.clone); |
| this.clone.style.width = `${inputRect.width}px`; |
| this.clone.style.height = `${inputRect.height}px`; |
| this.clone.style.left = `${inputRect.left}px`; |
| this.clone.style.top = `${inputRect.top}px`; |
| this.clone.style.whiteSpace = style.whiteSpace; |
| this.clone.style.tabSize = style.tabSize; |
| const text = message ?? this.editorMessage.value; |
| const before = text.slice(0, start); |
| this.clone.textContent = before; |
| const locator = document.createElement('span'); |
| locator.textContent = text.slice(start, end); |
| this.clone.append(locator); |
| this.clone.append(text.slice(end)); |
| this.clone.scrollTop = this.editorSyntax.scrollTop; |
| this.clone.scrollLeft = this.editorSyntax.scrollLeft; |
| const locatorRect = locator.getBoundingClientRect(); |
| const bodyRect = document.body.getBoundingClientRect(); |
| const location = { |
| left: locatorRect.left - bodyRect.left, |
| right: locatorRect.right - bodyRect.left, |
| top: locatorRect.top - bodyRect.top, |
| bottom: locatorRect.bottom - bodyRect.top, |
| }; |
| |
| return location; |
| } |
| async executeFromEditor() { |
| if (this.isExecuting) return; |
| this.editorPopup.onClosing = ()=>false; |
| const uuidCheck = /^[0-9a-z]{8}(-[0-9a-z]{4}){3}-[0-9a-z]{12}$/; |
| const oText = this.message; |
| this.isExecuting = true; |
| this.editorDom.classList.add('qr--isExecuting'); |
| const noSyntax = this.editorDom.querySelector('#qr--modal-messageHolder').classList.contains('qr--noSyntax'); |
| if (noSyntax) { |
| this.editorDom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax'); |
| } |
| this.editorExecuteBtn.classList.add('qr--busy'); |
| this.editorExecuteProgress.style.setProperty('--prog', '0'); |
| this.editorExecuteErrors.classList.remove('qr--hasErrors'); |
| this.editorExecuteResult.classList.remove('qr--hasResult'); |
| this.editorExecuteProgress.classList.remove('qr--error'); |
| this.editorExecuteProgress.classList.remove('qr--success'); |
| this.editorExecuteProgress.classList.remove('qr--paused'); |
| this.editorExecuteProgress.classList.remove('qr--aborted'); |
| this.editorExecuteErrors.innerHTML = ''; |
| this.editorExecuteResult.innerHTML = ''; |
| const syntax = this.editorDom.querySelector('#qr--modal-messageSyntaxInner'); |
| const updateScroll = (evt) => { |
| let left = syntax.scrollLeft; |
| let top = syntax.scrollTop; |
| if (evt) { |
| evt.preventDefault(); |
| left = syntax.scrollLeft + evt.deltaX; |
| top = syntax.scrollTop + evt.deltaY; |
| syntax.scrollTo({ |
| behavior: 'instant', |
| left, |
| top, |
| }); |
| } |
| this.editorMessage.scrollTo({ |
| behavior: 'instant', |
| left, |
| top, |
| }); |
| }; |
| const updateScrollDebounced = updateScroll; |
| syntax.addEventListener('wheel', (evt)=>{ |
| updateScrollDebounced(evt); |
| }); |
| |
| syntax.addEventListener('scroll', (evt)=>{ |
| updateScrollDebounced(); |
| }); |
| try { |
| this.abortController = new SlashCommandAbortController(); |
| this.debugController = new SlashCommandDebugController(); |
| this.debugController.onBreakPoint = async(closure, executor)=>{ |
| this.editorDom.classList.add('qr--isPaused'); |
| syntax.innerHTML = hljs.highlight(`${closure.fullText}${closure.fullText.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value; |
| this.editorMessageLabel.innerHTML = ''; |
| if (uuidCheck.test(closure.source)) { |
| const p0 = document.createElement('span'); { |
| p0.textContent = 'anonymous: '; |
| this.editorMessageLabel.append(p0); |
| } |
| const p1 = document.createElement('strong'); { |
| p1.textContent = executor.source.slice(0,5); |
| this.editorMessageLabel.append(p1); |
| } |
| const p2 = document.createElement('span'); { |
| p2.textContent = executor.source.slice(5, -5); |
| this.editorMessageLabel.append(p2); |
| } |
| const p3 = document.createElement('strong'); { |
| p3.textContent = executor.source.slice(-5); |
| this.editorMessageLabel.append(p3); |
| } |
| } else { |
| this.editorMessageLabel.textContent = executor.source; |
| } |
| const source = closure.source; |
| this.editorDebugState.innerHTML = ''; |
| let ci = -1; |
| const varNames = []; |
| const macroNames = []; |
| |
| |
| |
| const buildVars = (scope, isCurrent = false)=>{ |
| if (!isCurrent) { |
| ci--; |
| } |
| const c = this.debugController.stack.slice(ci)[0]; |
| const wrap = document.createElement('div'); { |
| wrap.classList.add('qr--scope'); |
| if (isCurrent) { |
| const executor = this.debugController.cmdStack.slice(-1)[0]; |
| { |
| const namedTitle = document.createElement('div'); { |
| namedTitle.classList.add('qr--title'); |
| namedTitle.textContent = `Named Args - /${executor.name}`; |
| if (executor.command.name == 'run') { |
| namedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`; |
| } |
| wrap.append(namedTitle); |
| } |
| const keys = new Set([...Object.keys(this.debugController.namedArguments ?? {}), ...(executor.namedArgumentList ?? []).map(it=>it.name)]); |
| for (const key of keys) { |
| if (key[0] == '_') continue; |
| const item = document.createElement('div'); { |
| item.classList.add('qr--var'); |
| const k = document.createElement('div'); { |
| k.classList.add('qr--key'); |
| k.textContent = key; |
| item.append(k); |
| } |
| const vUnresolved = document.createElement('div'); { |
| vUnresolved.classList.add('qr--val'); |
| vUnresolved.classList.add('qr--singleCol'); |
| const val = executor.namedArgumentList.find(it=>it.name == key)?.value; |
| if (val instanceof SlashCommandClosure) { |
| vUnresolved.classList.add('qr--closure'); |
| vUnresolved.title = val.rawText; |
| vUnresolved.textContent = val.toString(); |
| } else if (val === undefined) { |
| vUnresolved.classList.add('qr--undefined'); |
| vUnresolved.textContent = 'undefined'; |
| } else { |
| let jsonVal; |
| try { jsonVal = JSON.parse(val); } catch { } |
| if (jsonVal && typeof jsonVal == 'object') { |
| vUnresolved.textContent = JSON.stringify(jsonVal, null, 2); |
| } else { |
| vUnresolved.textContent = val; |
| vUnresolved.classList.add('qr--simple'); |
| } |
| } |
| item.append(vUnresolved); |
| } |
| const vResolved = document.createElement('div'); { |
| vResolved.classList.add('qr--val'); |
| vResolved.classList.add('qr--singleCol'); |
| if (this.debugController.namedArguments === undefined) { |
| vResolved.classList.add('qr--unresolved'); |
| } else { |
| const val = this.debugController.namedArguments?.[key]; |
| if (val instanceof SlashCommandClosure) { |
| vResolved.classList.add('qr--closure'); |
| vResolved.title = val.rawText; |
| vResolved.textContent = val.toString(); |
| } else if (val === undefined) { |
| vResolved.classList.add('qr--undefined'); |
| vResolved.textContent = 'undefined'; |
| } else { |
| let jsonVal; |
| try { jsonVal = JSON.parse(val); } catch { } |
| if (jsonVal && typeof jsonVal == 'object') { |
| vResolved.textContent = JSON.stringify(jsonVal, null, 2); |
| } else { |
| vResolved.textContent = val; |
| vResolved.classList.add('qr--simple'); |
| } |
| } |
| } |
| item.append(vResolved); |
| } |
| wrap.append(item); |
| } |
| } |
| } |
| { |
| const unnamedTitle = document.createElement('div'); { |
| unnamedTitle.classList.add('qr--title'); |
| unnamedTitle.textContent = `Unnamed Args - /${executor.name}`; |
| if (executor.command.name == 'run') { |
| unnamedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`; |
| } |
| wrap.append(unnamedTitle); |
| } |
| let i = 0; |
| let unnamed = this.debugController.unnamedArguments ?? []; |
| if (!Array.isArray(unnamed)) unnamed = [unnamed]; |
| |
| while (unnamed.length < executor.unnamedArgumentList?.length ?? 0) unnamed.push(undefined); |
| |
| unnamed = unnamed.map((it,idx)=>[executor.unnamedArgumentList?.[idx], it]); |
| |
| for (const arg of unnamed) { |
| i++; |
| const item = document.createElement('div'); { |
| item.classList.add('qr--var'); |
| const k = document.createElement('div'); { |
| k.classList.add('qr--key'); |
| k.textContent = i.toString(); |
| item.append(k); |
| } |
| const vUnresolved = document.createElement('div'); { |
| vUnresolved.classList.add('qr--val'); |
| vUnresolved.classList.add('qr--singleCol'); |
| const val = arg[0]?.value; |
| if (val instanceof SlashCommandClosure) { |
| vUnresolved.classList.add('qr--closure'); |
| vUnresolved.title = val.rawText; |
| vUnresolved.textContent = val.toString(); |
| } else if (val === undefined) { |
| vUnresolved.classList.add('qr--undefined'); |
| vUnresolved.textContent = 'undefined'; |
| } else { |
| let jsonVal; |
| try { jsonVal = JSON.parse(val); } catch { } |
| if (jsonVal && typeof jsonVal == 'object') { |
| vUnresolved.textContent = JSON.stringify(jsonVal, null, 2); |
| } else { |
| vUnresolved.textContent = val; |
| vUnresolved.classList.add('qr--simple'); |
| } |
| } |
| item.append(vUnresolved); |
| } |
| const vResolved = document.createElement('div'); { |
| vResolved.classList.add('qr--val'); |
| vResolved.classList.add('qr--singleCol'); |
| if (this.debugController.unnamedArguments === undefined) { |
| vResolved.classList.add('qr--unresolved'); |
| } else if ((Array.isArray(this.debugController.unnamedArguments) ? this.debugController.unnamedArguments : [this.debugController.unnamedArguments]).length < i) { |
| |
| } else { |
| const val = arg[1]; |
| if (val instanceof SlashCommandClosure) { |
| vResolved.classList.add('qr--closure'); |
| vResolved.title = val.rawText; |
| vResolved.textContent = val.toString(); |
| } else if (val === undefined) { |
| vResolved.classList.add('qr--undefined'); |
| vResolved.textContent = 'undefined'; |
| } else { |
| let jsonVal; |
| try { jsonVal = JSON.parse(val); } catch { } |
| if (jsonVal && typeof jsonVal == 'object') { |
| vResolved.textContent = JSON.stringify(jsonVal, null, 2); |
| } else { |
| vResolved.textContent = val; |
| vResolved.classList.add('qr--simple'); |
| } |
| } |
| } |
| item.append(vResolved); |
| } |
| wrap.append(item); |
| } |
| } |
| } |
| } |
| |
| const title = document.createElement('div'); { |
| title.classList.add('qr--title'); |
| title.textContent = isCurrent ? 'Current Scope' : 'Parent Scope'; |
| if (c.source == source) { |
| let hi; |
| title.addEventListener('pointerenter', ()=>{ |
| const loc = this.getEditorPosition(Math.max(0, c.executorList[0].start - 1), c.executorList.slice(-1)[0].end, c.fullText); |
| const layer = syntax.getBoundingClientRect(); |
| hi = document.createElement('div'); |
| hi.classList.add('qr--highlight-secondary'); |
| hi.style.left = `${loc.left - layer.left}px`; |
| hi.style.width = `${loc.right - loc.left}px`; |
| hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`; |
| hi.style.height = `${loc.bottom - loc.top}px`; |
| syntax.append(hi); |
| }); |
| title.addEventListener('pointerleave', ()=>hi?.remove()); |
| } |
| wrap.append(title); |
| } |
| for (const key of Object.keys(scope.variables)) { |
| const isHidden = varNames.includes(key); |
| if (!isHidden) varNames.push(key); |
| const item = document.createElement('div'); { |
| item.classList.add('qr--var'); |
| if (isHidden) item.classList.add('qr--isHidden'); |
| const k = document.createElement('div'); { |
| k.classList.add('qr--key'); |
| k.textContent = key; |
| item.append(k); |
| } |
| const v = document.createElement('div'); { |
| v.classList.add('qr--val'); |
| const val = scope.variables[key]; |
| if (val instanceof SlashCommandClosure) { |
| v.classList.add('qr--closure'); |
| v.title = val.rawText; |
| v.textContent = val.toString(); |
| } else if (val === undefined) { |
| v.classList.add('qr--undefined'); |
| v.textContent = 'undefined'; |
| } else { |
| let jsonVal; |
| try { jsonVal = JSON.parse(val); } catch { } |
| if (jsonVal && typeof jsonVal == 'object') { |
| v.textContent = JSON.stringify(jsonVal, null, 2); |
| } else { |
| v.textContent = val; |
| v.classList.add('qr--simple'); |
| } |
| } |
| item.append(v); |
| } |
| wrap.append(item); |
| } |
| } |
| for (const key of Object.keys(scope.macros)) { |
| const isHidden = macroNames.includes(key); |
| if (!isHidden) macroNames.push(key); |
| const item = document.createElement('div'); { |
| item.classList.add('qr--macro'); |
| if (isHidden) item.classList.add('qr--isHidden'); |
| const k = document.createElement('div'); { |
| k.classList.add('qr--key'); |
| k.textContent = key; |
| item.append(k); |
| } |
| const v = document.createElement('div'); { |
| v.classList.add('qr--val'); |
| const val = scope.macros[key]; |
| if (val instanceof SlashCommandClosure) { |
| v.classList.add('qr--closure'); |
| v.title = val.rawText; |
| v.textContent = val.toString(); |
| } else if (val === undefined) { |
| v.classList.add('qr--undefined'); |
| v.textContent = 'undefined'; |
| } else { |
| let jsonVal; |
| try { jsonVal = JSON.parse(val); } catch { } |
| if (jsonVal && typeof jsonVal == 'object') { |
| v.textContent = JSON.stringify(jsonVal, null, 2); |
| } else { |
| v.textContent = val; |
| v.classList.add('qr--simple'); |
| } |
| } |
| item.append(v); |
| } |
| wrap.append(item); |
| } |
| } |
| const pipeItem = document.createElement('div'); { |
| pipeItem.classList.add('qr--pipe'); |
| const k = document.createElement('div'); { |
| k.classList.add('qr--key'); |
| k.textContent = 'pipe'; |
| pipeItem.append(k); |
| } |
| const v = document.createElement('div'); { |
| v.classList.add('qr--val'); |
| const val = scope.pipe; |
| if (val instanceof SlashCommandClosure) { |
| v.classList.add('qr--closure'); |
| v.title = val.rawText; |
| v.textContent = val.toString(); |
| } else if (val === undefined) { |
| v.classList.add('qr--undefined'); |
| v.textContent = 'undefined'; |
| } else { |
| let jsonVal; |
| try { jsonVal = JSON.parse(val); } catch { } |
| if (jsonVal && typeof jsonVal == 'object') { |
| v.textContent = JSON.stringify(jsonVal, null, 2); |
| } else { |
| v.textContent = val; |
| v.classList.add('qr--simple'); |
| } |
| } |
| pipeItem.append(v); |
| } |
| wrap.append(pipeItem); |
| } |
| if (scope.parent) { |
| wrap.append(buildVars(scope.parent)); |
| } |
| } |
| return wrap; |
| }; |
| const buildStack = ()=>{ |
| const wrap = document.createElement('div'); { |
| wrap.classList.add('qr--stack'); |
| const title = document.createElement('div'); { |
| title.classList.add('qr--title'); |
| title.textContent = 'Call Stack'; |
| wrap.append(title); |
| } |
| let ei = -1; |
| for (const executor of this.debugController.cmdStack.toReversed()) { |
| ei++; |
| const c = this.debugController.stack.toReversed()[ei]; |
| const item = document.createElement('div'); { |
| item.classList.add('qr--item'); |
| if (executor.source == source) { |
| let hi; |
| item.addEventListener('pointerenter', ()=>{ |
| const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, c.fullText); |
| const layer = syntax.getBoundingClientRect(); |
| hi = document.createElement('div'); |
| hi.classList.add('qr--highlight-secondary'); |
| hi.style.left = `${loc.left - layer.left}px`; |
| hi.style.width = `${loc.right - loc.left}px`; |
| hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`; |
| hi.style.height = `${loc.bottom - loc.top}px`; |
| syntax.append(hi); |
| }); |
| item.addEventListener('pointerleave', ()=>hi?.remove()); |
| } |
| const cmd = document.createElement('div'); { |
| cmd.classList.add('qr--cmd'); |
| cmd.textContent = `/${executor.name}`; |
| if (executor.command.name == 'run') { |
| cmd.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`; |
| } |
| item.append(cmd); |
| } |
| const src = document.createElement('div'); { |
| src.classList.add('qr--source'); |
| const line = closure.fullText.slice(0, executor.start).split('\n').length; |
| if (uuidCheck.test(executor.source)) { |
| const p1 = document.createElement('span'); { |
| p1.classList.add('qr--fixed'); |
| p1.textContent = executor.source.slice(0,5); |
| src.append(p1); |
| } |
| const p2 = document.createElement('span'); { |
| p2.classList.add('qr--truncated'); |
| p2.textContent = '…'; |
| src.append(p2); |
| } |
| const p3 = document.createElement('span'); { |
| p3.classList.add('qr--fixed'); |
| p3.textContent = `${executor.source.slice(-5)}:${line}`; |
| src.append(p3); |
| } |
| src.title = `anonymous: ${executor.source}`; |
| } else { |
| src.textContent = `${executor.source}:${line}`; |
| } |
| item.append(src); |
| } |
| wrap.append(item); |
| } |
| } |
| } |
| return wrap; |
| }; |
| this.editorDebugState.append(buildVars(closure.scope, true)); |
| this.editorDebugState.append(buildStack()); |
| this.editorDebugState.classList.add('qr--active'); |
| const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, closure.fullText); |
| const layer = syntax.getBoundingClientRect(); |
| const hi = document.createElement('div'); |
| hi.classList.add('qr--highlight'); |
| if (this.debugController.namedArguments === undefined) { |
| hi.classList.add('qr--unresolved'); |
| } |
| hi.style.left = `${loc.left - layer.left}px`; |
| hi.style.width = `${loc.right - loc.left}px`; |
| hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`; |
| hi.style.height = `${loc.bottom - loc.top}px`; |
| syntax.append(hi); |
| const isStepping = await this.debugController.awaitContinue(); |
| hi.remove(); |
| this.editorDebugState.textContent = ''; |
| this.editorDebugState.classList.remove('qr--active'); |
| this.editorDom.classList.remove('qr--isPaused'); |
| return isStepping; |
| }; |
| const result = await this.onDebug(this); |
| if (this.abortController?.signal?.aborted) { |
| this.editorExecuteProgress.classList.add('qr--aborted'); |
| } else { |
| this.editorExecuteResult.textContent = result?.toString(); |
| this.editorExecuteResult.classList.add('qr--hasResult'); |
| this.editorExecuteProgress.classList.add('qr--success'); |
| } |
| this.editorExecuteProgress.classList.remove('qr--paused'); |
| } catch (ex) { |
| this.editorExecuteErrors.classList.add('qr--hasErrors'); |
| this.editorExecuteProgress.classList.add('qr--error'); |
| this.editorExecuteProgress.classList.remove('qr--paused'); |
| if (ex instanceof SlashCommandParserError) { |
| this.editorExecuteErrors.innerHTML = ` |
| <div>${ex.message}</div> |
| <div>Line: ${ex.line} Column: ${ex.column}</div> |
| <pre style="text-align:left;">${ex.hint}</pre> |
| `; |
| } else { |
| this.editorExecuteErrors.innerHTML = ` |
| <div>${ex.message}</div> |
| `; |
| } |
| } |
| if (noSyntax) { |
| this.editorDom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax'); |
| } |
| this.editorMessageLabel.innerHTML = ''; |
| this.editorMessageLabel.textContent = 'Message / Command: '; |
| this.editorMessage.value = oText; |
| this.editorMessage.dispatchEvent(new Event('input', { bubbles:true })); |
| this.editorExecutePromise = null; |
| this.editorExecuteBtn.classList.remove('qr--busy'); |
| this.editorDom.classList.remove('qr--isExecuting'); |
| this.isExecuting = false; |
| this.editorPopup.onClosing = null; |
| } |
|
|
| updateEditorProgress(done, total) { |
| this.editorExecuteProgress.style.setProperty('--prog', `${done / total * 100}`); |
| } |
|
|
|
|
|
|
|
|
| delete() { |
| if (this.onDelete) { |
| this.unrender(); |
| this.unrenderSettings(); |
| this.onDelete(this); |
| } |
| } |
|
|
| |
| |
| |
| updateMessage(value) { |
| if (this.onUpdate) { |
| if (this.settingsDomMessage && this.settingsDomMessage.value != value) { |
| this.settingsDomMessage.value = value; |
| } |
| this.message = value; |
| this.updateRender(); |
| this.onUpdate(this); |
| } |
| } |
|
|
| |
| |
| |
| updateIcon(value) { |
| if (this.onUpdate) { |
| if (value === null) return; |
| if (this.settingsDomIcon) { |
| if (this.icon != value) { |
| if (value == '') { |
| if (this.icon) { |
| this.settingsDomIcon.classList.remove(this.icon); |
| } |
| this.settingsDomIcon.textContent = '…'; |
| this.settingsDomIcon.classList.remove('fa-solid'); |
| } else { |
| if (this.icon) { |
| this.settingsDomIcon.classList.remove(this.icon); |
| } else { |
| this.settingsDomIcon.classList.add('fa-solid'); |
| } |
| this.settingsDomIcon.classList.add(value); |
| } |
| } |
| } |
| this.icon = value; |
| this.updateRender(); |
| this.onUpdate(this); |
| } |
| } |
|
|
| |
| |
| |
| updateShowLabel(value) { |
| if (this.onUpdate) { |
| this.showLabel = value; |
| this.updateRender(); |
| this.onUpdate(this); |
| } |
| } |
|
|
| |
| |
| |
| updateLabel(value) { |
| if (this.onUpdate) { |
| if (this.settingsDomLabel && this.settingsDomLabel.value != value) { |
| this.settingsDomLabel.value = value; |
| } |
| this.label = value; |
| this.updateRender(); |
| this.onUpdate(this); |
| } |
| } |
|
|
| |
| |
| |
| updateTitle(value) { |
| if (this.onUpdate) { |
| this.title = value; |
| this.updateRender(); |
| this.onUpdate(this); |
| } |
| } |
|
|
| updateContext() { |
| if (this.onUpdate) { |
| this.updateRender(); |
| this.onUpdate(this); |
| } |
| } |
| addContextLink(cl) { |
| this.contextList.push(cl); |
| this.updateContext(); |
| } |
| removeContextLink(setName) { |
| const idx = this.contextList.findIndex(it=>it.set.name == setName); |
| if (idx > -1) { |
| this.contextList.splice(idx, 1); |
| this.updateContext(); |
| } |
| } |
| clearContextLinks() { |
| if (this.contextList.length) { |
| this.contextList.splice(0, this.contextList.length); |
| this.updateContext(); |
| } |
| } |
|
|
|
|
| async execute(args = {}, isEditor = false, isRun = false, options = {}) { |
| if (this.message?.length > 0 && this.onExecute) { |
| const scope = new SlashCommandScope(); |
| for (const key of Object.keys(args)) { |
| if (key[0] == '_') continue; |
| if (key == 'isAutoExecute') continue; |
| scope.setMacro(`arg::${key}`, args[key]); |
| } |
| scope.setMacro('arg::*', ''); |
| if (isEditor) { |
| this.abortController = new SlashCommandAbortController(); |
| } |
| return await this.onExecute(this, { |
| message: this.message, |
| isAutoExecute: args.isAutoExecute ?? false, |
| isEditor, |
| isRun, |
| scope, |
| executionOptions: options, |
| }); |
| } |
| } |
|
|
|
|
|
|
|
|
| toJSON() { |
| return { |
| id: this.id, |
| icon: this.icon, |
| showLabel: this.showLabel, |
| label: this.label, |
| title: this.title, |
| message: this.message, |
| contextList: this.contextList, |
| preventAutoExecute: this.preventAutoExecute, |
| isHidden: this.isHidden, |
| executeOnStartup: this.executeOnStartup, |
| executeOnUser: this.executeOnUser, |
| executeOnAi: this.executeOnAi, |
| executeOnChatChange: this.executeOnChatChange, |
| executeOnGroupMemberDraft: this.executeOnGroupMemberDraft, |
| executeOnNewChat: this.executeOnNewChat, |
| executeBeforeGeneration: this.executeBeforeGeneration, |
| automationId: this.automationId, |
| }; |
| } |
| } |
|
|