diff --git a/public/components/conversation-inspector.js b/public/components/conversation-inspector.js index d233830..1a14dba 100644 --- a/public/components/conversation-inspector.js +++ b/public/components/conversation-inspector.js @@ -276,6 +276,9 @@ class ConversationInspector extends HTMLElement { this.rowMap.set(msgId, optItem); this.rowOrder.push(msgId); } + + // Auto-scroll al final + list.scrollTop = list.scrollHeight; } applyHeights() { diff --git a/public/components/run-timeline.js b/public/components/run-timeline.js index 6b45ae6..783d72c 100644 --- a/public/components/run-timeline.js +++ b/public/components/run-timeline.js @@ -197,10 +197,14 @@ class RunTimeline extends HTMLElement { addedOptimistic = true; } - // auto-scroll solo si agregamos burbujas optimistas nuevas - if (addedOptimistic) { + // auto-scroll al final cuando hay mensajes nuevos + // Solo si el usuario estaba cerca del final (dentro de 150px) o si había optimistas + const wasNearBottom = this._lastScrollPosition === undefined || + (log.scrollHeight - this._lastScrollPosition - log.clientHeight) < 150; + if (addedOptimistic || wasNearBottom) { log.scrollTop = log.scrollHeight; } + this._lastScrollPosition = log.scrollTop; requestAnimationFrame(() => this.emitLayout()); this.bindScroll(log); @@ -211,6 +215,7 @@ class RunTimeline extends HTMLElement { if (this._scrollBound) return; this._scrollBound = true; log.addEventListener("scroll", () => { + this._lastScrollPosition = log.scrollTop; emit("ui:chatScroll", { chat_id: this.chatId, scrollTop: log.scrollTop }); }); } diff --git a/src/modules/2-identity/services/pipeline.js b/src/modules/2-identity/services/pipeline.js index 17e494d..2fc2216 100644 --- a/src/modules/2-identity/services/pipeline.js +++ b/src/modules/2-identity/services/pipeline.js @@ -162,10 +162,7 @@ let externalCustomerId = await getExternalCustomerIdByChat({ logStage(stageDebug, "history", { has_history: Array.isArray(conversation_history), state: prev_state }); let reducedContext = prev?.context && typeof prev?.context === "object" ? { ...prev.context } : {}; - // #region agent log - fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"pipeline.js:164",message:"pipeline_loaded_context",data:{prev_state,has_prev_context:!!prev?.context,reducedContext_has_order_basket:!!reducedContext?.order_basket,reducedContext_basket_count:reducedContext?.order_basket?.items?.length||0,reducedContext_basket_labels:(reducedContext?.order_basket?.items||[]).map(i=>i.label)},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H3-H4"})}).catch(()=>{}); - // #endregion - let decision; +let decision; let plan; let llmMeta; let tools = []; @@ -341,12 +338,7 @@ const runStatus = llmMeta?.error ? "warn" : "ok"; railguard: { simulated: isSimulated, source: meta?.source || null }, woo_customer_error: wooCustomerError, }; - - // #region agent log - fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"pipeline.js:345",message:"pipeline_saving_context",data:{reducedContext_basket_count:reducedContext?.order_basket?.items?.length||0,context_patch_basket_count:decision?.context_patch?.order_basket?.items?.length||0,final_context_basket_count:context?.order_basket?.items?.length||0,final_context_basket_labels:(context?.order_basket?.items||[]).map(i=>i.label),plan_intent:plan?.intent},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H2-H4"})}).catch(()=>{}); - // #endregion - - const nextState = safeNextState(prev_state, context, { requested_checkout: plan.intent === "checkout" }).next_state; +const nextState = safeNextState(prev_state, context, { requested_checkout: plan.intent === "checkout" }).next_state; plan.next_state = nextState; const stateRow = await upsertConversationState({ diff --git a/src/modules/3-turn-engine/turnEngineV3.js b/src/modules/3-turn-engine/turnEngineV3.js index efaaf53..1480e97 100644 --- a/src/modules/3-turn-engine/turnEngineV3.js +++ b/src/modules/3-turn-engine/turnEngineV3.js @@ -367,12 +367,7 @@ function applyTypeSelection(item, selection, text) { const hasExplicitUnit = item.unit != null && item.unit !== ""; const quantityIsGeneric = hasQty && Number(item.quantity) <= 2 && !hasExplicitUnit; const needsQuantityClarification = sellsByWeight && quantityIsGeneric; - - // #region agent log - fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"turnEngineV3.js:applyTypeSelection",message:"type_selection_qty_check",data:{product_name:resolvedProduct?.name,display_unit:resolvedProduct?.display_unit,item_quantity:item.quantity,item_unit:item.unit,hasQty,sellsByWeight,hasExplicitUnit,quantityIsGeneric,needsQuantityClarification,final_status:needsQuantityClarification?"NEEDS_QUANTITY":"READY"},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H-qty"})}).catch(()=>{}); - // #endregion - - return { +return { ...item, candidates: [], resolved: resolvedProduct, @@ -464,12 +459,7 @@ async function initializePendingItems({ audit.catalog_init = audit.catalog_init || []; audit.catalog_init.push({ query: mention.query, count: candidates?.length || 0 }); - - // #region agent log - fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"turnEngineV3.js:initializePendingItems",message:"creating_pending_item",data:{query:mention.query,quantity:mention.quantity,unit:mention.unit,candidates_count:candidates?.length||0},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H-init"})}).catch(()=>{}); - // #endregion - - const item = createPendingItem({ +const item = createPendingItem({ id: `pi_${timestamp}_${i}`, query: mention.query, quantity: mention.quantity, @@ -901,12 +891,7 @@ export async function runTurnV3({ const actions = []; const context_patch = {}; const audit = {}; - - // #region agent log - fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"turnEngineV3.js:898",message:"runTurnV3_entry",data:{prev_state,text_preview:String(text||"").slice(0,50),has_prev_context:!!prev_context,prev_order_basket_exists:!!prev?.order_basket,prev_basket_items_count:prev?.order_basket?.items?.length||0,prev_basket_labels:(prev?.order_basket?.items||[]).map(i=>i.label)},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H3-H4"})}).catch(()=>{}); - // #endregion - - // Observabilidad (NO se envía al LLM) +// Observabilidad (NO se envía al LLM) audit.trace = { tenantId: tenantId || null, chat_id: chat_id || null, @@ -929,12 +914,25 @@ export async function runTurnV3({ }; const { nlu, raw_text, model, usage, validation } = await llmNluV3({ input: nluInput }); audit.nlu = { raw_text, model, usage, validation, parsed: nlu }; + + // ───────────────────────────────────────────────────────────── + // PRIORIDAD: Checkout steps (payment/shipping) tienen prioridad sobre pending_items + // ───────────────────────────────────────────────────────────── + const isInCheckoutFlow = prev?.checkout_step === "payment_method" || prev?.checkout_step === "shipping_method"; + const isNumericCheckoutResponse = /^\s*[12]\s*$/.test(String(text || "")); + const skipPendingItemsForCheckout = isInCheckoutFlow && isNumericCheckoutResponse; + + // #region agent log + fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"turnEngineV3.js:checkout_priority_check",message:"checkout_flow_check",data:{checkout_step:prev?.checkout_step,isInCheckoutFlow,isNumericCheckoutResponse,skipPendingItemsForCheckout,text,nlu_intent:nlu?.intent},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H-checkout"})}).catch(()=>{}); + // #endregion + // ───────────────────────────────────────────────────────────── // NUEVO FLUJO: Carrito acumulativo con pending_items + // (Solo si NO estamos en checkout flow con respuesta numérica) // ───────────────────────────────────────────────────────────── // 0a) Si hay pending_items existentes, continuar clarificación - if (Array.isArray(prev?.pending_items) && prev.pending_items.length > 0) { + if (!skipPendingItemsForCheckout && Array.isArray(prev?.pending_items) && prev.pending_items.length > 0) { const hasPendingToProcess = prev.pending_items.some( i => i.status === PENDING_ITEM_STATUS.NEEDS_TYPE || i.status === PENDING_ITEM_STATUS.NEEDS_QUANTITY ); @@ -953,13 +951,14 @@ const { nlu, raw_text, model, usage, validation } = await llmNluV3({ input: nluI } // 0b) Si el NLU detecta productos y no hay pending_items, inicializar nuevo flujo + // (Solo si NO estamos en checkout flow con respuesta numérica) const nluIntent = nlu?.intent || "other"; const isProductIntent = ["add_to_cart", "browse", "price_query"].includes(nluIntent); const hasProducts = hasProductMentions(nlu); const noPendingItems = !Array.isArray(prev?.pending_items) || prev.pending_items.length === 0; const noLegacyPending = !prev?.pending_clarification?.candidates?.length && !prev?.pending_item?.product_id; - if (isProductIntent && hasProducts && noPendingItems && noLegacyPending) { + if (!skipPendingItemsForCheckout && isProductIntent && hasProducts && noPendingItems && noLegacyPending) { const result = await initializePendingItems({ tenantId, nlu, @@ -977,7 +976,9 @@ const { nlu, raw_text, model, usage, validation } = await llmNluV3({ input: nluI // 0) Procesar multi-items si hay varios productos en un mensaje (LEGACY) // Solo si no hay pending_clarification ni pending_item (flujo limpio) + // Y NO estamos en checkout flow if ( + !skipPendingItemsForCheckout && Array.isArray(nlu?.entities?.items) && nlu.entities.items.length > 0 && !prev?.pending_clarification?.candidates?.length && @@ -998,7 +999,8 @@ const { nlu, raw_text, model, usage, validation } = await llmNluV3({ input: nluI } // 1) Resolver pending_clarification primero (LEGACY) - if (prev?.pending_clarification?.candidates?.length) { + // Saltar si estamos en checkout flow + if (!skipPendingItemsForCheckout && prev?.pending_clarification?.candidates?.length) { const resolved = resolvePendingSelection({ text, nlu, pending: prev.pending_clarification }); if (resolved.kind === "more") { const nextPending = resolved.pending || prev.pending_clarification; @@ -1367,7 +1369,12 @@ if (qty?.quantity) { } // Handler para select_payment: usuario elige método de pago - if (intent === "select_payment" || (prev?.checkout_step === "payment_method" && (nlu?.entities?.payment_method || nlu?.entities?.selection))) { + // Detectar número simple como selección cuando estamos en checkout_step payment_method + const isPaymentSelection = prev?.checkout_step === "payment_method" && /^\s*[12]\s*$/.test(String(text || "")); + // #region agent log + fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"turnEngineV3.js:select_payment_check",message:"payment_handler_entry",data:{intent,checkout_step:prev?.checkout_step,text,isPaymentSelection,has_payment_method:!!nlu?.entities?.payment_method,has_selection:!!nlu?.entities?.selection,nlu_intent:nlu?.intent},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H1-H4"})}).catch(()=>{}); + // #endregion + if (intent === "select_payment" || isPaymentSelection || (prev?.checkout_step === "payment_method" && (nlu?.entities?.payment_method || nlu?.entities?.selection))) { const basketItems = Array.isArray(prev?.order_basket?.items) ? prev.order_basket.items : []; // Determinar método de pago @@ -1419,7 +1426,12 @@ if (qty?.quantity) { } // Handler para select_shipping: usuario elige delivery o retiro - if (intent === "select_shipping" || (prev?.checkout_step === "shipping_method" && (nlu?.entities?.shipping_method || nlu?.entities?.selection))) { + // Detectar número simple como selección cuando estamos en checkout_step shipping_method + const isShippingSelection = prev?.checkout_step === "shipping_method" && /^\s*[12]\s*$/.test(String(text || "")); + // #region agent log + fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"turnEngineV3.js:select_shipping_check",message:"shipping_handler_entry",data:{intent,checkout_step:prev?.checkout_step,text,isShippingSelection,has_shipping_method:!!nlu?.entities?.shipping_method,has_selection:!!nlu?.entities?.selection,nlu_intent:nlu?.intent},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H1-H4"})}).catch(()=>{}); + // #endregion + if (intent === "select_shipping" || isShippingSelection || (prev?.checkout_step === "shipping_method" && (nlu?.entities?.shipping_method || nlu?.entities?.selection))) { const basketItems = Array.isArray(prev?.order_basket?.items) ? prev.order_basket.items : []; // Determinar método de envío @@ -1661,11 +1673,7 @@ if (qty?.quantity) { // Agregar nuevo item prevItems.push(item); } - - // #region agent log - fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"turnEngineV3.js:1645",message:"add_to_cart_basket_state",data:{prev_basket_exists:!!prev?.order_basket,prev_basket_items_count:prev?.order_basket?.items?.length||0,prev_items_labels:prevItems.map(i=>i.label),new_item_label:item.label,new_item_qty:item.quantity,new_item_unit:item.unit,was_update:existingIdx>=0},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H1-H2"})}).catch(()=>{}); - // #endregion - context_patch.order_basket = { items: prevItems }; +context_patch.order_basket = { items: prevItems }; actions.push({ type: actionType, payload: item }); const { next_state, validation: v } = safeNextState(prev_state, { ...prev, ...context_patch }, { pending_item_completed: true }); const display = qty.display_unit === "kg"