VueAnt-Design搭建AI聊天对话
目录
Vue+Ant Design搭建AI聊天对话
今天在这里介绍一下 Ant Design X,这是蚂蚁设计团队推出的一款专注于人工智能(AI)领域的组件库,主要面向 React 生态系统(目前支持Openai,通义千问)。官方也推出了ant-design-x-vue 面向 Vue。当然我们今天的主题也是使用 Vue 搭建。
搭建 Vue 3 项目
1. 创建项目
首先,我们先创建一个 Vue 3 项目,这里使用 Vite 创建:
npm create vite@latest
随后跟着提示选择 Vue 即可。使用编辑器打开项目后,在终端进行安装依赖并运行:
npm i
npm run dev
这样我们的 Vue 3 demo 就搭建好了。
2. 安装依赖
接下来开始安装
ant-design-x-vue
需要的依赖:
npm i ant-design-vue
npm i ant-design-x-vue
npm i @vitejs/plugin-vue-jsx
3. 创建项目模板(可选)
npm create vite@latest my-vue-macros -- --template vue-ts
配置文件
1. 配置 vite.config.ts
import { defineConfig } from 'vite';
import VueMacros from 'unplugin-vue-macros/vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
// https://vite.dev/config/
export default defineConfig({
plugins: [
VueMacros({
plugins: {
vue: vue(),
vueJsx: vueJsx(),
},
}),
],
});
2. 配置 tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "bundler",
"jsx": "preserve",
"jsxImportSource": "vue"
}
}
App.vue
模板
<script setup lang="tsx">
// 引入 Ant Design X Vue 相关组件和类型
import {
Attachments,
type AttachmentsProps,
Bubble,
type BubbleListProps,
Conversations,
type ConversationsProps,
Prompts,
type PromptsProps,
Sender,
Welcome,
useXAgent,
useXChat,
} from 'ant-design-x-vue';
// 引入 Ant Design 图标组件
import {
CloudUploadOutlined,
CommentOutlined,
EllipsisOutlined,
FireOutlined,
HeartOutlined,
PaperClipOutlined,
PlusOutlined,
ReadOutlined,
ShareAltOutlined,
SmileOutlined,
} from '@ant-design/icons-vue';
// 引入 Ant Design Vue 通用组件和主题
import { Badge, Button, Space, theme } from 'ant-design-vue';
// 引入 Vue 相关 API
import { computed, ref, watch, type VNode } from 'vue';
// 定义组件名称
defineOptions({ name: 'PlaygroundIndependent' });
// 辅助函数:模拟延迟
const sleep = () => new Promise((resolve) => setTimeout(resolve, 500));
// 辅助函数:渲染标题
const renderTitle = (icon: VNode, title: string) => (
<Space align="start">
{icon}
<span>{title}</span>
</Space>
);
// 会话列表默认数据
const defaultConversationsItems = [
{
key: '0',
label: 'What is Ant Design X?',
},
];
// 占位提示词列表数据
const placeholderPromptsItems: PromptsProps['items'] = [
{
key: '1',
label: renderTitle(<FireOutlined style={{ color: '#FF4D4F' }} />, 'Hot Topics'),
description: 'What are you interested in?',
children: [
{
key: '1-1',
description: `What's new in X?`,
},
{
key: '1-2',
description: `What's AGI?`,
},
{
key: '1-3',
description: `Where is the doc?`,
},
],
},
{
key: '2',
label: renderTitle(<ReadOutlined style={{ color: '#1890FF' }} />, 'Design Guide'),
description: 'How to design a good product?',
children: [
{
key: '2-1',
icon: <HeartOutlined />,
description: `Know the well`,
},
{
key: '2-2',
icon: <SmileOutlined />,
description: `Set the AI role`,
},
{
key: '2-3',
icon: <CommentOutlined />,
description: `Express the feeling`,
},
],
},
];
// 发送者提示词列表数据
const senderPromptsItems: PromptsProps['items'] = [
{
key: '1',
description: 'Hot Topics',
icon: <FireOutlined style={{ color: '#FF4D4F' }} />,
},
{
key: '2',
description: 'Design Guide',
icon: <ReadOutlined style={{ color: '#1890FF' }} />,
},
];
// 消息气泡角色配置
const roles: BubbleListProps['roles'] = {
ai: {
placement: 'start',
typing: { step: 5, interval: 20 },
styles: {
content: {
borderRadius: '16px',
},
},
},
local: {
placement: 'end',
variant: 'shadow',
},
};
// ==================== Style ====================
// 获取主题 token
const { token } = theme.useToken();
// 计算样式对象
const styles = computed(() => {
return {
layout: {
width: '100%',
'min-width': '1000px',
height: '722px',
'border-radius': `${token.value.borderRadius}px`,
display: 'flex',
background: `${token.value.colorBgContainer}`,
'font-family': `AlibabaPuHuiTi, ${token.value.fontFamily}, sans-serif`,
},
menu: {
background: `${token.value.colorBgLayout}80`,
width: '280px',
height: '100%',
display: 'flex',
'flex-direction': 'column',
},
conversations: {
padding: '0 12px',
flex: 1,
'overflow-y': 'auto',
},
chat: {
height: '100%',
width: '100%',
'max-width': '700px',
margin: '0 auto',
'box-sizing': 'border-box',
display: 'flex',
'flex-direction': 'column',
padding: `${token.value.paddingLG}px`,
gap: '16px',
},
messages: {
flex: 1,
},
placeholder: {
'padding-top': '32px',
},
sender: {
'box-shadow': token.value.boxShadow,
},
logo: {
display: 'flex',
height: '72px',
'align-items': 'center',
'justify-content': 'start',
padding: '0 24px',
'box-sizing': 'border-box',
},
'logo-img': {
width: '24px',
height: '24px',
display: 'inline-block',
},
'logo-span': {
display: 'inline-block',
margin: '0 8px',
'font-weight': 'bold',
color: token.value.colorText,
'font-size': '16px',
},
addBtn: {
background: '#1677ff0f',
border: '1px solid #1677ff34',
width: 'calc(100% - 24px)',
margin: '0 12px 24px 12px',
},
} as const;
});
// ==================== State ====================
// 头部展开状态
const headerOpen = ref(false);
// 输入框内容
const content = ref('');
// 会话列表数据
const conversationsItems = ref(defaultConversationsItems);
// 当前激活的会话 key
const activeKey = ref(defaultConversationsItems[0].key);
// 附件列表
const attachedFiles = ref<AttachmentsProps['items']>([]);
// 代理请求加载状态
const agentRequestLoading = ref(false);
// ==================== Runtime ====================
// 使用 useXAgent 初始化代理
const [agent] = useXAgent({
request: async ({ message }, { onSuccess }) => {
agentRequestLoading.value = true;
await sleep();
agentRequestLoading.value = false;
onSuccess(`Mock success return. You said: ${message}`);
},
});
// 使用 useXChat 管理聊天逻辑
const { onRequest, messages, setMessages } = useXChat({
agent: agent.value,
});
// 监听激活会话变化,清空消息列表
watch(activeKey, () => {
if (activeKey.value!== undefined) {
setMessages([]);
}
}, { immediate: true });
// ==================== Event ====================
// 提交消息处理函数
const onSubmit = (nextContent: string) => {
if (!nextContent) return;
onRequest(nextContent);
content.value = '';
};
// 提示词项点击处理函数
const onPromptsItemClick: PromptsProps['onItemClick'] = (info) => {
onRequest(info.data.description as string);
};
// 添加会话处理函数
const onAddConversation = () => {
conversationsItems.value = [
...conversationsItems.value,
{
key: `${conversationsItems.value.length}`,
label: `New Conversation ${conversationsItems.value.length}`,
},
];
activeKey.value = `${conversationsItems.value.length}`;
};
// 会话点击处理函数
const onConversationClick: ConversationsProps['onActiveChange'] = (key) => {
activeKey.value = key;
};
// 附件变化处理函数
const handleFileChange: AttachmentsProps['onChange'] = (info) =>
attachedFiles.value = info.fileList;
// ==================== Nodes ====================
// 占位节点计算属性
const placeholderNode = computed(() => (
<Space direction="vertical" size={16} style={styles.value.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>
}
/>
<Prompts
title="Do you want?"
items={placeholderPromptsItems}
styles={{
list: {
width: '100%',
},
item: {
flex: 1,
},
}}
onItemClick={onPromptsItemClick}
/>
</Space>
));
// 消息列表项计算属性
const items = computed<BubbleListProps['items']>(() => messages.value.map(({ id, message, status }) => ({
key: id,
loading: status === 'loading',
role: status === 'local'? 'local' : 'ai',
content: message,
})));
// 附件节点计算属性
const attachmentsNode = computed(() => (
<Badge dot={attachedFiles.value.length > 0 &&!headerOpen.value}>
<Button type="text" icon={<PaperClipOutlined />} onClick={() => headerOpen.value =!headerOpen.value} />
</Badge>
));
// 发送者头部节点计算属性
const senderHeader = computed(() => (
<Sender.Header
title="Attachments"
open={headerOpen.value}
onOpenChange={(open) => headerOpen.value = open}
styles={{
content: {
padding: 0,
},
}}
>
<Attachments
beforeUpload={() => false}
items={attachedFiles.value}
onChange={handleFileChange}
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 logoNode = computed(() => (
<div style={styles.value.logo}>
<img
src="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*eco6RrQhxbMAAAAAAAAAAAAADgCCAQ/original"
draggable={false}
alt="logo"
style={styles.value['logo-img']}
/>
<span style={styles.value['logo-span']}>Ant Design X Vue</span>
</div>
));
// 定义渲染函数
defineRender(() => {
return (
<div style={styles.value.layout}>
<div style={styles.value.menu}>
{/* 🌟 Logo */}
{logoNode.value}
{/* 🌟 添加会话 */}
<Button
onClick={onAddConversation}
type="link"
style={styles.value.addBtn}
icon={<PlusOutlined />}
>
New Conversation
</Button>
{/* 🌟 会话管理 */}
<Conversations
items={conversationsItems.value}
style={styles.value.conversations}
activeKey={activeKey.value}
onActiveChange={onConversationClick}
/>
</div>
<div style={styles.value.chat}>
{/* 🌟 消息列表 */}
<Bubble.List
items={items.value.length > 0? items.value : [{ content: placeholderNode.value, variant: 'borderless' }]}
roles={roles}
style={styles.value.messages}
/>
{/* 🌟 提示词 */}
<Prompts style={{ color: token.value.colorText }} items={senderPromptsItems} onItemClick={onPromptsItemClick} />
{/* 🌟 输入框 */}
<Sender
value={content.value}
header={senderHeader.value}
onSubmit={onSubmit}
onChange={(value) => content.value = value}
prefix={attachmentsNode.value}
loading={agentRequestLoading.value}
style={styles.value.sender}
/>
</div>
</div>
);
});
</script>
vue文档:
https://antd-design-x-vue.netlify.app/component/prompts.html
Ant Design X文档(React):
https://ant-design-x.antgroup.com/index-cn?theme=compact