diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts
index d277da51..27ce118b 100644
--- a/app/api/chat/route.ts
+++ b/app/api/chat/route.ts
@@ -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));
diff --git a/app/components/home.module.scss b/app/components/home.module.scss
index 0014e402..6b3ee91c 100644
--- a/app/components/home.module.scss
+++ b/app/components/home.module.scss
@@ -328,4 +328,8 @@
position: absolute;
right: 30px;
bottom: 10px;
+}
+
+.export-content {
+ white-space: break-spaces;
}
\ No newline at end of file
diff --git a/app/components/home.tsx b/app/components/home.tsx
index db8edb16..27f9ab9f 100644
--- a/app/components/home.tsx
+++ b/app/components/home.tsx
@@ -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 }) {
}
bordered
- title="导出聊天记录为 Markdown(开发中)"
+ title="导出聊天记录"
+ onClick={() => {
+ exportMessages(session.messages, session.topic)
+ }}
/>
@@ -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:
, actions: [
+ } bordered text="全部复制" onClick={() => copyToClipboard(mdText)} />,
+ } bordered text="下载文件" onClick={() => downloadAs(mdText, filename)} />
+ ]
+ })
+}
+
export function Home() {
const [createNewSession] = useChatStore((state) => [state.newSession]);
const loading = !useChatStore?.persist?.hasHydrated();
diff --git a/app/components/ui-lib.module.scss b/app/components/ui-lib.module.scss
index 1cfce093..a86b4b9e 100644
--- a/app/components/ui-lib.module.scss
+++ b/app/components/ui-lib.module.scss
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx
index be9974af..8418b908 100644
--- a/app/components/ui-lib.tsx
+++ b/app/components/ui-lib.tsx
@@ -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;
@@ -46,4 +49,43 @@ export function Loading() {
alignItems: "center",
justifyContent: "center"
}}>
+}
+
+interface ModalProps {
+ title: string,
+ children?: JSX.Element,
+ actions?: JSX.Element[],
+ onClose?: () => void,
+}
+export function Modal(props: ModalProps) {
+ return
+
+
{props.title}
+
+
+
+
+
+
+
{props.children}
+
+
+
+ {props.actions?.map(action =>
{action}
)}
+
+
+
+}
+
+export function showModal(props: ModalProps) {
+ const div = document.createElement('div')
+ div.className = "modal-mask";
+ document.body.appendChild(div)
+
+ const root = createRoot(div)
+ root.render( {
+ props.onClose?.();
+ root.unmount();
+ div.remove();
+ }}>)
}
\ No newline at end of file
diff --git a/app/globals.scss b/app/globals.scss
index 4d823df6..20622582 100644
--- a/app/globals.scss
+++ b/app/globals.scss
@@ -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);
}
@@ -162,4 +162,17 @@ 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;
}
\ No newline at end of file
diff --git a/app/icons/copy.svg b/app/icons/copy.svg
new file mode 100644
index 00000000..356b33f9
--- /dev/null
+++ b/app/icons/copy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/icons/download.svg b/app/icons/download.svg
new file mode 100644
index 00000000..2a8f387a
--- /dev/null
+++ b/app/icons/download.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index f5f00e21..6699ad27 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -12,7 +12,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
-
+
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);
diff --git a/app/utils.ts b/app/utils.ts
index 0b3f8f01..d1e8d3ee 100644
--- a/app/utils.ts
+++ b/app/utils.ts
@@ -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);
+}
\ No newline at end of file