Support conditional command keyword aliases
This commit is contained in:
parent
4853092956
commit
66d0b63077
@ -1,5 +1,9 @@
|
|||||||
# Lumi changelog
|
# Lumi changelog
|
||||||
|
|
||||||
|
## 0.2.20
|
||||||
|
|
||||||
|
- Added comma-separated aliases to conditional command reply rules, allowing several keywords to return one shared response while retaining exact and fuzzy matching.
|
||||||
|
|
||||||
## 0.2.19
|
## 0.2.19
|
||||||
|
|
||||||
- Added a protected, implicitly inherited default command group that can be edited but not deleted.
|
- Added a protected, implicitly inherited default command group that can be edited but not deleted.
|
||||||
|
|||||||
3
TODO.md
3
TODO.md
@ -21,6 +21,9 @@ search suggestions with positive-only live setting/source filters; and atomic
|
|||||||
page-wide saving for every command so collapsed or filtered drafts survive the
|
page-wide saving for every command so collapsed or filtered drafts survive the
|
||||||
save.
|
save.
|
||||||
|
|
||||||
|
Patch completed on 2026-07-19: conditional reply rules now accept
|
||||||
|
comma-separated keyword aliases that share one response.
|
||||||
|
|
||||||
Remaining work:
|
Remaining work:
|
||||||
|
|
||||||
- Add platform action providers when YouTube or future chat integrations expose
|
- Add platform action providers when YouTube or future chat integrations expose
|
||||||
|
|||||||
@ -44,6 +44,9 @@ For example, a `rules` command can answer `!rules backseating` and
|
|||||||
`!rules spoilers` differently. The optional main response is shown for `!rules`
|
`!rules spoilers` differently. The optional main response is shown for `!rules`
|
||||||
without an argument; if it is empty, Lumi lists the available keywords.
|
without an argument; if it is empty, Lumi lists the available keywords.
|
||||||
|
|
||||||
|
Separate keywords with commas when several aliases should return the same
|
||||||
|
reply. For example, `backseat, backseating, hints` can share one rules response.
|
||||||
|
|
||||||
Exact keyword matches always win. Optional fuzzy matching accepts only a clear,
|
Exact keyword matches always win. Optional fuzzy matching accepts only a clear,
|
||||||
close typo and refuses ambiguous or unrelated text. Reply templates use the
|
close typo and refuses ambiguous or unrelated text. Reply templates use the
|
||||||
same shared placeholder validation and rendering as static custom commands.
|
same shared placeholder validation and rendering as static custom commands.
|
||||||
|
|||||||
@ -14,7 +14,7 @@ editable: false
|
|||||||
Lumi is the core web UI and bot runtime.
|
Lumi is the core web UI and bot runtime.
|
||||||
## Runtime
|
## Runtime
|
||||||
Package: lumi-bot
|
Package: lumi-bot
|
||||||
Version: 0.2.19
|
Version: 0.2.20
|
||||||
## Routes
|
## Routes
|
||||||
- GET /api/events
|
- GET /api/events
|
||||||
- POST /api/destructive-confirmations
|
- POST /api/destructive-confirmations
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.2.19",
|
"version": "0.2.20",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.2.19",
|
"version": "0.2.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.12",
|
"adm-zip": "^0.5.12",
|
||||||
"better-sqlite3": "^11.5.0",
|
"better-sqlite3": "^11.5.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.2.19",
|
"version": "0.2.20",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -2,6 +2,36 @@
|
|||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"channel": "stable",
|
"channel": "stable",
|
||||||
"releases": [
|
"releases": [
|
||||||
|
{
|
||||||
|
"version": "0.2.20",
|
||||||
|
"ref": "refs/tags/v0.2.20",
|
||||||
|
"released_at": "2026-07-19",
|
||||||
|
"installable": true,
|
||||||
|
"rollback_safe": true,
|
||||||
|
"replaces_versions": [
|
||||||
|
"1.2.0"
|
||||||
|
],
|
||||||
|
"data_policy": "preserve",
|
||||||
|
"dependency_policy": "sync_on_restart",
|
||||||
|
"migration_notes": "Adds comma-separated aliases for conditional command reply rules. Existing commands, policies, groups, usage totals, overlays, tokens, scenes, sources, settings, databases, plugin data, models, uploads, feedback, and secrets are preserved.",
|
||||||
|
"plugins": {
|
||||||
|
"auto-vc": "0.1.6",
|
||||||
|
"birthday": "0.1.3",
|
||||||
|
"economy-framework": "0.2.10",
|
||||||
|
"economy-games": "0.1.7",
|
||||||
|
"expression-interaction": "0.2.1",
|
||||||
|
"lumi_ai": "0.8.5",
|
||||||
|
"moderation": "0.1.5",
|
||||||
|
"okf": "0.1.1",
|
||||||
|
"quotes": "0.1.2",
|
||||||
|
"sample-plugin": "0.1.0",
|
||||||
|
"throne_wishlist": "0.1.2",
|
||||||
|
"welcome_messages": "0.1.1"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"lumi_ai_web_search": "0.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.2.19",
|
"version": "0.2.19",
|
||||||
"ref": "refs/tags/v0.2.19",
|
"ref": "refs/tags/v0.2.19",
|
||||||
|
|||||||
@ -27,6 +27,22 @@ try {
|
|||||||
assert.strictEqual(fuzzy.match.keyword, "backseating");
|
assert.strictEqual(fuzzy.match.keyword, "backseating");
|
||||||
assert.strictEqual(fuzzy.fuzzy, true);
|
assert.strictEqual(fuzzy.fuzzy, true);
|
||||||
assert.strictEqual(conditional.findConditionalReply(replies, "something unrelated").match, null);
|
assert.strictEqual(conditional.findConditionalReply(replies, "something unrelated").match, null);
|
||||||
|
const aliases = conditional.conditionalRepliesFromBody({
|
||||||
|
conditional_keyword: "backseat, backseating, hints",
|
||||||
|
conditional_response: "Please ask before helping."
|
||||||
|
});
|
||||||
|
assert.strictEqual(aliases.ok, true);
|
||||||
|
assert.strictEqual(aliases.replies[0].keyword, "backseat, backseating, hints");
|
||||||
|
assert.strictEqual(conditional.normalizeConditionalReplyRows(JSON.stringify(aliases.replies))[0].keyword, "backseat, backseating, hints");
|
||||||
|
for (const alias of ["backseat", "backseating", "hints"]) {
|
||||||
|
assert.strictEqual(conditional.findConditionalReply(aliases.replies, alias).match.response, "Please ask before helping.");
|
||||||
|
}
|
||||||
|
const duplicateAlias = conditional.conditionalRepliesFromBody({
|
||||||
|
conditional_keyword: ["spoilers, leaks", "leaks"],
|
||||||
|
conditional_response: ["First", "Second"]
|
||||||
|
});
|
||||||
|
assert.strictEqual(duplicateAlias.ok, false);
|
||||||
|
assert.match(duplicateAlias.errors[0], /leaks.*more than once/);
|
||||||
|
|
||||||
const groupId = policies.createGroup({
|
const groupId = policies.createGroup({
|
||||||
name: "Verification group",
|
name: "Verification group",
|
||||||
@ -102,6 +118,7 @@ try {
|
|||||||
assert(view.includes("data-policy-search") && view.includes("data-policy-setting-filter"));
|
assert(view.includes("data-policy-search") && view.includes("data-policy-setting-filter"));
|
||||||
assert(view.includes("Protected default") && view.includes("data-command-policy-batch"));
|
assert(view.includes("Protected default") && view.includes("data-command-policy-batch"));
|
||||||
assert(commandView.includes('value="conditional"') && commandView.includes("data-conditional-replies"));
|
assert(commandView.includes('value="conditional"') && commandView.includes("data-conditional-replies"));
|
||||||
|
assert(commandView.includes("Separate aliases with commas"));
|
||||||
|
|
||||||
database.db.close();
|
database.db.close();
|
||||||
database = null;
|
database = null;
|
||||||
|
|||||||
@ -4,8 +4,8 @@ const path = require("path");
|
|||||||
const { findSafeTarget } = require("../src/services/versioning");
|
const { findSafeTarget } = require("../src/services/versioning");
|
||||||
|
|
||||||
const root = path.join(__dirname, "..");
|
const root = path.join(__dirname, "..");
|
||||||
const releaseVersion = "0.2.19";
|
const releaseVersion = "0.2.20";
|
||||||
const previousCoreVersion = "0.2.18";
|
const previousCoreVersion = "0.2.19";
|
||||||
const earliestCompatibleCoreVersion = "0.1.9";
|
const earliestCompatibleCoreVersion = "0.1.9";
|
||||||
const changedPlugins = {
|
const changedPlugins = {
|
||||||
"auto-vc": { from: "0.1.5", to: "0.1.6", knowledge: "auto-vc" },
|
"auto-vc": { from: "0.1.5", to: "0.1.6", knowledge: "auto-vc" },
|
||||||
@ -87,4 +87,4 @@ assert.equal(webSearch.minimum_lumi_version, "0.2.0");
|
|||||||
assert.equal(webSearch.minimum_lumi_ai_version, "0.8.2");
|
assert.equal(webSearch.minimum_lumi_ai_version, "0.8.2");
|
||||||
assert.equal(hasVersionHeading(readText("plugins/lumi_ai_web_search/CHANGELOG.md"), webSearch.version), true);
|
assert.equal(hasVersionHeading(readText("plugins/lumi_ai_web_search/CHANGELOG.md"), webSearch.version), true);
|
||||||
|
|
||||||
console.log("Release metadata verification passed: core 0.2.19, Lumi AI 0.8.5, and synchronized package metadata.");
|
console.log("Release metadata verification passed: core 0.2.20, Lumi AI 0.8.5, and synchronized package metadata.");
|
||||||
|
|||||||
@ -16,7 +16,7 @@ function readJson(relativePath) {
|
|||||||
|
|
||||||
const releaseIndex = readJson("release-index.json");
|
const releaseIndex = readJson("release-index.json");
|
||||||
const releaseVersions = releaseIndex.releases.map((release) => release.version);
|
const releaseVersions = releaseIndex.releases.map((release) => release.version);
|
||||||
assert.deepEqual(releaseVersions, ["0.2.19", "0.2.18", "0.2.17", "0.2.16", "0.2.15", "0.2.14", "0.2.13", "0.2.12", "0.2.11", "0.2.10", "0.2.9", "0.2.8", "0.2.7", "0.2.6", "0.2.5", "0.2.4", "0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]);
|
assert.deepEqual(releaseVersions, ["0.2.20", "0.2.19", "0.2.18", "0.2.17", "0.2.16", "0.2.15", "0.2.14", "0.2.13", "0.2.12", "0.2.11", "0.2.10", "0.2.9", "0.2.8", "0.2.7", "0.2.6", "0.2.5", "0.2.4", "0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]);
|
||||||
assert.equal(new Set(releaseVersions).size, releaseVersions.length, "release versions must be unique");
|
assert.equal(new Set(releaseVersions).size, releaseVersions.length, "release versions must be unique");
|
||||||
for (const release of releaseIndex.releases) {
|
for (const release of releaseIndex.releases) {
|
||||||
assert.equal(normalizeRepositoryRef(release.ref), release.ref);
|
assert.equal(normalizeRepositoryRef(release.ref), release.ref);
|
||||||
@ -37,6 +37,7 @@ for (const [toolId, version] of Object.entries(currentRelease.tools)) {
|
|||||||
const baseTarget = {
|
const baseTarget = {
|
||||||
current_version: "0.2.4",
|
current_version: "0.2.4",
|
||||||
available_versions: [
|
available_versions: [
|
||||||
|
{ version: "0.2.20", ref: "refs/tags/v0.2.20", rollback_safe: true },
|
||||||
{ version: "0.2.19", ref: "refs/tags/v0.2.19", rollback_safe: true },
|
{ version: "0.2.19", ref: "refs/tags/v0.2.19", rollback_safe: true },
|
||||||
{ version: "0.2.18", ref: "refs/tags/v0.2.18", rollback_safe: true },
|
{ version: "0.2.18", ref: "refs/tags/v0.2.18", rollback_safe: true },
|
||||||
{ version: "0.2.17", ref: "refs/tags/v0.2.17", rollback_safe: true },
|
{ version: "0.2.17", ref: "refs/tags/v0.2.17", rollback_safe: true },
|
||||||
@ -79,7 +80,7 @@ const corrected = buildStatus({
|
|||||||
channel: "stable"
|
channel: "stable"
|
||||||
});
|
});
|
||||||
assert.equal(corrected.version_correction, true);
|
assert.equal(corrected.version_correction, true);
|
||||||
assert.equal(corrected.safe_target_version, "0.2.19");
|
assert.equal(corrected.safe_target_version, "0.2.20");
|
||||||
assert.equal(corrected.update_available, true);
|
assert.equal(corrected.update_available, true);
|
||||||
assert.equal(corrected.blocked, false);
|
assert.equal(corrected.blocked, false);
|
||||||
|
|
||||||
|
|||||||
@ -9,21 +9,46 @@ function normalizeConditionalKey(value) {
|
|||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function splitConditionalKeywords(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.split(",")
|
||||||
|
.map((keyword) => keyword.trim().slice(0, 100))
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeConditionalReplies(value) {
|
function normalizeConditionalReplies(value) {
|
||||||
|
return normalizeConditionalReplyRows(value).flatMap((entry) => splitConditionalKeywords(entry.keyword).map((keyword) => ({
|
||||||
|
keyword,
|
||||||
|
normalized: normalizeConditionalKey(keyword),
|
||||||
|
response: entry.response
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeConditionalReplyRows(value) {
|
||||||
let input = value;
|
let input = value;
|
||||||
if (typeof input === "string") {
|
if (typeof input === "string") {
|
||||||
try { input = JSON.parse(input); } catch { input = []; }
|
try { input = JSON.parse(input); } catch { input = []; }
|
||||||
}
|
}
|
||||||
if (!Array.isArray(input)) return [];
|
if (!Array.isArray(input)) return [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
return input.slice(0, MAX_CONDITIONAL_REPLIES).flatMap((entry) => {
|
const rows = [];
|
||||||
const keyword = String(entry?.keyword || "").trim().slice(0, 100);
|
let aliasCount = 0;
|
||||||
|
for (const entry of input) {
|
||||||
const response = String(entry?.response || "").trim().slice(0, 4000);
|
const response = String(entry?.response || "").trim().slice(0, 4000);
|
||||||
|
if (!response) continue;
|
||||||
|
const aliases = [];
|
||||||
|
for (const keyword of splitConditionalKeywords(entry?.keyword)) {
|
||||||
const normalized = normalizeConditionalKey(keyword);
|
const normalized = normalizeConditionalKey(keyword);
|
||||||
if (!normalized || !response || seen.has(normalized)) return [];
|
if (!normalized || seen.has(normalized)) continue;
|
||||||
seen.add(normalized);
|
seen.add(normalized);
|
||||||
return [{ keyword, normalized, response }];
|
aliases.push(keyword);
|
||||||
});
|
aliasCount += 1;
|
||||||
|
if (aliasCount >= MAX_CONDITIONAL_REPLIES) break;
|
||||||
|
}
|
||||||
|
if (aliases.length) rows.push({ keyword: aliases.join(", "), response });
|
||||||
|
if (aliasCount >= MAX_CONDITIONAL_REPLIES) break;
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
function conditionalRepliesFromBody(body = {}) {
|
function conditionalRepliesFromBody(body = {}) {
|
||||||
@ -32,26 +57,28 @@ function conditionalRepliesFromBody(body = {}) {
|
|||||||
const replies = [];
|
const replies = [];
|
||||||
const errors = [];
|
const errors = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
|
let aliasCount = 0;
|
||||||
const count = Math.max(keywords.length, responses.length);
|
const count = Math.max(keywords.length, responses.length);
|
||||||
for (let index = 0; index < count; index += 1) {
|
for (let index = 0; index < count; index += 1) {
|
||||||
const keyword = String(keywords[index] || "").trim();
|
const keyword = String(keywords[index] || "").trim();
|
||||||
|
const aliases = splitConditionalKeywords(keyword);
|
||||||
const response = String(responses[index] || "").trim();
|
const response = String(responses[index] || "").trim();
|
||||||
if (!keyword && !response) continue;
|
if (!keyword && !response) continue;
|
||||||
if (!keyword || !response) {
|
if (!aliases.length || !response) {
|
||||||
errors.push("Each conditional reply needs both a keyword and a response.");
|
errors.push("Each conditional reply needs both a keyword and a response.");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const normalized = normalizeConditionalKey(keyword);
|
for (const alias of aliases) {
|
||||||
if (seen.has(normalized)) {
|
const normalized = normalizeConditionalKey(alias);
|
||||||
errors.push(`The keyword “${keyword}” is listed more than once.`);
|
if (seen.has(normalized)) errors.push(`The keyword “${alias}” is listed more than once.`);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
seen.add(normalized);
|
seen.add(normalized);
|
||||||
replies.push({ keyword: keyword.slice(0, 100), response: response.slice(0, 4000) });
|
}
|
||||||
|
aliasCount += aliases.length;
|
||||||
|
replies.push({ keyword: aliases.join(", "), response: response.slice(0, 4000) });
|
||||||
}
|
}
|
||||||
if (!replies.length) errors.push("Add at least one keyword reply.");
|
if (!replies.length) errors.push("Add at least one keyword reply.");
|
||||||
if (replies.length > MAX_CONDITIONAL_REPLIES) errors.push(`Use no more than ${MAX_CONDITIONAL_REPLIES} keyword replies.`);
|
if (aliasCount > MAX_CONDITIONAL_REPLIES) errors.push(`Use no more than ${MAX_CONDITIONAL_REPLIES} keywords.`);
|
||||||
return { ok: errors.length === 0, errors, replies: replies.slice(0, MAX_CONDITIONAL_REPLIES) };
|
return { ok: errors.length === 0, errors, replies };
|
||||||
}
|
}
|
||||||
|
|
||||||
function findConditionalReply(replies, argument, { fuzzy = true } = {}) {
|
function findConditionalReply(replies, argument, { fuzzy = true } = {}) {
|
||||||
@ -107,5 +134,7 @@ module.exports = {
|
|||||||
conditionalRepliesFromBody,
|
conditionalRepliesFromBody,
|
||||||
findConditionalReply,
|
findConditionalReply,
|
||||||
normalizeConditionalKey,
|
normalizeConditionalKey,
|
||||||
normalizeConditionalReplies
|
normalizeConditionalReplies,
|
||||||
|
normalizeConditionalReplyRows,
|
||||||
|
splitConditionalKeywords
|
||||||
};
|
};
|
||||||
|
|||||||
@ -79,7 +79,8 @@ const { getClient: getTwitchClient } = require("../services/twitch");
|
|||||||
const { getClient: getYouTubeClient } = require("../services/youtube");
|
const { getClient: getYouTubeClient } = require("../services/youtube");
|
||||||
const {
|
const {
|
||||||
conditionalRepliesFromBody,
|
conditionalRepliesFromBody,
|
||||||
normalizeConditionalReplies
|
normalizeConditionalReplies,
|
||||||
|
normalizeConditionalReplyRows
|
||||||
} = require("../services/command-conditional");
|
} = require("../services/command-conditional");
|
||||||
const {
|
const {
|
||||||
createGroup: createCommandGroup,
|
createGroup: createCommandGroup,
|
||||||
@ -1320,7 +1321,7 @@ function buildRandomCommandPresentation(command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildConditionalCommandPresentation(command) {
|
function buildConditionalCommandPresentation(command) {
|
||||||
return { replies: normalizeConditionalReplies(command.conditional_replies_json) };
|
return { replies: normalizeConditionalReplyRows(command.conditional_replies_json) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateConditionalReplyTemplates(config, user) {
|
function validateConditionalReplyTemplates(config, user) {
|
||||||
|
|||||||
@ -64,14 +64,14 @@
|
|||||||
<label>Keyword replies</label>
|
<label>Keyword replies</label>
|
||||||
<div class="command-random-replies" data-conditional-replies>
|
<div class="command-random-replies" data-conditional-replies>
|
||||||
<div class="command-random-row command-conditional-row" data-conditional-reply-row>
|
<div class="command-random-row command-conditional-row" data-conditional-reply-row>
|
||||||
<input name="conditional_keyword" placeholder="backseating" aria-label="Keyword" />
|
<input name="conditional_keyword" placeholder="backseat, backseating" aria-label="Keywords, separated with commas" />
|
||||||
<input name="conditional_response" placeholder="The reply for this keyword" aria-label="Reply" data-placeholder-field="core.custom_commands.static_response" data-placeholder-output-audience="user" />
|
<input name="conditional_response" placeholder="The reply for this keyword" aria-label="Reply" data-placeholder-field="core.custom_commands.static_response" data-placeholder-output-audience="user" />
|
||||||
<button type="button" class="button subtle" data-conditional-remove>Remove</button>
|
<button type="button" class="button subtle" data-conditional-remove>Remove</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="button subtle" data-conditional-add>Add keyword</button>
|
<button type="button" class="button subtle" data-conditional-add>Add keyword</button>
|
||||||
<label class="switch"><input type="checkbox" class="switch-input" name="conditional_fuzzy" checked /><span class="switch-track" aria-hidden="true"></span><span class="switch-text">Suggest a close keyword when there is a small typo</span></label>
|
<label class="switch"><input type="checkbox" class="switch-input" name="conditional_fuzzy" checked /><span class="switch-track" aria-hidden="true"></span><span class="switch-text">Suggest a close keyword when there is a small typo</span></label>
|
||||||
<p class="hint">Example: <code>!rules backseating</code>. Exact matches are preferred; fuzzy matching only accepts a clear close match.</p>
|
<p class="hint">Separate aliases with commas to give them the same reply. Example: <code>backseat, backseating</code>. Exact matches are preferred; fuzzy matching only accepts a clear close match.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field full js-field-random">
|
<div class="field full js-field-random">
|
||||||
<label>Random replies</label>
|
<label>Random replies</label>
|
||||||
@ -138,7 +138,7 @@
|
|||||||
<% if (command.random.rng !== null) { %><span class="hint">Example roll: <%= command.random.rng %></span><% } %>
|
<% if (command.random.rng !== null) { %><span class="hint">Example roll: <%= command.random.rng %></span><% } %>
|
||||||
<% if (command.random.error) { %><span class="command-preview-unavailable"><%= command.random.error %></span><% } %>
|
<% if (command.random.error) { %><span class="command-preview-unavailable"><%= command.random.error %></span><% } %>
|
||||||
<% } else if (command.mode === "conditional") { %>
|
<% } else if (command.mode === "conditional") { %>
|
||||||
<span><strong><%= command.conditional.replies.length %> keyword repl<%= command.conditional.replies.length === 1 ? "y" : "ies" %></strong></span>
|
<span><strong><%= command.conditional.replies.length %> reply rule<%= command.conditional.replies.length === 1 ? "" : "s" %></strong></span>
|
||||||
<span class="hint"><%= command.conditional.replies.map((reply) => reply.keyword).join(", ") %></span>
|
<span class="hint"><%= command.conditional.replies.map((reply) => reply.keyword).join(", ") %></span>
|
||||||
<% } else if (command.preview.status === "ready") { %>
|
<% } else if (command.preview.status === "ready") { %>
|
||||||
<% const previewId = `command-preview-${command.id}`; %>
|
<% const previewId = `command-preview-${command.id}`; %>
|
||||||
@ -239,7 +239,7 @@
|
|||||||
<div class="command-random-replies" data-conditional-replies>
|
<div class="command-random-replies" data-conditional-replies>
|
||||||
<% (command.conditional.replies.length ? command.conditional.replies : [{ keyword: "", response: "" }]).forEach((reply) => { %>
|
<% (command.conditional.replies.length ? command.conditional.replies : [{ keyword: "", response: "" }]).forEach((reply) => { %>
|
||||||
<div class="command-random-row command-conditional-row" data-conditional-reply-row>
|
<div class="command-random-row command-conditional-row" data-conditional-reply-row>
|
||||||
<input name="conditional_keyword" value="<%= reply.keyword %>" placeholder="backseating" aria-label="Keyword" />
|
<input name="conditional_keyword" value="<%= reply.keyword %>" placeholder="backseat, backseating" aria-label="Keywords, separated with commas" />
|
||||||
<input name="conditional_response" value="<%= reply.response %>" placeholder="The reply for this keyword" aria-label="Reply" data-placeholder-field="core.custom_commands.static_response" data-placeholder-output-audience="user" />
|
<input name="conditional_response" value="<%= reply.response %>" placeholder="The reply for this keyword" aria-label="Reply" data-placeholder-field="core.custom_commands.static_response" data-placeholder-output-audience="user" />
|
||||||
<button type="button" class="button subtle" data-conditional-remove>Remove</button>
|
<button type="button" class="button subtle" data-conditional-remove>Remove</button>
|
||||||
</div>
|
</div>
|
||||||
@ -247,6 +247,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button type="button" class="button subtle" data-conditional-add>Add keyword</button>
|
<button type="button" class="button subtle" data-conditional-add>Add keyword</button>
|
||||||
<label class="switch"><input type="checkbox" class="switch-input" name="conditional_fuzzy" <%= command.conditional_fuzzy ? "checked" : "" %> /><span class="switch-track" aria-hidden="true"></span><span class="switch-text">Suggest a close keyword when there is a small typo</span></label>
|
<label class="switch"><input type="checkbox" class="switch-input" name="conditional_fuzzy" <%= command.conditional_fuzzy ? "checked" : "" %> /><span class="switch-track" aria-hidden="true"></span><span class="switch-text">Suggest a close keyword when there is a small typo</span></label>
|
||||||
|
<p class="hint">Separate aliases with commas to give them the same reply.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field full js-field-random">
|
<div class="field full js-field-random">
|
||||||
<label>Random replies</label>
|
<label>Random replies</label>
|
||||||
@ -309,8 +310,8 @@
|
|||||||
row.dataset.conditionalReplyRow = "";
|
row.dataset.conditionalReplyRow = "";
|
||||||
const keyword = document.createElement("input");
|
const keyword = document.createElement("input");
|
||||||
keyword.name = "conditional_keyword";
|
keyword.name = "conditional_keyword";
|
||||||
keyword.placeholder = "keyword";
|
keyword.placeholder = "keyword, alias";
|
||||||
keyword.setAttribute("aria-label", "Keyword");
|
keyword.setAttribute("aria-label", "Keywords, separated with commas");
|
||||||
const response = document.createElement("input");
|
const response = document.createElement("input");
|
||||||
response.name = "conditional_response";
|
response.name = "conditional_response";
|
||||||
response.placeholder = "The reply for this keyword";
|
response.placeholder = "The reply for this keyword";
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Lumi Core",
|
"name": "Lumi Core",
|
||||||
"version": "0.2.19",
|
"version": "0.2.20",
|
||||||
"channel": "stable",
|
"channel": "stable",
|
||||||
"released_at": "2026-07-19",
|
"released_at": "2026-07-19",
|
||||||
"compatible_from": "0.1.9",
|
"compatible_from": "0.1.9",
|
||||||
@ -8,7 +8,7 @@
|
|||||||
"replaces_versions": [
|
"replaces_versions": [
|
||||||
"1.2.0"
|
"1.2.0"
|
||||||
],
|
],
|
||||||
"migration_notes": "Improves command-policy administration with a protected inherited default group, conditional effective-value summaries, fuzzy searching and filters, clearer command origins, and atomic multi-command saves. Existing commands, policies, groups, usage totals, overlays, website URLs, CSS, tokens, scenes, sources, settings, databases, logs, plugin data, community knowledge, AI models, runtimes, uploads, feedback, and secrets are preserved.",
|
"migration_notes": "Adds comma-separated aliases for conditional command reply rules. Existing commands, policies, groups, usage totals, overlays, website URLs, CSS, tokens, scenes, sources, settings, databases, logs, plugin data, community knowledge, AI models, runtimes, uploads, feedback, and secrets are preserved.",
|
||||||
"rollback_safe": true,
|
"rollback_safe": true,
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"Node.js 18 or newer"
|
"Node.js 18 or newer"
|
||||||
@ -253,6 +253,18 @@
|
|||||||
],
|
],
|
||||||
"rollback_safe": true,
|
"rollback_safe": true,
|
||||||
"migration_notes": "Improves command-policy administration with a protected inherited default group, conditional effective-value summaries, fuzzy searching and filters, clearer command origins, and atomic multi-command saves; existing commands, policies, groups, usage totals, overlays, tokens, scenes, sources, settings, databases, plugin data, models, uploads, feedback, and secrets remain preserved."
|
"migration_notes": "Improves command-policy administration with a protected inherited default group, conditional effective-value summaries, fuzzy searching and filters, clearer command origins, and atomic multi-command saves; existing commands, policies, groups, usage totals, overlays, tokens, scenes, sources, settings, databases, plugin data, models, uploads, feedback, and secrets remain preserved."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.2.20",
|
||||||
|
"channel": "stable",
|
||||||
|
"released_at": "2026-07-19",
|
||||||
|
"compatible_from": "0.1.9",
|
||||||
|
"migration_kind": "patch",
|
||||||
|
"replaces_versions": [
|
||||||
|
"1.2.0"
|
||||||
|
],
|
||||||
|
"rollback_safe": true,
|
||||||
|
"migration_notes": "Adds comma-separated aliases for conditional command reply rules; existing commands, policies, groups, usage totals, overlays, tokens, scenes, sources, settings, databases, plugin data, models, uploads, feedback, and secrets remain preserved."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user