Skip to content

Commit d7ad47b

Browse files
committed
refactor(docs-ai): share mdx component map for chat rendering
1 parent 3e12f68 commit d7ad47b

File tree

4 files changed

+134
-101
lines changed

4 files changed

+134
-101
lines changed
Lines changed: 83 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
"use client";
22

3-
import Link from "next/link";
3+
import { sharedMdxComponents } from "@/components/mdx-shared-components";
44
import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock";
55
import type {
66
Blockquote,
77
Code,
88
Content,
99
Emphasis,
1010
Heading,
11-
HTML,
1211
InlineCode,
1312
Link as MdLink,
1413
List,
@@ -20,69 +19,70 @@ import type {
2019
TableCell,
2120
TableRow,
2221
Text,
23-
ThematicBreak,
2422
} from "mdast";
25-
import { useMemo, type ReactNode } from "react";
23+
import { createElement, Fragment, useMemo, type ElementType, type ReactNode } from "react";
2624
import remarkGfm from "remark-gfm";
2725
import remarkParse from "remark-parse";
2826
import { unified } from "unified";
2927

3028
const markdownProcessor = unified().use(remarkParse).use(remarkGfm);
3129

32-
function toInternalSeedHref(url: string): string | null {
33-
try {
34-
const parsed = new URL(url);
35-
const isSeedDomain =
36-
parsed.hostname === "seed-design.io" || parsed.hostname === "www.seed-design.io";
37-
if (!isSeedDomain) return null;
38-
39-
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
40-
} catch {
41-
return null;
42-
}
30+
type HtmlTag =
31+
| "h1"
32+
| "h2"
33+
| "h3"
34+
| "h4"
35+
| "h5"
36+
| "h6"
37+
| "p"
38+
| "strong"
39+
| "em"
40+
| "code"
41+
| "a"
42+
| "ol"
43+
| "ul"
44+
| "li"
45+
| "blockquote"
46+
| "hr"
47+
| "td";
48+
49+
function renderTag(
50+
tag: HtmlTag,
51+
props: Record<string, unknown>,
52+
key: string,
53+
children?: ReactNode,
54+
) {
55+
const component = (sharedMdxComponents as Record<string, ElementType | undefined>)[tag] ?? tag;
56+
return createElement(component, { ...props, key }, children);
4357
}
4458

4559
function renderInlineNode(node: Content, key: string): ReactNode {
4660
switch (node.type) {
4761
case "text":
48-
return <span key={key}>{(node as Text).value}</span>;
62+
return (node as Text).value;
4963
case "strong":
50-
return (
51-
<strong key={key}>
52-
{(node as Strong).children.map((child, i) => renderInlineNode(child, `${key}-${i}`))}
53-
</strong>
64+
return renderTag(
65+
"strong",
66+
{},
67+
key,
68+
(node as Strong).children.map((child, i) => renderInlineNode(child, `${key}-${i}`)),
5469
);
5570
case "emphasis":
56-
return (
57-
<em key={key}>
58-
{(node as Emphasis).children.map((child, i) => renderInlineNode(child, `${key}-${i}`))}
59-
</em>
71+
return renderTag(
72+
"em",
73+
{},
74+
key,
75+
(node as Emphasis).children.map((child, i) => renderInlineNode(child, `${key}-${i}`)),
6076
);
6177
case "inlineCode":
62-
return <code key={key}>{(node as InlineCode).value}</code>;
78+
return renderTag("code", {}, key, (node as InlineCode).value);
6379
case "link": {
6480
const linkNode = node as MdLink;
65-
const internalHref = toInternalSeedHref(linkNode.url);
66-
const children = linkNode.children.map((child, i) => renderInlineNode(child, `${key}-${i}`));
67-
68-
if (internalHref) {
69-
return (
70-
<Link key={key} href={internalHref} className="text-fd-primary hover:underline break-all">
71-
{children}
72-
</Link>
73-
);
74-
}
75-
76-
return (
77-
<a
78-
key={key}
79-
href={linkNode.url}
80-
target="_blank"
81-
rel="noreferrer"
82-
className="text-fd-primary hover:underline break-all"
83-
>
84-
{children}
85-
</a>
81+
return renderTag(
82+
"a",
83+
{ href: linkNode.url },
84+
key,
85+
linkNode.children.map((child, i) => renderInlineNode(child, `${key}-${i}`)),
8686
);
8787
}
8888
case "break":
@@ -96,67 +96,65 @@ function renderTableCell(cell: TableCell, key: string, align?: "left" | "right"
9696
const className =
9797
align === "right" ? "text-right" : align === "center" ? "text-center" : "text-left";
9898

99-
return (
100-
<td key={key} className={`px-2 py-1 align-top ${className}`}>
101-
{cell.children.map((child, i) => renderInlineNode(child, `${key}-${i}`))}
102-
</td>
99+
return renderTag(
100+
"td",
101+
{ className: `px-2 py-1 align-top ${className}` },
102+
key,
103+
cell.children.map((child, i) => renderInlineNode(child, `${key}-${i}`)),
103104
);
104105
}
105106

106107
function renderBlockNode(node: Content, key: string): ReactNode {
107108
switch (node.type) {
108109
case "heading": {
109110
const headingNode = node as Heading;
110-
const content = headingNode.children.map((child, i) =>
111-
renderInlineNode(child, `${key}-${i}`),
111+
const tagName = `h${Math.min(headingNode.depth, 6)}` as HtmlTag;
112+
return renderTag(
113+
tagName,
114+
{},
115+
key,
116+
headingNode.children.map((child, i) => renderInlineNode(child, `${key}-${i}`)),
112117
);
113-
114-
if (headingNode.depth <= 2) return <h2 key={key}>{content}</h2>;
115-
if (headingNode.depth === 3) return <h3 key={key}>{content}</h3>;
116-
if (headingNode.depth === 4) return <h4 key={key}>{content}</h4>;
117-
return <h5 key={key}>{content}</h5>;
118118
}
119-
120119
case "paragraph": {
121120
const paragraphNode = node as Paragraph;
122-
return (
123-
<p key={key}>
124-
{paragraphNode.children.map((child, i) => renderInlineNode(child, `${key}-${i}`))}
125-
</p>
121+
return renderTag(
122+
"p",
123+
{},
124+
key,
125+
paragraphNode.children.map((child, i) => renderInlineNode(child, `${key}-${i}`)),
126126
);
127127
}
128-
129128
case "list": {
130129
const listNode = node as List;
131-
const Element = listNode.ordered ? "ol" : "ul";
132-
return (
133-
<Element key={key}>
134-
{listNode.children.map((child, i) => renderBlockNode(child, `${key}-${i}`))}
135-
</Element>
130+
const tagName = listNode.ordered ? "ol" : "ul";
131+
return renderTag(
132+
tagName,
133+
{},
134+
key,
135+
listNode.children.map((child, i) => renderBlockNode(child, `${key}-${i}`)),
136136
);
137137
}
138-
139138
case "listItem": {
140139
const listItem = node as ListItem;
141-
return (
142-
<li key={key}>
143-
{listItem.children.map((child, i) => renderBlockNode(child, `${key}-${i}`))}
144-
</li>
140+
return renderTag(
141+
"li",
142+
{},
143+
key,
144+
listItem.children.map((child, i) => renderBlockNode(child, `${key}-${i}`)),
145145
);
146146
}
147-
148147
case "blockquote": {
149148
const blockquote = node as Blockquote;
150-
return (
151-
<blockquote key={key}>
152-
{blockquote.children.map((child, i) => renderBlockNode(child, `${key}-${i}`))}
153-
</blockquote>
149+
return renderTag(
150+
"blockquote",
151+
{},
152+
key,
153+
blockquote.children.map((child, i) => renderBlockNode(child, `${key}-${i}`)),
154154
);
155155
}
156-
157156
case "thematicBreak":
158-
return <hr key={key} />;
159-
157+
return renderTag("hr", {}, key);
160158
case "code": {
161159
const codeNode = node as Code;
162160
return (
@@ -165,10 +163,10 @@ function renderBlockNode(node: Content, key: string): ReactNode {
165163
</div>
166164
);
167165
}
168-
169166
case "table": {
170167
const tableNode = node as Table;
171168
const [head, ...body] = tableNode.children as TableRow[];
169+
172170
return (
173171
<div key={key} className="my-2 overflow-x-auto">
174172
<table className="w-full border-collapse text-sm">
@@ -211,10 +209,6 @@ function renderBlockNode(node: Content, key: string): ReactNode {
211209
</div>
212210
);
213211
}
214-
215-
case "html":
216-
return <p key={key}>{(node as HTML).value}</p>;
217-
218212
default:
219213
return null;
220214
}
@@ -237,7 +231,9 @@ export function ChatMarkdown({ markdown }: { markdown: string }) {
237231

238232
return (
239233
<div className="prose prose-sm dark:prose-invert max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
240-
{root.children.map((node, i) => renderBlockNode(node, `chat-md-${i}`))}
234+
{root.children.map((node, i) => (
235+
<Fragment key={`chat-md-${i}`}>{renderBlockNode(node, `chat-md-${i}`)}</Fragment>
236+
))}
241237
</div>
242238
);
243239
}

docs/components/ai-panel/chat-message.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,6 @@ function sanitizeTextForTools(text: string, toolContext: ToolRenderContext): str
505505
}
506506

507507
sanitized = sanitized
508-
.replace(/^#{1,6}\s*/gm, "")
509508
.split("\n")
510509
.map((line) => line.replace(/\s+$/g, ""))
511510
.join("\n")

docs/components/mdx-components.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ComponentExample } from "@/components/component-example";
33
import { ComponentGrid } from "@/components/component-grid";
44
import { ComponentSpecBlock } from "@/components/component-spec-block";
55
import { ManualInstallation } from "@/components/manual-installation";
6+
import { sharedMdxComponents } from "@/components/mdx-shared-components";
67
import { StackflowExample } from "@/components/stackflow-example";
78
import { TokenReference } from "@/components/token-reference";
89
import { createReactTypeTable } from "@/components/type-table/react-type-table";
@@ -19,8 +20,6 @@ import { Step, Steps } from "fumadocs-ui/components/steps";
1920
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
2021
import { ThemeToggle } from "fumadocs-ui/components/layout/theme-toggle";
2122
import { TypeTable } from "fumadocs-ui/components/type-table";
22-
import defaultMdxComponents from "fumadocs-ui/mdx";
23-
import clsx from "clsx";
2423
import type { MDXComponents } from "mdx/types";
2524
import { BreezeManualInstallation } from "./breeze-manual-installation";
2625
import { DoImage } from "./guideline/do-image";
@@ -39,17 +38,7 @@ import { typeTableGenerator } from "./type-table/generator";
3938
const { ReactTypeTable } = createReactTypeTable(typeTableGenerator);
4039

4140
export const mdxComponents: MDXComponents = {
42-
...defaultMdxComponents,
43-
44-
img: ({ className, ...rest }) => (
45-
<ImageZoom
46-
className={clsx(
47-
className,
48-
"bg-palette-gray-100 dark:bg-palette-gray-900 rounded-r2 overflow-hidden",
49-
)}
50-
{...rest}
51-
/>
52-
),
41+
...sharedMdxComponents,
5342

5443
// Layout
5544
Grid: ({ children }) => (
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import Link from "next/link";
2+
import { ImageZoom } from "fumadocs-ui/components/image-zoom";
3+
import defaultMdxComponents from "fumadocs-ui/mdx";
4+
import clsx from "clsx";
5+
import type { MDXComponents } from "mdx/types";
6+
7+
function toInternalSeedHref(href: string): string | null {
8+
if (href.startsWith("/")) {
9+
return href;
10+
}
11+
12+
try {
13+
const parsed = new URL(href);
14+
const isSeedDomain =
15+
parsed.hostname === "seed-design.io" || parsed.hostname === "www.seed-design.io";
16+
17+
if (!isSeedDomain) return null;
18+
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
19+
} catch {
20+
return null;
21+
}
22+
}
23+
24+
export const sharedMdxComponents: MDXComponents = {
25+
...defaultMdxComponents,
26+
27+
img: ({ className, ...rest }) => (
28+
<ImageZoom
29+
className={clsx(
30+
className,
31+
"bg-palette-gray-100 dark:bg-palette-gray-900 rounded-r2 overflow-hidden",
32+
)}
33+
{...rest}
34+
/>
35+
),
36+
37+
a: ({ href, ...rest }) => {
38+
if (typeof href !== "string") {
39+
return <a {...rest} />;
40+
}
41+
42+
const internalHref = toInternalSeedHref(href);
43+
if (internalHref) {
44+
return <Link href={internalHref} {...rest} />;
45+
}
46+
47+
return <a href={href} {...rest} />;
48+
},
49+
};

0 commit comments

Comments
 (0)