Skip to content

Commit

Permalink
feat: share to ShareGPT
Browse files Browse the repository at this point in the history
  • Loading branch information
Yidadaa committed May 24, 2023
1 parent 4ca34e0 commit 3e65ef3
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 28 deletions.
37 changes: 36 additions & 1 deletion app/client/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ACCESS_CODE_PREFIX } from "../constant";
import { ModelConfig, ModelType, useAccessStore } from "../store";
import { ChatMessage, ModelConfig, ModelType, useAccessStore } from "../store";
import { ChatGPTApi } from "./platforms/openai";

export const ROLES = ["system", "user", "assistant"] as const;
Expand Down Expand Up @@ -54,6 +54,41 @@ export class ClientApi {
prompts() {}

masks() {}

async share(messages: ChatMessage[], avatarUrl: string | null = null) {
const msgs = messages
.map((m) => ({
from: m.role === "user" ? "human" : "gpt",
value: m.content,
}))
.concat([
{
from: "human",
value:
"Share from [ChatGPT Next Web]: https://github.com/Yidadaa/ChatGPT-Next-Web",
},
]);
// 敬告二开开发者们,为了开源大模型的发展,请不要修改上述消息,此消息用于后续数据清洗使用
// Please do not modify this message

console.log("[Share]", msgs);
const res = await fetch("/sharegpt", {
body: JSON.stringify({
avatarUrl,
items: msgs,
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});

const resJson = await res.json();
console.log("[Share]", resJson);
if (resJson.id) {
return `https://shareg.pt/${resJson.id}`;
}
}
}

export const api = new ClientApi();
Expand Down
152 changes: 127 additions & 25 deletions app/components/exporter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ 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 { 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 { toBlob, toJpeg, 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";

const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />,
Expand Down Expand Up @@ -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: {
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;
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 (
<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
text={Locale.Export.Copy}
text={Locale.Export.Download}
bordered
shadow
icon={<CopyIcon />}
onClick={props.copy}
icon={<DownloadIcon />}
onClick={props.download}
></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>
</>
);
}

Expand Down Expand Up @@ -323,7 +416,12 @@ export function ImagePreviewer(props: {

return (
<div className={styles["image-previewer"]}>
<PreviewActions copy={copy} download={download} showCopy={!isMobile} />
<PreviewActions
copy={copy}
download={download}
showCopy={!isMobile}
messages={props.messages}
/>
<div
className={`${styles["preview-body"]} ${styles["default-theme"]}`}
ref={previewRef}
Expand Down Expand Up @@ -417,7 +515,11 @@ export function MarkdownPreviewer(props: {

return (
<>
<PreviewActions copy={copy} download={download} />
<PreviewActions
copy={copy}
download={download}
messages={props.messages}
/>
<div className="markdown-body">
<pre className={styles["export-content"]}>{mdText}</pre>
</div>
Expand Down
4 changes: 3 additions & 1 deletion app/components/message-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ export function MessageSelector(props: {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [startIndex, endIndex]);

const LATEST_COUNT = 4;

return (
<div className={styles["message-selector"]}>
<div className={styles["message-filter"]}>
Expand Down Expand Up @@ -155,7 +157,7 @@ export function MessageSelector(props: {
props.updateSelection((selection) => {
selection.clear();
messages
.slice(messageCount - 10)
.slice(messageCount - LATEST_COUNT)
.forEach((m) => selection.add(m.id!));
})
}
Expand Down
2 changes: 2 additions & 0 deletions app/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,5 @@ export const ACCESS_CODE_PREFIX = "ak-";
export const LAST_INPUT_KEY = "last-input";

export const REQUEST_TIMEOUT_MS = 60000;

export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
2 changes: 1 addition & 1 deletion app/locales/cn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const cn = {
Select: {
Search: "搜索消息",
All: "选取全部",
Latest: "最近十条",
Latest: "最近几条",
Clear: "清除选中",
},
Memory: {
Expand Down
4 changes: 4 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const nextConfig = {
source: "/google-fonts/:path*",
destination: "https://fonts.googleapis.com/:path*",
},
{
source: "/sharegpt",
destination: "https://sharegpt.com/api/conversations",
},
];

const apiUrl = process.env.API_URL;
Expand Down

0 comments on commit 3e65ef3

Please sign in to comment.