智能对话框的开发实践

76 阅读2分钟

基本功能:

# 简单聊天&用户与 AI 检索代理之间的流式对话

#聊天框支持操作以及文件导入

#对外接入openai key

使用技术站:ant-design/x

核心代码块
"use client";import {  Attachments,  Bubble,  Prompts,  Sender,  Welcome,  useXAgent,  useXChat,} from '@ant-design/x';import React from 'react';import {  CloudUploadOutlined,  CommentOutlined,  EllipsisOutlined,  FireOutlined,  HeartOutlined,  PaperClipOutlined,  ReadOutlined,  ShareAltOutlined,  SmileOutlined,} from '@ant-design/icons';import { Badge, Button, type GetProp, Space } from 'antd';import { toast } from 'react-toastify';import 'react-toastify/dist/ReactToastify.css';import { useChat } from "ai/react";import { useState } from "react";import type { FormEvent } from "react";import styles from '../app/css/page.module.css'export function ChatWindow(props: {  endpoint: string,  placeholder?: string,  activeKey: string,}) {  const renderTitle = (icon: React.ReactElement, title: string) => (    <Space align="start">      {icon}      <span>{title}</span>    </Space>  );  const placeholderPromptsItems: GetProp<typeof Prompts, '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: GetProp<typeof Prompts, 'items'> = [    {      key: '1',      description: 'Hot Topics',      icon: <FireOutlined style={{ color: '#FF4D4F' }} />,    },    {      key: '2',      description: 'Design Guide',      icon: <ReadOutlined style={{ color: '#1890FF' }} />,    },  ];  const roles: GetProp<typeof Bubble.List, 'roles'> = {    ai: {      placement: 'start',      typing: { step: 5, interval: 20 },      styles: {        content: {          borderRadius: 16,        },      },    },    local: {      placement: 'end',      variant: 'shadow',    },  };  // ==================== Props ====================  const { endpoint, placeholder, activeKey } = props;  // ==================== State ====================  const [headerOpen, setHeaderOpen] = React.useState(false);  const [content, setContent] = React.useState('');  const [attachedFiles, setAttachedFiles] = React.useState<GetProp<typeof Attachments, 'items'>>(    [],  );  const [sourcesForMessages, setSourcesForMessages] = useState<Record<string, any>>({});  // ==================== Runtime ====================  const [agent] = useXAgent({    request: async ({ message }, { onSuccess }) => {      onSuccess(`Mock success return. You said: ${message}`);    },  });  const { onRequest, messages: messages_chat, setMessages: setMessages__chat } = useXChat({    agent,  });  const { messages, input, setInput, handleInputChange, handleSubmit, isLoading: chatEndpointIsLoading, setMessages } =    useChat({      api: endpoint,      onResponse(response: Response) {        // TODO 响应后处理        const sourcesHeader = response.headers.get("x-sources");        const sources = sourcesHeader ? JSON.parse((Buffer.from(sourcesHeader, 'base64')).toString('utf8')) : [];        const messageIndexHeader = response.headers.get("x-message-index");        if (sources.length && messageIndexHeader !== null) {          setSourcesForMessages({ ...sourcesForMessages, [messageIndexHeader]: sources });        }      },      streamMode: "text",      onError: (e: Error) => {        toast(e.message, {          theme: "dark"        });      }    });  // ==================== Adapter ====================  // 创建提交事件适配器  function createSubmitEvent(value?: string): FormEvent<HTMLFormElement> {    return {      preventDefault: () => { },      stopPropagation: () => { },      target: {        value: value || ''      }    } as unknown as FormEvent<HTMLFormElement>;  }  // 创建输出变更适配器  function createChangeEvent(value: string): React.ChangeEvent<HTMLInputElement> {    return {      target: { value },      currentTarget: { value },      persist: () => { },      type: 'change',      preventDefault: () => { },      stopPropagation: () => { }    } as React.ChangeEvent<HTMLInputElement>;  }  // ==================== Event ====================  const sendMessage = async (e: FormEvent<HTMLFormElement>) => {    e.preventDefault();    if (!messages.length) {      await new Promise(resolve => setTimeout(resolve, 300));    }    if (chatEndpointIsLoading) {      return;    }    handleSubmit();  }  const handleSenderSubmit = (value?: string) => {    if (!value) return;    setContent('');    // Sender onSubmit 无法访问完整事件对象    const event = createSubmitEvent(value);    // handleSubmit(event);    // onRequest(value);    sendMessage(event);  };  const handleSenderChange = (value: string) => {    // Sender onChange 无法访问完整事件对象    handleInputChange(createChangeEvent(value));    // 设置 Sender 内容    setContent(value);  };  const onPromptsItemClick: GetProp<typeof Prompts, 'onItemClick'> = (info) => {    onRequest(info.data.description as string);  };  const handleFileChange: GetProp<typeof Attachments, 'onChange'> = (info) =>    setAttachedFiles(info.fileList);  // ==================== Nodes ====================  const placeholderNode = (    <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  X"        description="Base on , 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>  );  console.log("mock", messages_chat)  console.log("langchain", messages)  const items: GetProp<typeof Bubble.List, 'items'> = messages_chat.map(({ id, message, status }) => ({    key: id,    loading: status === 'loading',    role: status === 'local' ? 'local' : 'ai',    content: message,  }));  const items_langchain: GetProp<typeof Bubble.List, 'items'> = messages.map(({ id, content, role, createdAt }: any) => ({    key: id,    role: role === 'user' ? 'local' : 'ai',    content,    createdAt  }));  const attachmentsNode = (    <Badge dot={attachedFiles.length > 0 && !headerOpen}>      <Button type="text" icon={<PaperClipOutlined />} onClick={() => setHeaderOpen(!headerOpen)} />    </Badge>  );  const senderHeader = (    <Sender.Header      title="Attachments"      open={headerOpen}      onOpenChange={setHeaderOpen}      styles={{        content: {          padding: 0,        },      }}    >      <Attachments        beforeUpload={() => false}        items={attachedFiles}        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>  );  // ==================== Render =================  return (    <div className={styles.chat}>      {/* 🌟 消息列表 */}      {        Number(activeKey) === 1 ? <div          className={styles.messageList}        >          <Bubble.List            items={items.length > 0 ? items : []}            roles={roles}            className={styles.messages}          />          <Bubble.List            items={items.length > 0 ? items : []}            roles={roles}            className={styles.messages}          />        </div> :          <Bubble.List            items={items_langchain.length > 0 ? items_langchain : [{ content: placeholderNode, variant: 'borderless' }]}            roles={roles}            className={styles.messages}          />      }      {/* 🌟 提示词 */}      <Prompts items={senderPromptsItems} onItemClick={onPromptsItemClick} />      {/* 🌟 输入框 */}      <Sender        value={content}        header={senderHeader}        placeholder={placeholder}        onSubmit={(v) => {          handleSenderSubmit(v)        }}        onChange={(v) => {          handleSenderChange(v)        }}        prefix={attachmentsNode}        loading={agent.isRequesting()}        className={styles.sender}      />    </div>  );}

github地址 github.com/missshang-h…

有兴趣的给个star