|
@@ -12,14 +12,17 @@ import ShareIcon from "../icons/share.svg";
|
|
import BotIcon from "../icons/bot.png";
|
|
import BotIcon from "../icons/bot.png";
|
|
|
|
|
|
import DownloadIcon from "../icons/download.svg";
|
|
import DownloadIcon from "../icons/download.svg";
|
|
-import { useMemo, useRef, useState } from "react";
|
|
|
|
|
|
+import { useEffect, useMemo, useRef, useState } from "react";
|
|
import { MessageSelector, useMessageSelector } from "./message-selector";
|
|
import { MessageSelector, useMessageSelector } from "./message-selector";
|
|
import { Avatar } from "./emoji";
|
|
import { Avatar } from "./emoji";
|
|
import dynamic from "next/dynamic";
|
|
import dynamic from "next/dynamic";
|
|
import NextImage from "next/image";
|
|
import NextImage from "next/image";
|
|
|
|
|
|
-import { toBlob, toPng } from "html-to-image";
|
|
|
|
|
|
+import { toBlob, toJpeg, toPng } from "html-to-image";
|
|
import { DEFAULT_MASK_AVATAR } from "../store/mask";
|
|
import { DEFAULT_MASK_AVATAR } from "../store/mask";
|
|
|
|
+import { api } from "../client/api";
|
|
|
|
+import { prettyObject } from "../utils/format";
|
|
|
|
+import { EXPORT_MESSAGE_CLASS_NAME } from "../constant";
|
|
|
|
|
|
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
|
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
|
loading: () => <LoadingIcon />,
|
|
loading: () => <LoadingIcon />,
|
|
@@ -214,37 +217,127 @@ export function MessageExporter() {
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+export function RenderExport(props: {
|
|
|
|
+ messages: ChatMessage[];
|
|
|
|
+ onRender: (messages: ChatMessage[]) => void;
|
|
|
|
+}) {
|
|
|
|
+ const domRef = useRef<HTMLDivElement>(null);
|
|
|
|
+
|
|
|
|
+ useEffect(() => {
|
|
|
|
+ if (!domRef.current) return;
|
|
|
|
+ const dom = domRef.current;
|
|
|
|
+ const messages = Array.from(
|
|
|
|
+ dom.getElementsByClassName(EXPORT_MESSAGE_CLASS_NAME),
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ if (messages.length !== props.messages.length) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const renderMsgs = messages.map((v) => {
|
|
|
|
+ const [_, role] = v.id.split(":");
|
|
|
|
+ return {
|
|
|
|
+ role: role as any,
|
|
|
|
+ content: v.innerHTML,
|
|
|
|
+ date: "",
|
|
|
|
+ };
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ props.onRender(renderMsgs);
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ return (
|
|
|
|
+ <div ref={domRef}>
|
|
|
|
+ {props.messages.map((m, i) => (
|
|
|
|
+ <div
|
|
|
|
+ key={i}
|
|
|
|
+ id={`${m.role}:${i}`}
|
|
|
|
+ className={EXPORT_MESSAGE_CLASS_NAME}
|
|
|
|
+ >
|
|
|
|
+ <Markdown content={m.content} defaultShow />
|
|
|
|
+ </div>
|
|
|
|
+ ))}
|
|
|
|
+ </div>
|
|
|
|
+ );
|
|
|
|
+}
|
|
|
|
+
|
|
export function PreviewActions(props: {
|
|
export function PreviewActions(props: {
|
|
download: () => void;
|
|
download: () => void;
|
|
copy: () => void;
|
|
copy: () => void;
|
|
showCopy?: boolean;
|
|
showCopy?: boolean;
|
|
|
|
+ messages?: ChatMessage[];
|
|
}) {
|
|
}) {
|
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
|
+ const [shouldExport, setShouldExport] = useState(false);
|
|
|
|
+
|
|
|
|
+ const onRenderMsgs = (msgs: ChatMessage[]) => {
|
|
|
|
+ setShouldExport(false);
|
|
|
|
+
|
|
|
|
+ api
|
|
|
|
+ .share(msgs)
|
|
|
|
+ .then((res) => {
|
|
|
|
+ if (!res) return;
|
|
|
|
+ copyToClipboard(res);
|
|
|
|
+ setTimeout(() => {
|
|
|
|
+ window.open(res, "_blank");
|
|
|
|
+ }, 800);
|
|
|
|
+ })
|
|
|
|
+ .catch((e) => {
|
|
|
|
+ console.error("[Share]", e);
|
|
|
|
+ showToast(prettyObject(e));
|
|
|
|
+ })
|
|
|
|
+ .finally(() => setLoading(false));
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const share = async () => {
|
|
|
|
+ if (props.messages?.length) {
|
|
|
|
+ setLoading(true);
|
|
|
|
+ setShouldExport(true);
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
return (
|
|
return (
|
|
- <div className={styles["preview-actions"]}>
|
|
|
|
- {props.showCopy && (
|
|
|
|
|
|
+ <>
|
|
|
|
+ <div className={styles["preview-actions"]}>
|
|
|
|
+ {props.showCopy && (
|
|
|
|
+ <IconButton
|
|
|
|
+ text={Locale.Export.Copy}
|
|
|
|
+ bordered
|
|
|
|
+ shadow
|
|
|
|
+ icon={<CopyIcon />}
|
|
|
|
+ onClick={props.copy}
|
|
|
|
+ ></IconButton>
|
|
|
|
+ )}
|
|
<IconButton
|
|
<IconButton
|
|
- text={Locale.Export.Copy}
|
|
|
|
|
|
+ text={Locale.Export.Download}
|
|
bordered
|
|
bordered
|
|
shadow
|
|
shadow
|
|
- icon={<CopyIcon />}
|
|
|
|
- onClick={props.copy}
|
|
|
|
|
|
+ icon={<DownloadIcon />}
|
|
|
|
+ onClick={props.download}
|
|
></IconButton>
|
|
></IconButton>
|
|
- )}
|
|
|
|
- <IconButton
|
|
|
|
- text={Locale.Export.Download}
|
|
|
|
- bordered
|
|
|
|
- shadow
|
|
|
|
- icon={<DownloadIcon />}
|
|
|
|
- onClick={props.download}
|
|
|
|
- ></IconButton>
|
|
|
|
- <IconButton
|
|
|
|
- text={Locale.Export.Share}
|
|
|
|
- bordered
|
|
|
|
- shadow
|
|
|
|
- icon={<ShareIcon />}
|
|
|
|
- onClick={() => showToast(Locale.WIP)}
|
|
|
|
- ></IconButton>
|
|
|
|
- </div>
|
|
|
|
|
|
+ <IconButton
|
|
|
|
+ text={Locale.Export.Share}
|
|
|
|
+ bordered
|
|
|
|
+ shadow
|
|
|
|
+ icon={loading ? <LoadingIcon /> : <ShareIcon />}
|
|
|
|
+ onClick={share}
|
|
|
|
+ ></IconButton>
|
|
|
|
+ </div>
|
|
|
|
+ <div
|
|
|
|
+ style={{
|
|
|
|
+ position: "fixed",
|
|
|
|
+ right: "200vw",
|
|
|
|
+ pointerEvents: "none",
|
|
|
|
+ }}
|
|
|
|
+ >
|
|
|
|
+ {shouldExport && (
|
|
|
|
+ <RenderExport
|
|
|
|
+ messages={props.messages ?? []}
|
|
|
|
+ onRender={onRenderMsgs}
|
|
|
|
+ />
|
|
|
|
+ )}
|
|
|
|
+ </div>
|
|
|
|
+ </>
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
@@ -323,7 +416,12 @@ export function ImagePreviewer(props: {
|
|
|
|
|
|
return (
|
|
return (
|
|
<div className={styles["image-previewer"]}>
|
|
<div className={styles["image-previewer"]}>
|
|
- <PreviewActions copy={copy} download={download} showCopy={!isMobile} />
|
|
|
|
|
|
+ <PreviewActions
|
|
|
|
+ copy={copy}
|
|
|
|
+ download={download}
|
|
|
|
+ showCopy={!isMobile}
|
|
|
|
+ messages={props.messages}
|
|
|
|
+ />
|
|
<div
|
|
<div
|
|
className={`${styles["preview-body"]} ${styles["default-theme"]}`}
|
|
className={`${styles["preview-body"]} ${styles["default-theme"]}`}
|
|
ref={previewRef}
|
|
ref={previewRef}
|
|
@@ -417,7 +515,11 @@ export function MarkdownPreviewer(props: {
|
|
|
|
|
|
return (
|
|
return (
|
|
<>
|
|
<>
|
|
- <PreviewActions copy={copy} download={download} />
|
|
|
|
|
|
+ <PreviewActions
|
|
|
|
+ copy={copy}
|
|
|
|
+ download={download}
|
|
|
|
+ messages={props.messages}
|
|
|
|
+ />
|
|
<div className="markdown-body">
|
|
<div className="markdown-body">
|
|
<pre className={styles["export-content"]}>{mdText}</pre>
|
|
<pre className={styles["export-content"]}>{mdText}</pre>
|
|
</div>
|
|
</div>
|