feat: save to notion

This commit is contained in:
useMotionValue
2023-04-30 17:10:51 +08:00
parent b0aca0c43f
commit 59543179d7
16 changed files with 1120 additions and 451 deletions

43
app/api/notion/route.ts Normal file
View File

@@ -0,0 +1,43 @@
import { Client } from "@notionhq/client";
import { NextRequest, NextResponse } from "next/server";
import { markdownToBlocks } from "@tryfabric/martian";
export async function POST(req: NextRequest) {
const body = await req.text();
const { notionIntegrate, database_id, topic, mdText } = JSON.parse(body);
if (!(notionIntegrate && database_id && topic && mdText))
return NextResponse.json({
error:
"Request body must contains notionIntegrateToken and notionDatabaseID",
});
const notionService = new Client({
auth: notionIntegrate,
});
const blocks = markdownToBlocks(mdText);
console.log(blocks);
const res = await notionService.pages.create({
parent: {
database_id,
},
properties: {
Name: {
title: [
{
type: "text",
text: {
content: topic,
link: null,
},
plain_text: topic,
},
],
},
},
children: blocks,
});
return NextResponse.json(res);
}

View File

@@ -14,6 +14,7 @@ import MaskIcon from "../icons/mask.svg";
import MaxIcon from "../icons/max.svg";
import MinIcon from "../icons/min.svg";
import ResetIcon from "../icons/reload.svg";
import NotionIcon from "../icons/notion.svg";
import LightIcon from "../icons/light.svg";
import DarkIcon from "../icons/dark.svg";
@@ -109,6 +110,97 @@ function exportMessages(messages: Message[], topic: string) {
});
}
const SaveToNotionFC = ({
mdText,
topic,
}: Record<"mdText" | "topic", string>) => {
const [topicValue, setTopicValue] = useState(topic),
mdTextRef = useRef(mdText);
const handleTopicValueChange = (e: React.ChangeEvent<HTMLInputElement>) =>
setTopicValue(e.currentTarget.value),
handleMdTextChange = (e: React.FormEvent<HTMLPreElement>) => {
mdTextRef.current = e.currentTarget.innerText;
};
return (
<>
<ListItem title="Title:">
<input
className={styles["chat-input"]}
style={{
marginLeft: "1rem",
}}
value={topicValue}
onChange={handleTopicValueChange}
/>
</ListItem>
<div className="markdown-body">
<pre
contentEditable
className={styles["export-content"]}
onInput={handleMdTextChange}
>
{mdText}
</pre>
</div>
</>
);
};
function saveMessagesToNotion(
messages: Message[],
topic: string,
notionIntegrate: string,
database_id: string,
) {
const mdText =
`# ${topic}\n\n` +
messages
.map((m) => {
return m.role === "user"
? `## ${Locale.SaveToNotion.MessageFromYou}:\n${m.content}`
: `## ${
Locale.SaveToNotion.MessageFromChatGPT
}:\n${m.content.trim()}`;
})
.join("\n\n");
const handleSaveToNotion = async () => {
// TODO: editable topic, mdText and tags
fetch("/api/notion", {
method: "post",
body: JSON.stringify({
notionIntegrate,
database_id,
topic,
mdText,
}),
}).then((res) => res.json());
};
showModal({
title: Locale.SaveToNotion.Title,
children: <SaveToNotionFC mdText={mdText} topic={topic} />,
actions: [
<IconButton
key="copy"
icon={<CopyIcon />}
bordered
text={Locale.SaveToNotion.Copy}
onClick={() => copyToClipboard(mdText)}
/>,
<IconButton
key="download"
icon={<NotionIcon />}
bordered
text={Locale.SaveToNotion.Save}
onClick={handleSaveToNotion}
/>,
],
});
}
export function SessionConfigModel(props: { onClose: () => void }) {
const chatStore = useChatStore();
const session = chatStore.currentSession();
@@ -619,6 +711,21 @@ export function Chat() {
}}
/>
</div>
<div className="window-action-button">
<IconButton
icon={<NotionIcon />}
bordered
title={Locale.Chat.Actions.SaveToNotion}
onClick={() => {
saveMessagesToNotion(
session.messages.filter((msg) => !msg.isError),
session.topic,
config.notionInegration,
config.notionDatabaseID,
);
}}
/>
</div>
{!isMobileScreen && (
<div className="window-action-button">
<IconButton

View File

@@ -451,6 +451,35 @@ export function Settings() {
/>
)}
</ListItem>
<ListItem
title="Save To Notion"
subTitle="Your notion integration token"
>
<PasswordInput
value={config.notionInegration}
type="text"
placeholder="Integration Token"
onChange={(e) => {
updateConfig(
(config) => (config.notionInegration = e.currentTarget.value),
);
}}
/>
</ListItem>
<ListItem title="Save To Notion" subTitle="Your notion database id">
<PasswordInput
value={config.notionDatabaseID}
type="text"
placeholder="Notion Database ID"
onChange={(e) => {
updateConfig(
(config) => (config.notionDatabaseID = e.currentTarget.value),
);
}}
/>
</ListItem>
</List>
<List>

1
app/icons/notion.svg Normal file
View File

@@ -0,0 +1 @@
<svg width="800px" height="800px" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="#000000" fill-rule="evenodd" d="m138.462 21.522 27.784 19.588.044.033.275.201c1.713 1.256 3.349 2.455 4.452 3.83 1.411 1.76 1.884 3.644 1.884 5.877v104.706c0 3.587-.635 7.178-3.058 9.934-2.451 2.789-6.145 4.067-10.732 4.394l-.018.001-98.629 5.971-.021.001c-3.242.154-6.094.035-8.669-.907-2.688-.984-4.719-2.727-6.604-5.129l-.01-.012-19.979-25.979-.012-.017c-3.81-5.086-5.723-9.348-5.723-14.509V34.509c0-3.12.688-6.394 2.745-9.033 2.124-2.727 5.356-4.328 9.503-4.686l.058-.005 84.854-4.344c5.192-.445 8.938-.576 12.286.185 3.459.787 6.208 2.452 9.57 4.896ZM56.43 157.336h.002v3.3c0 1.904.47 2.337.613 2.452.296.235 1.203.652 3.642.518l97.449-5.371c1.928-.106 2.256-.649 2.348-.801l.005-.008c.29-.476.486-1.407.486-3.357V60.001c0-1.635-.334-2.218-.421-2.327l-.005-.007-.002-.003-.006-.002a.117.117 0 0 1-.012-.004c-.053-.019-.263-.078-.724-.037l-.057.005-101.622 5.668c-.624.056-.973.163-1.152.242-.142.062-.173.104-.181.116l-.002.002c-.066.085-.36.586-.36 2.321v91.361Zm9.085-106.705 87.074-4.506-21.028-15.375-.039-.031c-1.259-.98-2.507-1.854-4.12-2.46-1.588-.597-3.695-.993-6.669-.734l-.05.005-87.009 4.898h-.01a6.453 6.453 0 0 0-.893.116L49.934 48.56c2.037 1.646 3.109 2.146 4.337 2.367 1.538.277 3.52.167 7.722-.115l3.522-.237v.056Zm-34.231-3.586v83.893c0 .538.175 1.061.498 1.49l13.174 17.464V61.224a2.47 2.47 0 0 0-.877-1.889l-.08-.068-12.715-12.222Zm109.871 35.062c.451 2.04 0 4.082-2.041 4.315l-3.393.673v49.881c-2.947 1.586-5.66 2.492-7.927 2.492-3.622 0-4.528-1.134-7.239-4.53l-.003-.003L98.36 100.02v33.78l7.02 1.59s0 4.082-5.664 4.082l-15.615.906c-.455-.91 0-3.176 1.582-3.627l4.078-1.131V90.955l-5.66-.459c-.454-2.04.677-4.987 3.85-5.216l16.754-1.128 23.09 35.367V88.231l-5.885-.677c-.455-2.499 1.356-4.315 3.618-4.536l15.627-.91v-.001Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -15,6 +15,7 @@ const cn = {
ChatList: "查看消息列表",
CompressedHistory: "查看压缩后的历史 Prompt",
Export: "导出聊天记录",
SaveToNotion: "保存聊天记录到Notion",
Copy: "复制",
Stop: "停止",
Retry: "重试",
@@ -42,6 +43,13 @@ const cn = {
MessageFromYou: "来自你的消息",
MessageFromChatGPT: "来自 ChatGPT 的消息",
},
SaveToNotion: {
Title: "保存聊天记录到Notion",
Copy: "全部复制",
Save: "保存",
MessageFromYou: "来自你的消息",
MessageFromChatGPT: "来自 ChatGPT 的消息",
},
Memory: {
Title: "历史摘要",
EmptyContent: "对话内容过短,无需总结",

View File

@@ -16,6 +16,7 @@ const de: LocaleType = {
ChatList: "Zur Chat-Liste gehen",
CompressedHistory: "Komprimierter Gedächtnis-Prompt",
Export: "Alle Nachrichten als Markdown exportieren",
SaveToNotion: "Alle Nachrichten in Notion speichern",
Copy: "Kopieren",
Stop: "Stop",
Retry: "Wiederholen",
@@ -43,6 +44,13 @@ const de: LocaleType = {
MessageFromYou: "Deine Nachricht",
MessageFromChatGPT: "Nachricht von ChatGPT",
},
SaveToNotion: {
Title: "Alle Nachrichten in Notion speichern",
Copy: "Alles kopieren",
Save: "Speichern",
MessageFromYou: "Deine Nachricht",
MessageFromChatGPT: "Nachricht von ChatGPT",
},
Memory: {
Title: "Gedächtnis-Prompt",
EmptyContent: "Noch nichts.",

View File

@@ -16,6 +16,7 @@ const en: LocaleType = {
ChatList: "Go To Chat List",
CompressedHistory: "Compressed History Memory Prompt",
Export: "Export All Messages as Markdown",
SaveToNotion: "Save All Messages to notion",
Copy: "Copy",
Stop: "Stop",
Retry: "Retry",
@@ -43,6 +44,13 @@ const en: LocaleType = {
MessageFromYou: "Message From You",
MessageFromChatGPT: "Message From ChatGPT",
},
SaveToNotion: {
Title: "Save All Messages To Notion",
Copy: "Copy All",
Save: "Save",
MessageFromYou: "Message From You",
MessageFromChatGPT: "Message From ChatGPT",
},
Memory: {
Title: "Memory Prompt",
EmptyContent: "Nothing yet.",

View File

@@ -16,6 +16,7 @@ const es: LocaleType = {
ChatList: "Ir a la lista de chats",
CompressedHistory: "Historial de memoria comprimido",
Export: "Exportar todos los mensajes como Markdown",
SaveToNotion: "Guardar todos los mensajes en Notion",
Copy: "Copiar",
Stop: "Detener",
Retry: "Reintentar",
@@ -43,6 +44,13 @@ const es: LocaleType = {
MessageFromYou: "Mensaje de ti",
MessageFromChatGPT: "Mensaje de ChatGPT",
},
SaveToNotion: {
Title: "Guardar todos los mensajes en Notion",
Copy: "Copiar todo",
Save: "Guardar",
MessageFromYou: "Mensaje de ti",
MessageFromChatGPT: "Mensaje de ChatGPT",
},
Memory: {
Title: "Historial de memoria",
EmptyContent: "Aún no hay nada.",

View File

@@ -16,6 +16,7 @@ const it: LocaleType = {
ChatList: "Vai alla Chat List",
CompressedHistory: "Prompt di memoria della cronologia compressa",
Export: "Esportazione di tutti i messaggi come Markdown",
SaveToNotion: "Salva tutti i messaggi su Notion",
Copy: "Copia",
Stop: "Stop",
Retry: "Riprova",
@@ -43,6 +44,13 @@ const it: LocaleType = {
MessageFromYou: "Messaggio da te",
MessageFromChatGPT: "Messaggio da ChatGPT",
},
SaveToNotion: {
Title: "Salva tutti i messaggi su Notion",
Copy: "Copia tutto",
Save: "Salva",
MessageFromYou: "Messaggio da te",
MessageFromChatGPT: "Messaggio da ChatGPT",
},
Memory: {
Title: "Prompt di memoria",
EmptyContent: "Vuoto.",

View File

@@ -16,6 +16,7 @@ const jp: LocaleType = {
ChatList: "メッセージリストを表示",
CompressedHistory: "圧縮された履歴プロンプトを表示",
Export: "チャット履歴をエクスポート",
SaveToNotion: "すべてのメッセージをNotionに保存する",
Copy: "コピー",
Stop: "停止",
Retry: "リトライ",
@@ -43,6 +44,13 @@ const jp: LocaleType = {
MessageFromYou: "あなたからのメッセージ",
MessageFromChatGPT: "ChatGPTからのメッセージ",
},
SaveToNotion: {
Title: "すべてのメッセージをNotionに保存する",
Copy: "すべてコピー",
Save: "保存する",
MessageFromYou: "あなたからのメッセージ",
MessageFromChatGPT: "ChatGPTからのメッセージ",
},
Memory: {
Title: "履歴メモリ",
EmptyContent: "まだ記憶されていません",

View File

@@ -16,6 +16,7 @@ const tr: LocaleType = {
ChatList: "Sohbet Listesine Git",
CompressedHistory: "Sıkıştırılmış Geçmiş Bellek Komutu",
Export: "Tüm Mesajları Markdown Olarak Dışa Aktar",
SaveToNotion: "Tüm Mesajları Notion'a Kaydet",
Copy: "Kopyala",
Stop: "Durdur",
Retry: "Tekrar Dene",
@@ -43,6 +44,13 @@ const tr: LocaleType = {
MessageFromYou: "Sizin Mesajınız",
MessageFromChatGPT: "ChatGPT'nin Mesajı",
},
SaveToNotion: {
Title: "Tüm Mesajları Notion'a Kaydet",
Copy: "Tümünü Kopyala",
Save: "Kaydet",
MessageFromYou: "Sizin Mesajınız",
MessageFromChatGPT: "ChatGPT'nin Mesajı",
},
Memory: {
Title: "Bellek Komutları",
EmptyContent: "Henüz değil.",

View File

@@ -15,6 +15,7 @@ const tw: LocaleType = {
ChatList: "查看訊息列表",
CompressedHistory: "查看壓縮後的歷史 Prompt",
Export: "匯出聊天紀錄",
SaveToNotion: "將聊天記錄保存到notion",
Copy: "複製",
Stop: "停止",
Retry: "重試",
@@ -42,6 +43,13 @@ const tw: LocaleType = {
MessageFromYou: "來自您的訊息",
MessageFromChatGPT: "來自 ChatGPT 的訊息",
},
SaveToNotion: {
Title: "將聊天記錄保存到Notion",
Copy: "複製全部",
Save: "保存",
MessageFromYou: "來自您的訊息",
MessageFromChatGPT: "來自 ChatGPT 的訊息",
},
Memory: {
Title: "上下文記憶 Prompt",
EmptyContent: "尚未記憶",

View File

@@ -26,6 +26,9 @@ export const DEFAULT_CONFIG = {
sendPreviewBubble: true,
sidebarWidth: 300,
notionInegration: "",
notionDatabaseID: "",
disablePromptHint: false,
dontShowMaskSplashScreen: false, // dont show splash screen when create chat

View File

@@ -29,7 +29,6 @@ export function middleware(req: NextRequest) {
console.log("[Auth] hashed access code:", hashedCode);
console.log("[User IP] ", getIP(req));
console.log("[Time] ", new Date().toLocaleString());
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
return NextResponse.json(
{

View File

@@ -14,7 +14,9 @@
},
"dependencies": {
"@hello-pangea/dnd": "^16.2.0",
"@notionhq/client": "^2.2.4",
"@svgr/webpack": "^6.5.1",
"@tryfabric/martian": "^1.2.4",
"@vercel/analytics": "^0.1.11",
"emoji-picker-react": "^4.4.7",
"eventsource-parser": "^0.1.0",

1321
yarn.lock

File diff suppressed because it is too large Load Diff