1、创建umi项目
$ npx create-umi@latest
Need to install the following packages:
create-umi@latest
Ok to proceed? (y) y
✔ Pick Umi App Template › Simple App
✔ Pick Npm Client › npm
✔ Pick Npm Registry › taobao
Write: .gitignore
Write: .npmrc
Write: .umirc.ts
Write: package.json
Copy: src/assets/yay.jpg
Copy: src/layouts/index.less
Write: src/layouts/index.tsx
Copy: src/pages/docs.tsx
Copy: src/pages/index.tsx
Write: tsconfig.json
Copy: typings.d.ts
> postinstall
> umi setup
2、运行umi程序,localhost:8000,执行umi程序
3、在umi中加入ant design x
4、umi的路由程序
在.umirc.ts文件中
5、增加页面
增加antdesignx的助手式界面
import {
AppstoreAddOutlined,
CloseOutlined,
CloudUploadOutlined,
CommentOutlined,
CopyOutlined,
DislikeOutlined,
LikeOutlined,
OpenAIFilled,
PaperClipOutlined,
PlusOutlined,
ProductOutlined,
ReloadOutlined,
ScheduleOutlined,
} from '@ant-design/icons';
import {
Attachments,
type AttachmentsProps,
Bubble,
Conversations,
Prompts,
Sender,
Suggestion,
Welcome,
useXAgent,
useXChat,
} from '@ant-design/x';
import type { BubbleDataType } from '@ant-design/x/es/bubble/BubbleList';
import type { Conversation } from '@ant-design/x/es/conversations';
import { Button, GetProp, GetRef, Image, Popover, Space, Spin, message } from 'antd';
import { createStyles } from 'antd-style';
import dayjs from 'dayjs';
import React, { useEffect, useRef, useState } from 'react';
const MOCK_SESSION_LIST = [
{
key: '5',
label: 'New session',
group: 'Today',
},
{
key: '4',
label: 'What has Ant Design X upgraded?',
group: 'Today',
},
{
key: '3',
label: 'New AGI Hybrid Interface',
group: 'Today',
},
{
key: '2',
label: 'How to quickly install and import components?',
group: 'Yesterday',
},
{
key: '1',
label: 'What is Ant Design X?',
group: 'Yesterday',
},
];
const MOCK_SUGGESTIONS = [
{ label: 'Write a report', value: 'report' },
{ label: 'Draw a picture', value: 'draw' },
{
label: 'Check some knowledge',
value: 'knowledge',
icon: <OpenAIFilled />,
children: [
{ label: 'About React', value: 'react' },
{ label: 'About Ant Design', value: 'antd' },
],
},
];
const MOCK_QUESTIONS = [
'What has Ant Design X upgraded?',
'What components are in Ant Design X?',
'How to quickly install and import components?',
];
const AGENT_PLACEHOLDER = 'Generating content, please wait...';
const useCopilotStyle = createStyles(({ token, css }) => {
return {
copilotChat: css`
display: flex;
flex-direction: column;
background: ${token.colorBgContainer};
color: ${token.colorText};
`,
// chatHeader 样式
chatHeader: css`
height: 52px;
box-sizing: border-box;
border-bottom: 1px solid ${token.colorBorder};
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px 0 16px;
`,
headerTitle: css`
font-weight: 600;
font-size: 15px;
`,
headerButton: css`
font-size: 18px;
`,
conversations: css`
width: 300px;
.ant-conversations-list {
padding-inline-start: 0;
}
`,
// chatList 样式
chatList: css`
overflow: auto;
padding: 16px;
flex: 1;
`,
chatWelcome: css`
padding: 12px 16px;
border-radius: 2px 12px 12px 12px;
background: ${token.colorBgTextHover};
margin-bottom: 16px;
`,
loadingMessage: css`
background-image: linear-gradient(90deg, #ff6b23 0%, #af3cb8 31%, #53b6ff 89%);
background-size: 100% 2px;
background-repeat: no-repeat;
background-position: bottom;
`,
// chatSend 样式
chatSend: css`
padding: 12px;
`,
sendAction: css`
display: flex;
align-items: center;
margin-bottom: 12px;
gap: 8px;
`,
speechButton: css`
font-size: 18px;
color: ${token.colorText} !important;
`,
};
});
interface CopilotProps {
copilotOpen: boolean;
setCopilotOpen: (open: boolean) => void;
}
const Copilot = (props: CopilotProps) => {
const { copilotOpen, setCopilotOpen } = props;
const { styles } = useCopilotStyle();
const attachmentsRef = useRef<GetRef<typeof Attachments>>(null);
const abortController = useRef<AbortController>(null);
// ==================== State ====================
const [messageHistory, setMessageHistory] = useState<Record<string, any>>({});
const [sessionList, setSessionList] = useState<Conversation[]>(MOCK_SESSION_LIST);
const [curSession, setCurSession] = useState(sessionList[0].key);
const [attachmentsOpen, setAttachmentsOpen] = useState(false);
const [files, setFiles] = useState<GetProp<AttachmentsProps, 'items'>>([]);
const [inputValue, setInputValue] = useState('');
// ==================== Runtime ====================
const [agent] = useXAgent<BubbleDataType>({
baseURL: 'https://api.siliconflow.cn/v1/chat/completions',
model: 'deepseek-ai/DeepSeek-R1-Distill-Qwen-7B',
dangerouslyApiKey: 'Bearer sk-ravoadhrquyrkvaqsgyeufqdgphwxfheifujmaoscudjgldr',
});
const loading = agent.isRequesting();
const { messages, onRequest, setMessages } = useXChat({
agent,
requestFallback: (_, { error }) => {
if (error.name === 'AbortError') {
return {
content: 'Request is aborted',
role: 'assistant',
};
}
return {
content: 'Request failed, please try again!',
role: 'assistant',
};
},
transformMessage: (info) => {
const { originMessage, chunk } = info || {};
let currentText = '';
try {
if (chunk?.data && !chunk?.data.includes('DONE')) {
const message = JSON.parse(chunk?.data);
currentText = !message?.choices?.[0].delta?.reasoning_content
? ''
: message?.choices?.[0].delta?.reasoning_content;
}
} catch (error) {
console.error(error);
}
return {
content: (originMessage?.content || '') + currentText,
role: 'assistant',
};
},
resolveAbortController: (controller) => {
abortController.current = controller;
},
});
// ==================== Event ====================
const handleUserSubmit = (val: string) => {
onRequest({
stream: true,
message: { content: val, role: 'user' },
});
// session title mock
if (sessionList.find((i) => i.key === curSession)?.label === 'New session') {
setSessionList(
sessionList.map((i) => (i.key !== curSession ? i : { ...i, label: val?.slice(0, 20) })),
);
}
};
const onPasteFile = (_: File, files: FileList) => {
for (const file of files) {
attachmentsRef.current?.upload(file);
}
setAttachmentsOpen(true);
};
// ==================== Nodes ====================
const chatHeader = (
<div className={styles.chatHeader}>
<div className={styles.headerTitle}>✨ AI Copilot</div>
<Space size={0}>
<Button
type="text"
icon={<PlusOutlined />}
onClick={() => {
if (messages?.length) {
const timeNow = dayjs().valueOf().toString();
abortController.current?.abort();
// The abort execution will trigger an asynchronous requestFallback, which may lead to timing issues.
// In future versions, the sessionId capability will be added to resolve this problem.
setTimeout(() => {
setSessionList([
{ key: timeNow, label: 'New session', group: 'Today' },
...sessionList,
]);
setCurSession(timeNow);
setMessages([]);
}, 100);
} else {
message.error('It is now a new conversation.');
}
}}
className={styles.headerButton}
/>
<Popover
placement="bottom"
styles={{ body: { padding: 0, maxHeight: 600 } }}
content={
<Conversations
items={sessionList?.map((i) =>
i.key === curSession ? { ...i, label: `[current] ${i.label}` } : i,
)}
activeKey={curSession}
groupable
onActiveChange={async (val) => {
abortController.current?.abort();
// The abort execution will trigger an asynchronous requestFallback, which may lead to timing issues.
// In future versions, the sessionId capability will be added to resolve this problem.
setTimeout(() => {
setCurSession(val);
setMessages(messageHistory?.[val] || []);
}, 100);
}}
styles={{ item: { padding: '0 8px' } }}
className={styles.conversations}
/>
}
>
<Button type="text" icon={<CommentOutlined />} className={styles.headerButton} />
</Popover>
<Button
type="text"
icon={<CloseOutlined />}
onClick={() => setCopilotOpen(false)}
className={styles.headerButton}
/>
</Space>
</div>
);
const chatList = (
<div className={styles.chatList}>
{messages?.length ? (
/** 消息列表 */
<Bubble.List
style={{ height: '100%' }}
items={messages?.map((i) => ({
...i.message,
classNames: {
content: i.status === 'loading' ? styles.loadingMessage : '',
},
typing: i.status === 'loading' ? { step: 5, interval: 20, suffix: <>💗</> } : false,
}))}
roles={{
assistant: {
placement: 'start',
footer: (
<div style={{ display: 'flex' }}>
<Button type="text" size="small" icon={<ReloadOutlined />} />
<Button type="text" size="small" icon={<CopyOutlined />} />
<Button type="text" size="small" icon={<LikeOutlined />} />
<Button type="text" size="small" icon={<DislikeOutlined />} />
</div>
),
loadingRender: () => (
<Space>
<Spin size="small" />
{AGENT_PLACEHOLDER}
</Space>
),
},
user: { placement: 'end' },
}}
/>
) : (
/** 没有消息时的 welcome */
<>
<Welcome
variant="borderless"
title="👋 Hello, I'm Ant Design X"
description="Base on Ant Design, AGI product interface solution, create a better intelligent vision~"
className={styles.chatWelcome}
/>
<Prompts
vertical
title="I can help:"
items={MOCK_QUESTIONS.map((i) => ({ key: i, description: i }))}
onItemClick={(info) => handleUserSubmit(info?.data?.description as string)}
styles={{
title: { fontSize: '14px' },
}}
/>
</>
)}
</div>
);
const sendHeader = (
<Sender.Header
title="Upload File"
styles={{ content: { padding: 0 } }}
open={attachmentsOpen}
onOpenChange={setAttachmentsOpen}
forceRender
>
<Attachments
ref={attachmentsRef}
beforeUpload={() => false}
items={files}
onChange={({ fileList }) => setFiles(fileList)}
placeholder={(type) =>
type === 'drop'
? { title: 'Drop file here' }
: {
icon: <CloudUploadOutlined />,
title: 'Upload files',
description: 'Click or drag files to this area to upload',
}
}
/>
</Sender.Header>
);
const chatSender = (
<div className={styles.chatSend}>
<div className={styles.sendAction}>
<Button
icon={<ScheduleOutlined />}
onClick={() => handleUserSubmit('What has Ant Design X upgraded?')}
>
Upgrades
</Button>
<Button
icon={<ProductOutlined />}
onClick={() => handleUserSubmit('What component assets are available in Ant Design X?')}
>
Components
</Button>
<Button icon={<AppstoreAddOutlined />}>More</Button>
</div>
{/** 输入框 */}
<Suggestion items={MOCK_SUGGESTIONS} onSelect={(itemVal) => setInputValue(`[${itemVal}]:`)}>
{({ onTrigger, onKeyDown }) => (
<Sender
loading={loading}
value={inputValue}
onChange={(v) => {
onTrigger(v === '/');
setInputValue(v);
}}
onSubmit={() => {
handleUserSubmit(inputValue);
setInputValue('');
}}
onCancel={() => {
abortController.current?.abort();
}}
allowSpeech
placeholder="Ask or input / use skills"
onKeyDown={onKeyDown}
header={sendHeader}
prefix={
<Button
type="text"
icon={<PaperClipOutlined style={{ fontSize: 18 }} />}
onClick={() => setAttachmentsOpen(!attachmentsOpen)}
/>
}
onPasteFile={onPasteFile}
actions={(_, info) => {
const { SendButton, LoadingButton, SpeechButton } = info.components;
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<SpeechButton className={styles.speechButton} />
{loading ? <LoadingButton type="default" /> : <SendButton type="primary" />}
</div>
);
}}
/>
)}
</Suggestion>
</div>
);
useEffect(() => {
// history mock
if (messages?.length) {
setMessageHistory((prev) => ({
...prev,
[curSession]: messages,
}));
}
}, [messages]);
return (
<div className={styles.copilotChat} style={{ width: copilotOpen ? 400 : 0 }}>
{/** 对话区 - header */}
{chatHeader}
{/** 对话区 - 消息列表 */}
{chatList}
{/** 对话区 - 输入框 */}
{chatSender}
</div>
);
};
const useWorkareaStyle = createStyles(({ token, css }) => {
return {
copilotWrapper: css`
min-width: 1000px;
height: 100vh;
display: flex;
`,
workarea: css`
flex: 1;
background: ${token.colorBgLayout};
display: flex;
flex-direction: column;
`,
workareaHeader: css`
box-sizing: border-box;
height: 52px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 48px 0 28px;
border-bottom: 1px solid ${token.colorBorder};
`,
headerTitle: css`
font-weight: 600;
font-size: 15px;
color: ${token.colorText};
display: flex;
align-items: center;
gap: 8px;
`,
headerButton: css`
background-image: linear-gradient(78deg, #8054f2 7%, #3895da 95%);
border-radius: 12px;
height: 24px;
width: 93px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: all 0.3s;
&:hover {
opacity: 0.8;
}
`,
workareaBody: css`
flex: 1;
padding: 16px;
background: ${token.colorBgContainer};
border-radius: 16px;
min-height: 0;
`,
bodyContent: css`
overflow: auto;
height: 100%;
padding-right: 10px;
`,
bodyText: css`
color: ${token.colorText};
padding: 8px;
`,
};
});
const CopilotDemo = () => {
const { styles: workareaStyles } = useWorkareaStyle();
// ==================== State =================
const [copilotOpen, setCopilotOpen] = useState(true);
// ==================== Render =================
return (
<div className={workareaStyles.copilotWrapper}>
{/** 左侧工作区 */}
<div className={workareaStyles.workarea}>
<div className={workareaStyles.workareaHeader}>
<div className={workareaStyles.headerTitle}>
<img
src="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*eco6RrQhxbMAAAAAAAAAAAAADgCCAQ/original"
draggable={false}
alt="logo"
width={20}
height={20}
/>
Ant Design X
</div>
{!copilotOpen && (
<div onClick={() => setCopilotOpen(true)} className={workareaStyles.headerButton}>
✨ AI Copilot
</div>
)}
</div>
<div
className={workareaStyles.workareaBody}
style={{ margin: copilotOpen ? 16 : '16px 48px' }}
>
<div className={workareaStyles.bodyContent}>
<Image
src="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*48RLR41kwHIAAAAAAAAAAAAADgCCAQ/fmt.webp"
preview={false}
/>
<div className={workareaStyles.bodyText}>
<h4>What is the RICH design paradigm?</h4>
<div>
RICH is an AI interface design paradigm we propose, similar to how the WIMP paradigm
relates to graphical user interfaces.
</div>
<br />
<div>
The ACM SIGCHI 2005 (the premier conference on human-computer interaction) defined
that the core issues of human-computer interaction can be divided into three levels:
</div>
<ul>
<li>
Interface Paradigm Layer: Defines the design elements of human-computer
interaction interfaces, guiding designers to focus on core issues.
</li>
<li>
User model layer: Build an interface experience evaluation model to measure the
quality of the interface experience.
</li>
<li>
Software framework layer: The underlying support algorithms and data structures
for human-computer interfaces, which are the contents hidden behind the front-end
interface.
</li>
</ul>
<div>
The interface paradigm is the aspect that designers need to focus on and define the
most when a new human-computer interaction technology is born. The interface
paradigm defines the design elements that designers should pay attention to, and
based on this, it is possible to determine what constitutes good design and how to
achieve it.
</div>
</div>
</div>
</div>
</div>
{/** 右侧对话区 */}
<Copilot copilotOpen={copilotOpen} setCopilotOpen={setCopilotOpen} />
</div>
);
};
export default CopilotDemo;
6、增加智能问答界面
import {
AppstoreAddOutlined,
CloudUploadOutlined,
CommentOutlined,
CopyOutlined,
DeleteOutlined,
DislikeOutlined,
EditOutlined,
EllipsisOutlined,
FileSearchOutlined,
HeartOutlined,
LikeOutlined,
PaperClipOutlined,
PlusOutlined,
ProductOutlined,
QuestionCircleOutlined,
ReloadOutlined,
ScheduleOutlined,
ShareAltOutlined,
SmileOutlined,
} from '@ant-design/icons';
import {
Attachments,
Bubble,
Conversations,
Prompts,
Sender,
Welcome,
useXAgent,
useXChat,
} from '@ant-design/x';
import type { BubbleDataType } from '@ant-design/x/es/bubble/BubbleList';
import { Avatar, Button, Flex, type GetProp, Space, Spin, message } from 'antd';
import { createStyles } from 'antd-style';
import dayjs from 'dayjs';
import React, { useEffect, useRef, useState } from 'react';
const DEFAULT_CONVERSATIONS_ITEMS = [
{
key: 'default-0',
label: 'What is Ant Design X?',
group: 'Today',
},
{
key: 'default-1',
label: 'How to quickly install and import components?',
group: 'Today',
},
{
key: 'default-2',
label: 'New AGI Hybrid Interface',
group: 'Yesterday',
},
];
const HOT_TOPICS = {
key: '1',
label: 'Hot Topics',
children: [
{
key: '1-1',
description: 'What has Ant Design X upgraded?',
icon: <span style={{ color: '#f93a4a', fontWeight: 700 }}>1</span>,
},
{
key: '1-2',
description: 'New AGI Hybrid Interface',
icon: <span style={{ color: '#ff6565', fontWeight: 700 }}>2</span>,
},
{
key: '1-3',
description: 'What components are in Ant Design X?',
icon: <span style={{ color: '#ff8f1f', fontWeight: 700 }}>3</span>,
},
{
key: '1-4',
description: 'Come and discover the new design paradigm of the AI era.',
icon: <span style={{ color: '#00000040', fontWeight: 700 }}>4</span>,
},
{
key: '1-5',
description: 'How to quickly install and import components?',
icon: <span style={{ color: '#00000040', fontWeight: 700 }}>5</span>,
},
],
};
const DESIGN_GUIDE = {
key: '2',
label: 'Design Guide',
children: [
{
key: '2-1',
icon: <HeartOutlined />,
label: 'Intention',
description: 'AI understands user needs and provides solutions.',
},
{
key: '2-2',
icon: <SmileOutlined />,
label: 'Role',
description: "AI's public persona and image",
},
{
key: '2-3',
icon: <CommentOutlined />,
label: 'Chat',
description: 'How AI Can Express Itself in a Way Users Understand',
},
{
key: '2-4',
icon: <PaperClipOutlined />,
label: 'Interface',
description: 'AI balances "chat" & "do" behaviors.',
},
],
};
const SENDER_PROMPTS: GetProp<typeof Prompts, 'items'> = [
{
key: '1',
description: 'Upgrades',
icon: <ScheduleOutlined />,
},
{
key: '2',
description: 'Components',
icon: <ProductOutlined />,
},
{
key: '3',
description: 'RICH Guide',
icon: <FileSearchOutlined />,
},
{
key: '4',
description: 'Installation Introduction',
icon: <AppstoreAddOutlined />,
},
];
const useStyle = createStyles(({ token, css }) => {
return {
layout: css`
width: 100%;
min-width: 1000px;
height: 100vh;
display: flex;
background: ${token.colorBgContainer};
font-family: AlibabaPuHuiTi, ${token.fontFamily}, sans-serif;
`,
// sider 样式
sider: css`
background: ${token.colorBgLayout}80;
width: 280px;
height: 100%;
display: flex;
flex-direction: column;
padding: 0 12px;
box-sizing: border-box;
`,
logo: css`
display: flex;
align-items: center;
justify-content: start;
padding: 0 24px;
box-sizing: border-box;
gap: 8px;
margin: 24px 0;
span {
font-weight: bold;
color: ${token.colorText};
font-size: 16px;
}
`,
addBtn: css`
background: #1677ff0f;
border: 1px solid #1677ff34;
height: 40px;
`,
conversations: css`
flex: 1;
overflow-y: auto;
margin-top: 12px;
padding: 0;
.ant-conversations-list {
padding-inline-start: 0;
}
`,
siderFooter: css`
border-top: 1px solid ${token.colorBorderSecondary};
height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
`,
// chat list 样式
chat: css`
height: 100%;
width: 100%;
max-width: 700px;
margin: 0 auto;
box-sizing: border-box;
display: flex;
flex-direction: column;
padding: ${token.paddingLG}px;
gap: 16px;
`,
chatPrompt: css`
.ant-prompts-label {
color: #000000e0 !important;
}
.ant-prompts-desc {
color: #000000a6 !important;
width: 100%;
}
.ant-prompts-icon {
color: #000000a6 !important;
}
`,
chatList: css`
flex: 1;
overflow: auto;
padding-right: 10px;
`,
loadingMessage: css`
background-image: linear-gradient(90deg, #ff6b23 0%, #af3cb8 31%, #53b6ff 89%);
background-size: 100% 2px;
background-repeat: no-repeat;
background-position: bottom;
`,
placeholder: css`
padding-top: 32px;
`,
// sender 样式
sender: css`
box-shadow: ${token.boxShadow};
color: ${token.colorText};
`,
speechButton: css`
font-size: 18px;
color: ${token.colorText} !important;
`,
senderPrompt: css`
color: ${token.colorText};
`,
};
});
const Independent: React.FC = () => {
const { styles } = useStyle();
const abortController = useRef<AbortController>(null);
// ==================== State ====================
const [messageHistory, setMessageHistory] = useState<Record<string, any>>({});
const [conversations, setConversations] = useState(DEFAULT_CONVERSATIONS_ITEMS);
const [curConversation, setCurConversation] = useState(DEFAULT_CONVERSATIONS_ITEMS[0].key);
const [attachmentsOpen, setAttachmentsOpen] = useState(false);
const [attachedFiles, setAttachedFiles] = useState<GetProp<typeof Attachments, 'items'>>([]);
const [inputValue, setInputValue] = useState('');
// ==================== Runtime ====================
const [agent] = useXAgent<BubbleDataType>({
baseURL: 'https://api.siliconflow.cn/v1/chat/completions',
model: 'deepseek-ai/DeepSeek-R1-Distill-Qwen-7B',
dangerouslyApiKey: 'Bearer sk-ravoadhrquyrkvaqsgyeufqdgphwxfheifujmaoscudjgldr',
});
const loading = agent.isRequesting();
const { onRequest, messages, setMessages } = useXChat({
agent,
requestFallback: (_, { error }) => {
if (error.name === 'AbortError') {
return {
content: 'Request is aborted',
role: 'assistant',
};
}
return {
content: 'Request failed, please try again!',
role: 'assistant',
};
},
transformMessage: (info) => {
const { originMessage, chunk } = info || {};
let currentText = '';
try {
if (chunk?.data && !chunk?.data.includes('DONE')) {
const message = JSON.parse(chunk?.data);
currentText = !message?.choices?.[0].delta?.reasoning_content
? ''
: message?.choices?.[0].delta?.reasoning_content;
}
} catch (error) {
console.error(error);
}
return {
content: (originMessage?.content || '') + currentText,
role: 'assistant',
};
},
resolveAbortController: (controller) => {
abortController.current = controller;
},
});
// ==================== Event ====================
const onSubmit = (val: string) => {
if (!val) return;
if (loading) {
message.error('Request is in progress, please wait for the request to complete.');
return;
}
onRequest({
stream: true,
message: { role: 'user', content: val },
});
};
// ==================== Nodes ====================
const chatSider = (
<div className={styles.sider}>
{/* 🌟 Logo */}
<div className={styles.logo}>
<img
src="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*eco6RrQhxbMAAAAAAAAAAAAADgCCAQ/original"
draggable={false}
alt="logo"
width={24}
height={24}
/>
<span>Ant Design X</span>
</div>
{/* 🌟 添加会话 */}
<Button
onClick={() => {
const now = dayjs().valueOf().toString();
setConversations([
{
key: now,
label: `New Conversation ${conversations.length + 1}`,
group: 'Today',
},
...conversations,
]);
setCurConversation(now);
setMessages([]);
}}
type="link"
className={styles.addBtn}
icon={<PlusOutlined />}
>
New Conversation
</Button>
{/* 🌟 会话管理 */}
<Conversations
items={conversations}
className={styles.conversations}
activeKey={curConversation}
onActiveChange={async (val) => {
abortController.current?.abort();
// The abort execution will trigger an asynchronous requestFallback, which may lead to timing issues.
// In future versions, the sessionId capability will be added to resolve this problem.
setTimeout(() => {
setCurConversation(val);
setMessages(messageHistory?.[val] || []);
}, 100);
}}
groupable
styles={{ item: { padding: '0 8px' } }}
menu={(conversation) => ({
items: [
{
label: 'Rename',
key: 'rename',
icon: <EditOutlined />,
},
{
label: 'Delete',
key: 'delete',
icon: <DeleteOutlined />,
danger: true,
onClick: () => {
const newList = conversations.filter((item) => item.key !== conversation.key);
const newKey = newList?.[0]?.key;
setConversations(newList);
// The delete operation modifies curConversation and triggers onActiveChange, so it needs to be executed with a delay to ensure it overrides correctly at the end.
// This feature will be fixed in a future version.
setTimeout(() => {
if (conversation.key === curConversation) {
setCurConversation(newKey);
setMessages(messageHistory?.[newKey] || []);
}
}, 200);
},
},
],
})}
/>
<div className={styles.siderFooter}>
<Avatar size={24} />
<Button type="text" icon={<QuestionCircleOutlined />} />
</div>
</div>
);
const chatList = (
<div className={styles.chatList}>
{messages?.length ? (
/* 🌟 消息列表 */
<Bubble.List
items={messages?.map((i) => ({
...i.message,
classNames: {
content: i.status === 'loading' ? styles.loadingMessage : '',
},
typing: i.status === 'loading' ? { step: 5, interval: 20, suffix: <>💗</> } : false,
}))}
style={{ height: '100%' }}
roles={{
assistant: {
placement: 'start',
footer: (
<div style={{ display: 'flex' }}>
<Button type="text" size="small" icon={<ReloadOutlined />} />
<Button type="text" size="small" icon={<CopyOutlined />} />
<Button type="text" size="small" icon={<LikeOutlined />} />
<Button type="text" size="small" icon={<DislikeOutlined />} />
</div>
),
loadingRender: () => <Spin size="small" />,
},
user: { placement: 'end' },
}}
/>
) : (
<Space direction="vertical" size={16} className={styles.placeholder}>
<Welcome
variant="borderless"
icon="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*s5sNRo5LjfQAAAAAAAAAAAAADgCCAQ/fmt.webp"
title="Hello, I'm Ant Design X"
description="Base on Ant Design, AGI product interface solution, create a better intelligent vision~"
extra={
<Space>
<Button icon={<ShareAltOutlined />} />
<Button icon={<EllipsisOutlined />} />
</Space>
}
/>
<Flex gap={16}>
<Prompts
items={[HOT_TOPICS]}
styles={{
list: { height: '100%' },
item: {
flex: 1,
backgroundImage: 'linear-gradient(123deg, #e5f4ff 0%, #efe7ff 100%)',
borderRadius: 12,
border: 'none',
},
subItem: { padding: 0, background: 'transparent' },
}}
onItemClick={(info) => {
onSubmit(info.data.description as string);
}}
className={styles.chatPrompt}
/>
<Prompts
items={[DESIGN_GUIDE]}
styles={{
item: {
flex: 1,
backgroundImage: 'linear-gradient(123deg, #e5f4ff 0%, #efe7ff 100%)',
borderRadius: 12,
border: 'none',
},
subItem: { background: '#ffffffa6' },
}}
onItemClick={(info) => {
onSubmit(info.data.description as string);
}}
className={styles.chatPrompt}
/>
</Flex>
</Space>
)}
</div>
);
const senderHeader = (
<Sender.Header
title="Upload File"
open={attachmentsOpen}
onOpenChange={setAttachmentsOpen}
styles={{ content: { padding: 0 } }}
>
<Attachments
beforeUpload={() => false}
items={attachedFiles}
onChange={(info) => setAttachedFiles(info.fileList)}
placeholder={(type) =>
type === 'drop'
? { title: 'Drop file here' }
: {
icon: <CloudUploadOutlined />,
title: 'Upload files',
description: 'Click or drag files to this area to upload',
}
}
/>
</Sender.Header>
);
const chatSender = (
<>
{/* 🌟 提示词 */}
<Prompts
items={SENDER_PROMPTS}
onItemClick={(info) => {
onSubmit(info.data.description as string);
}}
styles={{ item: { padding: '6px 12px' } }}
className={styles.senderPrompt}
/>
{/* 🌟 输入框 */}
<Sender
value={inputValue}
header={senderHeader}
onSubmit={() => {
onSubmit(inputValue);
setInputValue('');
}}
onChange={setInputValue}
onCancel={() => {
abortController.current?.abort();
}}
prefix={
<Button
type="text"
icon={<PaperClipOutlined style={{ fontSize: 18 }} />}
onClick={() => setAttachmentsOpen(!attachmentsOpen)}
/>
}
loading={loading}
className={styles.sender}
allowSpeech
actions={(_, info) => {
const { SendButton, LoadingButton, SpeechButton } = info.components;
return (
<Flex gap={4}>
<SpeechButton className={styles.speechButton} />
{loading ? <LoadingButton type="default" /> : <SendButton type="primary" />}
</Flex>
);
}}
placeholder="Ask or input / use skills"
/>
</>
);
useEffect(() => {
// history mock
if (messages?.length) {
setMessageHistory((prev) => ({
...prev,
[curConversation]: messages,
}));
}
}, [messages]);
// ==================== Render =================
return (
<div className={styles.layout}>
{chatSider}
<div className={styles.chat}>
{chatList}
{chatSender}
</div>
</div>
);
};
export default Independent;
6、在umi中增加页面
默认页面的redirect,也要增加一个componet说明layout在哪
import { defineConfig } from 'umi';
export default defineConfig({
routes
: [
{
path
: '/',
component
: '@/pages/index',
// 使用全局布局(如果有配置)
},
{
path
: '/custom',
layout
: '@/layouts/CustomLayout', // 指定自定义布局
routes
: [
{ path: '', redirect: '/custom/page1' },
{ path: 'page1', component: '@/pages/custom/page1' },
{ path: 'page2', component: '@/pages/custom/page2' },
],
},
{
path
: '/other',
layout
: false, // 禁用布局(直接渲染页面内容)
component
: '@/pages/other',
},
],
});
@/layouts/CustomLayout'