feat: add export to .md button

This commit is contained in:
Yifei Zhang 2023-03-15 17:24:03 +00:00
parent 64e331a3e3
commit bab470d000
11 changed files with 181 additions and 18 deletions

View File

@ -1,7 +1,6 @@
import { OpenAIApi, Configuration } from "openai";
import { ChatRequest } from "./typing";
const isProd = process.env.NODE_ENV === "production";
const apiKey = process.env.OPENAI_API_KEY;
const openai = new OpenAIApi(
@ -16,16 +15,7 @@ export async function POST(req: Request) {
const completion = await openai!.createChatCompletion(
{
...requestBody,
},
isProd
? {}
: {
proxy: {
protocol: "socks",
host: "127.0.0.1",
port: 7890,
},
}
}
);
return new Response(JSON.stringify(completion.data));

View File

@ -329,3 +329,7 @@
right: 30px;
bottom: 10px;
}
.export-content {
white-space: break-spaces;
}

View File

@ -23,9 +23,13 @@ import DeleteIcon from "../icons/delete.svg";
import LoadingIcon from "../icons/three-dots.svg";
import MenuIcon from "../icons/menu.svg";
import CloseIcon from "../icons/close.svg";
import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg";
import { Message, SubmitKey, useChatStore, Theme } from "../store";
import { Settings } from "./settings";
import { showModal } from "./ui-lib";
import { copyToClipboard, downloadAs } from "../utils";
export function Markdown(props: { content: string }) {
return (
@ -208,7 +212,10 @@ export function Chat(props: { showSideBar?: () => void }) {
<IconButton
icon={<ExportIcon />}
bordered
title="导出聊天记录为 Markdown开发中"
title="导出聊天记录"
onClick={() => {
exportMessages(session.messages, session.topic)
}}
/>
</div>
</div>
@ -294,6 +301,22 @@ function useSwitchTheme() {
}, [config.theme]);
}
function exportMessages(messages: Message[], topic: string) {
const mdText = `# ${topic}\n\n` + messages.map(m => {
return m.role === 'user' ? `## ${m.content}` : m.content.trim()
}).join('\n\n')
const filename = `${topic}.md`
showModal({
title: "导出聊天记录为 Markdown", children: <div className="markdown-body">
<pre className={styles['export-content']}>{mdText}</pre>
</div>, actions: [
<IconButton icon={<CopyIcon />} bordered text="全部复制" onClick={() => copyToClipboard(mdText)} />,
<IconButton icon={<DownloadIcon />} bordered text="下载文件" onClick={() => downloadAs(mdText, filename)} />
]
})
}
export function Home() {
const [createNewSession] = useChatStore((state) => [state.newSession]);
const loading = !useChatStore?.persist?.hasHydrated();

View File

@ -29,6 +29,7 @@
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
@ -56,3 +57,68 @@
.list .list-item:last-child {
border: 0;
}
.modal-container {
box-shadow: var(--card-shadow);
background-color: var(--white);
border-radius: 12px;
width: 50vw;
--modal-padding: 20px;
.modal-header {
padding: var(--modal-padding);
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: var(--border-in-light);
.modal-title {
font-weight: bolder;
font-size: 16px;
}
.modal-close-btn {
cursor: pointer;
&:hover {
filter: brightness(1.2);
}
}
}
.modal-content {
height: 40vh;
padding: var(--modal-padding);
overflow: auto;
}
.modal-footer {
padding: var(--modal-padding);
display: flex;
justify-content: flex-end;
.modal-actions {
display: flex;
align-items: center;
.modal-action {
&:not(:last-child) {
margin-right: 20px;
}
}
}
}
}
@media only screen and (max-width: 600px) {
.modal-container {
width: 90vw;
.modal-content {
height: 50vh;
}
}
}

View File

@ -1,5 +1,8 @@
import styles from "./ui-lib.module.scss";
import LoadingIcon from "../icons/three-dots.svg";
import CloseIcon from "../icons/close.svg";
import { createRoot } from 'react-dom/client'
import { IconButton } from "./button";
export function Popover(props: {
children: JSX.Element;
@ -47,3 +50,42 @@ export function Loading() {
justifyContent: "center"
}}><LoadingIcon /></div>
}
interface ModalProps {
title: string,
children?: JSX.Element,
actions?: JSX.Element[],
onClose?: () => void,
}
export function Modal(props: ModalProps) {
return <div className={styles['modal-container']}>
<div className={styles['modal-header']}>
<div className={styles['modal-title']}>{props.title}</div>
<div className={styles['modal-close-btn']} onClick={props.onClose}>
<CloseIcon />
</div>
</div>
<div className={styles['modal-content']}>{props.children}</div>
<div className={styles['modal-footer']}>
<div className={styles['modal-actions']}>
{props.actions?.map(action => <div className={styles['modal-action']}>{action}</div>)}
</div>
</div>
</div>
}
export function showModal(props: ModalProps) {
const div = document.createElement('div')
div.className = "modal-mask";
document.body.appendChild(div)
const root = createRoot(div)
root.render(<Modal {...props} onClose={() => {
props.onClose?.();
root.unmount();
div.remove();
}}></Modal>)
}

View File

@ -6,7 +6,7 @@
--primary: rgb(29, 147, 171);
--second: rgb(231, 248, 255);
--hover-color: #f3f3f3;
--bar-color: var(--primary);
--bar-color: rgba(0, 0, 0, 0.1);
/* shadow */
--shadow: 50px 50px 100px 10px rgb(0, 0, 0, 0.1);
@ -25,7 +25,7 @@
--second: rgb(27 38 42);
--hover-color: #323232;
--bar-color: var(--primary);
--bar-color: rgba(255, 255, 255, 0.1);
--border-in-light: 1px solid rgba(255, 255, 255, 0.192);
}
@ -82,7 +82,7 @@ body {
}
::-webkit-scrollbar {
--bar-width: 1px;
--bar-width: 5px;
width: var(--bar-width);
height: var(--bar-width);
}
@ -163,3 +163,16 @@ input[type="range"]::-webkit-slider-thumb:hover {
div.math {
overflow-x: auto;
}
.modal-mask {
z-index: 9999;
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
background-color: rgba($color: #000000, $alpha: 0.5);
display: flex;
align-items: center;
justify-content: center;
}

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

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4.333333333333333 1.6666666666666665) rotate(0 5 5)" d="M0,2.48L0,0.94C0,0.42 0.42,0 0.94,0L9.06,0C9.58,0 10,0.42 10,0.94L10,9.06C10,9.58 9.58,10 9.06,10L7.51,10 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.6666666666666665 4.333333333333333) rotate(0 5 5)" d="M0.94,0C0.42,0 0,0.42 0,0.94L0,9.06C0,9.58 0.42,10 0.94,10L9.06,10C9.58,10 10,9.58 10,9.06L10,0.94C10,0.42 9.58,0 9.06,0L0.94,0Z " /></g></g></svg>

After

Width:  |  Height:  |  Size: 1010 B

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

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(2 2) rotate(0 6 6)" d="M1,12L11,12C11.55,12 12,11.55 12,11L12,1C12,0.45 11.55,0 11,0L1,0C0.45,0 0,0.45 0,1L0,11C0,11.55 0.45,12 1,12Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 10.333333333333332) rotate(0 6.666666666666666 0.6666666666666666)" d="M0,0L3.67,0L4.33,1.33L9,1.33L9.67,0L13.33,0 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(14 8.666666666666666) rotate(0 0 1.6666666666666665)" d="M0,3.33L0,0 " /><path id="路径 4" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(6 7.333333333333333) rotate(0 2 1)" d="M0,0L2,2L4,0 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8 4) rotate(0 0 2.6666666666666665)" d="M0,5.33L0,0 " /><path id="路径 6" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(2 8.666666666666666) rotate(0 0 1.6666666666666665)" d="M0,3.33L0,0 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -12,7 +12,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="zh-Hans-CN">
<html lang="en">
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"

View File

@ -1,6 +1,8 @@
import type { ChatRequest, ChatReponse } from "./api/chat/typing";
import { Message } from "./store";
const TIME_OUT_MS = 30000
const makeRequestParam = (
messages: Message[],
options?: {
@ -52,7 +54,7 @@ export async function requestChatStream(
});
const controller = new AbortController();
const reqTimeoutId = setTimeout(() => controller.abort(), 10000);
const reqTimeoutId = setTimeout(() => controller.abort(), TIME_OUT_MS);
try {
const res = await fetch("/api/chat-stream", {
@ -78,7 +80,7 @@ export async function requestChatStream(
while (true) {
// handle time out, will stop if no response in 10 secs
const resTimeoutId = setTimeout(() => finish(), 10000);
const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
const content = await reader?.read();
clearTimeout(resTimeoutId);
const text = decoder.decode(content?.value);

View File

@ -9,3 +9,24 @@ export function trimTopic(topic: string) {
return s.join("");
}
export function copyToClipboard(text: string) {
navigator.clipboard.writeText(text).then(res => {
alert('复制成功')
}).catch(err => {
alert('复制失败,请赋予剪切板权限')
})
}
export function downloadAs(text: string, filename: string) {
const element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}