80 lines
2.3 KiB
TypeScript
80 lines
2.3 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo } from "react";
|
|
import type { AnchorHTMLAttributes } from "react";
|
|
|
|
import {
|
|
MessageResponse,
|
|
type MessageResponseProps,
|
|
} from "@/components/ai-elements/message";
|
|
import { streamdownPlugins } from "@/core/streamdown";
|
|
import { desensitize } from "@/lib/desensitize";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
import { CitationLink } from "../citations/citation-link";
|
|
|
|
function isExternalUrl(href: string | undefined): boolean {
|
|
return !!href && /^https?:\/\//.test(href);
|
|
}
|
|
|
|
export type MarkdownContentProps = {
|
|
content: string;
|
|
isLoading: boolean;
|
|
rehypePlugins: MessageResponseProps["rehypePlugins"];
|
|
className?: string;
|
|
remarkPlugins?: MessageResponseProps["remarkPlugins"];
|
|
components?: MessageResponseProps["components"];
|
|
};
|
|
|
|
/** Renders markdown content. */
|
|
export function MarkdownContent({
|
|
content,
|
|
rehypePlugins,
|
|
className,
|
|
remarkPlugins = streamdownPlugins.remarkPlugins,
|
|
components: componentsFromProps,
|
|
}: MarkdownContentProps) {
|
|
const components = useMemo(() => {
|
|
return {
|
|
a: (props: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
|
if (typeof props.children === "string") {
|
|
const match = /^citation:(.+)$/.exec(props.children);
|
|
if (match) {
|
|
const [, text] = match;
|
|
return <CitationLink {...props}>{text}</CitationLink>;
|
|
}
|
|
}
|
|
const { className, target, rel, ...rest } = props;
|
|
const external = isExternalUrl(props.href);
|
|
return (
|
|
<a
|
|
{...rest}
|
|
className={cn(
|
|
"text-primary decoration-primary/30 hover:decoration-primary/60 underline underline-offset-2 transition-colors",
|
|
className,
|
|
)}
|
|
target={target ?? (external ? "_blank" : undefined)}
|
|
rel={rel ?? (external ? "noopener noreferrer" : undefined)}
|
|
/>
|
|
);
|
|
},
|
|
...componentsFromProps,
|
|
};
|
|
}, [componentsFromProps]);
|
|
|
|
if (!content) return null;
|
|
|
|
const displayContent = desensitize(content);
|
|
|
|
return (
|
|
<MessageResponse
|
|
className={className}
|
|
remarkPlugins={remarkPlugins}
|
|
rehypePlugins={rehypePlugins}
|
|
components={components}
|
|
>
|
|
{displayContent}
|
|
</MessageResponse>
|
|
);
|
|
}
|