leonardo vor 4 Wochen
Ursprung
Commit
3798a964bf

+ 14 - 0
src/components/ChatMessageContent.vue

@@ -0,0 +1,14 @@
+<template>
+  <div class="prose-content" v-html="html" />
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { renderMarkdown } from "../utils/markdownRenderer.js";
+
+const props = defineProps({
+  content: { type: String, default: "" }
+});
+
+const html = computed(() => renderMarkdown(props.content));
+</script>

+ 4 - 51
src/components/ChatWindow.vue

@@ -52,7 +52,7 @@
 
               <template v-else>
 
-                <div class="prose-content" v-html="renderMarkdown(m.content)" />
+                <ChatMessageContent :content="m.content" />
                 <span
                   v-if="m.streaming"
                   class="ml-0.5 inline-block h-[1em] w-0.5 animate-pulse rounded-sm bg-current align-middle opacity-60"
@@ -132,13 +132,12 @@
 
 <script setup>
 import { ref, computed, nextTick, onMounted, onUnmounted, watch } from "vue";
-import { marked, Renderer } from "marked";
-import DOMPurify from "dompurify";
-import hljs from "highlight.js";
+import { getStoredCode } from "../utils/markdownRenderer.js";
 import BaseButton from "./base/BaseButton.vue";
 import BaseIcon from "./base/BaseIcon.vue";
 import { Copy, Check, FileText, Send, X } from "lucide-vue-next";
 import ChatModeToggle from "./ChatModeToggle.vue";
