diff --git a/.dumi/theme/common/Loading.tsx b/.dumi/theme/common/Loading.tsx
index 337db990f9..1d0eeabd9d 100644
--- a/.dumi/theme/common/Loading.tsx
+++ b/.dumi/theme/common/Loading.tsx
@@ -1,15 +1,21 @@
import React from 'react';
+import { StyleProvider } from '@ant-design/cssinjs';
import { Flex, Skeleton, Spin } from 'antd';
import { useLocation } from 'dumi';
+import { Common } from './styles';
+
const Loading: React.FC = () => {
const { pathname } = useLocation();
+
+ let loadingNode: React.ReactNode = null;
+
if (
pathname.startsWith('/components') ||
pathname.startsWith('/docs') ||
pathname.startsWith('/changelog')
) {
- return (
+ loadingNode = (

{
);
+ } else {
+ loadingNode = (
+
+
+
+ );
}
+ // Loading 组件独立于 GlobalLayout,而它也会影响站点样式。
+ // 所以我们这边需要 hardcode 一下启动 layer。
return (
-
-
-
+
+
+ {loadingNode}
+
);
};
diff --git a/.dumi/theme/common/ThemeSwitch/PromptDrawer.tsx b/.dumi/theme/common/ThemeSwitch/PromptDrawer.tsx
new file mode 100644
index 0000000000..9a42827468
--- /dev/null
+++ b/.dumi/theme/common/ThemeSwitch/PromptDrawer.tsx
@@ -0,0 +1,108 @@
+import React, { useRef, useState } from 'react';
+import { AntDesignOutlined, UserOutlined } from '@ant-design/icons';
+import { Bubble, Sender } from '@ant-design/x';
+import { Drawer, Flex, GetProp, Typography } from 'antd';
+
+import useLocale from '../../../hooks/useLocale';
+import type { SiteContextProps } from '../../../theme/slots/SiteContext';
+import usePromptTheme from './usePromptTheme';
+
+const locales = {
+ cn: {
+ title: 'AI 生成主题',
+ finishTips: '生成完成,对话以重新生成。',
+ },
+ en: {
+ title: 'AI Theme Generator',
+ finishTips: 'Completed. Regenerate by start a new conversation.',
+ },
+};
+
+export interface PromptDrawerProps {
+ open: boolean;
+ onClose: () => void;
+ onThemeChange?: (themeConfig: SiteContextProps['dynamicTheme']) => void;
+}
+
+const PromptDrawer: React.FC = ({ open, onClose, onThemeChange }) => {
+ const [locale] = useLocale(locales);
+ const [inputValue, setInputValue] = useState('');
+ const senderRef = useRef(null);
+
+ const [submitPrompt, loading, prompt, resText, cancelRequest] = usePromptTheme(onThemeChange);
+
+ const handleSubmit = (value: string) => {
+ submitPrompt(value);
+ setInputValue('');
+ };
+
+ const handleAfterOpenChange = (isOpen: boolean) => {
+ if (isOpen && senderRef.current) {
+ // Focus the Sender component when drawer opens
+ senderRef.current.focus?.();
+ }
+ };
+
+ const items = React.useMemo>(() => {
+ if (!prompt) {
+ return [];
+ }
+
+ const nextItems: GetProp = [
+ {
+ placement: 'end',
+ content: prompt,
+ avatar: { icon: },
+ shape: 'corner',
+ },
+ {
+ placement: 'start',
+ content: resText,
+ avatar: { icon: },
+ loading: !resText,
+ messageRender: (content: string) => (
+
+ {content}
+
+ ),
+ },
+ ];
+
+ if (!loading) {
+ nextItems.push({
+ placement: 'start',
+ content: locale.finishTips,
+ avatar: { icon: },
+ shape: 'corner',
+ });
+ }
+
+ return nextItems;
+ }, [prompt, resText, loading]);
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default PromptDrawer;
diff --git a/.dumi/theme/common/ThemeSwitch/index.tsx b/.dumi/theme/common/ThemeSwitch/index.tsx
index cce622ed76..d93afe5d29 100644
--- a/.dumi/theme/common/ThemeSwitch/index.tsx
+++ b/.dumi/theme/common/ThemeSwitch/index.tsx
@@ -1,5 +1,11 @@
-import React, { use, useRef } from 'react';
-import { BgColorsOutlined, LinkOutlined, SmileOutlined, SunOutlined } from '@ant-design/icons';
+import React, { use, useRef, useState } from 'react';
+import {
+ BgColorsOutlined,
+ LinkOutlined,
+ ShopOutlined,
+ SmileOutlined,
+ SunOutlined,
+} from '@ant-design/icons';
import { Badge, Button, Dropdown } from 'antd';
import type { MenuProps } from 'antd';
import { CompactTheme, DarkTheme } from 'antd-token-previewer/es/icons';
@@ -10,6 +16,7 @@ import type { SiteContextProps } from '../../slots/SiteContext';
import SiteContext from '../../slots/SiteContext';
import { getLocalizedPathname, isZhCN } from '../../utils';
import Link from '../Link';
+import PromptDrawer from './PromptDrawer';
import ThemeIcon from './ThemeIcon';
export type ThemeName = 'light' | 'dark' | 'compact' | 'motion-off' | 'happy-work';
@@ -20,9 +27,10 @@ export interface ThemeSwitchProps {
const ThemeSwitch: React.FC = () => {
const { pathname, search } = useLocation();
- const { theme, updateSiteConfig } = use(SiteContext);
+ const { theme, updateSiteConfig, dynamicTheme } = use(SiteContext);
const toggleAnimationTheme = useThemeAnimation();
const lastThemeKey = useRef(theme.includes('dark') ? 'dark' : 'light');
+ const [isMarketDrawerOpen, setIsMarketDrawerOpen] = useState(false);
const badge = ;
@@ -61,6 +69,12 @@ const ThemeSwitch: React.FC = () => {
{
type: 'divider',
},
+ {
+ id: 'app.theme.switch.market',
+ icon: ,
+ key: 'market',
+ showBadge: () => !!dynamicTheme,
+ },
{
id: 'app.footer.theme',
icon: ,
@@ -95,8 +109,25 @@ const ThemeSwitch: React.FC = () => {
// 处理主题切换
const handleThemeChange = (key: string, domEvent: React.MouseEvent) => {
- // 主题编辑器特殊处理
- if (key === 'theme-editor') {
+ // 查找对应的选项配置
+ const option = themeOptions.find((opt) => opt.key === key);
+
+ // 链接类型的菜单项特殊处理,不执行主题切换逻辑
+ if (option?.isLink) {
+ return;
+ }
+
+ // Market 选项特殊处理
+ if (key === 'market') {
+ // 如果已经有动态主题,点击时清除动态主题
+ if (dynamicTheme) {
+ updateSiteConfig({
+ dynamicTheme: undefined,
+ });
+ } else {
+ // 否则打开 Drawer 生成新主题
+ setIsMarketDrawerOpen(true);
+ }
return;
}
@@ -131,9 +162,21 @@ const ThemeSwitch: React.FC = () => {
};
return (
-
- } style={{ fontSize: 16 }} />
-
+ <>
+
+ } style={{ fontSize: 16 }} />
+
+
+ setIsMarketDrawerOpen(false)}
+ onThemeChange={(nextTheme) => {
+ updateSiteConfig({
+ dynamicTheme: nextTheme,
+ });
+ }}
+ />
+ >
);
};
diff --git a/.dumi/theme/common/ThemeSwitch/usePromptTheme.ts b/.dumi/theme/common/ThemeSwitch/usePromptTheme.ts
new file mode 100644
index 0000000000..21d1ff4bec
--- /dev/null
+++ b/.dumi/theme/common/ThemeSwitch/usePromptTheme.ts
@@ -0,0 +1,131 @@
+/* eslint-disable compat/compat */
+import { useRef, useState } from 'react';
+import { XStream } from '@ant-design/x';
+
+import type { SiteContextProps } from '../../../theme/slots/SiteContext';
+
+const fetchTheme = async (
+ prompt: string,
+ update: (currentFullContent: string) => void,
+ abortSignal?: AbortSignal,
+) => {
+ const options = {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: prompt,
+ userId: 'AntDesignSite',
+ }),
+ signal: abortSignal,
+ };
+
+ try {
+ const response = await fetch('https://api.x.ant.design/api/agent_tbox_antd', options);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ if (!response.body) {
+ throw new Error('Response body is null');
+ }
+
+ let fullContent = '';
+
+ for await (const chunk of XStream({
+ readableStream: response.body,
+ })) {
+ if (chunk.event === 'message') {
+ const data = JSON.parse(chunk.data) as {
+ lane: string;
+ payload: string;
+ };
+
+ const payload = JSON.parse(data.payload) as {
+ text: string;
+ };
+
+ fullContent += payload.text || '';
+ update(fullContent);
+ }
+ }
+
+ return fullContent;
+ } catch (error) {
+ console.error('Error in fetchTheme:', error);
+ throw error;
+ }
+};
+
+// Remove '```json' code block from the response
+function getJsonText(raw: string, rmComment = false): string {
+ const replaced = raw.trim().replace(/^```json\s*|\s*```$/g, '');
+
+ return rmComment ? replaced.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, '').trim() : replaced;
+}
+
+export default function usePromptTheme(
+ onThemeChange?: (themeConfig: SiteContextProps['dynamicTheme']) => void,
+) {
+ const [prompt, setPrompt] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [resText, setResText] = useState('');
+ const abortControllerRef = useRef(null);
+
+ const submitPrompt = async (nextPrompt: string) => {
+ if (!nextPrompt.trim()) {
+ return;
+ }
+
+ setPrompt(nextPrompt);
+
+ // Cancel previous request if it exists
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+
+ // Create new AbortController for this request
+ const abortController = new AbortController();
+ abortControllerRef.current = abortController;
+
+ setLoading(true);
+ setResText('');
+
+ try {
+ const data = await fetchTheme(
+ nextPrompt,
+ (currentContent) => {
+ setResText(currentContent);
+ },
+ abortController.signal,
+ );
+
+ // Handle the response
+ if (data && onThemeChange) {
+ const nextConfig = JSON.parse(getJsonText(data, true));
+ onThemeChange(nextConfig);
+ }
+ } catch (error) {
+ if (error instanceof Error && error.name === 'AbortError') {
+ console.warn('Request was aborted');
+ } else {
+ console.error('Failed to generate theme:', error);
+ }
+ } finally {
+ setLoading(false);
+ abortControllerRef.current = null;
+ }
+ };
+
+ const cancelRequest = () => {
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ abortControllerRef.current = null;
+ setLoading(false);
+ }
+ };
+
+ return [submitPrompt, loading, prompt, getJsonText(resText), cancelRequest] as const;
+}
diff --git a/.dumi/theme/common/styles/Common.tsx b/.dumi/theme/common/styles/Common.tsx
index 23f900c108..fc2fc5bbe7 100644
--- a/.dumi/theme/common/styles/Common.tsx
+++ b/.dumi/theme/common/styles/Common.tsx
@@ -7,7 +7,7 @@ export default () => {
const { anchorTop } = useTheme();
React.useInsertionEffect(() => {
- updateCSS(`@layer global, antd;`, 'site-global', {
+ updateCSS(`@layer theme, base, global, antd, components, utilities;`, 'site-global', {
prepend: true,
});
}, []);
diff --git a/.dumi/theme/layouts/DocLayout/index.tsx b/.dumi/theme/layouts/DocLayout/index.tsx
index 0d642b80e2..adc14382af 100644
--- a/.dumi/theme/layouts/DocLayout/index.tsx
+++ b/.dumi/theme/layouts/DocLayout/index.tsx
@@ -13,7 +13,6 @@ import useLocation from '../../../hooks/useLocation';
import GlobalStyles from '../../common/GlobalStyles';
import Header from '../../slots/Header';
import SiteContext from '../../slots/SiteContext';
-
import IndexLayout from '../IndexLayout';
import ResourceLayout from '../ResourceLayout';
import SidebarLayout from '../SidebarLayout';
@@ -84,7 +83,7 @@ const DocLayout: React.FC = () => {
if (pathname.startsWith('/docs/resource')) {
return {outlet};
}
- if (pathname.startsWith('/theme-editor')) {
+ if (pathname.startsWith('/theme-editor') || pathname.startsWith('/theme-market')) {
return outlet;
}
return {outlet};
diff --git a/.dumi/theme/layouts/GlobalLayout.tsx b/.dumi/theme/layouts/GlobalLayout.tsx
index 571ade57ac..6dbe2c2a44 100644
--- a/.dumi/theme/layouts/GlobalLayout.tsx
+++ b/.dumi/theme/layouts/GlobalLayout.tsx
@@ -12,7 +12,7 @@ import {
} from '@ant-design/cssinjs';
import { HappyProvider } from '@ant-design/happy-work-theme';
import { getSandpackCssText } from '@codesandbox/sandpack-react';
-import { theme as antdTheme, App } from 'antd';
+import { theme as antdTheme, App, ConfigProvider } from 'antd';
import type { MappingAlgorithm } from 'antd';
import type { DirectionType, ThemeConfig } from 'antd/es/config-provider';
import { createSearchParams, useOutlet, useSearchParams, useServerInsertedHTML } from 'dumi';
@@ -21,13 +21,12 @@ import { DarkContext } from '../../hooks/useDark';
import useLayoutState from '../../hooks/useLayoutState';
import type { ThemeName } from '../common/ThemeSwitch';
import SiteThemeProvider from '../SiteThemeProvider';
-import type { SiteContextProps } from '../slots/SiteContext';
+import type { SimpleComponentClassNames, SiteContextProps } from '../slots/SiteContext';
import SiteContext from '../slots/SiteContext';
import '@ant-design/v5-patch-for-react-19';
-type Entries = { [K in keyof T]: [K, T[K]] }[keyof T][];
-type SiteState = Partial>;
+type SiteState = Partial>;
const RESPONSIVE_MOBILE = 768;
export const ANT_DESIGN_NOT_SHOW_BANNER = 'ANT_DESIGN_NOT_SHOW_BANNER';
@@ -72,12 +71,13 @@ const getAlgorithm = (themes: ThemeName[] = []) =>
const GlobalLayout: React.FC = () => {
const outlet = useOutlet();
const [searchParams, setSearchParams] = useSearchParams();
- const [{ theme = [], direction, isMobile, bannerVisible = false }, setSiteState] =
+ const [{ theme = [], direction, isMobile, bannerVisible = false, dynamicTheme }, setSiteState] =
useLayoutState({
isMobile: false,
direction: 'ltr',
theme: [],
bannerVisible: false,
+ dynamicTheme: undefined,
});
const [systemTheme, setSystemTheme] = React.useState<'dark' | 'light'>(() => getSystemTheme());
@@ -90,7 +90,9 @@ const GlobalLayout: React.FC = () => {
const oldSearchStr = searchParams.toString();
let nextSearchParams: URLSearchParams = searchParams;
- (Object.entries(props) as Entries).forEach(([key, value]) => {
+ Object.entries(props).forEach((kv) => {
+ const [key, value] = kv as [string, string];
+
if (key === 'direction') {
if (value === 'rtl') {
nextSearchParams.set('direction', 'rtl');
@@ -192,18 +194,47 @@ const GlobalLayout: React.FC = () => {
theme: theme!,
isMobile: isMobile!,
bannerVisible,
+ dynamicTheme,
}),
- [isMobile, direction, updateSiteConfig, theme, bannerVisible],
+ [isMobile, direction, updateSiteConfig, theme, bannerVisible, dynamicTheme],
);
- const themeConfig = React.useMemo(
- () => ({
- algorithm: getAlgorithm(theme),
- token: { motion: !theme.includes('motion-off') },
- hashed: false,
- }),
- [theme],
- );
+ const [themeConfig, componentsClassNames] = React.useMemo<
+ [ThemeConfig, SimpleComponentClassNames]
+ >(() => {
+ let mergedTheme = theme;
+
+ const {
+ algorithm: dynamicAlgorithm,
+ token: dynamicToken,
+ ...rawComponentsClassNames
+ } = dynamicTheme || {};
+
+ if (dynamicAlgorithm) {
+ mergedTheme = mergedTheme.filter((c) => c !== 'dark' && c !== 'light');
+ mergedTheme.push(dynamicAlgorithm);
+ }
+
+ // Convert rawComponentsClassNames to nextComponentsClassNames
+ const nextComponentsClassNames: any = {};
+ Object.keys(rawComponentsClassNames).forEach((key) => {
+ nextComponentsClassNames[key] = {
+ classNames: (rawComponentsClassNames as any)[key],
+ };
+ });
+
+ return [
+ {
+ algorithm: getAlgorithm(mergedTheme),
+ token: {
+ motion: !theme.includes('motion-off'),
+ ...dynamicToken,
+ },
+ hashed: false,
+ },
+ nextComponentsClassNames,
+ ];
+ }, [theme, dynamicTheme]);
const [styleCache] = React.useState(() => createCache());
@@ -245,12 +276,15 @@ const GlobalLayout: React.FC = () => {
- {outlet}
+
+ {outlet}
+
diff --git a/.dumi/theme/locales/en-US.json b/.dumi/theme/locales/en-US.json
index 2b051a850b..a6cf704cf8 100644
--- a/.dumi/theme/locales/en-US.json
+++ b/.dumi/theme/locales/en-US.json
@@ -6,6 +6,7 @@
"app.theme.switch.motion.on": "Motion On",
"app.theme.switch.motion.off": "Motion Off",
"app.theme.switch.happy-work": "Happy Work Effect",
+ "app.theme.switch.market": "AI Theme Generator",
"app.header.search": "Search...",
"app.header.menu.documentation": "Docs",
"app.header.menu.more": "More",
diff --git a/.dumi/theme/locales/zh-CN.json b/.dumi/theme/locales/zh-CN.json
index b20abb81fc..1bffdfa09f 100644
--- a/.dumi/theme/locales/zh-CN.json
+++ b/.dumi/theme/locales/zh-CN.json
@@ -6,6 +6,7 @@
"app.theme.switch.motion.on": "动画开启",
"app.theme.switch.motion.off": "动画关闭",
"app.theme.switch.happy-work": "快乐工作特效",
+ "app.theme.switch.market": "AI 生成主题",
"app.header.search": "全文本搜索...",
"app.header.menu.documentation": "文档",
"app.header.menu.more": "更多",
diff --git a/.dumi/theme/slots/SiteContext.ts b/.dumi/theme/slots/SiteContext.ts
index 40c10f3d95..b3e1be8660 100644
--- a/.dumi/theme/slots/SiteContext.ts
+++ b/.dumi/theme/slots/SiteContext.ts
@@ -1,14 +1,24 @@
import * as React from 'react';
import type { DirectionType } from 'antd/es/config-provider';
+import type { ConfigComponentProps } from '../../../components/config-provider/context';
import type { ThemeName } from '../common/ThemeSwitch';
+export type SimpleComponentClassNames = Partial<
+ Record>
+>;
+
export interface SiteContextProps {
isMobile: boolean;
bannerVisible: boolean;
direction: DirectionType;
theme: ThemeName[];
updateSiteConfig: (props: Partial) => void;
+
+ dynamicTheme?: {
+ algorithm?: 'light' | 'dark';
+ token: Record;
+ } & SimpleComponentClassNames;
}
const SiteContext = React.createContext({
diff --git a/.dumirc.ts b/.dumirc.ts
index 5b9957f2f0..324bd897e7 100644
--- a/.dumirc.ts
+++ b/.dumirc.ts
@@ -1,12 +1,12 @@
+import os from 'node:os';
import path from 'path';
import { defineConfig } from 'dumi';
import * as fs from 'fs-extra';
-import os from 'node:os';
import rehypeAntd from './.dumi/rehypeAntd';
import rehypeChangelog from './.dumi/rehypeChangelog';
-import remarkAntd from './.dumi/remarkAntd';
import remarkAnchor from './.dumi/remarkAnchor';
+import remarkAntd from './.dumi/remarkAntd';
import { version } from './package.json';
export default defineConfig({
@@ -187,6 +187,10 @@ export default defineConfig({
`,
],
scripts: [
+ {
+ src: 'https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4',
+ async: true,
+ },
{
async: true,
content: fs
diff --git a/components/date-picker/demo/_semantic.tsx b/components/date-picker/demo/_semantic.tsx
index 50d8dc131d..d6c6aae387 100644
--- a/components/date-picker/demo/_semantic.tsx
+++ b/components/date-picker/demo/_semantic.tsx
@@ -12,11 +12,11 @@ const locales = {
input: '输入框元素',
suffix: '后缀元素',
popup: '弹出框元素',
- popupHeader: '弹出框头部元素',
- popupBody: '弹出框主体元素',
- popupContent: '弹出框内容元素',
- popupItem: '弹出框单项元素',
- popupFooter: '弹出框底部元素',
+ 'popup.header': '弹出框头部元素',
+ 'popup.body': '弹出框主体元素',
+ 'popup.content': '弹出框内容元素',
+ 'popup.item': '弹出框单项元素',
+ 'popup.footer': '弹出框底部元素',
},
en: {
root: 'Root element',
@@ -24,11 +24,11 @@ const locales = {
input: 'Input element',
suffix: 'Suffix element',
popup: 'Popup element',
- popupHeader: 'Popup header element',
- popupBody: 'Popup body element',
- popupContent: 'Popup content element',
- popupItem: 'Popup content item element',
- popupFooter: 'Popup footer element',
+ 'popup.header': 'Popup header element',
+ 'popup.body': 'Popup body element',
+ 'popup.content': 'Popup content element',
+ 'popup.item': 'Popup content item element',
+ 'popup.footer': 'Popup footer element',
},
};
@@ -97,11 +97,11 @@ export function PickerSemanticTemplate(props: PickerSemanticTemplateProps) {
{ name: 'input', desc: locale.input },
{ name: 'suffix', desc: locale.suffix },
{ name: 'popup.root', desc: locale.popup },
- { name: 'popup.header', desc: locale.popupHeader },
- { name: 'popup.body', desc: locale.popupBody },
- { name: 'popup.content', desc: locale.popupContent },
- { name: 'popup.item', desc: locale.popupItem },
- { name: 'popup.footer', desc: locale.popupFooter },
+ { name: 'popup.header', desc: locale['popup.header'] },
+ { name: 'popup.body', desc: locale['popup.body'] },
+ { name: 'popup.content', desc: locale['popup.content'] },
+ { name: 'popup.item', desc: locale['popup.item'] },
+ { name: 'popup.footer', desc: locale['popup.footer'] },
].filter((semantic) => !ignoreSemantics.includes(semantic.name))}
>
= {
+ 'badge:ribbon': 'ribbon',
+ 'floatButton:group': 'floatButtonGroup',
+ 'input:input': 'input',
+ 'input:otp': 'otp',
+ 'input:search': 'inputSearch',
+ 'input:textarea': 'textArea',
+};
+
+// 将 kebab-case 转换为 camelCase
+function toCamelCase(str: string): string {
+ return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
+}
+
+// 构建嵌套结构的辅助函数
+function buildNestedStructure(flatSemantics: Record): any {
+ const result: any = {};
+
+ // 首先,收集所有带点号的键,确定哪些是父级
+ const nestedKeys = new Set();
+ for (const key of Object.keys(flatSemantics)) {
+ if (key.includes('.')) {
+ const parentKey = key.split('.')[0];
+ nestedKeys.add(parentKey);
+ }
+ }
+
+ for (const [key, value] of Object.entries(flatSemantics)) {
+ if (key.includes('.')) {
+ const parts = key.split('.');
+ let current = result;
+
+ // 遍历除最后一个部分外的所有部分
+ for (let i = 0; i < parts.length - 1; i++) {
+ const part = parts[i];
+ if (!current[part]) {
+ current[part] = {};
+ } else if (typeof current[part] === 'string') {
+ // 如果已经存在一个字符串值,需要将其转换为对象
+ // 保留原有的值作为一个特殊的 _value 属性或者忽略冲突
+ // console.warn(
+ // `Warning: Key conflict for '${part}', nested structure will override simple value`,
+ // );
+ current[part] = {};
+ }
+ current = current[part];
+ }
+
+ // 设置最后一个部分的值
+ current[parts[parts.length - 1]] = value;
+ } else {
+ // 只有当这个键不是某个嵌套结构的父级时,才直接设置值
+ if (!nestedKeys.has(key)) {
+ result[key] = value;
+ } else {
+ // 如果这个键将成为父级,先确保它是一个对象
+ if (!result[key]) {
+ result[key] = {};
+ }
+ }
+ }
+ }
+
+ return result;
+}
+
+// 递归生成 markdown 格式的嵌套结构
+function generateMarkdownStructure(obj: any, prefix = '', indent = 0): string {
+ let result = '';
+ for (const [key, value] of Object.entries(obj)) {
+ const indentStr = ' '.repeat(indent);
+ if (typeof value === 'string') {
+ result += `${indentStr}- \`${key}\`: ${value}\n`;
+ } else {
+ result += `${indentStr}- \`${key}\`:\n`;
+ result += generateMarkdownStructure(value, prefix, indent + 1);
+ }
+ }
+ return result;
+}
+
+// 递归打印嵌套结构的辅助函数(已注释掉)
+// function printNestedStructure(obj: any, prefix = '', indent = 0): void {
+// for (const [key, value] of Object.entries(obj)) {
+// const indentStr = ' '.repeat(indent);
+// if (typeof value === 'string') {
+// console.log(`${indentStr}- ${key}: ${value}`);
+// } else {
+// console.log(`${indentStr}- ${key}:`);
+// printNestedStructure(value, prefix, indent + 1);
+// }
+// }
+// }
+
+async function generateSemanticDesc() {
+ const cwd = process.cwd();
+ const siteDir = path.resolve(cwd, '_site');
+ const docsDir = ['components', 'docs'];
+
+ // Ensure siteDir
+ await fs.ensureDir(siteDir);
+
+ const docs = await glob(`{${docsDir.join(',')}}/**/demo/_semantic*.tsx`);
+
+ // Read `docs` file and generate semantic description.e.g.
+ // components/float-button/demo/_semantic_group.tsx is to:
+ // - root: 根元素
+ // - list: 列表元素
+ // - item: 列表项元素
+ // - root: 列表项根元素
+ // - icon: 列表项图标元素
+ // - content: 列表项内容元素
+ // - trigger:
+ // - root: 触发元素
+ // - icon: 触发图标元素
+ // - content: 触发内容元素
+
+ const semanticDescriptions: Record = {};
+
+ for (const docPath of docs) {
+ try {
+ // 读取文件内容
+ const content = await fs.readFile(docPath, 'utf-8');
+
+ // 提取组件名称(从路径中获取)
+ const componentName = path.basename(path.dirname(path.dirname(docPath)));
+ const fileName = path.basename(docPath, '.tsx');
+
+ // 转换为 camelCase
+ const camelCaseComponentName = toCamelCase(componentName);
+
+ // 如果是 _semantic.tsx,使用组件名;如果是其他变体,添加后缀
+ let semanticKey = camelCaseComponentName;
+ if (fileName !== '_semantic') {
+ const variant = fileName.replace('_semantic_', '').replace('_semantic', '');
+ if (variant) {
+ semanticKey = `${camelCaseComponentName}:${variant}`;
+ }
+ }
+
+ // 检查是否有特殊转换映射
+ if (ConvertMap[semanticKey]) {
+ semanticKey = ConvertMap[semanticKey];
+ }
+
+ // 使用正则表达式提取 locales 对象
+ const localesMatch = content.match(/const locales = \{([\s\S]*?)\};/);
+ if (!localesMatch) {
+ // console.warn(`No locales found in ${docPath}`);
+ continue;
+ }
+
+ // 提取中文 locales
+ const cnMatch = content.match(/cn:\s*\{([\s\S]*?)\},?\s*en:/);
+ if (!cnMatch) {
+ // console.warn(`No Chinese locales found in ${docPath}`);
+ continue;
+ }
+
+ const cnContent = cnMatch[1];
+ const flatSemantics: Record = {};
+
+ // 提取每个语义描述
+ const semanticMatches = cnContent.matchAll(/['"]?([^'":\s]+)['"]?\s*:\s*['"]([^'"]+)['"],?/g);
+ for (const match of semanticMatches) {
+ const [, key, value] = match;
+ flatSemantics[key] = value;
+ }
+
+ if (Object.keys(flatSemantics).length > 0) {
+ // 将扁平的语义描述转换为层级结构
+ const nestedSemantics = buildNestedStructure(flatSemantics);
+ semanticDescriptions[semanticKey] = nestedSemantics;
+
+ // 生成层级结构的描述(已注释掉)
+ // console.log(`\n${semanticKey}:`);
+ // printNestedStructure(nestedSemantics);
+ }
+ } catch (error) {
+ console.error(`Error processing ${docPath}:`, error);
+ }
+ }
+
+ // 生成 markdown 内容
+ let markdownContent = '# Ant Design 组件语义化描述\n\n';
+ markdownContent += '本文档包含了 Ant Design 组件库中所有组件的语义化描述信息。\n\n';
+ markdownContent += `> 总计 ${Object.keys(semanticDescriptions).length} 个组件包含语义化描述\n\n`;
+ markdownContent += '## 组件列表\n\n';
+
+ // 按组件名排序
+ const sortedComponents = Object.keys(semanticDescriptions).sort();
+
+ for (const componentName of sortedComponents) {
+ const semantics = semanticDescriptions[componentName];
+ markdownContent += `### ${componentName}\n\n`;
+ markdownContent += generateMarkdownStructure(semantics);
+ markdownContent += '\n';
+ }
+
+ // 生成总结(已注释掉)
+ // console.log('\n=== Semantic Descriptions Summary ===');
+ // console.log(
+ // `Total components with semantic descriptions: ${Object.keys(semanticDescriptions).length}`,
+ // );
+
+ // 将结果写入 markdown 文件
+ const outputPath = path.join(siteDir, 'llms-semantic.md');
+ await fs.writeFile(outputPath, markdownContent, 'utf-8');
+ console.log(`Semantic descriptions saved to: ${outputPath}`);
+}
+(async () => {
+ if (require.main === module) {
+ await generateSemanticDesc();
+ }
+})().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});
diff --git a/scripts/generate-llms.ts b/scripts/generate-llms.ts
index a23d91fad8..17496e8a0a 100644
--- a/scripts/generate-llms.ts
+++ b/scripts/generate-llms.ts
@@ -65,13 +65,26 @@ async function generateLLms() {
'',
'- Ant Design, developed by Ant Group, is a React UI library that aims to provide a high-quality design language and development framework for enterprise-level backend management systems. It offers a rich set of components and design guidelines, helping developers build modern, responsive, and high-performance web applications.',
'',
+ '## Semantic Descriptions',
+ '',
+ '- [Ant Design Component Semantic Descriptions](https://ant.design/llms-semantic.md)',
+ '',
'## Docs',
'',
...docsIndex.map(({ title, url }) => `- [${title}](${url})`),
'',
].join('\n');
- const docsBodyContent = docsBody.join('\n');
+ const docsBodyContent = [
+ '---',
+ 'Title: Ant Design Component Semantic Descriptions',
+ 'URL: https://ant.design/llms-semantic.md',
+ '---',
+ '',
+ (await fs.readFile(path.join(siteDir, 'llms-semantic.md'), 'utf-8')).trim(),
+ '',
+ ...docsBody,
+ ].join('\n');
await fs.writeFile(path.join(siteDir, 'llms.txt'), docsIndexContent);
await fs.writeFile(path.join(siteDir, 'llms-full.txt'), docsBodyContent);