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 (
);
}
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 && (
[0]})
)}
{getMessageImages(m).length > 1 && (
{getMessageImages(m).map((src, i) => (

))}
)}
);
})}
);
}
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 (
<>
>
);
}
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 (
<>
>
);
}