feat: add HTML code detection utility and integrate into CodeBlockView

- Introduced `isHtmlCode` function to identify HTML content based on DOCTYPE and tag presence.
- Updated `CodeBlockView` to utilize `isHtmlCode` for conditional rendering of HTML artifacts.
- Added comprehensive tests for `isHtmlCode` to ensure accurate detection of HTML structures.
This commit is contained in:
kangfenmao 2025-07-11 12:04:02 +08:00
parent 5b9ff3053b
commit 84a6c2da59
7 changed files with 76 additions and 6 deletions

View File

@ -250,7 +250,7 @@ const Container = styled.div<{ $isStreaming: boolean }>`
border: 1px solid var(--color-border);
border-radius: 8px;
overflow: hidden;
margin: 16px 0;
margin: 10px 0;
`
const GeneratingContainer = styled.div`

View File

@ -142,7 +142,7 @@ const MermaidPreview: React.FC<BasicPreviewProps> = ({ children, setTools }) =>
<Spin spinning={isLoading} indicator={<SvgSpinners180Ring color="var(--color-text-2)" />}>
<Flex vertical style={{ minHeight: isLoading ? '2rem' : 'auto' }}>
{(mermaidError || error) && <PreviewError>{mermaidError || error}</PreviewError>}
<StyledMermaid ref={mermaidRef} className="mermaid" />
<StyledMermaid ref={mermaidRef} className="mermaid special-preview" />
</Flex>
</Spin>
)

View File

@ -4,7 +4,7 @@ import { CodeTool, CodeToolbar, TOOL_SPECS, useCodeTool } from '@renderer/compon
import { useSettings } from '@renderer/hooks/useSettings'
import { pyodideService } from '@renderer/services/PyodideService'
import { extractTitle } from '@renderer/utils/formats'
import { getExtensionByLanguage, isValidPlantUML } from '@renderer/utils/markdown'
import { getExtensionByLanguage, isHtmlCode, isValidPlantUML } from '@renderer/utils/markdown'
import dayjs from 'dayjs'
import { CirclePlay, CodeXml, Copy, Download, Eye, Square, SquarePen, SquareSplitHorizontal } from 'lucide-react'
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'
@ -229,7 +229,7 @@ export const CodeBlockView: React.FC<Props> = memo(({ children, language, onSave
}, [specialView, sourceView, viewMode])
// HTML 代码块特殊处理 - 在所有 hooks 调用之后
if (language === 'html') {
if (language === 'html' && isHtmlCode(children)) {
return <HtmlArtifactsCard html={children} />
}

View File

@ -95,7 +95,7 @@ const MessageItem: FC<Props> = ({
stopEditing()
}, [stopEditing])
const isLastMessage = index === 0
const isLastMessage = index === 0 || !!isGrouped
const isAssistantMessage = message.role === 'assistant'
const showMenubar = !hideMenuBar && !isStreaming && !message.status.includes('ing') && !isEditing
@ -190,6 +190,7 @@ const MessageContainer = styled.div`
transform: translateZ(0);
will-change: transform;
padding: 10px;
padding-bottom: 0;
border-radius: 10px;
&.message-highlight {
background-color: var(--color-primary-mute);

View File

@ -14,7 +14,7 @@ const MessageContent: React.FC<Props> = ({ message }) => {
return (
<>
{!isEmpty(message.mentions) && (
<Flex gap="8px" wrap>
<Flex gap="8px" wrap style={{ marginBottom: '10px' }}>
{message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)}
</Flex>
)}

View File

@ -8,6 +8,7 @@ import {
findCitationInChildren,
getCodeBlockId,
getExtensionByLanguage,
isHtmlCode,
markdownToPlainText,
processLatexBrackets,
removeTrailingDoubleSpaces,
@ -698,4 +699,31 @@ $$
})
})
})
describe('isHtmlCode', () => {
it('should detect HTML with DOCTYPE', () => {
expect(isHtmlCode('<!DOCTYPE html>')).toBe(true)
expect(isHtmlCode('<!doctype html>')).toBe(true)
})
it('should detect HTML with html/head/body tags', () => {
expect(isHtmlCode('<html>')).toBe(true)
expect(isHtmlCode('</html>')).toBe(true)
expect(isHtmlCode('<head>')).toBe(true)
expect(isHtmlCode('<body>')).toBe(true)
})
it('should detect complete HTML structure', () => {
const html = '<html><head><title>Test</title></head><body>Hello</body></html>'
expect(isHtmlCode(html)).toBe(true)
})
it('should return false for non-HTML content', () => {
expect(isHtmlCode(null)).toBe(false)
expect(isHtmlCode('')).toBe(false)
expect(isHtmlCode('Hello world')).toBe(false)
expect(isHtmlCode('a < b')).toBe(false)
expect(isHtmlCode('<div>')).toBe(false)
})
})
})

View File

@ -267,6 +267,47 @@ export function isValidPlantUML(code: string | null): boolean {
return diagramType !== undefined && code.search(`@end${diagramType}`) !== -1
}
/**
* HTML特征
* @param code
* @returns HTML代码 true false
*/
export function isHtmlCode(code: string | null): boolean {
if (!code || !code.trim()) {
return false
}
const trimmedCode = code.trim()
// 检查是否包含HTML文档类型声明
if (trimmedCode.includes('<!DOCTYPE html>') || trimmedCode.includes('<!doctype html>')) {
return true
}
// 检查是否包含html标签
if (trimmedCode.includes('<html') || trimmedCode.includes('</html>')) {
return true
}
// 检查是否包含head标签
if (trimmedCode.includes('<head>') || trimmedCode.includes('</head>')) {
return true
}
// 检查是否包含body标签
if (trimmedCode.includes('<body') || trimmedCode.includes('</body>')) {
return true
}
// 检查是否以HTML标签开头和结尾的完整HTML结构
const htmlTagPattern = /^\s*<html[^>]*>[\s\S]*<\/html>\s*$/i
if (htmlTagPattern.test(trimmedCode)) {
return true
}
return false
}
/**
* Markdown
* @param markdown Markdown