diff --git a/public/script.js b/public/script.js index cf35cb74f..d731d8b28 100644 --- a/public/script.js +++ b/public/script.js @@ -219,6 +219,7 @@ import { ScraperManager } from './scripts/scrapers.js'; import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js'; import { SlashCommand } from './scripts/slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument } from './scripts/slash-commands/SlashCommandArgument.js'; +import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.js'; //exporting functions and vars for mods export { @@ -2447,6 +2448,14 @@ function sendSystemMessage(type, text, extra = {}) { chat.push(newMessage); addOneMessage(newMessage); is_send_press = false; + if (type == system_message_types.SLASH_COMMANDS) { + const browser = new SlashCommandBrowser(); + const spinner = document.querySelector('#chat .last_mes .custom-slashHelp'); + const parent = spinner.parentElement; + spinner.remove(); + browser.renderInto(parent); + browser.search.focus(); + } } /** diff --git a/public/scripts/slash-commands/SlashCommand.js b/public/scripts/slash-commands/SlashCommand.js index 68632fab6..4a6fb7d07 100644 --- a/public/scripts/slash-commands/SlashCommand.js +++ b/public/scripts/slash-commands/SlashCommand.js @@ -190,4 +190,171 @@ export class SlashCommand { } 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'); { + 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'); + 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); + } + 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; + frag.append(help); + } + if (aliasList.length > 0) { + const aliases = document.createElement('span'); { + aliases.classList.add('aliases'); + aliases.append(' (alias: '); + for (const aliasName of aliasList) { + const alias = document.createElement('span'); { + alias.classList.add('monospace'); + alias.textContent = `/${aliasName}`; + aliases.append(alias); + } + } + aliases.append(')'); + frag.append(aliases); + } + } + return frag; + } } diff --git a/public/scripts/slash-commands/SlashCommandAutoCompleteOption.js b/public/scripts/slash-commands/SlashCommandAutoCompleteOption.js index 919994950..cbab3ba3a 100644 --- a/public/scripts/slash-commands/SlashCommandAutoCompleteOption.js +++ b/public/scripts/slash-commands/SlashCommandAutoCompleteOption.js @@ -282,171 +282,10 @@ export class SlashCommandAutoCompleteOption { return frag; } renderCommandDetails() { - const frag = document.createDocumentFragment(); const key = this.name; /**@type {SlashCommand} */ // @ts-ignore const cmd = this.value; - 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'); - 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); - } - 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; - frag.append(help); - } - if (aliasList.length > 0) { - const aliases = document.createElement('span'); { - aliases.classList.add('aliases'); - aliases.append(' (alias: '); - for (const aliasName of aliasList) { - const alias = document.createElement('span'); { - alias.classList.add('monospace'); - alias.textContent = `/${aliasName}`; - aliases.append(alias); - } - } - aliases.append(')'); - frag.append(aliases); - } - } - return frag; + return cmd.renderHelpDetails(key); } } diff --git a/public/scripts/slash-commands/SlashCommandBrowser.js b/public/scripts/slash-commands/SlashCommandBrowser.js new file mode 100644 index 000000000..1a984f2ef --- /dev/null +++ b/public/scripts/slash-commands/SlashCommandBrowser.js @@ -0,0 +1,148 @@ +import { escapeRegex } from '../utils.js'; +import { SlashCommand } from './SlashCommand.js'; +import { SlashCommandParser } from './SlashCommandParser.js'; + +export class SlashCommandBrowser { + /**@type {SlashCommand[]}*/ cmdList; + /**@type {HTMLElement}*/ dom; + /**@type {HTMLElement}*/ search; + /**@type {HTMLElement}*/ details; + /**@type {Object.}*/ itemMap = {}; + /**@type {MutationObserver}*/ mo; + + renderInto(parent) { + if (!this.dom) { + const queryRegex = /(?:(?:^|\s+)([^\s"][^\s]*?)(?:\s+|$))|(?:(?:^|\s+)"(.*?)(?:"|$)(?:\s+|$))/; + const root = document.createElement('div'); { + this.dom = root; + const search = document.createElement('div'); { + search.classList.add('search'); + const lbl = document.createElement('label'); { + lbl.classList.add('searchLabel'); + lbl.textContent = 'Search: '; + const inp = document.createElement('input'); { + this.search = inp; + inp.classList.add('searchInput'); + inp.classList.add('text_pole'); + inp.type = 'search'; + inp.placeholder = 'Search slash commands - use quotes to search "literal" instead of fuzzy'; + inp.addEventListener('input', ()=>{ + this.details?.remove(); + this.details = null; + let query = inp.value.trim(); + if (query.slice(-1) == '"' && !/(?:^|\s+)"/.test(query)) { + query = `"${query}`; + } + let fuzzyList = []; + let quotedList = []; + while (query.length > 0) { + const match = queryRegex.exec(query); + if (!match) break; + if (match[1] !== undefined) { + fuzzyList.push(new RegExp(`^(.*?)${match[1].split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i')); + } else if (match[2] !== undefined) { + quotedList.push(match[2]); + } + query = query.slice(match.index + match[0].length); + } + for (const cmd of this.cmdList) { + const targets = [ + cmd.name, + ...cmd.namedArgumentList.map(it=>it.name), + ...cmd.namedArgumentList.map(it=>it.description), + ...cmd.namedArgumentList.map(it=>it.enumList).flat(), + ...cmd.namedArgumentList.map(it=>it.typeList).flat(), + ...cmd.unnamedArgumentList.map(it=>it.description), + ...cmd.unnamedArgumentList.map(it=>it.enumList).flat(), + ...cmd.unnamedArgumentList.map(it=>it.typeList).flat(), + ...cmd.aliases, + cmd.helpString, + ]; + const find = ()=>targets.find(t=>(fuzzyList.find(f=>f.test(t)) ?? quotedList.find(q=>t.includes(q))) !== undefined) !== undefined; + if (fuzzyList.length + quotedList.length == 0 || find()) { + this.itemMap[cmd.name].classList.remove('isFiltered'); + } else { + this.itemMap[cmd.name].classList.add('isFiltered'); + } + } + }); + lbl.append(inp); + } + search.append(lbl); + } + root.append(search); + } + const container = document.createElement('div'); { + container.classList.add('commandContainer'); + const list = document.createElement('div'); { + list.classList.add('slashCommandAutoComplete'); + this.cmdList = Object + .keys(SlashCommandParser.commands) + .filter(key => SlashCommandParser.commands[key].name == key) // exclude aliases + .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())) + .map(key => SlashCommandParser.commands[key]) + ; + for (const cmd of this.cmdList) { + const item = cmd.renderHelpItem(); + this.itemMap[cmd.name] = item; + let details; + item.addEventListener('click', ()=>{ + if (!details) { + details = document.createElement('div'); { + details.classList.add('slashCommandAutoComplete-detailsWrap'); + const inner = document.createElement('div'); { + inner.classList.add('slashCommandAutoComplete-details'); + inner.append(cmd.renderHelpDetails()); + details.append(inner); + } + } + } + if (this.details != details) { + Array.from(list.querySelectorAll('.selected')).forEach(it=>it.classList.remove('selected')); + item.classList.add('selected'); + this.details?.remove(); + container.append(details); + this.details = details; + const pRect = list.getBoundingClientRect(); + const rect = item.children[0].getBoundingClientRect(); + details.style.setProperty('--targetOffset', rect.top - pRect.top); + } else { + item.classList.remove('selected'); + details.remove(); + this.details = null; + } + }); + list.append(item); + } + container.append(list); + } + root.append(container); + } + root.classList.add('slashCommandBrowser'); + } + } + parent.append(this.dom); + + this.mo = new MutationObserver(muts=>{ + if (muts.find(mut=>Array.from(mut.removedNodes).find(it=>it == this.dom || it.contains(this.dom)))) { + this.mo.disconnect(); + window.removeEventListener('keydown', boundHandler); + } + }); + this.mo.observe(document.querySelector('#chat'), { childList:true, subtree:true }); + const boundHandler = this.handleKeyDown.bind(this); + window.addEventListener('keydown', boundHandler); + return this.dom; + } + + handleKeyDown(evt) { + if (!evt.shiftKey && !evt.altKey && evt.ctrlKey && evt.key.toLowerCase() == 'f') { + if (!this.dom.closest('body')) return; + if (this.dom.closest('.mes') && !this.dom.closest('.last_mes')) return; + evt.preventDefault(); + evt.stopPropagation(); + evt.stopImmediatePropagation(); + this.search.focus(); + } + } +} diff --git a/public/scripts/slash-commands/SlashCommandParser.js b/public/scripts/slash-commands/SlashCommandParser.js index 165656653..915a41ebe 100644 --- a/public/scripts/slash-commands/SlashCommandParser.js +++ b/public/scripts/slash-commands/SlashCommandParser.js @@ -255,6 +255,9 @@ export class SlashCommandParser { } getHelpString() { + return '
Loading...
'; + } + getHelpStringX() { const listItems = Object .keys(this.commands) .filter(key=>this.commands[key].name == key) diff --git a/public/style.css b/public/style.css index 8ecda6551..78777395d 100644 --- a/public/style.css +++ b/public/style.css @@ -1197,6 +1197,7 @@ select { } } } + .slashCommandAutoComplete, .slashCommandAutoComplete-details { --ac-color-border: rgb(69 69 69); --ac-color-background: rgb(32 32 32); @@ -1225,6 +1226,9 @@ select { margin: 0px; overflow: auto; padding: 0px; + padding-bottom: 1px; + line-height: 1.2; + text-align: left; z-index: 10000; } .slashCommandAutoComplete { @@ -1232,7 +1236,7 @@ select { /* position: absolute; */ display: grid; grid-template-columns: 0fr auto minmax(50%, 1fr); - align-items: baseline; + align-items: center; max-height: calc(95vh - var(--bottom)); /* gap: 0.5em; */ > .item { @@ -1497,6 +1501,60 @@ select { } } +.slashCommandBrowser { + > .search { + display: flex; + gap: 1em; + align-items: baseline; + white-space: nowrap; + > .searchLabel { + flex: 1 1 auto; + display: flex; + gap: 0.5em; + align-items: baseline; + > .searchInput { + flex: 1 1 auto; + } + } + > .searchOptions { + display: flex; + gap: 1em; + align-items: baseline; + } + } + > .commandContainer { + display: flex; + align-items: flex-start; + > .slashCommandAutoComplete { + flex: 1 1 auto; + max-height: unset; + > .isFiltered { + display: none; + } + .specs { + grid-column: 2 / 4; + } + .help { + grid-column: 2 / 4; + padding-left: 1em; + opacity: 0.75; + } + } + > .slashCommandAutoComplete-detailsWrap { + flex: 0 0 auto; + align-self: stretch; + width: 30%; + position: static; + &:before { + flex: 0 1 calc(var(--targetOffset) * 1px); + } + > .slashCommandAutoComplete-details { + max-height: 50vh; + } + } + } +} + #character_popup .editor_maximize { cursor: pointer; margin: 5px;