diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index 5c0bc64..24965e5 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -823,7 +823,11 @@ "defaultCount": "How many results to return when the user doesn't specify a count (1-20).", "responseMode": "Media: send actual photos. Text: send filenames/links only. Media mode uses more bandwidth.", "botLocale": "Language for command descriptions in Telegram's menu and bot response messages.", - "rateLimits": "Cooldown in seconds between uses of each command category per chat. 0 = no limit." + "rateLimits": "Cooldown in seconds between uses of each command category per chat. 0 = no limit.", + "commandResponses": "Reply templates for each /command. Use {variables} to inject dynamic data.", + "commandErrors": "Fallback messages shown when a command can't run (rate-limited) or returns nothing.", + "commandDescriptions": "Short menu blurbs Telegram shows next to each /command in the chat command picker.", + "commandUsage": "Example invocations rendered inside /help to show users how to call each command." }, "matrixBot": { "titleEmphasis": "matrix", @@ -876,7 +880,9 @@ "noConfigs": "No command template configs yet.", "confirmDelete": "Delete this command template config?", "commandResponses": "Command Responses", - "commandResponsesHint": "Leave a slot empty to use the default hardcoded response." + "commandErrors": "Error Messages", + "commandDescriptions": "Command Descriptions", + "commandUsage": "Usage Examples" }, "commandConfig": { "titleEmphasis": "configs", diff --git a/frontend/src/lib/i18n/ru.json b/frontend/src/lib/i18n/ru.json index 6fa2926..12acfd8 100644 --- a/frontend/src/lib/i18n/ru.json +++ b/frontend/src/lib/i18n/ru.json @@ -823,7 +823,11 @@ "defaultCount": "Сколько результатов возвращать, если пользователь не указал количество (1-20).", "responseMode": "Медиа: отправка фото. Текст: только имена файлов/ссылки. Медиа-режим использует больше трафика.", "botLocale": "Язык описаний команд в меню Telegram и ответов бота.", - "rateLimits": "Кулдаун в секундах между использованиями команд в каждом чате. 0 = без ограничений." + "rateLimits": "Кулдаун в секундах между использованиями команд в каждом чате. 0 = без ограничений.", + "commandResponses": "Шаблоны ответов на каждую /команду. Используйте {переменные} для динамических данных.", + "commandErrors": "Резервные сообщения, когда команда не может выполниться (превышен лимит) или ничего не возвращает.", + "commandDescriptions": "Короткие подписи в меню команд Telegram, которые показываются рядом с каждой /командой.", + "commandUsage": "Примеры вызовов, отображаемые в /help, чтобы показать пользователям как вызывать каждую команду." }, "matrixBot": { "titleEmphasis": "matrix", @@ -876,7 +880,9 @@ "noConfigs": "Шаблонов команд пока нет.", "confirmDelete": "Удалить этот шаблон команд?", "commandResponses": "Ответы команд", - "commandResponsesHint": "Оставьте слот пустым, чтобы использовать ответ по умолчанию." + "commandErrors": "Сообщения об ошибках", + "commandDescriptions": "Описания команд", + "commandUsage": "Примеры использования" }, "commandConfig": { "titleEmphasis": "конфигурации", diff --git a/frontend/src/routes/command-template-configs/+page.svelte b/frontend/src/routes/command-template-configs/+page.svelte index cab6529..0fa18c5 100644 --- a/frontend/src/routes/command-template-configs/+page.svelte +++ b/frontend/src/routes/command-template-configs/+page.svelte @@ -20,6 +20,7 @@ import Modal from '$lib/components/Modal.svelte'; import JinjaEditor from '$lib/components/JinjaEditor.svelte'; import CollapsibleSlot from '$lib/components/CollapsibleSlot.svelte'; + import Hint from '$lib/components/Hint.svelte'; import EntitySelect, { type EntityItem } from '$lib/components/EntitySelect.svelte'; import { getLocaleMeta } from '$lib/locales'; import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte'; @@ -126,11 +127,40 @@ let commandSlots = $derived( allCapabilities[form.provider_type]?.command_slots || [] ); - let filteredCmdSlots = $derived( - slotFilter - ? commandSlots.filter(s => s.name.toLowerCase().includes(slotFilter.toLowerCase()) || s.description.toLowerCase().includes(slotFilter.toLowerCase())) - : commandSlots - ); + + const ERROR_SLOTS = new Set(['rate_limited', 'no_results']); + + /** + * Group command slots by purpose so the form mirrors how notification + * templates are split (event vs scheduled vs settings). + * + * commandResponses — primary reply templates (/start, /help, /status, data slots) + * commandErrors — fallback messages (rate_limited, no_results) + * commandDescriptions — desc_* slots: short menu blurbs in Telegram's command picker + * commandUsage — usage_* slots: invocation examples shown by /help + */ + let commandSlotGroups = $derived([ + { + group: 'commandResponses', + slots: commandSlots.filter(s => + !s.name.startsWith('desc_') && + !s.name.startsWith('usage_') && + !ERROR_SLOTS.has(s.name) + ), + }, + { + group: 'commandErrors', + slots: commandSlots.filter(s => ERROR_SLOTS.has(s.name)), + }, + { + group: 'commandDescriptions', + slots: commandSlots.filter(s => s.name.startsWith('desc_')), + }, + { + group: 'commandUsage', + slots: commandSlots.filter(s => s.name.startsWith('usage_')), + }, + ]); /** Get slot template for current locale, with fallback. */ function getSlotValue(slotName: string): string { @@ -424,93 +454,98 @@ {/if} -
- {t('cmdTemplateConfig.commandResponses')} -

{t('cmdTemplateConfig.commandResponsesHint')}

- - -
- - {t('templateConfig.language')} - -
- { activeLocale = (v as string) || primaryLocale; refreshAllPreviews(); }} - /> -
- {#if form.provider_type} - - {/if} -
- - - {#if commandSlots.length > 4} -
- + +
+ + {t('templateConfig.language')} + +
+ { activeLocale = (v as string) || primaryLocale; refreshAllPreviews(); }} + />
+ {#if form.provider_type} + {/if} +
-
- {#each filteredCmdSlots as slot} - toggleSlot(slot.name)} - > -
- {#if slotPreview[slot.name] && !slotErrors[slot.name]} - + {/if} + {#if getVarsFor(slot.name)} + + {/if} + - {/if} - {#if getVarsFor(slot.name)} - - {/if} - -
- - {#if showPreviewFor.has(slot.name) && slotPreview[slot.name] && !slotErrors[slot.name]} -
-
{@html sanitizePreview(slotPreview[slot.name])}
- {:else} - { setSlotValue(slot.name, v); validateSlot(slot.name, v); }} - rows={3} - errorLine={slotErrorLines[slot.name] || null} - variables={getVarsFor(slot.name) || undefined} - /> - {/if} - {#if slotErrors[slot.name]} - {#if slotErrorTypes[slot.name] === 'undefined'} -

⚠ {t('common.undefinedVar')}: {slotErrors[slot.name]}

+ {#if showPreviewFor.has(slot.name) && slotPreview[slot.name] && !slotErrors[slot.name]} +
+
{@html sanitizePreview(slotPreview[slot.name])}
+
{:else} -

✕ {t('common.syntaxError')}: {slotErrors[slot.name]}{slotErrorLines[slot.name] ? ` (${t('common.line')} ${slotErrorLines[slot.name]})` : ''}

+ { setSlotValue(slot.name, v); validateSlot(slot.name, v); }} + rows={3} + errorLine={slotErrorLines[slot.name] || null} + variables={getVarsFor(slot.name) || undefined} + /> {/if} - {/if} -
- {/each} -
-
+ + {#if slotErrors[slot.name]} + {#if slotErrorTypes[slot.name] === 'undefined'} +

⚠ {t('common.undefinedVar')}: {slotErrors[slot.name]}

+ {:else} +

✕ {t('common.syntaxError')}: {slotErrors[slot.name]}{slotErrorLines[slot.name] ? ` (${t('common.line')} ${slotErrorLines[slot.name]})` : ''}

+ {/if} + {/if} + + {/each} + + + {/if} + {/each}