leonardo há 1 mês atrás
pai
commit
d1767661fd

+ 4 - 0
src/api/conversas.js

@@ -17,6 +17,10 @@ export function excluirConversa(id) {
   return apiFetch(`/api/conversas/${id}`, { method: "DELETE" });
 }
 
+export function gerarSugestaoResposta(id) {
+  return apiFetch(`/api/conversas/${id}/sugestao`, { method: "POST"});
+}
+
 // a rota de mídia exige Bearer token, então <img>/<audio>/<video> não podem apontar
 // direto pra ela — busca o arquivo autenticado e devolve um blob URL local
 export async function buscarMidiaBlob(conversaId, mensagemId) {

+ 35 - 2
src/composables/useConversas.js

@@ -1,5 +1,5 @@
 import { ref } from "vue";
-import { listarConversas, listarMensagensConversa, responderConversa, excluirConversa as excluirConversaApi, buscarMidiaBlob } from "../api/conversas.js";
+import { listarConversas, listarMensagensConversa, responderConversa, excluirConversa as excluirConversaApi, buscarMidiaBlob, gerarSugestaoResposta } from "../api/conversas.js";
 
 let singleton;
 
@@ -27,6 +27,9 @@ export function useConversas() {
   const enviando = ref(false);
   const midiaUrls = ref({});
   const midiaErros = ref({});
+  const sugestao = ref(null);
+  const sugestaoLoading = ref(false);
+  const sugestaoError =  ref("");
 
   let pollTimer = null;
   let ultimoTimestamp = null;
@@ -76,6 +79,24 @@ export function useConversas() {
     }
   }
 
+  async function buscarSugestao() {
+    if(!conversaAtivaId.value) return;
+    const idAlvo = conversaAtivaId.value;
+    sugestaoLoading.value = true;
+    sugestaoError.value = "";
+    try {
+      const { sugestao: texto } = await gerarSugestaoResposta(idAlvo);
+      if ( conversaAtivaId.value !== idAlvo) return;
+      sugestao.value = texto;
+    } catch (err){
+      if ( conversaAtivaId.value !== idAlvo) return;
+      sugestao.value = null;
+      sugestaoError.value = err?.message ?? "Falha ao gerar sugestão";
+    } finally {
+      if (conversaAtivaId.value === idAlvo) sugestaoLoading.value = false;
+    }
+  }
+
   async function buscarNovasMensagens() {
     if (!conversaAtivaId.value || buscandoNovasMensagens) return;
     buscandoNovasMensagens = true;
@@ -86,6 +107,7 @@ export function useConversas() {
         const novas = results.filter((m) => !idsExistentes.has(m.Id));
         mensagens.value = [...mensagens.value, ...novas];
         ultimoTimestamp = results[results.length - 1].Timestamp;
+        if ( novas.some((m) => m.Direcao === "entrada")) buscarSugestao();
       }
       await carregarConversas();
     } catch {
@@ -109,6 +131,9 @@ export function useConversas() {
       if (conversaAtivaId.value !== id) return;
       mensagens.value = results;
       ultimoTimestamp = results.length > 0 ? results[results.length - 1].Timestamp : null;
+      sugestao.value = null;
+      sugestaoError.value = "";
+      if (results.length && results[results.length - 1].Direcao === "entrada") buscarSugestao();
     } catch (err) {
       if (conversaAtivaId.value !== id) return;
       mensagensError.value = err?.message ?? "Falha ao carregar mensagens";
@@ -125,6 +150,8 @@ export function useConversas() {
     mensagens.value = [];
     limparMidiaCache();
     ultimoTimestamp = null;
+    sugestao.value = null;
+    sugestaoError.value = "";
   }
 
   async function enviarResposta(texto) {
@@ -134,6 +161,8 @@ export function useConversas() {
       const mensagem = await responderConversa(conversaAtivaId.value, texto.trim());
       mensagens.value.push(mensagem);
       ultimoTimestamp = mensagem.Timestamp;
+      sugestao.value = null;
+      sugestaoError.value = "";
       await carregarConversas();
     } finally {
       enviando.value = false;
@@ -157,12 +186,16 @@ export function useConversas() {
     enviando,
     midiaUrls,
     midiaErros,
+    sugestao,
+    sugestaoLoading,
+    sugestaoError,
     resolverMidia,
     carregarConversas,
     abrirConversa,
     fecharConversa,
     enviarResposta,
-    excluirConversa
+    excluirConversa,
+    buscarSugestao
   };
 
   return singleton;

+ 32 - 1
src/views/conversas/ConversasWhatsappView.vue

@@ -1,5 +1,5 @@
 <script setup>
-import { computed, onMounted, onUnmounted, ref, watch } from "vue";
+import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
 import LayoutSistema from "../../layout/LayoutSistema.vue";
 import ModalWhatsappConexao from "../../components/ModalWhatsappConexao.vue";
 import ModalMidia from "../../components/ModalMidia.vue";
@@ -23,6 +23,9 @@ const {
   enviando,
   midiaUrls,
   midiaErros,
+  sugestao,
+  sugestaoLoading,
+  sugestaoError,
   resolverMidia,
   carregarConversas,
   abrirConversa,
@@ -114,6 +117,13 @@ async function desconectar() {
 }
 
 const texto = ref("");
+const inputResposta = ref(null);
+
+function usarSugestao() {
+  if (!sugestao.value) return;
+  texto.value = sugestao.value;
+  nextTick(() => inputResposta.value?.focus());
+}
 
 const conversaAtiva = computed(() => conversas.value.find((c) => c.Id === conversaAtivaId.value) ?? null);
 
@@ -369,8 +379,29 @@ onUnmounted(() => {
             </div>
           </div>
 
+          <div v-if="sugestao" class="mx-3 mb-2 rounded-xl border border-primary/30 bg-primary/5 p-3 dark:border-primary/40 dark:bg-primary/10">
+            <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-primary">
+              Sugestão do star-atendente
+            </div>
+            <p class="whitespace-pre-wrap break-words text-sm text-gray-800 dark:text-gray-200">{{ sugestao }}</p>
+            <button
+              type="button"
+              class="mt-2 rounded-lg bg-primary px-3 py-1 text-xs font-medium text-white"
+              @click="usarSugestao"
+            >
+              Usar esta resposta
+            </button>
+          </div>
+          <div v-else-if="sugestaoLoading" class="mx-3 mb-2 text-xs italic text-gray-400 dark:text-gray-500">
+            Gerando sugestão...
+          </div>
+          <div v-else-if="sugestaoError" class="mx-3 mb-2 text-xs text-gray-400 dark:text-gray-500">
+            {{ sugestaoError }}
+          </div>
+
           <form class="flex items-center gap-2 border-t border-gray-200 p-3 dark:border-gray-700" @submit.prevent="enviar">
             <input
+              ref="inputResposta"
               v-model="texto"
               type="text"
               placeholder="Digite uma resposta..."