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:
+
{mdText}
+
, 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