|
@@ -52,7 +52,7 @@
|
|
|
|
|
|
|
|
<template v-else>
|
|
<template v-else>
|
|
|
|
|
|
|
|
- <div class="prose-content" v-html="renderMarkdown(m.content)" />
|
|
|
|
|
|
|
+ <ChatMessageContent :content="m.content" />
|
|
|
<span
|
|
<span
|
|
|
v-if="m.streaming"
|
|
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"
|
|
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>
|
|
<script setup>
|
|
|
import { ref, computed, nextTick, onMounted, onUnmounted, watch } from "vue";
|
|
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 BaseButton from "./base/BaseButton.vue";
|
|
|
import BaseIcon from "./base/BaseIcon.vue";
|
|
import BaseIcon from "./base/BaseIcon.vue";
|
|
|
import { Copy, Check, FileText, Send, X } from "lucide-vue-next";
|
|
import { Copy, Check, FileText, Send, X } from "lucide-vue-next";
|
|
|
import ChatModeToggle from "./ChatModeToggle.vue";
|
|
import ChatModeToggle from "./ChatModeToggle.vue";
|
|
|
|
|
+import ChatMessageContent from "./ChatMessageContent.vue";
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
const props = defineProps({
|
|
|
messages: { type: Array, required: true },
|
|
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) {
|
|
function agruparFontes(sources) {
|
|
|
const porDocumento = new Map();
|
|
const porDocumento = new Map();
|
|
|
for (const s of sources ?? []) {
|
|
for (const s of sources ?? []) {
|
|
@@ -216,21 +178,12 @@ function agruparFontes(sources) {
|
|
|
return [...porDocumento.values()];
|
|
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();
|
|
const codeCopyTimeouts = new Set();
|
|
|
|
|
|
|
|
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 = codeBlockStore.get(btn.dataset.codeId) ?? "";
|
|
|
|
|
|
|
+ const code = getStoredCode(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";
|