import { api } from "../lib/api.js"; import { emit, on } from "../lib/bus.js"; class RunTimeline extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); this.chatId = null; this.items = []; this.shadowRoot.innerHTML = `
Conversación
Seleccioná una conversación.
`; } connectedCallback() { this.shadowRoot.getElementById("refresh").onclick = () => this.loadMessages(); this._unsubSel = on("ui:selectedChat", async ({ chat_id }) => { // Si es el mismo chat, no recargar (para no borrar burbujas optimistas) if (this.chatId === chat_id) return; this.chatId = chat_id; await this.loadMessages(); }); this._unsubRun = on("run:created", (run) => { if (this.chatId && run.chat_id === this.chatId) { // nuevo run => refrescar mensajes para ver los bubbles actualizados this.loadMessages(); } }); this._unsubHighlight = on("ui:highlightMessage", ({ message_id }) => { this.highlightMessage(message_id); }); // Listen for inspector heights to sync bubble heights this._unsubInspector = on("ui:inspectorLayout", ({ chat_id, items }) => { if (!this.chatId || chat_id !== this.chatId) return; this.applyInspectorHeights(items); }); // Listen for optimistic messages (show bubble immediately before API response) this._unsubOptimistic = on("message:optimistic", (msg) => { // Si no hay chatId seteado, setearlo al del mensaje if (!this.chatId) { this.chatId = msg.chat_id; this.shadowRoot.getElementById("chat").textContent = msg.chat_id; this.shadowRoot.getElementById("meta").textContent = "Nueva conversación"; } if (msg.chat_id !== this.chatId) return; this.addOptimisticBubble(msg); }); } disconnectedCallback() { this._unsubSel?.(); this._unsubRun?.(); this._unsubHighlight?.(); this._unsubInspector?.(); this._unsubOptimistic?.(); } async loadMessages() { this.shadowRoot.getElementById("chat").textContent = this.chatId || "—"; this.shadowRoot.getElementById("meta").textContent = "Cargando…"; this.shadowRoot.getElementById("count").textContent = ""; if (!this.chatId) { this.shadowRoot.getElementById("meta").textContent = "Seleccioná una conversación."; this.shadowRoot.getElementById("log").innerHTML = ""; return; } try { const data = await api.messages({ chat_id: this.chatId, limit: 200 }); this.items = data.items || []; this.render(); } catch (e) { this.items = []; this.shadowRoot.getElementById("meta").textContent = `Error cargando mensajes: ${String( e?.message || e )}`; this.shadowRoot.getElementById("log").innerHTML = ""; } } isErrorMsg(m) { const txt = String(m?.text || ""); return Boolean(m?.payload?.error) || txt.startsWith("[ERROR]") || txt.toLowerCase().includes("internal_error"); } displayNameFor(m) { // Inbound: usar pushName si vino del webhook; fallback al "from" (teléfono) si existe. const pushName = m?.payload?.raw?.meta?.pushName || m?.payload?.raw?.meta?.pushname || null; const from = m?.payload?.raw?.from || null; if (m?.direction === "in") return pushName || from || "test_lucas"; // Outbound: nombre del bot return "Piaf"; } render() { const meta = this.shadowRoot.getElementById("meta"); const count = this.shadowRoot.getElementById("count"); const log = this.shadowRoot.getElementById("log"); meta.textContent = `Mostrando historial (últimos ${this.items.length}).`; count.textContent = this.items.length ? `${this.items.length} msgs` : ""; // Capturar info de burbujas optimistas antes de limpiar const optimisticBubbles = [...log.querySelectorAll('.bubble[data-message-id^="optimistic-"]')]; const optimisticTexts = optimisticBubbles.map(b => { const textEl = b.querySelector("div:not(.name):not(.meta)"); return (textEl ? textEl.textContent : "").trim().toLowerCase(); }); log.innerHTML = ""; // Obtener textos de mensajes IN del servidor (normalizados para comparación) const serverUserTexts = this.items .filter(m => m.direction === "in") .map(m => (m.text || "").trim().toLowerCase()); for (const m of this.items) { const who = m.direction === "in" ? "user" : "bot"; const isErr = this.isErrorMsg(m); const bubble = document.createElement("div"); bubble.className = `bubble ${isErr ? "err" : who}`; bubble.dataset.messageId = m.message_id; const nameEl = document.createElement("span"); nameEl.className = "name"; nameEl.textContent = this.displayNameFor(m); bubble.appendChild(nameEl); const textEl = document.createElement("div"); textEl.textContent = m.text || (isErr ? "Error" : "—"); bubble.appendChild(textEl); const metaEl = document.createElement("span"); metaEl.className = "meta"; metaEl.textContent = `${new Date(m.ts).toLocaleString()} • ${m.provider} • ${m.message_id}`; bubble.appendChild(metaEl); bubble.title = "Click para ver detalles (JSON)"; bubble.onclick = () => emit("ui:selectedMessage", { message: m }); log.appendChild(bubble); } // Re-agregar burbujas optimistas SOLO si su texto no está ya en los mensajes del servidor // Comparación case-insensitive y trimmed let addedOptimistic = false; for (let i = 0; i < optimisticBubbles.length; i++) { const optText = optimisticTexts[i]; // Si el texto ya existe en un mensaje del servidor, no re-agregar if (serverUserTexts.includes(optText)) { continue; } log.appendChild(optimisticBubbles[i]); addedOptimistic = true; } // auto-scroll solo si agregamos burbujas optimistas nuevas if (addedOptimistic) { log.scrollTop = log.scrollHeight; } requestAnimationFrame(() => this.emitLayout()); this.bindScroll(log); } bindScroll(log) { if (this._scrollBound) return; this._scrollBound = true; log.addEventListener("scroll", () => { emit("ui:chatScroll", { chat_id: this.chatId, scrollTop: log.scrollTop }); }); } emitLayout() { const log = this.shadowRoot.getElementById("log"); const box = this.shadowRoot.querySelector(".box"); const bubbles = [...log.querySelectorAll(".bubble")]; const items = bubbles.map((el) => { const styles = window.getComputedStyle(el); const marginBottom = parseInt(styles.marginBottom || "0", 10) || 0; return { message_id: el.dataset.messageId || null, height: (el.offsetHeight || 0) + marginBottom, }; }); emit("ui:bubblesLayout", { chat_id: this.chatId, items }); } highlightMessage(message_id) { const log = this.shadowRoot.getElementById("log"); if (!log) return; const bubbles = [...log.querySelectorAll(".bubble")]; for (const el of bubbles) { el.classList.toggle("active", el.dataset.messageId === message_id); } // No auto-scroll - mantener posición actual del usuario } applyInspectorHeights(items) { const log = this.shadowRoot.getElementById("log"); if (!log) return; const BUBBLE_MARGIN = 12; const MIN_HEIGHT = 120; const heightMap = new Map((items || []).map((it) => [it.message_id, it.height || 0])); const bubbles = [...log.querySelectorAll(".bubble")]; for (const el of bubbles) { const messageId = el.dataset.messageId; const inspectorHeight = heightMap.get(messageId) || 0; // Inspector height includes margin, extract content height const inspectorContentHeight = Math.max(0, inspectorHeight - BUBBLE_MARGIN); // Use max between inspector height and our minimum const targetHeight = Math.max(inspectorContentHeight, MIN_HEIGHT); // Always apply to ensure sync el.style.minHeight = `${targetHeight}px`; el.style.marginBottom = `${BUBBLE_MARGIN}px`; } } addOptimisticBubble(msg) { const log = this.shadowRoot.getElementById("log"); if (!log) return; // Check if already exists (by optimistic ID pattern) const existing = log.querySelector(`.bubble[data-message-id^="optimistic-"]`); if (existing) existing.remove(); const bubble = document.createElement("div"); bubble.className = "bubble user"; bubble.dataset.messageId = msg.message_id; const nameEl = document.createElement("span"); nameEl.className = "name"; nameEl.textContent = msg.pushName || "test_lucas"; bubble.appendChild(nameEl); const textEl = document.createElement("div"); textEl.textContent = msg.text || "—"; bubble.appendChild(textEl); const metaEl = document.createElement("span"); metaEl.className = "meta"; metaEl.textContent = `${new Date(msg.ts).toLocaleString()} • ${msg.provider} • enviando...`; bubble.appendChild(metaEl); log.appendChild(bubble); // Solo hacer scroll si el usuario ya estaba cerca del final (dentro de 100px) const wasNearBottom = (log.scrollHeight - log.scrollTop - log.clientHeight) < 100; if (wasNearBottom) { log.scrollTop = log.scrollHeight; } // Emit layout update requestAnimationFrame(() => this.emitLayout()); } } customElements.define("run-timeline", RunTimeline);