feat: new UI code block and Enable copying of code blocks or plain text mid-stream (#4010)
* feat: improvement ui codeblock * chore: update ui code block * chore: finalize ui code block and latex * chore: fix jest testing and cleanup unused deps
This commit is contained in:
parent
8a81678bb4
commit
e196aefcd3
@ -25,6 +25,11 @@ const ListContainer = ({ children }: Props) => {
|
|||||||
isUserManuallyScrollingUp.current = false
|
isUserManuallyScrollingUp.current = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isUserManuallyScrollingUp.current === true) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
prevScrollTop.current = currentScrollTop
|
prevScrollTop.current = currentScrollTop
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,11 @@ const config = {
|
|||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
// ...
|
// ...
|
||||||
'^@/(.*)$': '<rootDir>/$1',
|
'^@/(.*)$': '<rootDir>/$1',
|
||||||
|
'react-markdown': '<rootDir>/mock/empty-mock.tsx',
|
||||||
|
'rehype-highlight': '<rootDir>/mock/empty-mock.tsx',
|
||||||
|
'rehype-katex': '<rootDir>/mock/empty-mock.tsx',
|
||||||
|
'rehype-raw': '<rootDir>/mock/empty-mock.tsx',
|
||||||
|
'remark-math': '<rootDir>/mock/empty-mock.tsx',
|
||||||
},
|
},
|
||||||
// Add more setup options before each test is run
|
// Add more setup options before each test is run
|
||||||
// setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
// setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||||
|
|||||||
2
web/mock/empty-mock.tsx
Normal file
2
web/mock/empty-mock.tsx
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
const EmptyMock = {}
|
||||||
|
export default EmptyMock
|
||||||
@ -14,13 +14,10 @@
|
|||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/react": "^2.0.18",
|
|
||||||
"@hookform/resolvers": "^3.3.2",
|
|
||||||
"@janhq/core": "link:./core",
|
"@janhq/core": "link:./core",
|
||||||
"@janhq/joi": "link:./joi",
|
"@janhq/joi": "link:./joi",
|
||||||
"autoprefixer": "10.4.16",
|
"autoprefixer": "10.4.16",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"csstype": "^3.0.10",
|
|
||||||
"framer-motion": "^10.16.4",
|
"framer-motion": "^10.16.4",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
"jotai": "^2.6.0",
|
"jotai": "^2.6.0",
|
||||||
@ -28,8 +25,6 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.291.0",
|
"lucide-react": "^0.291.0",
|
||||||
"marked": "^9.1.2",
|
"marked": "^9.1.2",
|
||||||
"marked-highlight": "^2.0.6",
|
|
||||||
"marked-katex-extension": "^5.0.2",
|
|
||||||
"next": "14.2.3",
|
"next": "14.2.3",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
@ -39,22 +34,25 @@
|
|||||||
"react-circular-progressbar": "^2.1.0",
|
"react-circular-progressbar": "^2.1.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-dropzone": "14.2.3",
|
"react-dropzone": "14.2.3",
|
||||||
"react-hook-form": "^7.47.0",
|
|
||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-icons": "^4.12.0",
|
"react-icons": "^4.12.0",
|
||||||
"react-scroll-to-bottom": "^4.2.0",
|
"react-markdown": "^9.0.1",
|
||||||
"react-toastify": "^9.1.3",
|
"react-toastify": "^9.1.3",
|
||||||
|
"rehype-highlight": "^7.0.1",
|
||||||
|
"rehype-highlight-code-lines": "^1.0.4",
|
||||||
|
"rehype-katex": "^7.0.1",
|
||||||
|
"rehype-raw": "^7.0.0",
|
||||||
|
"remark-math": "^6.0.0",
|
||||||
"sass": "^1.69.4",
|
"sass": "^1.69.4",
|
||||||
|
"slate": "latest",
|
||||||
|
"slate-dom": "0.111.0",
|
||||||
|
"slate-history": "0.110.3",
|
||||||
|
"slate-react": "0.110.3",
|
||||||
"tailwind-merge": "^2.0.0",
|
"tailwind-merge": "^2.0.0",
|
||||||
"tailwindcss": "3.3.5",
|
"tailwindcss": "3.3.5",
|
||||||
"ulidx": "^2.3.0",
|
"ulidx": "^2.3.0",
|
||||||
"use-debounce": "^10.0.0",
|
"use-debounce": "^10.0.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1"
|
||||||
"zod": "^3.22.4",
|
|
||||||
"slate": "latest",
|
|
||||||
"slate-dom": "0.111.0",
|
|
||||||
"slate-react": "0.110.3",
|
|
||||||
"slate-history": "0.110.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/eslint-plugin-next": "^14.0.1",
|
"@next/eslint-plugin-next": "^14.0.1",
|
||||||
@ -65,7 +63,7 @@
|
|||||||
"@types/react": "18.2.34",
|
"@types/react": "18.2.34",
|
||||||
"@types/react-dom": "18.2.14",
|
"@types/react-dom": "18.2.14",
|
||||||
"@types/react-icons": "^3.0.0",
|
"@types/react-icons": "^3.0.0",
|
||||||
"@types/react-scroll-to-bottom": "^4.2.4",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@types/uuid": "^9.0.6",
|
"@types/uuid": "^9.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
||||||
"@typescript-eslint/parser": "^6.8.0",
|
"@typescript-eslint/parser": "^6.8.0",
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
|||||||
|
|
||||||
import { MessageStatus } from '@janhq/core'
|
import { MessageStatus } from '@janhq/core'
|
||||||
import hljs from 'highlight.js'
|
import hljs from 'highlight.js'
|
||||||
|
|
||||||
import { useAtom, useAtomValue } from 'jotai'
|
import { useAtom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import { BaseEditor, createEditor, Editor, Transforms } from 'slate'
|
import { BaseEditor, createEditor, Editor, Transforms } from 'slate'
|
||||||
import { withHistory } from 'slate-history' // Import withHistory
|
import { withHistory } from 'slate-history' // Import withHistory
|
||||||
import {
|
import {
|
||||||
@ -270,7 +270,8 @@ const RichTextEditor = ({
|
|||||||
textareaRef.current.style.height = activeSettingInputBox
|
textareaRef.current.style.height = activeSettingInputBox
|
||||||
? '100px'
|
? '100px'
|
||||||
: '40px'
|
: '40px'
|
||||||
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'
|
textareaRef.current.style.height =
|
||||||
|
textareaRef.current.scrollHeight + 2 + 'px'
|
||||||
textareaRef.current?.scrollTo({
|
textareaRef.current?.scrollTo({
|
||||||
top: textareaRef.current.scrollHeight,
|
top: textareaRef.current.scrollHeight,
|
||||||
behavior: 'instant',
|
behavior: 'instant',
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react'
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import Markdown from 'react-markdown'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChatCompletionRole,
|
ChatCompletionRole,
|
||||||
@ -8,14 +13,15 @@ import {
|
|||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
|
|
||||||
import { Tooltip } from '@janhq/joi'
|
import { Tooltip } from '@janhq/joi'
|
||||||
import hljs from 'highlight.js'
|
|
||||||
|
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
import { FolderOpenIcon } from 'lucide-react'
|
import { FolderOpenIcon } from 'lucide-react'
|
||||||
import { Marked, Renderer } from 'marked'
|
import rehypeHighlight from 'rehype-highlight'
|
||||||
import { markedHighlight } from 'marked-highlight'
|
import rehypeHighlightCodeLines from 'rehype-highlight-code-lines'
|
||||||
import markedKatex from 'marked-katex-extension'
|
import rehypeKatex from 'rehype-katex'
|
||||||
|
import rehypeRaw from 'rehype-raw'
|
||||||
|
import remarkMath from 'remark-math'
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
import LogoMark from '@/containers/Brand/Logo/Mark'
|
import LogoMark from '@/containers/Brand/Logo/Mark'
|
||||||
@ -23,6 +29,7 @@ import LogoMark from '@/containers/Brand/Logo/Mark'
|
|||||||
import { useClipboard } from '@/hooks/useClipboard'
|
import { useClipboard } from '@/hooks/useClipboard'
|
||||||
import { usePath } from '@/hooks/usePath'
|
import { usePath } from '@/hooks/usePath'
|
||||||
|
|
||||||
|
import { getLanguageFromExtension } from '@/utils/codeLanguageExtension'
|
||||||
import { toGibibytes } from '@/utils/converter'
|
import { toGibibytes } from '@/utils/converter'
|
||||||
import { displayDate } from '@/utils/datetime'
|
import { displayDate } from '@/utils/datetime'
|
||||||
|
|
||||||
@ -53,88 +60,181 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
|
|
||||||
const clipboard = useClipboard({ timeout: 1000 })
|
const clipboard = useClipboard({ timeout: 1000 })
|
||||||
|
|
||||||
function escapeHtml(html: string): string {
|
function extractCodeLines(node: { children: { children: any[] }[] }) {
|
||||||
return html
|
const codeLines: any[] = []
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
// Helper function to extract text recursively from children
|
||||||
.replace(/>/g, '>')
|
function getTextFromNode(node: {
|
||||||
.replace(/"/g, '"')
|
type: string
|
||||||
.replace(/'/g, ''')
|
value: any
|
||||||
|
children: any[]
|
||||||
|
}): string {
|
||||||
|
if (node.type === 'text') {
|
||||||
|
return node.value
|
||||||
|
} else if (node.children) {
|
||||||
|
return node.children.map(getTextFromNode).join('')
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traverse each line in the <code> block
|
||||||
|
node.children[0].children.forEach(
|
||||||
|
(lineNode: {
|
||||||
|
type: string
|
||||||
|
tagName: string
|
||||||
|
value: any
|
||||||
|
children: any[]
|
||||||
|
}) => {
|
||||||
|
if (lineNode.type === 'element' && lineNode.tagName === 'span') {
|
||||||
|
const lineContent = getTextFromNode(lineNode)
|
||||||
|
codeLines.push(lineContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Join the lines with newline characters for proper formatting
|
||||||
|
return codeLines.join('\n')
|
||||||
|
}
|
||||||
|
function wrapCodeBlocksWithoutVisit() {
|
||||||
|
return (tree: { children: any[] }) => {
|
||||||
|
tree.children = tree.children.map((node) => {
|
||||||
|
if (node.tagName === 'pre' && node.children[0]?.tagName === 'code') {
|
||||||
|
const language = node.children[0].properties.className?.[1]?.replace(
|
||||||
|
'language-',
|
||||||
|
''
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!language) return node
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'element',
|
||||||
|
tagName: 'div',
|
||||||
|
properties: {
|
||||||
|
className: ['code-block-wrapper'],
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
tagName: 'div',
|
||||||
|
properties: {
|
||||||
|
className: [
|
||||||
|
'code-block',
|
||||||
|
'group/item',
|
||||||
|
'relative',
|
||||||
|
'my-4',
|
||||||
|
'overflow-auto',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
tagName: 'div',
|
||||||
|
properties: {
|
||||||
|
className:
|
||||||
|
'code-header bg-[hsla(var(--app-code-block))] flex justify-between items-center py-2 px-3 border-b border-[hsla(var(--app-border))] rounded-t-lg',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
tagName: 'span',
|
||||||
|
properties: {
|
||||||
|
className: 'text-xs font-medium text-gray-300',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
value: language
|
||||||
|
? `${getLanguageFromExtension(language)}`
|
||||||
|
: 'No file name',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
tagName: 'button',
|
||||||
|
properties: {
|
||||||
|
className:
|
||||||
|
'copy-button ml-auto flex items-center gap-1 text-xs font-medium text-gray-400 hover:text-gray-600 focus:outline-none',
|
||||||
|
onClick: (event: Event) => {
|
||||||
|
clipboard.copy(extractCodeLines(node))
|
||||||
|
|
||||||
|
const button = event.currentTarget as HTMLElement
|
||||||
|
button.innerHTML = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check pointer-events-none text-green-600"><path d="M20 6 9 17l-5-5"/></svg>
|
||||||
|
<span>Copied</span>
|
||||||
|
`
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
button.innerHTML = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy pointer-events-none text-gray-400"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
||||||
|
<span>Copy</span>
|
||||||
|
`
|
||||||
|
}, 2000)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
tagName: 'svg',
|
||||||
|
properties: {
|
||||||
|
xmlns: 'http://www.w3.org/2000/svg',
|
||||||
|
width: '16',
|
||||||
|
height: '16',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
strokeWidth: '2',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
className:
|
||||||
|
'lucide lucide-copy pointer-events-none text-gray-400',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
tagName: 'rect',
|
||||||
|
properties: {
|
||||||
|
width: '14',
|
||||||
|
height: '14',
|
||||||
|
x: '8',
|
||||||
|
y: '8',
|
||||||
|
rx: '2',
|
||||||
|
ry: '2',
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
tagName: 'path',
|
||||||
|
properties: {
|
||||||
|
d: 'M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2',
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ type: 'text', value: 'Copy' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
node,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const marked: Marked = new Marked(
|
|
||||||
markedHighlight({
|
|
||||||
langPrefix: 'hljs',
|
|
||||||
highlight(code, lang) {
|
|
||||||
if (lang === undefined || lang === '') {
|
|
||||||
return hljs.highlight(code, { language: 'plaintext' }).value
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return hljs.highlight(code, { language: lang }).value
|
|
||||||
} catch (err) {
|
|
||||||
return hljs.highlight(code, { language: 'javascript' }).value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
renderer: {
|
|
||||||
html: (html: string) => {
|
|
||||||
return escapeHtml(html) // Escape any HTML
|
|
||||||
},
|
|
||||||
link: (href, title, text) => {
|
|
||||||
return Renderer.prototype.link
|
|
||||||
?.apply(this, [href, title, text])
|
|
||||||
.replace('<a', "<a target='_blank'")
|
|
||||||
},
|
|
||||||
code(code, lang) {
|
|
||||||
return `
|
|
||||||
<div class="relative code-block group/item overflow-auto">
|
|
||||||
<button class='text-xs copy-action hidden group-hover/item:block p-2 rounded-lg absolute top-6 right-2'>
|
|
||||||
${
|
|
||||||
clipboard.copied
|
|
||||||
? `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check pointer-events-none text-green-600"><path d="M20 6 9 17l-5-5"/></svg>`
|
|
||||||
: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy pointer-events-none text-gray-400"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
<pre class="hljs">
|
|
||||||
<code class="language-${lang ?? ''}">${code}</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
marked.use(markedKatex({ throwOnError: false }))
|
|
||||||
|
|
||||||
const { onViewFile, onViewFileContainer } = usePath()
|
const { onViewFile, onViewFileContainer } = usePath()
|
||||||
const parsedText = marked.parse(text)
|
|
||||||
const [tokenCount, setTokenCount] = useState(0)
|
const [tokenCount, setTokenCount] = useState(0)
|
||||||
const [lastTimestamp, setLastTimestamp] = useState<number | undefined>()
|
const [lastTimestamp, setLastTimestamp] = useState<number | undefined>()
|
||||||
const [tokenSpeed, setTokenSpeed] = useState(0)
|
const [tokenSpeed, setTokenSpeed] = useState(0)
|
||||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
|
|
||||||
const codeBlockCopyEvent = useRef((e: Event) => {
|
|
||||||
const target: HTMLElement = e.target as HTMLElement
|
|
||||||
if (typeof target.className !== 'string') return null
|
|
||||||
|
|
||||||
const isCopyActionClassName = target?.className.includes('copy-action')
|
|
||||||
|
|
||||||
if (isCopyActionClassName) {
|
|
||||||
const content = target?.parentNode?.querySelector('code')?.innerText ?? ''
|
|
||||||
clipboard.copy(content)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.addEventListener('click', codeBlockCopyEvent.current)
|
|
||||||
return () => {
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
document.removeEventListener('click', codeBlockCopyEvent.current)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.status !== MessageStatus.Pending) {
|
if (props.status !== MessageStatus.Pending) {
|
||||||
return
|
return
|
||||||
@ -285,8 +385,22 @@ const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
|||||||
className={twMerge(
|
className={twMerge(
|
||||||
'message max-width-[100%] flex flex-col gap-y-2 overflow-auto break-all leading-relaxed '
|
'message max-width-[100%] flex flex-col gap-y-2 overflow-auto break-all leading-relaxed '
|
||||||
)}
|
)}
|
||||||
dangerouslySetInnerHTML={{ __html: parsedText }}
|
dir="ltr"
|
||||||
/>
|
>
|
||||||
|
<Markdown
|
||||||
|
remarkPlugins={[remarkMath]}
|
||||||
|
rehypePlugins={[
|
||||||
|
[rehypeKatex, { throwOnError: false }],
|
||||||
|
rehypeRaw,
|
||||||
|
rehypeHighlight,
|
||||||
|
[rehypeHighlightCodeLines, { showLineNumbers: true }],
|
||||||
|
wrapCodeBlocksWithoutVisit,
|
||||||
|
]}
|
||||||
|
skipHtml={true}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Markdown>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -55,22 +55,25 @@
|
|||||||
.hljs {
|
.hljs {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: block;
|
display: block;
|
||||||
width: auto;
|
|
||||||
background: hsla(var(--app-code-block));
|
|
||||||
color: #f8f8f2;
|
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
word-wrap: normal;
|
border-bottom-left-radius: 0.4rem;
|
||||||
|
border-bottom-right-radius: 0.4rem;
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: hsla(var(--app-code-block));
|
||||||
|
overflow: auto;
|
||||||
|
padding: 8px 16px;
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
margin-top: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
}
|
||||||
pre > code {
|
pre > code {
|
||||||
display: block;
|
|
||||||
text-indent: 0;
|
text-indent: 0;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
max-width: 10vw;
|
font-size: 14px;
|
||||||
|
overflow: auto;
|
||||||
|
color: #f8f8f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-emphasis {
|
.hljs-emphasis {
|
||||||
@ -81,6 +84,14 @@ pre > code {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
pre {
|
||||||
|
padding: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (-ms-high-contrast: active) {
|
@media screen and (-ms-high-contrast: active) {
|
||||||
.hljs-addition,
|
.hljs-addition,
|
||||||
.hljs-attribute,
|
.hljs-attribute,
|
||||||
@ -105,3 +116,52 @@ pre > code {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.code-block-wrapper {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-line {
|
||||||
|
// padding-left: 12px;
|
||||||
|
padding-right: 12px;
|
||||||
|
margin-left: -12px;
|
||||||
|
margin-right: -12px;
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.code-line:empty {
|
||||||
|
height: 21.5938px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.code-line {
|
||||||
|
// min-width: 100%;
|
||||||
|
white-space: pre;
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 10vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-line.inserted {
|
||||||
|
background-color: var(--color-inserted-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-line.deleted {
|
||||||
|
background-color: var(--color-deleted-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlighted-code-line {
|
||||||
|
background-color: var(--color-highlighted-line);
|
||||||
|
border-left: 4px solid var(--color-highlighted-line-indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
.numbered-code-line::before {
|
||||||
|
content: attr(data-line-number);
|
||||||
|
|
||||||
|
margin-left: -8px;
|
||||||
|
margin-right: 16px;
|
||||||
|
width: 1rem;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-weak);
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|||||||
@ -27,11 +27,3 @@
|
|||||||
@apply inline-flex flex-col border-s-4 border-[hsla(var(--primary-bg))] bg-[hsla(var(--primary-bg-soft))] px-4 py-2;
|
@apply inline-flex flex-col border-s-4 border-[hsla(var(--primary-bg))] bg-[hsla(var(--primary-bg-soft))] px-4 py-2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-block {
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
max-width: 95vw;
|
|
||||||
}
|
|
||||||
|
|||||||
34
web/utils/codeLanguageExtension.ts
Normal file
34
web/utils/codeLanguageExtension.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Utility function using switch-case for extension to language mapping
|
||||||
|
export function getLanguageFromExtension(extension: string): string {
|
||||||
|
switch (extension.toLowerCase()) {
|
||||||
|
case 'ts':
|
||||||
|
case 'tsx':
|
||||||
|
return 'typescript'
|
||||||
|
case 'js':
|
||||||
|
case 'jsx':
|
||||||
|
return 'javascript'
|
||||||
|
case 'py':
|
||||||
|
return 'python'
|
||||||
|
case 'java':
|
||||||
|
return 'java'
|
||||||
|
case 'rb':
|
||||||
|
return 'ruby'
|
||||||
|
case 'cs':
|
||||||
|
return 'csharp'
|
||||||
|
case 'md':
|
||||||
|
return 'markdown'
|
||||||
|
case 'yaml':
|
||||||
|
case 'yml':
|
||||||
|
return 'yaml'
|
||||||
|
case 'sh':
|
||||||
|
return 'bash'
|
||||||
|
case 'rs':
|
||||||
|
return 'rust'
|
||||||
|
case 'kt':
|
||||||
|
return 'kotlin'
|
||||||
|
case 'swift':
|
||||||
|
return 'swift'
|
||||||
|
default:
|
||||||
|
return extension
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user