From 829823ac3dc0dc69bd08981f1085a1d6b1cd582e Mon Sep 17 00:00:00 2001 From: Lucas Tettamanti <757326+lkzwieder@users.noreply.github.com> Date: Mon, 5 Jan 2026 10:58:54 -0300 Subject: [PATCH] styling and create customer woo bug handled --- index.js | 2 + public/components/chat-simulator.js | 7 +- public/components/run-timeline.js | 100 +++++++++++++++++++++------- public/lib/api.js | 7 ++ src/controllers/messages.js | 18 +++++ src/db/repo.js | 21 ++++++ src/handlers/messages.js | 12 ++++ 7 files changed, 141 insertions(+), 26 deletions(-) create mode 100644 src/controllers/messages.js create mode 100644 src/handlers/messages.js diff --git a/index.js b/index.js index 991bb93..19e712e 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,7 @@ import { makeListRuns, makeGetRunById } from "./src/controllers/runs.js"; import { makeSimSend } from "./src/controllers/sim.js"; import { makeEvolutionWebhook } from "./src/controllers/evolution.js"; import { makeGetConversationState } from "./src/controllers/conversationState.js"; +import { makeListMessages } from "./src/controllers/messages.js"; async function configureUndiciDispatcher() { // Node 18+ usa undici debajo de fetch. Esto suele arreglar timeouts “fantasma” por keep-alive/pooling. @@ -75,6 +76,7 @@ app.post("/sim/send", makeSimSend()); app.get("/conversations", makeGetConversations(() => TENANT_ID)); app.get("/conversations/state", makeGetConversationState(() => TENANT_ID)); +app.get("/messages", makeListMessages(() => TENANT_ID)); app.get("/runs", makeListRuns(() => TENANT_ID)); diff --git a/public/components/chat-simulator.js b/public/components/chat-simulator.js index 7881c7b..a6e16ef 100644 --- a/public/components/chat-simulator.js +++ b/public/components/chat-simulator.js @@ -16,9 +16,10 @@ class ChatSimulator extends HTMLElement { button { cursor:pointer; } button.primary { background:#1f6feb; border-color:#1f6feb; } .chatlog { display:flex; flex-direction:column; gap:8px; max-height:280px; overflow:auto; padding-right:6px; margin-top:8px; } - .msg { max-width:90%; padding:8px 10px; border-radius:12px; border:1px solid #253245; background:#0f1520; font-size:13px; } - .msg.user { align-self:flex-end; } - .msg.bot { align-self:flex-start; border-color:#1f6feb; } + /* WhatsApp-ish dark theme bubbles */ + .msg { max-width:90%; padding:8px 10px; border-radius:14px; border:1px solid #253245; font-size:13px; line-height:1.35; white-space:pre-wrap; word-break:break-word; box-shadow: 0 1px 0 rgba(0,0,0,.35); } + .msg.user { align-self:flex-end; background:#0f2a1a; border-color:#1f6f43; color:#e7eef7; } + .msg.bot { align-self:flex-start; background:#111b2a; border-color:#2a3a55; color:#e7eef7; } pre { white-space:pre-wrap; word-break:break-word; background:#0f1520; border:1px solid #253245; border-radius:10px; padding:10px; margin:0; font-size:12px; color:#d7e2ef; } diff --git a/public/components/run-timeline.js b/public/components/run-timeline.js index 7276418..1fbf60b 100644 --- a/public/components/run-timeline.js +++ b/public/components/run-timeline.js @@ -6,6 +6,8 @@ class RunTimeline extends HTMLElement { super(); this.attachShadow({ mode: "open" }); this.chatId = null; + this.items = []; + this.selectedDebug = null; this.shadowRoot.innerHTML = ` @@ -21,29 +35,33 @@ class RunTimeline extends HTMLElement {
Conversación
Seleccioná una conversación.
+
+ + +
+
-
Último run (raw)
-
+
Debug (click en burbuja roja para ver detalles)
+
-
-
Invariantes
-
-
`; } connectedCallback() { + this.shadowRoot.getElementById("refresh").onclick = () => this.loadMessages(); + this._unsubSel = on("ui:selectedChat", async ({ chat_id }) => { this.chatId = chat_id; - await this.loadLatest(); + await this.loadMessages(); }); this._unsubRun = on("run:created", (run) => { if (this.chatId && run.chat_id === this.chatId) { - this.show(run); + // nuevo run => refrescar mensajes para ver los bubbles actualizados + this.loadMessages(); } }); } @@ -53,29 +71,65 @@ class RunTimeline extends HTMLElement { this._unsubRun?.(); } - async loadLatest() { + async loadMessages() { this.shadowRoot.getElementById("chat").textContent = this.chatId || "—"; this.shadowRoot.getElementById("meta").textContent = "Cargando…"; + this.shadowRoot.getElementById("count").textContent = ""; - const data = await api.runs({ chat_id: this.chatId, limit: 1 }); - const run = (data.items || [])[0]; - - if (!run) { - this.shadowRoot.getElementById("meta").textContent = "Sin runs aún."; - this.shadowRoot.getElementById("run").textContent = "—"; - this.shadowRoot.getElementById("inv").textContent = "—"; + if (!this.chatId) { + this.shadowRoot.getElementById("meta").textContent = "Seleccioná una conversación."; + this.shadowRoot.getElementById("log").innerHTML = ""; + this.shadowRoot.getElementById("debug").textContent = "—"; return; } - this.show(run); + + const data = await api.messages({ chat_id: this.chatId, limit: 200 }); + this.items = data.items || []; + this.render(); } - show(run) { - this.shadowRoot.getElementById("chat").textContent = run.chat_id; - this.shadowRoot.getElementById("meta").textContent = - `run_id=${run.run_id} • ${run.latency_ms}ms • ${new Date(run.ts).toLocaleString()}`; + isErrorMsg(m) { + const txt = String(m?.text || ""); + return Boolean(m?.payload?.error) || txt.startsWith("[ERROR]") || txt.toLowerCase().includes("internal_error"); + } - this.shadowRoot.getElementById("run").textContent = JSON.stringify(run, null, 2); - this.shadowRoot.getElementById("inv").textContent = JSON.stringify(run.invariants, null, 2); + render() { + const meta = this.shadowRoot.getElementById("meta"); + const count = this.shadowRoot.getElementById("count"); + const log = this.shadowRoot.getElementById("log"); + const dbg = this.shadowRoot.getElementById("debug"); + + meta.textContent = `Mostrando historial (últimos ${this.items.length}).`; + count.textContent = this.items.length ? `${this.items.length} msgs` : ""; + + log.innerHTML = ""; + + 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.textContent = m.text || (isErr ? "Error" : "—"); + + const metaEl = document.createElement("span"); + metaEl.className = "meta"; + metaEl.textContent = `${new Date(m.ts).toLocaleString()} • ${m.provider} • ${m.message_id}`; + bubble.appendChild(metaEl); + + if (isErr) { + bubble.title = "Click para ver detalles (JSON)"; + bubble.onclick = () => { + dbg.textContent = JSON.stringify(m, null, 2); + }; + } + + log.appendChild(bubble); + } + + // auto-scroll + log.scrollTop = log.scrollHeight; + + if (!this.items.length) dbg.textContent = "—"; } } diff --git a/public/lib/api.js b/public/lib/api.js index db9dff5..ec27145 100644 --- a/public/lib/api.js +++ b/public/lib/api.js @@ -7,6 +7,13 @@ export const api = { return fetch(u).then(r => r.json()); }, + async messages({ chat_id, limit = 200 } = {}) { + const u = new URL("/messages", location.origin); + if (chat_id) u.searchParams.set("chat_id", chat_id); + u.searchParams.set("limit", String(limit)); + return fetch(u).then(r => r.json()); + }, + async runs({ chat_id, limit = 1 } = {}) { const u = new URL("/runs", location.origin); if (chat_id) u.searchParams.set("chat_id", chat_id); diff --git a/src/controllers/messages.js b/src/controllers/messages.js new file mode 100644 index 0000000..f9a3e1d --- /dev/null +++ b/src/controllers/messages.js @@ -0,0 +1,18 @@ +import { handleListMessages } from "../handlers/messages.js"; + +export const makeListMessages = (tenantIdOrFn) => async (req, res) => { + try { + const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn; + const items = await handleListMessages({ + tenantId, + chat_id: req.query.chat_id || null, + limit: req.query.limit || "200", + }); + res.json({ items }); + } catch (err) { + console.error(err); + res.status(500).json({ ok: false, error: "internal_error" }); + } +}; + + diff --git a/src/db/repo.js b/src/db/repo.js index 482ddf6..04a5809 100644 --- a/src/db/repo.js +++ b/src/db/repo.js @@ -278,6 +278,27 @@ export async function getRecentMessagesForLLM({ })); } +export async function listMessages({ tenant_id, wa_chat_id, limit = 200 }) { + const lim = Math.max(1, Math.min(500, parseInt(limit, 10) || 200)); + const q = ` + select provider, message_id, direction, ts, text, payload, run_id + from wa_messages + where tenant_id=$1 and wa_chat_id=$2 + order by ts asc + limit $3 + `; + const { rows } = await pool.query(q, [tenant_id, wa_chat_id, lim]); + return rows.map((r) => ({ + provider: r.provider, + message_id: r.message_id, + direction: r.direction, + ts: r.ts, + text: r.text, + payload: r.payload, + run_id: r.run_id, + })); +} + export async function getTenantByKey(key) { const { rows } = await pool.query(`select id, key, name from tenants where key=$1`, [key]); return rows[0] || null; diff --git a/src/handlers/messages.js b/src/handlers/messages.js new file mode 100644 index 0000000..12e2541 --- /dev/null +++ b/src/handlers/messages.js @@ -0,0 +1,12 @@ +import { listMessages } from "../db/repo.js"; + +export async function handleListMessages({ tenantId, chat_id, limit = "200" }) { + if (!chat_id) return []; + return listMessages({ + tenant_id: tenantId, + wa_chat_id: String(chat_id), + limit: parseInt(limit, 10) || 200, + }); +} + +