feat: add svg preview
This commit is contained in:
parent
96737ed695
commit
0074d5c8b4
@ -94,14 +94,8 @@
|
|||||||
background-color: var(--color-border);
|
background-color: var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.katex-display {
|
|
||||||
span {
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
span {
|
||||||
white-space: pre-wrap;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
p code,
|
p code,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
import Artifacts from './Artifacts'
|
import Artifacts from './Artifacts'
|
||||||
import Mermaid from './Mermaid'
|
import Mermaid from './Mermaid'
|
||||||
|
import SvgPreview from './SvgPreview'
|
||||||
|
|
||||||
interface CodeBlockProps {
|
interface CodeBlockProps {
|
||||||
children: string
|
children: string
|
||||||
@ -79,8 +80,20 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
|||||||
return <Mermaid chart={children} />
|
return <Mermaid chart={children} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (language === 'svg') {
|
||||||
|
return (
|
||||||
|
<CodeBlockWrapper className="code-block">
|
||||||
|
<CodeHeader>
|
||||||
|
<CodeLanguage>{'<SVG>'}</CodeLanguage>
|
||||||
|
<CopyButton text={children} />
|
||||||
|
</CodeHeader>
|
||||||
|
<SvgPreview>{children}</SvgPreview>
|
||||||
|
</CodeBlockWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return match ? (
|
return match ? (
|
||||||
<div className="code-block">
|
<CodeBlockWrapper className="code-block">
|
||||||
<CodeHeader>
|
<CodeHeader>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
{codeCollapsible && shouldShowExpandButton && (
|
{codeCollapsible && shouldShowExpandButton && (
|
||||||
@ -118,7 +131,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
|||||||
</CodeFooter>
|
</CodeFooter>
|
||||||
)}
|
)}
|
||||||
{language === 'html' && children?.includes('</html>') && <Artifacts html={children} />}
|
{language === 'html' && children?.includes('</html>') && <Artifacts html={children} />}
|
||||||
</div>
|
</CodeBlockWrapper>
|
||||||
) : (
|
) : (
|
||||||
<code className={className}>{children}</code>
|
<code className={className}>{children}</code>
|
||||||
)
|
)
|
||||||
@ -142,6 +155,8 @@ const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ t
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CodeBlockWrapper = styled.div``
|
||||||
|
|
||||||
const CodeContent = styled.div<{ isShowLineNumbers: boolean }>`
|
const CodeContent = styled.div<{ isShowLineNumbers: boolean }>`
|
||||||
.shiki {
|
.shiki {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import 'katex/dist/katex.min.css'
|
|||||||
|
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { Message } from '@renderer/types'
|
import { Message } from '@renderer/types'
|
||||||
import { escapeBrackets } from '@renderer/utils/formula'
|
import { escapeBrackets, removeSvgEmptyLines } from '@renderer/utils/formula'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { FC, useMemo } from 'react'
|
import { FC, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -19,7 +19,7 @@ import ImagePreview from './ImagePreview'
|
|||||||
import Link from './Link'
|
import Link from './Link'
|
||||||
|
|
||||||
const ALLOWED_ELEMENTS =
|
const ALLOWED_ELEMENTS =
|
||||||
/<(style|p|div|span|b|i|strong|em|ul|ol|li|table|tr|td|th|thead|tbody|h[1-6]|blockquote|pre|code|br|hr)/i
|
/<(style|p|div|span|b|i|strong|em|ul|ol|li|table|tr|td|th|thead|tbody|h[1-6]|blockquote|pre|code|br|hr|svg|path|circle|rect|line|polyline|polygon|text|g|defs|title|desc|tspan)/i
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
@ -35,7 +35,7 @@ const Markdown: FC<Props> = ({ message }) => {
|
|||||||
const empty = isEmpty(message.content)
|
const empty = isEmpty(message.content)
|
||||||
const paused = message.status === 'paused'
|
const paused = message.status === 'paused'
|
||||||
const content = empty && paused ? t('message.chat.completion.paused') : message.content
|
const content = empty && paused ? t('message.chat.completion.paused') : message.content
|
||||||
return escapeBrackets(content)
|
return removeSvgEmptyLines(escapeBrackets(content))
|
||||||
}, [message.content, message.status, t])
|
}, [message.content, message.status, t])
|
||||||
|
|
||||||
const rehypePlugins = useMemo(() => {
|
const rehypePlugins = useMemo(() => {
|
||||||
@ -52,7 +52,6 @@ const Markdown: FC<Props> = ({ message }) => {
|
|||||||
className="markdown"
|
className="markdown"
|
||||||
rehypePlugins={rehypePlugins}
|
rehypePlugins={rehypePlugins}
|
||||||
remarkPlugins={[remarkMath, remarkGfm]}
|
remarkPlugins={[remarkMath, remarkGfm]}
|
||||||
disallowedElements={mathEngine === 'KaTeX' ? ['style'] : []}
|
|
||||||
components={
|
components={
|
||||||
{
|
{
|
||||||
a: Link,
|
a: Link,
|
||||||
|
|||||||
16
src/renderer/src/pages/home/Markdown/SvgPreview.tsx
Normal file
16
src/renderer/src/pages/home/Markdown/SvgPreview.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
const SvgPreview = ({ children }: { children: string }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: children }}
|
||||||
|
style={{
|
||||||
|
padding: '1em',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
border: '0.5px solid var(--color-code-background)',
|
||||||
|
borderTopLeftRadius: 0,
|
||||||
|
borderTopRightRadius: 0
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SvgPreview
|
||||||
@ -43,3 +43,16 @@ export function extractTitle(html: string): string | null {
|
|||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removeSvgEmptyLines(text: string): string {
|
||||||
|
// 用正则表达式匹配 <svg> 标签内的内容
|
||||||
|
const svgPattern = /(<svg[\s\S]*?<\/svg>)/g
|
||||||
|
|
||||||
|
return text.replace(svgPattern, (svgMatch) => {
|
||||||
|
// 将 SVG 内容按行分割,过滤掉空行,然后重新组合
|
||||||
|
return svgMatch
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line.trim() !== '')
|
||||||
|
.join('\n')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user