import React from 'react';
import { LuArrowRight, LuSparkles } from 'react-icons/lu';
/** Matches **bold**, `code`, and [label](href) inline tokens. */
const INLINE_TOKEN = /(\*\*[^*]+\*\*|`[^`]+`|\[[^\]]+\]\([^)]+\))/g;
/**
* The model sometimes prefixes a link/button label with the directive key
* itself ("primary_action: View articles"); strip it so the chip reads cleanly.
*/
const ACTION_LABEL_PREFIX =
/^\s*(?:primary|secondary)?[ _-]?action(?:[ _-]?(?:label|route|type))?\s*:\s*/i;
/**
* Callback invoked when the merchant taps an internal-link chip. The panel
* decides whether the href is a real plugin route (navigate) or an action the
* copilot should act on (send the label as a follow-up message).
*
* @param href - The internal href the model wrote (always starts with ``/``).
* @param label - The chip's cleaned label text.
* @return Nothing.
*/
type OnLinkHandler = (href: string, label: string) => void;
/**
* Renders a single inline token (bold, code, or link) or plain text.
*
* @param token - The raw token text.
* @param index - Stable key index within the line.
* @param onLink - Handler for internal-link chip taps.
* @return The rendered node.
*/
function renderToken(
token: string,
index: number,
onLink: OnLinkHandler
): React.ReactNode {
if (token.startsWith('**') && token.endsWith('**')) {
return (
{token.slice(2, -2)}
);
}
if (token.startsWith('`') && token.endsWith('`')) {
return (
{token.slice(1, -1)}
);
}
const linkMatch: RegExpMatchArray | null = token.match(
/^\[([^\]]+)\]\(([^)]+)\)$/
);
if (linkMatch) {
const label: string = linkMatch[1].replace(ACTION_LABEL_PREFIX, '');
const href: string = linkMatch[2];
const isInternal: boolean = href.startsWith('/');
if (isInternal) {
// Internal references render as a tappable chip. The model often writes
// these as fake routes (e.g. "/confirm-schedule") for actions it should
// perform rather than pages it can reach, so we don't navigate blindly -
// ``onLink`` routes real plugin routes and sends everything else back to
// the copilot as a follow-up message.
return (
);
}
return (
{label}
);
}
return {token};
}
/**
* Splits a line into inline tokens and renders each.
*
* @param line - One line of markdown text.
* @param onLink - Handler for internal-link chip taps.
* @return Rendered inline nodes.
*/
function renderLine(line: string, onLink: OnLinkHandler): React.ReactNode[] {
return line
.split(INLINE_TOKEN)
.filter((segment: string) => segment.length > 0)
.map((segment: string, segmentIndex: number) =>
renderToken(segment, segmentIndex, onLink)
);
}
/**
* Lightweight markdown renderer for copilot messages: paragraphs, simple
* bullet/numbered lines, bold, inline code, and internal/external links.
*
* @param props - Component props.
* @param props.content - The markdown content to render.
* @param props.onLink - Handler invoked when an internal-link chip is tapped.
* @return The rendered message body.
*/
export function CopilotMarkdown({
content,
onLink,
}: {
content: string;
onLink: OnLinkHandler;
}): React.ReactElement {
const lines: string[] = content.split('\n');
return (
{renderLine(line, onLink)}
; })}