Spaces:
Sleeping
Sleeping
<script lang="ts" setup> | |
import { computed, onMounted, onUnmounted, onUpdated, ref } from 'vue' | |
import MarkdownIt from 'markdown-it' | |
import mdKatex from '@traptitech/markdown-it-katex' | |
import mila from 'markdown-it-link-attributes' | |
import hljs from 'highlight.js' | |
import { useBasicLayout } from '@/hooks/useBasicLayout' | |
import { t } from '@/locales' | |
import { copyToClip } from '@/utils/copy' | |
interface Props { | |
inversion?: boolean | |
error?: boolean | |
text?: string | |
loading?: boolean | |
asRawText?: boolean | |
} | |
const props = defineProps<Props>() | |
const { isMobile } = useBasicLayout() | |
const textRef = ref<HTMLElement>() | |
const mdi = new MarkdownIt({ | |
html: false, | |
linkify: true, | |
highlight(code, language) { | |
const validLang = !!(language && hljs.getLanguage(language)) | |
if (validLang) { | |
const lang = language ?? '' | |
return highlightBlock(hljs.highlight(code, { language: lang }).value, lang) | |
} | |
return highlightBlock(hljs.highlightAuto(code).value, '') | |
}, | |
}) | |
mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } }) | |
mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' }) | |
const wrapClass = computed(() => { | |
return [ | |
'text-wrap', | |
'min-w-[20px]', | |
'rounded-md', | |
isMobile.value ? 'p-2' : 'px-3 py-2', | |
props.inversion ? 'bg-[#d2f9d1]' : 'bg-[#f4f6f8]', | |
props.inversion ? 'dark:bg-[#a1dc95]' : 'dark:bg-[#1e1e20]', | |
props.inversion ? 'message-request' : 'message-reply', | |
{ 'text-red-500': props.error }, | |
] | |
}) | |
const text = computed(() => { | |
const value = props.text ?? '' | |
if (!props.asRawText) | |
return mdi.render(value) | |
return value | |
}) | |
function highlightBlock(str: string, lang?: string) { | |
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${t('chat.copyCode')}</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>` | |
} | |
function addCopyEvents() { | |
if (textRef.value) { | |
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy') | |
copyBtn.forEach((btn) => { | |
btn.addEventListener('click', () => { | |
const code = btn.parentElement?.nextElementSibling?.textContent | |
if (code) { | |
copyToClip(code).then(() => { | |
btn.textContent = t('chat.copied') | |
setTimeout(() => { | |
btn.textContent = t('chat.copyCode') | |
}, 1000) | |
}) | |
} | |
}) | |
}) | |
} | |
} | |
function removeCopyEvents() { | |
if (textRef.value) { | |
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy') | |
copyBtn.forEach((btn) => { | |
btn.removeEventListener('click', () => {}) | |
}) | |
} | |
} | |
onMounted(() => { | |
addCopyEvents() | |
}) | |
onUpdated(() => { | |
addCopyEvents() | |
}) | |
onUnmounted(() => { | |
removeCopyEvents() | |
}) | |
</script> | |
<template> | |
<div class="text-black" :class="wrapClass"> | |
<div ref="textRef" class="leading-relaxed break-words"> | |
<div v-if="!inversion"> | |
<div v-if="!asRawText" class="markdown-body" :class="{ 'markdown-body-generate': loading }" v-html="text" /> | |
<div v-else class="whitespace-pre-wrap" v-text="text" /> | |
</div> | |
<div v-else class="whitespace-pre-wrap" v-text="text" /> | |
</div> | |
</div> | |
</template> | |
<style lang="less"> | |
@import url(./style.less); | |
</style> | |