Compare commits

...

1 Commits

Author SHA1 Message Date
afc163
8f95529432 commit 2026-02-11 18:52:17 +08:00
3 changed files with 212 additions and 26 deletions

View File

@@ -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>
);
};

View File

@@ -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;

View 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 };
}