| | |
| | | auto-install-peers=true |
| | | registry=https://registry.npmjs.org/ |
| | | # registry=https://registry.npmjs.org/ |
| | | registry=https://registry.npmmirror.com |
| | | |
| | |
| | | "@ant-design/pro-components": "^2.8.7", |
| | | "@ant-design/x": "^1.1.0", |
| | | "@tinyflow-ai/react": "^0.2.1", |
| | | "@types/react-syntax-highlighter": "^15.5.13", |
| | | "@uiw/react-markdown-preview": "^5.1.4", |
| | | "ahooks": "^3.8.4", |
| | | "aieditor": "^1.3.6", |
| | | "antd": "^5.24.6", |
| | |
| | | "react-i18next": "^15.1.0", |
| | | "react-markdown": "^10.1.0", |
| | | "react-router-dom": "^6.30.0", |
| | | "react-syntax-highlighter": "^15.6.1", |
| | | "react-to-print": "^3.0.5", |
| | | "react18-json-view": "^0.2.9", |
| | | "rehype-highlight": "^7.0.2", |
| | | "rehype-raw": "^7.0.0", |
| | | "rehype-sanitize": "^6.0.0", |
| | | "remark-breaks": "^4.0.0", |
| | | "remark-gfm": "^4.0.1", |
| | | "sort-by": "^1.2.0", |
| | |
| | | onBlur={() => { |
| | | }} |
| | | > |
| | | AIFlowy 智能助理 |
| | | 仁智企智能助理 |
| | | </div> |
| | | } |
| | | footer={<></>} |
| | |
| | | import React, {useLayoutEffect, useRef, useState} from 'react'; |
| | | import {Bubble, Sender, Welcome} from '@ant-design/x'; |
| | | import {Button, message, Space, Spin} from 'antd'; |
| | | import {CopyOutlined, OpenAIOutlined, SyncOutlined} from '@ant-design/icons'; |
| | | import React, { useLayoutEffect, useRef, useState, useEffect } from 'react'; |
| | | import { Bubble, Sender, Welcome } from '@ant-design/x'; |
| | | import { Button, message, Space, Spin } from 'antd'; |
| | | import { CopyOutlined, OpenAIOutlined, SyncOutlined } from '@ant-design/icons'; |
| | | import ReactMarkdown from 'react-markdown'; |
| | | import remarkGfm from 'remark-gfm'; |
| | | import remarkBreaks from 'remark-breaks'; |
| | | import logo from "/favicon.png"; |
| | | import rehypeRaw from 'rehype-raw'; |
| | | import rehypeSanitize from 'rehype-sanitize'; |
| | | import rehypeHighlight from 'rehype-highlight'; |
| | | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; |
| | | import { ghcolors } from 'react-syntax-highlighter/dist/esm/styles/prism'; |
| | | import logo from '/favicon.png'; |
| | | import { UserOutlined } from '@ant-design/icons'; |
| | | |
| | | const fooAvatar: React.CSSProperties = { |
| | | color: '#fff', |
| | | backgroundColor: '#87d068', |
| | | import './markdown-styles.css'; // 自定义Markdown样式 |
| | | import FileUploader from '../FileUploader'; // 根据实际路径调整 |
| | | // 动态加载mermaid(流程图支持) |
| | | const loadMermaid = () => { |
| | | if (typeof window !== 'undefined' && !(window as any).mermaid) { |
| | | const script = document.createElement('script'); |
| | | script.src = 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js'; |
| | | script.onload = () => { |
| | | (window as any).mermaid.initialize({ |
| | | startOnLoad: true, |
| | | theme: 'default', |
| | | securityLevel: 'loose' |
| | | }); |
| | | }; |
| | | document.body.appendChild(script); |
| | | } |
| | | }; |
| | | |
| | | export type ChatMessage = { |
| | | id: string; |
| | | content: string; |
| | | role: 'user' | 'assistant' | 'aiLoading'; |
| | | created: number; |
| | | updateAt: number; |
| | | loading?: boolean; |
| | | id: string; |
| | | content: string; |
| | | role: 'user' | 'assistant' | 'aiLoading'; |
| | | created: number; |
| | | updateAt: number; |
| | | loading?: boolean; |
| | | }; |
| | | |
| | | export type AiProChatProps = { |
| | | loading?: boolean; |
| | | chats?: ChatMessage[]; |
| | | onChatsChange?: (value: ((prevState: ChatMessage[]) => ChatMessage[]) | ChatMessage[]) => void; |
| | | style?: React.CSSProperties; |
| | | appStyle?: React.CSSProperties; |
| | | helloMessage?: string; |
| | | botAvatar?: string; |
| | | request: (messages: ChatMessage[]) => Promise<Response>; |
| | | clearMessage?: () => void; |
| | | loading?: boolean; |
| | | chats?: ChatMessage[]; |
| | | onChatsChange?: (value: ((prevState: ChatMessage[]) => ChatMessage[]) | ChatMessage[]) => void; |
| | | style?: React.CSSProperties; |
| | | appStyle?: React.CSSProperties; |
| | | helloMessage?: string; |
| | | botAvatar?: string; |
| | | request: (messages: ChatMessage[]) => Promise<Response>; |
| | | clearMessage?: () => void; |
| | | }; |
| | | |
| | | export const AiProChat = ({ |
| | | loading, |
| | | chats: parentChats, |
| | | onChatsChange: parentOnChatsChange, |
| | | style = {}, |
| | | appStyle = {}, |
| | | helloMessage = '欢迎使用仁智企', |
| | | botAvatar = `${logo}`, |
| | | request, |
| | | clearMessage |
| | | }: AiProChatProps) => { |
| | | const isControlled = parentChats !== undefined && parentOnChatsChange !== undefined; |
| | | const [internalChats, setInternalChats] = useState<ChatMessage[]>([]); |
| | | const chats = isControlled ? parentChats : internalChats; |
| | | const setChats = isControlled ? parentOnChatsChange : setInternalChats; |
| | | const [content, setContent] = useState(''); |
| | | const [sendLoading, setSendLoading] = useState(false); |
| | | const [isStreaming, setIsStreaming] = useState(false); |
| | | const messagesContainerRef = useRef<HTMLDivElement>(null); |
| | | const messagesEndRef = useRef<HTMLDivElement>(null); |
| | | // 控制是否允许自动滚动 |
| | | const autoScrollEnabled = useRef(true); // 默认允许自动滚动 |
| | | const isUserScrolledUp = useRef(false); // 用户是否向上滚动过 |
| | | // 滚动到底部逻辑 |
| | | const scrollToBottom = () => { |
| | | const container = messagesContainerRef.current; |
| | | if (container && autoScrollEnabled.current) { |
| | | container.scrollTop = container.scrollHeight; |
| | | } |
| | | loading, |
| | | chats: parentChats, |
| | | onChatsChange: parentOnChatsChange, |
| | | style = {}, |
| | | appStyle = {}, |
| | | helloMessage = '欢迎使用AI助手', |
| | | botAvatar = `${logo}`, |
| | | request, |
| | | clearMessage |
| | | }: AiProChatProps) => { |
| | | const isControlled = parentChats !== undefined && parentOnChatsChange !== undefined; |
| | | const [internalChats, setInternalChats] = useState<ChatMessage[]>([]); |
| | | const chats = isControlled ? parentChats : internalChats; |
| | | const setChats = isControlled ? parentOnChatsChange : setInternalChats; |
| | | const [content, setContent] = useState(''); |
| | | const [sendLoading, setSendLoading] = useState(false); |
| | | const [isStreaming, setIsStreaming] = useState(false); |
| | | const messagesContainerRef = useRef<HTMLDivElement>(null); |
| | | const messagesEndRef = useRef<HTMLDivElement>(null); |
| | | const autoScrollEnabled = useRef(true); |
| | | const isUserScrolledUp = useRef(false); |
| | | |
| | | // 初始化mermaid |
| | | useEffect(() => { |
| | | loadMermaid(); |
| | | }, []); |
| | | |
| | | // 渲染后重新初始化mermaid流程图 |
| | | useEffect(() => { |
| | | if (typeof window !== 'undefined' && (window as any).mermaid) { |
| | | (window as any).mermaid.init(undefined, '.mermaid'); |
| | | } |
| | | }, [chats]); |
| | | |
| | | const scrollToBottom = () => { |
| | | const container = messagesContainerRef.current; |
| | | if (container && autoScrollEnabled.current) { |
| | | container.scrollTop = container.scrollHeight; |
| | | } |
| | | }; |
| | | |
| | | useLayoutEffect(() => { |
| | | scrollToBottom(); |
| | | }, []); |
| | | |
| | | useLayoutEffect(() => { |
| | | if (autoScrollEnabled.current) { |
| | | scrollToBottom(); |
| | | } |
| | | }, [chats]); |
| | | |
| | | useLayoutEffect(() => { |
| | | const container = messagesContainerRef.current; |
| | | if (!container) return; |
| | | |
| | | const handleScroll = () => { |
| | | const { scrollTop, scrollHeight, clientHeight } = container; |
| | | const atBottom = scrollHeight - scrollTop <= clientHeight + 5; |
| | | |
| | | if (atBottom) { |
| | | autoScrollEnabled.current = true; |
| | | isUserScrolledUp.current = false; |
| | | } else { |
| | | autoScrollEnabled.current = false; |
| | | isUserScrolledUp.current = true; |
| | | } |
| | | }; |
| | | |
| | | // 组件挂载时滚动 |
| | | useLayoutEffect(() => { |
| | | scrollToBottom(); |
| | | }, []); |
| | | container.addEventListener('scroll', handleScroll); |
| | | return () => { |
| | | container.removeEventListener('scroll', handleScroll); |
| | | }; |
| | | }, []); |
| | | |
| | | // 消息更新时滚动 |
| | | useLayoutEffect(() => { |
| | | if (autoScrollEnabled.current) { |
| | | const handleSubmit = async () => { |
| | | if (!content.trim()) return; |
| | | setSendLoading(true); |
| | | setIsStreaming(true); |
| | | const userMessage: ChatMessage = { |
| | | role: 'user', |
| | | id: Date.now().toString(), |
| | | content, |
| | | created: Date.now(), |
| | | updateAt: Date.now(), |
| | | }; |
| | | const aiMessage: ChatMessage = { |
| | | role: 'assistant', |
| | | id: Date.now().toString(), |
| | | content: '', |
| | | loading: true, |
| | | created: Date.now(), |
| | | updateAt: Date.now(), |
| | | }; |
| | | const temp = [userMessage, aiMessage]; |
| | | setChats?.((prev: ChatMessage[]) => [...(prev || []), ...temp]); |
| | | setTimeout(scrollToBottom, 50); |
| | | setContent(''); |
| | | try { |
| | | const response = await request([...(chats || []), userMessage]); |
| | | if (!response?.body) return; |
| | | const reader = response.body.getReader(); |
| | | const decoder = new TextDecoder(); |
| | | let partial = ''; |
| | | let currentContent = ''; |
| | | while (true) { |
| | | const {done, value} = await reader.read(); |
| | | if (done) break; |
| | | partial += decoder.decode(value, {stream: true}); |
| | | |
| | | const id = setInterval(() => { |
| | | currentContent = partial.slice(0, currentContent.length + 2); |
| | | setChats?.((prev: ChatMessage[]) => { |
| | | const newChats = [...(prev || [])]; |
| | | const lastMsg = newChats[newChats.length - 1]; |
| | | if (lastMsg?.role === 'assistant') { |
| | | lastMsg.loading = false; |
| | | lastMsg.content = currentContent; |
| | | lastMsg.updateAt = Date.now(); |
| | | } |
| | | return newChats; |
| | | }); |
| | | if (autoScrollEnabled.current) { |
| | | scrollToBottom(); |
| | | } |
| | | }, [chats]); |
| | | useLayoutEffect(() => { |
| | | const container = messagesContainerRef.current; |
| | | if (!container) return; |
| | | } |
| | | if (currentContent === partial) { |
| | | clearInterval(id); |
| | | } |
| | | }, 50); |
| | | } |
| | | } catch (error) { |
| | | console.error('Error:', error); |
| | | message.error('请求失败,请重试'); |
| | | } finally { |
| | | setIsStreaming(false); |
| | | setSendLoading(false); |
| | | } |
| | | }; |
| | | |
| | | const handleScroll = () => { |
| | | const { scrollTop, scrollHeight, clientHeight } = container; |
| | | const atBottom = scrollHeight - scrollTop <= clientHeight + 5; // 允许误差 5px |
| | | |
| | | if (atBottom) { |
| | | // 用户回到底部,恢复自动滚动 |
| | | autoScrollEnabled.current = true; |
| | | isUserScrolledUp.current = false; |
| | | } else { |
| | | // 用户向上滚动,禁用自动滚动 |
| | | autoScrollEnabled.current = false; |
| | | isUserScrolledUp.current = true; |
| | | } |
| | | }; |
| | | |
| | | container.addEventListener('scroll', handleScroll); |
| | | return () => { |
| | | container.removeEventListener('scroll', handleScroll); |
| | | }; |
| | | }, []); |
| | | // 提交流程优化 |
| | | const handleSubmit = async () => { |
| | | if (!content.trim()) return; |
| | | setSendLoading(true); |
| | | setIsStreaming(true); |
| | | const userMessage: ChatMessage = { |
| | | role: 'user', |
| | | id: Date.now().toString(), |
| | | content, |
| | | created: Date.now(), |
| | | updateAt: Date.now(), |
| | | }; |
| | | const aiMessage: ChatMessage = { |
| | | role: 'assistant', |
| | | id: Date.now().toString(), |
| | | content: '', |
| | | loading: true, |
| | | created: Date.now(), |
| | | updateAt: Date.now(), |
| | | }; |
| | | const temp = [userMessage, aiMessage]; |
| | | setChats?.((prev: ChatMessage[]) => [...(prev || []), ...temp]); |
| | | setTimeout(scrollToBottom, 50); |
| | | setContent(''); |
| | | try { |
| | | const response = await request([...(chats || []), userMessage]); |
| | | if (!response?.body) return; |
| | | const reader = response.body.getReader(); |
| | | const decoder = new TextDecoder(); |
| | | let partial = ''; |
| | | let currentContent = ''; |
| | | while (true) { |
| | | const {done, value} = await reader.read(); |
| | | if (done) break; |
| | | partial += decoder.decode(value, {stream: true}); |
| | | |
| | | const id = setInterval(() => { |
| | | currentContent = partial.slice(0, currentContent.length + 2); |
| | | setChats?.((prev: ChatMessage[]) => { |
| | | const newChats = [...(prev || [])]; |
| | | const lastMsg = newChats[newChats.length - 1]; |
| | | if (lastMsg?.role === 'assistant') { |
| | | lastMsg.loading = false; |
| | | lastMsg.content = currentContent; |
| | | lastMsg.updateAt = Date.now(); |
| | | } |
| | | return newChats; |
| | | }); |
| | | if (autoScrollEnabled.current) { |
| | | scrollToBottom(); // 只有在自动滚动开启时才滚动 |
| | | } |
| | | if (currentContent === partial) { |
| | | clearInterval(id); |
| | | } |
| | | }, 50); |
| | | |
| | | } |
| | | } catch (error) { |
| | | console.error('Error:', error); |
| | | console.error('出错了:', error); |
| | | } finally { |
| | | setIsStreaming(false); |
| | | setSendLoading(false); |
| | | } |
| | | const handleRegenerate = async (index: number) => { |
| | | const prevMessage: ChatMessage = { |
| | | role: 'user', |
| | | id: Date.now().toString(), |
| | | content: chats[index - 1].content, |
| | | loading: false, |
| | | created: Date.now(), |
| | | updateAt: Date.now(), |
| | | }; |
| | | const aiMessage: ChatMessage = { |
| | | role: 'assistant', |
| | | id: Date.now().toString(), |
| | | content: '', |
| | | loading: true, |
| | | created: Date.now(), |
| | | updateAt: Date.now(), |
| | | }; |
| | | const temp = [prevMessage, aiMessage]; |
| | | setChats?.((prev: ChatMessage[]) => [...(prev || []), ...temp]); |
| | | setTimeout(scrollToBottom, 50); |
| | | try { |
| | | const response = await request([...(chats || []), prevMessage]); |
| | | if (!response?.body) return; |
| | | const reader = response.body.getReader(); |
| | | const decoder = new TextDecoder(); |
| | | let partial = ''; |
| | | let currentContent = ''; |
| | | while (true) { |
| | | const {done, value} = await reader.read(); |
| | | if (done) break; |
| | | partial += decoder.decode(value, {stream: true}); |
| | | |
| | | // 重新生成消息 |
| | | const handleRegenerate = async (index: number) => { |
| | | // 找到当前 assistant 消息对应的上一条用户消息 |
| | | const prevMessage: ChatMessage = { |
| | | role: 'user', |
| | | id: Date.now().toString(), |
| | | content: chats[index - 1].content, |
| | | loading: false, |
| | | created: Date.now(), |
| | | updateAt: Date.now(), |
| | | }; |
| | | setContent(prevMessage.content) |
| | | const aiMessage: ChatMessage = { |
| | | role: 'assistant', |
| | | id: Date.now().toString(), |
| | | content: '', |
| | | loading: true, |
| | | created: Date.now(), |
| | | updateAt: Date.now(), |
| | | }; |
| | | const temp = [prevMessage, aiMessage]; |
| | | setChats?.((prev: ChatMessage[]) => [...(prev || []), ...temp]); |
| | | setTimeout(scrollToBottom, 50); |
| | | setContent(''); |
| | | try { |
| | | const response = await request([...(chats || []), prevMessage]); |
| | | if (!response?.body) return; |
| | | const reader = response.body.getReader(); |
| | | const decoder = new TextDecoder(); |
| | | let partial = ''; |
| | | let currentContent = ''; |
| | | while (true) { |
| | | const {done, value} = await reader.read(); |
| | | if (done) break; |
| | | partial += decoder.decode(value, {stream: true}); |
| | | |
| | | const id = setInterval(() => { |
| | | currentContent = partial.slice(0, currentContent.length + 2); |
| | | setChats?.((prev: ChatMessage[]) => { |
| | | const newChats = [...(prev || [])]; |
| | | const lastMsg = newChats[newChats.length - 1]; |
| | | if (lastMsg.role === 'assistant') { |
| | | lastMsg.loading = false; |
| | | lastMsg.content = currentContent; |
| | | lastMsg.updateAt = Date.now(); |
| | | } |
| | | return newChats; |
| | | }); |
| | | if (currentContent === partial) { |
| | | clearInterval(id); |
| | | } |
| | | }, 50); |
| | | |
| | | |
| | | |
| | | const id = setInterval(() => { |
| | | currentContent = partial.slice(0, currentContent.length + 2); |
| | | setChats?.((prev: ChatMessage[]) => { |
| | | const newChats = [...(prev || [])]; |
| | | const lastMsg = newChats[newChats.length - 1]; |
| | | if (lastMsg.role === 'assistant') { |
| | | lastMsg.loading = false; |
| | | lastMsg.content = currentContent; |
| | | lastMsg.updateAt = Date.now(); |
| | | } |
| | | } catch (error) { |
| | | console.error('Error:', error); |
| | | } finally { |
| | | } |
| | | return newChats; |
| | | }); |
| | | if (currentContent === partial) { |
| | | clearInterval(id); |
| | | } |
| | | }, 50); |
| | | } |
| | | } catch (error) { |
| | | console.error('Error:', error); |
| | | } |
| | | }; |
| | | |
| | | |
| | | }; |
| | | |
| | | // 渲染消息列表 |
| | | const renderMessages = () => { |
| | | if (!chats?.length) { |
| | | return ( |
| | | <Welcome |
| | | variant="borderless" |
| | | icon={<img |
| | | src={botAvatar} |
| | | style={{ width: 32, height: 32, borderRadius: '50%' }} |
| | | alt="AI Avatar" |
| | | />} |
| | | description={helloMessage} |
| | | styles={{icon: {width: 40, height: 40}}} |
| | | /> |
| | | ); |
| | | } |
| | | return ( |
| | | <Bubble.List |
| | | autoScroll={true} |
| | | items={chats.map((chat, index) => ({ |
| | | key: chat.id + Math.random().toString(), |
| | | typing: {suffix: <>💗</>}, |
| | | header: ( |
| | | <Space> |
| | | {new Date(chat.created).toLocaleString()} |
| | | </Space> |
| | | ), |
| | | loading: chat.loading, |
| | | loadingRender: () => ( |
| | | <Space> |
| | | <Spin size="small"/> |
| | | AI正在思考中... |
| | | </Space> |
| | | ), |
| | | footer: ( |
| | | <Space> |
| | | {(chat.role === 'assistant') && (<Button |
| | | color="default" |
| | | variant="text" |
| | | size="small" |
| | | icon={<SyncOutlined/>} |
| | | onClick={() => { |
| | | // 点击按钮时重新生成该消息 |
| | | if (chat.role === 'assistant') { |
| | | handleRegenerate(index); |
| | | } |
| | | }} |
| | | />)} |
| | | |
| | | <Button |
| | | color="default" |
| | | variant="text" |
| | | size="small" |
| | | icon={<CopyOutlined/>} |
| | | onClick={async () => { |
| | | try { |
| | | await navigator.clipboard.writeText(chat.content); |
| | | message.success('复制成功'); |
| | | } catch (error) { |
| | | message.error('复制失败'); |
| | | } |
| | | }} |
| | | /> |
| | | </Space> |
| | | ), |
| | | role: chat.role === 'user' ? 'local' : 'ai', |
| | | content: ( |
| | | <div> |
| | | {chat.role === 'assistant' ? ( |
| | | <ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]}> |
| | | {chat.content} |
| | | </ReactMarkdown> |
| | | ) : chat.content} |
| | | </div> |
| | | ), |
| | | avatar: chat.role === 'assistant' ? ( |
| | | <img |
| | | src={botAvatar} |
| | | style={{ width: 32, height: 32, borderRadius: '50%' }} |
| | | alt="AI Avatar" |
| | | /> |
| | | ) : { icon: <UserOutlined />, style: fooAvatar }, |
| | | }))} |
| | | roles={{ai: {placement: 'start'}, local: {placement: 'end'}}} |
| | | /> |
| | | ); |
| | | }; |
| | | |
| | | const renderMessages = () => { |
| | | if (!chats?.length) { |
| | | return ( |
| | | <Welcome |
| | | variant="borderless" |
| | | icon={<img src={botAvatar} style={{ width: 32, height: 32, borderRadius: '50%' }} alt="AI Avatar" />} |
| | | description={helloMessage} |
| | | styles={{icon: {width: 40, height: 40}}} |
| | | /> |
| | | ); |
| | | } |
| | | return ( |
| | | <div |
| | | style={{ |
| | | width: '100%', |
| | | height: '100%', |
| | | display: 'flex', |
| | | flexDirection: 'column', |
| | | background: '#fff', |
| | | border: '1px solid #f3f3f3', |
| | | ...appStyle, |
| | | ...style, |
| | | }} |
| | | > |
| | | {/* 消息容器 */} |
| | | <div |
| | | ref={messagesContainerRef} |
| | | style={{ |
| | | flex: 1, |
| | | overflowY: 'auto', |
| | | padding: '16px', |
| | | scrollbarWidth: 'none', |
| | | }} |
| | | > |
| | | {loading ? ( |
| | | <Spin tip="加载中..."/> |
| | | ) : ( |
| | | <> |
| | | {renderMessages()} |
| | | <div ref={messagesEndRef}/> |
| | | {/* 锚点元素 */} |
| | | </> |
| | | )} |
| | | </div> |
| | | {/* 输入区域 */} |
| | | <div |
| | | style={{ |
| | | borderTop: '1px solid #eee', |
| | | padding: '12px', |
| | | display: 'flex', |
| | | gap: '8px', |
| | | }} |
| | | > |
| | | <Sender |
| | | value={content} |
| | | onChange={setContent} |
| | | onSubmit={handleSubmit} |
| | | loading={sendLoading || isStreaming} |
| | | actions={(_, info) => ( |
| | | <Space size="small"> |
| | | <info.components.ClearButton |
| | | disabled={false} // 强制不禁用 |
| | | title="删除对话记录" |
| | | style={{ fontSize: 20 }} |
| | | onClick={(e) => { |
| | | e.preventDefault(); // 阻止默认行为(如果有) |
| | | clearMessage?.(); |
| | | }} |
| | | /> |
| | | <info.components.SendButton |
| | | type="primary" |
| | | icon={<OpenAIOutlined/>} |
| | | loading={sendLoading || isStreaming} |
| | | /> |
| | | </Space> |
| | | )} |
| | | <Bubble.List |
| | | autoScroll={true} |
| | | items={chats.map((chat, index) => ({ |
| | | key: chat.id + Math.random().toString(), |
| | | typing: {suffix: <>💗</>}, |
| | | header: ( |
| | | <Space> |
| | | {new Date(chat.created).toLocaleString()} |
| | | </Space> |
| | | ), |
| | | loading: chat.loading, |
| | | loadingRender: () => ( |
| | | <Space> |
| | | <Spin size="small"/> |
| | | AI正在思考中... |
| | | </Space> |
| | | ), |
| | | footer: ( |
| | | <Space> |
| | | {(chat.role === 'assistant') && ( |
| | | <Button |
| | | color="default" |
| | | variant="text" |
| | | size="small" |
| | | icon={<SyncOutlined/>} |
| | | onClick={() => handleRegenerate(index)} |
| | | /> |
| | | )} |
| | | <Button |
| | | color="default" |
| | | variant="text" |
| | | size="small" |
| | | icon={<CopyOutlined/>} |
| | | onClick={async () => { |
| | | try { |
| | | await navigator.clipboard.writeText(chat.content); |
| | | message.success('复制成功'); |
| | | } catch (error) { |
| | | message.error('复制失败'); |
| | | } |
| | | }} |
| | | /> |
| | | </Space> |
| | | ), |
| | | role: chat.role === 'user' ? 'local' : 'ai', |
| | | content: ( |
| | | <div className="markdown-body"> |
| | | {chat.role === 'assistant' ? ( |
| | | <ReactMarkdown |
| | | remarkPlugins={[remarkGfm, remarkBreaks]} |
| | | rehypePlugins={[ |
| | | rehypeRaw, |
| | | rehypeSanitize, |
| | | rehypeHighlight, |
| | | ]} |
| | | components={{ |
| | | code({ node, inline, className, children, ...props }) { |
| | | const match = /language-(\w+)/.exec(className || ''); |
| | | return !inline ? ( |
| | | <SyntaxHighlighter |
| | | language={match?.[1] || 'text'} |
| | | style={ghcolors} |
| | | PreTag="div" |
| | | {...props} |
| | | > |
| | | {String(children).replace(/\n$/, '')} |
| | | </SyntaxHighlighter> |
| | | ) : ( |
| | | <code className={className} {...props}> |
| | | {children} |
| | | </code> |
| | | ); |
| | | }, |
| | | }} |
| | | > |
| | | {chat.content} |
| | | </ReactMarkdown> |
| | | ) : ( |
| | | chat.content |
| | | )} |
| | | </div> |
| | | </div> |
| | | ), |
| | | avatar: chat.role === 'assistant' ? ( |
| | | <img |
| | | src={botAvatar} |
| | | style={{ width: 32, height: 32, borderRadius: '50%' }} |
| | | alt="AI Avatar" |
| | | /> |
| | | ) : { icon: <UserOutlined />, style: { color: '#fff', backgroundColor: '#87d068' } }, |
| | | }))} |
| | | roles={{ai: {placement: 'start'}, local: {placement: 'end'}}} |
| | | /> |
| | | ); |
| | | }; |
| | | |
| | | return ( |
| | | <div |
| | | style={{ |
| | | width: '100%', |
| | | height: '100%', |
| | | display: 'flex', |
| | | flexDirection: 'column', |
| | | background: '#fff', |
| | | border: '1px solid #f3f3f3', |
| | | ...appStyle, |
| | | ...style, |
| | | }} |
| | | > |
| | | <div |
| | | ref={messagesContainerRef} |
| | | style={{ |
| | | flex: 1, |
| | | overflowY: 'auto', |
| | | padding: '16px', |
| | | scrollbarWidth: 'none', |
| | | }} |
| | | > |
| | | {loading ? ( |
| | | <Spin tip="加载中..."/> |
| | | ) : ( |
| | | <> |
| | | {renderMessages()} |
| | | <div ref={messagesEndRef}/> |
| | | </> |
| | | )} |
| | | </div> |
| | | <div |
| | | style={{ |
| | | borderTop: '1px solid #eee', |
| | | padding: '12px', |
| | | display: 'flex', |
| | | gap: '8px', |
| | | }} |
| | | > |
| | | {/* <Sender |
| | | value={content} |
| | | onChange={setContent} |
| | | onSubmit={handleSubmit} |
| | | loading={sendLoading || isStreaming} |
| | | actions={(_, info) => ( |
| | | <Space size="small"> |
| | | <info.components.ClearButton |
| | | disabled={false} |
| | | title="删除对话记录" |
| | | style={{ fontSize: 20 }} |
| | | onClick={(e) => { |
| | | e.preventDefault(); |
| | | clearMessage?.(); |
| | | }} |
| | | /> |
| | | <info.components.SendButton |
| | | type="primary" |
| | | icon={<OpenAIOutlined/>} |
| | | loading={sendLoading || isStreaming} |
| | | /> |
| | | </Space> |
| | | )} |
| | | /> */} |
| | | |
| | | <Sender |
| | | value={content} |
| | | onChange={setContent} |
| | | onSubmit={handleSubmit} |
| | | loading={sendLoading || isStreaming} |
| | | actions={(_, info) => ( |
| | | <Space size="small"> |
| | | {/* 使用项目中的上传组件 */} |
| | | <FileUploader |
| | | onChange={(fileUrl) => { |
| | | if (fileUrl) { |
| | | // 将文件链接添加到输入框(或直接发送) |
| | | setContent(prev => `${prev}\n[文件] ${fileUrl}`); |
| | | message.success('文件上传成功'); |
| | | } |
| | | }} |
| | | /> |
| | | |
| | | {/* 原有按钮 */} |
| | | <info.components.ClearButton |
| | | disabled={false} |
| | | title="删除对话记录" |
| | | style={{ fontSize: 20 }} |
| | | onClick={(e) => { |
| | | e.preventDefault(); |
| | | clearMessage?.(); |
| | | }} |
| | | /> |
| | | <info.components.SendButton |
| | | type="primary" |
| | | icon={<OpenAIOutlined />} |
| | | loading={sendLoading || isStreaming} |
| | | /> |
| | | </Space> |
| | | )} |
| | | /> |
| | | |
| | | </div> |
| | | </div> |
| | | ); |
| | | }; |
| New file |
| | |
| | | /* =============== 基础Markdown样式 =============== */ |
| | | .markdown-body { |
| | | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; |
| | | font-size: 16px; |
| | | line-height: 1.6; |
| | | word-wrap: break-word; |
| | | color: #24292e; |
| | | } |
| | | |
| | | /* =============== 标题样式 =============== */ |
| | | .markdown-body h1, |
| | | .markdown-body h2, |
| | | .markdown-body h3, |
| | | .markdown-body h4, |
| | | .markdown-body h5, |
| | | .markdown-body h6 { |
| | | margin-top: 24px; |
| | | margin-bottom: 16px; |
| | | font-weight: 600; |
| | | line-height: 1.25; |
| | | } |
| | | |
| | | .markdown-body h1 { |
| | | padding-bottom: 0.3em; |
| | | font-size: 2em; |
| | | border-bottom: 1px solid #eaecef; |
| | | } |
| | | |
| | | .markdown-body h2 { |
| | | padding-bottom: 0.3em; |
| | | font-size: 1.5em; |
| | | border-bottom: 1px solid #eaecef; |
| | | } |
| | | |
| | | /* =============== 列表样式 =============== */ |
| | | .markdown-body ul, |
| | | .markdown-body ol { |
| | | padding-left: 2em; |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .markdown-body li { |
| | | margin-bottom: 0.25em; |
| | | } |
| | | |
| | | .markdown-body .task-list-item { |
| | | list-style-type: none; |
| | | } |
| | | |
| | | .markdown-body .task-list-item-checkbox { |
| | | margin: 0 0.2em 0.25em -1.6em; |
| | | vertical-align: middle; |
| | | } |
| | | |
| | | /* =============== 表格样式 =============== */ |
| | | .markdown-body table { |
| | | display: block; |
| | | width: 100%; |
| | | overflow: auto; |
| | | border-spacing: 0; |
| | | border-collapse: collapse; |
| | | margin: 16px 0; |
| | | } |
| | | |
| | | .markdown-body table th { |
| | | font-weight: 600; |
| | | background-color: #f6f8fa; |
| | | } |
| | | |
| | | .markdown-body table th, |
| | | .markdown-body table td { |
| | | padding: 8px 16px; |
| | | border: 1px solid #dfe2e5; |
| | | } |
| | | |
| | | .markdown-body table tr { |
| | | background-color: #fff; |
| | | border-top: 1px solid #c6cbd1; |
| | | } |
| | | |
| | | .markdown-body table tr:nth-child(2n) { |
| | | background-color: #f6f8fa; |
| | | } |
| | | |
| | | /* =============== 代码块样式 =============== */ |
| | | .markdown-body pre { |
| | | background-color: #f6f8fa; |
| | | border-radius: 6px; |
| | | padding: 16px; |
| | | overflow: auto; |
| | | margin: 16px 0; |
| | | } |
| | | |
| | | .markdown-body code { |
| | | font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; |
| | | font-size: 85%; |
| | | background-color: rgba(27, 31, 35, 0.05); |
| | | border-radius: 3px; |
| | | padding: 0.2em 0.4em; |
| | | } |
| | | |
| | | .markdown-body pre code { |
| | | background-color: transparent; |
| | | padding: 0; |
| | | } |
| | | |
| | | /* =============== Mermaid图表专用样式 =============== */ |
| | | .markdown-body .mermaid { |
| | | font-family: 'Arial', sans-serif; |
| | | margin: 24px 0; |
| | | text-align: center; |
| | | background-color: white; |
| | | padding: 24px; |
| | | border-radius: 8px; |
| | | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); |
| | | overflow: visible !important; |
| | | } |
| | | |
| | | .markdown-body .mermaid svg { |
| | | max-width: 100% !important; |
| | | height: auto !important; |
| | | margin: 0 auto; |
| | | } |
| | | |
| | | /* 节点样式 */ |
| | | .markdown-body .mermaid .node rect { |
| | | rx: 8px; |
| | | ry: 8px; |
| | | stroke-width: 2px; |
| | | stroke: #0366d6; |
| | | fill: #f6f8fa; |
| | | } |
| | | |
| | | /* 连线样式 */ |
| | | .markdown-body .mermaid .edgePath .path { |
| | | stroke: #586069; |
| | | stroke-width: 2px; |
| | | } |
| | | |
| | | .markdown-body .mermaid .arrowheadPath { |
| | | fill: #586069 !important; |
| | | } |
| | | |
| | | /* 文本样式 */ |
| | | .markdown-body .mermaid .node text { |
| | | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; |
| | | font-size: 14px; |
| | | color: #24292e; |
| | | } |
| | | |
| | | /* 特殊图表类型调整 */ |
| | | .markdown-body .mermaid .pieCircle { |
| | | stroke: #f6f8fa; |
| | | stroke-width: 2px; |
| | | } |
| | | |
| | | /* =============== 响应式调整 =============== */ |
| | | @media (max-width: 768px) { |
| | | .markdown-body { |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .markdown-body .mermaid { |
| | | padding: 16px; |
| | | margin: 16px -8px; |
| | | border-radius: 0; |
| | | } |
| | | |
| | | .markdown-body pre { |
| | | padding: 12px; |
| | | border-radius: 4px; |
| | | } |
| | | } |
| | | |
| | | /* =============== 语法高亮覆盖 =============== */ |
| | | pre[class*="language-"] { |
| | | border-radius: 6px !important; |
| | | margin: 16px 0 !important; |
| | | background: #f6f8fa !important; |
| | | } |
| | | |
| | | /* =============== 其他元素样式 =============== */ |
| | | .markdown-body blockquote { |
| | | padding: 0 1em; |
| | | color: #6a737d; |
| | | border-left: 0.25em solid #dfe2e5; |
| | | margin: 0 0 16px 0; |
| | | } |
| | | |
| | | .markdown-body img { |
| | | max-width: 100%; |
| | | box-sizing: content-box; |
| | | background-color: #fff; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | |
| | | import { Actions, ColumnConfig, ColumnGroup, ColumnsConfig } from "../AntdCrud"; |
| | | import { convertAttrsToObject } from "../../libs/utils.ts"; |
| | | import EditForm, { EditLayout } from "../AntdCrud/EditForm.tsx"; |
| | | import { App,message } from "antd"; |
| | | import { App } from "antd"; |
| | | |
| | | interface CurdPageProps { |
| | | tableAlias: string; |
| New file |
| | |
| | | import { |
| | | Conversations, ConversationsProps, |
| | | } from '@ant-design/x'; |
| | | import { createStyles } from 'antd-style'; |
| | | import React, {useEffect, useRef, useState} from 'react'; |
| | | import { |
| | | DeleteOutlined, |
| | | EditOutlined, ExclamationCircleFilled, |
| | | PlusOutlined, |
| | | } from '@ant-design/icons'; |
| | | import {Button, type GetProp, Modal, Input, message} from 'antd'; |
| | | import { AiProChat, ChatMessage } from "../components/AiProChat/AiProChat.tsx"; |
| | | import {getExternalSessionId, setNewExternalSessionId, updateExternalSessionId} from "../libs/getExternalSessionId.ts"; |
| | | import { useSse } from "../hooks/useSse.ts"; |
| | | import { useParams } from "react-router-dom"; |
| | | import { useGetManual } from "../hooks/useApis.ts"; |
| | | import {uuid} from "../libs/uuid.ts"; |
| | | |
| | | const useStyle = createStyles(({ token, css }) => { |
| | | return { |
| | | layout: css` |
| | | width: 100%; |
| | | min-width: 1000px; |
| | | height: 100vh; |
| | | border-radius: ${token.borderRadius}px; |
| | | display: flex; |
| | | background: ${token.colorBgContainer}; |
| | | font-family: AlibabaPuHuiTi, ${token.fontFamily}, sans-serif; |
| | | .ant-prompts { |
| | | color: ${token.colorText}; |
| | | } |
| | | `, |
| | | menu: css` |
| | | background: ${token.colorBgLayout}80; |
| | | width: 300px; |
| | | min-width: 300px; |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | `, |
| | | conversations: css` |
| | | padding: 0 12px; |
| | | flex: 1; |
| | | overflow-y: auto; |
| | | `, |
| | | chat: css` |
| | | height: 100%; |
| | | width: 100%; |
| | | margin: 0 auto; |
| | | box-sizing: border-box; |
| | | display: flex; |
| | | flex-direction: column; |
| | | padding: ${token.paddingLG}px; |
| | | gap: 16px; |
| | | `, |
| | | messages: css` |
| | | flex: 1; |
| | | `, |
| | | placeholder: css` |
| | | padding-top: 32px; |
| | | `, |
| | | sender: css` |
| | | box-shadow: ${token.boxShadow}; |
| | | `, |
| | | logo: css` |
| | | display: flex; |
| | | height: 72px; |
| | | align-items: center; |
| | | justify-content: start; |
| | | padding: 0 24px; |
| | | box-sizing: border-box; |
| | | img { |
| | | width: 45px; |
| | | height: 40px; |
| | | display: inline-block; |
| | | } |
| | | span { |
| | | display: inline-block; |
| | | margin: 0 8px; |
| | | font-weight: bold; |
| | | color: ${token.colorText}; |
| | | font-size: 16px; |
| | | } |
| | | `, |
| | | addBtn: css` |
| | | background: #1677ff0f; |
| | | border: 1px solid #1677ff34; |
| | | width: calc(100% - 24px); |
| | | margin: 0 12px 24px 12px; |
| | | `, |
| | | }; |
| | | }); |
| | | |
| | | export const ExternalBot: React.FC = () => { |
| | | const urlParams = new URLSearchParams(location.search); |
| | | const [isExternalIFrame, setIsExternalIFrame] = useState<boolean>(false); |
| | | const isExternalIFrameRef = useRef(isExternalIFrame); |
| | | const {doGet: doGetCreateToken} = useGetManual('/api/temp-token/create') |
| | | useEffect(() => { |
| | | isExternalIFrameRef.current = isExternalIFrame; |
| | | }, [isExternalIFrame]); |
| | | useEffect(() => { |
| | | const isFrame = urlParams.get('isIframe'); |
| | | const token = urlParams.get('authKey'); |
| | | if (isFrame) { |
| | | const newValue = true; |
| | | setIsExternalIFrame(newValue); |
| | | isExternalIFrameRef.current = newValue; // 手动同步 ref |
| | | doGetCreateToken().then((res: any) => { |
| | | if (res.data.errorCode === 0){ |
| | | localStorage.setItem('authKey', res.data.data); |
| | | const link = document.querySelector("link[rel*='icon']") as HTMLLinkElement; |
| | | doGetBotInfo({params: {id: params?.id}}).then((r: any) => { |
| | | if (link) { |
| | | link.href = r?.data?.data?.icon || '/favicon.png'; |
| | | } |
| | | document.title = r?.data?.data?.title; |
| | | }); |
| | | } |
| | | }); |
| | | } else if (token) { |
| | | localStorage.setItem('authKey', token); |
| | | } |
| | | |
| | | |
| | | }, []); // 空依赖数组表示只在组件挂载时执行一次 |
| | | const [newTitle, setNewTitle] = useState<string>(''); |
| | | |
| | | // ==================== Style ==================== |
| | | const { styles } = useStyle(); |
| | | |
| | | // ==================== State ==================== |
| | | const [conversationsItems, setConversationsItems] = React.useState<{ key: string; label: string }[]>([]); |
| | | const [activeKey, setActiveKey] = React.useState(''); |
| | | const [open, setOpen] = useState(false); |
| | | const params = useParams(); |
| | | const { doGet: doGetBotInfo, result: botInfo} =useGetManual("/api/v1/aiBot/detail") |
| | | const { start: startChat } = useSse("/api/v1/aiBot/chat"); |
| | | // 查询会话列表的数据 |
| | | const { doGet: getConversationManualGet } = useGetManual('/api/v1/conversation/externalList'); |
| | | const { doGet: doGetManual } = useGetManual("/api/v1/aiBotMessage/messageList"); |
| | | const { doGet: doGetConverManualDelete } = useGetManual("/api/v1/conversation/deleteConversation"); |
| | | const { doGet: doGetConverManualUpdate } = useGetManual("/api/v1/conversation/updateConversation"); |
| | | const menuConfig: ConversationsProps['menu'] = () => ({ |
| | | items: [ |
| | | { |
| | | label: '重命名', |
| | | key: 'update', |
| | | icon: <EditOutlined />, |
| | | }, |
| | | { |
| | | label: '删除', |
| | | key: 'delete', |
| | | icon: <DeleteOutlined />, |
| | | danger: true, |
| | | }, |
| | | ], |
| | | onClick: (menuInfo) => { |
| | | if (menuInfo.key === 'delete') { |
| | | Modal.confirm({ |
| | | title: '删除对话', |
| | | icon: <ExclamationCircleFilled />, |
| | | content: '删除后,该对话将不可恢复。确认删除吗?', |
| | | onOk() { |
| | | doGetConverManualDelete({ |
| | | params: { |
| | | sessionId: getExternalSessionId(), |
| | | botId: params?.id, |
| | | }, |
| | | }).then((res: any) => { |
| | | if (res.data.errorCode === 0){ |
| | | message.success('删除成功'); |
| | | setChats([]) |
| | | getConversationManualGet({params: { "botId": params?.id }}) |
| | | } |
| | | }); |
| | | }, |
| | | onCancel() { |
| | | }, |
| | | }); |
| | | |
| | | |
| | | } else if (menuInfo.key === 'update') { |
| | | showModal() |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | |
| | | const [chats, setChats] = useState<ChatMessage[]>([]); |
| | | |
| | | const getConversations = (options: { sessionId: any; title: any }[]): { key: any; label: any }[] => { |
| | | if (options) { |
| | | return options.map((item) => ({ |
| | | key: item.sessionId, |
| | | label: item.title, |
| | | })); |
| | | } |
| | | return []; |
| | | }; |
| | | useEffect(() => { |
| | | if (isExternalIFrameRef.current) { |
| | | return; |
| | | } |
| | | if (chats.length === 2 && chats[1].content.length < 1){ |
| | | getConversationManualGet({ |
| | | params: { "botId": params?.id } |
| | | }).then((r: any) => { |
| | | setConversationsItems(getConversations(r?.data?.data?.cons)); |
| | | }); |
| | | } |
| | | }, [chats]) |
| | | |
| | | |
| | | useEffect(() => { |
| | | if (isExternalIFrameRef.current) { |
| | | return; |
| | | } |
| | | const link = document.querySelector("link[rel*='icon']") as HTMLLinkElement; |
| | | doGetBotInfo({params: {id: params?.id}}).then((r: any) => { |
| | | if (link) { |
| | | link.href = r?.data?.data?.icon || '/favicon.png'; |
| | | } |
| | | document.title = r?.data?.data?.title; |
| | | }); |
| | | |
| | | updateExternalSessionId(uuid()) |
| | | getConversationManualGet( |
| | | { |
| | | params: { "botId": params?.id } |
| | | } |
| | | ).then((r: any) => { |
| | | setActiveKey(getExternalSessionId()); |
| | | setConversationsItems(getConversations(r?.data?.data?.cons)); |
| | | }); |
| | | }, []) |
| | | |
| | | const onAddConversation = () => { |
| | | setNewExternalSessionId(); |
| | | // setConversationsItems(prev => [ { key: getExternalSessionId(), label: '新建会话' }, ...prev]); |
| | | setActiveKey(getExternalSessionId()); |
| | | setChats([]) |
| | | }; |
| | | |
| | | const onConversationClick: GetProp<typeof Conversations, 'onActiveChange'> = (key) => { |
| | | setActiveKey(key); |
| | | updateExternalSessionId(key); |
| | | doGetManual({ |
| | | params: { |
| | | sessionId: key, |
| | | botId: params?.id, |
| | | // 是externalBot页面提交的消息记录 |
| | | isExternalMsg: 1 |
| | | }, |
| | | }).then((r: any) => { |
| | | setChats(r?.data.data); |
| | | }); |
| | | |
| | | }; |
| | | |
| | | const logoNode = ( |
| | | |
| | | <div className={styles.logo}> |
| | | <img |
| | | src={botInfo?.data?.icon || "/favicon.png"} |
| | | style={{ width: 32, height: 32, borderRadius: '50%' }} |
| | | draggable={false} |
| | | alt="logo" |
| | | /> |
| | | <span>{botInfo?.data?.title}</span> |
| | | </div> |
| | | ); |
| | | |
| | | // 更新会话标题的辅助函数 |
| | | const updateConversationTitle = (sessionId: string, newTitle: string) => { |
| | | setConversationsItems((prevItems) => |
| | | prevItems.map((item) => |
| | | item.key === sessionId ? { ...item, label: newTitle } : item |
| | | ) |
| | | ); |
| | | }; |
| | | const showModal = () => { |
| | | setOpen(true); |
| | | |
| | | }; |
| | | const updateTitle = () => { |
| | | doGetConverManualUpdate({ |
| | | params: { |
| | | sessionId: activeKey, |
| | | botId: params?.id, |
| | | title: newTitle, |
| | | }, |
| | | }).then((res: any) => { |
| | | if (res.data.errorCode === 0){ |
| | | // 更新本地状态 |
| | | updateConversationTitle(activeKey, newTitle) |
| | | message.success('更新成功'); |
| | | setOpen(false); |
| | | |
| | | } |
| | | }); |
| | | }; |
| | | const hideModal = () => { |
| | | setOpen(false); |
| | | }; |
| | | // ==================== Render ==================== |
| | | return ( |
| | | <div className={styles.layout}> |
| | | <Modal |
| | | title="修改会话名称" |
| | | open={open} |
| | | onOk={updateTitle} |
| | | onCancel={hideModal} |
| | | okText="确认" |
| | | cancelText="取消" |
| | | > |
| | | <Input placeholder="请输入新的会话标题" |
| | | defaultValue={newTitle} |
| | | onChange={(e) => { |
| | | setNewTitle(e.target.value) |
| | | }} |
| | | /> |
| | | </Modal> |
| | | <div className={styles.menu}> |
| | | {/* 🌟 Logo */} |
| | | {logoNode} |
| | | {/* 🌟 添加会话 */} |
| | | <Button |
| | | onClick={onAddConversation} |
| | | type="link" |
| | | className={styles.addBtn} |
| | | icon={<PlusOutlined />} |
| | | > |
| | | 新建会话 |
| | | </Button> |
| | | {/* 🌟 会话管理 */} |
| | | {conversationsItems && ( |
| | | <Conversations |
| | | items={conversationsItems} |
| | | className={styles.conversations} |
| | | activeKey={activeKey} |
| | | menu={menuConfig} |
| | | onActiveChange={onConversationClick} |
| | | /> |
| | | )} |
| | | </div> |
| | | <div className={styles.chat}> |
| | | <AiProChat |
| | | chats={chats} |
| | | onChatsChange={setChats} // 确保正确传递 onChatsChange |
| | | helloMessage="欢迎使用仁智企 ,我是你的专属机器人,有什么问题可以随时问我。" |
| | | botAvatar={botInfo?.data?.icon} |
| | | request={async (messages) => { |
| | | const readableStream = new ReadableStream({ |
| | | async start(controller) { |
| | | const encoder = new TextEncoder(); |
| | | startChat({ |
| | | data: { |
| | | botId: params.id, |
| | | sessionId: getExternalSessionId(), |
| | | prompt: messages[messages.length - 1].content as string, |
| | | isExternalMsg: 1 |
| | | }, |
| | | onMessage: (msg) => { |
| | | controller.enqueue(encoder.encode(msg)); |
| | | }, |
| | | onFinished: () => { |
| | | controller.close(); |
| | | }, |
| | | }) |
| | | }, |
| | | }); |
| | | return new Response(readableStream); |
| | | }} |
| | | /> |
| | | </div> |
| | | </div> |
| | | ); |
| | | }; |
| | | |
| | | export default { |
| | | path: "/ai/externalBot-copy/:id", |
| | | element: ExternalBot, |
| | | frontEnable: true, |
| | | }; |
| | |
| | | ); // 二级分类ID |
| | | |
| | | // 设置列配置的状态 |
| | | const [columnsConfig, setColumnsConfig] = useState<ColumnsConfig<any>>( |
| | | baseColumns |
| | | ); |
| | | const [columnsConfig, setColumnsConfig] = |
| | | useState<ColumnsConfig<any>>(baseColumns); |
| | | |
| | | // 设置默认的一级分类 |
| | | useEffect(() => { |
| | |
| | | |
| | | newColumns.push( |
| | | { |
| | | title: "外接类型", |
| | | dataIndex: "botTypeId", |
| | | key: "botTypeId", |
| | | render: (value: number | string | null | undefined) => { |
| | | // 处理接口可能返回的 null/undefined |
| | | if (value === null || value === undefined) return "-"; |
| | | |
| | | // 统一转为字符串比较(兼容数字和字符串类型的值) |
| | | const valueStr = String(value); |
| | | const map: Record<string, string> = { |
| | | "-1": "无", |
| | | "1": "对话模式", |
| | | "2": "文件工作流模式" |
| | | }; |
| | | return map[valueStr] || valueStr; // 未知值原样显示 |
| | | }, |
| | | form: { |
| | | type: "select", |
| | | attrs: { |
| | | defaultValue: -1, |
| | | options: [ |
| | | { value: -1, label: "无" }, // 表单中建议用数字(与接口一致) |
| | | { value: 1, label: "对话模式" }, |
| | | { value: 2, label: "文件工作流模式" }, |
| | | ], |
| | | }, |
| | | }, |
| | | } |
| | | , |
| | | { |
| | | title: "智能体API", |
| | | dataIndex: "modelAPI", |
| | | key: "modelAPI", |
| | |
| | | key: "modelKEY", |
| | | placeholder: "请输入外接智能体的KEY", |
| | | form: { |
| | | extra: "输入外接智能体API和KEY后,当前智能体本地的知识库以及工作流配置将自动忽略" |
| | | } |
| | | extra: |
| | | "输入外接智能体API和KEY后,当前智能体本地的知识库以及工作流配置将自动忽略", |
| | | }, |
| | | } |
| | | ); |
| | | |
| | |
| | | > |
| | | 全部 |
| | | </h3> |
| | | {secondMenuResult?.data?.map((secondMenu: { id: string; secondMenuName: string }) => ( |
| | | <h3 |
| | | key={secondMenu.id} |
| | | onClick={() => handleCategoryClick(secondMenu.id)} // 储存 ID |
| | | style={{ |
| | | cursor: "pointer", |
| | | color: selectedCategoryId === secondMenu.id ? "#1890ff" : "#000", |
| | | fontWeight: selectedCategoryId === secondMenu.id ? "bold" : "normal", |
| | | }} |
| | | > |
| | | {secondMenu.secondMenuName} |
| | | </h3> |
| | | ))} |
| | | {secondMenuResult?.data?.map( |
| | | (secondMenu: { id: string; secondMenuName: string }) => ( |
| | | <h3 |
| | | key={secondMenu.id} |
| | | onClick={() => handleCategoryClick(secondMenu.id)} // 储存 ID |
| | | style={{ |
| | | cursor: "pointer", |
| | | color: |
| | | selectedCategoryId === secondMenu.id ? "#1890ff" : "#000", |
| | | fontWeight: |
| | | selectedCategoryId === secondMenu.id ? "bold" : "normal", |
| | | }} |
| | | > |
| | | {secondMenu.secondMenuName} |
| | | </h3> |
| | | ) |
| | | )} |
| | | </div> |
| | | {/* 右侧的主内容 */} |
| | | <div style={{ flex: 1, padding: "16px" }}> |
| | |
| | | { |
| | | supportSearch: true, |
| | | form: { |
| | | type: "input" |
| | | type: "input", |
| | | rules: [{required: true, message: "请输入名称"}], |
| | | }, |
| | | dataIndex: "secondMenuName", |
| | | title: "名称", |
| | | key: "secondMenuName" |
| | | key: "secondMenuName", |
| | | }, |
| | | |
| | | ]; |