| | |
| | | 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 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'; |
| | | import './markdown-styles.css'; // 自定义Markdown样式 |
| | | import FileUploader from '../FileUploader'; // 根据实际路径调整 |
| | | // 动态加载mermaid(流程图支持) |
| | | /* eslint-disable no-constant-condition */ |
| | | import React, { useLayoutEffect, useRef, useState, useEffect } from "react"; |
| | | import { Bubble, Sender, Welcome } from "@ant-design/x"; |
| | | import { App, Button, message, Space, Spin, Upload, UploadFile } from "antd"; |
| | | import { |
| | | CopyOutlined, |
| | | OpenAIOutlined, |
| | | SyncOutlined, |
| | | UserOutlined, |
| | | UploadOutlined, |
| | | DeleteOutlined, |
| | | } from "@ant-design/icons"; |
| | | import ReactMarkdown from "react-markdown"; |
| | | import remarkGfm from "remark-gfm"; |
| | | import remarkBreaks from "remark-breaks"; |
| | | 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 "./markdown-styles.css"; |
| | | |
| | | const codeStyle: Record<string, React.CSSProperties> = { |
| | | ...(Object.fromEntries( |
| | | Object.entries(ghcolors).filter(([key]) => typeof key === "string") |
| | | ) as Record<string, React.CSSProperties>), |
| | | }; |
| | | |
| | | 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'; |
| | | 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({ |
| | | (window as any).mermaid.initialize({ |
| | | startOnLoad: true, |
| | | theme: 'default', |
| | | securityLevel: 'loose' |
| | | theme: "default", |
| | | securityLevel: "loose", |
| | | }); |
| | | }; |
| | | document.body.appendChild(script); |
| | |
| | | export type ChatMessage = { |
| | | id: string; |
| | | content: string; |
| | | role: 'user' | 'assistant' | 'aiLoading'; |
| | | role: "user" | "assistant" | "aiLoading"; |
| | | created: number; |
| | | updateAt: number; |
| | | loading?: boolean; |
| | |
| | | export type AiProChatProps = { |
| | | loading?: boolean; |
| | | chats?: ChatMessage[]; |
| | | onChatsChange?: (value: ((prevState: ChatMessage[]) => ChatMessage[]) | ChatMessage[]) => void; |
| | | onChatsChange?: ( |
| | | value: ((prevState: ChatMessage[]) => ChatMessage[]) | ChatMessage[] |
| | | ) => void; |
| | | style?: React.CSSProperties; |
| | | appStyle?: React.CSSProperties; |
| | | helloMessage?: string; |
| | | botAvatar?: string; |
| | | request: (messages: ChatMessage[]) => Promise<Response>; |
| | | clearMessage?: () => void; |
| | | botId?: string; // 新增 botId 参数 |
| | | botTypeId?: number; |
| | | onFileUploaded?: (fileId: string) => void; // 新增文件上传回调 |
| | | onFileRemoved?: (removedFileId?: string) => void; // 新增文件删除回调 |
| | | }; |
| | | |
| | | export const AiProChat = ({ |
| | |
| | | onChatsChange: parentOnChatsChange, |
| | | style = {}, |
| | | appStyle = {}, |
| | | helloMessage = '欢迎使用AI助手', |
| | | helloMessage = "欢迎使用AI助手", |
| | | botAvatar = `${logo}`, |
| | | request, |
| | | clearMessage |
| | | clearMessage, |
| | | botId, // 新增 |
| | | botTypeId, |
| | | onFileUploaded, // 新增 |
| | | onFileRemoved, // 新增 |
| | | }: AiProChatProps) => { |
| | | const isControlled = parentChats !== undefined && parentOnChatsChange !== undefined; |
| | | 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 [content, setContent] = useState(""); |
| | | const [sendLoading, setSendLoading] = useState(false); |
| | | const [isStreaming, setIsStreaming] = useState(false); |
| | | const [uploadedFiles, setUploadedFiles] = useState<string[]>([]); |
| | | const [fileList, setFileList] = useState<UploadFile[]>([]); |
| | | const messagesContainerRef = useRef<HTMLDivElement>(null); |
| | | const messagesEndRef = useRef<HTMLDivElement>(null); |
| | | const autoScrollEnabled = useRef(true); |
| | | const isUserScrolledUp = useRef(false); |
| | | const { message: messageApi } = App.useApp(); |
| | | |
| | | // 初始化mermaid |
| | | useEffect(() => { |
| | | loadMermaid(); |
| | | }, []); |
| | | |
| | | // 渲染后重新初始化mermaid流程图 |
| | | useEffect(() => { |
| | | if (typeof window !== 'undefined' && (window as any).mermaid) { |
| | | (window as any).mermaid.init(undefined, '.mermaid'); |
| | | if (typeof window !== "undefined" && (window as any).mermaid) { |
| | | (window as any).mermaid.init(undefined, ".mermaid"); |
| | | } |
| | | }, [chats]); |
| | | |
| | | |
| | | const handleRemoveFile = (name: string) => { |
| | | const fileToRemove = fileList.find(f => f.name === name); |
| | | console.log("handleRemoveFile", fileToRemove); |
| | | |
| | | if (!fileToRemove) return; |
| | | |
| | | const newUploadedFiles = uploadedFiles.filter(f => f !== name); |
| | | const newFileList = fileList.filter(f => f.name !== name); |
| | | |
| | | setUploadedFiles(newUploadedFiles); |
| | | setFileList(newFileList); |
| | | |
| | | // 确保传递的是文件的 url (即文件ID) |
| | | if (fileToRemove.url) { |
| | | onFileRemoved?.(fileToRemove.url); |
| | | } |
| | | }; |
| | | |
| | | |
| | | const handleUpload = async (file: File) => { |
| | | if (botTypeId === 2 && uploadedFiles.length >= 1) { |
| | | messageApi.warning("只能上传一个文件,请删除已上传文件"); |
| | | return false; |
| | | } |
| | | // 多文件模式检查(假设限制最多5个文件) |
| | | if (botTypeId === 3 && uploadedFiles.length >= 5) { |
| | | messageApi.warning("最多只能上传5个文件"); |
| | | return false; |
| | | } |
| | | |
| | | setFileList(prev => [ |
| | | ...prev, |
| | | { |
| | | uid: file.name, |
| | | name: file.name, |
| | | status: "uploading", |
| | | }, |
| | | ]); |
| | | |
| | | const formData = new FormData(); |
| | | const tokenKey = `${import.meta.env.VITE_APP_TOKEN_KEY}`; |
| | | formData.append("file", file); |
| | | formData.append("botId", botId || ""); |
| | | try { |
| | | const res = await fetch("/api/v1/aiBot/files/upload", { |
| | | method: "POST", |
| | | headers: { |
| | | [tokenKey]: localStorage.getItem("authKey") || "", |
| | | Accept: "application/json", |
| | | }, |
| | | body: formData, |
| | | }); |
| | | if (!res.ok) throw new Error(`上传失败: ${res.statusText}`); |
| | | const result = await res.json(); |
| | | const fileData = JSON.parse(result.data.trim()); |
| | | |
| | | setFileList(prev => |
| | | prev.map(f => |
| | | f.name === file.name |
| | | ? { ...f, uid: fileData.name, status: "done", url: fileData.id } |
| | | : f |
| | | ) |
| | | ); |
| | | |
| | | setUploadedFiles((prev) => [...prev, fileData.name]); |
| | | messageApi.success(`${fileData.name} 上传成功`); |
| | | // 调用回调函数传递文件ID |
| | | onFileUploaded?.(fileData.id); |
| | | |
| | | return fileData; |
| | | } catch (error) { |
| | | setFileList(prev => |
| | | prev.map(f => |
| | | f.name === file.name |
| | | ? { ...f, status: "error" } |
| | | : f |
| | | ) |
| | | ); |
| | | messageApi.error((error as Error).message || "上传失败"); |
| | | throw error; |
| | | } |
| | | }; |
| | | |
| | | const scrollToBottom = () => { |
| | | const container = messagesContainerRef.current; |
| | |
| | | } |
| | | }; |
| | | |
| | | container.addEventListener('scroll', handleScroll); |
| | | container.addEventListener("scroll", handleScroll); |
| | | return () => { |
| | | container.removeEventListener('scroll', handleScroll); |
| | | container.removeEventListener("scroll", handleScroll); |
| | | }; |
| | | }, []); |
| | | |
| | | const handleSubmit = async () => { |
| | | if (!content.trim()) return; |
| | | if (!content.trim() && uploadedFiles.length === 0) return; |
| | | |
| | | setSendLoading(true); |
| | | setIsStreaming(true); |
| | | |
| | | let finalContent = content; |
| | | if (uploadedFiles.length > 0) { |
| | | finalContent = |
| | | uploadedFiles.map((name) => `📄 ${name}`).join("\n") + |
| | | (content ? "\n" + content : ""); |
| | | } |
| | | |
| | | const userMessage: ChatMessage = { |
| | | role: 'user', |
| | | role: "user", |
| | | id: Date.now().toString(), |
| | | content, |
| | | content: finalContent, |
| | | created: Date.now(), |
| | | updateAt: Date.now(), |
| | | }; |
| | | |
| | | const aiMessage: ChatMessage = { |
| | | role: 'assistant', |
| | | role: "assistant", |
| | | id: Date.now().toString(), |
| | | content: '', |
| | | content: "", |
| | | loading: true, |
| | | created: Date.now(), |
| | | updateAt: Date.now(), |
| | | }; |
| | | const temp = [userMessage, aiMessage]; |
| | | setChats?.((prev: ChatMessage[]) => [...(prev || []), ...temp]); |
| | | |
| | | setChats?.((prev: ChatMessage[]) => [ |
| | | ...(prev || []), |
| | | userMessage, |
| | | aiMessage, |
| | | ]); |
| | | setUploadedFiles([]); |
| | | setContent(""); |
| | | 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 = ''; |
| | | let partial = ""; |
| | | let currentContent = ""; |
| | | while (true) { |
| | | const {done, value} = await reader.read(); |
| | | const { done, value } = await reader.read(); |
| | | if (done) break; |
| | | partial += decoder.decode(value, {stream: true}); |
| | | 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') { |
| | | if (lastMsg?.role === "assistant") { |
| | | lastMsg.loading = false; |
| | | lastMsg.content = currentContent; |
| | | lastMsg.updateAt = Date.now(); |
| | |
| | | }, 50); |
| | | } |
| | | } catch (error) { |
| | | console.error('Error:', error); |
| | | message.error('请求失败,请重试'); |
| | | console.error("Error:", error); |
| | | message.error("请求失败,请重试"); |
| | | } finally { |
| | | setIsStreaming(false); |
| | | setSendLoading(false); |
| | |
| | | |
| | | const handleRegenerate = async (index: number) => { |
| | | const prevMessage: ChatMessage = { |
| | | role: 'user', |
| | | role: "user", |
| | | id: Date.now().toString(), |
| | | content: chats[index - 1].content, |
| | | loading: false, |
| | |
| | | updateAt: Date.now(), |
| | | }; |
| | | const aiMessage: ChatMessage = { |
| | | role: 'assistant', |
| | | role: "assistant", |
| | | id: Date.now().toString(), |
| | | content: '', |
| | | content: "", |
| | | loading: true, |
| | | created: Date.now(), |
| | | updateAt: Date.now(), |
| | |
| | | if (!response?.body) return; |
| | | const reader = response.body.getReader(); |
| | | const decoder = new TextDecoder(); |
| | | let partial = ''; |
| | | let currentContent = ''; |
| | | let partial = ""; |
| | | let currentContent = ""; |
| | | while (true) { |
| | | const {done, value} = await reader.read(); |
| | | const { done, value } = await reader.read(); |
| | | if (done) break; |
| | | partial += decoder.decode(value, {stream: true}); |
| | | 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') { |
| | | if (lastMsg.role === "assistant") { |
| | | lastMsg.loading = false; |
| | | lastMsg.content = currentContent; |
| | | lastMsg.updateAt = Date.now(); |
| | |
| | | }, 50); |
| | | } |
| | | } catch (error) { |
| | | console.error('Error:', error); |
| | | console.error("Error:", error); |
| | | } |
| | | }; |
| | | |
| | |
| | | return ( |
| | | <Welcome |
| | | variant="borderless" |
| | | icon={<img src={botAvatar} style={{ width: 32, height: 32, borderRadius: '50%' }} alt="AI Avatar" />} |
| | | icon={ |
| | | <img |
| | | src={botAvatar} |
| | | style={{ width: 32, height: 32, borderRadius: "50%" }} |
| | | alt="AI Avatar" |
| | | /> |
| | | } |
| | | description={helloMessage} |
| | | styles={{icon: {width: 40, height: 40}}} |
| | | styles={{ icon: { width: 40, height: 40 } }} |
| | | /> |
| | | ); |
| | | } |
| | |
| | | autoScroll={true} |
| | | items={chats.map((chat, index) => ({ |
| | | key: chat.id + Math.random().toString(), |
| | | typing: {suffix: <>💗</>}, |
| | | header: ( |
| | | <Space> |
| | | {new Date(chat.created).toLocaleString()} |
| | | </Space> |
| | | ), |
| | | typing: { suffix: <>💗</> }, |
| | | header: <Space>{new Date(chat.created).toLocaleString()}</Space>, |
| | | loading: chat.loading, |
| | | loadingRender: () => ( |
| | | <Space> |
| | | <Spin size="small"/> |
| | | <Spin size="small" /> |
| | | AI正在思考中... |
| | | </Space> |
| | | ), |
| | | footer: ( |
| | | <Space> |
| | | {(chat.role === 'assistant') && ( |
| | | {chat.role === "assistant" && ( |
| | | <Button |
| | | color="default" |
| | | variant="text" |
| | | size="small" |
| | | icon={<SyncOutlined/>} |
| | | icon={<SyncOutlined />} |
| | | onClick={() => handleRegenerate(index)} |
| | | /> |
| | | )} |
| | |
| | | color="default" |
| | | variant="text" |
| | | size="small" |
| | | icon={<CopyOutlined/>} |
| | | icon={<CopyOutlined />} |
| | | onClick={async () => { |
| | | try { |
| | | await navigator.clipboard.writeText(chat.content); |
| | | message.success('复制成功'); |
| | | message.success("复制成功"); |
| | | } catch (error) { |
| | | message.error('复制失败'); |
| | | message.error("复制失败"); |
| | | } |
| | | }} |
| | | /> |
| | | </Space> |
| | | ), |
| | | role: chat.role === 'user' ? 'local' : 'ai', |
| | | role: chat.role === "user" ? "local" : "ai", |
| | | content: ( |
| | | <div className="markdown-body"> |
| | | {chat.role === 'assistant' ? ( |
| | | {chat.role === "assistant" ? ( |
| | | <ReactMarkdown |
| | | remarkPlugins={[remarkGfm, remarkBreaks]} |
| | | rehypePlugins={[ |
| | | rehypeRaw, |
| | | rehypeSanitize, |
| | | rehypeHighlight, |
| | | ]} |
| | | rehypePlugins={[rehypeRaw, rehypeSanitize, rehypeHighlight]} |
| | | components={{ |
| | | code({ node, inline, className, children, ...props }) { |
| | | const match = /language-(\w+)/.exec(className || ''); |
| | | return !inline ? ( |
| | | code({ node, className, children, ...props }) { |
| | | const match = /language-(\w+)/.exec(className || ""); |
| | | const isInline = !!( |
| | | node?.position?.start && |
| | | node.position.end && |
| | | node.position.start.line === node.position.end.line |
| | | ); |
| | | |
| | | return !isInline ? ( |
| | | <SyntaxHighlighter |
| | | language={match?.[1] || 'text'} |
| | | style={ghcolors} |
| | | language={match?.[1] || "text"} |
| | | style={codeStyle as { [key: string]: React.CSSProperties }} |
| | | PreTag="div" |
| | | {...props} |
| | | > |
| | | {String(children).replace(/\n$/, '')} |
| | | {String(children).replace(/\n$/, "")} |
| | | </SyntaxHighlighter> |
| | | ) : ( |
| | | <code className={className} {...props}> |
| | |
| | | )} |
| | | </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' } }, |
| | | 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'}}} |
| | | roles={{ ai: { placement: "start" }, local: { placement: "end" } }} |
| | | /> |
| | | ); |
| | | }; |
| | |
| | | return ( |
| | | <div |
| | | style={{ |
| | | width: '100%', |
| | | height: '100%', |
| | | display: 'flex', |
| | | flexDirection: 'column', |
| | | background: '#fff', |
| | | border: '1px solid #f3f3f3', |
| | | width: "100%", |
| | | height: "100%", |
| | | display: "flex", |
| | | flexDirection: "column", |
| | | background: "#fff", |
| | | border: "1px solid #f3f3f3", |
| | | ...appStyle, |
| | | ...style, |
| | | }} |
| | |
| | | ref={messagesContainerRef} |
| | | style={{ |
| | | flex: 1, |
| | | overflowY: 'auto', |
| | | padding: '16px', |
| | | scrollbarWidth: 'none', |
| | | overflowY: "auto", |
| | | padding: "16px", |
| | | scrollbarWidth: "none", |
| | | }} |
| | | > |
| | | {loading ? ( |
| | | <Spin tip="加载中..."/> |
| | | <Spin tip="加载中..." /> |
| | | ) : ( |
| | | <> |
| | | {renderMessages()} |
| | | <div ref={messagesEndRef}/> |
| | | <div ref={messagesEndRef} /> |
| | | </> |
| | | )} |
| | | </div> |
| | | <div |
| | | style={{ |
| | | borderTop: '1px solid #eee', |
| | | padding: '12px', |
| | | display: 'flex', |
| | | gap: '8px', |
| | | }} |
| | | > |
| | | {/* <Sender |
| | | <div style={{ borderTop: "1px solid #eee", padding: "12px" }}> |
| | | {uploadedFiles.length > 0 && ( |
| | | <div |
| | | style={{ |
| | | marginBottom: 8, |
| | | padding: 8, |
| | | background: "#f5f5f5", |
| | | borderRadius: 4, |
| | | display: "flex", |
| | | flexWrap: "wrap", |
| | | gap: 8, |
| | | }} |
| | | > |
| | | {uploadedFiles.map((name) => ( |
| | | <div |
| | | key={name} |
| | | style={{ |
| | | display: "flex", |
| | | alignItems: "center", |
| | | padding: "4px 8px", |
| | | background: "#fff", |
| | | borderRadius: 4, |
| | | border: "1px solid #d9d9d9", |
| | | }} |
| | | > |
| | | <span style={{ marginRight: 4 }}>📄</span> |
| | | <span |
| | | style={{ |
| | | maxWidth: 200, |
| | | overflow: "hidden", |
| | | textOverflow: "ellipsis", |
| | | whiteSpace: "nowrap", |
| | | }} |
| | | > |
| | | {name} |
| | | </span> |
| | | {/* 添加删除按钮 */} |
| | | <Button |
| | | type="text" |
| | | size="small" |
| | | icon={<DeleteOutlined />} |
| | | onClick={() => { |
| | | handleRemoveFile(name); |
| | | // setUploadedFiles(uploadedFiles.filter((f) => f !== name)); |
| | | // setFileList(fileList.filter((f) => f.name !== name)); |
| | | }} |
| | | /> |
| | | </div> |
| | | ))} |
| | | </div> |
| | | )} |
| | | |
| | | <Sender |
| | | value={content} |
| | | onChange={setContent} |
| | | onSubmit={handleSubmit} |
| | | loading={sendLoading || isStreaming} |
| | | actions={(_, info) => ( |
| | | <Space size="small"> |
| | | {(botTypeId === 2 || botTypeId === 3) && ( |
| | | <Upload |
| | | showUploadList={false} |
| | | beforeUpload={(file) => { |
| | | handleUpload(file); |
| | | return false; |
| | | }} |
| | | fileList={fileList} |
| | | > |
| | | <Button |
| | | icon={<UploadOutlined />} |
| | | loading={fileList.some((f) => f.status === "uploading")} |
| | | > |
| | | 上传文件 |
| | | </Button> |
| | | </Upload> |
| | | )} |
| | | <info.components.ClearButton |
| | | disabled={false} |
| | | title="删除对话记录" |
| | |
| | | /> |
| | | <info.components.SendButton |
| | | type="primary" |
| | | icon={<OpenAIOutlined/>} |
| | | 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> |
| | | ); |
| | |
| | | import { Conversations, ConversationsProps } from "@ant-design/x"; |
| | | import { createStyles } from "antd-style"; |
| | | import React, { useEffect, useRef, useState } from "react"; |
| | | 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'; |
| | | 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 { |
| | | 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"; |
| | | 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; |
| | | `, |
| | | }; |
| | | 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) => { |
| | | 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'; |
| | | 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>(""); |
| | | |
| | | updateExternalSessionId(uuid()) |
| | | getConversationManualGet( |
| | | { |
| | | params: { "botId": params?.id } |
| | | } |
| | | ).then((r: any) => { |
| | | setActiveKey(getExternalSessionId()); |
| | | setConversationsItems(getConversations(r?.data?.data?.cons)); |
| | | }); |
| | | }, []) |
| | | // ==================== Style ==================== |
| | | const { styles } = useStyle(); |
| | | |
| | | 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, |
| | | // ==================== 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, |
| | | // 是externalBot页面提交的消息记录 |
| | | isExternalMsg: 1 |
| | | }, |
| | | }).then((r: any) => { |
| | | setChats(r?.data.data); |
| | | }, |
| | | }).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 logoNode = ( |
| | | 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]); |
| | | |
| | | <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> |
| | | 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 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 = () => { |
| | | }; |
| | | 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); |
| | | }; |
| | | // ==================== 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> |
| | | ); |
| | | } |
| | | }); |
| | | }; |
| | | const hideModal = () => { |
| | | setOpen(false); |
| | | }; |
| | | const [uploadedFileId, setUploadedFileId] = useState<string>(""); |
| | | const [uploadedFileIds, setUploadedFileIds] = useState<string[]>([]); // 改为数组存储 |
| | | const [botTypeId, setBotTypeId] = useState<number>(0); // 新增 botTypeId 状态 |
| | | |
| | | // 获取机器人信息时同时获取 botTypeId |
| | | useEffect(() => { |
| | | doGetBotInfo({ params: { id: params?.id } }).then((r: any) => { |
| | | setBotTypeId(r?.data?.data?.botTypeId); // 假设返回数据中包含 botTypeId |
| | | // ...其他原有逻辑... |
| | | }); |
| | | }, []); |
| | | |
| | | // 处理文件上传回调 |
| | | const handleFileUploaded = (fileId: string) => { |
| | | if (botTypeId === 2) { |
| | | setUploadedFileIds([fileId]); |
| | | } else if (botTypeId === 3) { |
| | | setUploadedFileIds(prev => |
| | | prev.includes(fileId) ? prev : [...prev, fileId] |
| | | ); |
| | | } |
| | | }; |
| | | |
| | | |
| | | // 处理文件删除回调 |
| | | const handleFileRemoved = (removedFileId?: string) => { |
| | | if (!removedFileId) return; |
| | | console.log("handleFileRemoved called with fileId:", removedFileId); |
| | | |
| | | if (botTypeId === 2) { |
| | | setUploadedFileIds([]); |
| | | } else if (botTypeId === 3) { |
| | | setUploadedFileIds(prev => |
| | | prev.filter(id => id && id !== removedFileId) |
| | | ); |
| | | } |
| | | }; |
| | | |
| | | |
| | | |
| | | // 新增:重置文件状态的函数 |
| | | const resetFileState = () => { |
| | | setUploadedFileIds([]); |
| | | // 如果需要同时清空文件列表显示,可以在这里添加 |
| | | }; |
| | | // ==================== 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 |
| | | botTypeId={botTypeId} // 传递 botTypeId |
| | | botId={params.id} // 传递 botId |
| | | onFileUploaded={handleFileUploaded} |
| | | onFileRemoved={handleFileRemoved} |
| | | chats={chats} |
| | | onChatsChange={setChats} // 确保正确传递 onChatsChange |
| | | helloMessage="欢迎使用仁智企 ,我是你的专属机器人,有什么问题可以随时问我。" |
| | | botAvatar={botInfo?.data?.icon} |
| | | request={async (messages) => { |
| | | const readableStream = new ReadableStream({ |
| | | async start(controller) { |
| | | const encoder = new TextEncoder(); |
| | | const requestData: any = { |
| | | botId: params.id, |
| | | sessionId: getExternalSessionId(), |
| | | prompt: messages[messages.length - 1].content as string, |
| | | isExternalMsg: 1, |
| | | }; |
| | | |
| | | // 根据 botTypeId 添加不同的文件参数 |
| | | if (botTypeId === 2 && uploadedFileIds.length > 0) { |
| | | requestData.file = uploadedFileIds[0]; |
| | | } else if (botTypeId === 3 && uploadedFileIds.length > 0) { |
| | | requestData.files = Array.from(new Set(uploadedFileIds)).filter(id => id); |
| | | } |
| | | |
| | | startChat({ |
| | | data: requestData, |
| | | onMessage: (msg) => { |
| | | controller.enqueue(encoder.encode(msg)); |
| | | }, |
| | | onFinished: () => { |
| | | controller.close(); |
| | | resetFileState(); |
| | | }, |
| | | onError: () => { |
| | | resetFileState(); |
| | | }, |
| | | }); |
| | | }, |
| | | }); |
| | | return new Response(readableStream); |
| | | }} |
| | | |
| | | /> |
| | | </div> |
| | | </div> |
| | | ); |
| | | }; |
| | | |
| | | export default { |
| | | path: "/ai/externalBot/:id", |
| | | element: ExternalBot, |
| | | frontEnable: true, |
| | | path: "/ai/externalBot/:id", |
| | | element: ExternalBot, |
| | | frontEnable: true, |
| | | }; |
| | |
| | | const map: Record<string, string> = { |
| | | "-1": "无", |
| | | "1": "对话模式", |
| | | "2": "文件工作流模式" |
| | | "2": "单文件工作流模式", |
| | | "3": "多文件工作流模式" |
| | | }; |
| | | return map[valueStr] || valueStr; // 未知值原样显示 |
| | | }, |
| | |
| | | options: [ |
| | | { value: -1, label: "无" }, // 表单中建议用数字(与接口一致) |
| | | { value: 1, label: "对话模式" }, |
| | | { value: 2, label: "文件工作流模式" }, |
| | | { value: 2, label: "单文件工作流模式" }, |
| | | { value: 3, label: "多文件工作流模式" }, |
| | | ], |
| | | }, |
| | | }, |