Nexo Bot
Nexo Bot
Configuración del Bot
CENTRO DE CONFIGURACIÓN

Configura el comportamiento real de tu bot.

Cargando...

Configurador guiado

Aquí configuras el bot por pasos. El sistema convierte esto automáticamente a nodos compatibles con tu motor actual.

1) Bienvenida

Este es el primer mensaje que verá el cliente.

2) Menú principal

Configura las opciones visibles del menú principal.



3) Segmentación opcional

Sirve para filtrar y priorizar clientes antes de pedir sus datos.

4) Captura de datos del cliente

Estas preguntas se harán antes de agendar la cita.

5) Citas y horarios

Define cómo el bot manejará la agenda.

6) Información de servicios

Este mensaje se envía cuando el cliente elige “Información sobre nuestros servicios”.

7) Confirmación final

Este mensaje se envía al final del flujo.

Puedes usar variables como {{name}}, {{phone}}, {{email}}, {{client_type}}, {{appointment_type}}, {{schedule}}, {{service_category}}, {{service_request}}.

Vista previa del menú principal

+ rentCarMoney(client.amount) + '', '
Frecuencia: ' + (client.frequency === "biweekly" ? "Quincenal" : client.frequency === "monthly" ? "Mensual" : "Semanal") + '
', '
Próximo pago: ' + due + '
', '
Días atraso: ' + debt.daysLate + '
', '
Mora: RD const customAgentPanel = document.getElementById("customAgentPanel"); const customAgentSummary = document.getElementById("customAgentSummary"); const customAgentHero = document.getElementById("customAgentHero"); const customAgentHeroText = document.getElementById("customAgentHeroText"); const customAgentTag = document.getElementById("customAgentTag"); const genericConfigurator = document.getElementById("genericConfigurator"); const donPopeBoard = document.getElementById("donPopeBoard"); const valeAcademyBoard = document.getElementById("valeAcademyBoard"); const discoveryWayBoard = document.getElementById("discoveryWayBoard"); if (!isCustomWorkspace()) { customAgentPanel.classList.add("hidden"); customAgentHero.classList.add("hidden"); donPopeBoard.classList.add("hidden"); valeAcademyBoard.classList.add("hidden"); discoveryWayBoard.classList.add("hidden"); genericConfigurator.classList.remove("hidden"); flowBadge.textContent = "Flow V3"; return; } customAgentPanel.classList.remove("hidden"); customAgentHero.classList.remove("hidden"); genericConfigurator.classList.add("hidden"); donPopeBoard.classList.add("hidden"); valeAcademyBoard.classList.add("hidden"); discoveryWayBoard.classList.add("hidden"); if (isDiscoveryWayWorkspace()) { flowBadge.textContent = "Modo Discovery Way"; customAgentTag.textContent = "discovery-way-sales-v1"; customAgentHeroText.textContent = "Este workspace usa IA de ventas para tours, aprendizaje del estilo del dueño y excursiones con copy + foto cargadas desde panel."; discoveryWayBoard.classList.remove("hidden"); customAgentSummary.innerHTML = `
• Carga excursiones con copy completo e imagen desde este panel.
• La IA decide cuál excursión mostrar según la conversación.
• Aprende de respuestas humanas reales del negocio.
• Guarda memoria visible y estilo comercial.
• Todo queda aislado solo en este workspace.
`; return; } if (isDonPopeWorkspace()) { flowBadge.textContent = "Modo Don Pope"; customAgentTag.textContent = "don-pope-v1"; customAgentHeroText.textContent = "Este workspace tiene reglas exclusivas aplicadas en backend para Don Pope. Aquí ves la lógica real que utiliza el bot, no la plantilla genérica."; donPopeBoard.classList.remove("hidden"); customAgentSummary.innerHTML = `
• Consultas financieras: pasan directamente al encargado.
• Compra, venta o cambio de equipos: atención presencial obligatoria.
• Problemas técnicos: evaluación presencial en tienda.
• Ubicación: respuesta automática configurada.
• Aprendizaje del bot: activo y exclusivo para este negocio.
`; return; } if (isValeAcademyWorkspace()) { flowBadge.textContent = "Modo Vale Academy"; customAgentTag.textContent = "vale-academy-v1"; customAgentHeroText.textContent = "Este workspace usa una lógica comercial personalizada para responder con información real de Vale Academy, empujar a la inscripción y mostrar al cliente su flujo exclusivo."; valeAcademyBoard.classList.remove("hidden"); customAgentSummary.innerHTML = `
• Bienvenida comercial personalizada para música e inglés.
• Precios fijos: inscripción RD$499 y mensualidad RD$990.
• Datos de pago automáticos cuando el prospecto los solicita.
• Respuestas por horarios, programas y beneficios.
• Cierre enfocado en reservar el cupo y enviar comprobante.
`; return; } flowBadge.textContent = "Modo personalizado"; customAgentTag.textContent = workspace.customAgent || "custom-agent"; customAgentHeroText.textContent = "Este workspace tiene una lógica especial aplicada desde backend. Aquí se muestra la vista personalizada en lugar del configurador genérico."; customAgentSummary.innerHTML = `
• Workspace con agente personalizado activo.
• Las respuestas se gestionan con reglas exclusivas en backend.
• La plantilla normal se oculta para evitar confusión.
`; } function setFormValues(v) { flowEnabled.checked = v.enabled === true; cmdBack.value = v.commands?.back || "0"; cmdMenu.value = v.commands?.menu || "00"; handoffText.value = v.handoffText || "? Perfecto. Te paso con un asesor humano ahora."; aiEnabled.checked = v.ai?.enabled === true; aiMinScore.value = typeof v.ai?.minScore === "number" ? v.ai.minScore : 0.72; setAiTrainingValues(v.ai?.training || {}, true); businessName.value = v.businessName || ""; welcomeText.value = v.welcomeText || ""; mainOption1Label.value = v.mainOption1Label || ""; serviceATitle.value = v.serviceATitle || ""; serviceAOptions.value = Array.isArray(v.serviceAOptions) ? v.serviceAOptions.join("\n") : ""; serviceAQuestion.value = v.serviceAQuestion || ""; mainOption2Label.value = v.mainOption2Label || ""; serviceBTitle.value = v.serviceBTitle || ""; serviceBOptions.value = Array.isArray(v.serviceBOptions) ? v.serviceBOptions.join("\n") : ""; serviceBQuestion.value = v.serviceBQuestion || ""; mainOption3Label.value = v.mainOption3Label || ""; mainOption4Label.value = v.mainOption4Label || ""; mainOption5Label.value = v.mainOption5Label || ""; mainFallbackText.value = v.mainFallbackText || ""; enableCategoryQuestion.checked = v.enableCategoryQuestion === true; categoryQuestionText.value = v.categoryQuestionText || ""; categoryOptions.value = Array.isArray(v.categoryOptions) ? v.categoryOptions.join("\n") : ""; askNameText.value = v.askNameText || ""; askPhoneText.value = v.askPhoneText || ""; askEmailText.value = v.askEmailText || ""; clientTypeQuestionText.value = v.clientTypeQuestionText || ""; clientTypeOptions.value = Array.isArray(v.clientTypeOptions) ? v.clientTypeOptions.join("\n") : ""; appointmentIntroText.value = v.appointmentIntroText || ""; appointmentTypeQuestionText.value = v.appointmentTypeQuestionText || ""; appointmentTypeOptions.value = Array.isArray(v.appointmentTypeOptions) ? v.appointmentTypeOptions.join("\n") : ""; scheduleQuestionText.value = v.scheduleQuestionText || ""; scheduleOptions.value = Array.isArray(v.scheduleOptions) ? v.scheduleOptions.join("\n") : ""; servicesInfoText.value = v.servicesInfoText || ""; confirmationText.value = v.confirmationText || ""; renderPreviewFromForm(); } function loadLegalTemplate() { setFormValues({ enabled: true, commands: { back: "0", menu: "00" }, handoffText: "? Perfecto. Te paso con un asistente ahora.", ai: { enabled: false, fallback: true, minScore: 0.72, training: { enabled: false, prompt: "", notes: "" } }, businessName: "Chevalier, Pérez & Asociados", welcomeText: `Bienvenido a Chevalier, Pérez & Asociados. Somos una firma especializada en servicios jurídicos y contables, brindando asesoría estratégica a particulares, empresas e inversionistas. Seleccione una opción:`, mainOption1Label: "Consulta legal", serviceATitle: "Por favor seleccione el área de su consulta:", serviceAOptions: [ "Derecho inmobiliario", "Derecho civil", "Litigios y demandas", "Derecho comercial / empresas", "Derecho laboral", "Sucesiones y herencias", "Inversión extranjera en República Dominicana", "Otro asunto" ], serviceAQuestion: "Por favor describa brevemente su caso.", mainOption2Label: "Servicios contables / impuestos", serviceBTitle: "Por favor seleccione el servicio que necesita:", serviceBOptions: [ "Declaraciones de impuestos (DGII)", "Contabilidad para empresas", "Constitución de empresas", "Asesoría fiscal", "Registro Nacional de Contribuyente (RNC)", "Reportes financieros", "Igualas contables mensuales", "Otro servicio" ], serviceBQuestion: "Por favor describa brevemente su solicitud.", mainOption3Label: "Agendar una cita", mainOption4Label: "Información sobre nuestros servicios", mainOption5Label: "Hablar con un asistente", mainFallbackText: "? Opción inválida. Por favor seleccione una opción válida del 1 al 5.", enableCategoryQuestion: true, categoryQuestionText: "¿Su consulta está relacionada con:", categoryOptions: [ "Compra o venta de propiedad", "Problema legal / demanda", "Creación o gestión de empresa", "Cumplimiento fiscal / impuestos" ], askNameText: "Para poder asistirle mejor, ¿Cuál es su nombre completo?", askPhoneText: "Por favor indique su número de teléfono.", askEmailText: "Ingrese su correo electrónico.", clientTypeQuestionText: "¿Es usted?", clientTypeOptions: [ "Persona física", "Empresa", "Inversionista extranjero" ], appointmentIntroText: "Un especialista de Chevalier, Pérez & Asociados puede atender su caso mediante una consulta.", appointmentTypeQuestionText: "Seleccione el tipo de cita:", appointmentTypeOptions: [ "Consulta presencial", "Consulta virtual (Zoom / Meet)", "Llamada telefónica" ], scheduleQuestionText: "Seleccione el horario disponible:", scheduleOptions: [ "9:00 AM", "10:00 AM", "11:00 AM", "2:00 PM", "3:00 PM", "4:00 PM" ], servicesInfoText: `En Chevalier, Pérez & Asociados ofrecemos: ? Servicios jurídicos Derecho inmobiliario Litigios civiles y comerciales Contratos y negociaciones Inversión extranjera Sucesiones ?? Servicios contables Declaraciones fiscales ante DGII Contabilidad empresarial Igualas contables Asesoría fiscal Constitución de empresas`, confirmationText: `Gracias por contactar Chevalier, Pérez & Asociados. Su solicitud ha sido recibida y uno de nuestros especialistas confirmará su cita en breve. Si su caso es urgente puede escribir URGENTE.` }); sideMsg.textContent = "? Plantilla jurídica/contable cargada."; } function tryFillFormFromExistingFlow(existingFlow, existingAi) { const nodes = Array.isArray(existingFlow?.nodes) ? existingFlow.nodes : []; const nodeMap = new Map(nodes.map(n => [n.id, n])); const menu = nodeMap.get("menu"); const servicioA = nodeMap.get("servicio_a_menu"); const servicioB = nodeMap.get("servicio_b_menu"); const descA = nodeMap.get("desc_servicio_a"); const descB = nodeMap.get("desc_servicio_b"); const categoria = nodeMap.get("categoria_valor"); const datosNombre = nodeMap.get("datos_nombre"); const datosTel = nodeMap.get("datos_tel"); const datosEmail = nodeMap.get("datos_email"); const tipoCliente = nodeMap.get("tipo_cliente"); const citaTipo = nodeMap.get("cita_tipo"); const citaHorario = nodeMap.get("cita_horario"); const info = nodeMap.get("info_servicios"); const confirmacion = nodeMap.get("confirmacion"); const recognizable = menu && servicioA && servicioB && descA && descB && datosNombre && datosTel && datosEmail && tipoCliente && citaTipo && citaHorario && info && confirmacion; if (!recognizable) { return false; } setFormValues({ enabled: existingFlow.enabled === true, commands: existingFlow.commands || { back: "0", menu: "00" }, handoffText: existingFlow.handoffText || "? Perfecto. Te paso con un asesor humano ahora.", ai: { enabled: existingAi?.enabled === true, fallback: true, minScore: typeof existingAi?.minScore === "number" ? existingAi.minScore : 0.72, training: existingAi?.training || { enabled: false, prompt: "", notes: "" } }, businessName: "", welcomeText: menu.text || "", mainOption1Label: menu.options?.[0]?.label || "", serviceATitle: servicioA.text || "", serviceAOptions: (servicioA.options || []).map(o => o.label).filter(Boolean), serviceAQuestion: descA.text || "", mainOption2Label: menu.options?.[1]?.label || "", serviceBTitle: servicioB.text || "", serviceBOptions: (servicioB.options || []).map(o => o.label).filter(Boolean), serviceBQuestion: descB.text || "", mainOption3Label: menu.options?.[2]?.label || "", mainOption4Label: menu.options?.[3]?.label || "", mainOption5Label: menu.options?.[4]?.label || "", mainFallbackText: menu.fallbackText || "", enableCategoryQuestion: !!categoria, categoryQuestionText: categoria?.text || "", categoryOptions: categoria ? (categoria.options || []).map(o => o.label).filter(Boolean) : [], askNameText: datosNombre.text || "", askPhoneText: datosTel.text || "", askEmailText: datosEmail.text || "", clientTypeQuestionText: tipoCliente.text || "", clientTypeOptions: (tipoCliente.options || []).map(o => o.label).filter(Boolean), appointmentIntroText: "", appointmentTypeQuestionText: citaTipo.text || "", appointmentTypeOptions: (citaTipo.options || []).map(o => o.label).filter(Boolean), scheduleQuestionText: citaHorario.text || "", scheduleOptions: (citaHorario.options || []).map(o => o.label).filter(Boolean), servicesInfoText: info.text || "", confirmationText: confirmacion.text || "" }); return true; } async function load() { sideMsg.textContent = "Cargando..."; try { const data = await window.NexoAPI.apiRequest("/api/bot-config/flow", { auth: true }); workspace = data.workspace || null; syncBehaviorFeaturesUI(); sub.textContent = `${data.workspaceName || "Workspace"} - ${data.timezone || ""}${workspace?.plan ? " - Plan: " + workspace.plan : ""}`; const f = data.botFlow || {}; flow = { enabled: f.enabled === true, startNode: f.startNode || "menu", commands: f.commands || { back: "0", menu: "00" }, handoffText: f.handoffText || "? Perfecto. Te paso con un asesor humano ahora.", nodes: Array.isArray(f.nodes) ? f.nodes : [] }; ai = { enabled: data.ai?.enabled === true, fallback: true, minScore: typeof data.ai?.minScore === "number" ? data.ai.minScore : 0.72, training: data.ai?.training || { enabled: false, prompt: "", normalizedPrompt: "", faqQuestions: "", faqAnswers: "", notes: "", updatedAt: null } }; toggleAiTrainingPanel(); toggleIntelligentFlowEditor(); renderWorkspaceMode(); if (isDiscoveryWayWorkspace()) { resetDiscoveryTourForm(); await loadDiscoveryTours(); await loadDiscoveryMemory(); } const loaded = tryFillFormFromExistingFlow(flow, ai); if (!loaded && !isCustomWorkspace()) { aiEnabled.checked = ai.enabled; aiMinScore.value = ai.minScore; if (!aiTrainingDirty) { setAiTrainingValues(ai.training || {}); } if (isIntelligentPlanWorkspace()) { sideMsg.textContent = "? Modo inteligente activo: las plantillas/flows están desactivadas. Configura el prompt maestro del negocio."; } else { loadLegalTemplate(); sideMsg.textContent = "? El flow actual no coincide con este configurador guiado. Se cargó la plantilla base para que la edites."; } } else { if (!aiTrainingDirty) { setAiTrainingValues(ai.training || {}); } sideMsg.textContent = isCustomWorkspace() ? "? Se cargó la vista real de la lógica personalizada de este workspace." : "? Configuración cargada."; } toggleIntelligentFlowEditor(); renderPreviewFromForm(); } catch (e) { sideMsg.textContent = "? " + (e.message || "Error cargando"); if (e.status === 401) { window.NexoStorage.clearToken(); window.location.href = "./login.html"; } } } btnReload.addEventListener("click", () => { aiTrainingDirty = false; load(); }); btnLoadTemplateLegal.addEventListener("click", () => { if (isIntelligentPlanWorkspace()) { sideMsg.textContent = "Plan inteligente activo: las plantillas/flows no se usan. Edita el prompt maestro de IA."; return; } loadLegalTemplate(); }); btnPreview.addEventListener("click", () => renderPreviewFromForm()); btnLoadAiTrainingTemplate?.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); if (!aiTrainingPrompt) { sideMsg.textContent = "? No se encontró el campo del prompt maestro."; return; } const template = getAiTrainingTemplate(); if (!template || !template.trim()) { sideMsg.textContent = "? No se pudo generar la plantilla sugerida."; return; } const currentValue = String(aiTrainingPrompt.value || "").trim(); if (currentValue && currentValue !== template) { const overwrite = window.confirm( "Ya hay un prompt escrito. ¿Quieres reemplazarlo por la estructura sugerida?" ); if (!overwrite) { sideMsg.textContent = "?? Se mantuvo el prompt actual."; return; } } aiTrainingDirty = true; aiTrainingPrompt.value = template; if (aiTrainingNotes && !String(aiTrainingNotes.value || "").trim()) { aiTrainingNotes.value = "Estructura inicial cargada desde panel para entrenamiento IA."; } if (aiTrainingEnabled && !aiTrainingEnabled.checked) { aiTrainingEnabled.checked = true; } setTimeout(() => { aiTrainingPrompt.value = template; aiTrainingPrompt.focus(); aiTrainingPrompt.setSelectionRange( aiTrainingPrompt.value.length, aiTrainingPrompt.value.length ); }, 0); requestAnimationFrame(() => { aiTrainingPrompt.value = template; }); sideMsg.textContent = "? Se cargó la estructura sugerida correctamente. Ahora revisa el contenido y pulsa Guardar."; }); aiTrainingPrompt?.addEventListener("input", markAiTrainingDirty); aiTrainingNotes?.addEventListener("input", markAiTrainingDirty); aiTrainingEnabled?.addEventListener("change", markAiTrainingDirty); [ flowEnabled, cmdBack, cmdMenu, handoffText, aiEnabled, aiMinScore, behaviorMode, featureDetectHumanTakeover, featurePauseBotOnHumanReply, featureFollowUpsEnabled, featureAcademyMode, featureInventoryMode, featureToursMode, businessName, welcomeText, mainOption1Label, serviceATitle, serviceAOptions, serviceAQuestion, mainOption2Label, serviceBTitle, serviceBOptions, serviceBQuestion, mainOption3Label, mainOption4Label, mainOption5Label, mainFallbackText, enableCategoryQuestion, categoryQuestionText, categoryOptions, askNameText, askPhoneText, askEmailText, clientTypeQuestionText, clientTypeOptions, appointmentIntroText, appointmentTypeQuestionText, appointmentTypeOptions, scheduleQuestionText, scheduleOptions, servicesInfoText, confirmationText ].forEach(el => { el.addEventListener("input", renderPreviewFromForm); el.addEventListener("change", renderPreviewFromForm); }); behaviorMode.addEventListener("change", () => { autoAlignFeaturesByMode(); renderWorkspaceMode(); if (presetSelect) { presetSelect.value = behaviorMode.value; } if (presetMsg) { presetMsg.textContent = ""; } renderPreviewFromForm(); }); btnApplyPreset?.addEventListener("click", () => { const selectedPreset = String(presetSelect?.value || "").trim(); if (!selectedPreset) { presetMsg.textContent = "? Selecciona un preset antes de aplicarlo."; return; } const applied = applyPresetToUI(selectedPreset); if (!applied) { presetMsg.textContent = "? No se pudo aplicar el preset."; return; } renderWorkspaceMode(); renderPreviewFromForm(); const preset = getPresetConfig(selectedPreset); if (presetMsg) { if (preset) { presetMsg.textContent = isCustomWorkspace() ? `? Preset aplicado: ${preset.label}. En este workspace personalizado se guardará la configuración operativa al pulsar Guardar.` : `? Preset aplicado: ${preset.label}. También se rellenó la plantilla guiada. Ahora pulsa Guardar para persistirlo.`; } else { presetMsg.textContent = "? Preset aplicado. Ahora pulsa Guardar para persistirlo."; } } }); btnReloadDiscoveryTours?.addEventListener("click", () => loadDiscoveryTours()); btnReloadDiscoveryMemory?.addEventListener("click", () => loadDiscoveryMemory()); btnSaveDiscoveryTour?.addEventListener("click", () => saveDiscoveryTour()); btnCancelDiscoveryEdit?.addEventListener("click", () => resetDiscoveryTourForm()); btnSave.addEventListener("click", async () => { sideMsg.textContent = "Guardando..."; const behaviorFeatures = getBehaviorFeaturesPayload(); const isCustom = isCustomWorkspace(); const trainingPayload = getAiTrainingPayload(); const shouldAutoApplyTrainingSuggestion = isIntelligentPlanWorkspace() && trainingPayload.enabled === true && String(trainingPayload.prompt || "").trim().length > 0; let builtFlow = flow; let builtAi = { ...(ai || {}), enabled: aiEnabled.checked, fallback: true, minScore: Math.max(0, Math.min(1, Number(aiMinScore.value || 0.72))), training: trainingPayload }; if (!isCustom && !isIntelligentPlanWorkspace()) { const data = getFormData(); const formErr = validateFormData(data); if (formErr) { sideMsg.textContent = "? " + formErr; return; } builtFlow = buildFlowFromForm(data); const flowErr = validateFlow(builtFlow); if (flowErr) { sideMsg.textContent = "? " + flowErr; return; } builtAi = { ...builtAi, ...data.ai, training: trainingPayload }; } try { const body = { features: behaviorFeatures.features, behavior: behaviorFeatures.behavior }; if (isIntelligentPlanWorkspace()) { body.ai = builtAi; } else if (!isCustom) { body.botFlow = builtFlow; body.ai = builtAi; } if (shouldAutoApplyTrainingSuggestion) { body.autoApplyTrainingSuggestion = true; } const response = await window.NexoAPI.apiRequest("/api/bot-config/flow", { method: "PATCH", auth: true, body }); flow = response.botFlow || builtFlow; ai = response.ai || builtAi; workspace = response.workspace || workspace; aiTrainingDirty = false; if (response.trainingSuggestionApplied === true && response.trainingSuggestion) { applyTrainingSuggestionToUI(response.trainingSuggestion); } syncBehaviorFeaturesUI(); toggleAiTrainingPanel(); renderWorkspaceMode(); if (response.trainingSuggestionApplied === true && response.trainingSuggestion) { const detected = String(response.trainingSuggestion.detectedBusinessType || response.trainingSuggestion.behavior?.mode || "generic"); sideMsg.textContent = `? Configuración guardada. El prompt detectó este negocio como "${detected}" y se aplicó automáticamente al panel.`; if (presetMsg) { presetMsg.textContent = `? Sugerencia del prompt aplicada automáticamente: ${detected}. Revisa los toggles y el modo del workspace.`; } } else if (response.trainingSuggestion && response.trainingAutoApplyAllowed === false) { const detected = String(response.trainingSuggestion.detectedBusinessType || response.trainingSuggestion.behavior?.mode || "generic"); sideMsg.textContent = `? Configuración guardada. El prompt sugiere "${detected}", pero este workspace usa lógica personalizada y no se aplicó automáticamente.`; if (presetMsg) { presetMsg.textContent = `? Sugerencia detectada por el prompt: ${detected}. No se aplicó automáticamente porque este workspace es personalizado.`; } } else { sideMsg.textContent = isCustom ? (isIntelligentPlanWorkspace() ? "? Configuración operativa y entrenamiento IA guardados." : "? Configuración operativa guardada.") : "? Guardado."; if (presetMsg && !String(presetMsg.textContent || "").includes("Sugerencia")) { presetMsg.textContent = ""; } } } catch (e) { sideMsg.textContent = "? " + (e.message || "Error guardando"); } }); load(); + rentCarMoney(debt.lateFee) + '
', '
Total: RD const customAgentPanel = document.getElementById("customAgentPanel"); const customAgentSummary = document.getElementById("customAgentSummary"); const customAgentHero = document.getElementById("customAgentHero"); const customAgentHeroText = document.getElementById("customAgentHeroText"); const customAgentTag = document.getElementById("customAgentTag"); const genericConfigurator = document.getElementById("genericConfigurator"); const donPopeBoard = document.getElementById("donPopeBoard"); const valeAcademyBoard = document.getElementById("valeAcademyBoard"); const discoveryWayBoard = document.getElementById("discoveryWayBoard"); if (!isCustomWorkspace()) { customAgentPanel.classList.add("hidden"); customAgentHero.classList.add("hidden"); donPopeBoard.classList.add("hidden"); valeAcademyBoard.classList.add("hidden"); discoveryWayBoard.classList.add("hidden"); genericConfigurator.classList.remove("hidden"); flowBadge.textContent = "Flow V3"; return; } customAgentPanel.classList.remove("hidden"); customAgentHero.classList.remove("hidden"); genericConfigurator.classList.add("hidden"); donPopeBoard.classList.add("hidden"); valeAcademyBoard.classList.add("hidden"); discoveryWayBoard.classList.add("hidden"); if (isDiscoveryWayWorkspace()) { flowBadge.textContent = "Modo Discovery Way"; customAgentTag.textContent = "discovery-way-sales-v1"; customAgentHeroText.textContent = "Este workspace usa IA de ventas para tours, aprendizaje del estilo del dueño y excursiones con copy + foto cargadas desde panel."; discoveryWayBoard.classList.remove("hidden"); customAgentSummary.innerHTML = `
• Carga excursiones con copy completo e imagen desde este panel.
• La IA decide cuál excursión mostrar según la conversación.
• Aprende de respuestas humanas reales del negocio.
• Guarda memoria visible y estilo comercial.
• Todo queda aislado solo en este workspace.
`; return; } if (isDonPopeWorkspace()) { flowBadge.textContent = "Modo Don Pope"; customAgentTag.textContent = "don-pope-v1"; customAgentHeroText.textContent = "Este workspace tiene reglas exclusivas aplicadas en backend para Don Pope. Aquí ves la lógica real que utiliza el bot, no la plantilla genérica."; donPopeBoard.classList.remove("hidden"); customAgentSummary.innerHTML = `
• Consultas financieras: pasan directamente al encargado.
• Compra, venta o cambio de equipos: atención presencial obligatoria.
• Problemas técnicos: evaluación presencial en tienda.
• Ubicación: respuesta automática configurada.
• Aprendizaje del bot: activo y exclusivo para este negocio.
`; return; } if (isValeAcademyWorkspace()) { flowBadge.textContent = "Modo Vale Academy"; customAgentTag.textContent = "vale-academy-v1"; customAgentHeroText.textContent = "Este workspace usa una lógica comercial personalizada para responder con información real de Vale Academy, empujar a la inscripción y mostrar al cliente su flujo exclusivo."; valeAcademyBoard.classList.remove("hidden"); customAgentSummary.innerHTML = `
• Bienvenida comercial personalizada para música e inglés.
• Precios fijos: inscripción RD$499 y mensualidad RD$990.
• Datos de pago automáticos cuando el prospecto los solicita.
• Respuestas por horarios, programas y beneficios.
• Cierre enfocado en reservar el cupo y enviar comprobante.
`; return; } flowBadge.textContent = "Modo personalizado"; customAgentTag.textContent = workspace.customAgent || "custom-agent"; customAgentHeroText.textContent = "Este workspace tiene una lógica especial aplicada desde backend. Aquí se muestra la vista personalizada en lugar del configurador genérico."; customAgentSummary.innerHTML = `
• Workspace con agente personalizado activo.
• Las respuestas se gestionan con reglas exclusivas en backend.
• La plantilla normal se oculta para evitar confusión.
`; } function setFormValues(v) { flowEnabled.checked = v.enabled === true; cmdBack.value = v.commands?.back || "0"; cmdMenu.value = v.commands?.menu || "00"; handoffText.value = v.handoffText || "? Perfecto. Te paso con un asesor humano ahora."; aiEnabled.checked = v.ai?.enabled === true; aiMinScore.value = typeof v.ai?.minScore === "number" ? v.ai.minScore : 0.72; setAiTrainingValues(v.ai?.training || {}, true); businessName.value = v.businessName || ""; welcomeText.value = v.welcomeText || ""; mainOption1Label.value = v.mainOption1Label || ""; serviceATitle.value = v.serviceATitle || ""; serviceAOptions.value = Array.isArray(v.serviceAOptions) ? v.serviceAOptions.join("\n") : ""; serviceAQuestion.value = v.serviceAQuestion || ""; mainOption2Label.value = v.mainOption2Label || ""; serviceBTitle.value = v.serviceBTitle || ""; serviceBOptions.value = Array.isArray(v.serviceBOptions) ? v.serviceBOptions.join("\n") : ""; serviceBQuestion.value = v.serviceBQuestion || ""; mainOption3Label.value = v.mainOption3Label || ""; mainOption4Label.value = v.mainOption4Label || ""; mainOption5Label.value = v.mainOption5Label || ""; mainFallbackText.value = v.mainFallbackText || ""; enableCategoryQuestion.checked = v.enableCategoryQuestion === true; categoryQuestionText.value = v.categoryQuestionText || ""; categoryOptions.value = Array.isArray(v.categoryOptions) ? v.categoryOptions.join("\n") : ""; askNameText.value = v.askNameText || ""; askPhoneText.value = v.askPhoneText || ""; askEmailText.value = v.askEmailText || ""; clientTypeQuestionText.value = v.clientTypeQuestionText || ""; clientTypeOptions.value = Array.isArray(v.clientTypeOptions) ? v.clientTypeOptions.join("\n") : ""; appointmentIntroText.value = v.appointmentIntroText || ""; appointmentTypeQuestionText.value = v.appointmentTypeQuestionText || ""; appointmentTypeOptions.value = Array.isArray(v.appointmentTypeOptions) ? v.appointmentTypeOptions.join("\n") : ""; scheduleQuestionText.value = v.scheduleQuestionText || ""; scheduleOptions.value = Array.isArray(v.scheduleOptions) ? v.scheduleOptions.join("\n") : ""; servicesInfoText.value = v.servicesInfoText || ""; confirmationText.value = v.confirmationText || ""; renderPreviewFromForm(); } function loadLegalTemplate() { setFormValues({ enabled: true, commands: { back: "0", menu: "00" }, handoffText: "? Perfecto. Te paso con un asistente ahora.", ai: { enabled: false, fallback: true, minScore: 0.72, training: { enabled: false, prompt: "", notes: "" } }, businessName: "Chevalier, Pérez & Asociados", welcomeText: `Bienvenido a Chevalier, Pérez & Asociados. Somos una firma especializada en servicios jurídicos y contables, brindando asesoría estratégica a particulares, empresas e inversionistas. Seleccione una opción:`, mainOption1Label: "Consulta legal", serviceATitle: "Por favor seleccione el área de su consulta:", serviceAOptions: [ "Derecho inmobiliario", "Derecho civil", "Litigios y demandas", "Derecho comercial / empresas", "Derecho laboral", "Sucesiones y herencias", "Inversión extranjera en República Dominicana", "Otro asunto" ], serviceAQuestion: "Por favor describa brevemente su caso.", mainOption2Label: "Servicios contables / impuestos", serviceBTitle: "Por favor seleccione el servicio que necesita:", serviceBOptions: [ "Declaraciones de impuestos (DGII)", "Contabilidad para empresas", "Constitución de empresas", "Asesoría fiscal", "Registro Nacional de Contribuyente (RNC)", "Reportes financieros", "Igualas contables mensuales", "Otro servicio" ], serviceBQuestion: "Por favor describa brevemente su solicitud.", mainOption3Label: "Agendar una cita", mainOption4Label: "Información sobre nuestros servicios", mainOption5Label: "Hablar con un asistente", mainFallbackText: "? Opción inválida. Por favor seleccione una opción válida del 1 al 5.", enableCategoryQuestion: true, categoryQuestionText: "¿Su consulta está relacionada con:", categoryOptions: [ "Compra o venta de propiedad", "Problema legal / demanda", "Creación o gestión de empresa", "Cumplimiento fiscal / impuestos" ], askNameText: "Para poder asistirle mejor, ¿Cuál es su nombre completo?", askPhoneText: "Por favor indique su número de teléfono.", askEmailText: "Ingrese su correo electrónico.", clientTypeQuestionText: "¿Es usted?", clientTypeOptions: [ "Persona física", "Empresa", "Inversionista extranjero" ], appointmentIntroText: "Un especialista de Chevalier, Pérez & Asociados puede atender su caso mediante una consulta.", appointmentTypeQuestionText: "Seleccione el tipo de cita:", appointmentTypeOptions: [ "Consulta presencial", "Consulta virtual (Zoom / Meet)", "Llamada telefónica" ], scheduleQuestionText: "Seleccione el horario disponible:", scheduleOptions: [ "9:00 AM", "10:00 AM", "11:00 AM", "2:00 PM", "3:00 PM", "4:00 PM" ], servicesInfoText: `En Chevalier, Pérez & Asociados ofrecemos: ? Servicios jurídicos Derecho inmobiliario Litigios civiles y comerciales Contratos y negociaciones Inversión extranjera Sucesiones ?? Servicios contables Declaraciones fiscales ante DGII Contabilidad empresarial Igualas contables Asesoría fiscal Constitución de empresas`, confirmationText: `Gracias por contactar Chevalier, Pérez & Asociados. Su solicitud ha sido recibida y uno de nuestros especialistas confirmará su cita en breve. Si su caso es urgente puede escribir URGENTE.` }); sideMsg.textContent = "? Plantilla jurídica/contable cargada."; } function tryFillFormFromExistingFlow(existingFlow, existingAi) { const nodes = Array.isArray(existingFlow?.nodes) ? existingFlow.nodes : []; const nodeMap = new Map(nodes.map(n => [n.id, n])); const menu = nodeMap.get("menu"); const servicioA = nodeMap.get("servicio_a_menu"); const servicioB = nodeMap.get("servicio_b_menu"); const descA = nodeMap.get("desc_servicio_a"); const descB = nodeMap.get("desc_servicio_b"); const categoria = nodeMap.get("categoria_valor"); const datosNombre = nodeMap.get("datos_nombre"); const datosTel = nodeMap.get("datos_tel"); const datosEmail = nodeMap.get("datos_email"); const tipoCliente = nodeMap.get("tipo_cliente"); const citaTipo = nodeMap.get("cita_tipo"); const citaHorario = nodeMap.get("cita_horario"); const info = nodeMap.get("info_servicios"); const confirmacion = nodeMap.get("confirmacion"); const recognizable = menu && servicioA && servicioB && descA && descB && datosNombre && datosTel && datosEmail && tipoCliente && citaTipo && citaHorario && info && confirmacion; if (!recognizable) { return false; } setFormValues({ enabled: existingFlow.enabled === true, commands: existingFlow.commands || { back: "0", menu: "00" }, handoffText: existingFlow.handoffText || "? Perfecto. Te paso con un asesor humano ahora.", ai: { enabled: existingAi?.enabled === true, fallback: true, minScore: typeof existingAi?.minScore === "number" ? existingAi.minScore : 0.72, training: existingAi?.training || { enabled: false, prompt: "", notes: "" } }, businessName: "", welcomeText: menu.text || "", mainOption1Label: menu.options?.[0]?.label || "", serviceATitle: servicioA.text || "", serviceAOptions: (servicioA.options || []).map(o => o.label).filter(Boolean), serviceAQuestion: descA.text || "", mainOption2Label: menu.options?.[1]?.label || "", serviceBTitle: servicioB.text || "", serviceBOptions: (servicioB.options || []).map(o => o.label).filter(Boolean), serviceBQuestion: descB.text || "", mainOption3Label: menu.options?.[2]?.label || "", mainOption4Label: menu.options?.[3]?.label || "", mainOption5Label: menu.options?.[4]?.label || "", mainFallbackText: menu.fallbackText || "", enableCategoryQuestion: !!categoria, categoryQuestionText: categoria?.text || "", categoryOptions: categoria ? (categoria.options || []).map(o => o.label).filter(Boolean) : [], askNameText: datosNombre.text || "", askPhoneText: datosTel.text || "", askEmailText: datosEmail.text || "", clientTypeQuestionText: tipoCliente.text || "", clientTypeOptions: (tipoCliente.options || []).map(o => o.label).filter(Boolean), appointmentIntroText: "", appointmentTypeQuestionText: citaTipo.text || "", appointmentTypeOptions: (citaTipo.options || []).map(o => o.label).filter(Boolean), scheduleQuestionText: citaHorario.text || "", scheduleOptions: (citaHorario.options || []).map(o => o.label).filter(Boolean), servicesInfoText: info.text || "", confirmationText: confirmacion.text || "" }); return true; } async function load() { sideMsg.textContent = "Cargando..."; try { const data = await window.NexoAPI.apiRequest("/api/bot-config/flow", { auth: true }); workspace = data.workspace || null; syncBehaviorFeaturesUI(); sub.textContent = `${data.workspaceName || "Workspace"} - ${data.timezone || ""}${workspace?.plan ? " - Plan: " + workspace.plan : ""}`; const f = data.botFlow || {}; flow = { enabled: f.enabled === true, startNode: f.startNode || "menu", commands: f.commands || { back: "0", menu: "00" }, handoffText: f.handoffText || "? Perfecto. Te paso con un asesor humano ahora.", nodes: Array.isArray(f.nodes) ? f.nodes : [] }; ai = { enabled: data.ai?.enabled === true, fallback: true, minScore: typeof data.ai?.minScore === "number" ? data.ai.minScore : 0.72, training: data.ai?.training || { enabled: false, prompt: "", normalizedPrompt: "", faqQuestions: "", faqAnswers: "", notes: "", updatedAt: null } }; toggleAiTrainingPanel(); toggleIntelligentFlowEditor(); renderWorkspaceMode(); if (isDiscoveryWayWorkspace()) { resetDiscoveryTourForm(); await loadDiscoveryTours(); await loadDiscoveryMemory(); } const loaded = tryFillFormFromExistingFlow(flow, ai); if (!loaded && !isCustomWorkspace()) { aiEnabled.checked = ai.enabled; aiMinScore.value = ai.minScore; if (!aiTrainingDirty) { setAiTrainingValues(ai.training || {}); } if (isIntelligentPlanWorkspace()) { sideMsg.textContent = "? Modo inteligente activo: las plantillas/flows están desactivadas. Configura el prompt maestro del negocio."; } else { loadLegalTemplate(); sideMsg.textContent = "? El flow actual no coincide con este configurador guiado. Se cargó la plantilla base para que la edites."; } } else { if (!aiTrainingDirty) { setAiTrainingValues(ai.training || {}); } sideMsg.textContent = isCustomWorkspace() ? "? Se cargó la vista real de la lógica personalizada de este workspace." : "? Configuración cargada."; } toggleIntelligentFlowEditor(); renderPreviewFromForm(); } catch (e) { sideMsg.textContent = "? " + (e.message || "Error cargando"); if (e.status === 401) { window.NexoStorage.clearToken(); window.location.href = "./login.html"; } } } btnReload.addEventListener("click", () => { aiTrainingDirty = false; load(); }); btnLoadTemplateLegal.addEventListener("click", () => { if (isIntelligentPlanWorkspace()) { sideMsg.textContent = "Plan inteligente activo: las plantillas/flows no se usan. Edita el prompt maestro de IA."; return; } loadLegalTemplate(); }); btnPreview.addEventListener("click", () => renderPreviewFromForm()); btnLoadAiTrainingTemplate?.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); if (!aiTrainingPrompt) { sideMsg.textContent = "? No se encontró el campo del prompt maestro."; return; } const template = getAiTrainingTemplate(); if (!template || !template.trim()) { sideMsg.textContent = "? No se pudo generar la plantilla sugerida."; return; } const currentValue = String(aiTrainingPrompt.value || "").trim(); if (currentValue && currentValue !== template) { const overwrite = window.confirm( "Ya hay un prompt escrito. ¿Quieres reemplazarlo por la estructura sugerida?" ); if (!overwrite) { sideMsg.textContent = "?? Se mantuvo el prompt actual."; return; } } aiTrainingDirty = true; aiTrainingPrompt.value = template; if (aiTrainingNotes && !String(aiTrainingNotes.value || "").trim()) { aiTrainingNotes.value = "Estructura inicial cargada desde panel para entrenamiento IA."; } if (aiTrainingEnabled && !aiTrainingEnabled.checked) { aiTrainingEnabled.checked = true; } setTimeout(() => { aiTrainingPrompt.value = template; aiTrainingPrompt.focus(); aiTrainingPrompt.setSelectionRange( aiTrainingPrompt.value.length, aiTrainingPrompt.value.length ); }, 0); requestAnimationFrame(() => { aiTrainingPrompt.value = template; }); sideMsg.textContent = "? Se cargó la estructura sugerida correctamente. Ahora revisa el contenido y pulsa Guardar."; }); aiTrainingPrompt?.addEventListener("input", markAiTrainingDirty); aiTrainingNotes?.addEventListener("input", markAiTrainingDirty); aiTrainingEnabled?.addEventListener("change", markAiTrainingDirty); [ flowEnabled, cmdBack, cmdMenu, handoffText, aiEnabled, aiMinScore, behaviorMode, featureDetectHumanTakeover, featurePauseBotOnHumanReply, featureFollowUpsEnabled, featureAcademyMode, featureInventoryMode, featureToursMode, businessName, welcomeText, mainOption1Label, serviceATitle, serviceAOptions, serviceAQuestion, mainOption2Label, serviceBTitle, serviceBOptions, serviceBQuestion, mainOption3Label, mainOption4Label, mainOption5Label, mainFallbackText, enableCategoryQuestion, categoryQuestionText, categoryOptions, askNameText, askPhoneText, askEmailText, clientTypeQuestionText, clientTypeOptions, appointmentIntroText, appointmentTypeQuestionText, appointmentTypeOptions, scheduleQuestionText, scheduleOptions, servicesInfoText, confirmationText ].forEach(el => { el.addEventListener("input", renderPreviewFromForm); el.addEventListener("change", renderPreviewFromForm); }); behaviorMode.addEventListener("change", () => { autoAlignFeaturesByMode(); renderWorkspaceMode(); if (presetSelect) { presetSelect.value = behaviorMode.value; } if (presetMsg) { presetMsg.textContent = ""; } renderPreviewFromForm(); }); btnApplyPreset?.addEventListener("click", () => { const selectedPreset = String(presetSelect?.value || "").trim(); if (!selectedPreset) { presetMsg.textContent = "? Selecciona un preset antes de aplicarlo."; return; } const applied = applyPresetToUI(selectedPreset); if (!applied) { presetMsg.textContent = "? No se pudo aplicar el preset."; return; } renderWorkspaceMode(); renderPreviewFromForm(); const preset = getPresetConfig(selectedPreset); if (presetMsg) { if (preset) { presetMsg.textContent = isCustomWorkspace() ? `? Preset aplicado: ${preset.label}. En este workspace personalizado se guardará la configuración operativa al pulsar Guardar.` : `? Preset aplicado: ${preset.label}. También se rellenó la plantilla guiada. Ahora pulsa Guardar para persistirlo.`; } else { presetMsg.textContent = "? Preset aplicado. Ahora pulsa Guardar para persistirlo."; } } }); btnReloadDiscoveryTours?.addEventListener("click", () => loadDiscoveryTours()); btnReloadDiscoveryMemory?.addEventListener("click", () => loadDiscoveryMemory()); btnSaveDiscoveryTour?.addEventListener("click", () => saveDiscoveryTour()); btnCancelDiscoveryEdit?.addEventListener("click", () => resetDiscoveryTourForm()); btnSave.addEventListener("click", async () => { sideMsg.textContent = "Guardando..."; const behaviorFeatures = getBehaviorFeaturesPayload(); const isCustom = isCustomWorkspace(); const trainingPayload = getAiTrainingPayload(); const shouldAutoApplyTrainingSuggestion = isIntelligentPlanWorkspace() && trainingPayload.enabled === true && String(trainingPayload.prompt || "").trim().length > 0; let builtFlow = flow; let builtAi = { ...(ai || {}), enabled: aiEnabled.checked, fallback: true, minScore: Math.max(0, Math.min(1, Number(aiMinScore.value || 0.72))), training: trainingPayload }; if (!isCustom && !isIntelligentPlanWorkspace()) { const data = getFormData(); const formErr = validateFormData(data); if (formErr) { sideMsg.textContent = "? " + formErr; return; } builtFlow = buildFlowFromForm(data); const flowErr = validateFlow(builtFlow); if (flowErr) { sideMsg.textContent = "? " + flowErr; return; } builtAi = { ...builtAi, ...data.ai, training: trainingPayload }; } try { const body = { features: behaviorFeatures.features, behavior: behaviorFeatures.behavior }; if (isIntelligentPlanWorkspace()) { body.ai = builtAi; } else if (!isCustom) { body.botFlow = builtFlow; body.ai = builtAi; } if (shouldAutoApplyTrainingSuggestion) { body.autoApplyTrainingSuggestion = true; } const response = await window.NexoAPI.apiRequest("/api/bot-config/flow", { method: "PATCH", auth: true, body }); flow = response.botFlow || builtFlow; ai = response.ai || builtAi; workspace = response.workspace || workspace; aiTrainingDirty = false; if (response.trainingSuggestionApplied === true && response.trainingSuggestion) { applyTrainingSuggestionToUI(response.trainingSuggestion); } syncBehaviorFeaturesUI(); toggleAiTrainingPanel(); renderWorkspaceMode(); if (response.trainingSuggestionApplied === true && response.trainingSuggestion) { const detected = String(response.trainingSuggestion.detectedBusinessType || response.trainingSuggestion.behavior?.mode || "generic"); sideMsg.textContent = `? Configuración guardada. El prompt detectó este negocio como "${detected}" y se aplicó automáticamente al panel.`; if (presetMsg) { presetMsg.textContent = `? Sugerencia del prompt aplicada automáticamente: ${detected}. Revisa los toggles y el modo del workspace.`; } } else if (response.trainingSuggestion && response.trainingAutoApplyAllowed === false) { const detected = String(response.trainingSuggestion.detectedBusinessType || response.trainingSuggestion.behavior?.mode || "generic"); sideMsg.textContent = `? Configuración guardada. El prompt sugiere "${detected}", pero este workspace usa lógica personalizada y no se aplicó automáticamente.`; if (presetMsg) { presetMsg.textContent = `? Sugerencia detectada por el prompt: ${detected}. No se aplicó automáticamente porque este workspace es personalizado.`; } } else { sideMsg.textContent = isCustom ? (isIntelligentPlanWorkspace() ? "? Configuración operativa y entrenamiento IA guardados." : "? Configuración operativa guardada.") : "? Guardado."; if (presetMsg && !String(presetMsg.textContent || "").includes("Sugerencia")) { presetMsg.textContent = ""; } } } catch (e) { sideMsg.textContent = "? " + (e.message || "Error guardando"); } }); load(); + rentCarMoney(debt.total) + '
', '', client.notes ? '
Notas: ' + client.notes + '
' : '', '
', '', '', '', '
', '' ].join(""); }).join(""); } function renderWorkspaceMode() { const customAgentPanel = document.getElementById("customAgentPanel"); const customAgentSummary = document.getElementById("customAgentSummary"); const customAgentHero = document.getElementById("customAgentHero"); const customAgentHeroText = document.getElementById("customAgentHeroText"); const customAgentTag = document.getElementById("customAgentTag"); const genericConfigurator = document.getElementById("genericConfigurator"); const donPopeBoard = document.getElementById("donPopeBoard"); const valeAcademyBoard = document.getElementById("valeAcademyBoard"); const discoveryWayBoard = document.getElementById("discoveryWayBoard"); if (!isCustomWorkspace()) { customAgentPanel.classList.add("hidden"); customAgentHero.classList.add("hidden"); donPopeBoard.classList.add("hidden"); valeAcademyBoard.classList.add("hidden"); discoveryWayBoard.classList.add("hidden"); genericConfigurator.classList.remove("hidden"); flowBadge.textContent = "Flow V3"; return; } customAgentPanel.classList.remove("hidden"); customAgentHero.classList.remove("hidden"); genericConfigurator.classList.add("hidden"); donPopeBoard.classList.add("hidden"); valeAcademyBoard.classList.add("hidden"); discoveryWayBoard.classList.add("hidden"); if (isDiscoveryWayWorkspace()) { flowBadge.textContent = "Modo Discovery Way"; customAgentTag.textContent = "discovery-way-sales-v1"; customAgentHeroText.textContent = "Este workspace usa IA de ventas para tours, aprendizaje del estilo del dueño y excursiones con copy + foto cargadas desde panel."; discoveryWayBoard.classList.remove("hidden"); customAgentSummary.innerHTML = `
• Carga excursiones con copy completo e imagen desde este panel.
• La IA decide cuál excursión mostrar según la conversación.
• Aprende de respuestas humanas reales del negocio.
• Guarda memoria visible y estilo comercial.
• Todo queda aislado solo en este workspace.
`; return; } if (isDonPopeWorkspace()) { flowBadge.textContent = "Modo Don Pope"; customAgentTag.textContent = "don-pope-v1"; customAgentHeroText.textContent = "Este workspace tiene reglas exclusivas aplicadas en backend para Don Pope. Aquí ves la lógica real que utiliza el bot, no la plantilla genérica."; donPopeBoard.classList.remove("hidden"); customAgentSummary.innerHTML = `
• Consultas financieras: pasan directamente al encargado.
• Compra, venta o cambio de equipos: atención presencial obligatoria.
• Problemas técnicos: evaluación presencial en tienda.
• Ubicación: respuesta automática configurada.
• Aprendizaje del bot: activo y exclusivo para este negocio.
`; return; } if (isValeAcademyWorkspace()) { flowBadge.textContent = "Modo Vale Academy"; customAgentTag.textContent = "vale-academy-v1"; customAgentHeroText.textContent = "Este workspace usa una lógica comercial personalizada para responder con información real de Vale Academy, empujar a la inscripción y mostrar al cliente su flujo exclusivo."; valeAcademyBoard.classList.remove("hidden"); customAgentSummary.innerHTML = `
• Bienvenida comercial personalizada para música e inglés.
• Precios fijos: inscripción RD$499 y mensualidad RD$990.
• Datos de pago automáticos cuando el prospecto los solicita.
• Respuestas por horarios, programas y beneficios.
• Cierre enfocado en reservar el cupo y enviar comprobante.
`; return; } flowBadge.textContent = "Modo personalizado"; customAgentTag.textContent = workspace.customAgent || "custom-agent"; customAgentHeroText.textContent = "Este workspace tiene una lógica especial aplicada desde backend. Aquí se muestra la vista personalizada en lugar del configurador genérico."; customAgentSummary.innerHTML = `
• Workspace con agente personalizado activo.
• Las respuestas se gestionan con reglas exclusivas en backend.
• La plantilla normal se oculta para evitar confusión.
`; } function setFormValues(v) { flowEnabled.checked = v.enabled === true; cmdBack.value = v.commands?.back || "0"; cmdMenu.value = v.commands?.menu || "00"; handoffText.value = v.handoffText || "? Perfecto. Te paso con un asesor humano ahora."; aiEnabled.checked = v.ai?.enabled === true; aiMinScore.value = typeof v.ai?.minScore === "number" ? v.ai.minScore : 0.72; setAiTrainingValues(v.ai?.training || {}, true); businessName.value = v.businessName || ""; welcomeText.value = v.welcomeText || ""; mainOption1Label.value = v.mainOption1Label || ""; serviceATitle.value = v.serviceATitle || ""; serviceAOptions.value = Array.isArray(v.serviceAOptions) ? v.serviceAOptions.join("\n") : ""; serviceAQuestion.value = v.serviceAQuestion || ""; mainOption2Label.value = v.mainOption2Label || ""; serviceBTitle.value = v.serviceBTitle || ""; serviceBOptions.value = Array.isArray(v.serviceBOptions) ? v.serviceBOptions.join("\n") : ""; serviceBQuestion.value = v.serviceBQuestion || ""; mainOption3Label.value = v.mainOption3Label || ""; mainOption4Label.value = v.mainOption4Label || ""; mainOption5Label.value = v.mainOption5Label || ""; mainFallbackText.value = v.mainFallbackText || ""; enableCategoryQuestion.checked = v.enableCategoryQuestion === true; categoryQuestionText.value = v.categoryQuestionText || ""; categoryOptions.value = Array.isArray(v.categoryOptions) ? v.categoryOptions.join("\n") : ""; askNameText.value = v.askNameText || ""; askPhoneText.value = v.askPhoneText || ""; askEmailText.value = v.askEmailText || ""; clientTypeQuestionText.value = v.clientTypeQuestionText || ""; clientTypeOptions.value = Array.isArray(v.clientTypeOptions) ? v.clientTypeOptions.join("\n") : ""; appointmentIntroText.value = v.appointmentIntroText || ""; appointmentTypeQuestionText.value = v.appointmentTypeQuestionText || ""; appointmentTypeOptions.value = Array.isArray(v.appointmentTypeOptions) ? v.appointmentTypeOptions.join("\n") : ""; scheduleQuestionText.value = v.scheduleQuestionText || ""; scheduleOptions.value = Array.isArray(v.scheduleOptions) ? v.scheduleOptions.join("\n") : ""; servicesInfoText.value = v.servicesInfoText || ""; confirmationText.value = v.confirmationText || ""; renderPreviewFromForm(); } function loadLegalTemplate() { setFormValues({ enabled: true, commands: { back: "0", menu: "00" }, handoffText: "? Perfecto. Te paso con un asistente ahora.", ai: { enabled: false, fallback: true, minScore: 0.72, training: { enabled: false, prompt: "", notes: "" } }, businessName: "Chevalier, Pérez & Asociados", welcomeText: `Bienvenido a Chevalier, Pérez & Asociados. Somos una firma especializada en servicios jurídicos y contables, brindando asesoría estratégica a particulares, empresas e inversionistas. Seleccione una opción:`, mainOption1Label: "Consulta legal", serviceATitle: "Por favor seleccione el área de su consulta:", serviceAOptions: [ "Derecho inmobiliario", "Derecho civil", "Litigios y demandas", "Derecho comercial / empresas", "Derecho laboral", "Sucesiones y herencias", "Inversión extranjera en República Dominicana", "Otro asunto" ], serviceAQuestion: "Por favor describa brevemente su caso.", mainOption2Label: "Servicios contables / impuestos", serviceBTitle: "Por favor seleccione el servicio que necesita:", serviceBOptions: [ "Declaraciones de impuestos (DGII)", "Contabilidad para empresas", "Constitución de empresas", "Asesoría fiscal", "Registro Nacional de Contribuyente (RNC)", "Reportes financieros", "Igualas contables mensuales", "Otro servicio" ], serviceBQuestion: "Por favor describa brevemente su solicitud.", mainOption3Label: "Agendar una cita", mainOption4Label: "Información sobre nuestros servicios", mainOption5Label: "Hablar con un asistente", mainFallbackText: "? Opción inválida. Por favor seleccione una opción válida del 1 al 5.", enableCategoryQuestion: true, categoryQuestionText: "¿Su consulta está relacionada con:", categoryOptions: [ "Compra o venta de propiedad", "Problema legal / demanda", "Creación o gestión de empresa", "Cumplimiento fiscal / impuestos" ], askNameText: "Para poder asistirle mejor, ¿Cuál es su nombre completo?", askPhoneText: "Por favor indique su número de teléfono.", askEmailText: "Ingrese su correo electrónico.", clientTypeQuestionText: "¿Es usted?", clientTypeOptions: [ "Persona física", "Empresa", "Inversionista extranjero" ], appointmentIntroText: "Un especialista de Chevalier, Pérez & Asociados puede atender su caso mediante una consulta.", appointmentTypeQuestionText: "Seleccione el tipo de cita:", appointmentTypeOptions: [ "Consulta presencial", "Consulta virtual (Zoom / Meet)", "Llamada telefónica" ], scheduleQuestionText: "Seleccione el horario disponible:", scheduleOptions: [ "9:00 AM", "10:00 AM", "11:00 AM", "2:00 PM", "3:00 PM", "4:00 PM" ], servicesInfoText: `En Chevalier, Pérez & Asociados ofrecemos: ? Servicios jurídicos Derecho inmobiliario Litigios civiles y comerciales Contratos y negociaciones Inversión extranjera Sucesiones ?? Servicios contables Declaraciones fiscales ante DGII Contabilidad empresarial Igualas contables Asesoría fiscal Constitución de empresas`, confirmationText: `Gracias por contactar Chevalier, Pérez & Asociados. Su solicitud ha sido recibida y uno de nuestros especialistas confirmará su cita en breve. Si su caso es urgente puede escribir URGENTE.` }); sideMsg.textContent = "? Plantilla jurídica/contable cargada."; } function tryFillFormFromExistingFlow(existingFlow, existingAi) { const nodes = Array.isArray(existingFlow?.nodes) ? existingFlow.nodes : []; const nodeMap = new Map(nodes.map(n => [n.id, n])); const menu = nodeMap.get("menu"); const servicioA = nodeMap.get("servicio_a_menu"); const servicioB = nodeMap.get("servicio_b_menu"); const descA = nodeMap.get("desc_servicio_a"); const descB = nodeMap.get("desc_servicio_b"); const categoria = nodeMap.get("categoria_valor"); const datosNombre = nodeMap.get("datos_nombre"); const datosTel = nodeMap.get("datos_tel"); const datosEmail = nodeMap.get("datos_email"); const tipoCliente = nodeMap.get("tipo_cliente"); const citaTipo = nodeMap.get("cita_tipo"); const citaHorario = nodeMap.get("cita_horario"); const info = nodeMap.get("info_servicios"); const confirmacion = nodeMap.get("confirmacion"); const recognizable = menu && servicioA && servicioB && descA && descB && datosNombre && datosTel && datosEmail && tipoCliente && citaTipo && citaHorario && info && confirmacion; if (!recognizable) { return false; } setFormValues({ enabled: existingFlow.enabled === true, commands: existingFlow.commands || { back: "0", menu: "00" }, handoffText: existingFlow.handoffText || "? Perfecto. Te paso con un asesor humano ahora.", ai: { enabled: existingAi?.enabled === true, fallback: true, minScore: typeof existingAi?.minScore === "number" ? existingAi.minScore : 0.72, training: existingAi?.training || { enabled: false, prompt: "", notes: "" } }, businessName: "", welcomeText: menu.text || "", mainOption1Label: menu.options?.[0]?.label || "", serviceATitle: servicioA.text || "", serviceAOptions: (servicioA.options || []).map(o => o.label).filter(Boolean), serviceAQuestion: descA.text || "", mainOption2Label: menu.options?.[1]?.label || "", serviceBTitle: servicioB.text || "", serviceBOptions: (servicioB.options || []).map(o => o.label).filter(Boolean), serviceBQuestion: descB.text || "", mainOption3Label: menu.options?.[2]?.label || "", mainOption4Label: menu.options?.[3]?.label || "", mainOption5Label: menu.options?.[4]?.label || "", mainFallbackText: menu.fallbackText || "", enableCategoryQuestion: !!categoria, categoryQuestionText: categoria?.text || "", categoryOptions: categoria ? (categoria.options || []).map(o => o.label).filter(Boolean) : [], askNameText: datosNombre.text || "", askPhoneText: datosTel.text || "", askEmailText: datosEmail.text || "", clientTypeQuestionText: tipoCliente.text || "", clientTypeOptions: (tipoCliente.options || []).map(o => o.label).filter(Boolean), appointmentIntroText: "", appointmentTypeQuestionText: citaTipo.text || "", appointmentTypeOptions: (citaTipo.options || []).map(o => o.label).filter(Boolean), scheduleQuestionText: citaHorario.text || "", scheduleOptions: (citaHorario.options || []).map(o => o.label).filter(Boolean), servicesInfoText: info.text || "", confirmationText: confirmacion.text || "" }); return true; } async function load() { sideMsg.textContent = "Cargando..."; try { const data = await window.NexoAPI.apiRequest("/api/bot-config/flow", { auth: true }); workspace = data.workspace || null; syncBehaviorFeaturesUI(); sub.textContent = `${data.workspaceName || "Workspace"} - ${data.timezone || ""}${workspace?.plan ? " - Plan: " + workspace.plan : ""}`; const f = data.botFlow || {}; flow = { enabled: f.enabled === true, startNode: f.startNode || "menu", commands: f.commands || { back: "0", menu: "00" }, handoffText: f.handoffText || "? Perfecto. Te paso con un asesor humano ahora.", nodes: Array.isArray(f.nodes) ? f.nodes : [] }; ai = { enabled: data.ai?.enabled === true, fallback: true, minScore: typeof data.ai?.minScore === "number" ? data.ai.minScore : 0.72, training: data.ai?.training || { enabled: false, prompt: "", normalizedPrompt: "", faqQuestions: "", faqAnswers: "", notes: "", updatedAt: null } }; toggleAiTrainingPanel(); toggleIntelligentFlowEditor(); renderWorkspaceMode(); if (isDiscoveryWayWorkspace()) { resetDiscoveryTourForm(); await loadDiscoveryTours(); await loadDiscoveryMemory(); } const loaded = tryFillFormFromExistingFlow(flow, ai); if (!loaded && !isCustomWorkspace()) { aiEnabled.checked = ai.enabled; aiMinScore.value = ai.minScore; if (!aiTrainingDirty) { setAiTrainingValues(ai.training || {}); } if (isIntelligentPlanWorkspace()) { sideMsg.textContent = "? Modo inteligente activo: las plantillas/flows están desactivadas. Configura el prompt maestro del negocio."; } else { loadLegalTemplate(); sideMsg.textContent = "? El flow actual no coincide con este configurador guiado. Se cargó la plantilla base para que la edites."; } } else { if (!aiTrainingDirty) { setAiTrainingValues(ai.training || {}); } sideMsg.textContent = isCustomWorkspace() ? "? Se cargó la vista real de la lógica personalizada de este workspace." : "? Configuración cargada."; } toggleIntelligentFlowEditor(); renderPreviewFromForm(); } catch (e) { sideMsg.textContent = "? " + (e.message || "Error cargando"); if (e.status === 401) { window.NexoStorage.clearToken(); window.location.href = "./login.html"; } } } btnReload.addEventListener("click", () => { aiTrainingDirty = false; load(); }); btnLoadTemplateLegal.addEventListener("click", () => { if (isIntelligentPlanWorkspace()) { sideMsg.textContent = "Plan inteligente activo: las plantillas/flows no se usan. Edita el prompt maestro de IA."; return; } loadLegalTemplate(); }); btnPreview.addEventListener("click", () => renderPreviewFromForm()); btnLoadAiTrainingTemplate?.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); if (!aiTrainingPrompt) { sideMsg.textContent = "? No se encontró el campo del prompt maestro."; return; } const template = getAiTrainingTemplate(); if (!template || !template.trim()) { sideMsg.textContent = "? No se pudo generar la plantilla sugerida."; return; } const currentValue = String(aiTrainingPrompt.value || "").trim(); if (currentValue && currentValue !== template) { const overwrite = window.confirm( "Ya hay un prompt escrito. ¿Quieres reemplazarlo por la estructura sugerida?" ); if (!overwrite) { sideMsg.textContent = "?? Se mantuvo el prompt actual."; return; } } aiTrainingDirty = true; aiTrainingPrompt.value = template; if (aiTrainingNotes && !String(aiTrainingNotes.value || "").trim()) { aiTrainingNotes.value = "Estructura inicial cargada desde panel para entrenamiento IA."; } if (aiTrainingEnabled && !aiTrainingEnabled.checked) { aiTrainingEnabled.checked = true; } setTimeout(() => { aiTrainingPrompt.value = template; aiTrainingPrompt.focus(); aiTrainingPrompt.setSelectionRange( aiTrainingPrompt.value.length, aiTrainingPrompt.value.length ); }, 0); requestAnimationFrame(() => { aiTrainingPrompt.value = template; }); sideMsg.textContent = "? Se cargó la estructura sugerida correctamente. Ahora revisa el contenido y pulsa Guardar."; }); aiTrainingPrompt?.addEventListener("input", markAiTrainingDirty); aiTrainingNotes?.addEventListener("input", markAiTrainingDirty); aiTrainingEnabled?.addEventListener("change", markAiTrainingDirty); [ flowEnabled, cmdBack, cmdMenu, handoffText, aiEnabled, aiMinScore, behaviorMode, featureDetectHumanTakeover, featurePauseBotOnHumanReply, featureFollowUpsEnabled, featureAcademyMode, featureInventoryMode, featureToursMode, businessName, welcomeText, mainOption1Label, serviceATitle, serviceAOptions, serviceAQuestion, mainOption2Label, serviceBTitle, serviceBOptions, serviceBQuestion, mainOption3Label, mainOption4Label, mainOption5Label, mainFallbackText, enableCategoryQuestion, categoryQuestionText, categoryOptions, askNameText, askPhoneText, askEmailText, clientTypeQuestionText, clientTypeOptions, appointmentIntroText, appointmentTypeQuestionText, appointmentTypeOptions, scheduleQuestionText, scheduleOptions, servicesInfoText, confirmationText ].forEach(el => { el.addEventListener("input", renderPreviewFromForm); el.addEventListener("change", renderPreviewFromForm); }); behaviorMode.addEventListener("change", () => { autoAlignFeaturesByMode(); renderWorkspaceMode(); if (presetSelect) { presetSelect.value = behaviorMode.value; } if (presetMsg) { presetMsg.textContent = ""; } renderPreviewFromForm(); }); btnApplyPreset?.addEventListener("click", () => { const selectedPreset = String(presetSelect?.value || "").trim(); if (!selectedPreset) { presetMsg.textContent = "? Selecciona un preset antes de aplicarlo."; return; } const applied = applyPresetToUI(selectedPreset); if (!applied) { presetMsg.textContent = "? No se pudo aplicar el preset."; return; } renderWorkspaceMode(); renderPreviewFromForm(); const preset = getPresetConfig(selectedPreset); if (presetMsg) { if (preset) { presetMsg.textContent = isCustomWorkspace() ? `? Preset aplicado: ${preset.label}. En este workspace personalizado se guardará la configuración operativa al pulsar Guardar.` : `? Preset aplicado: ${preset.label}. También se rellenó la plantilla guiada. Ahora pulsa Guardar para persistirlo.`; } else { presetMsg.textContent = "? Preset aplicado. Ahora pulsa Guardar para persistirlo."; } } }); btnReloadDiscoveryTours?.addEventListener("click", () => loadDiscoveryTours()); btnReloadDiscoveryMemory?.addEventListener("click", () => loadDiscoveryMemory()); btnSaveDiscoveryTour?.addEventListener("click", () => saveDiscoveryTour()); btnCancelDiscoveryEdit?.addEventListener("click", () => resetDiscoveryTourForm()); btnSave.addEventListener("click", async () => { sideMsg.textContent = "Guardando..."; const behaviorFeatures = getBehaviorFeaturesPayload(); const isCustom = isCustomWorkspace(); const trainingPayload = getAiTrainingPayload(); const shouldAutoApplyTrainingSuggestion = isIntelligentPlanWorkspace() && trainingPayload.enabled === true && String(trainingPayload.prompt || "").trim().length > 0; let builtFlow = flow; let builtAi = { ...(ai || {}), enabled: aiEnabled.checked, fallback: true, minScore: Math.max(0, Math.min(1, Number(aiMinScore.value || 0.72))), training: trainingPayload }; if (!isCustom && !isIntelligentPlanWorkspace()) { const data = getFormData(); const formErr = validateFormData(data); if (formErr) { sideMsg.textContent = "? " + formErr; return; } builtFlow = buildFlowFromForm(data); const flowErr = validateFlow(builtFlow); if (flowErr) { sideMsg.textContent = "? " + flowErr; return; } builtAi = { ...builtAi, ...data.ai, training: trainingPayload }; } try { const body = { features: behaviorFeatures.features, behavior: behaviorFeatures.behavior }; if (isIntelligentPlanWorkspace()) { body.ai = builtAi; } else if (!isCustom) { body.botFlow = builtFlow; body.ai = builtAi; } if (shouldAutoApplyTrainingSuggestion) { body.autoApplyTrainingSuggestion = true; } const response = await window.NexoAPI.apiRequest("/api/bot-config/flow", { method: "PATCH", auth: true, body }); flow = response.botFlow || builtFlow; ai = response.ai || builtAi; workspace = response.workspace || workspace; aiTrainingDirty = false; if (response.trainingSuggestionApplied === true && response.trainingSuggestion) { applyTrainingSuggestionToUI(response.trainingSuggestion); } syncBehaviorFeaturesUI(); toggleAiTrainingPanel(); renderWorkspaceMode(); if (response.trainingSuggestionApplied === true && response.trainingSuggestion) { const detected = String(response.trainingSuggestion.detectedBusinessType || response.trainingSuggestion.behavior?.mode || "generic"); sideMsg.textContent = `? Configuración guardada. El prompt detectó este negocio como "${detected}" y se aplicó automáticamente al panel.`; if (presetMsg) { presetMsg.textContent = `? Sugerencia del prompt aplicada automáticamente: ${detected}. Revisa los toggles y el modo del workspace.`; } } else if (response.trainingSuggestion && response.trainingAutoApplyAllowed === false) { const detected = String(response.trainingSuggestion.detectedBusinessType || response.trainingSuggestion.behavior?.mode || "generic"); sideMsg.textContent = `? Configuración guardada. El prompt sugiere "${detected}", pero este workspace usa lógica personalizada y no se aplicó automáticamente.`; if (presetMsg) { presetMsg.textContent = `? Sugerencia detectada por el prompt: ${detected}. No se aplicó automáticamente porque este workspace es personalizado.`; } } else { sideMsg.textContent = isCustom ? (isIntelligentPlanWorkspace() ? "? Configuración operativa y entrenamiento IA guardados." : "? Configuración operativa guardada.") : "? Guardado."; if (presetMsg && !String(presetMsg.textContent || "").includes("Sugerencia")) { presetMsg.textContent = ""; } } } catch (e) { sideMsg.textContent = "? " + (e.message || "Error guardando"); } }); load();