mirror of
https://github.com/ant-design/ant-design.git
synced 2026-02-11 20:09:22 +08:00
Compare commits
1 Commits
feature
...
docs/theme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f95529432 |
@@ -4,7 +4,6 @@ import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
ColorPicker,
|
||||
ConfigProvider,
|
||||
@@ -95,7 +94,7 @@ const ComponentsBlock: React.FC<ComponentsBlockProps> = (props) => {
|
||||
|
||||
return (
|
||||
<ConfigProvider {...config}>
|
||||
<Card className={clsx(containerClassName, styles.container)}>
|
||||
<div className={clsx(containerClassName, styles.container)}>
|
||||
<App>
|
||||
<Flex vertical gap="middle" style={style} className={className}>
|
||||
<ModalPanel title="Ant Design" width="100%">
|
||||
@@ -190,7 +189,7 @@ const ComponentsBlock: React.FC<ComponentsBlockProps> = (props) => {
|
||||
</Flex>
|
||||
</Flex>
|
||||
</App>
|
||||
</Card>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { AntDesignOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import { Bubble, Sender } from '@ant-design/x';
|
||||
import type { SenderRef } from '@ant-design/x/es/sender';
|
||||
import { Drawer, Flex, Typography } from 'antd';
|
||||
import type { GetProp } from 'antd';
|
||||
import type { BubbleProps } from '@ant-design/x';
|
||||
import { Button, Drawer, Flex, Space, Splitter, Typography } from 'antd';
|
||||
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import type { SiteContextProps } from '../../../theme/slots/SiteContext';
|
||||
import useLocale from '../../../hooks/useLocale';
|
||||
import ComponentsBlock from '../../../pages/index/components/ThemePreview/ComponentsBlock';
|
||||
import usePromptTheme from './usePromptTheme';
|
||||
import usePromptRecommend from './usePromptRecommend';
|
||||
|
||||
const antdLogoSrc = 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg';
|
||||
|
||||
const locales = {
|
||||
cn: {
|
||||
title: 'AI 生成主题',
|
||||
finishTips: '生成完成,对话以重新生成。',
|
||||
placeholder: '描述你想要的主题风格,如:温暖阳光、清新自然、科技感...',
|
||||
recommendTitle: 'AI 推荐主题',
|
||||
loading: '加载中...',
|
||||
},
|
||||
en: {
|
||||
title: 'AI Theme Generator',
|
||||
finishTips: 'Completed. Regenerate by start a new conversation.',
|
||||
placeholder: 'Describe your desired theme style, e.g., warm sunny, fresh natural, tech feel...',
|
||||
recommendTitle: 'AI Recommended',
|
||||
loading: 'Loading...',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -27,12 +37,16 @@ export interface PromptDrawerProps {
|
||||
}
|
||||
|
||||
const PromptDrawer: React.FC<PromptDrawerProps> = ({ open, onClose, onThemeChange }) => {
|
||||
const [locale] = useLocale(locales);
|
||||
// @ts-ignore - useLocale returns type with proper locale key
|
||||
const locale = useLocale(locales) as typeof locales.cn;
|
||||
const localeKey = locale.title === 'AI 生成主题' ? 'cn' : 'en';
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const senderRef = useRef<SenderRef>(null);
|
||||
|
||||
const [submitPrompt, loading, prompt, resText, cancelRequest] = usePromptTheme(onThemeChange);
|
||||
const { recommendations, loading: recommendLoading, fetch: fetchRecommendations } =
|
||||
usePromptRecommend(localeKey);
|
||||
|
||||
const handleSubmit = (value: string) => {
|
||||
submitPrompt(value);
|
||||
@@ -44,9 +58,13 @@ const PromptDrawer: React.FC<PromptDrawerProps> = ({ open, onClose, onThemeChang
|
||||
// Focus the Sender component when drawer opens
|
||||
senderRef.current.focus?.();
|
||||
}
|
||||
// Fetch AI recommendations when drawer opens
|
||||
if (isOpen) {
|
||||
fetchRecommendations(`prompt-drawer-${Date.now()}`);
|
||||
}
|
||||
};
|
||||
|
||||
const items = React.useMemo<GetProp<typeof Bubble.List, 'items'>>(() => {
|
||||
const items = React.useMemo<BubbleProps['ListProps']['items']>(() => {
|
||||
if (!prompt) {
|
||||
return [];
|
||||
}
|
||||
@@ -62,10 +80,9 @@ const PromptDrawer: React.FC<PromptDrawerProps> = ({ open, onClose, onThemeChang
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
role: 'system',
|
||||
placement: 'start',
|
||||
content: resText,
|
||||
avatar: <AntDesignOutlined />,
|
||||
avatar: <img src={antdLogoSrc} alt="Ant Design" style={{ width: 28, height: 28 }} />,
|
||||
loading: !resText,
|
||||
contentRender: (content: string) => (
|
||||
<Typography>
|
||||
@@ -81,7 +98,7 @@ const PromptDrawer: React.FC<PromptDrawerProps> = ({ open, onClose, onThemeChang
|
||||
role: 'divider',
|
||||
placement: 'start',
|
||||
content: locale.finishTips,
|
||||
avatar: <AntDesignOutlined />,
|
||||
avatar: <img src={antdLogoSrc} alt="Ant Design" style={{ width: 28, height: 28 }} />,
|
||||
shape: 'corner',
|
||||
});
|
||||
}
|
||||
@@ -89,29 +106,75 @@ const PromptDrawer: React.FC<PromptDrawerProps> = ({ open, onClose, onThemeChang
|
||||
return nextItems;
|
||||
}, [prompt, resText, loading, locale.finishTips]);
|
||||
|
||||
const prompts = React.useMemo<string[]>(() => recommendations, [recommendations]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={locale.title}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
size={480}
|
||||
width="75vw"
|
||||
placement="right"
|
||||
afterOpenChange={handleAfterOpenChange}
|
||||
>
|
||||
<Flex vertical style={{ height: '100%' }}>
|
||||
<Bubble.List style={{ flex: 1, overflow: 'auto' }} items={items} />
|
||||
<Sender
|
||||
ref={senderRef}
|
||||
style={{ flex: 0 }}
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
onSubmit={handleSubmit}
|
||||
loading={loading}
|
||||
onCancel={cancelRequest}
|
||||
/>
|
||||
</Flex>
|
||||
<Splitter style={{ height: '100%' }}>
|
||||
{/* 左侧预览区域 */}
|
||||
<Splitter.Panel defaultSize="50%" min="30%" max="70%">
|
||||
<Flex vertical style={{ height: '100%', padding: '0 8px' }}>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '24px 8px 8px',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<ComponentsBlock className="prompt-drawer-preview" />
|
||||
</div>
|
||||
</Flex>
|
||||
</Splitter.Panel>
|
||||
|
||||
{/* 右侧对话区域 */}
|
||||
<Splitter.Panel defaultSize="50%" min="30%" max="70%">
|
||||
<Flex vertical gap={12} style={{ height: '100%', padding: '0 8px' }}>
|
||||
{!prompt && (
|
||||
<Flex vertical gap={8}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{locale.recommendTitle}
|
||||
</Typography.Text>
|
||||
<Space wrap size="small">
|
||||
{recommendLoading && (
|
||||
<Button size="small" disabled>
|
||||
{locale.loading}
|
||||
</Button>
|
||||
)}
|
||||
{prompts.map((theme) => (
|
||||
<Button
|
||||
key={theme}
|
||||
size="small"
|
||||
onClick={() => handleSubmit(theme)}
|
||||
style={{ borderRadius: 6 }}
|
||||
>
|
||||
{theme}
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
</Flex>
|
||||
)}
|
||||
<Bubble.List style={{ flex: 1, overflow: 'auto' }} items={items} />
|
||||
<Sender
|
||||
ref={senderRef}
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
onSubmit={handleSubmit}
|
||||
loading={loading}
|
||||
onCancel={cancelRequest}
|
||||
placeholder={locale.placeholder}
|
||||
/>
|
||||
</Flex>
|
||||
</Splitter.Panel>
|
||||
</Splitter>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptDrawer;
|
||||
export default PromptDrawer;
|
||||
124
.dumi/theme/common/ThemeSwitch/usePromptRecommend.ts
Normal file
124
.dumi/theme/common/ThemeSwitch/usePromptRecommend.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { XStream } from '@ant-design/x-sdk';
|
||||
import { useRef, useState } from 'react';
|
||||
cn: {
|
||||
recommendPrompt:
|
||||
'请推荐 6 个适合 Ant Design 组件库的主题风格的名称,每个名称 4-6 个字,用逗号分隔。只返回主题名称,不要其他内容。',
|
||||
},
|
||||
en: {
|
||||
recommendPrompt:
|
||||
'Recommend 6 theme style names for Ant Design component library, each name 2-4 words, separated by commas. Return only theme names, nothing else.',
|
||||
},
|
||||
};
|
||||
|
||||
const fetchRecommendations = async (
|
||||
localeKey: keyof typeof locales,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<string[]> => {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: locales[localeKey].recommendPrompt,
|
||||
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 || '';
|
||||
}
|
||||
}
|
||||
|
||||
// Parse theme names from response
|
||||
const text = fullContent.replace(/```json\s*|\s*```/g, '').trim();
|
||||
const items = text.split(/,|,|\n/).map((item) => item.trim()).filter(Boolean);
|
||||
const result = items.slice(0, 6);
|
||||
|
||||
// If parsing failed or got no results, use fallback
|
||||
if (result.length === 0) {
|
||||
if (localeKey === 'cn') {
|
||||
return ['禅意简约', '科技蓝调', '温暖暖橙', '清新森绿', '优雅紫罗兰', '深色暗夜'];
|
||||
}
|
||||
return ['Zen Minimal', 'Tech Blue', 'Warm Orange', 'Fresh Green', 'Elegant Purple', 'Dark Night'];
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error in fetchRecommendations:', error);
|
||||
// Return fallback themes
|
||||
return localeKey === 'cn'
|
||||
? ['禅意简约', '科技蓝调', '温暖暖橙', '清新森绿', '优雅紫罗兰', '深色暗夜']
|
||||
: ['Zen Minimal', 'Tech Blue', 'Warm Orange', 'Fresh Green', 'Elegant Purple', 'Dark Night'];
|
||||
}
|
||||
};
|
||||
|
||||
export default function usePromptRecommend(localeKey: keyof typeof locales = 'cn') {
|
||||
const [recommendations, setRecommendations] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const fetchedKeyRef = useRef<string>('');
|
||||
|
||||
const fetch = async (key: string) => {
|
||||
if (fetchedKeyRef.current === key) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel previous request if it exists
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const items = await fetchRecommendations(localeKey, abortController.signal);
|
||||
setRecommendations(items);
|
||||
fetchedKeyRef.current = key;
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error && error.name === 'AbortError')) {
|
||||
console.error('Failed to fetch recommendations:', error);
|
||||
// Use fallback on error
|
||||
setRecommendations(
|
||||
localeKey === 'cn'
|
||||
? ['禅意简约', '科技蓝调', '温暖暖橙', '清新森绿', '优雅紫罗兰', '深色暗夜']
|
||||
: ['Zen Minimal', 'Tech Blue', 'Warm Orange', 'Fresh Green', 'Elegant Purple', 'Dark Night'],
|
||||
);
|
||||
fetchedKeyRef.current = key;
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
return { recommendations, loading, fetch };
|
||||
}
|
||||
Reference in New Issue
Block a user