feat(frontend): group command template slots into 4 logical fieldsets
Mirrors the notification-template page's group layout. Command slots now split by name prefix into Command Responses, Error Messages (rate_limited/no_results), Command Descriptions (desc_*), and Usage Examples (usage_*). Language picker, reset-all, and slot filter are hoisted above the groups so they apply across all fieldsets, and empty groups are hidden so providers without usage_* don't render empty headers. Drops the orphan cmdTemplateConfig.commandResponsesHint i18n key — hints.commandResponses replaces it.
This commit is contained in:
@@ -823,7 +823,11 @@
|
|||||||
"defaultCount": "How many results to return when the user doesn't specify a count (1-20).",
|
"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.",
|
"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.",
|
"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": {
|
"matrixBot": {
|
||||||
"titleEmphasis": "matrix",
|
"titleEmphasis": "matrix",
|
||||||
@@ -876,7 +880,9 @@
|
|||||||
"noConfigs": "No command template configs yet.",
|
"noConfigs": "No command template configs yet.",
|
||||||
"confirmDelete": "Delete this command template config?",
|
"confirmDelete": "Delete this command template config?",
|
||||||
"commandResponses": "Command Responses",
|
"commandResponses": "Command Responses",
|
||||||
"commandResponsesHint": "Leave a slot empty to use the default hardcoded response."
|
"commandErrors": "Error Messages",
|
||||||
|
"commandDescriptions": "Command Descriptions",
|
||||||
|
"commandUsage": "Usage Examples"
|
||||||
},
|
},
|
||||||
"commandConfig": {
|
"commandConfig": {
|
||||||
"titleEmphasis": "configs",
|
"titleEmphasis": "configs",
|
||||||
|
|||||||
@@ -823,7 +823,11 @@
|
|||||||
"defaultCount": "Сколько результатов возвращать, если пользователь не указал количество (1-20).",
|
"defaultCount": "Сколько результатов возвращать, если пользователь не указал количество (1-20).",
|
||||||
"responseMode": "Медиа: отправка фото. Текст: только имена файлов/ссылки. Медиа-режим использует больше трафика.",
|
"responseMode": "Медиа: отправка фото. Текст: только имена файлов/ссылки. Медиа-режим использует больше трафика.",
|
||||||
"botLocale": "Язык описаний команд в меню Telegram и ответов бота.",
|
"botLocale": "Язык описаний команд в меню Telegram и ответов бота.",
|
||||||
"rateLimits": "Кулдаун в секундах между использованиями команд в каждом чате. 0 = без ограничений."
|
"rateLimits": "Кулдаун в секундах между использованиями команд в каждом чате. 0 = без ограничений.",
|
||||||
|
"commandResponses": "Шаблоны ответов на каждую /команду. Используйте {переменные} для динамических данных.",
|
||||||
|
"commandErrors": "Резервные сообщения, когда команда не может выполниться (превышен лимит) или ничего не возвращает.",
|
||||||
|
"commandDescriptions": "Короткие подписи в меню команд Telegram, которые показываются рядом с каждой /командой.",
|
||||||
|
"commandUsage": "Примеры вызовов, отображаемые в /help, чтобы показать пользователям как вызывать каждую команду."
|
||||||
},
|
},
|
||||||
"matrixBot": {
|
"matrixBot": {
|
||||||
"titleEmphasis": "matrix",
|
"titleEmphasis": "matrix",
|
||||||
@@ -876,7 +880,9 @@
|
|||||||
"noConfigs": "Шаблонов команд пока нет.",
|
"noConfigs": "Шаблонов команд пока нет.",
|
||||||
"confirmDelete": "Удалить этот шаблон команд?",
|
"confirmDelete": "Удалить этот шаблон команд?",
|
||||||
"commandResponses": "Ответы команд",
|
"commandResponses": "Ответы команд",
|
||||||
"commandResponsesHint": "Оставьте слот пустым, чтобы использовать ответ по умолчанию."
|
"commandErrors": "Сообщения об ошибках",
|
||||||
|
"commandDescriptions": "Описания команд",
|
||||||
|
"commandUsage": "Примеры использования"
|
||||||
},
|
},
|
||||||
"commandConfig": {
|
"commandConfig": {
|
||||||
"titleEmphasis": "конфигурации",
|
"titleEmphasis": "конфигурации",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
import Modal from '$lib/components/Modal.svelte';
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
import JinjaEditor from '$lib/components/JinjaEditor.svelte';
|
import JinjaEditor from '$lib/components/JinjaEditor.svelte';
|
||||||
import CollapsibleSlot from '$lib/components/CollapsibleSlot.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 EntitySelect, { type EntityItem } from '$lib/components/EntitySelect.svelte';
|
||||||
import { getLocaleMeta } from '$lib/locales';
|
import { getLocaleMeta } from '$lib/locales';
|
||||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||||
@@ -126,11 +127,40 @@
|
|||||||
let commandSlots = $derived<SlotDef[]>(
|
let commandSlots = $derived<SlotDef[]>(
|
||||||
allCapabilities[form.provider_type]?.command_slots || []
|
allCapabilities[form.provider_type]?.command_slots || []
|
||||||
);
|
);
|
||||||
let filteredCmdSlots = $derived(
|
|
||||||
slotFilter
|
const ERROR_SLOTS = new Set(['rate_limited', 'no_results']);
|
||||||
? commandSlots.filter(s => s.name.toLowerCase().includes(slotFilter.toLowerCase()) || s.description.toLowerCase().includes(slotFilter.toLowerCase()))
|
|
||||||
: commandSlots
|
/**
|
||||||
);
|
* 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. */
|
/** Get slot template for current locale, with fallback. */
|
||||||
function getSlotValue(slotName: string): string {
|
function getSlotValue(slotName: string): string {
|
||||||
@@ -424,10 +454,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<fieldset class="border border-[var(--color-border)] rounded-md p-3">
|
|
||||||
<legend class="text-sm font-medium px-1">{t('cmdTemplateConfig.commandResponses')}</legend>
|
|
||||||
<p class="text-xs text-[var(--color-muted-foreground)] mb-2">{t('cmdTemplateConfig.commandResponsesHint')}</p>
|
|
||||||
|
|
||||||
<!-- Language picker -->
|
<!-- Language picker -->
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
<span class="text-xs font-medium text-[var(--color-muted-foreground)] shrink-0">
|
<span class="text-xs font-medium text-[var(--color-muted-foreground)] shrink-0">
|
||||||
@@ -453,14 +479,21 @@
|
|||||||
|
|
||||||
<!-- Slot filter -->
|
<!-- Slot filter -->
|
||||||
{#if commandSlots.length > 4}
|
{#if commandSlots.length > 4}
|
||||||
<div class="mb-3">
|
<div>
|
||||||
<input type="text" bind:value={slotFilter} placeholder={t('templateConfig.filterSlots')}
|
<input type="text" bind:value={slotFilter} placeholder={t('templateConfig.filterSlots')}
|
||||||
class="w-full px-3 py-1.5 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
class="w-full px-3 py-1.5 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="space-y-2">
|
{#each commandSlotGroups.filter(g => g.slots.length > 0) as group}
|
||||||
{#each filteredCmdSlots as slot}
|
{@const filteredSlots = slotFilter ? group.slots.filter(s => s.name.toLowerCase().includes(slotFilter.toLowerCase()) || s.description.toLowerCase().includes(slotFilter.toLowerCase())) : group.slots}
|
||||||
|
{#if filteredSlots.length > 0}
|
||||||
|
<fieldset class="border border-[var(--color-border)] rounded-md p-3">
|
||||||
|
<legend class="text-sm font-medium px-1">
|
||||||
|
{t(`cmdTemplateConfig.${group.group}`)}<Hint text={t(`hints.${group.group}`)} />
|
||||||
|
</legend>
|
||||||
|
<div class="space-y-2 mt-2">
|
||||||
|
{#each filteredSlots as slot}
|
||||||
<CollapsibleSlot
|
<CollapsibleSlot
|
||||||
label={slot.name}
|
label={slot.name}
|
||||||
description="/{slot.name} — {slot.description}"
|
description="/{slot.name} — {slot.description}"
|
||||||
@@ -511,6 +544,8 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
<button type="submit" class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
|
<button type="submit" class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
|
||||||
{editing ? t('common.save') : t('common.create')}
|
{editing ? t('common.save') : t('common.create')}
|
||||||
|
|||||||
Reference in New Issue
Block a user