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: 1px solid var(--color-border);
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
margin: 16px 0; margin: 10px 0;
` `
const GeneratingContainer = styled.div` 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)" />}> <Spin spinning={isLoading} indicator={<SvgSpinners180Ring color="var(--color-text-2)" />}>
<Flex vertical style={{ minHeight: isLoading ? '2rem' : 'auto' }}> <Flex vertical style={{ minHeight: isLoading ? '2rem' : 'auto' }}>
{(mermaidError || error) && <PreviewError>{mermaidError || error}</PreviewError>} {(mermaidError || error) && <PreviewError>{mermaidError || error}</PreviewError>}
<StyledMermaid ref={mermaidRef} className="mermaid" /> <StyledMermaid ref={mermaidRef} className="mermaid special-preview" />
</Flex> </Flex>
</Spin> </Spin>
) )

View File

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

View File

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

View File

@ -14,7 +14,7 @@ const MessageContent: React.FC<Props> = ({ message }) => {
return ( return (
<> <>
{!isEmpty(message.mentions) && ( {!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>)} {message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)}
</Flex> </Flex>
)} )}

View File

@ -8,6 +8,7 @@ import {
findCitationInChildren, findCitationInChildren,
getCodeBlockId, getCodeBlockId,
getExtensionByLanguage, getExtensionByLanguage,
isHtmlCode,
markdownToPlainText, markdownToPlainText,
processLatexBrackets, processLatexBrackets,
removeTrailingDoubleSpaces, 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 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 * Markdown
* @param markdown Markdown * @param markdown Markdown