- MediaTranscriber: if whisper_url set, POSTs to own server with
Basic auth (audio_file field); falls back to OpenAI cloud Whisper
- Admin AI tab: new section for whisper_url / whisper_user / whisper_pass
- save-ai endpoint: saves the three whisper fields to config_json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allows fields to declare a skip_if condition so the bot skips them
based on previously collected values — used for eps/diagnostico fields
that only apply when novedad is an incapacidad type.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NormalBot.process() gains suppressFallback param. When true, steps 7-8
(greeting menu fallback, static fallback text) return null instead of
responding, letting BotRouter hand off to NLU/AI.
BotRouter.runHybridBot() passes suppressFallback=$nluEnabled so that:
- NLU on: unrecognized text → NLU → route to flow or AI chat
- NLU off: existing fallback behavior unchanged
- Commands, active flows, first-session greeting: always unaffected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AiLogger.php: new service, inserts into ai_logs on every AI call
- ai_logs table: added to migrate.php (provider, model, call_type, tokens, duration_ms)
- AiBot: logs chat, media_response, nlu calls with timing and token counts
- MediaTranscriber: logs whisper, geminiAudio, openaiVision, geminiVision, claudeVision
- /admin/ai-logs: paginated table with provider/company filter
- Layout.php: added IA Logs nav link
- index.php: require AiLogger before AiBot; added GET /admin/ai-logs route
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WpWebhook: inject media_id into context before calling BotRouter so
MediaTranscriber can download audio/image (was passing caption='')
- BotRouter: use context['media_id'] for transcription (non-breaking fallback)
- AiBot::processMedia: fix canUpload to include permType 1 (solo reporta)
- AiBot::buildFlowCatalog: include text flows that have nlu_description set
- DashboardController + index.php: add nlu_description field per flow card
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of relying on the native HTML form (which couldn't reliably
isolate AI fields), the IA tab now uses an AJAX call identical to
save-per-type. The new /admin/bot-config/save-ai endpoint only
reads and merges AI fields into config_json, preserving menus/flows.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each tab (Comandos, Menús, Flujos, IA, General) now has its own
submit button so the user doesn't have to scroll to the bottom.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The JS FormData was built from the CONFIG object (DB state), missing
all AI inputs the user types: api_key, models, checkboxes (nlu,
ai_for_media). Now reads them directly from DOM before the fetch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NluRouter in AiBot: builds permission-filtered flow catalog, asks AI
to route user intent to exact flow key or return chat response
- MediaTranscriber: downloads WA media, transcribes audio (Whisper/Gemini),
extracts image intent (OpenAI/Gemini/Claude vision)
- BotRouter hybrid: text → NormalBot → NLU fallback if no match;
audio/image → transcribe → NLU route; sends transcript preview first
- Admin IA tab: NLU checkbox with description
- Per-company nlu flag saved in config_json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Global settings: custom AI panel with OpenAI/Gemini/Claude each with
API key, model selector, and live test-connection button (no save needed)
- Per-company IA tab: same three providers + fallback to global when empty
- AiBot: reads per-company config_json first, falls back to env() global
- Added callClaude() for Anthropic Messages API
- More OpenAI models: gpt-4.1, gpt-4.1-mini
- test-ai endpoint: accepts provider/api_key/model from POST for live test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added gemini-2.5-flash to the admin panel selector (first/default option)
and updated the fallback default in AiBot and migrate seed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After migration stripped base URLs from DB records, bot was calling
relative paths (e.g. /ruta) as-is without the base. buildUrl() helper
now prepends api_base_url for all four curl call sites.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After user selects a company, send short confirmation text then reset
bot_context and trigger BotRouter with empty message to fire the greeting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cuando un número pertenece a más de una empresa, el bot muestra un
menú interactivo "¿Con cuál empresa deseas comunicarte?" antes de
cualquier flujo normal. La selección se cachea 8h en la tabla
multi_company_sessions. Keywords de reinicio (salir, inicio, menú...)
limpian la sesión y vuelven a mostrar el pre-menú.
Números de una sola empresa: flujo intacto.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Recorre company_endpoints y elimina el prefijo api_base_url de
las URLs absolutas, dejando solo la ruta relativa.
Dry-run por defecto (?run=1 para ejecutar).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Banner en tab Endpoints muestra la URL base de General
- Inputs de URL aceptan rutas relativas (?param=x o /ruta)
- Preview live de la URL completa (base+ruta) bajo cada input
- Backend prepend api_base_url en test y sync si URL no es absoluta
- Compatible hacia atrás: URLs absolutas existentes siguen funcionando
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Agrega panel de clonación en la pestaña Endpoints API del editor de empresa.
Permite copiar los endpoints de otra empresa con dos modos: sobreescribir todo
o solo añadir los endpoints que aún no tienen URL configurada.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- dynamic_list now stores valid IDs in collecting state; rejects
free-text input that doesn't match a list option
- Fixed submenu_ciclo_cosecha button IDs to match real flow names
(ciclo_cosecha_dia / ciclo_cosecha_historico)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add last_month date_mode to executeApiReport
- prod_kilos_lotes now shows period menu instead of immediately asking dates
- lotes_mes_actual / lotes_mes_anterior use api_report with date_mode
- lotes_personalizado collects fecha_desde + fecha_hasta (2 messages only when needed)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace onchange+AJAX with explicit button '💾 Guardar categorías'
that shows alert on success/error for clear feedback
- Get company_id from DOM (not COMPANY_ID JS const from another script block)
- Main form save now preserves welcome_menu and per_type from DB even if
POST is truncated by max_input_vars
- Restore welcome_menu='welcome_msg' in DB (was wiped by a previous save)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The main save rebuilt config from scratch — per_type always started empty,
so clicking Guardar wiped whatever savePerType() had just stored.
Now: load existing DB config first, use its per_type as the base, apply
POST per_type_menu on top only when it is present. Same for welcome_menu.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The main form exceeded max_input_vars (27 flows × 37 fields each) so PHP
silently dropped per_type_menu before reaching the save handler.
- Add POST /admin/bot-config/save-per-type — reads existing DB config,
merges only the per_type_menu values, saves back
- Per-type selects fire savePerType() onchange → AJAX → toast on success
- Add public/.user.ini with max_input_vars=5000 as belt+suspenders
- Remove array_is_list() (PHP 8.1) — replaced with PHP 7.4 compatible check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Store PHP sessions in the `sessions` MySQL table (30-day TTL) instead of
server files, which disappear on container restart / redeploy.
Cookie lifetime also extended to 30 days.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Show welcome_msg (button with "Ver menú") for all new sessions before category menu
- Fix saveConfig() AJAX to include per_type_menu DOM values so they survive individual saves
- Add welcome_menu field to General tab in admin (render + save)
- Add 'atras' command alias (→ show_main_menu)
- Add ciclos_plagas, ciclos_palm, ciclos_tratamientos flows to DB config
- Fix per_type in DB from [] to {} (proper JSON object)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When user says "menu/inicio/volver", bot now shows the menu configured
for their category (per_type.greeting_menu) instead of the global
show_main_menu. Same redirect applies to interactive button taps.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WhatsApp rejects list messages with empty body.text. Using ?? missed
empty string from config; ?: correctly falls back to default.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
API returns {status,datos:[...]} but bot was looking for {data:[...]},
causing empty rows and dynamic lists never rendering.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixed vars use their preset (month_start, current_date, etc.); ask-mode
vars fall back to today's date so the test always sends a valid URL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Admin: each endpoint with {vars} shows a vars config section; per-var
mode is "ask user" (bot prompts in WhatsApp) or "fixed" (preset: today,
month_start, month_end, last_7, last_30, year_start, or literal)
- Bot: resolves fixed vars before calling endpoint; queues "ask" vars and
collects them sequentially from the user before executing the report
- date_mode now uses fecha_desde/fecha_hasta (ERP expected names); added
current_month mode
- JSON responses from ERP (sin_datos etc.) shown as text, not uploaded as file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Complete endpoint specification for the ERP team:
- 10 GET (download) endpoints returning PDF/Excel files
- 6 POST (upload) endpoints receiving WhatsApp form data
- Auth headers, request/response schemas, and endpoint_key mapping table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
saveConversation() now returns bool; BotRouter only runs when the
message_id is new (INSERT IGNORE rowCount>0). Prevents WhatsApp
webhook retries from triggering duplicate bot responses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes:
1. Any command (menu, salir, etc.) now clears collecting/__foreach/__cap
meta before executing, so navigation commands are always a clean reset
regardless of what flow the user was in.
2. DB per_type was empty ([]); rebuilt with correct structure:
- Type 1: greeting=submenu_subir, menu/inicio/volver → recibir_info
- Type 2: greeting=submenu_informes, menu/inicio/volver → descargar_informes
- Type 3: greeting=show_main_menu, inherits global commands
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Changed commands merge from ?? (replace) to array_merge (extend) so that
per-type overrides for menu/inicio/volver are applied without losing the
global salir, exit, informes, info commands.
Also updated DB config per_type:
- Type 1 (subir info): menu/inicio/volver → recibir_info → submenu_subir
- Type 2 (descargar): menu/inicio/volver → descargar_informes → submenu_informes
- Type 3 (admin): menu → show_main_menu (inherits global)
Each category now lands on its own menu from the first message.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of defining fields inline per flow, the admin now defines them
once on the endpoint (direction=upload). The bot reads body_fields from
company_endpoints at runtime, so the same endpoint can be reused by
multiple collect_and_post flows without duplicating field config.
DB: ALTER TABLE company_endpoints ADD COLUMN body_fields TEXT DEFAULT NULL
Changes:
- NormalBot: falls back to endpoint.body_fields when flow.fields is empty
- DashboardController: endpoint rows show a body_fields section for
upload endpoints (key / label / prompt per field, add/remove rows)
- companyEndpointSave: reads + validates body_fields JSON and persists it
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New flow type that asks the user for a configured list of fields one
by one, shows a summary with confirm/cancel, then POSTs all answers
as JSON to the configured endpoint with Bearer auth.
Config example:
{
"type": "collect_and_post",
"endpoint_key": "subir_cosecha",
"header": "🌿 Ciclos Cosecha",
"confirm": true,
"success_text": "✅ Ciclo registrado.",
"fields": [
{"key":"fecha", "label":"Fecha", "prompt":"📅 ¿Fecha del ciclo? (YYYY-MM-DD)"},
{"key":"cantidad", "label":"Racimos", "prompt":"🔢 ¿Cantidad de racimos?"},
{"key":"finca", "label":"Finca", "prompt":"🏡 ¿Nombre de la finca?"}
]
}
POST payload: {fecha, cantidad, finca, telefono, nombre}
Also adds the UI panel in bot-config with dynamic add/remove field rows.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The checkbox-to-0 logic was running on every save including partial
saves (e.g. bot-config only sends config_json). This silently set
is_active=0 every time the bot config was saved.
Now checkbox defaults only apply when 'name' is present in the data,
which is the case only for full company-form saves.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dropdown option '🌧 Registro por ítem' in both PHP-rendered cards
and the addFlowCard() JS template
- Fields panel: endpoint lista, endpoint upload, campo ID, campo nombre,
encabezado, pregunta por ítem, mensaje de éxito
- flowTabChange() now shows/hides ftg-foreach panel
- Save handler in index.php parses all flow_fe_* POST fields and
builds the collect_for_each config object
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New flow type that fetches a dynamic list from an endpoint then asks
the user for a value for each item one by one. On completion shows a
summary with confirm/cancel buttons, then POSTs all collected data as
a JSON array to a configured upload endpoint with Bearer + X-API-Key auth.
Usage in flow config:
{
"type": "collect_for_each",
"source_endpoint_key": "fincas_list",
"endpoint_key": "pluvio_upload",
"value_field": "id",
"label_field": "name",
"question": "¿Cuál es el valor de pluviometría? (mm)",
"header": "🌧 Pluviometría",
"success_text": "✅ Pluviometría registrada."
}
POST payload: [{id, label, valor}, ...]
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creating a company with the Activa checkbox unchecked (or missing)
set is_active=0, silently disabling the bot for that company.
New companies now default to active; existing companies keep their
current value when the form loads.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: WpWebhook called OutboundWorker::processQueue() after every
incoming message. BotRouter already sends synchronously in enqueueResponse();
failed messages stayed status='queued'. On the next incoming message
processQueue() would re-send ALL previously-failed messages, causing
the bot to repeat old responses.
- Remove OutboundWorker::processQueue() from WpWebhook::processEvent()
BotRouter handles immediate delivery; retries should be manual/cron only
- Fix chatSend(): instead of queuing + processQueue() (which drains ALL
queued messages), call WhatsAppSender::sendText() directly for the
one message being sent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>