mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-11 08:43:48 +00:00
🧵 fix: Preserve Fenced Markdown Artifacts (#14121)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
* fix: Preserve fenced markdown artifacts * fix: Satisfy artifact CI checks * fix: Handle longer artifact fences in updates
This commit is contained in:
parent
a03c574bef
commit
8fcb77fe6f
7 changed files with 350 additions and 60 deletions
|
|
@ -39,9 +39,9 @@ Artifacts are for substantial, self-contained content that users might modify or
|
|||
1. Create the artifact using the following format:
|
||||
|
||||
:::artifact{identifier="unique-identifier" type="mime-type" title="Artifact Title"}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
Your artifact content here
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
2. Assign an identifier to the \`identifier\` attribute. For updates, reuse the prior identifier. For new artifacts, the identifier should be descriptive and relevant to the content, using kebab-case (e.g., "example-code-snippet"). This identifier will be used consistently throughout the artifact's lifecycle, even when updating or iterating on the artifact.
|
||||
|
|
@ -67,7 +67,7 @@ Artifacts are for substantial, self-contained content that users might modify or
|
|||
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
|
||||
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
|
||||
6. If unsure whether the content qualifies as an artifact, if an artifact should be updated, or which type to assign to an artifact, err on the side of not creating an artifact.
|
||||
7. Always use triple backticks (\`\`\`) to enclose the content within the artifact, regardless of the content type.
|
||||
7. Use a backtick fence longer than any backtick fence in the artifact content. Use a 4-backtick fence by default; if the artifact content contains a 4-backtick fence, use 5 backticks, and so on.
|
||||
</artifact_instructions>
|
||||
|
||||
Here are some examples of correct usage of artifacts:
|
||||
|
|
@ -84,7 +84,7 @@ Here are some examples of correct usage of artifacts:
|
|||
Sure! Here's a simple flow chart depicting the process of making tea using Mermaid syntax:
|
||||
|
||||
:::artifact{identifier="tea-making-flowchart" type="application/vnd.mermaid" title="Flow chart: Making Tea"}
|
||||
\`\`\`mermaid
|
||||
\`\`\`\`mermaid
|
||||
graph TD
|
||||
A[Start] --> B{Water boiled?}
|
||||
B -->|Yes| C[Add tea leaves to cup]
|
||||
|
|
@ -96,7 +96,7 @@ Here are some examples of correct usage of artifacts:
|
|||
G --> H[Add milk or sugar, if desired]
|
||||
H --> I[Enjoy your tea!]
|
||||
I --> J[End]
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
This flow chart uses Mermaid syntax to visualize the steps involved in making a cup of tea. Here's a brief explanation of the process:
|
||||
|
|
@ -152,9 +152,9 @@ Artifacts are for substantial, self-contained content that users might modify or
|
|||
1. Create the artifact using the following format:
|
||||
|
||||
:::artifact{identifier="unique-identifier" type="mime-type" title="Artifact Title"}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
Your artifact content here
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
2. Assign an identifier to the \`identifier\` attribute. For updates, reuse the prior identifier. For new artifacts, the identifier should be descriptive and relevant to the content, using kebab-case (e.g., "example-code-snippet"). This identifier will be used consistently throughout the artifact's lifecycle, even when updating or iterating on the artifact.
|
||||
|
|
@ -191,7 +191,7 @@ Artifacts are for substantial, self-contained content that users might modify or
|
|||
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
|
||||
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
|
||||
6. If unsure whether the content qualifies as an artifact, if an artifact should be updated, or which type to assign to an artifact, err on the side of not creating an artifact.
|
||||
7. Always use triple backticks (\`\`\`) to enclose the content within the artifact, regardless of the content type.
|
||||
7. Use a backtick fence longer than any backtick fence in the artifact content. Use a 4-backtick fence by default; if the artifact content contains a 4-backtick fence, use 5 backticks, and so on.
|
||||
</artifact_instructions>
|
||||
|
||||
Here are some examples of correct usage of artifacts:
|
||||
|
|
@ -208,7 +208,7 @@ Here are some examples of correct usage of artifacts:
|
|||
Sure! Here's a simple flow chart depicting the process of making tea using Mermaid syntax:
|
||||
|
||||
:::artifact{identifier="tea-making-flowchart" type="application/vnd.mermaid" title="Flow chart: Making Tea"}
|
||||
\`\`\`mermaid
|
||||
\`\`\`\`mermaid
|
||||
graph TD
|
||||
A[Start] --> B{Water boiled?}
|
||||
B -->|Yes| C[Add tea leaves to cup]
|
||||
|
|
@ -220,7 +220,7 @@ Here are some examples of correct usage of artifacts:
|
|||
G --> H[Add milk or sugar, if desired]
|
||||
H --> I[Enjoy your tea!]
|
||||
I --> J[End]
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
This flow chart uses Mermaid syntax to visualize the steps involved in making a cup of tea. Here's a brief explanation of the process:
|
||||
|
|
@ -246,7 +246,7 @@ Here are some examples of correct usage of artifacts:
|
|||
Here's a simple React counter component:
|
||||
|
||||
:::artifact{identifier="react-counter" type="application/vnd.react" title="React Counter"}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function Counter() {
|
||||
|
|
@ -260,7 +260,7 @@ Here are some examples of correct usage of artifacts:
|
|||
</div>
|
||||
);
|
||||
}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
This component creates a simple counter with an increment button.
|
||||
|
|
@ -273,7 +273,7 @@ Here are some examples of correct usage of artifacts:
|
|||
Here's a basic HTML structure for a blog post:
|
||||
|
||||
:::artifact{identifier="blog-post-html" type="text/html" title="Blog Post HTML"}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
|
@ -300,7 +300,7 @@ Here are some examples of correct usage of artifacts:
|
|||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
This HTML structure provides a simple layout for a blog post.
|
||||
|
|
@ -343,18 +343,18 @@ Artifacts are for substantial, self-contained content that users might modify or
|
|||
1. Create the artifact using the following remark-directive markdown format:
|
||||
|
||||
:::artifact{identifier="unique-identifier" type="mime-type" title="Artifact Title"}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
Your artifact content here
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
a. Example of correct format:
|
||||
|
||||
:::artifact{identifier="example-artifact" type="text/plain" title="Example Artifact"}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
This is the content of the artifact.
|
||||
It can span multiple lines.
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
b. Common mistakes to avoid:
|
||||
|
|
@ -396,7 +396,7 @@ Artifacts are for substantial, self-contained content that users might modify or
|
|||
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
|
||||
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
|
||||
6. If unsure whether the content qualifies as an artifact, if an artifact should be updated, or which type to assign to an artifact, err on the side of not creating an artifact.
|
||||
7. NEVER use triple backticks to enclose the artifact, ONLY the content within the artifact.
|
||||
7. Use a backtick fence longer than any backtick fence in the artifact content. Use a 4-backtick fence by default; if the artifact content contains a 4-backtick fence, use 5 backticks, and so on.
|
||||
|
||||
Here are some examples of correct usage of artifacts:
|
||||
|
||||
|
|
@ -411,7 +411,7 @@ Here are some examples of correct usage of artifacts:
|
|||
Assistant: Sure! Here's a simple flow chart depicting the process of making tea using Mermaid syntax:
|
||||
|
||||
:::artifact{identifier="tea-making-flowchart" type="application/vnd.mermaid" title="Flow chart: Making Tea"}
|
||||
\`\`\`mermaid
|
||||
\`\`\`\`mermaid
|
||||
graph TD
|
||||
A[Start] --> B{Water boiled?}
|
||||
B -->|Yes| C[Add tea leaves to cup]
|
||||
|
|
@ -423,7 +423,7 @@ Here are some examples of correct usage of artifacts:
|
|||
G --> H[Add milk or sugar, if desired]
|
||||
H --> I[Enjoy your tea!]
|
||||
I --> J[End]
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
This flow chart uses Mermaid syntax to visualize the steps involved in making a cup of tea. Here's a brief explanation of the process:
|
||||
|
|
@ -450,7 +450,7 @@ Here are some examples of correct usage of artifacts:
|
|||
Assistant: Here's a simple React counter component:
|
||||
|
||||
:::artifact{identifier="react-counter" type="application/vnd.react" title="React Counter"}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function Counter() {
|
||||
|
|
@ -464,7 +464,7 @@ Here are some examples of correct usage of artifacts:
|
|||
</div>
|
||||
);
|
||||
}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
This component creates a simple counter with an increment button.
|
||||
|
|
@ -476,7 +476,7 @@ Here are some examples of correct usage of artifacts:
|
|||
Assistant: Here's a basic HTML structure for a blog post:
|
||||
|
||||
:::artifact{identifier="blog-post-html" type="text/html" title="Blog Post HTML"}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
|
@ -503,7 +503,7 @@ Here are some examples of correct usage of artifacts:
|
|||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
This HTML structure provides a simple layout for a blog post.
|
||||
|
|
|
|||
|
|
@ -136,6 +136,30 @@ ${ARTIFACT_END}`;
|
|||
expect(result[0].end).toBe(artifactText.length);
|
||||
});
|
||||
|
||||
test('should preserve markdown artifacts wrapped in longer fences with internal code blocks', () => {
|
||||
const content = `# Title
|
||||
|
||||
\`\`\`bash
|
||||
echo one
|
||||
\`\`\`
|
||||
Text between sections.
|
||||
## Section B
|
||||
\`\`\`bash
|
||||
echo two
|
||||
\`\`\``;
|
||||
const artifactText = `${ARTIFACT_START}{identifier="git-cheatsheet" type="text/markdown" title="Git Cheatsheet"}
|
||||
\`\`\`\`markdown
|
||||
${content}
|
||||
\`\`\`\`
|
||||
${ARTIFACT_END}`;
|
||||
const message = { text: `${artifactText}\ntrailer` };
|
||||
|
||||
const result = findAllArtifacts(message);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].end).toBe(artifactText.length);
|
||||
});
|
||||
|
||||
test('should preserve the first fallback close in an unclosed fence', () => {
|
||||
const firstArtifact = `${ARTIFACT_START}{identifier="first" type="text/html" title="First"}
|
||||
\`\`\`html
|
||||
|
|
@ -245,6 +269,49 @@ ${ARTIFACT_END}`;
|
|||
expect(result).toContain("console.log('inside');");
|
||||
});
|
||||
|
||||
test('should replace markdown artifacts wrapped in longer fences with internal code blocks', () => {
|
||||
const original = `# Notes
|
||||
|
||||
\`\`\`bash
|
||||
echo one
|
||||
\`\`\`
|
||||
|
||||
## More
|
||||
\`\`\`bash
|
||||
echo two
|
||||
\`\`\``;
|
||||
const updated = original.replace('## More', '## Updated');
|
||||
const artifactText = `${ARTIFACT_START}{identifier="notes" type="text/markdown" title="Notes"}
|
||||
\`\`\`\`markdown
|
||||
${original}
|
||||
\`\`\`\`
|
||||
${ARTIFACT_END}`;
|
||||
const message = { text: artifactText };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
const result = replaceArtifactContent(artifactText, artifacts[0], original, updated);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('## Updated');
|
||||
expect(result).toContain('```bash');
|
||||
expect(result).toContain('````markdown');
|
||||
});
|
||||
|
||||
test('should normalize editor trailing newlines before longer closing fences', () => {
|
||||
const original = '# Notes';
|
||||
const artifactText = `${ARTIFACT_START}{identifier="notes" type="text/markdown" title="Notes"}
|
||||
\`\`\`\`markdown
|
||||
${original}
|
||||
\`\`\`\`
|
||||
${ARTIFACT_END}`;
|
||||
const message = { text: artifactText };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
const result = replaceArtifactContent(artifactText, artifacts[0], original, `${original}\n`);
|
||||
|
||||
expect(result).toBe(artifactText);
|
||||
});
|
||||
|
||||
test('should replace unclosed artifacts with internal markers in fenced content', () => {
|
||||
const original = `before
|
||||
\`\`\`markdown
|
||||
|
|
|
|||
|
|
@ -17,11 +17,16 @@ import Markdown from '../Markdown';
|
|||
*/
|
||||
jest.mock('~/components/Artifacts/ArtifactButton', () => ({
|
||||
__esModule: true,
|
||||
default: ({ artifact }: { artifact?: { index?: number; identifier?: string } }) => (
|
||||
default: ({
|
||||
artifact,
|
||||
}: {
|
||||
artifact?: { index?: number; identifier?: string; content?: string };
|
||||
}) => (
|
||||
<div
|
||||
data-testid="art"
|
||||
data-index={String(artifact?.index)}
|
||||
data-id={String(artifact?.identifier)}
|
||||
data-content={String(artifact?.content ?? '')}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
|
@ -114,4 +119,31 @@ describe('MarkdownBlocks artifact-index parity (e2e)', () => {
|
|||
|
||||
expect(await readArtifacts()).toEqual([{ idx: '0', id: 'a' }]);
|
||||
});
|
||||
|
||||
it('preserves markdown artifact content with inner fenced code blocks', async () => {
|
||||
const markdown = [
|
||||
'# Title',
|
||||
'',
|
||||
'```bash',
|
||||
'echo one',
|
||||
'```',
|
||||
'Text between sections.',
|
||||
'## Section B',
|
||||
'```bash',
|
||||
'echo two',
|
||||
'```',
|
||||
].join('\n');
|
||||
const content = [
|
||||
':::artifact{identifier="git-cheatsheet" type="text/markdown" title="Git Cheatsheet"}',
|
||||
'````markdown',
|
||||
markdown,
|
||||
'````',
|
||||
':::',
|
||||
].join('\n');
|
||||
|
||||
render(wrap(<Markdown content={content} isLatestMessage={false} />));
|
||||
|
||||
const [artifactNode] = await screen.findAllByTestId('art');
|
||||
expect(artifactNode.getAttribute('data-content')).toBe(`${markdown}\n`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { renderHook, act } from '@testing-library/react';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import type { Artifact } from '~/common';
|
||||
|
||||
/** Mock dependencies */
|
||||
|
|
@ -176,6 +176,73 @@ describe('useArtifacts', () => {
|
|||
expect(result.current.activeTab).toBe('preview');
|
||||
});
|
||||
|
||||
it('should switch to preview when enclosed artifact uses a longer code fence', () => {
|
||||
(useRecoilValue as jest.Mock).mockReturnValue({});
|
||||
(useRecoilState as jest.Mock).mockReturnValue([null, mockSetCurrentArtifactId]);
|
||||
|
||||
const { result, rerender } = renderHook(() => useArtifacts());
|
||||
|
||||
act(() => {
|
||||
result.current.setActiveTab('code');
|
||||
});
|
||||
|
||||
expect(result.current.activeTab).toBe('code');
|
||||
|
||||
(useArtifactsContext as jest.Mock).mockReturnValue({
|
||||
...defaultContext,
|
||||
isSubmitting: true,
|
||||
latestMessageText: [
|
||||
':::artifact{title="Git" type="text/markdown"}',
|
||||
'````markdown',
|
||||
'# Title',
|
||||
'```bash',
|
||||
'echo one',
|
||||
'```',
|
||||
'````',
|
||||
':::',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
rerender();
|
||||
|
||||
expect(result.current.activeTab).toBe('preview');
|
||||
});
|
||||
|
||||
it('should not switch to preview when an inner marker appears before the longer fence closes', () => {
|
||||
(useRecoilValue as jest.Mock).mockReturnValue({});
|
||||
(useRecoilState as jest.Mock).mockReturnValue([null, mockSetCurrentArtifactId]);
|
||||
|
||||
const { result, rerender } = renderHook(() => useArtifacts());
|
||||
|
||||
act(() => {
|
||||
result.current.setActiveTab('code');
|
||||
});
|
||||
|
||||
expect(result.current.activeTab).toBe('code');
|
||||
|
||||
(useArtifactsContext as jest.Mock).mockReturnValue({
|
||||
...defaultContext,
|
||||
isSubmitting: true,
|
||||
latestMessageText: [
|
||||
':::artifact{title="Git" type="text/markdown"}',
|
||||
'````markdown',
|
||||
'# Title',
|
||||
'```bash',
|
||||
'echo one',
|
||||
'```',
|
||||
':::',
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
rerender();
|
||||
|
||||
expect(result.current.activeTab).toBe('code');
|
||||
expect(logger.log).not.toHaveBeenCalledWith(
|
||||
'artifacts',
|
||||
expect.stringContaining('Enclosed artifact'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not switch to preview if artifact is not enclosed', () => {
|
||||
const artifact = createArtifact({
|
||||
content: 'const App = () => <div>Test</div>',
|
||||
|
|
|
|||
|
|
@ -1,11 +1,140 @@
|
|||
import { useMemo, useState, useEffect, useRef } from 'react';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil';
|
||||
import { useArtifactsContext } from '~/Providers';
|
||||
import { isCodeOnlyArtifact } from '~/utils/artifacts';
|
||||
import { useArtifactsContext } from '~/Providers';
|
||||
import { logger } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
type ArtifactFence = {
|
||||
marker: string;
|
||||
length: number;
|
||||
contentStart: number;
|
||||
};
|
||||
|
||||
const getLineEnd = (text: string, start: number): number => {
|
||||
const index = text.indexOf('\n', start);
|
||||
return index === -1 ? text.length : index;
|
||||
};
|
||||
|
||||
const getNextLineStart = (text: string, lineEnd: number): number =>
|
||||
lineEnd >= text.length ? text.length : lineEnd + 1;
|
||||
|
||||
const getFirstContentLineStart = (text: string, start: number): number => {
|
||||
let currentIndex = start;
|
||||
|
||||
while (currentIndex < text.length) {
|
||||
const lineEnd = getLineEnd(text, currentIndex);
|
||||
const line = text.slice(currentIndex, lineEnd);
|
||||
if (line.trim().length > 0) {
|
||||
return currentIndex;
|
||||
}
|
||||
currentIndex = getNextLineStart(text, lineEnd);
|
||||
}
|
||||
|
||||
return text.length;
|
||||
};
|
||||
|
||||
const getArtifactFence = (text: string, lineStart: number): ArtifactFence | null => {
|
||||
const lineEnd = getLineEnd(text, lineStart);
|
||||
const line = text.slice(lineStart, lineEnd);
|
||||
const match = line.trimStart().match(/^(`{3,}|~{3,})/);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
marker: match[1][0],
|
||||
length: match[1].length,
|
||||
contentStart: getNextLineStart(text, lineEnd),
|
||||
};
|
||||
};
|
||||
|
||||
const isClosingArtifactFence = (line: string, openingFence: ArtifactFence): boolean => {
|
||||
const closePattern = new RegExp(`^\\${openingFence.marker}{${openingFence.length},}\\s*$`);
|
||||
return closePattern.test(line.trim());
|
||||
};
|
||||
|
||||
const isArtifactCloseLine = (line: string): boolean => {
|
||||
const trimmed = line.trimStart();
|
||||
return trimmed.startsWith(':::') && !trimmed.startsWith(':::artifact');
|
||||
};
|
||||
|
||||
const hasArtifactCloseAfter = (text: string, start: number): boolean => {
|
||||
const closeStart = getFirstContentLineStart(text, start);
|
||||
if (closeStart >= text.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const closeLineEnd = getLineEnd(text, closeStart);
|
||||
return isArtifactCloseLine(text.slice(closeStart, closeLineEnd));
|
||||
};
|
||||
|
||||
const hasUnfencedArtifactClose = (text: string, start: number): boolean => {
|
||||
let currentIndex = start;
|
||||
let codeFence: ArtifactFence | null = null;
|
||||
|
||||
while (currentIndex < text.length) {
|
||||
const lineEnd = getLineEnd(text, currentIndex);
|
||||
const line = text.slice(currentIndex, lineEnd);
|
||||
const fence = getArtifactFence(text, currentIndex);
|
||||
|
||||
if (isArtifactCloseLine(line) && !codeFence) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fence && !codeFence) {
|
||||
codeFence = fence;
|
||||
} else if (codeFence && isClosingArtifactFence(line, codeFence)) {
|
||||
codeFence = null;
|
||||
}
|
||||
|
||||
currentIndex = getNextLineStart(text, lineEnd);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const hasEnclosedArtifact = (messageText: string): boolean => {
|
||||
const text = messageText.trim();
|
||||
const artifactPattern = /:::artifact(?:\{[^}]*\})?/g;
|
||||
let artifactMatch = artifactPattern.exec(text);
|
||||
|
||||
while (artifactMatch) {
|
||||
const openingLineEnd = getLineEnd(text, artifactMatch.index);
|
||||
const contentStart = getFirstContentLineStart(text, getNextLineStart(text, openingLineEnd));
|
||||
const openingFence = getArtifactFence(text, contentStart);
|
||||
|
||||
if (!openingFence) {
|
||||
if (hasUnfencedArtifactClose(text, contentStart)) {
|
||||
return true;
|
||||
}
|
||||
artifactMatch = artifactPattern.exec(text);
|
||||
continue;
|
||||
}
|
||||
|
||||
let currentIndex = openingFence.contentStart;
|
||||
while (currentIndex < text.length) {
|
||||
const lineEnd = getLineEnd(text, currentIndex);
|
||||
const line = text.slice(currentIndex, lineEnd);
|
||||
|
||||
if (isClosingArtifactFence(line, openingFence)) {
|
||||
if (hasArtifactCloseAfter(text, getNextLineStart(text, lineEnd))) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
currentIndex = getNextLineStart(text, lineEnd);
|
||||
}
|
||||
|
||||
artifactMatch = artifactPattern.exec(text);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export default function useArtifacts() {
|
||||
const [activeTab, setActiveTab] = useState('preview');
|
||||
const { isSubmitting, latestMessageId, latestMessageText, conversationId } =
|
||||
|
|
@ -136,12 +265,7 @@ export default function useArtifacts() {
|
|||
return;
|
||||
}
|
||||
|
||||
const hasEnclosedArtifact =
|
||||
/:::artifact(?:\{[^}]*\})?(?:\s|\n)*(?:```[\s\S]*?```(?:\s|\n)*)?:::/m.test(
|
||||
latestMessageText.trim(),
|
||||
);
|
||||
|
||||
if (hasEnclosedArtifact) {
|
||||
if (hasEnclosedArtifact(latestMessageText)) {
|
||||
logger.log('artifacts', 'Enclosed artifact detected during generation, switching to preview');
|
||||
setActiveTab('preview');
|
||||
hasEnclosedArtifactRef.current = true;
|
||||
|
|
|
|||
|
|
@ -213,6 +213,9 @@ const replaceRange = (
|
|||
return originalText.substring(0, start) + updated + separator + endText;
|
||||
};
|
||||
|
||||
const normalizeBeforeClosingArtifactFence = (text: string): string =>
|
||||
text.replace(/\n+(?=(?:`{3,}|~{3,})\s*\n\s*:::)/g, '\n');
|
||||
|
||||
export const findAllArtifacts = (message: ArtifactMessage): ArtifactBoundary[] => {
|
||||
const artifacts: ArtifactBoundary[] = [];
|
||||
|
||||
|
|
@ -287,10 +290,7 @@ export const replaceArtifactContent = (
|
|||
}
|
||||
|
||||
const absoluteIndex = artifact.start + searchStart + relativeIndex;
|
||||
return replaceRange(
|
||||
originalText,
|
||||
absoluteIndex,
|
||||
absoluteIndex + originalTrimmed.length,
|
||||
updated,
|
||||
).replace(/\n+(?=```\n:::)/g, '\n');
|
||||
return normalizeBeforeClosingArtifactFence(
|
||||
replaceRange(originalText, absoluteIndex, absoluteIndex + originalTrimmed.length, updated),
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ Artifacts are for substantial, self-contained content that users might modify or
|
|||
1. Create the artifact using the following format:
|
||||
|
||||
:::artifact{identifier="unique-identifier" type="mime-type" title="Artifact Title"}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
Your artifact content here
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
2. Assign an identifier to the \`identifier\` attribute. For updates, reuse the prior identifier. For new artifacts, the identifier should be descriptive and relevant to the content, using kebab-case (e.g., "example-code-snippet"). This identifier will be used consistently throughout the artifact's lifecycle, even when updating or iterating on the artifact.
|
||||
|
|
@ -77,7 +77,7 @@ Artifacts are for substantial, self-contained content that users might modify or
|
|||
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
|
||||
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
|
||||
6. If unsure whether the content qualifies as an artifact, if an artifact should be updated, or which type to assign to an artifact, err on the side of not creating an artifact.
|
||||
7. Always use triple backticks (\`\`\`) to enclose the content within the artifact, regardless of the content type.
|
||||
7. Use a backtick fence longer than any backtick fence in the artifact content. Use a 4-backtick fence by default; if the artifact content contains a 4-backtick fence, use 5 backticks, and so on.
|
||||
</artifact_instructions>
|
||||
|
||||
Here are some examples of correct usage of artifacts:
|
||||
|
|
@ -94,7 +94,7 @@ Here are some examples of correct usage of artifacts:
|
|||
Sure! Here's a simple flow chart depicting the process of making tea using Mermaid syntax:
|
||||
|
||||
:::artifact{identifier="tea-making-flowchart" type="application/vnd.mermaid" title="Flow chart: Making Tea"}
|
||||
\`\`\`mermaid
|
||||
\`\`\`\`mermaid
|
||||
graph TD
|
||||
A[Start] --> B{Water boiled?}
|
||||
B -->|Yes| C[Add tea leaves to cup]
|
||||
|
|
@ -106,7 +106,7 @@ Here are some examples of correct usage of artifacts:
|
|||
G --> H[Add milk or sugar, if desired]
|
||||
H --> I[Enjoy your tea!]
|
||||
I --> J[End]
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
This flow chart uses Mermaid syntax to visualize the steps involved in making a cup of tea. Here's a brief explanation of the process:
|
||||
|
|
@ -132,7 +132,7 @@ Here are some examples of correct usage of artifacts:
|
|||
Here's a simple React counter component:
|
||||
|
||||
:::artifact{identifier="react-counter" type="application/vnd.react" title="React Counter"}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function Counter() {
|
||||
|
|
@ -146,7 +146,7 @@ Here are some examples of correct usage of artifacts:
|
|||
</div>
|
||||
);
|
||||
}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
This component creates a simple counter with an increment button.
|
||||
|
|
@ -159,7 +159,7 @@ Here are some examples of correct usage of artifacts:
|
|||
Here's a basic HTML structure for a blog post:
|
||||
|
||||
:::artifact{identifier="blog-post-html" type="text/html" title="Blog Post HTML"}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
|
@ -186,7 +186,7 @@ Here are some examples of correct usage of artifacts:
|
|||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
This HTML structure provides a simple layout for a blog post.
|
||||
|
|
@ -229,18 +229,18 @@ Artifacts are for substantial, self-contained content that users might modify or
|
|||
1. Create the artifact using the following remark-directive markdown format:
|
||||
|
||||
:::artifact{identifier="unique-identifier" type="mime-type" title="Artifact Title"}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
Your artifact content here
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
a. Example of correct format:
|
||||
|
||||
:::artifact{identifier="example-artifact" type="text/plain" title="Example Artifact"}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
This is the content of the artifact.
|
||||
It can span multiple lines.
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
b. Common mistakes to avoid:
|
||||
|
|
@ -282,7 +282,7 @@ Artifacts are for substantial, self-contained content that users might modify or
|
|||
- If you are unable to follow the above requirements for any reason, don't use artifacts and use regular code blocks instead, which will not attempt to render the component.
|
||||
5. Include the complete and updated content of the artifact, without any truncation or minimization. Don't use "// rest of the code remains the same...".
|
||||
6. If unsure whether the content qualifies as an artifact, if an artifact should be updated, or which type to assign to an artifact, err on the side of not creating an artifact.
|
||||
7. NEVER use triple backticks to enclose the artifact, ONLY the content within the artifact.
|
||||
7. Use a backtick fence longer than any backtick fence in the artifact content. Use a 4-backtick fence by default; if the artifact content contains a 4-backtick fence, use 5 backticks, and so on.
|
||||
|
||||
Here are some examples of correct usage of artifacts:
|
||||
|
||||
|
|
@ -297,7 +297,7 @@ Here are some examples of correct usage of artifacts:
|
|||
Assistant: Sure! Here's a simple flow chart depicting the process of making tea using Mermaid syntax:
|
||||
|
||||
:::artifact{identifier="tea-making-flowchart" type="application/vnd.mermaid" title="Flow chart: Making Tea"}
|
||||
\`\`\`mermaid
|
||||
\`\`\`\`mermaid
|
||||
graph TD
|
||||
A[Start] --> B{Water boiled?}
|
||||
B -->|Yes| C[Add tea leaves to cup]
|
||||
|
|
@ -309,7 +309,7 @@ Here are some examples of correct usage of artifacts:
|
|||
G --> H[Add milk or sugar, if desired]
|
||||
H --> I[Enjoy your tea!]
|
||||
I --> J[End]
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
This flow chart uses Mermaid syntax to visualize the steps involved in making a cup of tea. Here's a brief explanation of the process:
|
||||
|
|
@ -336,7 +336,7 @@ Here are some examples of correct usage of artifacts:
|
|||
Assistant: Here's a simple React counter component:
|
||||
|
||||
:::artifact{identifier="react-counter" type="application/vnd.react" title="React Counter"}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function Counter() {
|
||||
|
|
@ -350,7 +350,7 @@ Here are some examples of correct usage of artifacts:
|
|||
</div>
|
||||
);
|
||||
}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
This component creates a simple counter with an increment button.
|
||||
|
|
@ -362,7 +362,7 @@ Here are some examples of correct usage of artifacts:
|
|||
Assistant: Here's a basic HTML structure for a blog post:
|
||||
|
||||
:::artifact{identifier="blog-post-html" type="text/html" title="Blog Post HTML"}
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
|
@ -389,7 +389,7 @@ Here are some examples of correct usage of artifacts:
|
|||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
\`\`\`
|
||||
\`\`\`\`
|
||||
:::
|
||||
|
||||
This HTML structure provides a simple layout for a blog post.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue