GabrielRamison 2 месяцев назад
Родитель
Сommit
2866318e10

+ 2 - 2
dist/index.html

@@ -6,8 +6,8 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
     <link rel="icon" type="image/x-icon" href="/favicon.ico?v=2" />
     <title>ORÁCULO</title>
-    <script type="module" crossorigin src="/assets/index-fdZrkscK.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-Giq68b07.css">
+    <script type="module" crossorigin src="/assets/index-D0c_LByC.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-BdiuteT-.css">
   </head>
   <body class="bg-background">
     <noscript>

+ 2 - 1
src/api/atendimentos.js

@@ -27,12 +27,13 @@ export function avaliarAtendimento(id) {
   return apiFetch(`/api/atendimentos/${id}/avaliar`, { method: "POST", body: {} });
 }
 
-export function listarAvaliacoes({ page = 1, pageSize = 20, setor, resolvido, sentimento, scoreMax } = {}) {
+export function listarAvaliacoes({ page = 1, pageSize = 20, setor, resolvido, sentimento, scoreMax, busca } = {}) {
   const query = new URLSearchParams({ page: String(page), pageSize: String(pageSize) });
   if (setor) query.set("setor", setor);
   if (resolvido) query.set("resolvido", resolvido);
   if (sentimento) query.set("sentimento", sentimento);
   if (scoreMax !== undefined) query.set("scoreMax", String(scoreMax));
+  if (busca) query.set("busca", busca);
   return apiFetch(`/api/atendimentos/avaliacoes?${query.toString()}`);
 }
 

+ 3 - 1
src/router/index.js

@@ -5,6 +5,7 @@ import ConversasPesquisarView from "../views/conversas/ConversasPesquisarView.vu
 import ConfiguracoesView from "../views/configuracoes/ConfiguracoesView.vue";
 import UsuariosView from "../views/usuarios/UsuariosView.vue";
 import AvaliacoesView from "../views/atendimentos/AvaliacoesView.vue";
+import AtendimentoView from "../views/atendimentos/AtendimentoView.vue";
 import LoginView from "../views/auth/LoginView.vue";
 import { useAuth } from "../composables/useAuth.js";
 
@@ -18,6 +19,7 @@ export const router = createRouter({
     { path: "/configuracoes", name: "configuracoes", component: ConfiguracoesView, meta: { requiresAuth: true } },
     { path: "/usuarios", name: "usuarios", component: UsuariosView, meta: { requiresAuth: true } },
     { path: "/atendimentos/avaliacoes", name: "avaliacoes", component: AvaliacoesView, meta: { requiresAuth: true } },
+    { path: "/atendimentos/:id(\\d+)", name: "atendimento", component: AtendimentoView, meta: { requiresAuth: true } },
     { path: "/:pathMatch(.*)*", redirect: "/" }
   ]
 });
@@ -36,7 +38,7 @@ router.beforeEach((to) => {
     };
   }
 
