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, writeModel } from "@/app/constant"; import { useLocation, useNavigate } from "react-router-dom"; import { getClientConfig } from "@/app/config/client"; import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { useAppConfig, useChatStore, useMindMapStore } from "@/app/store"; import { ChatAction } from "../chat"; import { useWindowSize } from "@/app/utils"; import { exportHtmlToWord } from "@/app/utils/fileExport/word"; 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 { Button, Dropdown, MenuProps, message as msgModal, Space } from "antd"; import { HTMLPreview } from "../artifacts"; import { getMindPrompt, getWrtingPrompt } from "@/app/utils/prompt"; import { htmlToPdf2 } from "@/app/utils/fileExport/toPdf"; import { hasTable, htmlToExcel } from "@/app/utils/fileExport/export2Excel"; import dynamic from "next/dynamic"; import { getNavgParam, rewriteItemsMap } from "./menuData"; import { RequestMessage } from "@/app/typing"; const EditorComponent = dynamic( async () => (await import("./editor")).EditorComponent, { loading: () => null, }, ); export function WritingPage() { const chatStore = useChatStore(); 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 [htmlHeader, setHtmlheader] = useState(""); const [htmlCode, setHtmlCode] = useState<string>( localStorage.getItem("htmlCode") || "", ); const query = useLocation(); //获取路由参数 let { msg, writeMessage } = query.state || {}; //获取路由参数 const items: MenuProps["items"] = Object.entries(rewriteItemsMap).map( ([key, value]) => ({ key, label: ( <a target="_blank" rel="noopener noreferrer" href="#" onClick={(e) => { e.preventDefault(); // 阻止默认行为 rewrite(value); // 调用 rewrite 函数并传递对应的值 }} > {key} </a> ), }), ); const requestWrite = async (content: string, messages: RequestMessage[]) => { try { setLoading(true); messages.push({ role: "user", content: content }); const response = await chatStore.sendContext(messages, writeModel); messages.push({ role: "assistant", content: response }); let cleanedContent = response.startsWith("```html") ? response.substring(8) : response; if (cleanedContent.endsWith("```")) { cleanedContent = cleanedContent.substring(0, cleanedContent.length - 4); } const bodyTagRegex = /<body[^>]*>/i; const bodyTagMatch = cleanedContent.match(bodyTagRegex); if (bodyTagMatch?.index !== undefined) { setHtmlheader( cleanedContent.slice(0, bodyTagMatch.index + bodyTagMatch[0].length), ); } localStorage.setItem("htmlCode", cleanedContent); localStorage.setItem("aiWrite", JSON.stringify(messages)); setHtmlCode(cleanedContent); } catch (error) { msgModal.error("生成失败,请重试"); } finally { setLoading(false); } }; useEffect(() => { if (!msg) return; if (!writeMessage) return; const navigateGetData = async () => { let messages = []; const input = getWrtingPrompt(getNavgParam(writeMessage)); console.log("------------------------" + input); requestWrite(input, []); }; navigateGetData(); }, []); function wrapContentInDivWithWidth(html: string): string { const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); const body = doc.body; const centerStyle = "display: flex;flex-direction: column;justify-content: center;align-items: center;margin:0"; body.style.cssText += centerStyle; if (!body) { return `<html><head><meta charset="UTF-8"></head> <body style="${centerStyle}"> <div style="width: ${width}">${html}</div></body></html>`; } // 创建一个新的<div>,并设置宽度 const wrapperDiv = doc.createElement("div"); wrapperDiv.style.width = width; // 将<body>中的所有子节点移到<div>中 while (body.firstChild) { wrapperDiv.appendChild(body.firstChild); } // 将<div>添加到<body>中 body.appendChild(wrapperDiv); // 将修改后的DOM转换回HTML字符串 return doc.documentElement.outerHTML; } const handleCopy = async () => { try { const blob = new Blob([htmlCode], { type: "text/html" }); const clipboardItem = new ClipboardItem({ "text/html": blob }); await navigator.clipboard.write([clipboardItem]); msgModal.success("复制成功!"); } catch (error) { msgModal.error("复制失败"); } }; //跳转到ppt页面 function toPowerpoint(pptMessage: string) { navigate(Path.Powerpoint, { state: { msg: true, pptMessage: pptMessage } }); } // 转至思维导图页面 const toMind = useCallback( (content: string) => { const { setMindMapData } = useMindMapStore.getState(); setMindMapData( [ { role: "user", content: getMindPrompt(content, true), }, ], content, ); navigate(Path.Mind, { state: { msg: true } }); }, [navigate], ); //导出html文件 const exportHtml = useCallback(() => { try { const htmlContent = wrapContentInDivWithWidth(htmlCode); const parser = new DOMParser(); const doc = parser.parseFromString(htmlContent, "text/html"); // 提取<h1>标签的内容作为文件名 let fileName = "output.html"; // 默认文件名 const h1Element = doc.querySelector("h1"); if (h1Element && h1Element.textContent) { // 使用<h1>的内容作为文件名,并清理非法字符 fileName = h1Element.textContent .trim() .replace(/[\u0000-\u001f\\?*:"<>|]/g, "") + ".html"; } const blob = new Blob([htmlContent], { type: "text/html" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = fileName; a.click(); URL.revokeObjectURL(url); msgModal.success("导出成功"); } catch (error) { msgModal.error("导出失败"); } }, [wrapContentInDivWithWidth]); async function rewrite(content: string) { const messagesStr = localStorage.getItem("aiWrite"); if (!messagesStr) return; let messages: RequestMessage[]; try { messages = JSON.parse(messagesStr); } catch (error) { return; } // 检查是否是数组且包含合法 message 对象 if (!Array.isArray(messages)) return; if ( !messages.every( (m) => typeof m === "object" && m !== null && "role" in m && "content" in m && typeof m.role === "string" && typeof m.content === "string", ) ) return; requestWrite(content, messages); } useEffect(() => { localStorage.setItem("htmlCode", htmlCode); }, [htmlCode]); 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"]}> <Dropdown menu={{ items }} placement="bottom" arrow={{ pointAtCenter: true }} > <Button> <Space> <ReloadIcon /> {Locale.Chat.Actions.ReWrite} </Space> </Button> </Dropdown> <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); const html = wrapContentInDivWithWidth(htmlCode); await htmlToPdf2(html); setLoading(false); }} disabled={isEdit} /> <ChatAction text={Locale.Export.Word} icon={<WordIcon />} onClick={() => { const html = wrapContentInDivWithWidth(htmlCode); 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 ? ( <EditorComponent htmlCode={htmlCode} setHtmlCode={setHtmlCode} /> ) : ( <HTMLPreview code={htmlCode} autoHeight={!document.fullscreenElement} height={!document.fullscreenElement ? "100%" : height} width={width} /> )) )} </div> </div> </WindowContent> </> ); }