function parseIndexSelection(text) { const t = String(text || "").toLowerCase(); const m = /\b(\d{1,2})\b/.exec(t); if (m) return parseInt(m[1], 10); if (/\bprimera\b|\bprimero\b/.test(t)) return 1; if (/\bsegunda\b|\bsegundo\b/.test(t)) return 2; if (/\btercera\b|\btercero\b/.test(t)) return 3; if (/\bcuarta\b|\bcuarto\b/.test(t)) return 4; if (/\bquinta\b|\bquinto\b/.test(t)) return 5; if (/\bsexta\b|\bsexto\b/.test(t)) return 6; if (/\bs[eé]ptima\b|\bs[eé]ptimo\b/.test(t)) return 7; if (/\boctava\b|\boctavo\b/.test(t)) return 8; if (/\bnovena\b|\bnoveno\b/.test(t)) return 9; if (/\bd[eé]cima\b|\bd[eé]cimo\b/.test(t)) return 10; return null; } function isShowMoreRequest(text) { const t = String(text || "").toLowerCase(); return ( /\bmostr(a|ame)\s+m[aá]s\b/.test(t) || /\bmas\s+opciones\b/.test(t) || (/\bm[aá]s\b/.test(t) && /\b(opciones|productos|variedades|tipos)\b/.test(t)) || /\bsiguiente(s)?\b/.test(t) ); } function normalizeText(s) { return String(s || "") .toLowerCase() .replace(/[¿?¡!.,;:()"]/g, " ") .replace(/\s+/g, " ") .trim(); } function scoreTextMatch(query, candidateName) { const qt = new Set(normalizeText(query).split(" ").filter(Boolean)); const nt = new Set(normalizeText(candidateName).split(" ").filter(Boolean)); let hits = 0; for (const w of qt) if (nt.has(w)) hits++; return hits / Math.max(qt.size, 1); } export function buildPagedOptions({ candidates, candidateOffset = 0, baseIdx = 1, pageSize = 9 }) { const cands = (candidates || []).filter((c) => c && c.woo_product_id && c.name); const off = Math.max(0, parseInt(candidateOffset, 10) || 0); const size = Math.max(1, Math.min(20, parseInt(pageSize, 10) || 9)); const slice = cands.slice(off, off + size); const options = slice.map((c, i) => ({ idx: baseIdx + i, type: "product", woo_product_id: c.woo_product_id, name: c.name, })); const hasMore = off + size < cands.length; if (hasMore) options.push({ idx: baseIdx + size, type: "more", name: "Mostrame más…" }); const list = options .map((o) => (o.type === "more" ? `- ${o.idx}) ${o.name}` : `- ${o.idx}) ${o.name}`)) .join("\n"); const question = `¿Cuál de estos querés?\n${list}\n\nRespondé con el número.`; const pending = { candidates: cands, options, candidate_offset: off, page_size: size, base_idx: baseIdx, has_more: hasMore, next_candidate_offset: off + size, next_base_idx: baseIdx + size + (hasMore ? 1 : 0), }; return { question, pending, options, hasMore }; } export function resolvePendingSelection({ text, nlu, pending }) { if (!pending?.candidates?.length) return { kind: "none" }; if (isShowMoreRequest(text)) { const { question, pending: nextPending } = buildPagedOptions({ candidates: pending.candidates, candidateOffset: pending.next_candidate_offset ?? ((pending.candidate_offset || 0) + (pending.page_size || 9)), baseIdx: pending.next_base_idx ?? ((pending.base_idx || 1) + (pending.page_size || 9) + 1), pageSize: pending.page_size || 9, }); return { kind: "more", question, pending: nextPending }; } const idx = (nlu?.entities?.selection?.type === "index" ? parseInt(String(nlu.entities.selection.value), 10) : null) ?? parseIndexSelection(text); if (idx && Array.isArray(pending.options)) { const opt = pending.options.find((o) => o.idx === idx); if (opt?.type === "more") return { kind: "more", question: null, pending }; if (opt?.woo_product_id) { const chosen = pending.candidates.find((c) => c.woo_product_id === opt.woo_product_id) || null; if (chosen) return { kind: "chosen", chosen }; } } const selText = nlu?.entities?.selection?.type === "text" ? String(nlu.entities.selection.value || "").trim() : null; const q = selText || nlu?.entities?.product_query || null; if (q) { const scored = pending.candidates .map((c) => ({ c, s: scoreTextMatch(q, c?.name) })) .sort((a, b) => b.s - a.s); if (scored[0]?.s >= 0.6 && (!scored[1] || scored[0].s - scored[1].s >= 0.2)) { return { kind: "chosen", chosen: scored[0].c }; } } return { kind: "ask" }; }