import {
Children,
isValidElement,
useCallback,
useRef,
useState,
type CSSProperties,
type JSX,
type ReactNode
} from "react";
import { Check, Copy } from "lucide-react";
import { translate } from "../i18n/index";
import { cn } from "../app/renderer/lib/utils";
import type { MarkdownDomProps } from "./AgentMessageMarkdown";
import { AgentMessageMermaid } from "./AgentMessageMermaid";
export function MarkdownCode({
node: _node,
children,
className,
...props
}: MarkdownDomProps<"code">): JSX.Element {
"use memo";
return (
{children}
);
}
const MARKDOWN_ORDERED_LIST_STYLE: CSSProperties = {
listStylePosition: "outside",
margin: "12px 0 8px",
paddingInlineStart: 34,
paddingInlineEnd: 16
};
const MARKDOWN_UNORDERED_LIST_STYLE: CSSProperties = {
margin: "12px 0 8px",
paddingInlineStart: 0
};
const MARKDOWN_LIST_ITEM_STYLE: CSSProperties = {
margin: "4px 0"
};
export function MarkdownUnorderedList({
node: _node,
className,
style,
...props
}: MarkdownDomProps<"ul">): JSX.Element {
"use memo";
return (
);
}
export function MarkdownOrderedList({
node: _node,
style,
...props
}: MarkdownDomProps<"ol">): JSX.Element {
"use memo";
return (
);
}
export function MarkdownListItem({
node: _node,
style,
...props
}: MarkdownDomProps<"li">): JSX.Element {
"use memo";
return ;
}
export function MarkdownParagraph({
node: _node,
inline,
...props
}: MarkdownDomProps<"p"> & {
inline: boolean;
}): JSX.Element {
"use memo";
if (inline) {
return ;
}
return ;
}
export function textFromReactNode(node: ReactNode): string {
if (typeof node === "string" || typeof node === "number") {
return String(node);
}
if (Array.isArray(node)) {
return node.map(textFromReactNode).join("");
}
return "";
}
export function MarkdownPre({
children,
mermaidEnabled = true,
node: _node,
streaming = false,
...props
}: MarkdownDomProps<"pre"> & {
mermaidEnabled?: boolean;
streaming?: boolean;
}): JSX.Element {
"use memo";
const preRef = useRef(null);
const [copied, setCopied] = useState(false);
const copyResetRef = useRef | null>(null);
const mermaidSource = mermaidEnabled
? resolveMermaidCodeBlockSource(children)
: null;
const streamingMermaidSource =
mermaidEnabled && streaming
? resolveStreamingMermaidCodeBlockSource(children)
: null;
const effectiveMermaidSource = mermaidSource ?? streamingMermaidSource;
const handleCopy = useCallback(() => {
const text = preRef.current?.textContent?.trim();
if (!text) {
return;
}
void navigator.clipboard?.writeText(text).then(() => {
setCopied(true);
if (copyResetRef.current) {
clearTimeout(copyResetRef.current);
}
// timing: revert the "copied" affordance after the confirmation window
copyResetRef.current = setTimeout(() => setCopied(false), 1500);
});
}, []);
const codeBlock = (
{children}
);
if (effectiveMermaidSource === null) {
return codeBlock;
}
return (
);
}
export function resolveMermaidCodeBlockSource(
children: ReactNode
): string | null {
return resolveMermaidCodeBlockSourceWithLanguage(children, (language) => {
return language === "mermaid";
});
}
function resolveStreamingMermaidCodeBlockSource(
children: ReactNode
): string | null {
return resolveMermaidCodeBlockSourceWithLanguage(children, (language) => {
return (
language.length > 0 &&
language.length < "mermaid".length &&
"mermaid".startsWith(language)
);
});
}
function resolveMermaidCodeBlockSourceWithLanguage(
children: ReactNode,
acceptsLanguage: (language: string) => boolean
): string | null {
const childElements = Children.toArray(children);
if (childElements.length !== 1 || !isValidElement(childElements[0])) {
return null;
}
const codeElement = childElements[0] as {
props: {
children?: ReactNode;
className?: unknown;
};
};
const className =
typeof codeElement.props.className === "string"
? codeElement.props.className
: "";
const languageClass = className
.split(/\s+/)
.find((candidate) => candidate.toLowerCase().startsWith("language-"));
const language = languageClass?.slice("language-".length).toLowerCase() ?? "";
if (!acceptsLanguage(language)) {
return null;
}
return textFromReactNode(codeElement.props.children).replace(/\n$/, "");
}