diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 73464a5a..fb6bfd7d 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -1,3 +1,5 @@ +@import "./window.scss"; + @mixin container { background-color: var(--white); border: var(--border-in-light); @@ -11,6 +13,9 @@ display: flex; overflow: hidden; box-sizing: border-box; + + width: var(--window-width); + height: var(--window-height); } .container { @@ -18,20 +23,20 @@ max-width: 1080px; max-height: 864px; - width: 90vw; - height: 90vh; } .tight-container { + --window-width: 100vw; + --window-height: 100vw; + @include container(); - width: 100vw; - height: 100vh; border-radius: 0; } .sidebar { - width: 300px; + width: var(--sidebar-width); + box-sizing: border-box; padding: 20px; background-color: var(--second); display: flex; @@ -159,7 +164,7 @@ } .window-content { - width: 100%; + width: var(--window-content-width); height: 100%; } @@ -170,38 +175,6 @@ height: 100%; } -.window-header { - padding: 14px 20px; - border-bottom: rgba(0, 0, 0, 0.1) 1px solid; - - display: flex; - justify-content: space-between; - align-items: center; -} - -.window-header-title { - font-size: 20px; - font-weight: bolder; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} - -.window-header-sub-title { - font-size: 14px; - margin-top: 5px; -} - -.window-actions { - display: inline-flex; -} - -.window-action-button { - margin-left: 10px; -} - .chat-body { flex: 1; overflow: auto; @@ -220,7 +193,7 @@ } .chat-message-container { - max-width: 60%; + max-width: 80%; display: flex; flex-direction: column; align-items: flex-start; @@ -254,13 +227,14 @@ } .chat-message-item { + max-width: 100%; margin-top: 10px; border-radius: 10px; background-color: rgba(0, 0, 0, 0.05); padding: 10px; font-size: 14px; user-select: text; - word-break: break-all; + word-break: break-word; border: var(--border-in-light); } @@ -327,16 +301,3 @@ right: 30px; bottom: 10px; } - -.settings { - padding: 20px; -} - -.settings-title { - font-size: 14px; - font-weight: bolder; -} - -.avatar { - cursor: pointer; -} diff --git a/app/components/home.tsx b/app/components/home.tsx index b92aeb8a..8339f890 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -6,7 +6,7 @@ import "katex/dist/katex.min.css"; import RemarkMath from "remark-math"; import RehypeKatex from "rehype-katex"; -import EmojiPicker, { Emoji, Theme as EmojiTheme } from "emoji-picker-react"; +import { Emoji } from "emoji-picker-react"; import { IconButton } from "./button"; import styles from "./home.module.scss"; @@ -21,10 +21,21 @@ import BotIcon from "../icons/bot.svg"; import AddIcon from "../icons/add.svg"; import DeleteIcon from "../icons/delete.svg"; import LoadingIcon from "../icons/three-dots.svg"; -import ResetIcon from "../icons/reload.svg"; import { Message, SubmitKey, useChatStore, Theme } from "../store"; -import { Card, List, ListItem, Popover } from "./ui-lib"; +import { Settings } from "./settings"; +import dynamic from "next/dynamic"; + +export const LazySettings = dynamic( + async () => await (await import("./settings")).Settings, + { + loading: () => ( +
+ +
+ ), + } +); export function Markdown(props: { content: string }) { return ( @@ -288,6 +299,7 @@ function useSwitchTheme() { export function Home() { const [createNewSession] = useChatStore((state) => [state.newSession]); + const loading = !useChatStore.persist.hasHydrated(); // settings const [openSettings, setOpenSettings] = useState(false); @@ -295,6 +307,15 @@ export function Home() { useSwitchTheme(); + if (loading) { + return ( +
+ + +
+ ); + } + return (
- {openSettings ? : } + {openSettings ? : }
); } - -export function Settings() { - const [showEmojiPicker, setShowEmojiPicker] = useState(false); - const [config, updateConfig, resetConfig] = useChatStore((state) => [ - state.config, - state.updateConfig, - state.resetConfig, - ]); - - return ( - <> -
-
-
设置
-
设置选项
-
-
-
- } - onClick={resetConfig} - bordered - title="重置所有选项" - /> -
-
-
-
- - -
头像
- setShowEmojiPicker(false)} - content={ - { - updateConfig((config) => (config.avatar = e.unified)); - setShowEmojiPicker(false); - }} - /> - } - open={showEmojiPicker} - > -
setShowEmojiPicker(true)} - > - -
-
-
- - -
发送键
-
- -
-
- - -
主题
-
- -
-
- - -
紧凑边框
- - updateConfig( - (config) => (config.tightBorder = e.currentTarget.checked) - ) - } - > -
-
- - -
最大上下文消息数
- - updateConfig( - (config) => - (config.historyMessageCount = e.target.valueAsNumber) - ) - } - > -
- - -
- 上下文中包含机器人消息 -
- - updateConfig( - (config) => (config.sendBotMessages = e.currentTarget.checked) - ) - } - > -
-
-
- - ); -} diff --git a/app/components/settings.module.scss b/app/components/settings.module.scss new file mode 100644 index 00000000..e393076f --- /dev/null +++ b/app/components/settings.module.scss @@ -0,0 +1,14 @@ +@import "./window.scss"; + +.settings { + padding: 20px; +} + +.settings-title { + font-size: 14px; + font-weight: bolder; +} + +.avatar { + cursor: pointer; +} diff --git a/app/components/settings.tsx b/app/components/settings.tsx new file mode 100644 index 00000000..84974f3d --- /dev/null +++ b/app/components/settings.tsx @@ -0,0 +1,160 @@ +import { useState, useRef, useEffect } from "react"; + +import EmojiPicker, { Emoji, Theme as EmojiTheme } from "emoji-picker-react"; + +import styles from "./settings.module.scss"; + +import ResetIcon from "../icons/reload.svg"; + +import { List, ListItem, Popover } from "./ui-lib"; + +import { IconButton } from "./button"; +import { SubmitKey, useChatStore, Theme } from "../store"; +import { Avatar } from "./home"; +import dynamic from "next/dynamic"; + +export function Settings() { + const [showEmojiPicker, setShowEmojiPicker] = useState(false); + const [config, updateConfig, resetConfig] = useChatStore((state) => [ + state.config, + state.updateConfig, + state.resetConfig, + ]); + + return ( + <> +
+
+
设置
+
设置选项
+
+
+
+ } + onClick={resetConfig} + bordered + title="重置所有选项" + /> +
+
+
+
+ + +
头像
+ setShowEmojiPicker(false)} + content={ + { + updateConfig((config) => (config.avatar = e.unified)); + setShowEmojiPicker(false); + }} + /> + } + open={showEmojiPicker} + > +
setShowEmojiPicker(true)} + > + +
+
+
+ + +
发送键
+
+ +
+
+ + +
主题
+
+ +
+
+ + +
紧凑边框
+ + updateConfig( + (config) => (config.tightBorder = e.currentTarget.checked) + ) + } + > +
+
+ + +
最大上下文消息数
+ + updateConfig( + (config) => + (config.historyMessageCount = e.target.valueAsNumber) + ) + } + > +
+ + +
+ 上下文中包含机器人消息 +
+ + updateConfig( + (config) => (config.sendBotMessages = e.currentTarget.checked) + ) + } + > +
+
+
+ + ); +} diff --git a/app/components/window.scss b/app/components/window.scss new file mode 100644 index 00000000..8b8906b3 --- /dev/null +++ b/app/components/window.scss @@ -0,0 +1,31 @@ +.window-header { + padding: 14px 20px; + border-bottom: rgba(0, 0, 0, 0.1) 1px solid; + + display: flex; + justify-content: space-between; + align-items: center; +} + +.window-header-title { + font-size: 20px; + font-weight: bolder; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.window-header-sub-title { + font-size: 14px; + margin-top: 5px; +} + +.window-actions { + display: inline-flex; +} + +.window-action-button { + margin-left: 10px; +} diff --git a/app/globals.scss b/app/globals.scss index 03af9b85..b17048aa 100644 --- a/app/globals.scss +++ b/app/globals.scss @@ -41,6 +41,11 @@ :root { @include light; + + --window-width: 90vw; + --window-height: 90vh; + --sidebar-width: 300px; + --window-content-width: calc(var(--window-width) - var(--sidebar-width)); } @media (prefers-color-scheme: dark) { diff --git a/app/layout.tsx b/app/layout.tsx index 8afd3d66..708e9aac 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,5 @@ import "./globals.scss"; -import "./markdown.css"; +import "./markdown.scss"; export const metadata = { title: "ChatGPT Next Web", diff --git a/app/markdown.css b/app/markdown.scss similarity index 72% rename from app/markdown.css rename to app/markdown.scss index 534d5ecb..5fb45a30 100644 --- a/app/markdown.css +++ b/app/markdown.scss @@ -1,97 +1,93 @@ -@media (prefers-color-scheme: dark) { - .markdown-body { - color-scheme: dark; - --color-prettylights-syntax-comment: #8b949e; - --color-prettylights-syntax-constant: #79c0ff; - --color-prettylights-syntax-entity: #d2a8ff; - --color-prettylights-syntax-storage-modifier-import: #c9d1d9; - --color-prettylights-syntax-entity-tag: #7ee787; - --color-prettylights-syntax-keyword: #ff7b72; - --color-prettylights-syntax-string: #a5d6ff; - --color-prettylights-syntax-variable: #ffa657; - --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; - --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; - --color-prettylights-syntax-invalid-illegal-bg: #8e1519; - --color-prettylights-syntax-carriage-return-text: #f0f6fc; - --color-prettylights-syntax-carriage-return-bg: #b62324; - --color-prettylights-syntax-string-regexp: #7ee787; - --color-prettylights-syntax-markup-list: #f2cc60; - --color-prettylights-syntax-markup-heading: #1f6feb; - --color-prettylights-syntax-markup-italic: #c9d1d9; - --color-prettylights-syntax-markup-bold: #c9d1d9; - --color-prettylights-syntax-markup-deleted-text: #ffdcd7; - --color-prettylights-syntax-markup-deleted-bg: #67060c; - --color-prettylights-syntax-markup-inserted-text: #aff5b4; - --color-prettylights-syntax-markup-inserted-bg: #033a16; - --color-prettylights-syntax-markup-changed-text: #ffdfb6; - --color-prettylights-syntax-markup-changed-bg: #5a1e02; - --color-prettylights-syntax-markup-ignored-text: #c9d1d9; - --color-prettylights-syntax-markup-ignored-bg: #1158c7; - --color-prettylights-syntax-meta-diff-range: #d2a8ff; - --color-prettylights-syntax-brackethighlighter-angle: #8b949e; - --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; - --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; - --color-fg-default: #c9d1d9; - --color-fg-muted: #8b949e; - --color-fg-subtle: #6e7681; - --color-canvas-default: transparent; - --color-canvas-subtle: #161b22; - --color-border-default: #30363d; - --color-border-muted: #21262d; - --color-neutral-muted: rgba(110,118,129,0.4); - --color-accent-fg: #58a6ff; - --color-accent-emphasis: #1f6feb; - --color-attention-subtle: rgba(187,128,9,0.15); - --color-danger-fg: #f85149; - } +@mixin light { + color-scheme: light; + --color-prettylights-syntax-comment: #6e7781; + --color-prettylights-syntax-constant: #0550ae; + --color-prettylights-syntax-entity: #8250df; + --color-prettylights-syntax-storage-modifier-import: #24292f; + --color-prettylights-syntax-entity-tag: #116329; + --color-prettylights-syntax-keyword: #cf222e; + --color-prettylights-syntax-string: #0a3069; + --color-prettylights-syntax-variable: #953800; + --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; + --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; + --color-prettylights-syntax-invalid-illegal-bg: #82071e; + --color-prettylights-syntax-carriage-return-text: #f6f8fa; + --color-prettylights-syntax-carriage-return-bg: #cf222e; + --color-prettylights-syntax-string-regexp: #116329; + --color-prettylights-syntax-markup-list: #3b2300; + --color-prettylights-syntax-markup-heading: #0550ae; + --color-prettylights-syntax-markup-italic: #24292f; + --color-prettylights-syntax-markup-bold: #24292f; + --color-prettylights-syntax-markup-deleted-text: #82071e; + --color-prettylights-syntax-markup-deleted-bg: #ffebe9; + --color-prettylights-syntax-markup-inserted-text: #116329; + --color-prettylights-syntax-markup-inserted-bg: #dafbe1; + --color-prettylights-syntax-markup-changed-text: #953800; + --color-prettylights-syntax-markup-changed-bg: #ffd8b5; + --color-prettylights-syntax-markup-ignored-text: #eaeef2; + --color-prettylights-syntax-markup-ignored-bg: #0550ae; + --color-prettylights-syntax-meta-diff-range: #8250df; + --color-prettylights-syntax-brackethighlighter-angle: #57606a; + --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; + --color-prettylights-syntax-constant-other-reference-link: #0a3069; + --color-fg-default: #24292f; + --color-fg-muted: #57606a; + --color-fg-subtle: #6e7781; + --color-canvas-default: transparent; + --color-canvas-subtle: #f6f8fa; + --color-border-default: #d0d7de; + --color-border-muted: hsla(210, 18%, 87%, 1); + --color-neutral-muted: rgba(175, 184, 193, 0.2); + --color-accent-fg: #0969da; + --color-accent-emphasis: #0969da; + --color-attention-subtle: #fff8c5; + --color-danger-fg: #cf222e; } -@media (prefers-color-scheme: light) { - .markdown-body { - color-scheme: light; - --color-prettylights-syntax-comment: #6e7781; - --color-prettylights-syntax-constant: #0550ae; - --color-prettylights-syntax-entity: #8250df; - --color-prettylights-syntax-storage-modifier-import: #24292f; - --color-prettylights-syntax-entity-tag: #116329; - --color-prettylights-syntax-keyword: #cf222e; - --color-prettylights-syntax-string: #0a3069; - --color-prettylights-syntax-variable: #953800; - --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; - --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; - --color-prettylights-syntax-invalid-illegal-bg: #82071e; - --color-prettylights-syntax-carriage-return-text: #f6f8fa; - --color-prettylights-syntax-carriage-return-bg: #cf222e; - --color-prettylights-syntax-string-regexp: #116329; - --color-prettylights-syntax-markup-list: #3b2300; - --color-prettylights-syntax-markup-heading: #0550ae; - --color-prettylights-syntax-markup-italic: #24292f; - --color-prettylights-syntax-markup-bold: #24292f; - --color-prettylights-syntax-markup-deleted-text: #82071e; - --color-prettylights-syntax-markup-deleted-bg: #ffebe9; - --color-prettylights-syntax-markup-inserted-text: #116329; - --color-prettylights-syntax-markup-inserted-bg: #dafbe1; - --color-prettylights-syntax-markup-changed-text: #953800; - --color-prettylights-syntax-markup-changed-bg: #ffd8b5; - --color-prettylights-syntax-markup-ignored-text: #eaeef2; - --color-prettylights-syntax-markup-ignored-bg: #0550ae; - --color-prettylights-syntax-meta-diff-range: #8250df; - --color-prettylights-syntax-brackethighlighter-angle: #57606a; - --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; - --color-prettylights-syntax-constant-other-reference-link: #0a3069; - --color-fg-default: #24292f; - --color-fg-muted: #57606a; - --color-fg-subtle: #6e7781; - --color-canvas-default: transparent; - --color-canvas-subtle: #f6f8fa; - --color-border-default: #d0d7de; - --color-border-muted: hsla(210,18%,87%,1); - --color-neutral-muted: rgba(175,184,193,0.2); - --color-accent-fg: #0969da; - --color-accent-emphasis: #0969da; - --color-attention-subtle: #fff8c5; - --color-danger-fg: #cf222e; - } +@mixin dark { + color-scheme: dark; + --color-prettylights-syntax-comment: #8b949e; + --color-prettylights-syntax-constant: #79c0ff; + --color-prettylights-syntax-entity: #d2a8ff; + --color-prettylights-syntax-storage-modifier-import: #c9d1d9; + --color-prettylights-syntax-entity-tag: #7ee787; + --color-prettylights-syntax-keyword: #ff7b72; + --color-prettylights-syntax-string: #a5d6ff; + --color-prettylights-syntax-variable: #ffa657; + --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; + --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; + --color-prettylights-syntax-invalid-illegal-bg: #8e1519; + --color-prettylights-syntax-carriage-return-text: #f0f6fc; + --color-prettylights-syntax-carriage-return-bg: #b62324; + --color-prettylights-syntax-string-regexp: #7ee787; + --color-prettylights-syntax-markup-list: #f2cc60; + --color-prettylights-syntax-markup-heading: #1f6feb; + --color-prettylights-syntax-markup-italic: #c9d1d9; + --color-prettylights-syntax-markup-bold: #c9d1d9; + --color-prettylights-syntax-markup-deleted-text: #ffdcd7; + --color-prettylights-syntax-markup-deleted-bg: #67060c; + --color-prettylights-syntax-markup-inserted-text: #aff5b4; + --color-prettylights-syntax-markup-inserted-bg: #033a16; + --color-prettylights-syntax-markup-changed-text: #ffdfb6; + --color-prettylights-syntax-markup-changed-bg: #5a1e02; + --color-prettylights-syntax-markup-ignored-text: #c9d1d9; + --color-prettylights-syntax-markup-ignored-bg: #1158c7; + --color-prettylights-syntax-meta-diff-range: #d2a8ff; + --color-prettylights-syntax-brackethighlighter-angle: #8b949e; + --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; + --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; + --color-fg-default: #c9d1d9; + --color-fg-muted: #8b949e; + --color-fg-subtle: #6e7681; + --color-canvas-default: transparent; + --color-canvas-subtle: #161b22; + --color-border-default: #30363d; + --color-border-muted: #21262d; + --color-neutral-muted: rgba(110, 118, 129, 0.4); + --color-accent-fg: #58a6ff; + --color-accent-emphasis: #1f6feb; + --color-attention-subtle: rgba(187, 128, 9, 0.15); + --color-danger-fg: #f85149; } .markdown-body { @@ -100,12 +96,31 @@ margin: 0; color: var(--color-fg-default); background-color: var(--color-canvas-default); - font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", + Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; font-size: 14px; line-height: 1.5; word-wrap: break-word; } +.light { + @include light; +} + +.dark { + @include dark; +} + +:root { + @include light; +} + +@media (prefers-color-scheme: dark) { + :root { + @include dark; + } +} + .markdown-body .octicon { display: inline-block; fill: currentColor; @@ -120,7 +135,7 @@ .markdown-body h6:hover .anchor .octicon-link:before { width: 16px; height: 16px; - content: ' '; + content: " "; display: inline-block; background-color: currentColor; -webkit-mask-image: url("data:image/svg+xml,"); @@ -162,9 +177,9 @@ } .markdown-body h1 { - margin: .67em 0; + margin: 0.67em 0; font-weight: var(--base-text-weight-semibold, 600); - padding-bottom: .3em; + padding-bottom: 0.3em; font-size: 2em; border-bottom: 1px solid var(--color-border-muted); } @@ -218,7 +233,7 @@ overflow: hidden; background: transparent; border-bottom: 1px solid var(--color-border-muted); - height: .25em; + height: 0.25em; padding: 0; margin: 24px 0; background-color: var(--color-border-default); @@ -234,31 +249,31 @@ line-height: inherit; } -.markdown-body [type=button], -.markdown-body [type=reset], -.markdown-body [type=submit] { +.markdown-body [type="button"], +.markdown-body [type="reset"], +.markdown-body [type="submit"] { -webkit-appearance: button; } -.markdown-body [type=checkbox], -.markdown-body [type=radio] { +.markdown-body [type="checkbox"], +.markdown-body [type="radio"] { box-sizing: border-box; padding: 0; } -.markdown-body [type=number]::-webkit-inner-spin-button, -.markdown-body [type=number]::-webkit-outer-spin-button { +.markdown-body [type="number"]::-webkit-inner-spin-button, +.markdown-body [type="number"]::-webkit-outer-spin-button { height: auto; } -.markdown-body [type=search]::-webkit-search-cancel-button, -.markdown-body [type=search]::-webkit-search-decoration { +.markdown-body [type="search"]::-webkit-search-cancel-button, +.markdown-body [type="search"]::-webkit-search-decoration { -webkit-appearance: none; } .markdown-body ::-webkit-input-placeholder { color: inherit; - opacity: .54; + opacity: 0.54; } .markdown-body ::-webkit-file-upload-button { @@ -304,30 +319,30 @@ cursor: pointer; } -.markdown-body details:not([open])>*:not(summary) { +.markdown-body details:not([open]) > *:not(summary) { display: none !important; } .markdown-body a:focus, -.markdown-body [role=button]:focus, -.markdown-body input[type=radio]:focus, -.markdown-body input[type=checkbox]:focus { +.markdown-body [role="button"]:focus, +.markdown-body input[type="radio"]:focus, +.markdown-body input[type="checkbox"]:focus { outline: 2px solid var(--color-accent-fg); outline-offset: -2px; box-shadow: none; } .markdown-body a:focus:not(:focus-visible), -.markdown-body [role=button]:focus:not(:focus-visible), -.markdown-body input[type=radio]:focus:not(:focus-visible), -.markdown-body input[type=checkbox]:focus:not(:focus-visible) { +.markdown-body [role="button"]:focus:not(:focus-visible), +.markdown-body input[type="radio"]:focus:not(:focus-visible), +.markdown-body input[type="checkbox"]:focus:not(:focus-visible) { outline: solid 1px transparent; } .markdown-body a:focus-visible, -.markdown-body [role=button]:focus-visible, -.markdown-body input[type=radio]:focus-visible, -.markdown-body input[type=checkbox]:focus-visible { +.markdown-body [role="button"]:focus-visible, +.markdown-body input[type="radio"]:focus-visible, +.markdown-body input[type="checkbox"]:focus-visible { outline: 2px solid var(--color-accent-fg); outline-offset: -2px; box-shadow: none; @@ -335,17 +350,18 @@ .markdown-body a:not([class]):focus, .markdown-body a:not([class]):focus-visible, -.markdown-body input[type=radio]:focus, -.markdown-body input[type=radio]:focus-visible, -.markdown-body input[type=checkbox]:focus, -.markdown-body input[type=checkbox]:focus-visible { +.markdown-body input[type="radio"]:focus, +.markdown-body input[type="radio"]:focus-visible, +.markdown-body input[type="checkbox"]:focus, +.markdown-body input[type="checkbox"]:focus-visible { outline-offset: 0; } .markdown-body kbd { display: inline-block; padding: 3px 5px; - font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; + font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, + Liberation Mono, monospace; line-height: 10px; color: var(--color-fg-default); vertical-align: middle; @@ -370,7 +386,7 @@ .markdown-body h2 { font-weight: var(--base-text-weight-semibold, 600); - padding-bottom: .3em; + padding-bottom: 0.3em; font-size: 1.5em; border-bottom: 1px solid var(--color-border-muted); } @@ -387,12 +403,12 @@ .markdown-body h5 { font-weight: var(--base-text-weight-semibold, 600); - font-size: .875em; + font-size: 0.875em; } .markdown-body h6 { font-weight: var(--base-text-weight-semibold, 600); - font-size: .85em; + font-size: 0.85em; color: var(--color-fg-muted); } @@ -405,7 +421,7 @@ margin: 0; padding: 0 1em; color: var(--color-fg-muted); - border-left: .25em solid var(--color-border-default); + border-left: 0.25em solid var(--color-border-default); } .markdown-body ul, @@ -434,14 +450,16 @@ .markdown-body tt, .markdown-body code, .markdown-body samp { - font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; + font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, + Liberation Mono, monospace; font-size: 12px; } .markdown-body pre { margin-top: 0; margin-bottom: 0; - font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; + font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, + Liberation Mono, monospace; font-size: 12px; word-wrap: normal; } @@ -471,11 +489,11 @@ content: ""; } -.markdown-body>*:first-child { +.markdown-body > *:first-child { margin-top: 0 !important; } -.markdown-body>*:last-child { +.markdown-body > *:last-child { margin-bottom: 0 !important; } @@ -511,11 +529,11 @@ margin-bottom: 16px; } -.markdown-body blockquote>:first-child { +.markdown-body blockquote > :first-child { margin-top: 0; } -.markdown-body blockquote>:last-child { +.markdown-body blockquote > :last-child { margin-bottom: 0; } @@ -560,7 +578,7 @@ .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code { - padding: 0 .2em; + padding: 0 0.2em; font-size: inherit; } @@ -594,19 +612,19 @@ list-style-type: none; } -.markdown-body ol[type=a] { +.markdown-body ol[type="a"] { list-style-type: lower-alpha; } -.markdown-body ol[type=A] { +.markdown-body ol[type="A"] { list-style-type: upper-alpha; } -.markdown-body ol[type=i] { +.markdown-body ol[type="i"] { list-style-type: lower-roman; } -.markdown-body ol[type=I] { +.markdown-body ol[type="I"] { list-style-type: upper-roman; } @@ -614,7 +632,7 @@ list-style-type: decimal; } -.markdown-body div>ol:not([type]) { +.markdown-body div > ol:not([type]) { list-style-type: decimal; } @@ -626,12 +644,12 @@ margin-bottom: 0; } -.markdown-body li>p { +.markdown-body li > p { margin-top: 16px; } -.markdown-body li+li { - margin-top: .25em; +.markdown-body li + li { + margin-top: 0.25em; } .markdown-body dl { @@ -674,11 +692,11 @@ background-color: transparent; } -.markdown-body img[align=right] { +.markdown-body img[align="right"] { padding-left: 20px; } -.markdown-body img[align=left] { +.markdown-body img[align="left"] { padding-right: 20px; } @@ -693,7 +711,7 @@ overflow: hidden; } -.markdown-body span.frame>span { +.markdown-body span.frame > span { display: block; float: left; width: auto; @@ -721,7 +739,7 @@ clear: both; } -.markdown-body span.align-center>span { +.markdown-body span.align-center > span { display: block; margin: 13px auto 0; overflow: hidden; @@ -739,7 +757,7 @@ clear: both; } -.markdown-body span.align-right>span { +.markdown-body span.align-right > span { display: block; margin: 13px 0 0; overflow: hidden; @@ -769,7 +787,7 @@ overflow: hidden; } -.markdown-body span.float-right>span { +.markdown-body span.float-right > span { display: block; margin: 13px auto 0; overflow: hidden; @@ -778,7 +796,7 @@ .markdown-body code, .markdown-body tt { - padding: .2em .4em; + padding: 0.2em 0.4em; margin: 0; font-size: 85%; white-space: break-spaces; @@ -803,7 +821,7 @@ font-size: 100%; } -.markdown-body pre>code { +.markdown-body pre > code { padding: 0; margin: 0; word-break: normal; @@ -1042,7 +1060,7 @@ .markdown-body g-emoji { display: inline-block; min-width: 1ch; - font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; + font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 1em; font-style: normal !important; font-weight: var(--base-text-weight-normal, 400); @@ -1067,7 +1085,7 @@ cursor: pointer; } -.markdown-body .task-list-item+.task-list-item { +.markdown-body .task-list-item + .task-list-item { margin-top: 4px; } @@ -1076,12 +1094,12 @@ } .markdown-body .task-list-item-checkbox { - margin: 0 .2em .25em -1.4em; + margin: 0 0.2em 0.25em -1.4em; vertical-align: middle; } .markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { - margin: 0 -1.6em .25em .2em; + margin: 0 -1.6em 0.25em 0.2em; } .markdown-body .contains-task-list { @@ -1089,7 +1107,9 @@ } .markdown-body .contains-task-list:hover .task-list-item-convert-container, -.markdown-body .contains-task-list:focus-within .task-list-item-convert-container { +.markdown-body + .contains-task-list:focus-within + .task-list-item-convert-container { display: block; width: auto; height: 24px; @@ -1100,4 +1120,3 @@ .markdown-body ::-webkit-calendar-picker-indicator { filter: invert(50%); } - diff --git a/app/requests.ts b/app/requests.ts index e96c97e7..4e1ca057 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -51,35 +51,48 @@ export async function requestChatStream( filterBot: options?.filterBot, }); - const res = await fetch("/api/chat-stream", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(req), - }); + const controller = new AbortController(); + setTimeout(() => controller.abort(), 10000); - let responseText = ""; + try { + const res = await fetch("/api/chat-stream", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(req), + signal: controller.signal, + }); - if (res.ok) { - const reader = res.body?.getReader(); - const decoder = new TextDecoder(); + let responseText = ""; - while (true) { - const content = await reader?.read(); - const text = decoder.decode(content?.value); - responseText += text; + const finish = () => options?.onMessage(responseText, true); - const done = !content || content.done; - options?.onMessage(responseText, false); + if (res.ok) { + const reader = res.body?.getReader(); + const decoder = new TextDecoder(); - if (done) { - break; + while (true) { + // handle time out, will stop if no response in 10 secs + const timeoutId = setTimeout(() => finish(), 10000); + const content = await reader?.read(); + clearTimeout(timeoutId); + const text = decoder.decode(content?.value); + responseText += text; + + const done = !content || content.done; + options?.onMessage(responseText, false); + + if (done) { + break; + } } - } - options?.onMessage(responseText, true); - } else { + finish(); + } else { + options?.onError(new Error("Stream Error")); + } + } catch (err) { options?.onError(new Error("NetWork Error")); } } @@ -87,7 +100,7 @@ export async function requestChatStream( export async function requestWithPrompt(messages: Message[], prompt: string) { messages = messages.concat([ { - role: "system", + role: "user", content: prompt, date: new Date().toLocaleString(), }, diff --git a/app/store.ts b/app/store.ts index a9e50e42..baf35df8 100644 --- a/app/store.ts +++ b/app/store.ts @@ -108,6 +108,8 @@ interface ChatStore { updateConfig: (updater: (config: ChatConfig) => void) => void; } +const LOCAL_KEY = "chat-next-web-store"; + export const useChatStore = create()( persist( (set, get) => ({ @@ -254,16 +256,16 @@ export const useChatStore = create()( summarizeSession() { const session = get().currentSession(); - if (session.topic !== DEFAULT_TOPIC) return; - - requestWithPrompt( - session.messages, - "简明扼要地 10 字以内总结主题" - ).then((res) => { - get().updateCurrentSession( - (session) => (session.topic = trimTopic(res)) + if (session.topic === DEFAULT_TOPIC) { + // should summarize topic + requestWithPrompt(session.messages, "返回这句话的简要主题").then( + (res) => { + get().updateCurrentSession( + (session) => (session.topic = trimTopic(res)) + ); + } ); - }); + } }, updateStat(message) { @@ -280,6 +282,8 @@ export const useChatStore = create()( set(() => ({ sessions })); }, }), - { name: "chat-next-web-store" } + { + name: LOCAL_KEY, + } ) ); diff --git a/package.json b/package.json index 5c281e9e..c4a33e07 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "private": true, "scripts": { "dev": "next dev", + "local:dev": "./dev/proxychains.exe -f ./scripts/proxychains.conf yarn dev", + "local:start": "./dev/proxychains.exe -f ./scripts/proxychains.conf yarn start", "build": "next build", "start": "next start", "lint": "next lint" diff --git a/scripts/proxychains.conf b/scripts/proxychains.conf new file mode 100644 index 00000000..9535ac99 --- /dev/null +++ b/scripts/proxychains.conf @@ -0,0 +1,228 @@ +# proxychains.conf FOR PROXYCHAINS.EXE ALPHA +# +# SOCKS5 tunneling proxifier with Fake DNS. +# + +# The option below identifies how the ProxyList is treated. +# only one option should be uncommented at time, +# otherwise the last appearing option will be accepted +# +# +# DYNAMIC_CHAIN IS NOT SUPPORTED AT PRESENT +#dynamic_chain +# DYNAMIC_CHAIN IS NOT SUPPORTED AT PRESENT +# +# Dynamic - Each connection will be done via chained proxies +# all proxies chained in the order as they appear in the list +# at least one proxy must be online to play in chain +# (dead proxies are skipped) +# otherwise EINTR is returned to the app +# +# +strict_chain +# +# Strict - Each connection will be done via chained proxies +# all proxies chained in the order as they appear in the list +# all proxies must be online to play in chain +# otherwise EINTR is returned to the app +# +# RANDOM_CHAIN IS NOT SUPPORTED AT PRESENT +#random_chain +# RANDOM_CHAIN IS NOT SUPPORTED AT PRESENT +# +# Random - Each connection will be done via random proxy +# (or proxy chain, see chain_len) from the list. +# this option is good to test your IDS :) + +# Make sense only if random_chain +#chain_len = 2 + +# Quiet mode (no output from library) +#quiet_mode + +# Proxy DNS requests using Fake IP - no leak for DNS data +proxy_dns + +# Proxy DNS requests using UDP associate feature provided by SOCKS5 proxy +# NOT SUPPORTED AT PRESENT +#proxy_dns_udp_associate +# NOT SUPPORTED AT PRESENT + +# set the class A subnet number to usefor use of the internal remote DNS mapping +# we use the reserved 224.x.x.x range by default, +# if the proxified app does a DNS request, we will return an IP from that range. +# on further accesses to this ip we will send the saved DNS name to the proxy. +# in case some control-freak app checks the returned ip, and denies to +# connect, you can use another subnet, e.g. 10.x.x.x or 127.x.x.x. +# of course you should make sure that the proxified app does not need +# *real* access to this subnet. +# i.e. dont use the same subnet then in the localnet section +#remote_dns_subnet 127 +#remote_dns_subnet 10 +remote_dns_subnet 224 + +# This enables you to set a CIDR block for the internal remote DNS mapping +# for example, remote_dns_subnet_cidr_v4 224.0.0.0/8 is equivalent to +# remote_dns_subnet 224. +# subnet mask format like 255.255.0.0 is not allowed here +# By default 224.0.0.0/8 and 250d::/16 +#remote_dns_subnet_cidr_v4 224.0.0.0/8 +#remote_dns_subnet_cidr_v6 250d::/16 + +# Some timeouts in milliseconds +# Defaults: tcp_read_time_out 5000, tcp_connect_time_out 3000 +#tcp_read_time_out 15000 +#tcp_connect_time_out 8000 + + +# ==== Rules ==== +# You can control which IP range not to be proxied. +# First matched rule decides the target (PROXIED, DIRECT or BLOCK) of a +# connection. + +# localnet always has a "DIRECT" target, which means they will not be +# proxied. +# By default enable localnet for loopback address ranges +# RFC5735 Loopback address range +localnet 127.0.0.0/255.0.0.0 +# RFC1918 Private Address Ranges +# localnet 10.0.0.0/255.0.0.0 +# localnet 172.16.0.0/255.240.0.0 +# localnet 192.168.0.0/255.255.0.0 + + +# Example for localnet exclusion +## Exclude connections to 192.168.1.0/24 with port 80 +# localnet 192.168.1.0:80/255.255.255.0 + +## Exclude connections to 192.168.100.0/24 +# localnet 192.168.100.0/255.255.255.0 + +## Exclude connections to ANYwhere with port 80 +# localnet 0.0.0.0:80/0.0.0.0 + +# === Additional routing rules === +# These rules enables further control on websites/addresses which +# should be proxied or not. +# All rules can have an extra optional restriction of target port. +# However, if the proxied application uses gethostbyname() to do DNS +# query instead of getaddrinfo() series, this port part of the rule +# is invalidated. +# Three target is allowed: PROXY, DIRECT and BLOCK. +# +# - DOMAIN-KEYWORD rule, matching requests where the FQDN contains +# a specific string. +# e.g. DOMAIN-KEYWORD,google:80,DIRECT means any request to FQDN +# containing "google" with the target port 80 will NOT be proxied. +# +# - DOMAIN-SUFFIX rule, matching requests where the FQDN is suffixed +# with a specific string. +# e.g. DOMAIN-SUFFIX,.ru,PROXY means any FQDN that ends with ".ru" +# will be proxied. +# +# - DOMAIN-FULL rule, matching requests where the FQDN is exactly +# identical to a specific string. +# e.g. DOMAIN-FULL,duckduckgo.com,DIRECT means every request to +# "duckduckgo.com" (must entirely match) will NOT be proxied. +# +# - DOMAIN rule, the alias of DOMAIN-FULL rule. +# +# - IP-CIDR rule, matching requests to IP address in a CIDR block. +# Note if an FQDN is previously matched by a DOMAIN* rule, this rule +# is not applied to the resolved IPs. (Because fake IPs are used in +# this case) +# e.g. IP-CIDR,8.8.8.8/32,DIRECT +# IP-CIDR,250e::/16,PROXY +# IP-CIDR,[250c::]:443/16,PROXY +# IP-CIDR,10.0.0.0:80/8,DIRECT +# Note that "IP-CIDR,127.0.0.0/8,DIRECT" is equivalent to +# "localnet 127.0.0.0/255.0.0.0". +IP-CIDR,10.0.0.0/8,DIRECT +IP-CIDR,172.16.0.0/12,DIRECT +IP-CIDR,192.168.0.0/255.255.0.0,DIRECT +IP-CIDR,fe80::/8,DIRECT +# - PORT rule, matching requests to a target port. +# e.g. PORT,25,BLOCK +# +# - FINAL "rule", deciding the destiny of a request immediately. +# When this "rule" is used, it is not treated as a "match". +# If you want an unconditional match, try other rules instead, like +# IP-CIDR,0.0.0.0/0,PROXY or DOMAIN-KEYWORD,,PROXY. +# +# When no rules and no FINAL "rule" matched, a connection will be +# PROXIED by default, unless you specify option default_target. +# e.g. FINAL,PROXY + +# Will fake IP entries created by a descendant process be removed if this +# process exited? 1 by default. +delete_fake_ip_after_child_exits 1 + +# When no rules and no FINAL "rule" matched, a connection's default +# target. PROXY by default. +default_target PROXY + +# Will the rules apply to the resolved IP if corresponding hostname +# did not match any rules? (FINAL is not counted as a rule) +# IF SO, SET THIS OPTION'S VALUE TO 0. 1 by default. +use_fake_ip_when_hostname_not_matched 1 + +# ===== Keep them as-is ===== + +map_resolved_ip_to_host 0 +search_for_host_by_resolved_ip 0 +# or force_resolve_by_hosts_file 1 +resolve_locally_if_match_hosts 1 + +# ===== Keep them as-is - end ===== + +# Generate fake ips by FQDN hash - 1 (better to get rid of SSH safe +# warnings) +# Generate fake ips sequentially - 0 +# Default: 1 +gen_fake_ip_using_hashed_hostname 1 + +# If your *first* proxy supports connecting to it by an IPv4 address +# (resolved by a hostname or specified manually), set its value to 1. +# This enables proxying IPv4 address. +# If disabled, fake IPv4 address is not returned. +# 1 by default +first_tunnel_uses_ipv4 1 + +# If your *first* proxy supports connecting to it by an IPv6 address +# (resolved by a hostname or specified manually), set its value to 1. +# This enables proxying IPv6 address. +# If disabled, fake IPv6 address is not returned. +# 0 by default +first_tunnel_uses_ipv6 0 + +# Custom hosts file path +#custom_hosts_file_path C:\Some Path\hosts +#custom_hosts_file_path /etc/alternative/hosts + +# Custom log level. +# 600 - VERBOSE +# 500 - DEBUG +# 400 - INFO +# 300 - WARNING +# 200 - ERROR +# 100 - CRITICAL +# "log_level 200" is equivalent to "quiet_mode" +log_level 400 + +# ProxyList format +# type host port [user pass] +# (values separated by 'tab' or 'blank') +# +# +# Examples: +# +# socks5 localhost 1080 +# socks5 localhost 1080 user password +# socks5 192.168.67.78 1080 lamer secret +# +# +# proxy types: socks5 +# ( auth types supported: "user/pass"-socks5 ) +# +[ProxyList] +socks5 localhost 7890 \ No newline at end of file