/* eslint-disable @next/next/no-img-element */ import { ChatMessage, ModelType, useAppConfig, useChatStore } from "../store"; import Locale from "../locales"; import styles from "./exporter.module.scss"; import { List, ListItem, Modal, Select, showImageModal, showModal, showToast, } from "./ui-lib"; import { IconButton } from "./button"; import { copyToClipboard, downloadAs, useMobileScreen } from "../utils"; import CopyIcon from "../icons/copy.svg"; import LoadingIcon from "../icons/three-dots.svg"; import ChatGptIcon from "../icons/chatgpt.png"; import ShareIcon from "../icons/share.svg"; import BotIcon from "../icons/bot.png"; import DownloadIcon from "../icons/download.svg"; import { useEffect, useMemo, useRef, useState } from "react"; import { MessageSelector, useMessageSelector } from "./message-selector"; import { Avatar } from "./emoji"; import dynamic from "next/dynamic"; import NextImage from "next/image"; import { toBlob, toPng } from "html-to-image"; import { DEFAULT_MASK_AVATAR } from "../store/mask"; import { api } from "../client/api"; import { prettyObject } from "../utils/format"; import { EXPORT_MESSAGE_CLASS_NAME } from "../constant"; import { getClientConfig } from "../config/client"; const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { loading: () => , }); export function ExportMessageModal(props: { onClose: () => void }) { return (
{Locale.Exporter.Description.Title}
} >
); } function useSteps( steps: Array<{ name: string; value: string; }>, ) { const stepCount = steps.length; const [currentStepIndex, setCurrentStepIndex] = useState(0); const nextStep = () => setCurrentStepIndex((currentStepIndex + 1) % stepCount); const prevStep = () => setCurrentStepIndex((currentStepIndex - 1 + stepCount) % stepCount); return { currentStepIndex, setCurrentStepIndex, nextStep, prevStep, currentStep: steps[currentStepIndex], }; } function Steps< T extends { name: string; value: string; }[], >(props: { steps: T; onStepChange?: (index: number) => void; index: number }) { const steps = props.steps; const stepCount = steps.length; return (
{steps.map((step, i) => { return (
{ props.onStepChange?.(i); }} role="button" > {i + 1} {step.name}
); })}
); } export function MessageExporter() { const steps = [ { name: Locale.Export.Steps.Select, value: "select", }, { name: Locale.Export.Steps.Preview, value: "preview", }, ]; const { currentStep, setCurrentStepIndex, currentStepIndex } = useSteps(steps); const formats = ["text", "image", "json"] as const; type ExportFormat = (typeof formats)[number]; const [exportConfig, setExportConfig] = useState({ format: "image" as ExportFormat, includeContext: true, }); function updateExportConfig(updater: (config: typeof exportConfig) => void) { const config = { ...exportConfig }; updater(config); setExportConfig(config); } const chatStore = useChatStore(); const session = chatStore.currentSession(); const { selection, updateSelection } = useMessageSelector(); const selectedMessages = useMemo(() => { const ret: ChatMessage[] = []; if (exportConfig.includeContext) { ret.push(...session.mask.context); } ret.push(...session.messages.filter((m) => selection.has(m.id))); return ret; }, [ exportConfig.includeContext, session.messages, session.mask.context, selection, ]); function preview() { if (exportConfig.format === "text") { return ( ); } else if (exportConfig.format === "json") { return ( ); } else { return ( ); } } return ( <>
{ updateExportConfig( (config) => (config.includeContext = e.currentTarget.checked), ); }} >
{currentStep.value === "preview" && (
{preview()}
)} ); } export function RenderExport(props: { messages: ChatMessage[]; onRender: (messages: ChatMessage[]) => void; }) { const domRef = useRef(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, i) => { const [role, _] = v.id.split(":"); return { id: i.toString(), role: role as any, content: role === "user" ? v.textContent ?? "" : v.innerHTML, date: "", }; }); props.onRender(renderMsgs); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return (
{props.messages.map((m, i) => (
))}
); } export function PreviewActions(props: { download: () => void; copy: () => void; 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; showModal({ title: Locale.Export.Share, children: [ e.currentTarget.select()} >, ], actions: [ } text={Locale.Chat.Actions.Copy} key="copy" onClick={() => 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 ( <>
{props.showCopy && ( } onClick={props.copy} > )} } onClick={props.download} > {/* : }*/} {/* onClick={share}*/} {/*>*/}
{shouldExport && ( )}
); } function ExportAvatar(props: { avatar: string }) { if (props.avatar === DEFAULT_MASK_AVATAR) { return ( bot ); } return ; } export function ImagePreviewer(props: { messages: ChatMessage[]; topic: string; }) { const chatStore = useChatStore(); const session = chatStore.currentSession(); const mask = session.mask; const config = useAppConfig(); const previewRef = useRef(null); const copy = () => { showToast(Locale.Export.Image.Toast); const dom = previewRef.current; if (!dom) return; toBlob(dom).then((blob) => { if (!blob) return; try { navigator.clipboard .write([ new ClipboardItem({ "image/png": blob, }), ]) .then(() => { showToast(Locale.Copy.Success); refreshPreview(); }); } catch (e) { console.error("[Copy Image] ", e); showToast(Locale.Copy.Failed); } }); }; const isMobile = useMobileScreen(); const download = async () => { showToast(Locale.Export.Image.Toast); const dom = previewRef.current; if (!dom) return; const isApp = getClientConfig()?.isApp; try { const blob = await toPng(dom); if (!blob) return; if (isMobile || (isApp && window.__TAURI__)) { if (isApp && window.__TAURI__) { const result = await window.__TAURI__.dialog.save({ defaultPath: `${props.topic}.png`, filters: [ { name: "PNG Files", extensions: ["png"], }, { name: "All Files", extensions: ["*"], }, ], }); if (result !== null) { const response = await fetch(blob); const buffer = await response.arrayBuffer(); const uint8Array = new Uint8Array(buffer); await window.__TAURI__.fs.writeBinaryFile(result, uint8Array); showToast(Locale.Download.Success); } else { showToast(Locale.Download.Failed); } } else { showImageModal(blob); } } else { const link = document.createElement("a"); link.download = `${props.topic}.png`; link.href = blob; link.click(); refreshPreview(); } } catch (error) { showToast(Locale.Download.Failed); } }; const refreshPreview = () => { const dom = previewRef.current; if (dom) { dom.innerHTML = dom.innerHTML; // Refresh the content of the preview by resetting its HTML for fix a bug glitching } }; return (
ChatGPT Next Web
github.com/Yidadaa/ChatGPT-Next-Web
&
{Locale.Exporter.Model}: {mask.modelConfig.model}
{Locale.Exporter.Messages}: {props.messages.length}
{Locale.Exporter.Topic}: {session.topic}
{Locale.Exporter.Time}:{" "} {new Date( props.messages.at(-1)?.date ?? Date.now(), ).toLocaleString()}
{props.messages.map((m, i) => { return (
); })}
); } export function MarkdownPreviewer(props: { messages: ChatMessage[]; topic: string; }) { const mdText = `# ${props.topic}\n\n` + props.messages .map((m) => { return m.role === "user" ? `## ${Locale.Export.MessageFromYou}:\n${m.content}` : `## ${Locale.Export.MessageFromChatGPT}:\n${m.content.trim()}`; }) .join("\n\n"); const copy = () => { copyToClipboard(mdText); }; const download = () => { downloadAs(mdText, `${props.topic}.md`); }; return ( <>
{mdText}
); } export function JsonPreviewer(props: { messages: ChatMessage[]; topic: string; }) { const msgs = { messages: [ { role: "system", content: `${Locale.FineTuned.Sysmessage} ${props.topic}`, }, ...props.messages.map((m) => ({ role: m.role, content: m.content, })), ], }; const mdText = "```json\n" + JSON.stringify(msgs, null, 2) + "\n```"; const minifiedJson = JSON.stringify(msgs); const copy = () => { copyToClipboard(minifiedJson); }; const download = () => { downloadAs(JSON.stringify(msgs), `${props.topic}.json`); }; return ( <>
); }