feat(soporte): botón de borrador que redacta con la base de conocimiento

En cada ticket, " Borrador" propone una respuesta usando los fragmentos más
parecidos de la base de conocimiento del agente de uMind que se elija en la
configuración de soporte, más el hilo de la conversación.

El borrador cae en el cuadro de respuesta y no se manda: lo revisa una persona
y aprieta enviar. Contestarle a un cliente con lo que dijo un modelo, sin que
nadie lo lea, es la forma más rápida de perderlo.

Sin agente configurado el borrador se arma igual, solo con la conversación —
peor, pero mejor que un error. El prompt le prohíbe inventar precios, plazos o
pasos que no estén en la documentación, y le pide decir qué falta preguntar
cuando no alcanza para resolver.

El cuadro de respuesta pasa a textarea: un borrador de varios renglones no
entraba en un input de una línea.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-17 19:45:38 -05:00
co-authored by Claude Opus 5
parent bfcc320b7c
commit 1b599606f7
7 changed files with 225 additions and 54 deletions
+25
View File
@@ -217,6 +217,24 @@
<p class="text-xs text-slate-400 mt-1">Ayuda a la IA a distinguir tus casos raros. Los correos masivos se descartan por sus encabezados, sin gastar una llamada.</p>
</div>
<!-- ─── Borradores de respuesta ──────────────────────────────── -->
<div class="border-t border-slate-200 pt-5">
<p class="text-sm font-semibold text-slate-700 mb-1">Borradores de respuesta</p>
<p class="text-xs text-slate-400 mb-3">
En cada ticket aparece un botón <strong>✨ Borrador</strong> que propone una respuesta usando la base
de conocimiento del agente que elijas. <strong>Nunca la envía</strong>: cae en el cuadro de respuesta
para que la revises y la mandes vos. Sin agente elegido el borrador sale igual, pero solo con la
conversación del ticket.
</p>
<label class="block text-xs font-medium text-slate-600 mb-1">Agente de uMind</label>
<select x-model="cfg.agente_borrador_id" class="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm outline-none">
<option value="">Sin base de conocimiento</option>
<template x-for="a in agentes" :key="a.id">
<option :value="a.id" x-text="a.nombre"></option>
</template>
</select>
</div>
<div class="pt-4">
<button @click="guardar()"
class="px-6 py-2 rounded-xl text-white text-sm font-medium transition-colors"
@@ -263,8 +281,10 @@ function soporteWebhook() {
imap_horas_atras: 12,
clasificar_con_ia: false,
contexto_negocio: '',
agente_borrador_id: '',
},
admins: [],
agentes: [],
tieneImapPassword: false,
ocupado: false,
mensajeImap: '',
@@ -287,6 +307,10 @@ function soporteWebhook() {
const r = await axios.get('/app/tickets/admins');
this.admins = r.data || [];
} catch {}
try {
const r = await axios.get('/app/soporte/agentes');
this.agentes = r.data || [];
} catch {}
if (!this.cfg.api_key) {
this.cfg.api_key = this.generarClave();
}
@@ -329,6 +353,7 @@ function soporteWebhook() {
imap_horas_atras: Number(this.cfg.imap_horas_atras) || 0,
clasificar_con_ia: !!this.cfg.clasificar_con_ia,
contexto_negocio: this.cfg.contexto_negocio || '',
agente_borrador_id: this.cfg.agente_borrador_id ? parseInt(this.cfg.agente_borrador_id) : null,
};
try {
await axios.post('/app/soporte/webhook', payload);
+22 -4
View File
@@ -103,10 +103,16 @@
<!-- Responder -->
<div class="mt-3 flex gap-2">
<input x-model="t._reply" type="text" placeholder="Escribir respuesta..."
class="flex-1 border border-slate-200 rounded-xl px-3 py-2 text-sm outline-none"
@keydown.enter.prevent="responder(t)"
onfocus="this.style.borderColor='#8eb02f'" onblur="this.style.borderColor='#e2e8f0'">
<!-- Textarea y no input: el borrador de la IA viene en varios renglones. -->
<textarea x-model="t._reply" rows="2" placeholder="Escribir respuesta..."
class="flex-1 border border-slate-200 rounded-xl px-3 py-2 text-sm outline-none resize-y"
@keydown.enter.exact.prevent="responder(t)"
onfocus="this.style.borderColor='#8eb02f'" onblur="this.style.borderColor='#e2e8f0'"></textarea>
<button @click="redactar(t)" :disabled="t._redactando"
title="Propone una respuesta con la base de conocimiento. No la envía."
class="px-3 py-2 rounded-xl border border-slate-300 text-sm text-slate-700 disabled:opacity-50 flex-shrink-0">
<span x-text="t._redactando ? 'Redactando…' : '✨ Borrador'"></span>
</button>
<button @click="responder(t)"
class="px-4 py-2 rounded-xl text-white text-sm font-medium transition-colors"
style="background:#8eb02f"
@@ -196,6 +202,18 @@ function ticketsAdmin() {
}
},
async redactar(t) {
t._redactando = true;
try {
const { data } = await axios.post(`/app/tickets/${t.ID}/borrador`);
// Se escribe en el cuadro de respuesta, no se envía: lo revisa una persona.
t._reply = data.borrador || '';
} catch (e) {
alert(e.response?.data?.error || e.message);
}
t._redactando = false;
},
async responder(t) {
const contenido = (t._reply || '').trim();
if (!contenido) return;