feat: update style and timeout handler

This commit is contained in:
Yidadaa 2023-03-14 00:25:07 +08:00
parent ff0cf2f9dc
commit 5c70456e18
12 changed files with 704 additions and 392 deletions

View File

@ -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;
}

View File

@ -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>
</>
);
}

View File

@ -0,0 +1,14 @@
@import "./window.scss";
.settings {
padding: 20px;
}
.settings-title {
font-size: 14px;
font-weight: bolder;
}
.avatar {
cursor: pointer;
}

160
app/components/settings.tsx Normal file
View File

@ -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>
</>
);
}

View File

@ -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;
}

View File

@ -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) {

View File

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

View File

@ -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,<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%);
}

View File

@ -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(),
},

View File

@ -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,
}
)
);

View File

@ -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
scripts/proxychains.conf Normal file
View File

@ -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