feat(HtmlArtifacts): make fancy code block optional (#10043)

* feat(HtmlArtifacts): make fancy code block optional

* test: fix mocks
This commit is contained in:
one 2025-09-08 16:02:28 +08:00 committed by GitHub
parent f9c60423a8
commit 9f81a77943
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 59 additions and 7 deletions

View File

@ -19,7 +19,7 @@ import { MAX_COLLAPSED_CODE_HEIGHT } from '@renderer/config/constant'
import { useSettings } from '@renderer/hooks/useSettings'
import { pyodideService } from '@renderer/services/PyodideService'
import { getExtensionByLanguage } from '@renderer/utils/code-language'
import { extractHtmlTitle } from '@renderer/utils/formats'
import { extractHtmlTitle, getFileNameFromHtmlTitle } from '@renderer/utils/formats'
import dayjs from 'dayjs'
import React, { memo, startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -135,8 +135,8 @@ export const CodeBlockView: React.FC<Props> = memo(({ children, language, onSave
let fileName = ''
// 尝试提取 HTML 标题
if (language === 'html' && children.includes('</html>')) {
fileName = extractHtmlTitle(children) || ''
if (language === 'html') {
fileName = getFileNameFromHtmlTitle(extractHtmlTitle(children)) || ''
}
// 默认使用日期格式命名

View File

@ -538,6 +538,10 @@
"tip": "The run button will be displayed in the toolbar of executable code blocks, please do not execute dangerous code!",
"title": "Code Execution"
},
"code_fancy_block": {
"label": "Fancy code block",
"tip": "Enable fancy style for code block, e.g., html card"
},
"code_image_tools": {
"label": "Enable preview tools",
"tip": "Enable preview tools for images rendered from code blocks such as mermaid"

View File

@ -538,6 +538,10 @@
"tip": "可执行的代码块工具栏中会显示运行按钮,注意不要执行危险代码!",
"title": "代码执行"
},
"code_fancy_block": {
"label": "花式代码块",
"tip": "使用更美观的代码块样式,例如 HTML 卡片"
},
"code_image_tools": {
"label": "启用预览工具",
"tip": "为 mermaid 等代码块渲染后的图像启用预览工具"

View File

@ -538,6 +538,10 @@
"tip": "可執行的程式碼塊工具欄中會顯示運行按鈕,注意不要執行危險程式碼!",
"title": "程式碼執行"
},
"code_fancy_block": {
"label": "花式程式碼區塊",
"tip": "使用更美觀的程式碼區塊樣式,例如 HTML 卡片"
},
"code_image_tools": {
"label": "啟用預覽工具",
"tip": "為 mermaid 等程式碼區塊渲染後的圖像啟用預覽工具"

View File

@ -1,4 +1,5 @@
import { CodeBlockView, HtmlArtifactsCard } from '@renderer/components/CodeBlockView'
import { useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import store from '@renderer/store'
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
@ -19,6 +20,7 @@ const CodeBlock: React.FC<Props> = ({ children, className, node, blockId }) => {
const languageMatch = /language-([\w-+]+)/.exec(className || '')
const isMultiline = children?.includes('\n')
const language = languageMatch?.[1] ?? (isMultiline ? 'text' : null)
const { codeFancyBlock } = useSettings()
// 代码块 id
const id = useMemo(() => getCodeBlockId(node?.position?.start), [node?.position?.start])
@ -41,10 +43,12 @@ const CodeBlock: React.FC<Props> = ({ children, className, node, blockId }) => {
)
if (language !== null) {
// HTML 代码块特殊处理
if (language === 'html') {
const isOpenFence = isOpenFenceBlock(children?.length, languageMatch?.[1]?.length, node?.position)
return <HtmlArtifactsCard html={children} onSave={handleSave} isStreaming={isStreaming && isOpenFence} />
// Fancy code block
if (codeFancyBlock) {
if (language === 'html') {
const isOpenFence = isOpenFenceBlock(children?.length, languageMatch?.[1]?.length, node?.position)
return <HtmlArtifactsCard html={children} onSave={handleSave} isStreaming={isStreaming && isOpenFence} />
}
}
return (

View File

@ -12,6 +12,7 @@ const mocks = vi.hoisted(() => ({
getCodeBlockId: vi.fn(),
isOpenFenceBlock: vi.fn(),
selectById: vi.fn(),
useSettings: vi.fn().mockReturnValue({ codeFancyBlock: true }),
CodeBlockView: vi.fn(({ onSave, children }) => (
<div>
<code>{children}</code>
@ -53,6 +54,10 @@ vi.mock('@renderer/store/messageBlock', () => ({
}
}))
vi.mock('@renderer/hooks/useSettings', () => ({
useSettings: () => mocks.useSettings()
}))
vi.mock('@renderer/components/CodeBlockView', () => ({
CodeBlockView: mocks.CodeBlockView,
HtmlArtifactsCard: mocks.HtmlArtifactsCard

View File

@ -23,6 +23,7 @@ import {
setCodeCollapsible,
setCodeEditor,
setCodeExecution,
setCodeFancyBlock,
setCodeImageTools,
setCodeShowLineNumbers,
setCodeViewer,
@ -99,6 +100,7 @@ const SettingsTab: FC<Props> = (props) => {
codeViewer,
codeImageTools,
codeExecution,
codeFancyBlock,
mathEngine,
mathEnableSingleDollar,
autoTranslateWithSpace,
@ -451,6 +453,18 @@ const SettingsTab: FC<Props> = (props) => {
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>
{t('chat.settings.code_fancy_block.label')}
<HelpTooltip title={t('chat.settings.code_fancy_block.tip')} />
</SettingRowTitleSmall>
<Switch
size="small"
checked={codeFancyBlock}
onChange={(checked) => dispatch(setCodeFancyBlock(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>
{t('chat.settings.code_execution.title')}

View File

@ -2417,6 +2417,17 @@ const migrateConfig = {
logger.error('migrate 150 error', error as Error)
return state
}
},
'151': (state: RootState) => {
try {
if (state.settings) {
state.settings.codeFancyBlock = true
}
return state
} catch (error) {
logger.error('migrate 151 error', error as Error)
return state
}
}
}

View File

@ -97,6 +97,7 @@ export interface SettingsState {
codeCollapsible: boolean
codeWrappable: boolean
codeImageTools: boolean
codeFancyBlock: boolean
mathEngine: MathEngine
mathEnableSingleDollar: boolean
messageStyle: 'plain' | 'bubble'
@ -282,6 +283,7 @@ export const initialState: SettingsState = {
codeCollapsible: false,
codeWrappable: false,
codeImageTools: false,
codeFancyBlock: true,
mathEngine: 'KaTeX',
mathEnableSingleDollar: true,
messageStyle: 'plain',
@ -611,6 +613,9 @@ const settingsSlice = createSlice({
setCodeImageTools: (state, action: PayloadAction<boolean>) => {
state.codeImageTools = action.payload
},
setCodeFancyBlock: (state, action: PayloadAction<boolean>) => {
state.codeFancyBlock = action.payload
},
setMathEngine: (state, action: PayloadAction<MathEngine>) => {
state.mathEngine = action.payload
},
@ -900,6 +905,7 @@ export const {
setCodeCollapsible,
setCodeWrappable,
setCodeImageTools,
setCodeFancyBlock,
setMathEngine,
setMathEnableSingleDollar,
setFoldDisplayMode,