"use client"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { cn } from "@/lib/utils"; import Link from "next/link"; import type { Nodes } from "hast"; import { toJsxRuntime } from "hast-util-to-jsx-runtime"; import type { Components } from "hast-util-to-jsx-runtime"; import type { ComponentProps, HTMLAttributes, MouseEvent } from "react"; import { Fragment, memo, useCallback, useMemo, useState } from "react"; import { jsx, jsxs } from "react/jsx-runtime"; import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; import remarkGfm from "remark-gfm"; import remarkParse from "remark-parse"; import remarkRehype from "remark-rehype"; import remend from "remend"; import { unified } from "unified"; // Raw HTML never reaches the tree: remark-rehype drops it (allowDangerousHtml // is off by default) and rehype-sanitize is the second line of defence, mainly // for its href protocol allow-list — that is what stops `javascript:` URLs. // Relative hrefs carry no protocol, so storefront links pass through untouched. const schema = { ...defaultSchema, attributes: { ...defaultSchema.attributes, // GFM task lists render as disabled checkboxes. The default schema allows // `type` and `disabled` but not `checked`, so every box would read unticked. input: [...(defaultSchema.attributes?.input ?? []), "checked"], }, }; const processor = unified() .use(remarkParse) .use(remarkGfm) .use(remarkRehype) .use(rehypeSanitize, schema); // `text-only` leaves a half-streamed `[label](htt` as plain text. remend's // default instead emits a `streamdown:incomplete-link` placeholder href, which // the sanitizer would strip anyway. const REMEND_OPTIONS = { linkMode: "text-only" } as const; // remend only ever runs on text that is still arriving. On finished text it can // do damage: it reads `*$89*` as an unclosed italic (the `$` throws off its // closing-delimiter scan) and appends a stray `*`, which then parses as an empty // bullet. Prices in italics are ordinary storefront copy, so settled messages // render verbatim and only the in-flight one gets repaired. const repair = (markdown: string, isStreaming: boolean) => isStreaming ? remend(markdown, REMEND_OPTIONS) : markdown; export interface LinkSafetyConfig { enabled: boolean; /** Return true for links that may open without a confirmation step. */ onLinkCheck?: (url: string) => boolean; } type AnchorProps = ComponentProps<"a"> & { linkSafety?: LinkSafetyConfig; onUntrusted: (url: string) => void; }; const LINK_CLASS = "font-medium underline underline-offset-4 hover:text-foreground"; // Route-relative hrefs are the storefront links the assistant emits. Deciding // this from the string alone keeps the server and client passes identical; // `linkSafety.onLinkCheck` does the origin-aware check later, at click time. const isRouteHref = (href: string) => /^[/#?]/.test(href); const MarkdownLink = ({ href, linkSafety, onUntrusted, ...props }: AnchorProps) => { // Runs on click rather than on render so it can read `window.location`, // which is unavailable during the server pass. const handleClick = useCallback( (event: MouseEvent) => { if (!href || !linkSafety?.enabled || !linkSafety.onLinkCheck) return; if (linkSafety.onLinkCheck(href)) return; event.preventDefault(); onUntrusted(href); }, [href, linkSafety, onUntrusted] ); // In-app destinations navigate client-side in the same tab; sending a shopper // to a product page in a new tab would strand the conversation behind it. if (href && isRouteHref(href)) { return ; } return ( ); }; // Fenced code renders as plain preformatted text — no tokenizer, no themes. const buildComponents = ( linkSafety: LinkSafetyConfig | undefined, onUntrusted: (url: string) => void ): Partial => ({ a: (props: ComponentProps<"a">) => ( ), blockquote: (props: ComponentProps<"blockquote">) => (
), code: ({ className, ...props }: ComponentProps<"code">) => { // Only fenced blocks carry a language class, and those already sit inside a //
, so they must not get the inline pill treatment.
    const isBlock =
      typeof className === "string" && className.includes("language-");

    return (
      
    );
  },
  em: (props: ComponentProps<"em">) => ,
  h1: (props: ComponentProps<"h1">) => (
    

), h2: (props: ComponentProps<"h2">) => (

), h3: (props: ComponentProps<"h3">) => (

), h4: (props: ComponentProps<"h4">) => (

), h5: (props: ComponentProps<"h5">) => (

), h6: (props: ComponentProps<"h6">) => (
), hr: (props: ComponentProps<"hr">) => (
), // GFM task-list boxes are display only; readOnly silences React's warning // about a `checked` input with no change handler. input: (props: ComponentProps<"input">) => ( ), img: ({ alt, ...props }: ComponentProps<"img">) => ( // biome-ignore lint/nursery/noImgElement: model output, not a known asset {alt ), li: (props: ComponentProps<"li">) =>
  • , ol: (props: ComponentProps<"ol">) => (
      ), p: (props: ComponentProps<"p">) => (

      ), pre: (props: ComponentProps<"pre">) => (

        ),
        strong: (props: ComponentProps<"strong">) => (
          
        ),
        table: (props: ComponentProps<"table">) => (
          
      ), td: (props: ComponentProps<"td">) => (
      ), th: (props: ComponentProps<"th">) => ( ), ul: (props: ComponentProps<"ul">) => (
        ), }); export type MarkdownProps = Omit, "children"> & { children: string; /** True while tokens are still arriving; enables incomplete-syntax repair. */ isAnimating?: boolean; linkSafety?: LinkSafetyConfig; }; export const Markdown = memo( ({ children, className, isAnimating, linkSafety, ...props }: MarkdownProps) => { const [pending, setPending] = useState(null); const handleUntrusted = useCallback((url: string) => setPending(url), []); const content = useMemo(() => { // While streaming, close syntax the model has not finished emitting so a // partial `**bold` renders as bold rather than as literal asterisks. const source = repair(children ?? "", isAnimating === true); const tree = processor.runSync(processor.parse(source)) as Nodes; return toJsxRuntime(tree, { components: buildComponents(linkSafety, handleUntrusted), Fragment, jsx, jsxs, }); }, [children, isAnimating, linkSafety, handleUntrusted]); return (
        *:first-child]:mt-0 [&>*:last-child]:mb-0", className )} {...props} > {content} !open && setPending(null)} open={pending !== null} > Leave this site? This link points somewhere outside the store. Continue only if you trust it.

        {pending}

        ); }, (prev, next) => prev.children === next.children && prev.isAnimating === next.isAnimating ); Markdown.displayName = "Markdown";