writing.tsx 10.6 KB
import chatStyles from "@/app/components/chat.module.scss";
import homeStyles from "@/app/components/home.module.scss";
import styles from "@/app/components/writing/writing.module.scss";
import clsx from "clsx";

import { WriteSiderBar } from "./write-siderBar";
import { WindowContent } from "@/app/components/home";
import { useMobileScreen } from "@/app/utils";
import { IconButton } from "../button";
import Locale from "@/app/locales";
import { Path } from "@/app/constant";
import { useNavigate } from "react-router-dom";
import { getClientConfig } from "@/app/config/client";
import React, { useCallback, useMemo, useRef, useState } from "react";
import { useAppConfig, useMindMapStore } from "@/app/store";
import { ChatAction } from "../chat";
import { useWindowSize } from "@/app/utils";
import { exportHtmlToWord } from "@/app/utils/fileExport/word";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";

import ReturnIcon from "@/app/icons/return.svg";
import MinIcon from "@/app/icons/min.svg";
import MaxIcon from "@/app/icons/max.svg";
import SDIcon from "@/app/icons/sd.svg";
import LoadingIcon from "@/app/icons/three-dots.svg";
import BotIcon from "@/app/icons/bot.svg";
import EditIcon from "@/app/icons/rename.svg";
import ReloadIcon from "@/app/icons/reload.svg";
import CopyIcon from "@/app/icons/copy.svg";
import ExcelIcon from "@/app/icons/excel.svg";
import WordIcon from "@/app/icons/word.svg";
import MindIcon from "@/app/icons/mind.svg";
import PptIcon from "@/app/icons/ppt.svg";
import PdfIcon from "@/app/icons/pdf.svg";
import HtmlIcon from "@/app/icons/HTML.svg";

import { message } from "antd";
import { HTMLPreview } from "../artifacts";
import { getMindPrompt } from "@/app/utils/prompt";
import { htmlToPdf2 } from "@/app/utils/fileExport/toPdf";
import { htmlToExcel } from "@/app/utils/fileExport/export2Excel";

