import { ChatMessage, useAppConfig, useChatStore } from "../store"; import Locale from "../locales"; import styles from "./exporter.module.scss"; import { List, ListItem, Modal, Select, 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 { 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"; const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { loading: () => , }); export function ExportMessageModal(props: { onClose: () => void }) { return (
); } 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"] 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, i) => selection.has(m.id ?? i))); return ret; }, [ exportConfig.includeContext, session.messages, session.mask.context, selection, ]); return ( <>
{ updateExportConfig( (config) => (config.includeContext = e.currentTarget.checked), ); }} >
{currentStep.value === "preview" && (
{exportConfig.format === "text" ? ( ) : ( )}
)} ); } export function PreviewActions(props: { download: () => void; copy: () => void; showCopy?: boolean; }) { return (
{props.showCopy && ( } onClick={props.copy} > )} } onClick={props.download} > } onClick={() => showToast(Locale.WIP)} >
); } function ExportAvatar(props: { avatar: string }) { if (props.avatar === DEFAULT_MASK_AVATAR) { return ( ); } 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 = () => { 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); }); } catch (e) { console.error("[Copy Image] ", e); showToast(Locale.Copy.Failed); } }); }; const isMobile = useMobileScreen(); const download = () => { const dom = previewRef.current; if (!dom) return; toPng(dom) .then((blob) => { if (!blob) return; if (isMobile) { const image = new Image(); image.src = blob; const win = window.open(""); win?.document.write(image.outerHTML); } else { const link = document.createElement("a"); link.download = `${props.topic}.png`; link.href = blob; link.click(); } }) .catch((e) => console.log("[Export Image] ", e)); }; return (
ChatGPT Next Web
github.com/Yidadaa/ChatGPT-Next-Web
&
Model: {mask.modelConfig.model}
Messages: {props.messages.length}
Topic: {session.topic}
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}
); }