🐛 fix: Artifact Parsing and Rendering Bug Fix for Gemini 2.0 Flash (#5633)

*  feat: Enhance LobeArtifact processing and rehype plugin

- Add test cases for artifact processing with adjacent lobeThinking tags
- Modify utils to insert empty line between lobeThinking and lobeArtifact
- Implement rehype plugin for transforming LobeArtifact tags in markdown

*  feat: Improve LobeArtifact processing with advanced code block removal

- Add comprehensive test cases for artifact processing with various code block scenarios
- Enhance utils to handle fenced code blocks within and around lobeArtifact tags
- Support removing code blocks for HTML and other artifact types

*  feat: Enhance LobeArtifact code block removal tests

- Add comprehensive test cases for processWithArtifact function
- Cover scenarios with HTML and tool_code code blocks
- Test handling of code blocks with content before and after
- Verify processing of artifacts with and without surrounding code blocks

*  feat: Improve LobeArtifact code block processing regex

- Enhance regex in processWithArtifact to handle more complex code block scenarios
- Support better extraction of content before, within, and after code blocks
- Improve handling of artifacts with surrounding text and multiple tags

*  feat: Add artifact processing and selector tests

- Enhance `processWithArtifact` with debug logging and improved code block handling
- Add comprehensive test cases for artifact-related selectors in chat store
- Implement tests for message content, artifact code extraction, and tag closure detection

*  feat: Improve artifact code block extraction in selectors

- Add support for removing markdown code block wrapping in artifact content
- Update `artifactCode` selector to handle HTML and other code block scenarios
- Enhance test coverage for artifact code extraction with markdown-wrapped content

* 🔇 refactor: Remove debug console logs from processWithArtifact

- Clean up unnecessary console.log statements in artifact processing utility
- Improve code readability and performance by removing debug logging
- Maintain existing logic for artifact tag and code block processing

---------

Co-authored-by: yale <yale@example.com>
This commit is contained in:
Yale Huang
2025-02-06 01:31:34 +08:00
committed by GitHub
parent a90a75e613
commit 7d782b1165
6 changed files with 624 additions and 12 deletions

1
.gitignore vendored
View File

@@ -68,3 +68,4 @@ public/swe-worker*
*.patch
*.pdf
vertex-ai-key.json
.pnpm-store

View File

@@ -147,4 +147,288 @@ describe('processWithArtifact', () => {
expect(output).toEqual(`<lobeThinking>这个词汇涉及了`);
});
it('should handle no empty line between lobeThinking and lobeArtifact', () => {
const input = `<lobeThinking>这是一个思考过程。</lobeThinking>
<lobeArtifact identifier="test" type="image/svg+xml" title="测试">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" fill="blue"/>
</svg>
</lobeArtifact>`;
const output = processWithArtifact(input);
expect(output).toEqual(`<lobeThinking>这是一个思考过程。</lobeThinking>
<lobeArtifact identifier="test" type="image/svg+xml" title="测试"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> <rect width="100" height="100" fill="blue"/></svg></lobeArtifact>`);
});
it('should remove fenced code block between lobeArtifact and HTML content', () => {
const input = `<lobeArtifact identifier="web-calculator" type="text/html" title="简单的 Web 计算器">
\`\`\`html
<!DOCTYPE html>
<html lang="zh">
<head>
<title>计算器</title>
</head>
<body>
<div>计算器</div>
</body>
</html>
\`\`\`
</lobeArtifact>`;
const output = processWithArtifact(input);
expect(output).toEqual(
`<lobeArtifact identifier="web-calculator" type="text/html" title="简单的 Web 计算器"><!DOCTYPE html><html lang="zh"><head> <title>计算器</title></head><body> <div>计算器</div></body></html></lobeArtifact>`,
);
});
it('should remove fenced code block between lobeArtifact and HTML content without doctype', () => {
const input = `<lobeArtifact identifier="web-calculator" type="text/html" title="简单的 Web 计算器">
\`\`\`html
<html lang="zh">
<head>
<title>计算器</title>
</head>
<body>
<div>计算器</div>
</body>
</html>
\`\`\`
</lobeArtifact>`;
const output = processWithArtifact(input);
expect(output).toEqual(
`<lobeArtifact identifier="web-calculator" type="text/html" title="简单的 Web 计算器"><html lang="zh"><head> <title>计算器</title></head><body> <div>计算器</div></body></html></lobeArtifact>`,
);
});
it('should remove outer fenced code block wrapping lobeThinking and lobeArtifact', () => {
const input =
'```tool_code\n<lobeThinking>这是一个思考过程。</lobeThinking>\n\n<lobeArtifact identifier="test" type="text/html" title="测试">\n<div>测试内容</div>\n</lobeArtifact>\n```';
const output = processWithArtifact(input);
expect(output).toEqual(
'<lobeThinking>这是一个思考过程。</lobeThinking>\n\n<lobeArtifact identifier="test" type="text/html" title="测试"><div>测试内容</div></lobeArtifact>',
);
});
it('should handle both outer code block and inner HTML code block', () => {
const input =
'```tool_code\n<lobeThinking>这是一个思考过程。</lobeThinking>\n\n<lobeArtifact identifier="test" type="text/html" title="测试">\n```html\n<!DOCTYPE html>\n<html>\n<body>\n<div>测试内容</div>\n</body>\n</html>\n```\n</lobeArtifact>\n```';
const output = processWithArtifact(input);
expect(output).toEqual(
'<lobeThinking>这是一个思考过程。</lobeThinking>\n\n<lobeArtifact identifier="test" type="text/html" title="测试"><!DOCTYPE html><html><body><div>测试内容</div></body></html></lobeArtifact>',
);
});
it('should handle complete conversation with text and tags', () => {
const input = `Sure, I can help you with that! Here is a basic calculator built using HTML, CSS, and JavaScript.
<lobeThinking>A web calculator is a substantial piece of code and a good candidate for an artifact. It's self-contained, and it's likely that the user will want to modify it. This is a new request, so I will create a new artifact.</lobeThinking>
<lobeArtifact identifier="web-calculator" type="text/html" title="Web Calculator">
\`\`\`html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple Calculator</title>
</head>
<body>
<div>Calculator</div>
</body>
</html>
\`\`\`
</lobeArtifact>
This code provides a basic calculator that can perform addition, subtraction, multiplication, and division.`;
const output = processWithArtifact(input);
expect(output)
.toEqual(`Sure, I can help you with that! Here is a basic calculator built using HTML, CSS, and JavaScript.
<lobeThinking>A web calculator is a substantial piece of code and a good candidate for an artifact. It's self-contained, and it's likely that the user will want to modify it. This is a new request, so I will create a new artifact.</lobeThinking>
<lobeArtifact identifier="web-calculator" type="text/html" title="Web Calculator"><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Simple Calculator</title></head><body> <div>Calculator</div></body></html></lobeArtifact>
This code provides a basic calculator that can perform addition, subtraction, multiplication, and division.`);
});
});
describe('outer code block removal', () => {
it('should remove outer html code block', () => {
const input = `\`\`\`html
<lobeThinking>Test thinking</lobeThinking>
<lobeArtifact identifier="test" type="text/html" title="Test">
<!DOCTYPE html>
<html>
<body>Test</body>
</html>
</lobeArtifact>
\`\`\``;
const output = processWithArtifact(input);
expect(output).toEqual(`<lobeThinking>Test thinking</lobeThinking>
<lobeArtifact identifier="test" type="text/html" title="Test"><!DOCTYPE html><html><body>Test</body></html></lobeArtifact>`);
});
it('should remove outer tool_code code block', () => {
const input = `\`\`\`tool_code
<lobeThinking>Test thinking</lobeThinking>
<lobeArtifact identifier="test" type="text/html" title="Test">
<!DOCTYPE html>
<html>
<body>Test</body>
</html>
</lobeArtifact>
\`\`\``;
const output = processWithArtifact(input);
expect(output).toEqual(`<lobeThinking>Test thinking</lobeThinking>
<lobeArtifact identifier="test" type="text/html" title="Test"><!DOCTYPE html><html><body>Test</body></html></lobeArtifact>`);
});
it('should handle input without outer code block', () => {
const input = `<lobeThinking>Test thinking</lobeThinking>
<lobeArtifact identifier="test" type="text/html" title="Test">
<!DOCTYPE html>
<html>
<body>Test</body>
</html>
</lobeArtifact>`;
const output = processWithArtifact(input);
expect(output).toEqual(`<lobeThinking>Test thinking</lobeThinking>
<lobeArtifact identifier="test" type="text/html" title="Test"><!DOCTYPE html><html><body>Test</body></html></lobeArtifact>`);
});
it('should handle code block with content before and after', () => {
const input = `Some text before
\`\`\`html
<lobeThinking>Test thinking</lobeThinking>
<lobeArtifact identifier="test" type="text/html" title="Test">
<!DOCTYPE html>
<html>
<body>Test</body>
</html>
</lobeArtifact>
\`\`\`
Some text after`;
const output = processWithArtifact(input);
expect(output).toEqual(`Some text before
<lobeThinking>Test thinking</lobeThinking>
<lobeArtifact identifier="test" type="text/html" title="Test"><!DOCTYPE html><html><body>Test</body></html></lobeArtifact>
Some text after`);
});
it('should handle code block with only lobeArtifact tag', () => {
const input = `\`\`\`html
<lobeArtifact identifier="test" type="text/html" title="Test">
<!DOCTYPE html>
<html>
<body>Test</body>
</html>
</lobeArtifact>
\`\`\``;
const output = processWithArtifact(input);
expect(output).toEqual(
`<lobeArtifact identifier="test" type="text/html" title="Test"><!DOCTYPE html><html><body>Test</body></html></lobeArtifact>`,
);
});
it('should handle code block with surrounding text and both lobeThinking and lobeArtifact', () => {
const input = `---
\`\`\`tool_code
<lobeThinking>The user reported a \`SyntaxError\` in the browser console, indicating a problem with the JavaScript code in the calculator artifact. The error message "Identifier 'display' has already been declared" suggests a variable naming conflict. I need to review the JavaScript code and correct the issue. This is an update to the existing "calculator-web-artifact" artifact.</lobeThinking>
<lobeArtifact identifier="calculator-web-artifact" type="text/html" title="Simple Calculator">
<!DOCTYPE html>
<html lang="en">
...
</html>
</lobeArtifact>
\`\`\`
I've updated the calculator artifact. The issue was a naming conflict with the \`display\` variable. I've renamed the input element's ID to \`calc-display\` and the JavaScript variable to \`displayElement\` to avoid the conflict. The calculator should now function correctly.
---`;
const output = processWithArtifact(input);
expect(output).toEqual(`---
<lobeThinking>The user reported a \`SyntaxError\` in the browser console, indicating a problem with the JavaScript code in the calculator artifact. The error message "Identifier 'display' has already been declared" suggests a variable naming conflict. I need to review the JavaScript code and correct the issue. This is an update to the existing "calculator-web-artifact" artifact.</lobeThinking>
<lobeArtifact identifier="calculator-web-artifact" type="text/html" title="Simple Calculator"><!DOCTYPE html><html lang="en">...</html></lobeArtifact>
I've updated the calculator artifact. The issue was a naming conflict with the \`display\` variable. I've renamed the input element's ID to \`calc-display\` and the JavaScript variable to \`displayElement\` to avoid the conflict. The calculator should now function correctly.
---`);
});
it('should handle code block before lobeThinking and lobeArtifact', () => {
const input = `
Okay, I'll create a temperature converter with the logic wrapped in an IIFE and event listeners attached in Javascript.
\`\`\`html
<!DOCTYPE html>
<html lang="en">
...
</html>
\`\`\`
<lobeThinking>This is a good candidate for an artifact. It's a self-contained HTML document with embedded JavaScript that provides a functional temperature converter. It's more than a simple code snippet and can be reused or modified. This is a new request, so I'll create a new artifact with the identifier "temperature-converter".</lobeThinking>
<lobeArtifact identifier="temperature-converter" type="text/html" title="Temperature Converter">
\`\`\`html
<!DOCTYPE html>
<html lang="en">
...
</html>
\`\`\`
</lobeArtifact>
This HTML document includes the temperature converter with the requested features: the logic is wrapped in an IIFE, and event listeners are attached in JavaScript.
`;
const output = processWithArtifact(input);
expect(output)
.toEqual(`Okay, I'll create a temperature converter with the logic wrapped in an IIFE and event listeners attached in Javascript.
\`\`\`html
<!DOCTYPE html>
<html lang="en">
...
</html>
\`\`\`
<lobeThinking>This is a good candidate for an artifact. It's a self-contained HTML document with embedded JavaScript that provides a functional temperature converter. It's more than a simple code snippet and can be reused or modified. This is a new request, so I'll create a new artifact with the identifier "temperature-converter".</lobeThinking>
<lobeArtifact identifier="temperature-converter" type="text/html" title="Temperature Converter"><!DOCTYPE html><html lang="en">...</html></lobeArtifact>
This HTML document includes the temperature converter with the requested features: the logic is wrapped in an IIFE, and event listeners are attached in JavaScript.`);
});
});

