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