Browse Source

feat: update style and timeout handler

Yidadaa 2 years ago
parent
commit
5c70456e18

+ 14 - 53
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;
-}

+ 25 - 150
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: () => (
+      <div className="">
+        <LoadingIcon />
+      </div>
+    ),
+  }
+);
 
 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 (
+      <div>
+        <Avatar role="assistant"></Avatar>
+        <LoadingIcon />
+      </div>
+    );
+  }
+
   return (
     <div
       className={`${
@@ -344,154 +365,8 @@ export function Home() {
       </div>
 
       <div className={styles["window-content"]}>
-        {openSettings ? <Settings /> : <Chat key="chat" />}
+        {openSettings ? <LazySettings /> : <Chat key="chat" />}
       </div>
     </div>
   );
 }
-
-export function Settings() {
-  const [showEmojiPicker, setShowEmojiPicker] = useState(false);
-  const [config, updateConfig, resetConfig] = useChatStore((state) => [
-    state.config,
-    state.updateConfig,
-    state.resetConfig,
-  ]);
-
-  return (
-    <>
-      <div className={styles["window-header"]}>
-        <div>
-          <div className={styles["window-header-title"]}>设置</div>
-          <div className={styles["window-header-sub-title"]}>设置选项</div>
-        </div>
-        <div className={styles["window-actions"]}>
-          <div className={styles["window-action-button"]}>
-            <IconButton
-              icon={<ResetIcon />}
-              onClick={resetConfig}
-              bordered
-              title="重置所有选项"
-            />
-          </div>
-        </div>
-      </div>
-      <div className={styles["settings"]}>
-        <List>
-          <ListItem>
-            <div className={styles["settings-title"]}>头像</div>
-            <Popover
-              onClose={() => setShowEmojiPicker(false)}
-              content={
-                <EmojiPicker
-                  lazyLoadEmojis
-                  theme={EmojiTheme.AUTO}
-                  onEmojiClick={(e) => {
-                    updateConfig((config) => (config.avatar = e.unified));
-                    setShowEmojiPicker(false);
-                  }}
-                />
-              }
-              open={showEmojiPicker}
-            >
-              <div
-                className={styles.avatar}
-                onClick={() => setShowEmojiPicker(true)}
-              >
-                <Avatar role="user" />
-              </div>
-            </Popover>
-          </ListItem>
-
-          <ListItem>
-            <div className={styles["settings-title"]}>发送键</div>
-            <div className="">
-              <select
-                value={config.submitKey}
-                onChange={(e) => {
-                  updateConfig(
-                    (config) =>
-                      (config.submitKey = e.target.value as any as SubmitKey)
-                  );
-                }}
-              >
-                {Object.values(SubmitKey).map((v) => (
-                  <option value={v} key={v}>
-                    {v}
-                  </option>
-                ))}
-              </select>
-            </div>
-          </ListItem>
-
-          <ListItem>
-            <div className={styles["settings-title"]}>主题</div>
-            <div className="">
-              <select
-                value={config.theme}
-                onChange={(e) => {
-                  updateConfig(
-                    (config) => (config.theme = e.target.value as any as Theme)
-                  );
-                }}
-              >
-                {Object.values(Theme).map((v) => (
-                  <option value={v} key={v}>
-                    {v}
-                  </option>
-                ))}
-              </select>
-            </div>
-          </ListItem>
-
-          <ListItem>
-            <div className={styles["settings-title"]}>紧凑边框</div>
-            <input
-              type="checkbox"
-              checked={config.tightBorder}
-              onChange={(e) =>
-                updateConfig(
-                  (config) => (config.tightBorder = e.currentTarget.checked)
-                )
-              }
-            ></input>
-          </ListItem>
-        </List>
-        <List>
-          <ListItem>
-            <div className={styles["settings-title"]}>最大上下文消息数</div>
-            <input
-              type="range"
-              title={config.historyMessageCount.toString()}
-              value={config.historyMessageCount}
-              min="5"
-              max="20"
-              step="5"
-              onChange={(e) =>
-                updateConfig(
-                  (config) =>
-                    (config.historyMessageCount = e.target.valueAsNumber)
-                )
-              }
-            ></input>
-          </ListItem>
-
-          <ListItem>
-            <div className={styles["settings-title"]}>
-              上下文中包含机器人消息
-            </div>
-            <input
-              type="checkbox"
-              checked={config.sendBotMessages}
-              onChange={(e) =>
-                updateConfig(
-                  (config) => (config.sendBotMessages = e.currentTarget.checked)
-                )
-              }
-            ></input>
-          </ListItem>
-        </List>
-      </div>
-    </>
-  );
-}

+ 14 - 0
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;
+}

+ 160 - 0
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 (
+    <>
+      <div className={styles["window-header"]}>
+        <div>
+          <div className={styles["window-header-title"]}>设置</div>
+          <div className={styles["window-header-sub-title"]}>设置选项</div>
+        </div>
+        <div className={styles["window-actions"]}>
+          <div className={styles["window-action-button"]}>
+            <IconButton
+              icon={<ResetIcon />}
+              onClick={resetConfig}
+              bordered
+              title="重置所有选项"
+            />
+          </div>
+        </div>
+      </div>
+      <div className={styles["settings"]}>
+        <List>
+          <ListItem>
+            <div className={styles["settings-title"]}>头像</div>
+            <Popover
+              onClose={() => setShowEmojiPicker(false)}
+              content={
+                <EmojiPicker
+                  lazyLoadEmojis
+                  theme={EmojiTheme.AUTO}
+                  onEmojiClick={(e) => {
+                    updateConfig((config) => (config.avatar = e.unified));
+                    setShowEmojiPicker(false);
+                  }}
+                />
+              }
+              open={showEmojiPicker}
+            >
+              <div
+                className={styles.avatar}
+                onClick={() => setShowEmojiPicker(true)}
+              >
+                <Avatar role="user" />
+              </div>
+            </Popover>
+          </ListItem>
+
+          <ListItem>
+            <div className={styles["settings-title"]}>发送键</div>
+            <div className="">
+              <select
+                value={config.submitKey}
+                onChange={(e) => {
+                  updateConfig(
+                    (config) =>
+                      (config.submitKey = e.target.value as any as SubmitKey)
+                  );
+                }}
+              >
+                {Object.values(SubmitKey).map((v) => (
+                  <option value={v} key={v}>
+                    {v}
+                  </option>
+                ))}
+              </select>
+            </div>
+          </ListItem>
+
+          <ListItem>
+            <div className={styles["settings-title"]}>主题</div>
+            <div className="">
+              <select
+                value={config.theme}
+                onChange={(e) => {
+                  updateConfig(
+                    (config) => (config.theme = e.target.value as any as Theme)
+                  );
+                }}
+              >
+                {Object.values(Theme).map((v) => (
+                  <option value={v} key={v}>
+                    {v}
+                  </option>
+                ))}
+              </select>
+            </div>
+          </ListItem>
+
+          <ListItem>
+            <div className={styles["settings-title"]}>紧凑边框</div>
+            <input
+              type="checkbox"
+              checked={config.tightBorder}
+              onChange={(e) =>
+                updateConfig(
+                  (config) => (config.tightBorder = e.currentTarget.checked)
+                )
+              }
+            ></input>
+          </ListItem>
+        </List>
+        <List>
+          <ListItem>
+            <div className={styles["settings-title"]}>最大上下文消息数</div>
+            <input
+              type="range"
+              title={config.historyMessageCount.toString()}
+              value={config.historyMessageCount}
+              min="5"
+              max="20"
+              step="5"
+              onChange={(e) =>
+                updateConfig(
+                  (config) =>
+                    (config.historyMessageCount = e.target.valueAsNumber)
+                )
+              }
+            ></input>
+          </ListItem>
+
+          <ListItem>
+            <div className={styles["settings-title"]}>
+              上下文中包含机器人消息
+            </div>
+            <input
+              type="checkbox"
+              checked={config.sendBotMessages}
+              onChange={(e) =>
+                updateConfig(
+                  (config) => (config.sendBotMessages = e.currentTarget.checked)
+                )
+              }
+            ></input>
+          </ListItem>
+        </List>
+      </div>
+    </>
+  );
+}

+ 31 - 0
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;
+}

+ 5 - 0
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) {

+ 1 - 1
app/layout.tsx

@@ -1,5 +1,5 @@
 import "./globals.scss";
-import "./markdown.css";
+import "./markdown.scss";
 
 export const metadata = {
   title: "ChatGPT Next Web",

+ 176 - 157
app/markdown.css → 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;
-  }
-}
-
-@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 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;
+}
+
+@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,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
@@ -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%);
 }
-

+ 41 - 28
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),
-  });
-
-  let responseText = "";
-
-  if (res.ok) {
-    const reader = res.body?.getReader();
-    const decoder = new TextDecoder();
-
-    while (true) {
-      const content = await reader?.read();
-      const text = decoder.decode(content?.value);
-      responseText += text;
-
-      const done = !content || content.done;
-      options?.onMessage(responseText, false);
-
-      if (done) {
-        break;
+  const controller = new AbortController();
+  setTimeout(() => controller.abort(), 10000);
+
+  try {
+    const res = await fetch("/api/chat-stream", {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify(req),
+      signal: controller.signal,
+    });
+
+    let responseText = "";
+
+    const finish = () => options?.onMessage(responseText, true);
+
+    if (res.ok) {
+      const reader = res.body?.getReader();
+      const decoder = new TextDecoder();
+
+      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(),
     },

+ 14 - 10
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<ChatStore>()(
   persist(
     (set, get) => ({
@@ -254,16 +256,16 @@ export const useChatStore = create<ChatStore>()(
       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<ChatStore>()(
         set(() => ({ sessions }));
       },
     }),
-    { name: "chat-next-web-store" }
+    {
+      name: LOCAL_KEY,
+    }
   )
 );

+ 2 - 0
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"

+ 228 - 0
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