feat: per-category greeting menu in bot config

Add a "Por Categoría" tab in the bot config UI where each permission
category (1, 2, 3) can be assigned its own greeting button menu.
When a user writes without an active context, the bot shows the menu
configured for their category instead of falling through to the global
greeting text.

NormalBot.process() now checks per_type[X].greeting_menu first, which
directly references a menu key and serves as both greeting and fallback
for that category. Fully backward-compatible with existing greeting_flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-28 13:04:29 -05:00
co-authored by Claude Sonnet 4.6
parent 0b851208ea
commit 32e2a2a82e
3 changed files with 64 additions and 0 deletions
+36
View File
@@ -2372,6 +2372,34 @@ HTML;
$flows = $config['flows'] ?? [];
$aiProviderOpenai = $aiProviderVal === 'openai' ? ' selected' : '';
$aiProviderMock = $aiProviderVal === 'mock' ? ' selected' : '';
// Per-category config HTML
$perTypeConfig = $config['per_type'] ?? [];
$menuKeysList = array_keys($menus);
$catLabels = ['1' => 'Categoría 1 — Solo recibe info', '2' => 'Categoría 2 — Solo descarga informes', '3' => 'Categoría 3 — Reporta y recibe'];
$categoryTabHtml = '';
foreach ([1, 2, 3] as $cat) {
$catCfg = $perTypeConfig[(string)$cat] ?? [];
$selMenu = $catCfg['greeting_menu'] ?? '';
$opts = '<option value="">— Usa configuración global —</option>';
foreach ($menuKeysList as $mk) {
$s = $selMenu === $mk ? ' selected' : '';
$opts .= '<option value="' . self::h($mk) . '"' . $s . '>' . self::h($mk) . '</option>';
}
$label = $catLabels[(string)$cat];
$categoryTabHtml .= <<<CATHTML
<div style="background:#f8f9fd;border:1px solid #eef1f5;border-radius:10px;padding:18px;margin-bottom:16px">
<div style="font-weight:700;font-size:13px;color:#0b3d91;margin-bottom:12px">{$label}</div>
<div class="form-group" style="margin-bottom:0">
<label>Menú de bienvenida (greeting menu)</label>
<select name="per_type_menu[{$cat}]" style="width:100%;padding:9px 12px;border:1px solid #e2e5ea;border-radius:8px;font-size:13px">
{$opts}
</select>
<div style="font-size:11px;color:#7a8291;margin-top:4px">Menú en botones que se muestra cuando el usuario escribe por primera vez o sin contexto activo</div>
</div>
</div>
CATHTML;
}
$aiModelMini = $aiModelVal === 'gpt-4o-mini' ? ' selected' : '';
$aiModel4o = $aiModelVal === 'gpt-4o' ? ' selected' : '';
$aiModel35 = $aiModelVal === 'gpt-3.5-turbo' ? ' selected' : '';
@@ -2384,6 +2412,7 @@ HTML;
<button type="button" class="tab-btn active" onclick="switchTab('commands',this)">📋 Comandos</button>
<button type="button" class="tab-btn" onclick="switchTab('menus',this)">📑 Menús</button>
<button type="button" class="tab-btn" onclick="switchTab('flows',this)">🔀 Flujos</button>
<button type="button" class="tab-btn" onclick="switchTab('categories',this)">👥 Por Categoría</button>
<button type="button" class="tab-btn" onclick="switchTab('ai',this)">🤖 IA</button>
<button type="button" class="tab-btn" onclick="switchTab('general',this)">⚙️ General</button>
</div>
@@ -2551,6 +2580,13 @@ HTML;
<button type="button" class="add-btn" onclick="addFlow()">+ Agregar flujo</button>
</div>
<!-- ─── POR CATEGORÍA ────────────────────────────────────────────────── -->
<div class="tab-content" id="tab-categories">
<div class="card-h">👥 Configuración por Categoría</div>
<p style="font-size:12px;color:#7a8291;margin-bottom:16px">Define qué menú de bienvenida ve cada categoría de usuario cuando escribe por primera vez o sin contexto activo. El menú debe ser de tipo <strong>botón</strong>.</p>
{$categoryTabHtml}
</div>
<!-- ─── AI ───────────────────────────────────────────────────────────── -->
<div class="tab-content" id="tab-ai">
<div class="card-h">🤖 Inteligencia Artificial</div>
+15
View File
@@ -410,6 +410,21 @@ $routes = [
$aiProvider = trim($_POST['ai_provider'] ?? '');
if ($aiProvider !== '') $config['ai_provider'] = $aiProvider;
// Per-category menus
$perTypeMenus = $_POST['per_type_menu'] ?? [];
$perType = [];
foreach ([1, 2, 3] as $cat) {
$mk = trim($perTypeMenus[$cat] ?? '');
if ($mk !== '') {
$perType[(string)$cat] = [
'greeting_menu' => $mk,
'greeting' => '',
'fallback' => '',
];
}
}
if (!empty($perType)) $config['per_type'] = $perType;
// General
$greeting = trim($_POST['greeting'] ?? '');
if ($greeting !== '') $config['greeting'] = $greeting;
+13
View File
@@ -46,6 +46,13 @@ class NormalBot
return null;
}
// Per-type greeting_menu: direct menu for this category (greeting + fallback in one)
$greetingMenuKey = $perType['greeting_menu'] ?? null;
if ($greetingMenuKey !== null && $currentNode === null && isset($menus[$greetingMenuKey])) {
ConversationContext::updateNode($ctxId, null);
return self::buildMenuResponse($menus[$greetingMenuKey], $context['from'], $company);
}
$greeting = $perType['greeting'] ?? $config['greeting'] ?? null;
if ($greeting !== null && $currentNode === null) {
$greetingFlowId = $perType['greeting_flow'] ?? 'greeting';
@@ -65,6 +72,12 @@ class NormalBot
return self::handleFlow($flows[$fallbackFlowId], $context, $company, $ctxId, $menus);
}
// greeting_menu also serves as fallback when nothing else matched
if ($greetingMenuKey !== null && isset($menus[$greetingMenuKey])) {
ConversationContext::updateNode($ctxId, null);
return self::buildMenuResponse($menus[$greetingMenuKey], $context['from'], $company);
}
$fallback = $perType['fallback'] ?? $config['fallback'] ?? null;
if ($fallback !== null) {
ConversationContext::updateNode($ctxId, null);