feat: 增加katex 公式渲染
This commit is contained in:
parent
8af4fb1c27
commit
46c8f85e9b
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AI Chat</title>
|
||||
<title>Chat</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"highlight.js": "^11.10.0",
|
||||
"katex": "^0.16.40",
|
||||
"marked": "^15.0.0",
|
||||
"vue": "^3.4.0"
|
||||
},
|
||||
|
|
@ -990,6 +991,15 @@
|
|||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz",
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
||||
|
|
@ -1111,6 +1121,22 @@
|
|||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/katex": {
|
||||
"version": "0.16.40",
|
||||
"resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.40.tgz",
|
||||
"integrity": "sha512-1DJcK/L05k1Y9Gf7wMcyuqFOL6BiY3vY0CFcAM/LPRN04NALxcl6u7lOWNsp3f/bCHWxigzQl6FbR95XJ4R84Q==",
|
||||
"funding": [
|
||||
"https://opencollective.com/katex",
|
||||
"https://github.com/sponsors/katex"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^8.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"katex": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"highlight.js": "^11.10.0",
|
||||
"katex": "^0.16.40",
|
||||
"marked": "^15.0.0",
|
||||
"vue": "^3.4.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -68,19 +68,7 @@
|
|||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import MessageBubble from './MessageBubble.vue'
|
||||
import MessageInput from './MessageInput.vue'
|
||||
import { marked } from 'marked'
|
||||
import hljs from 'highlight.js'
|
||||
|
||||
marked.setOptions({
|
||||
highlight(code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
return hljs.highlight(code, { language: lang }).value
|
||||
}
|
||||
return hljs.highlightAuto(code).value
|
||||
},
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
})
|
||||
import { renderMarkdown } from '../utils/markdown'
|
||||
|
||||
const props = defineProps({
|
||||
conversation: { type: Object, default: null },
|
||||
|
|
@ -99,7 +87,7 @@ const inputRef = ref(null)
|
|||
|
||||
const renderedStreamContent = computed(() => {
|
||||
if (!props.streamingContent) return ''
|
||||
return marked.parse(props.streamingContent)
|
||||
return renderMarkdown(props.streamingContent)
|
||||
})
|
||||
|
||||
function scrollToBottom(smooth = true) {
|
||||
|
|
@ -375,4 +363,13 @@ defineExpose({ scrollToBottom })
|
|||
.streaming-content :deep(.placeholder) {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.streaming-content :deep(.math-block),
|
||||
.message-content :deep(.math-block) {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 12px 0;
|
||||
margin: 8px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -38,8 +38,7 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import hljs from 'highlight.js'
|
||||
import { renderMarkdown } from '../utils/markdown'
|
||||
|
||||
const props = defineProps({
|
||||
role: { type: String, required: true },
|
||||
|
|
@ -54,20 +53,9 @@ defineEmits(['delete'])
|
|||
|
||||
const showThinking = ref(false)
|
||||
|
||||
marked.setOptions({
|
||||
highlight(code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
return hljs.highlight(code, { language: lang }).value
|
||||
}
|
||||
return hljs.highlightAuto(code).value
|
||||
},
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
})
|
||||
|
||||
const renderedContent = computed(() => {
|
||||
if (!props.content) return ''
|
||||
return marked.parse(props.content)
|
||||
return renderMarkdown(props.content)
|
||||
})
|
||||
|
||||
function formatTime(iso) {
|
||||
|
|
@ -287,4 +275,12 @@ function copyContent() {
|
|||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
|
||||
.message-content :deep(.math-block) {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 12px 0;
|
||||
margin: 8px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './styles/highlight.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import { marked } from 'marked'
|
||||
import katex from 'katex'
|
||||
import hljs from 'highlight.js'
|
||||
|
||||
function renderMath(text, displayMode) {
|
||||
try {
|
||||
return katex.renderToString(text, {
|
||||
displayMode,
|
||||
throwOnError: false,
|
||||
strict: false,
|
||||
})
|
||||
} catch {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
// marked extension for inline math $...$
|
||||
const mathExtension = {
|
||||
name: 'math',
|
||||
level: 'inline',
|
||||
start(src) {
|
||||
// Find $ not followed by $ (to avoid matching $$)
|
||||
const idx = src.search(/(?<!\$)\$(?!\$)/)
|
||||
return idx === -1 ? undefined : idx
|
||||
},
|
||||
tokenizer(src) {
|
||||
// Match $...$ (single $, not $$)
|
||||
const match = src.match(/^(?<!\$)\$(?!\$)([^\$\n]+?)\$(?!\$)/)
|
||||
if (match) {
|
||||
return { type: 'math', raw: match[0], text: match[1].trim(), displayMode: false }
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return renderMath(token.text, token.displayMode)
|
||||
},
|
||||
}
|
||||
|
||||
// marked extension for block math $$...$$
|
||||
const blockMathExtension = {
|
||||
name: 'blockMath',
|
||||
level: 'block',
|
||||
start(src) {
|
||||
const idx = src.indexOf('$$')
|
||||
return idx === -1 ? undefined : idx
|
||||
},
|
||||
tokenizer(src) {
|
||||
const match = src.match(/^\$\$([\s\S]+?)\$\$\n?/)
|
||||
if (match) {
|
||||
return { type: 'blockMath', raw: match[0], text: match[1].trim() }
|
||||
}
|
||||
},
|
||||
renderer(token) {
|
||||
return `<div class="math-block">${renderMath(token.text, true)}</div>`
|
||||
},
|
||||
}
|
||||
|
||||
marked.use({ extensions: [blockMathExtension, mathExtension] })
|
||||
|
||||
marked.setOptions({
|
||||
highlight(code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
return hljs.highlight(code, { language: lang }).value
|
||||
}
|
||||
return hljs.highlightAuto(code).value
|
||||
},
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
})
|
||||
|
||||
export function renderMarkdown(text) {
|
||||
return marked.parse(text)
|
||||
}
|
||||
Loading…
Reference in New Issue