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 (
{lines.map((line: string, lineIndex: number) => { const trimmed: string = line.trim(); if (!trimmed) return null; const isListItem: boolean = /^(-|\*|\d+\.)\s+/.test(trimmed); if (isListItem) { const text: string = trimmed.replace(/^(-|\*|\d+\.)\s+/, ''); return (
{renderLine(text, onLink)}
); } return

{renderLine(line, onLink)}

; })}
); }