Pass model and provider information through chat components to include in quick action messages. This fixes the issue of defaulting to anthropic model.
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');
|
|
};
|