-  if (to.name === "usuarios" || to.name === "avaliacoes") {
+  if (to.name === "usuarios" || to.name === "avaliacoes" || to.name === "atendimento") {
     const { user } = useAuth();
 
     if (String(user.value?.Nivel) !== "3") {

+ 13 - 0
src/utils/avaliacoes.js

@@ -0,0 +1,13 @@
+// aparência dos badges de avaliação, compartilhada entre a lista e a tela da conversa
+export function scoreVariant(score) {
+  if (score === null || score === undefined) return "default";
+  if (score >= 8) return "success";
+  if (score >= 6) return "info";
+  if (score >= 4) return "warning";
+  return "danger";
+}
+
+export const resolvidoVariant = { sim: "success", parcial: "warning", nao: "danger", indefinido: "default" };
+export const resolvidoLabel = { sim: "Resolvido", parcial: "Parcial", nao: "Não resolvido", indefinido: "Indefinido" };
+export const sentimentoVariant = { positivo: "success", neutro: "default", negativo: "danger", indefinido: "default" };
+export const sentimentoLabel = { positivo: "Positivo", neutro: "Neutro", negativo: "Negativo", indefinido: "Indefinido" };

+ 219 - 0
src/views/atendimentos/AtendimentoView.vue

@@ -0,0 +1,219 @@
+<script setup>
+import { computed, onMounted, ref } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import LayoutSistema from "../../layout/LayoutSistema.vue";
+import BaseBadge from "../../components/base/BaseBadge.vue";
+import { obterAtendimento } from "../../api/atendimentos.js";
+import { resolvidoLabel, resolvidoVariant, scoreVariant, sentimentoLabel, sentimentoVariant } from "../../utils/avaliacoes.js";
+
+const route = useRoute();
+const router = useRouter();
+
+const carregando = ref(true);
+const erro = ref("");
+const atendimento = ref(null);
+
+const avaliacao = computed(() => atendimento.value?.avaliacao ?? null);
+const mensagens = computed(() => atendimento.value?.mensagens ?? []);
+
+// volta para a lista de avaliações preservando página, filtros e avaliação aberta
+// (guardados na query string da rota anterior); se o gestor chegou por link direto,
+// não há histórico para voltar — vai para a lista limpa
+function voltar() {
+  const anterior = window.history.state?.back;
+  if (typeof anterior === "string" && anterior.startsWith("/atendimentos/avaliacoes")) {
+    router.back();
+  } else {
+    router.push({ name: "avaliacoes" });
+  }
+}
+
+function autorLabel(m) {
+  if (m.Papel === "atendente") return m.Autor ?? "Atendente";
+  if (m.Papel === "cliente") return atendimento.value?.cliente?.Nome ?? "Cliente";
+  return "Desconhecido";
+}
+
+function midiaLink(m) {
+  return m.Midia && /^https?:\/\//i.test(m.Midia) ? m.Midia : null;
+}
+
+const tiposAudio = new Set(["mpga", "oga", "mp3", "audio", "ptt"]);
+
+function ehAudio(m) {
+  return Boolean(m.Transcricao) || tiposAudio.has(m.Tipodemidia);
+}
+
+// path de armazenamento do arquivo (ex.: /data/aud/xxx.mp3); em áudios antigos sem
+// Midia o próprio Body/Texto é o path
+function caminhoMidia(m) {
+  if (m.Midia) return m.Midia;
+  if (m.Texto && /^\/[\w./-]+$/.test(m.Texto)) return m.Texto;
+  return null;
+}
+
+function formatData(value) {
+  if (!value) return "";
+  const d = new Date(value);
+  if (Number.isNaN(d.getTime())) return "";
+  return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" });
+}
+
+function formatHora(value) {
+  if (!value) return "";
+  const d = new Date(value);
+  if (Number.isNaN(d.getTime())) return "";
+  return d.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
+}
+
+function novoDia(idx) {
+  const atual = mensagens.value[idx]?.Timestamp;
+  if (!atual) return false;
+  if (idx === 0) return true;
+  const anterior = mensagens.value[idx - 1]?.Timestamp;
+  if (!anterior) return true;
+  return new Date(atual).toDateString() !== new Date(anterior).toDateString();
+}
+
+onMounted(async () => {
+  try {
+    atendimento.value = await obterAtendimento(route.params.id);
+  } catch (err) {
+    erro.value = err?.message ?? "Falha ao carregar o atendimento";
+  } finally {
+    carregando.value = false;
+  }
+});
+</script>
+
+<template>
+  <LayoutSistema>
+    <div class="min-h-[calc(100vh-var(--layout-header-height))] px-4 pt-8 pb-12">
+      <div class="mx-auto w-full max-w-[960px] space-y-6">
+        <section
+          class="rounded-2xl border border-gray-200 bg-gray-100/50 p-6 shadow-sm backdrop-blur-md dark:border-gray-700 dark:bg-secondary/50"
+        >
+          <div class="flex flex-wrap items-start justify-between gap-4">
+            <div class="flex min-w-0 items-start gap-4">
+              <button
+                type="button"
+                class="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border border-gray-300 text-gray-600 hover:bg-black/[0.03] dark:border-gray-600 dark:text-gray-300 dark:hover:bg-white/[0.04]"
+                title="Voltar para as avaliações"
+                @click="voltar"
+              >
+                <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                  <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
+                </svg>
+              </button>
+              <div class="min-w-0">
+                <h2 class="text-base font-semibold text-gray-900 dark:text-gray-100">
+                  {{ atendimento?.Codigo ?? `Atendimento #${route.params.id}` }}
+                  <span v-if="atendimento?.cliente?.Nome" class="font-normal text-gray-500 dark:text-gray-400">
+                    — {{ atendimento.cliente.Nome }}
+                  </span>
+                </h2>
+                <p v-if="atendimento" class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
+                  <span v-if="atendimento.Setor">{{ atendimento.Setor }} · </span>{{ formatData(atendimento.Abertura) }}
+                  · {{ mensagens.length }} mensagens
+                </p>
+              </div>
+            </div>
+            <div v-if="avaliacao" class="flex flex-wrap items-center gap-2">
+              <BaseBadge :variant="scoreVariant(avaliacao.ScoreAtendente)">
+                {{ avaliacao.ScoreAtendente !== null && avaliacao.ScoreAtendente !== undefined ? `Nota ${avaliacao.ScoreAtendente}` : "Sem nota" }}
+              </BaseBadge>
+              <BaseBadge :variant="resolvidoVariant[avaliacao.Resolvido] ?? 'default'">
+                {{ resolvidoLabel[avaliacao.Resolvido] ?? avaliacao.Resolvido }}
+              </BaseBadge>
+              <BaseBadge :variant="sentimentoVariant[avaliacao.Sentimento] ?? 'default'">
+                {{ sentimentoLabel[avaliacao.Sentimento] ?? avaliacao.Sentimento }}
+              </BaseBadge>
+            </div>
+          </div>
+
+          <p v-if="avaliacao?.Resumo" class="mt-4 text-sm text-gray-600 dark:text-gray-300">
+            <span class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Resumo da IA: </span>
+            {{ avaliacao.Resumo }}
+          </p>
+        </section>
+
+        <section class="rounded-2xl border border-gray-200 bg-background/70 shadow-sm dark:border-gray-700 dark:bg-background/20">
+          <div class="border-b border-gray-200 px-4 py-3 text-sm font-semibold text-gray-900 dark:border-gray-700 dark:text-gray-100">
+            Conversa completa
+          </div>
+
+          <div v-if="carregando" class="p-6 text-sm text-gray-500 dark:text-gray-400">Carregando conversa...</div>
+          <div v-else-if="erro" class="m-4 rounded-lg bg-red-50 px-4 py-3 text-xs text-red-800 dark:bg-red-900/30 dark:text-red-300">
+            {{ erro }}
+          </div>
+          <div v-else-if="mensagens.length === 0" class="p-6 text-sm text-gray-500 dark:text-gray-400">
+            Este atendimento não tem mensagens com conteúdo para exibir.
+          </div>
+
+          <div v-else class="space-y-2 p-4">
+            <template v-for="(m, i) in mensagens" :key="m.Id">
+              <div v-if="novoDia(i)" class="flex justify-center pt-2 first:pt-0">
+                <span class="rounded-full bg-gray-200/70 px-3 py-0.5 text-[11px] font-medium text-gray-500 dark:bg-gray-700/70 dark:text-gray-400">
+                  {{ formatData(m.Timestamp) }}
+                </span>
+              </div>
+
+              <div v-if="m.Papel === 'sistema'" class="flex justify-center">
+                <div class="max-w-[85%] px-3 py-1 text-center text-xs italic text-gray-400 dark:text-gray-500">
+                  {{ m.Texto }}<span v-if="formatHora(m.Timestamp)"> · {{ formatHora(m.Timestamp) }}</span>
+                </div>
+              </div>
+
+              <div v-else class="flex" :class="m.Papel === 'atendente' ? 'justify-end' : 'justify-start'">
+                <div
+                  class="max-w-[75%] rounded-2xl px-3.5 py-2 shadow-sm"
+                  :class="
+                    m.Papel === 'atendente'
+                      ? 'rounded-br-sm bg-primary/10 dark:bg-primary/20'
+                      : 'rounded-bl-sm bg-gray-100 dark:bg-gray-800'
+                  "
+                >
+                  <div
+                    class="text-xs font-semibold"
+                    :class="m.Papel === 'atendente' ? 'text-primary' : 'text-gray-500 dark:text-gray-400'"
+                  >
+                    {{ autorLabel(m) }}
+                  </div>
+                  <template v-if="ehAudio(m)">
+                    <div class="mt-1 flex items-center gap-1.5 text-xs font-medium text-gray-500 dark:text-gray-400">
+                      <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
+                        <path stroke-linecap="round" stroke-linejoin="round" d="M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 1 1 6 0v8.25a3 3 0 0 1-3 3Z" />
+                      </svg>
+                      Mensagem de áudio
+                    </div>
+                    <p v-if="m.Transcricao" class="mt-1 whitespace-pre-wrap break-words text-sm text-gray-800 dark:text-gray-200">
+                      {{ m.Transcricao }}
+                    </p>
+                    <p v-else class="mt-1 text-sm italic text-gray-400 dark:text-gray-500">Sem transcrição disponível</p>
+                    <div
+                      v-if="caminhoMidia(m)"
+                      class="mt-1.5 break-all rounded bg-black/[0.04] px-2 py-1 font-mono text-[11px] text-gray-500 dark:bg-white/[0.06] dark:text-gray-400"
+                    >
+                      {{ caminhoMidia(m) }}
+                    </div>
+                  </template>
+                  <p v-else class="mt-0.5 whitespace-pre-wrap break-words text-sm text-gray-800 dark:text-gray-200">{{ m.Texto }}</p>
+                  <a
+                    v-if="midiaLink(m)"
+                    :href="midiaLink(m)"
+                    target="_blank"
+                    rel="noopener noreferrer"
+                    class="mt-1 inline-block text-xs font-medium text-primary underline"
+                  >
+                    Abrir mídia
+                  </a>
+                  <div class="mt-0.5 text-right text-[10px] text-gray-400 dark:text-gray-500">{{ formatHora(m.Timestamp) }}</div>
+                </div>
+              </div>
+            </template>
+          </div>
+        </section>
+      </div>
+    </div>
+  </LayoutSistema>
+</template>

+ 100 - 24
src/views/atendimentos/AvaliacoesView.vue

@@ -1,5 +1,6 @@
 <script setup>
-import { computed, onBeforeUnmount, onMounted, ref } from "vue";
+import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
+import { useRoute, useRouter } from "vue-router";
 import LayoutSistema from "../../layout/LayoutSistema.vue";
 import BaseBadge from "../../components/base/BaseBadge.vue";
 import {
@@ -7,6 +8,10 @@ import {
   estatisticasAvaliacoes,
   listarAvaliacoes
 } from "../../api/atendimentos.js";
+import { resolvidoLabel, resolvidoVariant, scoreVariant, sentimentoLabel, sentimentoVariant } from "../../utils/avaliacoes.js";
+
+const route = useRoute();
+const router = useRouter();
 
 const carregando = ref(false);
 const avaliandoLote = ref(false);
@@ -16,12 +21,25 @@ const loteResultado = ref(null);
 const stats = ref(null);
 const avaliacoes = ref([]);
 const total = ref(0);
-const page = ref(1);
 const pageSize = 20;
-const expandido = ref(null);
 
-const filtroResolvido = ref("");
-const filtroSentimento = ref("");
+// página, filtros e avaliação aberta vivem na query string para o gestor não perder
+// o ponto da revisão ao entrar na conversa completa e voltar
+const page = ref(Math.max(1, Number(route.query.page) || 1));
+const expandido = ref(route.query.aberto ? Number(route.query.aberto) : null);
+const filtroResolvido = ref(typeof route.query.resolvido === "string" ? route.query.resolvido : "");
+const filtroSentimento = ref(typeof route.query.sentimento === "string" ? route.query.sentimento : "");
+const filtroBusca = ref(typeof route.query.q === "string" ? route.query.q : "");
+
+function sincronizarQuery() {
+  const query = {};
+  if (page.value > 1) query.page = String(page.value);
+  if (filtroResolvido.value) query.resolvido = filtroResolvido.value;
+  if (filtroSentimento.value) query.sentimento = filtroSentimento.value;
+  if (filtroBusca.value.trim()) query.q = filtroBusca.value.trim();
+  if (expandido.value) query.aberto = String(expandido.value);
+  router.replace({ query });
+}
 
 const totalPaginas = computed(() => Math.max(1, Math.ceil(total.value / pageSize)));
 
@@ -32,19 +50,6 @@ const progresso = computed(() => {
   return { total: totalAt, feitos, pct: Math.round((feitos / totalAt) * 100) };
 });
 
-function scoreVariant(score) {
-  if (score === null || score === undefined) return "default";
-  if (score >= 8) return "success";
-  if (score >= 6) return "info";
-  if (score >= 4) return "warning";
-  return "danger";
-}
-
-const resolvidoVariant = { sim: "success", parcial: "warning", nao: "danger", indefinido: "default" };
-const resolvidoLabel = { sim: "Resolvido", parcial: "Parcial", nao: "Não resolvido", indefinido: "Indefinido" };
-const sentimentoVariant = { positivo: "success", neutro: "default", negativo: "danger", indefinido: "default" };
-const sentimentoLabel = { positivo: "Positivo", neutro: "Neutro", negativo: "Negativo", indefinido: "Indefinido" };
-
 function formatData(value) {
   if (!value) return "";
   const d = new Date(value);
@@ -73,7 +78,8 @@ async function carregarLista() {
       page: page.value,
       pageSize,
       resolvido: filtroResolvido.value || undefined,
-      sentimento: filtroSentimento.value || undefined
+      sentimento: filtroSentimento.value || undefined,
+      busca: filtroBusca.value.trim() || undefined
     });
     avaliacoes.value = r.results ?? [];
     total.value = r.total ?? 0;
@@ -87,13 +93,28 @@ async function carregarLista() {
 function aplicarFiltros() {
   page.value = 1;
   expandido.value = null;
+  sincronizarQuery();
   carregarLista();
 }
 
+// busca digitável: espera a pessoa parar de digitar antes de consultar
+let buscaTimer = null;
+
+function onBuscaInput() {
+  clearTimeout(buscaTimer);
+  buscaTimer = setTimeout(aplicarFiltros, 400);
+}
+
+function onBuscaEnter() {
+  clearTimeout(buscaTimer);
+  aplicarFiltros();
+}
+
 function irParaPagina(nova) {
   if (nova < 1 || nova > totalPaginas.value || nova === page.value) return;
   page.value = nova;
   expandido.value = null;
+  sincronizarQuery();
   carregarLista();
 }
 
@@ -114,14 +135,26 @@ async function avaliarPendentes() {
 
 function alternarExpandido(id) {
   expandido.value = expandido.value === id ? null : id;
+  sincronizarQuery();
+}
+
+function verConversa(id) {
+  router.push({ name: "atendimento", params: { id } });
 }
 
 // enquanto houver pendentes, atualiza as estatísticas sozinho para a barra andar
 let statsTimer = null;
 
-onMounted(() => {
+onMounted(async () => {
   carregarStats();
-  carregarLista();
+  await carregarLista();
+
+  // voltando da conversa completa, reposiciona na avaliação que estava aberta
+  if (expandido.value) {
+    await nextTick();
+    document.getElementById(`avaliacao-${expandido.value}`)?.scrollIntoView({ block: "center" });
+  }
+
   statsTimer = setInterval(() => {
     if (stats.value?.pendentes > 0 && !avaliandoLote.value) carregarStats();
   }, 30_000);
@@ -129,6 +162,7 @@ onMounted(() => {
 
 onBeforeUnmount(() => {
   if (statsTimer) clearInterval(statsTimer);
+  clearTimeout(buscaTimer);
 });
 </script>
 
@@ -234,6 +268,36 @@ onBeforeUnmount(() => {
 
         <section class="rounded-2xl border border-gray-200 bg-background/70 shadow-sm dark:border-gray-700 dark:bg-background/20">
           <div class="flex flex-wrap items-center gap-3 border-b border-gray-200 p-4 dark:border-gray-700">
+            <div class="relative min-w-[240px] flex-1 sm:max-w-[340px]">
+              <svg
+                class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400 dark:text-gray-500"
+                viewBox="0 0 24 24"
+                fill="none"
+                stroke="currentColor"
+                stroke-width="2"
+              >
+                <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-4.35-4.35M17 10.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0Z" />
+              </svg>
+              <input
+                v-model="filtroBusca"
+                type="search"
+                placeholder="Protocolo, cliente, atendente ou data (dd/mm/aaaa)"
+                class="w-full rounded-lg border border-gray-300 bg-transparent py-1.5 pl-9 pr-8 text-sm text-gray-700 placeholder:text-gray-400 dark:border-gray-600 dark:text-gray-200 dark:placeholder:text-gray-500"
+                @input="onBuscaInput"
+                @keyup.enter="onBuscaEnter"
+              />
+              <button
+                v-if="filtroBusca"
+                type="button"
+                class="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
+                title="Limpar busca"
+                @click="filtroBusca = ''; onBuscaEnter();"
+              >
+                <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                  <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
+                </svg>
+              </button>
+            </div>
             <select
               v-model="filtroResolvido"
               class="rounded-lg border border-gray-300 bg-transparent px-3 py-1.5 text-sm text-gray-700 dark:border-gray-600 dark:bg-transparent dark:text-gray-200 [&>option]:dark:bg-gray-800"
@@ -267,7 +331,7 @@ onBeforeUnmount(() => {
           </div>
 
           <ul v-else class="divide-y divide-gray-200 dark:divide-gray-700">
-            <li v-for="a in avaliacoes" :key="a.AtendimentoId">
+            <li v-for="a in avaliacoes" :id="`avaliacao-${a.AtendimentoId}`" :key="a.AtendimentoId">
               <button
                 type="button"
                 class="flex w-full flex-wrap items-center gap-2 px-4 py-3 text-left hover:bg-black/[0.03] dark:hover:bg-white/[0.04]"
@@ -313,8 +377,20 @@ onBeforeUnmount(() => {
                   <template v-if="a.HorarioVisitaInformado === 'sim'">horário informado — {{ a.HorarioVisita }}</template>
                   <template v-else>agendada, mas o atendente não informou o horário</template>
                 </div>
-                <div class="text-xs text-gray-400 dark:text-gray-500">
-                  Avaliado por {{ a.Modelo }} em {{ formatData(a.UpdatedAt) }} · {{ a.TotalMensagens }} mensagens
+                <div class="flex flex-wrap items-center justify-between gap-3">
+                  <button
+                    type="button"
+                    class="inline-flex items-center gap-1.5 rounded-lg border border-gray-300 px-3 py-1.5 text-xs font-semibold text-gray-700 hover:bg-black/[0.03] dark:border-gray-600 dark:text-gray-200 dark:hover:bg-white/[0.04]"
+                    @click="verConversa(a.AtendimentoId)"
+                  >
+                    Ver conversa completa
+                    <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                      <path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
+                    </svg>
+                  </button>
+                  <div class="text-xs text-gray-400 dark:text-gray-500">
+                    Avaliado por {{ a.Modelo }} em {{ formatData(a.UpdatedAt) }} · {{ a.TotalMensagens }} mensagens
+                  </div>
                 </div>
               </div>
             </li>