mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-25 17:56:12 +00:00
- Implement design scheme system with palette, typography, and feature customization - Add color scheme dialog for user customization - Update chat UI components to use design scheme values - Improve header actions with consolidated deploy and export buttons - Adjust layout spacing and styling across multiple components (chat, workbench etc...) - Add model and provider info to chat messages - Refactor workbench and sidebar components for better responsiveness
202 lines
7.5 KiB
TypeScript
202 lines
7.5 KiB
TypeScript
import { memo, useMemo } from 'react';
|
|
import ReactMarkdown, { type Components } from 'react-markdown';
|
|
import type { BundledLanguage } from 'shiki';
|
|
import { createScopedLogger } from '~/utils/logger';
|
|
import { rehypePlugins, remarkPlugins, allowedHTMLElements } from '~/utils/markdown';
|
|
import { Artifact, openArtifactInWorkbench } from './Artifact';
|
|
import { CodeBlock } from './CodeBlock';
|
|
import type { Message } from 'ai';
|
|
import styles from './Markdown.module.scss';
|
|
import ThoughtBox from './ThoughtBox';
|
|
import type { ProviderInfo } from '~/types/model';
|
|
|
|
const logger = createScopedLogger('MarkdownComponent');
|
|
|
|
interface MarkdownProps {
|
|
children: string;
|
|
html?: boolean;
|
|
limitedMarkdown?: boolean;
|
|
append?: (message: Message) => void;
|
|
chatMode?: 'discuss' | 'build';
|
|
setChatMode?: (mode: 'discuss' | 'build') => void;
|
|
model?: string;
|
|
provider?: ProviderInfo;
|
|
}
|
|
|
|
export const Markdown = memo(
|
|
({ children, html = false, limitedMarkdown = false, append, setChatMode, model, provider }: MarkdownProps) => {
|
|
logger.trace('Render');
|
|
|
|
const components = useMemo(() => {
|
|
return {
|
|
div: ({ className, children, node, ...props }) => {
|
|
const dataProps = node?.properties as Record<string, unknown>;
|
|
|
|
if (className?.includes('__boltArtifact__')) {
|
|
const messageId = node?.properties.dataMessageId as string;
|
|
|
|
if (!messageId) {
|
|
logger.error(`Invalid message id ${messageId}`);
|
|
}
|
|
|
|
return <Artifact messageId={messageId} />;
|
|
}
|
|
|
|
if (className?.includes('__boltThought__')) {
|
|
return <ThoughtBox title="Thought process">{children}</ThoughtBox>;
|
|
}
|
|
|
|
if (className?.includes('__boltQuickAction__') || dataProps?.dataBoltQuickAction) {
|
|
return <div className="flex items-center gap-2 flex-wrap mt-3.5">{children}</div>;
|
|
}
|
|
|
|
return (
|
|
<div className={className} {...props}>
|
|
{children}
|
|
</div>
|
|
);
|
|
},
|
|
pre: (props) => {
|
|
const { children, node, ...rest } = props;
|
|
|
|
const [firstChild] = node?.children ?? [];
|
|
|
|
if (
|
|
firstChild &&
|
|
firstChild.type === 'element' &&
|
|
firstChild.tagName === 'code' &&
|
|
firstChild.children[0].type === 'text'
|
|
) {
|
|
const { className, ...rest } = firstChild.properties;
|
|
const [, language = 'plaintext'] = /language-(\w+)/.exec(String(className) || '') ?? [];
|
|
|
|
return <CodeBlock code={firstChild.children[0].value} language={language as BundledLanguage} {...rest} />;
|
|
}
|
|
|
|
return <pre {...rest}>{children}</pre>;
|
|
},
|
|
button: ({ node, children, ...props }) => {
|
|
const dataProps = node?.properties as Record<string, unknown>;
|
|
|
|
if (
|
|
dataProps?.class?.toString().includes('__boltQuickAction__') ||
|
|
dataProps?.dataBoltQuickAction === 'true'
|
|
) {
|
|
const type = dataProps['data-type'] || dataProps.dataType;
|
|
const message = dataProps['data-message'] || dataProps.dataMessage;
|
|
const path = dataProps['data-path'] || dataProps.dataPath;
|
|
const href = dataProps['data-href'] || dataProps.dataHref;
|
|
|
|
const iconClassMap: Record<string, string> = {
|
|
file: 'i-ph:file',
|
|
message: 'i-ph:chats',
|
|
implement: 'i-ph:code',
|
|
link: 'i-ph:link',
|
|
};
|
|
|
|
const safeType = typeof type === 'string' ? type : '';
|
|
const iconClass = iconClassMap[safeType] ?? 'i-ph:question';
|
|
|
|
return (
|
|
<button
|
|
className="rounded-md justify-center px-3 py-1.5 text-xs bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent opacity-90 hover:opacity-100 flex items-center gap-2 cursor-pointer"
|
|
data-type={type}
|
|
data-message={message}
|
|
data-path={path}
|
|
data-href={href}
|
|
onClick={() => {
|
|
if (type === 'file') {
|
|
openArtifactInWorkbench(path);
|
|
} else if (type === 'message' && append) {
|
|
append({
|
|
id: `quick-action-message-${Date.now()}`,
|
|
content: `[Model: ${model}]\n\n[Provider: ${provider?.name}]\n\n${message}`,
|
|
role: 'user',
|
|
});
|
|
console.log('Message appended:', message);
|
|
} else if (type === 'implement' && append && setChatMode) {
|
|
setChatMode('build');
|
|
append({
|
|
id: `quick-action-implement-${Date.now()}`,
|
|
content: `[Model: ${model}]\n\n[Provider: ${provider?.name}]\n\n${message}`,
|
|
role: 'user',
|
|
});
|
|
} else if (type === 'link' && typeof href === 'string') {
|
|
try {
|
|
const url = new URL(href, window.location.origin);
|
|
window.open(url.toString(), '_blank', 'noopener,noreferrer');
|
|
} catch (error) {
|
|
console.error('Invalid URL:', href, error);
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
<div className={`text-lg ${iconClass}`} />
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return <button {...props}>{children}</button>;
|
|
},
|
|
} satisfies Components;
|
|
}, []);
|
|
|
|
return (
|
|
<ReactMarkdown
|
|
allowedElements={allowedHTMLElements}
|
|
className={styles.MarkdownContent}
|
|
components={components}
|
|
remarkPlugins={remarkPlugins(limitedMarkdown)}
|
|
rehypePlugins={rehypePlugins(html)}
|
|
>
|
|
{stripCodeFenceFromArtifact(children)}
|
|
</ReactMarkdown>
|
|
);
|
|
},
|
|
);
|
|
|
|
/**
|
|
* Removes code fence markers (```) surrounding an artifact element while preserving the artifact content.
|
|
* This is necessary because artifacts should not be wrapped in code blocks when rendered for rendering action list.
|
|
*
|
|
* @param content - The markdown content to process
|
|
* @returns The processed content with code fence markers removed around artifacts
|
|
*
|
|
* @example
|
|
* // Removes code fences around artifact
|
|
* const input = "```xml\n<div class='__boltArtifact__'></div>\n```";
|
|
* stripCodeFenceFromArtifact(input);
|
|
* // Returns: "\n<div class='__boltArtifact__'></div>\n"
|
|
*
|
|
* @remarks
|
|
* - Only removes code fences that directly wrap an artifact (marked with __boltArtifact__ class)
|
|
* - Handles code fences with optional language specifications (e.g. ```xml, ```typescript)
|
|
* - Preserves original content if no artifact is found
|
|
* - Safely handles edge cases like empty input or artifacts at start/end of content
|
|
*/
|
|
export const stripCodeFenceFromArtifact = (content: string) => {
|
|
if (!content || !content.includes('__boltArtifact__')) {
|
|
return content;
|
|
}
|
|
|
|
const lines = content.split('\n');
|
|
const artifactLineIndex = lines.findIndex((line) => line.includes('__boltArtifact__'));
|
|
|
|
// Return original content if artifact line not found
|
|
if (artifactLineIndex === -1) {
|
|
return content;
|
|
}
|
|
|
|
// Check previous line for code fence
|
|
if (artifactLineIndex > 0 && lines[artifactLineIndex - 1]?.trim().match(/^```\w*$/)) {
|
|
lines[artifactLineIndex - 1] = '';
|
|
}
|
|
|
|
if (artifactLineIndex < lines.length - 1 && lines[artifactLineIndex + 1]?.trim().match(/^```$/)) {
|
|
lines[artifactLineIndex + 1] = '';
|
|
}
|
|
|
|
return lines.join('\n');
|
|
};
|