View File

@@ -4,24 +4,55 @@ import { ARTIFACT_TAG_REGEX, ARTIFACT_THINKING_TAG_REGEX } from '@/const/plugin'
* Replace all line breaks in the matched `lobeArtifact` tag with an empty string
*/
export const processWithArtifact = (input: string = '') => {
let output = input;
const thinkMatch = ARTIFACT_THINKING_TAG_REGEX.exec(input);
// First remove outer fenced code block if it exists
let output = input.replace(
/^([\S\s]*?)\s*```[^\n]*\n((?:<lobeThinking>[\S\s]*?<\/lobeThinking>\s*\n\s*)?<lobeArtifact[\S\s]*?<\/lobeArtifact>\s*)\n```\s*([\S\s]*?)$/,
(_, before = '', content, after = '') => {
return [before.trim(), content.trim(), after.trim()].filter(Boolean).join('\n\n');
},
);
const thinkMatch = ARTIFACT_THINKING_TAG_REGEX.exec(output);
// If the input contains the `lobeThinking` tag, replace all line breaks with an empty string
if (thinkMatch)
output = input.replace(ARTIFACT_THINKING_TAG_REGEX, (match) =>
if (thinkMatch) {
output = output.replace(ARTIFACT_THINKING_TAG_REGEX, (match) =>
match.replaceAll(/\r?\n|\r/g, ''),
);
}
const match = ARTIFACT_TAG_REGEX.exec(input);
// Add empty line between lobeThinking and lobeArtifact if they are adjacent
output = output.replace(/(<\/lobeThinking>)\r?\n(<lobeArtifact)/, '$1\n\n$2');
// Remove fenced code block between lobeArtifact and HTML content
output = output.replace(
/(<lobeArtifact[^>]*>)\s*```[^\n]*\n([\S\s]*?)(```\n)?(<\/lobeArtifact>)/,
(_, start, content, __, end) => {
if (content.trim().startsWith('<!DOCTYPE html') || content.trim().startsWith('<html')) {
return start + content.trim() + end;
}
return start + content + (__ || '') + end;
},
);
// Keep existing code blocks that are not part of lobeArtifact
output = output.replace(
/^([\S\s]*?)(<lobeThinking>[\S\s]*?<\/lobeThinking>\s*\n\s*<lobeArtifact[\S\s]*?<\/lobeArtifact>)([\S\s]*?)$/,
(_, before, content, after) => {
return [before.trim(), content.trim(), after.trim()].filter(Boolean).join('\n\n');
},
);
const match = ARTIFACT_TAG_REGEX.exec(output);
// If the input contains the `lobeArtifact` tag, replace all line breaks with an empty string
if (match)
return output.replace(ARTIFACT_TAG_REGEX, (match) => match.replaceAll(/\r?\n|\r/g, ''));
if (match) {
output = output.replace(ARTIFACT_TAG_REGEX, (match) => match.replaceAll(/\r?\n|\r/g, ''));
}
// if not match, check if it's start with <lobeArtifact but not closed
const regex = /<lobeArtifact\b(?:(?!\/?>)[\S\s])*$/;
if (regex.test(output)) {
return output.replace(regex, '<lobeArtifact>');
output = output.replace(regex, '<lobeArtifact>');
}
return output;

View File

@@ -0,0 +1,125 @@
import { describe, expect, it } from 'vitest';
import rehypePlugin from './rehypePlugin';
describe('rehypePlugin', () => {
it('should transform <lobeArtifact> tags with attributes', () => {
const tree = {
type: 'root',
children: [
{
type: 'element',
tagName: 'p',
children: [
{
type: 'raw',
value: '<lobeArtifact identifier="test-id" type="image/svg+xml" title="Test Title">',
},
{ type: 'text', value: 'Artifact content' },
{ type: 'raw', value: '</lobeArtifact>' },
],
},
],
};
const expectedTree = {
type: 'root',
children: [
{
type: 'element',
tagName: 'lobeArtifact',
properties: {
identifier: 'test-id',
type: 'image/svg+xml',
title: 'Test Title',
},
children: [{ type: 'text', value: 'Artifact content' }],
},
],
};
const plugin = rehypePlugin();
plugin(tree);
expect(tree).toEqual(expectedTree);
});
it('should handle mixed content with thinking tags and plain text', () => {
const tree = {
type: 'root',
children: [
{
type: 'element',
tagName: 'p',
children: [{ type: 'text', value: 'Initial plain text paragraph' }],
},
{
type: 'element',
tagName: 'p',
children: [
{ type: 'raw', value: '<lobeThinking>' },
{ type: 'text', value: 'AI is thinking...' },
{ type: 'raw', value: '</lobeThinking>' },
],
},
{
type: 'element',
tagName: 'p',
children: [
{
type: 'raw',
value: '<lobeArtifact identifier="test-id" type="image/svg+xml" title="Test Title">',
},
{ type: 'text', value: 'Artifact content' },
{ type: 'raw', value: '</lobeArtifact>' },
],
},
{
type: 'element',
tagName: 'p',
children: [{ type: 'text', value: 'Final plain text paragraph' }],
},
],
};
const expectedTree = {
type: 'root',
children: [
{
type: 'element',
tagName: 'p',
children: [{ type: 'text', value: 'Initial plain text paragraph' }],
},
{
type: 'element',
tagName: 'p',
children: [
{ type: 'raw', value: '<lobeThinking>' },
{ type: 'text', value: 'AI is thinking...' },
{ type: 'raw', value: '</lobeThinking>' },
],
},
{
type: 'element',
tagName: 'lobeArtifact',
properties: {
identifier: 'test-id',
type: 'image/svg+xml',
title: 'Test Title',
},
children: [{ type: 'text', value: 'Artifact content' }],
},
{
type: 'element',
tagName: 'p',
children: [{ type: 'text', value: 'Final plain text paragraph' }],
},
],
};
const plugin = rehypePlugin();
plugin(tree);
expect(tree).toEqual(expectedTree);
});
});

View File

@@ -1,16 +1,23 @@
import { describe, expect, it } from 'vitest';
import type { ChatStoreState } from '@/store/chat';
import { ChatMessage } from '@/types/message';
import { chatPortalSelectors } from './selectors';
describe('chatDockSelectors', () => {
const createState = (overrides?: Partial<ChatStoreState>) =>
({
const createState = (overrides?: Partial<ChatStoreState>) => {
const state = {
showPortal: false,
portalToolMessage: undefined,
messagesMap: {},
activeId: 'test-id',
activeTopicId: undefined,
...overrides,
}) as ChatStoreState;
} as ChatStoreState;
return state;
};
describe('showDock', () => {
it('should return the showDock state', () => {
@@ -92,4 +99,163 @@ describe('chatDockSelectors', () => {
expect(chatPortalSelectors.previewFileId(state)).toBe('file-id');
});
});
describe('artifactMessageContent', () => {
it('should return empty string when message not found', () => {
const state = createState();
expect(chatPortalSelectors.artifactMessageContent('non-existent-id')(state)).toBe('');
});
it('should return message content when message exists', () => {
const messageContent = 'Test message content';
const state = createState({
messagesMap: {
'test-id_null': [
{
id: 'test-id',
content: messageContent,
createdAt: Date.now(),
updatedAt: Date.now(),
role: 'user',
meta: {},
sessionId: 'test-id',
} as ChatMessage,
],
},
});
expect(chatPortalSelectors.artifactMessageContent('test-id')(state)).toBe(messageContent);
});
});
describe('artifactCode', () => {
it('should return empty string when no artifact tag found', () => {
const state = createState({
messagesMap: {
'test-id_null': [
{
id: 'test-id',
content: 'No artifact tag here',
createdAt: Date.now(),
updatedAt: Date.now(),
role: 'user',
meta: {},
sessionId: 'test-id',
} as ChatMessage,
],
},
});
expect(chatPortalSelectors.artifactCode('test-id')(state)).toBe('');
});
it('should extract content from artifact tag', () => {
const artifactContent = 'Test artifact content';
const state = createState({
messagesMap: {
'test-id_null': [
{
id: 'test-id',
content: `<lobeArtifact type="text">${artifactContent}</lobeArtifact>`,
createdAt: Date.now(),
updatedAt: Date.now(),
role: 'user',
meta: {},
sessionId: 'test-id',
} as ChatMessage,
],
},
});
expect(chatPortalSelectors.artifactCode('test-id')(state)).toBe(artifactContent);
});
it('should remove markdown code block wrapping HTML content', () => {
const htmlContent = `<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
<div>Test content</div>
</body>
</html>`;
const state = createState({
messagesMap: {
'test-id_null': [
{
id: 'test-id',
content: `<lobeArtifact type="text/html">
\`\`\`html
${htmlContent}
\`\`\`
</lobeArtifact>`,
createdAt: Date.now(),
updatedAt: Date.now(),
role: 'user',
meta: {},
sessionId: 'test-id',
} as ChatMessage,
],
},
});
expect(chatPortalSelectors.artifactCode('test-id')(state)).toBe(htmlContent);
});
});
describe('isArtifactTagClosed', () => {
it('should return false for unclosed artifact tag', () => {
const state = createState({
messagesMap: {
'test-id_null': [
{
id: 'test-id',
content: '<lobeArtifact type="text">Test content',
createdAt: Date.now(),
updatedAt: Date.now(),
role: 'user',
meta: {},
sessionId: 'test-id',
} as ChatMessage,
],
},
});
expect(chatPortalSelectors.isArtifactTagClosed('test-id')(state)).toBe(false);
});
it('should return true for closed artifact tag', () => {
const state = createState({
messagesMap: {
'test-id_null': [
{
id: 'test-id',
content: '<lobeArtifact type="text">Test content</lobeArtifact>',
createdAt: Date.now(),
updatedAt: Date.now(),
role: 'user',
meta: {},
sessionId: 'test-id',
} as ChatMessage,
],
},
});
expect(chatPortalSelectors.isArtifactTagClosed('test-id')(state)).toBe(true);
});
it('should return false when no artifact tag exists', () => {
const state = createState({
messagesMap: {
'test-id_null': [
{
id: 'test-id',
content: 'No artifact tag here',
createdAt: Date.now(),
updatedAt: Date.now(),
role: 'user',
meta: {},
sessionId: 'test-id',
} as ChatMessage,
],
},
});
expect(chatPortalSelectors.isArtifactTagClosed('test-id')(state)).toBe(false);
});
});
});

View File

@@ -35,7 +35,12 @@ const artifactCode = (id: string) => (s: ChatStoreState) => {
const messageContent = artifactMessageContent(id)(s);
const result = messageContent.match(ARTIFACT_TAG_REGEX);
return result?.groups?.content || '';
let content = result?.groups?.content || '';
// Remove markdown code block if content is wrapped
content = content.replace(/^\s*```[^\n]*\n([\S\s]*?)\n```\s*$/, '$1');
return content;
};
const isArtifactTagClosed = (id: string) => (s: ChatStoreState) => {