jialh
2025-06-07 51df74a3e9244ef2fe2885b7edc8cb1dcd169f52
备份
8个文件已修改
3个文件已添加
16861 ■■■■ 已修改文件
.npmrc 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
package-lock.json 9739 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AiChatModal/index.tsx 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AiProChat/AiProChat.tsx 794 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AiProChat/markdown-styles.css 196 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/EditPage/index.tsx 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/ExternalBot copy.tsx 385 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/ai/Bots.tsx 70 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/ai/second.tsx 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yarn.lock 5660 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.npmrc
@@ -1,2 +1,4 @@
auto-install-peers=true
registry=https://registry.npmjs.org/
# registry=https://registry.npmjs.org/
registry=https://registry.npmmirror.com
package-lock.json
Diff too large
package.json
@@ -14,6 +14,8 @@
    "@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",
@@ -38,10 +40,12 @@
    "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",
src/components/AiChatModal/index.tsx
@@ -57,7 +57,7 @@
                    onBlur={() => {
                    }}
                >
                    AIFlowy 智能助理
                    仁智企智能助理
                </div>
            }
            footer={<></>}
src/components/AiProChat/AiProChat.tsx
@@ -1,390 +1,458 @@
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>
  );
};
src/components/AiProChat/markdown-styles.css
New file
@@ -0,0 +1,196 @@
/* =============== 基础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;
  }
src/components/EditPage/index.tsx
@@ -3,7 +3,7 @@
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;
src/pages/ExternalBot copy.tsx
New file
@@ -0,0 +1,385 @@
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,
};
src/pages/ai/Bots.tsx
@@ -91,9 +91,8 @@
  ); // 二级分类ID
  // 设置列配置的状态
  const [columnsConfig, setColumnsConfig] = useState<ColumnsConfig<any>>(
    baseColumns
  );
  const [columnsConfig, setColumnsConfig] =
    useState<ColumnsConfig<any>>(baseColumns);
  // 设置默认的一级分类
  useEffect(() => {
@@ -113,6 +112,36 @@
      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",
@@ -127,8 +156,9 @@
          key: "modelKEY",
          placeholder: "请输入外接智能体的KEY",
          form: {
            extra: "输入外接智能体API和KEY后,当前智能体本地的知识库以及工作流配置将自动忽略"
          }
            extra:
              "输入外接智能体API和KEY后,当前智能体本地的知识库以及工作流配置将自动忽略",
          },
        }
      );
@@ -196,19 +226,23 @@
          >
            全部
          </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" }}>
src/pages/ai/second.tsx
@@ -45,11 +45,12 @@
    {
        supportSearch: true,
        form: {
            type: "input"
            type: "input",
            rules: [{required: true, message: "请输入名称"}],
        },
        dataIndex: "secondMenuName",
        title: "名称",
        key: "secondMenuName"
        key: "secondMenuName",
    },
];
yarn.lock
New file
Diff too large