Просмотр исходного кода

correção de alguns bugs do chat

leonardo 2 месяцев назад
Родитель
Сommit
2c79c512b9
2 измененных файлов с 44 добавлено и 14 удалено
  1. 33 7
      src/components/ChatWindow.vue
  2. 11 7
      src/layout/ListaConversas.vue

+ 33 - 7
src/components/ChatWindow.vue

@@ -168,14 +168,27 @@ const streamingStatusMsg = computed(() => {
 
 
 const renderer = new Renderer();
 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 }) => {
 renderer.code = ({ text, lang }) => {
   const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
   const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
   const highlighted = hljs.highlight(text, { language }).value;
   const highlighted = hljs.highlight(text, { language }).value;
-  const escapedCode = text.replace(/`/g, "`");
+  const id = storeCodeBlock(text);
   return `<div class="code-block-wrapper">
   return `<div class="code-block-wrapper">
     <div class="code-block-header">
     <div class="code-block-header">
       <span class="code-lang">${language}</span>
       <span class="code-lang">${language}</span>
-      <button class="code-copy-btn" data-code="${escapedCode}" aria-label="Copiar código">
+      <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>
         <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
         Copiar
       </button>
       </button>
@@ -205,7 +218,7 @@ function renderMarkdown(content) {
   if (!content) return "";
   if (!content) return "";
   const html = marked.parse(content);
   const html = marked.parse(content);
   return DOMPurify.sanitize(html, {
   return DOMPurify.sanitize(html, {
-    ADD_ATTR: ["data-code"],
+    ADD_ATTR: ["data-code-id"],
     ADD_TAGS: ["button"],
     ADD_TAGS: ["button"],
   });
   });
 }
 }
@@ -213,7 +226,7 @@ function renderMarkdown(content) {
 function handleCodeCopy(e) {
 function handleCodeCopy(e) {
   const btn = e.target.closest(".code-copy-btn");
   const btn = e.target.closest(".code-copy-btn");
   if (!btn) return;
   if (!btn) return;
-  const code = btn.dataset.code?.replace(/&#96;/g, "`") ?? "";
+  const code = codeBlockStore.get(btn.dataset.codeId) ?? "";
   navigator.clipboard.writeText(code).then(() => {
   navigator.clipboard.writeText(code).then(() => {
     btn.classList.add("copied");
     btn.classList.add("copied");
     btn.querySelector("svg").style.display = "none";
     btn.querySelector("svg").style.display = "none";
@@ -246,12 +259,25 @@ function formatarHora(ts) {
   }
   }
 }
 }
 
 
+const NEAR_BOTTOM_THRESHOLD_PX = 80;
+
+function isNearBottom() {
+  const el = chatEl.value;
+  if (!el) return true;
+  return el.scrollHeight - el.scrollTop - el.clientHeight <= NEAR_BOTTOM_THRESHOLD_PX;
+}
+
 watch(
 watch(
-  () => [props.messages.length, props.loading],
+  () => [
+    props.messages.length,
+    props.loading,
+    props.messages[props.messages.length - 1]?.content?.length ?? 0
+  ],
   async () => {
   async () => {
-    await nextTick();
     const el = chatEl.value;
     const el = chatEl.value;
-    if (!el) return;
+    const shouldStick = !el || isNearBottom();
+    await nextTick();
+    if (!el || !shouldStick) return;
     el.scrollTop = el.scrollHeight;
     el.scrollTop = el.scrollHeight;
   },
   },
   { immediate: true }
   { immediate: true }

+ 11 - 7
src/layout/ListaConversas.vue

@@ -61,29 +61,32 @@ async function abrirConversa(id) {
         {{ grupo.label }}
         {{ grupo.label }}
       </div>
       </div>
       <div class="grid gap-1">
       <div class="grid gap-1">
-      <button
+      <div
         v-for="c in grupo.items"
         v-for="c in grupo.items"
         :key="c.id"
         :key="c.id"
-        type="button"
-        class="relative w-full flex items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm transition-colors overflow-hidden !bg-none !border-0 !transform-none hover:!transform-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
+        role="button"
+        tabindex="0"
+        class="relative w-full flex items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm transition-colors overflow-hidden cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
         :class="String(activeConversationId) === c.id
         :class="String(activeConversationId) === c.id
           ? 'text-gray-900 bg-gray-200/70 dark:text-white dark:bg-white/10'
           ? 'text-gray-900 bg-gray-200/70 dark:text-white dark:bg-white/10'
           : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100/80 dark:text-white/70 dark:hover:text-white dark:hover:bg-white/10'"
           : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100/80 dark:text-white/70 dark:hover:text-white dark:hover:bg-white/10'"
         @mouseenter="hoveredConversaId = c.id"
         @mouseenter="hoveredConversaId = c.id"
         @mouseleave="hoveredConversaId = null"
         @mouseleave="hoveredConversaId = null"
         @click="editingConversaId !== c.id && abrirConversa(c.id)"
         @click="editingConversaId !== c.id && abrirConversa(c.id)"
+        @keydown.enter.prevent="editingConversaId !== c.id && abrirConversa(c.id)"
+        @keydown.space.prevent="editingConversaId !== c.id && abrirConversa(c.id)"
       >
       >
         <template v-if="editingConversaId === c.id">
         <template v-if="editingConversaId === c.id">
           <input
           <input
             type="text"
             type="text"
             v-model="editingTitulo"
             v-model="editingTitulo"
             class="flex-1 min-w-0 bg-transparent text-sm outline-none border-b border-gray-400 dark:border-white/40 text-gray-900 dark:text-white"
             class="flex-1 min-w-0 bg-transparent text-sm outline-none border-b border-gray-400 dark:border-white/40 text-gray-900 dark:text-white"
-            @keydown.enter.prevent="confirmarEdicao(c.id)"
-            @keydown.escape.prevent="cancelarEdicao"
+            @keydown.enter.prevent.stop="confirmarEdicao(c.id)"
+            @keydown.escape.prevent.stop="cancelarEdicao"
             @click.stop
             @click.stop
             autofocus
             autofocus
           />
           />
-          <div class="flex items-center gap-0.5 shrink-0" @click.stop>
+          <div class="flex items-center gap-0.5 shrink-0" @click.stop @keydown.stop>
             <button
             <button
               type="button"
               type="button"
               class="rounded p-0.5 text-gray-500 hover:text-gray-900 dark:text-white/50 dark:hover:text-white transition-colors"
               class="rounded p-0.5 text-gray-500 hover:text-gray-900 dark:text-white/50 dark:hover:text-white transition-colors"
@@ -115,6 +118,7 @@ async function abrirConversa(id) {
               ? 'bg-gradient-to-l from-gray-200 from-60% dark:from-[#131f30] to-transparent'
               ? 'bg-gradient-to-l from-gray-200 from-60% dark:from-[#131f30] to-transparent'
               : 'bg-gradient-to-l from-gray-100 from-60% dark:from-[#101827] to-transparent'"
               : 'bg-gradient-to-l from-gray-100 from-60% dark:from-[#101827] to-transparent'"
             @click.stop
             @click.stop
+            @keydown.stop
           >
           >
             <button
             <button
               type="button"
               type="button"
@@ -138,7 +142,7 @@ async function abrirConversa(id) {
             </button>
             </button>
           </div>
           </div>
         </template>
         </template>
-      </button>
+      </div>
       </div>
       </div>
     </template>
     </template>
     <div
     <div