+import ChatMessageContent from "./ChatMessageContent.vue";
 
 const props = defineProps({
   messages: { type: Array, required: true },
@@ -166,43 +165,6 @@ const streamingStatusMsg = computed(() => {
 });
 
 
-const renderer = new Renderer();
-
-const CODE_BLOCK_STORE_MAX = 500;
-let codeBlockCounter = 0;
-const codeBlockStore = new Map();
-
-function storeCodeBlock(text) {
-  const id = String(codeBlockCounter++);
-  codeBlockStore.set(id, text);
-  if (codeBlockStore.size > CODE_BLOCK_STORE_MAX) {
-    codeBlockStore.delete(codeBlockStore.keys().next().value);
-  }
-  return id;
-}
-
-renderer.code = ({ text, lang }) => {
-  const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
-  const highlighted = hljs.highlight(text, { language }).value;
-  const id = storeCodeBlock(text);
-  return `<div class="code-block-wrapper">
-    <div class="code-block-header">
-      <span class="code-lang">${language}</span>
-      <button class="code-copy-btn" data-code-id="${id}" aria-label="Copiar código">
-        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
-        Copiar
-      </button>
-    </div>
-    <pre><code class="hljs language-${language}">${highlighted}</code></pre>
-  </div>`;
-};
-
-marked.use({
-  renderer,
-  breaks: true,
-  gfm: true,
-});
-
 function agruparFontes(sources) {
   const porDocumento = new Map();
   for (const s of sources ?? []) {
@@ -216,21 +178,12 @@ function agruparFontes(sources) {
   return [...porDocumento.values()];
 }
 
-function renderMarkdown(content) {
-  if (!content) return "";
-  const html = marked.parse(content);
-  return DOMPurify.sanitize(html, {
-    ADD_ATTR: ["data-code-id"],
-    ADD_TAGS: ["button"],
-  });
-}
-
 const codeCopyTimeouts = new Set();
 
 function handleCodeCopy(e) {
   const btn = e.target.closest(".code-copy-btn");
   if (!btn) return;
-  const code = codeBlockStore.get(btn.dataset.codeId) ?? "";
+  const code = getStoredCode(btn.dataset.codeId);
   navigator.clipboard.writeText(code).then(() => {
     btn.classList.add("copied");
     btn.querySelector("svg").style.display = "none";

+ 9 - 1
src/composables/useConversas.js

@@ -76,11 +76,16 @@ export function useConversas() {
     }
   }
 
+  function handleVisibilityChange() {
+    if (document.visibilityState === "visible") buscarNovasMensagens();
+  }
+
   function pararPolling() {
     if (pollTimer) {
       clearInterval(pollTimer);
       pollTimer = null;
     }
+    document.removeEventListener("visibilitychange", handleVisibilityChange);
   }
 
   async function buscarSugestao() {
@@ -152,7 +157,10 @@ export function useConversas() {
       if (conversaAtivaId.value === id) mensagensLoading.value = false;
     }
 
-    pollTimer = setInterval(buscarNovasMensagens, POLL_INTERVAL_MS);
+    pollTimer = setInterval(() => {
+      if (document.visibilityState === "visible") buscarNovasMensagens();
+    }, POLL_INTERVAL_MS);
+    document.addEventListener("visibilitychange", handleVisibilityChange);
   }
 
   function fecharConversa() {

+ 14 - 13
src/router/index.js

@@ -1,19 +1,20 @@
 import { createRouter, createWebHistory } from "vue-router";
-import PaginaInicialView from "../views/pagina-inicial/PaginaInicialView.vue";
-import ConversasHistoricoView from "../views/conversas/ConversasHistoricoView.vue";
-import ConversasPesquisarView from "../views/conversas/ConversasPesquisarView.vue";
-import ConfiguracoesView from "../views/configuracoes/ConfiguracoesView.vue";
-import UsuariosView from "../views/usuarios/UsuariosView.vue";
-import AvaliacoesView from "../views/atendimentos/AvaliacoesView.vue";
-import NaoResolvidosView from "../views/atendimentos/NaoResolvidosView.vue";
-import AtendimentoView from "../views/atendimentos/AtendimentoView.vue";
-import GoldenSetView from "../views/atendimentos/GoldenSetView.vue";
-import ConversasWhatsappView from "../views/conversas/ConversasWhatsappView.vue";
-import DocumentosView from "../views/documentos/DocumentosView.vue";
-import CarregarDocumentoView from "../views/documentos/CarregarDocumentoView.vue";
-import LoginView from "../views/auth/LoginView.vue";
 import { useAuth } from "../composables/useAuth.js";
 
+const PaginaInicialView = () => import("../views/pagina-inicial/PaginaInicialView.vue");
+const ConversasHistoricoView = () => import("../views/conversas/ConversasHistoricoView.vue");
+const ConversasPesquisarView = () => import("../views/conversas/ConversasPesquisarView.vue");
+const ConfiguracoesView = () => import("../views/configuracoes/ConfiguracoesView.vue");
+const UsuariosView = () => import("../views/usuarios/UsuariosView.vue");
+const AvaliacoesView = () => import("../views/atendimentos/AvaliacoesView.vue");
+const NaoResolvidosView = () => import("../views/atendimentos/NaoResolvidosView.vue");
+const AtendimentoView = () => import("../views/atendimentos/AtendimentoView.vue");
+const GoldenSetView = () => import("../views/atendimentos/GoldenSetView.vue");
+const ConversasWhatsappView = () => import("../views/conversas/ConversasWhatsappView.vue");
+const DocumentosView = () => import("../views/documentos/DocumentosView.vue");
+const CarregarDocumentoView = () => import("../views/documentos/CarregarDocumentoView.vue");
+const LoginView = () => import("../views/auth/LoginView.vue");
+
 export const router = createRouter({
   history: createWebHistory(import.meta.env.BASE_URL),
   routes: [

+ 82 - 0
src/utils/markdownRenderer.js

@@ -0,0 +1,82 @@
+import { marked, Renderer } from "marked";
+import DOMPurify from "dompurify";
+import hljs from "highlight.js/lib/core";
+import javascript from "highlight.js/lib/languages/javascript";
+import typescript from "highlight.js/lib/languages/typescript";
+import python from "highlight.js/lib/languages/python";
+import bash from "highlight.js/lib/languages/bash";
+import json from "highlight.js/lib/languages/json";
+import sql from "highlight.js/lib/languages/sql";
+import xml from "highlight.js/lib/languages/xml";
+import css from "highlight.js/lib/languages/css";
+import yaml from "highlight.js/lib/languages/yaml";
+import markdown from "highlight.js/lib/languages/markdown";
+import java from "highlight.js/lib/languages/java";
+import csharp from "highlight.js/lib/languages/csharp";
+import php from "highlight.js/lib/languages/php";
+import plaintext from "highlight.js/lib/languages/plaintext";
+
+hljs.registerLanguage("javascript", javascript);
+hljs.registerLanguage("typescript", typescript);
+hljs.registerLanguage("python", python);
+hljs.registerLanguage("bash", bash);
+hljs.registerLanguage("json", json);
+hljs.registerLanguage("sql", sql);
+hljs.registerLanguage("xml", xml);
+hljs.registerLanguage("css", css);
+hljs.registerLanguage("yaml", yaml);
+hljs.registerLanguage("markdown", markdown);
+hljs.registerLanguage("java", java);
+hljs.registerLanguage("csharp", csharp);
+hljs.registerLanguage("php", php);
+hljs.registerLanguage("plaintext", plaintext);
+
+const renderer = new Renderer();
+
+const CODE_BLOCK_STORE_MAX = 500;
+let codeBlockCounter = 0;
+const codeBlockStore = new Map();
+
+function storeCodeBlock(text) {
+  const id = String(codeBlockCounter++);
+  codeBlockStore.set(id, text);
+  if (codeBlockStore.size > CODE_BLOCK_STORE_MAX) {
+    codeBlockStore.delete(codeBlockStore.keys().next().value);
+  }
+  return id;
+}
+
+export function getStoredCode(id) {
+  return codeBlockStore.get(id) ?? "";
+}
+
+renderer.code = ({ text, lang }) => {
+  const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
+  const highlighted = hljs.highlight(text, { language }).value;
+  const id = storeCodeBlock(text);
+  return `<div class="code-block-wrapper">
+    <div class="code-block-header">
+      <span class="code-lang">${language}</span>
+      <button class="code-copy-btn" data-code-id="${id}" aria-label="Copiar código">
+        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
+        Copiar
+      </button>
+    </div>
+    <pre><code class="hljs language-${language}">${highlighted}</code></pre>
+  </div>`;
+};
+
+marked.use({
+  renderer,
+  breaks: true,
+  gfm: true,
+});
+
+export function renderMarkdown(content) {
+  if (!content) return "";
+  const html = marked.parse(content);
+  return DOMPurify.sanitize(html, {
+    ADD_ATTR: ["data-code-id"],
+    ADD_TAGS: ["button"],
+  });
+}

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

@@ -222,13 +222,24 @@ async function enviar() {
 
 let refreshTimer = null;
 
+function pollConversas() {
+  if (document.visibilityState !== "visible") return;
+  carregarConversas();
+}
+
+function handleVisibilityChange() {
+  if (document.visibilityState === "visible") carregarConversas();
+}
+
 onMounted(() => {
   carregarConversas();
-  refreshTimer = setInterval(carregarConversas, 5000);
+  refreshTimer = setInterval(pollConversas, 5000);
+  document.addEventListener("visibilitychange", handleVisibilityChange);
 });
 
 onUnmounted(() => {
   if (refreshTimer) clearInterval(refreshTimer);
+  document.removeEventListener("visibilitychange", handleVisibilityChange);
   fecharConversa();
 });
 </script>