export function WritingPage() {
  const isMobileScreen = useMobileScreen();
  const navigate = useNavigate();
  const clientConfig = useMemo(() => getClientConfig(), []);
  const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
  const config = useAppConfig();
  const scrollRef = useRef<HTMLDivElement>(null);
  const isWriting = location.pathname === Path.Writing;
  const { height } = useWindowSize();
  const [width, setWidth] = useState("100%");
  const [isEdit, setIsEdit] = useState(false);
  const [loading, setLoading] = useState(false);
  const quillRef = useRef<ReactQuill | null>(null);
  const [htmlHeader, setHtmlheader] = useState("");
  const [htmlCode, setHtmlCode] = useState(
    localStorage.getItem("htmlCode") || "",
  );

  //编辑器
  const toolbarOptions = [
    [{ font: [] }, { size: ["small", false, "large", "huge"] }],
    [{ header: [1, 2, 3, 4, 5, 6, false] }],
    ["bold", "italic", "underline", "strike"],
    [{ list: "ordered" }, { list: "bullet" }, { align: [] }],
    [{ color: [] }, { background: [] }],
    ["link", "image"],
  ];

  // 生成完整HTML内容
  const generateFullHtml = useCallback(
    () => `${htmlHeader}${htmlCode}</body></html>`,
    [htmlHeader, htmlCode],
  );

  const handleCopy = async () => {
    try {
      const blob = new Blob([htmlCode], { type: "text/html" });
      const clipboardItem = new ClipboardItem({ "text/html": blob });
      await navigator.clipboard.write([clipboardItem]);
      message.success("复制成功!");
    } catch (error) {
      message.error("复制失败");
    }
  };
  //跳转到ppt页面
  function toPowerpoint(pptMessage: string) {
    navigate("/powerpoint", { state: { msg: true, pptMessage: pptMessage } });
  }
  // 转至思维导图页面
  const toMind = useCallback(
    (content: string) => {
      const { setMindMapData } = useMindMapStore.getState();
      setMindMapData(
        [
          {
            role: "user",
            content: getMindPrompt(content, true),
          },
        ],
        content,
      );
      navigate("/mind", { state: { msg: true } });
    },
    [navigate],
  );

  //导出html文件
  const exportHtml = useCallback(() => {
    try {
      const htmlContent = generateFullHtml();
      const blob = new Blob([htmlContent], { type: "text/html" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = "output.html";
      a.click();
      URL.revokeObjectURL(url);
      message.success("导出成功");
    } catch (error) {
      message.error("导出失败");
    }
  }, [generateFullHtml]);

  function hasTable(htmlContent: string): boolean {
    const parser = new DOMParser();
    const doc = parser.parseFromString(htmlContent, "text/html");
    return doc.querySelector("table") !== null;
  }

  return (
    <>
      <WriteSiderBar
        className={clsx({ [homeStyles["sidebar-show"]]: isWriting })}
        htmlCode={htmlCode}
        setHtmlCode={setHtmlCode}
        loading={loading}
        setLoading={setLoading}
        setWidth={setWidth}
        setHtmlheader={setHtmlheader}
      />
      <WindowContent>
        <div className={chatStyles.chat} key={"1"}>
          <div className="window-header" data-tauri-drag-region>
            {isMobileScreen && (
              <div className="window-actions">
                <div className={"window-action-button"}>
                  <IconButton
                    icon={<ReturnIcon />}
                    bordered
                    title={Locale.Chat.Actions.ChatList}
                    onClick={() => navigate(Path.BgRemoval)}
                  />
                </div>
              </div>
            )}
            <div
              className={clsx(
                "window-header-title",
                chatStyles["chat-body-title"],
              )}
            >
              <div className={`window-header-main-title`}>AI-Writing</div>
            </div>
            <div className={chatStyles["chat-message-actions"]}>
              {htmlCode && (
                <div className={chatStyles["chat-input-actions"]}>
                  <ChatAction
                    text={Locale.Chat.Actions.ReWrite}
                    icon={<ReloadIcon />}
                    onClick={() => {}}
                    disabled={isEdit}
                  />
                  <ChatAction
                    text={Locale.Chat.Actions.Copy}
                    icon={<CopyIcon />}
                    onClick={handleCopy}
                    disabled={isEdit}
                  />
                  {!isEdit ? (
                    <ChatAction
                      text={Locale.Chat.Actions.Edit}
                      icon={<EditIcon />}
                      onClick={() => {
                        setIsEdit(true);
                      }}
                    />
                  ) : (
                    <ChatAction
                      text={Locale.Chat.Actions.CancelEdit}
                      icon={<EditIcon />}
                      onClick={() => {
                        setIsEdit(false);
                      }}
                    />
                  )}
                  <ChatAction
                    text={Locale.Export.Pdf}
                    icon={<PdfIcon />}
                    onClick={async () => {
                      setLoading(true);
                      await htmlToPdf2(htmlCode);
                      setLoading(false);
                    }}
                    disabled={isEdit}
                  />
                  <ChatAction
                    text={Locale.Export.Word}
                    icon={<WordIcon />}
                    onClick={() => {
                      const html = `${htmlHeader}
                                ${htmlCode}
                                </body>
                                </html>
                      `;
                      exportHtmlToWord(html);
                    }}
                    disabled={isEdit}
                  />
                  {hasTable(htmlCode) && (
                    <ChatAction
                      text={Locale.Export.Excel}
                      icon={<ExcelIcon />}
                      onClick={() => {
                        htmlToExcel(htmlCode);
                      }}
                      disabled={isEdit}
                    />
                  )}

                  <ChatAction
                    text={Locale.Export.Ppt}
                    icon={<PptIcon />}
                    onClick={() => {
                      toPowerpoint(htmlCode);
                    }}
                    disabled={isEdit}
                  />
                  <ChatAction
                    text={Locale.Export.Mind.title}
                    icon={<MindIcon />}
                    onClick={() => {
                      toMind(htmlCode);
                    }}
                    disabled={isEdit}
                  />
                  <ChatAction
                    text={Locale.Export.Html}
                    icon={<HtmlIcon />}
                    onClick={() => {
                      exportHtml();
                    }}
                    disabled={isEdit}
                  />
                </div>
              )}
            </div>
            <div className="window-actions">
              {showMaxIcon && (
                <div className="window-action-button">
                  <IconButton
                    aria={Locale.Chat.Actions.FullScreen}
                    icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
                    bordered
                    onClick={() => {
                      config.update(
                        (config) => (config.tightBorder = !config.tightBorder),
                      );
                    }}
                  />
                </div>
              )}
              {isMobileScreen && <SDIcon width={50} height={50} />}
            </div>
          </div>
          <div
            className={`${chatStyles["chat-body"]} ${styles["write-body"]}`}
            ref={scrollRef}
          >
            {loading ? (
              <div className={clsx("no-dark", styles["loading-content"])}>
                <BotIcon />
                <LoadingIcon />
              </div>
            ) : (
              htmlCode &&
              (isEdit ? (
                <ReactQuill
                  ref={quillRef}
                  theme="snow"
                  value={htmlCode}
                  onChange={setHtmlCode}
                  modules={{
                    toolbar: toolbarOptions,
                  }}
                />
              ) : (
                <HTMLPreview
                  code={htmlCode}
                  autoHeight={!document.fullscreenElement}
                  height={!document.fullscreenElement ? "100%" : height}
                  width={width}
                />
              ))
            )}
          </div>
        </div>
      </WindowContent>
    </>
  );
}