目录

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

运行结果

https://i-blog.csdnimg.cn/direct/8e62ba9561f049edb44b5e96f086bea8.png#pic_center