mirror of
https://github.com/stackblitz/bolt.new
synced 2024-11-27 14:32:46 +00:00
feat: sanitize user messages (#42)
This commit is contained in:
parent
8f74cc61ec
commit
d364a6f774
@ -8,7 +8,7 @@ interface AssistantMessageProps {
|
|||||||
export const AssistantMessage = memo(({ content }: AssistantMessageProps) => {
|
export const AssistantMessage = memo(({ content }: AssistantMessageProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden w-full">
|
<div className="overflow-hidden w-full">
|
||||||
<Markdown>{content}</Markdown>
|
<Markdown html>{content}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -58,10 +58,14 @@ $code-font-size: 13px;
|
|||||||
color: #6a737d;
|
color: #6a737d;
|
||||||
}
|
}
|
||||||
|
|
||||||
p:not(:last-of-type) {
|
p {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
|
&:not(:last-of-type) {
|
||||||
margin-block-start: 0;
|
margin-block-start: 0;
|
||||||
margin-block-end: 16px;
|
margin-block-end: 16px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--bolt-elements-messages-linkColor);
|
color: var(--bolt-elements-messages-linkColor);
|
||||||
|
@ -2,7 +2,7 @@ import { memo, useMemo } from 'react';
|
|||||||
import ReactMarkdown, { type Components } from 'react-markdown';
|
import ReactMarkdown, { type Components } from 'react-markdown';
|
||||||
import type { BundledLanguage } from 'shiki';
|
import type { BundledLanguage } from 'shiki';
|
||||||
import { createScopedLogger } from '~/utils/logger';
|
import { createScopedLogger } from '~/utils/logger';
|
||||||
import { rehypePlugins, remarkPlugins } from '~/utils/markdown';
|
import { rehypePlugins, remarkPlugins, allowedHTMLElements } from '~/utils/markdown';
|
||||||
import { Artifact } from './Artifact';
|
import { Artifact } from './Artifact';
|
||||||
import { CodeBlock } from './CodeBlock';
|
import { CodeBlock } from './CodeBlock';
|
||||||
|
|
||||||
@ -12,12 +12,14 @@ const logger = createScopedLogger('MarkdownComponent');
|
|||||||
|
|
||||||
interface MarkdownProps {
|
interface MarkdownProps {
|
||||||
children: string;
|
children: string;
|
||||||
|
html?: boolean;
|
||||||
|
limitedMarkdown?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Markdown = memo(({ children }: MarkdownProps) => {
|
export const Markdown = memo(({ children, html = false, limitedMarkdown = false }: MarkdownProps) => {
|
||||||
logger.trace('Render');
|
logger.trace('Render');
|
||||||
|
|
||||||
const components = useMemo<Components>(() => {
|
const components = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
div: ({ className, children, node, ...props }) => {
|
div: ({ className, children, node, ...props }) => {
|
||||||
if (className?.includes('__boltArtifact__')) {
|
if (className?.includes('__boltArtifact__')) {
|
||||||
@ -55,15 +57,16 @@ export const Markdown = memo(({ children }: MarkdownProps) => {
|
|||||||
|
|
||||||
return <pre {...rest}>{children}</pre>;
|
return <pre {...rest}>{children}</pre>;
|
||||||
},
|
},
|
||||||
};
|
} satisfies Components;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
allowedElements={allowedHTMLElements}
|
||||||
className={styles.MarkdownContent}
|
className={styles.MarkdownContent}
|
||||||
components={components}
|
components={components}
|
||||||
remarkPlugins={remarkPlugins}
|
remarkPlugins={remarkPlugins(limitedMarkdown)}
|
||||||
rehypePlugins={rehypePlugins}
|
rehypePlugins={rehypePlugins(html)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
|
@ -8,7 +8,7 @@ interface UserMessageProps {
|
|||||||
export function UserMessage({ content }: UserMessageProps) {
|
export function UserMessage({ content }: UserMessageProps) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden pt-[4px]">
|
<div className="overflow-hidden pt-[4px]">
|
||||||
<Markdown>{sanitizeUserMessage(content)}</Markdown>
|
<Markdown limitedMarkdown>{sanitizeUserMessage(content)}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { MODIFICATIONS_TAG_NAME, WORK_DIR } from '~/utils/constants';
|
import { MODIFICATIONS_TAG_NAME, WORK_DIR } from '~/utils/constants';
|
||||||
|
import { allowedHTMLElements } from '~/utils/markdown';
|
||||||
import { stripIndents } from '~/utils/stripIndent';
|
import { stripIndents } from '~/utils/stripIndent';
|
||||||
|
|
||||||
export const getSystemPrompt = (cwd: string = WORK_DIR) => `
|
export const getSystemPrompt = (cwd: string = WORK_DIR) => `
|
||||||
@ -35,6 +36,10 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
|
|||||||
Use 2 spaces for code indentation
|
Use 2 spaces for code indentation
|
||||||
</code_formatting_info>
|
</code_formatting_info>
|
||||||
|
|
||||||
|
<message_formatting_info>
|
||||||
|
You can make the output pretty by using only the following available HTML elements: ${allowedHTMLElements.map((tagName) => `<${tagName}>`).join(', ')}
|
||||||
|
</message_formatting_info>
|
||||||
|
|
||||||
<diff_spec>
|
<diff_spec>
|
||||||
For user-made file modifications, a \`<${MODIFICATIONS_TAG_NAME}>\` section will appear at the start of the user message. It will contain either \`<diff>\` or \`<file>\` elements for each modified file:
|
For user-made file modifications, a \`<${MODIFICATIONS_TAG_NAME}>\` section will appear at the start of the user message. It will contain either \`<diff>\` or \`<file>\` elements for each modified file:
|
||||||
|
|
||||||
|
@ -272,7 +272,7 @@ export class StreamingMessageParser {
|
|||||||
const createArtifactElement: ElementFactory = (props) => {
|
const createArtifactElement: ElementFactory = (props) => {
|
||||||
const elementProps = [
|
const elementProps = [
|
||||||
'class="__boltArtifact__"',
|
'class="__boltArtifact__"',
|
||||||
Object.entries(props).map(([key, value]) => {
|
...Object.entries(props).map(([key, value]) => {
|
||||||
return `data-${camelToDashCase(key)}=${JSON.stringify(value)}`;
|
return `data-${camelToDashCase(key)}=${JSON.stringify(value)}`;
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
@ -1,6 +1,116 @@
|
|||||||
import rehypeRaw from 'rehype-raw';
|
import rehypeRaw from 'rehype-raw';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import type { PluggableList } from 'unified';
|
import type { PluggableList, Plugin } from 'unified';
|
||||||
|
import rehypeSanitize, { defaultSchema, type Options as RehypeSanitizeOptions } from 'rehype-sanitize';
|
||||||
|
import { SKIP, visit } from 'unist-util-visit';
|
||||||
|
import type { UnistNode, UnistParent } from 'node_modules/unist-util-visit/lib';
|
||||||
|
|
||||||
export const remarkPlugins = [remarkGfm] satisfies PluggableList;
|
export const allowedHTMLElements = [
|
||||||
export const rehypePlugins = [rehypeRaw] satisfies PluggableList;
|
'a',
|
||||||
|
'b',
|
||||||
|
'blockquote',
|
||||||
|
'br',
|
||||||
|
'code',
|
||||||
|
'dd',
|
||||||
|
'del',
|
||||||
|
'details',
|
||||||
|
'div',
|
||||||
|
'dl',
|
||||||
|
'dt',
|
||||||
|
'em',
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
'h4',
|
||||||
|
'h5',
|
||||||
|
'h6',
|
||||||
|
'hr',
|
||||||
|
'i',
|
||||||
|
'ins',
|
||||||
|
'kbd',
|
||||||
|
'li',
|
||||||
|
'ol',
|
||||||
|
'p',
|
||||||
|
'pre',
|
||||||
|
'q',
|
||||||
|
'rp',
|
||||||
|
'rt',
|
||||||
|
'ruby',
|
||||||
|
's',
|
||||||
|
'samp',
|
||||||
|
'source',
|
||||||
|
'span',
|
||||||
|
'strike',
|
||||||
|
'strong',
|
||||||
|
'sub',
|
||||||
|
'summary',
|
||||||
|
'sup',
|
||||||
|
'table',
|
||||||
|
'tbody',
|
||||||
|
'td',
|
||||||
|
'tfoot',
|
||||||
|
'th',
|
||||||
|
'thead',
|
||||||
|
'tr',
|
||||||
|
'ul',
|
||||||
|
'var',
|
||||||
|
];
|
||||||
|
|
||||||
|
const rehypeSanitizeOptions: RehypeSanitizeOptions = {
|
||||||
|
...defaultSchema,
|
||||||
|
tagNames: allowedHTMLElements,
|
||||||
|
attributes: {
|
||||||
|
...defaultSchema.attributes,
|
||||||
|
div: [...(defaultSchema.attributes?.div ?? []), 'data*', ['className', '__boltArtifact__']],
|
||||||
|
},
|
||||||
|
strip: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function remarkPlugins(limitedMarkdown: boolean) {
|
||||||
|
const plugins: PluggableList = [remarkGfm];
|
||||||
|
|
||||||
|
if (limitedMarkdown) {
|
||||||
|
plugins.unshift(limitedMarkdownPlugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rehypePlugins(html: boolean) {
|
||||||
|
const plugins: PluggableList = [];
|
||||||
|
|
||||||
|
if (html) {
|
||||||
|
plugins.push(rehypeRaw, [rehypeSanitize, rehypeSanitizeOptions]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
const limitedMarkdownPlugin: Plugin = () => {
|
||||||
|
return (tree, file) => {
|
||||||
|
const contents = file.toString();
|
||||||
|
|
||||||
|
visit(tree, (node: UnistNode, index, parent: UnistParent) => {
|
||||||
|
if (
|
||||||
|
index == null ||
|
||||||
|
['paragraph', 'text', 'inlineCode', 'code', 'strong', 'emphasis'].includes(node.type) ||
|
||||||
|
!node.position
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = contents.slice(node.position.start.offset, node.position.end.offset);
|
||||||
|
|
||||||
|
if (node.type === 'heading') {
|
||||||
|
value = `\n${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.children[index] = {
|
||||||
|
type: 'text',
|
||||||
|
value,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
return [SKIP, index] as const;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -64,9 +64,11 @@
|
|||||||
"react-resizable-panels": "^2.0.20",
|
"react-resizable-panels": "^2.0.20",
|
||||||
"react-toastify": "^10.0.5",
|
"react-toastify": "^10.0.5",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
|
"rehype-sanitize": "^6.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remix-utils": "^7.6.0",
|
"remix-utils": "^7.6.0",
|
||||||
"shiki": "^1.9.1"
|
"shiki": "^1.9.1",
|
||||||
|
"unist-util-visit": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20240620.0",
|
"@cloudflare/workers-types": "^4.20240620.0",
|
||||||
|
@ -179,6 +179,9 @@ importers:
|
|||||||
rehype-raw:
|
rehype-raw:
|
||||||
specifier: ^7.0.0
|
specifier: ^7.0.0
|
||||||
version: 7.0.0
|
version: 7.0.0
|
||||||
|
rehype-sanitize:
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
remark-gfm:
|
remark-gfm:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
@ -188,6 +191,9 @@ importers:
|
|||||||
shiki:
|
shiki:
|
||||||
specifier: ^1.9.1
|
specifier: ^1.9.1
|
||||||
version: 1.9.1
|
version: 1.9.1
|
||||||
|
unist-util-visit:
|
||||||
|
specifier: ^5.0.0
|
||||||
|
version: 5.0.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@cloudflare/workers-types':
|
'@cloudflare/workers-types':
|
||||||
specifier: ^4.20240620.0
|
specifier: ^4.20240620.0
|
||||||
@ -3214,6 +3220,9 @@ packages:
|
|||||||
hast-util-raw@9.0.4:
|
hast-util-raw@9.0.4:
|
||||||
resolution: {integrity: sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==}
|
resolution: {integrity: sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==}
|
||||||
|
|
||||||
|
hast-util-sanitize@5.0.1:
|
||||||
|
resolution: {integrity: sha512-IGrgWLuip4O2nq5CugXy4GI2V8kx4sFVy5Hd4vF7AR2gxS0N9s7nEAVUyeMtZKZvzrxVsHt73XdTsno1tClIkQ==}
|
||||||
|
|
||||||
hast-util-to-estree@2.3.3:
|
hast-util-to-estree@2.3.3:
|
||||||
resolution: {integrity: sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ==}
|
resolution: {integrity: sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ==}
|
||||||
|
|
||||||
@ -4586,6 +4595,9 @@ packages:
|
|||||||
rehype-raw@7.0.0:
|
rehype-raw@7.0.0:
|
||||||
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
|
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
|
||||||
|
|
||||||
|
rehype-sanitize@6.0.0:
|
||||||
|
resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==}
|
||||||
|
|
||||||
remark-frontmatter@4.0.1:
|
remark-frontmatter@4.0.1:
|
||||||
resolution: {integrity: sha512-38fJrB0KnmD3E33a5jZC/5+gGAC2WKNiPw1/fdXJvijBlhA7RCsvJklrYJakS0HedninvaCYW8lQGf9C918GfA==}
|
resolution: {integrity: sha512-38fJrB0KnmD3E33a5jZC/5+gGAC2WKNiPw1/fdXJvijBlhA7RCsvJklrYJakS0HedninvaCYW8lQGf9C918GfA==}
|
||||||
|
|
||||||
@ -8935,6 +8947,12 @@ snapshots:
|
|||||||
web-namespaces: 2.0.1
|
web-namespaces: 2.0.1
|
||||||
zwitch: 2.0.4
|
zwitch: 2.0.4
|
||||||
|
|
||||||
|
hast-util-sanitize@5.0.1:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
'@ungap/structured-clone': 1.2.0
|
||||||
|
unist-util-position: 5.0.0
|
||||||
|
|
||||||
hast-util-to-estree@2.3.3:
|
hast-util-to-estree@2.3.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.5
|
'@types/estree': 1.0.5
|
||||||
@ -10699,6 +10717,11 @@ snapshots:
|
|||||||
hast-util-raw: 9.0.4
|
hast-util-raw: 9.0.4
|
||||||
vfile: 6.0.1
|
vfile: 6.0.1
|
||||||
|
|
||||||
|
rehype-sanitize@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
hast-util-sanitize: 5.0.1
|
||||||
|
|
||||||
remark-frontmatter@4.0.1:
|
remark-frontmatter@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mdast': 3.0.15
|
'@types/mdast': 3.0.15
|
||||||
|
Loading…
Reference in New Issue
Block a user