logoassistant-ui

Markdown

Allow the assistant to display rich text using markdown.

Markdown support is already included by default in the Thread component.

Enabling markdown support

Add markdown-text

npx shadcn@latest add @assistant-ui/markdown-text

Main Component

npm install @assistant-ui/react-markdown remark-gfm
yarn add @assistant-ui/react-markdown remark-gfm
pnpm add @assistant-ui/react-markdown remark-gfm
bun add @assistant-ui/react-markdown remark-gfm
xpm add @assistant-ui/react-markdown remark-gfm
"use client";import "@assistant-ui/react-markdown/styles/dot.css";import {  type CodeHeaderProps,  MarkdownTextPrimitive,  unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,  useIsMarkdownCodeBlock,} from "@assistant-ui/react-markdown";import remarkGfm from "remark-gfm";import { type FC, memo, useState } from "react";import { CheckIcon, CopyIcon } from "lucide-react";import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";import { cn } from "@/lib/utils";const MarkdownTextImpl = () => {  return (    <MarkdownTextPrimitive      remarkPlugins={[remarkGfm]}      className="aui-md"      components={defaultComponents}    />  );};export const MarkdownText = memo(MarkdownTextImpl);const CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => {  const { isCopied, copyToClipboard } = useCopyToClipboard();  const onCopy = () => {    if (!code || isCopied) return;    copyToClipboard(code);  };  return (    <div className="aui-code-header-root mt-4 flex items-center justify-between gap-4 rounded-t-lg bg-muted-foreground/15 px-4 py-2 font-semibold text-foreground text-sm dark:bg-muted-foreground/20">      <span className="aui-code-header-language lowercase [&>span]:text-xs">        {language}      </span>      <TooltipIconButton tooltip="Copy" onClick={onCopy}>        {!isCopied && <CopyIcon />}        {isCopied && <CheckIcon />}      </TooltipIconButton>    </div>  );};const useCopyToClipboard = ({  copiedDuration = 3000,}: {  copiedDuration?: number;} = {}) => {  const [isCopied, setIsCopied] = useState<boolean>(false);  const copyToClipboard = (value: string) => {    if (!value) return;    navigator.clipboard.writeText(value).then(() => {      setIsCopied(true);      setTimeout(() => setIsCopied(false), copiedDuration);    });  };  return { isCopied, copyToClipboard };};const defaultComponents = memoizeMarkdownComponents({  h1: ({ className, ...props }) => (    <h1      className={cn(        "aui-md-h1 mb-8 scroll-m-20 font-extrabold text-4xl tracking-tight last:mb-0",        className,      )}      {...props}    />  ),  h2: ({ className, ...props }) => (    <h2      className={cn(        "aui-md-h2 mt-8 mb-4 scroll-m-20 font-semibold text-3xl tracking-tight first:mt-0 last:mb-0",        className,      )}      {...props}    />  ),  h3: ({ className, ...props }) => (    <h3      className={cn(        "aui-md-h3 mt-6 mb-4 scroll-m-20 font-semibold text-2xl tracking-tight first:mt-0 last:mb-0",        className,      )}      {...props}    />  ),  h4: ({ className, ...props }) => (    <h4      className={cn(        "aui-md-h4 mt-6 mb-4 scroll-m-20 font-semibold text-xl tracking-tight first:mt-0 last:mb-0",        className,      )}      {...props}    />  ),  h5: ({ className, ...props }) => (    <h5      className={cn(        "aui-md-h5 my-4 font-semibold text-lg first:mt-0 last:mb-0",        className,      )}      {...props}    />  ),  h6: ({ className, ...props }) => (    <h6      className={cn(        "aui-md-h6 my-4 font-semibold first:mt-0 last:mb-0",        className,      )}      {...props}    />  ),  p: ({ className, ...props }) => (    <p      className={cn(        "aui-md-p mt-5 mb-5 leading-7 first:mt-0 last:mb-0",        className,      )}      {...props}    />  ),  a: ({ className, ...props }) => (    <a      className={cn(        "aui-md-a font-medium text-primary underline underline-offset-4",        className,      )}      {...props}    />  ),  blockquote: ({ className, ...props }) => (    <blockquote      className={cn("aui-md-blockquote border-l-2 pl-6 italic", className)}      {...props}    />  ),  ul: ({ className, ...props }) => (    <ul      className={cn("aui-md-ul my-5 ml-6 list-disc [&>li]:mt-2", className)}      {...props}    />  ),  ol: ({ className, ...props }) => (    <ol      className={cn("aui-md-ol my-5 ml-6 list-decimal [&>li]:mt-2", className)}      {...props}    />  ),  hr: ({ className, ...props }) => (    <hr className={cn("aui-md-hr my-5 border-b", className)} {...props} />  ),  table: ({ className, ...props }) => (    <table      className={cn(        "aui-md-table my-5 w-full border-separate border-spacing-0 overflow-y-auto",        className,      )}      {...props}    />  ),  th: ({ className, ...props }) => (    <th      className={cn(        "aui-md-th bg-muted px-4 py-2 text-left font-bold first:rounded-tl-lg last:rounded-tr-lg [[align=center]]:text-center [[align=right]]:text-right",        className,      )}      {...props}    />  ),  td: ({ className, ...props }) => (    <td      className={cn(        "aui-md-td border-b border-l px-4 py-2 text-left last:border-r [[align=center]]:text-center [[align=right]]:text-right",        className,      )}      {...props}    />  ),  tr: ({ className, ...props }) => (    <tr      className={cn(        "aui-md-tr m-0 border-b p-0 first:border-t [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg",        className,      )}      {...props}    />  ),  sup: ({ className, ...props }) => (    <sup      className={cn("aui-md-sup [&>a]:text-xs [&>a]:no-underline", className)}      {...props}    />  ),  pre: ({ className, ...props }) => (    <pre      className={cn(        "aui-md-pre overflow-x-auto rounded-t-none! rounded-b-lg bg-black p-4 text-white",        className,      )}      {...props}    />  ),  code: function Code({ className, ...props }) {    const isCodeBlock = useIsMarkdownCodeBlock();    return (      <code        className={cn(          !isCodeBlock &&            "aui-md-inline-code rounded border bg-muted font-semibold",          className,        )}        {...props}      />    );  },  CodeHeader,});

assistant-ui dependencies

"use client";import { ComponentPropsWithRef, forwardRef } from "react";import { Slottable } from "@radix-ui/react-slot";import {  Tooltip,  TooltipContent,  TooltipTrigger,} from "@/components/ui/tooltip";import { Button } from "@/components/ui/button";import { cn } from "@/lib/utils";export type TooltipIconButtonProps = ComponentPropsWithRef<typeof Button> & {  tooltip: string;  side?: "top" | "bottom" | "left" | "right";};export const TooltipIconButton = forwardRef<  HTMLButtonElement,  TooltipIconButtonProps>(({ children, tooltip, side = "bottom", className, ...rest }, ref) => {  return (    <Tooltip>      <TooltipTrigger asChild>        <Button          variant="ghost"          size="icon"          {...rest}          className={cn("aui-button-icon size-6 p-1", className)}          ref={ref}        >          <Slottable>{children}</Slottable>          <span className="aui-sr-only sr-only">{tooltip}</span>        </Button>      </TooltipTrigger>      <TooltipContent side={side}>{tooltip}</TooltipContent>    </Tooltip>  );});TooltipIconButton.displayName = "TooltipIconButton";

shadcn/ui dependencies

npm install @radix-ui/react-slot @radix-ui/react-tooltip
yarn add @radix-ui/react-slot @radix-ui/react-tooltip
pnpm add @radix-ui/react-slot @radix-ui/react-tooltip
bun add @radix-ui/react-slot @radix-ui/react-tooltip
xpm add @radix-ui/react-slot @radix-ui/react-tooltip
"use client";import * as React from "react";import * as TooltipPrimitive from "@radix-ui/react-tooltip";import { cn } from "@/lib/utils";function TooltipProvider({  delayDuration = 0,  ...props}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {  return (    <TooltipPrimitive.Provider      data-slot="tooltip-provider"      delayDuration={delayDuration}      {...props}    />  );}function Tooltip({  ...props}: React.ComponentProps<typeof TooltipPrimitive.Root>) {  return (    <TooltipProvider>      <TooltipPrimitive.Root data-slot="tooltip" {...props} />    </TooltipProvider>  );}function TooltipTrigger({  ...props}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;}function TooltipContent({  className,  sideOffset = 0,  children,  ...props}: React.ComponentProps<typeof TooltipPrimitive.Content>) {  return (    <TooltipPrimitive.Portal>      <TooltipPrimitive.Content        data-slot="tooltip-content"        sideOffset={sideOffset}        className={cn(          "fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in text-balance rounded-md bg-foreground px-3 py-1.5 text-background text-xs data-[state=closed]:animate-out",          className,        )}        {...props}      >        {children}        <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />      </TooltipPrimitive.Content>    </TooltipPrimitive.Portal>  );}export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
import * as React from "react";import { Slot } from "@radix-ui/react-slot";import { cva, type VariantProps } from "class-variance-authority";import { cn } from "@/lib/utils";const buttonVariants = cva(  "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm no-underline outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",  {    variants: {      variant: {        default:          "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",        destructive:          "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",        outline:          "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",        secondary:          "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",        ghost:          "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",        link: "text-primary underline-offset-4 hover:underline",      },      size: {        default: "h-9 px-4 py-2 has-[>svg]:px-3",        sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",        lg: "h-10 rounded-md px-6 has-[>svg]:px-4",        icon: "size-9",      },    },    defaultVariants: {      variant: "default",      size: "default",    },  },);function Button({  className,  variant,  size,  asChild = false,  ...props}: React.ComponentProps<"button"> &  VariantProps<typeof buttonVariants> & {    asChild?: boolean;  }) {  const Comp = asChild ? Slot : "button";  return (    <Comp      data-slot="button"      className={cn(buttonVariants({ variant, size, className }))}      {...props}    />  );}export { Button, buttonVariants };

This adds a /components/assistant-ui/markdown-text.tsx file to your project, which you can adjust as needed.

Use it in your application

Pass the MarkdownText component to the MessagePrimitive.Parts component

/components/assistant-ui/thread.tsx
import {  } from "@/components/assistant-ui/markdown-text";

const :  = () => {
  return (
    <. ="...">
      < ="...">
        <. ={{ :  }} />
      </>
      < />

      < ="..." />
    </.>
  );
};

Syntax highlighting

Syntax Highlighting is not included by default, see Syntax Highlighting to learn how to add it.

On this page

Edit on GitHub