From 8bb21b4edbb1a41bfde99d9382311eecd86d052d Mon Sep 17 00:00:00 2001
From: Lucas Tettamanti <757326+lkzwieder@users.noreply.github.com>
Date: Tue, 6 Jan 2026 15:50:02 -0300
Subject: [PATCH] mejoras varias en frontend, separacion de intent y state,
pick de articulos
---
index.js | 5 +
public/app.js | 1 +
public/components/chat-simulator.js | 170 ++--
public/components/conversation-list.js | 146 +++-
public/components/debug-panel.js | 71 ++
public/components/ops-shell.js | 10 +-
public/components/run-timeline.js | 56 +-
public/lib/api.js | 29 +
src/controllers/admin.js | 48 ++
src/db/repo.js | 156 ++++
src/handlers/admin.js | 91 +++
src/handlers/evolution.js | 2 +-
src/services/evolutionParser.js | 2 +
src/services/openai.js | 109 ++-
src/services/pipeline.js | 1043 ++++++++++++++++++++++--
src/services/woo.js | 39 +
src/services/wooProducts.js | 57 ++
17 files changed, 1826 insertions(+), 209 deletions(-)
create mode 100644 public/components/debug-panel.js
create mode 100644 src/controllers/admin.js
create mode 100644 src/handlers/admin.js
diff --git a/index.js b/index.js
index c5cc351..eec1b4a 100644
--- a/index.js
+++ b/index.js
@@ -12,6 +12,7 @@ import { makeEvolutionWebhook } from "./src/controllers/evolution.js";
import { makeGetConversationState } from "./src/controllers/conversationState.js";
import { makeListMessages } from "./src/controllers/messages.js";
import { makeSearchProducts } from "./src/controllers/products.js";
+import { makeDeleteConversation, makeDeleteUser, makeListUsers, makeRetryLast } from "./src/controllers/admin.js";
async function configureUndiciDispatcher() {
// Node 18+ usa undici debajo de fetch. Esto suele arreglar timeouts “fantasma” por keep-alive/pooling.
@@ -77,8 +78,12 @@ app.post("/sim/send", makeSimSend());
app.get("/conversations", makeGetConversations(() => TENANT_ID));
app.get("/conversations/state", makeGetConversationState(() => TENANT_ID));
+app.delete("/conversations/:chat_id", makeDeleteConversation(() => TENANT_ID));
+app.post("/conversations/:chat_id/retry-last", makeRetryLast(() => TENANT_ID));
app.get("/messages", makeListMessages(() => TENANT_ID));
app.get("/products", makeSearchProducts(() => TENANT_ID));
+app.get("/users", makeListUsers(() => TENANT_ID));
+app.delete("/users/:chat_id", makeDeleteUser(() => TENANT_ID));
app.get("/runs", makeListRuns(() => TENANT_ID));
diff --git a/public/app.js b/public/app.js
index efb63db..8c203e2 100644
--- a/public/app.js
+++ b/public/app.js
@@ -2,6 +2,7 @@ import "./components/ops-shell.js";
import "./components/conversation-list.js";
import "./components/run-timeline.js";
import "./components/chat-simulator.js";
+import "./components/debug-panel.js";
import { connectSSE } from "./lib/sse.js";
connectSSE();
diff --git a/public/components/chat-simulator.js b/public/components/chat-simulator.js
index a6e16ef..6c14d33 100644
--- a/public/components/chat-simulator.js
+++ b/public/components/chat-simulator.js
@@ -5,6 +5,8 @@ class ChatSimulator extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
+ this._lastPayload = null;
+ this._sending = false;
this.shadowRoot.innerHTML = `
@@ -35,19 +33,15 @@ class ChatSimulator extends HTMLElement {
-
+
- Chat
-
-
+ Enviar mensaje
+
+
-
-
-
-
Última respuesta (raw)
-
—
+
—
`;
}
@@ -59,8 +53,54 @@ class ChatSimulator extends HTMLElement {
const evoPushEl = this.shadowRoot.getElementById("pushName");
const evoTextEl = this.shadowRoot.getElementById("evoText");
const sendEvoEl = this.shadowRoot.getElementById("sendEvo");
+ const retryEl = this.shadowRoot.getElementById("retry");
+ const statusEl = this.shadowRoot.getElementById("status");
- const sendAction = async () => {
+ const genId = () =>
+ (self.crypto?.randomUUID?.() || `${Date.now()}${Math.random()}`)
+ .replace(/-/g, "")
+ .slice(0, 22)
+ .toUpperCase();
+
+ const buildPayload = ({ text, from, to, instance, pushName }) => {
+ const nowSec = Math.floor(Date.now() / 1000);
+ return {
+ body: {
+ event: "messages.upsert",
+ instance,
+ data: {
+ key: {
+ remoteJid: from,
+ fromMe: false,
+ id: genId(),
+ participant: "",
+ addressingMode: "pn",
+ },
+ pushName: pushName || "test_lucas",
+ status: "DELIVERY_ACK",
+ message: { conversation: text },
+ messageType: "conversation",
+ messageTimestamp: nowSec,
+ instanceId: genId(),
+ source: "sim",
+ to,
+ },
+ date_time: new Date().toISOString(),
+ sender: from,
+ server_url: "http://localhost",
+ apikey: "SIM",
+ },
+ };
+ };
+
+ const setSending = (v) => {
+ this._sending = Boolean(v);
+ sendEvoEl.disabled = this._sending;
+ retryEl.disabled = this._sending;
+ statusEl.textContent = this._sending ? "Enviando…" : "—";
+ };
+
+ const sendAction = async ({ payloadOverride = null } = {}) => {
const instance = evoInstanceEl.value.trim() || "Piaf";
const from = evoFromEl.value.trim() || "5491133230322@s.whatsapp.net"; // cliente
const to = evoToEl.value.trim() || "5491137887040@s.whatsapp.net"; // canal/destino
@@ -72,57 +112,53 @@ class ChatSimulator extends HTMLElement {
return;
}
- const nowSec = Math.floor(Date.now() / 1000);
- const genId = () =>
- (self.crypto?.randomUUID?.() || `${Date.now()}${Math.random()}`)
- .replace(/-/g, "")
- .slice(0, 22)
- .toUpperCase();
-
- const payload = {
- body: {
- event: "messages.upsert",
- instance,
- data: {
- key: {
- // remoteJid debe ser el cliente (buyer)
- remoteJid: from,
- fromMe: false,
- id: genId(),
- participant: "",
- addressingMode: "pn",
- },
- pushName: pushName || "SimUser",
- status: "DELIVERY_ACK",
- message: { conversation: text },
- messageType: "conversation",
- messageTimestamp: nowSec,
- instanceId: genId(),
- source: "sim",
- },
- date_time: new Date().toISOString(),
- sender: from,
- server_url: "http://localhost",
- apikey: "SIM",
- },
- };
-
- const data = await api.simEvolution(payload);
- this.shadowRoot.getElementById("raw").textContent = JSON.stringify(data, null, 2);
- console.log("[evolution sim] webhook response:", data);
-
- if (!data.ok) {
- this.append("bot", "Error en Evolution Sim.");
- return;
- }
-
+ // Optimistic: que aparezca en la columna izquierda al instante
+ emit("conversation:upsert", {
+ chat_id: from,
+ from: pushName || "test_lucas",
+ state: "IDLE",
+ intent: "other",
+ status: "ok",
+ last_activity: new Date().toISOString(),
+ last_run_id: null,
+ });
emit("ui:selectedChat", { chat_id: from });
- this.append("user", text);
- this.append("bot", `[Evolution] enviado (sim): ${text}`);
- evoTextEl.value = "";
+
+ const payload = payloadOverride || buildPayload({ text, from, to, instance, pushName });
+ this._lastPayload = { ...payload, body: { ...payload.body, data: { ...payload.body.data, key: { ...payload.body.data.key, id: genId() } } } };
+ setSending(true);
+ try {
+ const data = await api.simEvolution(payload);
+ console.log("[evolution sim] webhook response:", data);
+
+ if (!data.ok) {
+ statusEl.textContent = "Error enviando (ver consola)";
+ return;
+ }
+
+ evoTextEl.value = "";
+ evoTextEl.focus();
+ } catch (e) {
+ statusEl.textContent = `Error: ${String(e?.message || e)}`;
+ } finally {
+ setSending(false);
+ }
};
sendEvoEl.onclick = sendAction;
+ retryEl.onclick = () => {
+ const chat_id = evoFromEl.value.trim() || "5491133230322@s.whatsapp.net";
+ setSending(true);
+ api
+ .retryLast(chat_id)
+ .then((r) => {
+ if (!r?.ok) statusEl.textContent = `Retry error: ${r?.error || "unknown"}`;
+ else statusEl.textContent = "Retry enviado.";
+ })
+ .catch((e) => (statusEl.textContent = `Retry error: ${String(e?.message || e)}`))
+ .finally(() => setSending(false));
+ };
+ retryEl.disabled = false;
evoTextEl.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
@@ -144,12 +180,8 @@ class ChatSimulator extends HTMLElement {
}
append(who, text) {
- const log = this.shadowRoot.getElementById("log");
- const el = document.createElement("div");
- el.className = "msg " + (who === "user" ? "user" : "bot");
- el.textContent = text;
- log.appendChild(el);
- log.scrollTop = log.scrollHeight;
+ void who;
+ void text;
}
}
diff --git a/public/components/conversation-list.js b/public/components/conversation-list.js
index e4fc541..34b73aa 100644
--- a/public/components/conversation-list.js
+++ b/public/components/conversation-list.js
@@ -7,6 +7,9 @@ class ConversationList extends HTMLElement {
this.attachShadow({ mode: "open" });
this.conversations = [];
this.selected = null;
+ this.tab = "chats"; // chats | users
+ this.users = [];
+ this.selectedUser = null;
this.shadowRoot.innerHTML = `
-
+
+
+
-
+
+
+
+
+
+
+
+
state: ${c.state}
@@ -108,7 +226,15 @@ class ConversationList extends HTMLElement {
last: ${new Date(c.last_activity).toLocaleTimeString()}
`;
- el.onclick = () => {
+ el.onclick = async (e) => {
+ if (e?.target?.dataset?.del) {
+ e.stopPropagation();
+ if (!confirm(`¿Borrar conversación completa de ${c.chat_id}?`)) return;
+ await api.deleteConversation(c.chat_id);
+ if (this.selected === c.chat_id) this.selected = null;
+ await this.refresh();
+ return;
+ }
this.selected = c.chat_id;
this.render();
emit("ui:selectedChat", { chat_id: c.chat_id });
diff --git a/public/components/debug-panel.js b/public/components/debug-panel.js
new file mode 100644
index 0000000..5f828ea
--- /dev/null
+++ b/public/components/debug-panel.js
@@ -0,0 +1,71 @@
+import { api } from "../lib/api.js";
+import { on } from "../lib/bus.js";
+
+class DebugPanel extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open" });
+
+ this.shadowRoot.innerHTML = `
+
+
+
+
Detalle (click en cualquier burbuja)
+
+
—
+
+ `;
+ }
+
+ connectedCallback() {
+ const out = this.shadowRoot.getElementById("out");
+ const intentEl = this.shadowRoot.getElementById("intent");
+ const stateEl = this.shadowRoot.getElementById("state");
+ this._unsub = on("ui:selectedMessage", async ({ message }) => {
+ if (!message) {
+ out.textContent = "—";
+ intentEl.textContent = "—";
+ stateEl.textContent = "—";
+ return;
+ }
+ const base = { message };
+ if (message.run_id) {
+ try {
+ const run = await api.runById(message.run_id);
+ base.run = run;
+ const intent = run?.llm_output?.intent || "—";
+ const state = run?.llm_output?.next_state || "—";
+ intentEl.textContent = intent;
+ stateEl.textContent = state;
+ } catch (e) {
+ base.run_error = String(e?.message || e);
+ intentEl.textContent = "—";
+ stateEl.textContent = "—";
+ }
+ } else {
+ intentEl.textContent = "—";
+ stateEl.textContent = "—";
+ }
+ out.textContent = JSON.stringify(base, null, 2);
+ });
+ }
+
+ disconnectedCallback() {
+ this._unsub?.();
+ }
+}
+
+customElements.define("debug-panel", DebugPanel);
+
+
diff --git a/public/components/ops-shell.js b/public/components/ops-shell.js
index 1179482..9c59171 100644
--- a/public/components/ops-shell.js
+++ b/public/components/ops-shell.js
@@ -17,6 +17,9 @@ class OpsShell extends HTMLElement {
.layout { flex:1; display:grid; grid-template-columns:320px 1fr 360px; min-height:0; }
.col { border-right:1px solid var(--line); min-height:0; overflow:auto; }
.col:last-child { border-right:none; }
+ .mid { display:flex; flex-direction:column; min-height:0; }
+ .midTop { flex:1; min-height:0; overflow:auto; border-bottom:1px solid var(--line); }
+ .midBottom { min-height:220px; overflow:auto; }
@@ -28,8 +31,11 @@ class OpsShell extends HTMLElement {
`;
diff --git a/public/components/run-timeline.js b/public/components/run-timeline.js
index 1fbf60b..3c208e9 100644
--- a/public/components/run-timeline.js
+++ b/public/components/run-timeline.js
@@ -1,5 +1,5 @@
import { api } from "../lib/api.js";
-import { on } from "../lib/bus.js";
+import { emit, on } from "../lib/bus.js";
class RunTimeline extends HTMLElement {
constructor() {
@@ -7,7 +7,6 @@ class RunTimeline extends HTMLElement {
this.attachShadow({ mode: "open" });
this.chatId = null;
this.items = [];
- this.selectedDebug = null;
this.shadowRoot.innerHTML = `