feat: add dark theme support

This commit is contained in:
Yidadaa
2023-03-13 03:06:21 +08:00
parent a9940cb05e
commit 14d50f1167
14 changed files with 395 additions and 92 deletions

View File

@@ -8,7 +8,7 @@
box-shadow: var(--card-shadow);
cursor: pointer;
transition: all .3s ease;
transition: all 0.3s ease;
overflow: hidden;
user-select: none;
}
@@ -29,7 +29,17 @@
align-items: center;
}
@media (prefers-color-scheme: dark) {
div:not(:global(.no-dark)) > .icon-button-icon {
filter: invert(0.5);
}
.icon-button:hover {
filter: brightness(1.2) hue-rotate(0.01turn);
}
}
.icon-button-text {
margin-left: 5px;
font-size: 12px;
}
}

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import styles from "./button.module.css";
import styles from "./button.module.scss";
export function IconButton(props: {
onClick?: () => void;
@@ -8,6 +8,7 @@ export function IconButton(props: {
text?: string;
bordered?: boolean;
className?: string;
title?: string;
}) {
return (
<div
@@ -16,6 +17,7 @@ export function IconButton(props: {
` ${props.bordered && styles.border} ${props.className ?? ""}`
}
onClick={props.onClick}
title={props.title}
>
<div className={styles["icon-button-icon"]}>{props.icon}</div>
{props.text && (

View File

@@ -8,6 +8,8 @@
border: var(--border-in-light);
border-radius: 20px;
box-shadow: var(--shadow);
color: var(--black);
background-color: var(--white);
display: flex;
overflow: hidden;
@@ -177,11 +179,11 @@
margin-top: 5px;
}
.chat-actions {
.window-actions {
display: inline-flex;
}
.chat-action-button {
.window-action-button {
margin-left: 10px;
}
@@ -237,13 +239,14 @@
}
.chat-message-item {
margin-top: 5px;
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;
border: var(--border-in-light);
}
.chat-message-user > .chat-message-container > .chat-message-item {
@@ -289,11 +292,12 @@
border-radius: 10px;
border: var(--border-in-light);
box-shadow: var(--card-shadow);
background-color: var(--white);
color: var(--black);
font-family: inherit;
padding: 10px 14px;
resize: none;
outline: none;
color: #333;
}
.chat-input:focus {

View File

@@ -6,10 +6,10 @@ import "katex/dist/katex.min.css";
import RemarkMath from "remark-math";
import RehypeKatex from "rehype-katex";
import EmojiPicker, { Emoji, EmojiClickData } from "emoji-picker-react";
import EmojiPicker, { Emoji, Theme as EmojiTheme } from "emoji-picker-react";
import { IconButton } from "./button";
import styles from "./home.module.css";
import styles from "./home.module.scss";
import SettingsIcon from "../icons/settings.svg";
import GithubIcon from "../icons/github.svg";
@@ -21,8 +21,9 @@ 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 } from "../store";
import { Message, SubmitKey, useChatStore, Theme } from "../store";
import { Card, List, ListItem, Popover } from "./ui-lib";
export function Markdown(props: { content: string }) {
@@ -101,12 +102,34 @@ export function ChatList() {
);
}
function useSubmitHandler() {
const config = useChatStore((state) => state.config);
const submitKey = config.submitKey;
const shouldSubmit = (e: KeyboardEvent) => {
if (e.key !== "Enter") return false;
return (
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
(config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) ||
config.submitKey === SubmitKey.Enter
);
};
return {
submitKey,
shouldSubmit,
};
}
export function Chat() {
type RenderMessage = Message & { preview?: boolean };
const session = useChatStore((state) => state.currentSession());
const [userInput, setUserInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler();
const onUserInput = useChatStore((state) => state.onUserInput);
const onUserSubmit = () => {
@@ -116,7 +139,7 @@ export function Chat() {
setUserInput("");
};
const onInputKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter" && (e.shiftKey || e.ctrlKey || e.metaKey)) {
if (shouldSubmit(e)) {
onUserSubmit();
e.preventDefault();
}
@@ -165,12 +188,20 @@ export function Chat() {
ChatGPT {session.messages.length}
</div>
</div>
<div className={styles["chat-actions"]}>
<div className={styles["chat-action-button"]}>
<IconButton icon={<BrainIcon />} bordered />
<div className={styles["window-actions"]}>
<div className={styles["window-action-button"]}>
<IconButton
icon={<BrainIcon />}
bordered
title="查看压缩后的历史 Prompt开发中"
/>
</div>
<div className={styles["chat-action-button"]}>
<IconButton icon={<ExportIcon />} bordered />
<div className={styles["window-action-button"]}>
<IconButton
icon={<ExportIcon />}
bordered
title="导出聊天记录为 Markdown开发中"
/>
</div>
</div>
</div>
@@ -223,7 +254,7 @@ export function Chat() {
<div className={styles["chat-input-panel-inner"]}>
<textarea
className={styles["chat-input"]}
placeholder="输入消息Ctrl + Enter 发送"
placeholder={`输入消息,${submitKey} 发送`}
rows={3}
onInput={(e) => setUserInput(e.currentTarget.value)}
value={userInput}
@@ -232,7 +263,7 @@ export function Chat() {
<IconButton
icon={<SendWhiteIcon />}
text={"发送"}
className={styles["chat-input-send"]}
className={styles["chat-input-send"] + " no-dark"}
onClick={onUserSubmit}
/>
</div>
@@ -241,12 +272,28 @@ export function Chat() {
);
}
function useSwitchTheme() {
const config = useChatStore((state) => state.config);
useEffect(() => {
document.body.classList.remove("light");
document.body.classList.remove("dark");
if (config.theme === "dark") {
document.body.classList.add("dark");
} else if (config.theme === "light") {
document.body.classList.add("light");
}
}, [config.theme]);
}
export function Home() {
const [createNewSession] = useChatStore((state) => [state.newSession]);
// settings
const [openSettings, setOpenSettings] = useState(false);
useSwitchTheme();
return (
<div className={styles.container}>
<div className={styles.sidebar}>
@@ -260,7 +307,10 @@ export function Home() {
</div>
</div>
<div className={styles["sidebar-body"]}>
<div
className={styles["sidebar-body"]}
onClick={() => setOpenSettings(false)}
>
<ChatList />
</div>
@@ -295,13 +345,6 @@ export function Home() {
);
}
export function EmojiPickerModal(props: {
show: boolean;
onClose: (_: boolean) => void;
}) {
return <div className=""></div>;
}
export function Settings() {
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const [config, updateConfig] = useChatStore((state) => [
@@ -316,6 +359,11 @@ export function Settings() {
<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 />} bordered title="重置所有选项" />
</div>
</div>
</div>
<div className={styles["settings"]}>
<List>
@@ -326,6 +374,7 @@ export function Settings() {
content={
<EmojiPicker
lazyLoadEmojis
theme={EmojiTheme.AUTO}
onEmojiClick={(e) => {
updateConfig((config) => (config.avatar = e.unified));
setShowEmojiPicker(false);
@@ -355,8 +404,28 @@ export function Settings() {
);
}}
>
{Object.entries(SubmitKey).map(([k, v]) => (
<option value={k} key={v}>
{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>
))}
@@ -366,13 +435,36 @@ export function Settings() {
</List>
<List>
<ListItem>
<div className={styles["settings-title"]}></div>
<div className="">{config.historyMessageCount}</div>
<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>
<div className="">{config.sendBotMessages ? "是" : "否"}</div>
<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

@@ -42,6 +42,7 @@
min-height: 40px;
border-bottom: var(--border-in-light);
padding: 10px 20px;
animation: slide-in ease 0.6s;
}
.list {
@@ -49,6 +50,7 @@
border-radius: 10px;
box-shadow: var(--card-shadow);
margin-bottom: 20px;
animation: slide-in ease 0.3s;
}
.list .list-item:last-child {

View File

@@ -1,4 +1,4 @@
import styles from "./ui-lib.module.css";
import styles from "./ui-lib.module.scss";
export function Popover(props: {
children: JSX.Element;