import type { ChatMessage } from '../store'; import clsx from 'clsx'; import { toBlob, toPng } from 'html-to-image'; import dynamic from 'next/dynamic'; import NextImage from 'next/image'; import { useEffect, useMemo, useRef, useState } from 'react'; import { type ClientApi, getClientApi } from '../client/api'; import { getClientConfig } from '../config/client'; import { EXPORT_MESSAGE_CLASS_NAME } from '../constant'; import BotIcon from '../icons/bot.png'; import ChatGptIcon from '../icons/chatgpt.png'; import CopyIcon from '../icons/copy.svg'; import DownloadIcon from '../icons/download.svg'; import ShareIcon from '../icons/share.svg'; import LoadingIcon from '../icons/three-dots.svg'; import Locale from '../locales'; import { useAppConfig, useChatStore } from '../store'; import { DEFAULT_MASK_AVATAR } from '../store/mask'; import { copyToClipboard, downloadAs, getMessageImages, getMessageTextContent, useMobileScreen, } from '../utils'; import { prettyObject } from '../utils/format'; import { IconButton } from './button'; import { Avatar } from './emoji'; import styles from './exporter.module.scss'; import { MessageSelector, useMessageSelector } from './message-selector'; import { List, ListItem, Modal, Select, showImageModal, showModal, showToast, } from './ui-lib'; 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 config = useAppConfig(); const onRenderMsgs = (msgs: ChatMessage[]) => { setShouldExport(false); const api: ClientApi = getClientApi(config.modelConfig.providerName); 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 (
NextChat
github.com/ChatGPTNextWeb/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 (
{getMessageImages(m).length == 1 && ( message )} {getMessageImages(m).length > 1 && (
{getMessageImages(m).map((src, i) => ( message ))}
)}
); })}
); } 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${getMessageTextContent(m)}` : `## ${Locale.Export.MessageFromChatGPT}:\n${getMessageTextContent( m, ).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 ( <>
); }