mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
🐛 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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -68,3 +68,4 @@ public/swe-worker*
|
||||
*.patch
|
||||
*.pdf
|
||||
vertex-ai-key.json
|
||||
.pnpm-store
|
||||
@@ -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.`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user