diff --git a/app/components/assistant/message-body.tsx b/app/components/assistant/message-body.tsx index 5003978..03aa7c0 100644 --- a/app/components/assistant/message-body.tsx +++ b/app/components/assistant/message-body.tsx @@ -1,31 +1,120 @@ -// Renders an assistant message: markdown for prose, plus pills for any -// command-bus action blocks or native tool calls attached to the message. -// Tool-role messages (results from a function call) render as a JSON card. +// Renders an assistant message: GFM markdown for prose, custom ```card``` +// blocks rendered as rich UI (status pills, tenant cards, KPIs), pills for +// command-bus action blocks, and tool-result cards (role: "tool"). -import { useMemo } from "react" +import { type ReactNode, useMemo } from "react" import ReactMarkdown from "react-markdown" +import remarkGfm from "remark-gfm" import { Sparkles, Wrench } from "lucide-react" import { extractActionBlocks } from "@crema/action-bus" import { stripToolCallTags, type ToolCall } from "@crema/llm-ui" const ACTION_BLOCK_RE = /```action\s*\n[\s\S]*?```/g +const CARD_BLOCK_RE = /```card\s*\n([\s\S]*?)```/g export type MessageBodyProps = { content: string - /** When set, render as a tool-result card. */ isToolResult?: boolean - /** Native tool calls attached to this assistant message, if any. */ toolCalls?: ToolCall[] } +type CardSpec = + | { kind: "pill"; status: string; label?: string } + | { kind: "stat"; label: string; value: string | number; tone?: string } + | { kind: "callout"; title?: string; tone?: "info" | "warning" | "danger" | "success"; body?: string } + | { kind: string; [k: string]: unknown } + +function parseCardBlocks(content: string): { blocks: CardSpec[]; stripped: string } { + const blocks: CardSpec[] = [] + CARD_BLOCK_RE.lastIndex = 0 + const stripped = content.replace(CARD_BLOCK_RE, (_, body: string) => { + try { + const parsed = JSON.parse(body.trim()) as CardSpec + if (parsed && typeof parsed === "object" && typeof parsed.kind === "string") { + blocks.push(parsed) + return "" // strip from prose + } + } catch { + // malformed — leave the original block in the prose so the user can see + // what the model tried to emit. + return _ + } + return _ + }) + return { blocks, stripped } +} + +function renderCard(spec: CardSpec): ReactNode { + switch (spec.kind) { + case "pill": { + const s = spec as { kind: "pill"; status: string; label?: string } + const tone = + s.status === "active" + ? "border-emerald-500/40 bg-emerald-500/15 text-emerald-700 dark:text-emerald-300" + : s.status === "suspended" + ? "border-amber-500/40 bg-amber-500/15 text-amber-700 dark:text-amber-300" + : s.status === "deactivated" + ? "border-rose-500/40 bg-rose-500/15 text-rose-700 dark:text-rose-300" + : "border-border bg-muted text-muted-foreground" + return ( + + {s.label ?? s.status} + + ) + } + case "stat": { + const s = spec as { kind: "stat"; label: string; value: string | number } + return ( + + {s.label} + {s.value} + + ) + } + case "callout": { + const s = spec as { + kind: "callout" + title?: string + tone?: "info" | "warning" | "danger" | "success" + body?: string + } + const tone = s.tone ?? "info" + const palette: Record = { + info: "border-sky-500/40 bg-sky-500/10", + warning: "border-amber-500/40 bg-amber-500/10", + danger: "border-rose-500/40 bg-rose-500/10", + success: "border-emerald-500/40 bg-emerald-500/10", + } + return ( +
+ {s.title &&
{s.title}
} + {s.body &&
{s.body}
} +
+ ) + } + default: + return ( +
+          {JSON.stringify(spec, null, 2)}
+        
+ ) + } +} + export function MessageBody({ content, isToolResult, toolCalls }: MessageBodyProps) { - const { prose, actionCount } = useMemo(() => { + const { prose, actionCount, cardBlocks } = useMemo(() => { const blocks = extractActionBlocks(content) - const cleaned = stripToolCallTags(content).replace(ACTION_BLOCK_RE, "").trim() + const cleaned = stripToolCallTags(content) + .replace(ACTION_BLOCK_RE, "") + .trim() + const { blocks: cardBlocks, stripped } = parseCardBlocks(cleaned) return { - prose: cleaned, + prose: stripped.trim(), actionCount: blocks.length, + cardBlocks, } }, [content]) @@ -47,6 +136,7 @@ export function MessageBody({ content, isToolResult, toolCalls }: MessageBodyPro
{prose && (

{children}

, code: ({ children, className }) => { @@ -68,15 +158,53 @@ export function MessageBody({ content, isToolResult, toolCalls }: MessageBodyPro ol: ({ children }) =>
    {children}
, li: ({ children }) =>
  • {children}
  • , a: ({ children, href }) => ( - + {children} ), + table: ({ children }) => ( +
    + {children}
    +
    + ), + thead: ({ children }) => ( + {children} + ), + th: ({ children }) => ( + {children} + ), + td: ({ children }) => {children}, + input: ({ checked, type, ...rest }) => + type === "checkbox" ? ( + + ) : ( + + ), }} > {prose}
    )} + {cardBlocks.length > 0 && ( +
    + {cardBlocks.map((spec, i) => ( + + {renderCard(spec)} + + ))} +
    + )} {actionCount > 0 && ( = { + active: { + bg: "bg-emerald-500/15 border-emerald-500/40", + fg: "text-emerald-700 dark:text-emerald-300", + icon: , + }, + suspended: { + bg: "bg-amber-500/15 border-amber-500/40", + fg: "text-amber-700 dark:text-amber-300", + icon: , + }, + deactivated: { + bg: "bg-rose-500/15 border-rose-500/40", + fg: "text-rose-700 dark:text-rose-300", + icon: , + }, +} + +function StatusPill({ status }: { status: Status }) { + const s = statusStyle[status] ?? { + bg: "bg-muted border-border", + fg: "text-muted-foreground", + icon: null, + } + return ( + + {s.icon} + {status} + + ) +} + +interface TenantRow { + id?: string + slug: string + name: string + status: string + plan?: string | null + inserted_at?: string +} + +function TenantsTable({ rows }: { rows: TenantRow[] }) { + if (rows.length === 0) { + return
    No tenants.
    + } + return ( +
    + + + + + + + + + + + {rows.map((t) => ( + + + + + + + ))} + +
    TenantSlugStatusPlan
    {t.name} + {t.slug} + + + {t.plan ?? "—"}
    +
    + ) +} + +function TenantCard({ t }: { t: TenantRow }) { + return ( +
    +
    + + {t.name} + + {t.slug} + + + + +
    +
    +
    Plan
    +
    {t.plan ?? "—"}
    + {t.inserted_at && ( + <> +
    Created
    +
    {new Date(t.inserted_at).toLocaleDateString()}
    + + )} +
    +
    + ) +} + +interface PlatformStats { + tenants_total: number + tenants_by_status: Record + tenants_by_plan: Record +} + +function PlatformStatsBlock({ stats }: { stats: PlatformStats }) { + return ( +
    + } /> + {Object.entries(stats.tenants_by_status).map(([status, n]) => ( + } compact /> + ))} +
    + ) +} + +function KpiTile({ + label, + value, + icon, + compact, +}: { + label: string + value: number | string + icon?: ReactNode + compact?: boolean +}) { + return ( +
    +
    + {!compact && icon} + {label} +
    +
    + {compact && icon} + {value} +
    +
    + ) +} + +interface AuditEntry { + actor_type: string + actor_id?: string | null + action: string + target?: string | null + inserted_at: string +} + +function AuditLogList({ entries }: { entries: AuditEntry[] }) { + if (entries.length === 0) { + return
    No audit entries.
    + } + return ( +
      + {entries.map((e, i) => ( +
    • + +
      +
      + + {e.actor_type} + + {e.action} + {e.target && ( + + {e.target} + + )} +
      +
      + {new Date(e.inserted_at).toLocaleString()} +
      +
      +
    • + ))} +
    + ) +} + +interface UserRow { + id: string + email: string + name?: string | null + roles?: string[] + verified?: boolean | null + inserted_at?: string +} + +function UsersList({ users }: { users: UserRow[] }) { + if (users.length === 0) { + return
    No users.
    + } + return ( +
      + {users.map((u) => ( +
    • + +
      +
      + {u.name || u.email} + {u.verified === false && ( + + unverified + + )} +
      + {u.name && ( +
      {u.email}
      + )} +
      +
      + {(u.roles ?? []).map((r) => ( + + {r} + + ))} +
      +
    • + ))} +
    + ) +} + +/** + * Dispatch by tool name. Returns null when no rich renderer applies — the + * caller falls back to a JSON dump (already part of ToolCallCard). + */ +export function renderToolResult( + toolName: string, + result: unknown, +): ReactNode | null { + if (result == null) return null + switch (toolName) { + case "list_tenants": + if (Array.isArray(result)) return + return null + case "get_tenant": + if (typeof result === "object" && (result as TenantRow).slug) { + return + } + return null + case "get_platform_stats": + if (typeof result === "object" && result && "tenants_total" in result) { + return + } + return null + case "list_audit_log": + if (Array.isArray(result)) return + return null + case "list_users": + if (Array.isArray(result)) return + return null + case "suspend_tenant": + case "activate_tenant": + if (typeof result === "object" && (result as TenantRow).slug) { + return ( +
    +
    + + {toolName === "suspend_tenant" ? "Suspended" : "Activated"} +
    + +
    + ) + } + return null + default: + return null + } +} diff --git a/app/lib/arcadia-knowledge.ts b/app/lib/arcadia-knowledge.ts index d2bad56..a4a35c3 100644 --- a/app/lib/arcadia-knowledge.ts +++ b/app/lib/arcadia-knowledge.ts @@ -32,4 +32,12 @@ Things to keep in mind when assisting: - Quotas / rate cards / billing config errors usually surface as 402/403 from /api/v1 endpoints — diagnose by checking the tenant's billing-config and api-metering quotas. - The reference Phoenix app lives at \`reference/arcadia-app/\` in the workspace; its OpenAPI spec is at /api/openapi (sync via \`node ../lib-arcadia-client/scripts/sync-spec.mjs\`). -When the user asks something that maps to a tool, call it. When they ask about a concept, explain it from this primer in plain language. Write tools (suspend_tenant, activate_tenant) prompt the operator with an inline confirm card before they actually run — you do not need to ask in prose first; just call the tool and the user will see the confirmation UI. If the user denies a write, do not retry it; ask what they'd like to do differently.` +When the user asks something that maps to a tool, call it. When they ask about a concept, explain it from this primer in plain language. Write tools (suspend_tenant, activate_tenant) prompt the operator with an inline confirm card before they actually run — you do not need to ask in prose first; just call the tool and the user will see the confirmation UI. If the user denies a write, do not retry it; ask what they'd like to do differently. + +Rich output (optional, use when it helps): +- You may use full GitHub-Flavoured Markdown — tables, task lists, code fences, etc. +- For inline UI accents, you may emit fenced \`\`\`card\`\`\` blocks containing one JSON object. The client renders them as small components and strips the block from your prose. Schemas: + - Status pill: \`{"kind":"pill","status":"active"|"suspended"|"deactivated","label"?:"text"}\` + - Stat tile: \`{"kind":"stat","label":"Tenants","value":42}\` + - Callout: \`{"kind":"callout","tone":"info"|"warning"|"danger"|"success","title"?:"…","body"?:"…"}\` + Tool results already render as rich tables/cards/timelines automatically — DO NOT re-render that data with markdown tables or cards. Refer to it as "above" and add only commentary.` diff --git a/app/routes/ai.tsx b/app/routes/ai.tsx index 46fa3f6..08be413 100644 --- a/app/routes/ai.tsx +++ b/app/routes/ai.tsx @@ -78,6 +78,13 @@ import { import { ARCADIA_KNOWLEDGE } from "~/lib/arcadia-knowledge" import { formatAdminContextForPrompt } from "~/lib/admin-context" import { ConfirmCard } from "~/components/assistant/confirm-card" +import { renderToolResult } from "~/components/assistant/tool-result-renderers" + +function ToolResultBlock({ name, result }: { name: string; result: unknown }) { + const rich = renderToolResult(name, result) + if (!rich) return null + return
    {rich}
    +} const SNAPSHOT_KEY = "crema.ai.snapshot" type StoredMessage = { role: "user" | "assistant"; content: string } @@ -549,7 +556,12 @@ function ChatSurface({ {calls.length > 0 && (
    {calls.map((c) => ( - +
    + + {c.status === "success" && ( + + )} +
    ))}
    )} diff --git a/package-lock.json b/package-lock.json index d37716a..627de6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "react-dom": "^19.2.4", "react-markdown": "^10.1.0", "react-router": "7.13.1", + "remark-gfm": "^4.0.1", "shadcn": "^4.2.0", "tailwind-merge": "^3.5.0", "topojson-client": "^3.1.0", @@ -4575,6 +4576,18 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -6228,6 +6241,16 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6237,6 +6260,22 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", @@ -6261,6 +6300,107 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", @@ -6508,6 +6648,127 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-factory-destination": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", @@ -8041,6 +8302,24 @@ "node": ">=8" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -8074,6 +8353,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index 9522faf..057d148 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "react-dom": "^19.2.4", "react-markdown": "^10.1.0", "react-router": "7.13.1", + "remark-gfm": "^4.0.1", "shadcn": "^4.2.0", "tailwind-merge": "^3.5.0", "topojson-client": "^3.1.0",