import React, { useEffect, useRef, useState } from 'react'; import { Button, GetProp, GetRef, Popover, Space, Spin, message } from 'antd'; import { CloseOutlined, CloudUploadOutlined, CommentOutlined, OpenAIFilled, PlusOutlined, } from '@ant-design/icons'; import { Attachments, type AttachmentsProps, Bubble, Conversations, Sender, Suggestion, Welcome, useXAgent, useXChat, } from '@ant-design/x'; import type { Conversation } from '@ant-design/x/es/conversations'; import { createStyles } from 'antd-style'; import dayjs from 'dayjs'; import ReactMarkdown from 'react-markdown'; type BubbleDataType = { role: string; content: string; }; 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: , children: [ { label: 'About React', value: 'react' }, { label: 'About Ant Design', value: 'antd' }, ], }, ]; 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-block: 16px; flex: 1; `, chatWelcome: css` margin-inline: 16px; 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>(null); const abortController = useRef(null); // ==================== State ==================== const [messageHistory, setMessageHistory] = useState>({}); const [sessionList, setSessionList] = useState(MOCK_SESSION_LIST); const [curSession, setCurSession] = useState(sessionList[0].key); const [attachmentsOpen, setAttachmentsOpen] = useState(false); const [files, setFiles] = useState>([]); const [inputValue, setInputValue] = useState(''); // 在这里切换请求 const [agent] = useXAgent({ baseURL: '/api/deepseek/api/chat', }); 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 currentContent = ''; let currentThink = ''; try { if (chunk?.data && !chunk?.data.includes('DONE')) { const message = JSON.parse(chunk?.data); currentThink = message?.choices?.[0]?.delta?.reasoning_content || ''; currentContent = message?.choices?.[0]?.delta?.content || ''; } } catch (error) { console.error(error); } let content = ''; if (!originMessage?.content && currentThink) { content = `${currentThink}`; } else if ( originMessage?.content?.includes('') && !originMessage?.content.includes('') && currentContent ) { content = `${originMessage?.content}${currentContent}`; } else { content = `${originMessage?.content || ''}${currentThink}${currentContent}`; } return { content: content, role: 'assistant', }; }, resolveAbortController: (controller) => { abortController.current = controller; }, }); // ==================== Event ==================== const handleUserSubmit = (val: string) => { onRequest({ message: { content: val, role: 'user' }, appId: '2942534530212696064', // 进阶配置 request_id: undefined, returnType: undefined, knowledge_ids: undefined, document_ids: undefined, }); // 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 = (
✨ AI Copilot
); const chatList = (
{messages?.length ? ( /** 消息列表 */ ({ ...i.message, classNames: { content: i.status === 'loading' ? styles.loadingMessage : '', }, typing: i.status === 'loading' ? { step: 5, interval: 20, suffix: <>💗 } : false, content: {i.message.content}, }))} roles={{ assistant: { placement: 'start', // footer: ( //
//
// ), loadingRender: () => ( {AGENT_PLACEHOLDER} ), }, user: { placement: 'end' }, }} /> ) : ( /** 没有消息时的 welcome */ )}
); const sendHeader = ( false} items={files} onChange={({ fileList }) => setFiles(fileList)} placeholder={(type) => type === 'drop' ? { title: 'Drop file here' } : { icon: , title: 'Upload files', description: 'Click or drag files to this area to upload', } } /> ); const chatSender = (
{/** 输入框 */} setInputValue(`[${itemVal}]:`)}> {({ onTrigger, onKeyDown }) => ( { onTrigger(v === '/'); setInputValue(v); }} onSubmit={() => { handleUserSubmit(inputValue); setInputValue(''); }} onCancel={() => { abortController.current?.abort(); }} allowSpeech placeholder="请输入" onKeyDown={onKeyDown} header={sendHeader} // prefix={ //
); useEffect(() => { // history mock if (messages?.length) { setMessageHistory((prev) => ({ ...prev, [curSession]: messages, })); } }, [messages]); return (
{/** 对话区 - header */} {/* {chatHeader} */} {/** 对话区 - 消息列表 */} {chatList} {/** 对话区 - 输入框 */} {chatSender}
); }; const useWorkareaStyle = createStyles(({ token, css }) => { return { copilotWrapper: css` height: 100%; 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 Chat: React.FC = () => { const { styles: workareaStyles } = useWorkareaStyle(); const [copilotOpen, setCopilotOpen] = useState(true); return (
); }; export default Chat;