jialh
2025-06-10 4d421ddfed1b725eabbed81eab113b4e440517c1
src/components/AiProChat/AiProChat.tsx
@@ -15,17 +15,8 @@
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) {
@@ -63,10 +54,10 @@
  botAvatar?: string;
  request: (messages: ChatMessage[]) => Promise<Response>;
  clearMessage?: () => void;
  botId?: string; // 新增 botId 参数
  botId?: string;
  botTypeId?: number;
  onFileUploaded?: (fileId: string) => void; // 新增文件上传回调
  onFileRemoved?: (removedFileId?: string) => void; // 新增文件删除回调
  onFileUploaded?: (fileId: string) => void;
  onFileRemoved?: (removedFileId?: string) => void;
};
export const AiProChat = ({
@@ -79,10 +70,10 @@
  botAvatar = `${logo}`,
  request,
  clearMessage,
  botId, // 新增
  botId,
  botTypeId,
  onFileUploaded, // 新增
  onFileRemoved, // 新增
  onFileUploaded,
  onFileRemoved,
}: AiProChatProps) => {
  const isControlled =
    parentChats !== undefined && parentOnChatsChange !== undefined;
@@ -110,38 +101,34 @@
    }
  }, [chats]);
  const handleRemoveFile = (name: string) => {
    const fileToRemove = fileList.find(f => f.name === name);
    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);
    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 => [
    setFileList((prev) => [
      ...prev,
      {
        uid: file.name,
@@ -149,7 +136,7 @@
        status: "uploading",
      },
    ]);
    const formData = new FormData();
    const tokenKey = `${import.meta.env.VITE_APP_TOKEN_KEY}`;
    formData.append("file", file);
@@ -167,9 +154,9 @@
      const result = await res.json();
      const fileData = JSON.parse(result.data.trim());
      setFileList(prev =>
        prev.map(f =>
          f.name === file.name
      setFileList((prev) =>
        prev.map((f) =>
          f.name === file.name
            ? { ...f, uid: fileData.name, status: "done", url: fileData.id }
            : f
        )
@@ -177,17 +164,12 @@
      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
        )
      setFileList((prev) =>
        prev.map((f) => (f.name === file.name ? { ...f, status: "error" } : f))
      );
      messageApi.error((error as Error).message || "上传失败");
      throw error;
@@ -368,122 +350,113 @@
    }
  };
  const renderMessages = () => {
    if (!chats?.length) {
      return (
        <Welcome
          variant="borderless"
          icon={
 // 修改 renderMessages 函数中的 ReactMarkdown 部分
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={() => 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]}
                components={{
                  code({ node, className, children, ...props }) {
                    const match = /language-(\w+)/.exec(className || "");
                    return match ? (
                      <pre className={className} {...props as React.HTMLAttributes<HTMLPreElement>}>
                        <code>{children}</code>
                      </pre>
                    ) : (
                      <code className={className} {...props as React.HTMLAttributes<HTMLElement>}>
                        {children}
                      </code>
                    );
                  },
                }}
              >
                {chat.content}
              </ReactMarkdown>
            ) : (
              chat.content
            )}
          </div>
        ),
        avatar:
          chat.role === "assistant" ? (
            <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>
          ) : (
            {
              icon: <UserOutlined />,
              style: { color: "#fff", backgroundColor: "#87d068" },
            }
          ),
          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, 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
                      );
      }))}
      roles={{ ai: { placement: "start" }, local: { placement: "end" } }}
    />
  );
};
                      return !isInline ? (
                        <SyntaxHighlighter
                          language={match?.[1] || "text"}
                          style={codeStyle as { [key: string]: React.CSSProperties }}
                          PreTag="div"
                          {...props}
                        >
                          {String(children).replace(/\n$/, "")}
                        </SyntaxHighlighter>
                      ) : (
                        <code className={className} {...props}>
                          {children}
                        </code>
                      );
                    },
                  }}
                >
                  {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: { color: "#fff", backgroundColor: "#87d068" },
              }
            ),
        }))}
        roles={{ ai: { placement: "start" }, local: { placement: "end" } }}
      />
    );
  };
  return (
    <div
@@ -552,15 +525,12 @@
                >
                  {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>